Repository: CodeAcmen/ios-async-socket-explorer Branch: master Commit: c385c9c091c5 Files: 1153 Total size: 4.8 MB Directory structure: gitextract_kmpt9gt3/ ├── LICENSE ├── Podfile ├── Pods/ │ ├── CocoaAsyncSocket/ │ │ ├── LICENSE.txt │ │ ├── README.markdown │ │ └── Source/ │ │ └── GCD/ │ │ ├── GCDAsyncSocket.h │ │ ├── GCDAsyncSocket.m │ │ ├── GCDAsyncUdpSocket.h │ │ └── GCDAsyncUdpSocket.m │ ├── DZNEmptyDataSet/ │ │ ├── LICENSE │ │ ├── README.md │ │ └── Source/ │ │ ├── UIScrollView+EmptyDataSet.h │ │ └── UIScrollView+EmptyDataSet.m │ ├── Masonry/ │ │ ├── LICENSE │ │ ├── Masonry/ │ │ │ ├── MASCompositeConstraint.h │ │ │ ├── MASCompositeConstraint.m │ │ │ ├── MASConstraint+Private.h │ │ │ ├── MASConstraint.h │ │ │ ├── MASConstraint.m │ │ │ ├── MASConstraintMaker.h │ │ │ ├── MASConstraintMaker.m │ │ │ ├── MASLayoutConstraint.h │ │ │ ├── MASLayoutConstraint.m │ │ │ ├── MASUtilities.h │ │ │ ├── MASViewAttribute.h │ │ │ ├── MASViewAttribute.m │ │ │ ├── MASViewConstraint.h │ │ │ ├── MASViewConstraint.m │ │ │ ├── Masonry.h │ │ │ ├── NSArray+MASAdditions.h │ │ │ ├── NSArray+MASAdditions.m │ │ │ ├── NSArray+MASShorthandAdditions.h │ │ │ ├── NSLayoutConstraint+MASDebugAdditions.h │ │ │ ├── NSLayoutConstraint+MASDebugAdditions.m │ │ │ ├── View+MASAdditions.h │ │ │ ├── View+MASAdditions.m │ │ │ ├── View+MASShorthandAdditions.h │ │ │ ├── ViewController+MASAdditions.h │ │ │ └── ViewController+MASAdditions.m │ │ └── README.md │ ├── OCMock/ │ │ ├── License.txt │ │ ├── README.md │ │ └── Source/ │ │ └── OCMock/ │ │ ├── NSInvocation+OCMAdditions.h │ │ ├── NSInvocation+OCMAdditions.m │ │ ├── NSMethodSignature+OCMAdditions.h │ │ ├── NSMethodSignature+OCMAdditions.m │ │ ├── NSNotificationCenter+OCMAdditions.h │ │ ├── NSNotificationCenter+OCMAdditions.m │ │ ├── NSObject+OCMAdditions.h │ │ ├── NSObject+OCMAdditions.m │ │ ├── NSValue+OCMAdditions.h │ │ ├── NSValue+OCMAdditions.m │ │ ├── OCClassMockObject.h │ │ ├── OCClassMockObject.m │ │ ├── OCMArg.h │ │ ├── OCMArg.m │ │ ├── OCMArgAction.h │ │ ├── OCMArgAction.m │ │ ├── OCMBlockArgCaller.h │ │ ├── OCMBlockArgCaller.m │ │ ├── OCMBlockCaller.h │ │ ├── OCMBlockCaller.m │ │ ├── OCMBoxedReturnValueProvider.h │ │ ├── OCMBoxedReturnValueProvider.m │ │ ├── OCMConstraint.h │ │ ├── OCMConstraint.m │ │ ├── OCMExceptionReturnValueProvider.h │ │ ├── OCMExceptionReturnValueProvider.m │ │ ├── OCMExpectationRecorder.h │ │ ├── OCMExpectationRecorder.m │ │ ├── OCMFunctions.h │ │ ├── OCMFunctions.m │ │ ├── OCMFunctionsPrivate.h │ │ ├── OCMIndirectReturnValueProvider.h │ │ ├── OCMIndirectReturnValueProvider.m │ │ ├── OCMInvocationExpectation.h │ │ ├── OCMInvocationExpectation.m │ │ ├── OCMInvocationMatcher.h │ │ ├── OCMInvocationMatcher.m │ │ ├── OCMInvocationStub.h │ │ ├── OCMInvocationStub.m │ │ ├── OCMLocation.h │ │ ├── OCMLocation.m │ │ ├── OCMMacroState.h │ │ ├── OCMMacroState.m │ │ ├── OCMNonRetainingObjectReturnValueProvider.h │ │ ├── OCMNonRetainingObjectReturnValueProvider.m │ │ ├── OCMNotificationPoster.h │ │ ├── OCMNotificationPoster.m │ │ ├── OCMObjectReturnValueProvider.h │ │ ├── OCMObjectReturnValueProvider.m │ │ ├── OCMObserverRecorder.h │ │ ├── OCMObserverRecorder.m │ │ ├── OCMPassByRefSetter.h │ │ ├── OCMPassByRefSetter.m │ │ ├── OCMQuantifier.h │ │ ├── OCMQuantifier.m │ │ ├── OCMRealObjectForwarder.h │ │ ├── OCMRealObjectForwarder.m │ │ ├── OCMRecorder.h │ │ ├── OCMRecorder.m │ │ ├── OCMStubRecorder.h │ │ ├── OCMStubRecorder.m │ │ ├── OCMVerifier.h │ │ ├── OCMVerifier.m │ │ ├── OCMock.h │ │ ├── OCMockMacros.h │ │ ├── OCMockObject.h │ │ ├── OCMockObject.m │ │ ├── OCObserverMockObject.h │ │ ├── OCObserverMockObject.m │ │ ├── OCPartialMockObject.h │ │ ├── OCPartialMockObject.m │ │ ├── OCProtocolMockObject.h │ │ └── OCProtocolMockObject.m │ ├── Pods.xcodeproj/ │ │ ├── project.pbxproj │ │ └── xcuserdata/ │ │ └── aarongtang.xcuserdatad/ │ │ └── xcschemes/ │ │ ├── CocoaAsyncSocket.xcscheme │ │ ├── DZNEmptyDataSet.xcscheme │ │ ├── Masonry.xcscheme │ │ ├── OCMock.xcscheme │ │ ├── Pods-iOS-Network-Stack-Dive.xcscheme │ │ ├── Pods-iOS-Network-Stack-DiveTests.xcscheme │ │ ├── Reachability.xcscheme │ │ ├── ReactiveObjC.xcscheme │ │ ├── SDWebImage-SDWebImage.xcscheme │ │ ├── SDWebImage.xcscheme │ │ ├── Typhoon.xcscheme │ │ ├── YYModel.xcscheme │ │ └── xcschememanagement.plist │ ├── Reachability/ │ │ ├── LICENCE.txt │ │ ├── README.md │ │ ├── Reachability.h │ │ └── Reachability.m │ ├── ReactiveObjC/ │ │ ├── LICENSE.md │ │ ├── README.md │ │ └── ReactiveObjC/ │ │ ├── MKAnnotationView+RACSignalSupport.h │ │ ├── MKAnnotationView+RACSignalSupport.m │ │ ├── NSArray+RACSequenceAdditions.h │ │ ├── NSArray+RACSequenceAdditions.m │ │ ├── NSData+RACSupport.h │ │ ├── NSData+RACSupport.m │ │ ├── NSDictionary+RACSequenceAdditions.h │ │ ├── NSDictionary+RACSequenceAdditions.m │ │ ├── NSEnumerator+RACSequenceAdditions.h │ │ ├── NSEnumerator+RACSequenceAdditions.m │ │ ├── NSFileHandle+RACSupport.h │ │ ├── NSFileHandle+RACSupport.m │ │ ├── NSIndexSet+RACSequenceAdditions.h │ │ ├── NSIndexSet+RACSequenceAdditions.m │ │ ├── NSInvocation+RACTypeParsing.h │ │ ├── NSInvocation+RACTypeParsing.m │ │ ├── NSNotificationCenter+RACSupport.h │ │ ├── NSNotificationCenter+RACSupport.m │ │ ├── NSObject+RACDeallocating.h │ │ ├── NSObject+RACDeallocating.m │ │ ├── NSObject+RACDescription.h │ │ ├── NSObject+RACDescription.m │ │ ├── NSObject+RACKVOWrapper.h │ │ ├── NSObject+RACKVOWrapper.m │ │ ├── NSObject+RACLifting.h │ │ ├── NSObject+RACLifting.m │ │ ├── NSObject+RACPropertySubscribing.h │ │ ├── NSObject+RACPropertySubscribing.m │ │ ├── NSObject+RACSelectorSignal.h │ │ ├── NSObject+RACSelectorSignal.m │ │ ├── NSOrderedSet+RACSequenceAdditions.h │ │ ├── NSOrderedSet+RACSequenceAdditions.m │ │ ├── NSSet+RACSequenceAdditions.h │ │ ├── NSSet+RACSequenceAdditions.m │ │ ├── NSString+RACKeyPathUtilities.h │ │ ├── NSString+RACKeyPathUtilities.m │ │ ├── NSString+RACSequenceAdditions.h │ │ ├── NSString+RACSequenceAdditions.m │ │ ├── NSString+RACSupport.h │ │ ├── NSString+RACSupport.m │ │ ├── NSURLConnection+RACSupport.h │ │ ├── NSURLConnection+RACSupport.m │ │ ├── NSUserDefaults+RACSupport.h │ │ ├── NSUserDefaults+RACSupport.m │ │ ├── RACAnnotations.h │ │ ├── RACArraySequence.h │ │ ├── RACArraySequence.m │ │ ├── RACBehaviorSubject.h │ │ ├── RACBehaviorSubject.m │ │ ├── RACBlockTrampoline.h │ │ ├── RACBlockTrampoline.m │ │ ├── RACChannel.h │ │ ├── RACChannel.m │ │ ├── RACCommand.h │ │ ├── RACCommand.m │ │ ├── RACCompoundDisposable.h │ │ ├── RACCompoundDisposable.m │ │ ├── RACCompoundDisposableProvider.d │ │ ├── RACDelegateProxy.h │ │ ├── RACDelegateProxy.m │ │ ├── RACDisposable.h │ │ ├── RACDisposable.m │ │ ├── RACDynamicSequence.h │ │ ├── RACDynamicSequence.m │ │ ├── RACDynamicSignal.h │ │ ├── RACDynamicSignal.m │ │ ├── RACEagerSequence.h │ │ ├── RACEagerSequence.m │ │ ├── RACEmptySequence.h │ │ ├── RACEmptySequence.m │ │ ├── RACEmptySignal.h │ │ ├── RACEmptySignal.m │ │ ├── RACErrorSignal.h │ │ ├── RACErrorSignal.m │ │ ├── RACEvent.h │ │ ├── RACEvent.m │ │ ├── RACGroupedSignal.h │ │ ├── RACGroupedSignal.m │ │ ├── RACImmediateScheduler.h │ │ ├── RACImmediateScheduler.m │ │ ├── RACIndexSetSequence.h │ │ ├── RACIndexSetSequence.m │ │ ├── RACKVOChannel.h │ │ ├── RACKVOChannel.m │ │ ├── RACKVOProxy.h │ │ ├── RACKVOProxy.m │ │ ├── RACKVOTrampoline.h │ │ ├── RACKVOTrampoline.m │ │ ├── RACMulticastConnection+Private.h │ │ ├── RACMulticastConnection.h │ │ ├── RACMulticastConnection.m │ │ ├── RACPassthroughSubscriber.h │ │ ├── RACPassthroughSubscriber.m │ │ ├── RACQueueScheduler+Subclass.h │ │ ├── RACQueueScheduler.h │ │ ├── RACQueueScheduler.m │ │ ├── RACReplaySubject.h │ │ ├── RACReplaySubject.m │ │ ├── RACReturnSignal.h │ │ ├── RACReturnSignal.m │ │ ├── RACScheduler+Private.h │ │ ├── RACScheduler+Subclass.h │ │ ├── RACScheduler.h │ │ ├── RACScheduler.m │ │ ├── RACScopedDisposable.h │ │ ├── RACScopedDisposable.m │ │ ├── RACSequence.h │ │ ├── RACSequence.m │ │ ├── RACSerialDisposable.h │ │ ├── RACSerialDisposable.m │ │ ├── RACSignal+Operations.h │ │ ├── RACSignal+Operations.m │ │ ├── RACSignal.h │ │ ├── RACSignal.m │ │ ├── RACSignalProvider.d │ │ ├── RACSignalSequence.h │ │ ├── RACSignalSequence.m │ │ ├── RACStream+Private.h │ │ ├── RACStream.h │ │ ├── RACStream.m │ │ ├── RACStringSequence.h │ │ ├── RACStringSequence.m │ │ ├── RACSubject.h │ │ ├── RACSubject.m │ │ ├── RACSubscriber+Private.h │ │ ├── RACSubscriber.h │ │ ├── RACSubscriber.m │ │ ├── RACSubscriptingAssignmentTrampoline.h │ │ ├── RACSubscriptingAssignmentTrampoline.m │ │ ├── RACSubscriptionScheduler.h │ │ ├── RACSubscriptionScheduler.m │ │ ├── RACTargetQueueScheduler.h │ │ ├── RACTargetQueueScheduler.m │ │ ├── RACTestScheduler.h │ │ ├── RACTestScheduler.m │ │ ├── RACTuple.h │ │ ├── RACTuple.m │ │ ├── RACTupleSequence.h │ │ ├── RACTupleSequence.m │ │ ├── RACUnarySequence.h │ │ ├── RACUnarySequence.m │ │ ├── RACUnit.h │ │ ├── RACUnit.m │ │ ├── RACValueTransformer.h │ │ ├── RACValueTransformer.m │ │ ├── ReactiveObjC.h │ │ ├── UIActionSheet+RACSignalSupport.h │ │ ├── UIActionSheet+RACSignalSupport.m │ │ ├── UIAlertView+RACSignalSupport.h │ │ ├── UIAlertView+RACSignalSupport.m │ │ ├── UIBarButtonItem+RACCommandSupport.h │ │ ├── UIBarButtonItem+RACCommandSupport.m │ │ ├── UIButton+RACCommandSupport.h │ │ ├── UIButton+RACCommandSupport.m │ │ ├── UICollectionReusableView+RACSignalSupport.h │ │ ├── UICollectionReusableView+RACSignalSupport.m │ │ ├── UIControl+RACSignalSupport.h │ │ ├── UIControl+RACSignalSupport.m │ │ ├── UIControl+RACSignalSupportPrivate.h │ │ ├── UIControl+RACSignalSupportPrivate.m │ │ ├── UIDatePicker+RACSignalSupport.h │ │ ├── UIDatePicker+RACSignalSupport.m │ │ ├── UIGestureRecognizer+RACSignalSupport.h │ │ ├── UIGestureRecognizer+RACSignalSupport.m │ │ ├── UIImagePickerController+RACSignalSupport.h │ │ ├── UIImagePickerController+RACSignalSupport.m │ │ ├── UIRefreshControl+RACCommandSupport.h │ │ ├── UIRefreshControl+RACCommandSupport.m │ │ ├── UISegmentedControl+RACSignalSupport.h │ │ ├── UISegmentedControl+RACSignalSupport.m │ │ ├── UISlider+RACSignalSupport.h │ │ ├── UISlider+RACSignalSupport.m │ │ ├── UIStepper+RACSignalSupport.h │ │ ├── UIStepper+RACSignalSupport.m │ │ ├── UISwitch+RACSignalSupport.h │ │ ├── UISwitch+RACSignalSupport.m │ │ ├── UITableViewCell+RACSignalSupport.h │ │ ├── UITableViewCell+RACSignalSupport.m │ │ ├── UITableViewHeaderFooterView+RACSignalSupport.h │ │ ├── UITableViewHeaderFooterView+RACSignalSupport.m │ │ ├── UITextField+RACSignalSupport.h │ │ ├── UITextField+RACSignalSupport.m │ │ ├── UITextView+RACSignalSupport.h │ │ ├── UITextView+RACSignalSupport.m │ │ └── extobjc/ │ │ ├── RACEXTKeyPathCoding.h │ │ ├── RACEXTRuntimeExtensions.h │ │ ├── RACEXTRuntimeExtensions.m │ │ ├── RACEXTScope.h │ │ └── RACmetamacros.h │ ├── SDWebImage/ │ │ ├── LICENSE │ │ ├── README.md │ │ ├── SDWebImage/ │ │ │ ├── Core/ │ │ │ │ ├── NSButton+WebCache.h │ │ │ │ ├── NSButton+WebCache.m │ │ │ │ ├── NSData+ImageContentType.h │ │ │ │ ├── NSData+ImageContentType.m │ │ │ │ ├── NSImage+Compatibility.h │ │ │ │ ├── NSImage+Compatibility.m │ │ │ │ ├── SDAnimatedImage.h │ │ │ │ ├── SDAnimatedImage.m │ │ │ │ ├── SDAnimatedImagePlayer.h │ │ │ │ ├── SDAnimatedImagePlayer.m │ │ │ │ ├── SDAnimatedImageRep.h │ │ │ │ ├── SDAnimatedImageRep.m │ │ │ │ ├── SDAnimatedImageView+WebCache.h │ │ │ │ ├── SDAnimatedImageView+WebCache.m │ │ │ │ ├── SDAnimatedImageView.h │ │ │ │ ├── SDAnimatedImageView.m │ │ │ │ ├── SDCallbackQueue.h │ │ │ │ ├── SDCallbackQueue.m │ │ │ │ ├── SDDiskCache.h │ │ │ │ ├── SDDiskCache.m │ │ │ │ ├── SDGraphicsImageRenderer.h │ │ │ │ ├── SDGraphicsImageRenderer.m │ │ │ │ ├── SDImageAPNGCoder.h │ │ │ │ ├── SDImageAPNGCoder.m │ │ │ │ ├── SDImageAWebPCoder.h │ │ │ │ ├── SDImageAWebPCoder.m │ │ │ │ ├── SDImageCache.h │ │ │ │ ├── SDImageCache.m │ │ │ │ ├── SDImageCacheConfig.h │ │ │ │ ├── SDImageCacheConfig.m │ │ │ │ ├── SDImageCacheDefine.h │ │ │ │ ├── SDImageCacheDefine.m │ │ │ │ ├── SDImageCachesManager.h │ │ │ │ ├── SDImageCachesManager.m │ │ │ │ ├── SDImageCoder.h │ │ │ │ ├── SDImageCoder.m │ │ │ │ ├── SDImageCoderHelper.h │ │ │ │ ├── SDImageCoderHelper.m │ │ │ │ ├── SDImageCodersManager.h │ │ │ │ ├── SDImageCodersManager.m │ │ │ │ ├── SDImageFrame.h │ │ │ │ ├── SDImageFrame.m │ │ │ │ ├── SDImageGIFCoder.h │ │ │ │ ├── SDImageGIFCoder.m │ │ │ │ ├── SDImageGraphics.h │ │ │ │ ├── SDImageGraphics.m │ │ │ │ ├── SDImageHEICCoder.h │ │ │ │ ├── SDImageHEICCoder.m │ │ │ │ ├── SDImageIOAnimatedCoder.h │ │ │ │ ├── SDImageIOAnimatedCoder.m │ │ │ │ ├── SDImageIOCoder.h │ │ │ │ ├── SDImageIOCoder.m │ │ │ │ ├── SDImageLoader.h │ │ │ │ ├── SDImageLoader.m │ │ │ │ ├── SDImageLoadersManager.h │ │ │ │ ├── SDImageLoadersManager.m │ │ │ │ ├── SDImageTransformer.h │ │ │ │ ├── SDImageTransformer.m │ │ │ │ ├── SDMemoryCache.h │ │ │ │ ├── SDMemoryCache.m │ │ │ │ ├── SDWebImageCacheKeyFilter.h │ │ │ │ ├── SDWebImageCacheKeyFilter.m │ │ │ │ ├── SDWebImageCacheSerializer.h │ │ │ │ ├── SDWebImageCacheSerializer.m │ │ │ │ ├── SDWebImageCompat.h │ │ │ │ ├── SDWebImageCompat.m │ │ │ │ ├── SDWebImageDefine.h │ │ │ │ ├── SDWebImageDefine.m │ │ │ │ ├── SDWebImageDownloader.h │ │ │ │ ├── SDWebImageDownloader.m │ │ │ │ ├── SDWebImageDownloaderConfig.h │ │ │ │ ├── SDWebImageDownloaderConfig.m │ │ │ │ ├── SDWebImageDownloaderDecryptor.h │ │ │ │ ├── SDWebImageDownloaderDecryptor.m │ │ │ │ ├── SDWebImageDownloaderOperation.h │ │ │ │ ├── SDWebImageDownloaderOperation.m │ │ │ │ ├── SDWebImageDownloaderRequestModifier.h │ │ │ │ ├── SDWebImageDownloaderRequestModifier.m │ │ │ │ ├── SDWebImageDownloaderResponseModifier.h │ │ │ │ ├── SDWebImageDownloaderResponseModifier.m │ │ │ │ ├── SDWebImageError.h │ │ │ │ ├── SDWebImageError.m │ │ │ │ ├── SDWebImageIndicator.h │ │ │ │ ├── SDWebImageIndicator.m │ │ │ │ ├── SDWebImageManager.h │ │ │ │ ├── SDWebImageManager.m │ │ │ │ ├── SDWebImageOperation.h │ │ │ │ ├── SDWebImageOperation.m │ │ │ │ ├── SDWebImageOptionsProcessor.h │ │ │ │ ├── SDWebImageOptionsProcessor.m │ │ │ │ ├── SDWebImagePrefetcher.h │ │ │ │ ├── SDWebImagePrefetcher.m │ │ │ │ ├── SDWebImageTransition.h │ │ │ │ ├── SDWebImageTransition.m │ │ │ │ ├── UIButton+WebCache.h │ │ │ │ ├── UIButton+WebCache.m │ │ │ │ ├── UIImage+ExtendedCacheData.h │ │ │ │ ├── UIImage+ExtendedCacheData.m │ │ │ │ ├── UIImage+ForceDecode.h │ │ │ │ ├── UIImage+ForceDecode.m │ │ │ │ ├── UIImage+GIF.h │ │ │ │ ├── UIImage+GIF.m │ │ │ │ ├── UIImage+MemoryCacheCost.h │ │ │ │ ├── UIImage+MemoryCacheCost.m │ │ │ │ ├── UIImage+Metadata.h │ │ │ │ ├── UIImage+Metadata.m │ │ │ │ ├── UIImage+MultiFormat.h │ │ │ │ ├── UIImage+MultiFormat.m │ │ │ │ ├── UIImage+Transform.h │ │ │ │ ├── UIImage+Transform.m │ │ │ │ ├── UIImageView+HighlightedWebCache.h │ │ │ │ ├── UIImageView+HighlightedWebCache.m │ │ │ │ ├── UIImageView+WebCache.h │ │ │ │ ├── UIImageView+WebCache.m │ │ │ │ ├── UIView+WebCache.h │ │ │ │ ├── UIView+WebCache.m │ │ │ │ ├── UIView+WebCacheOperation.h │ │ │ │ ├── UIView+WebCacheOperation.m │ │ │ │ ├── UIView+WebCacheState.h │ │ │ │ └── UIView+WebCacheState.m │ │ │ └── Private/ │ │ │ ├── NSBezierPath+SDRoundedCorners.h │ │ │ ├── NSBezierPath+SDRoundedCorners.m │ │ │ ├── SDAssociatedObject.h │ │ │ ├── SDAssociatedObject.m │ │ │ ├── SDAsyncBlockOperation.h │ │ │ ├── SDAsyncBlockOperation.m │ │ │ ├── SDDeviceHelper.h │ │ │ ├── SDDeviceHelper.m │ │ │ ├── SDDisplayLink.h │ │ │ ├── SDDisplayLink.m │ │ │ ├── SDFileAttributeHelper.h │ │ │ ├── SDFileAttributeHelper.m │ │ │ ├── SDImageAssetManager.h │ │ │ ├── SDImageAssetManager.m │ │ │ ├── SDImageCachesManagerOperation.h │ │ │ ├── SDImageCachesManagerOperation.m │ │ │ ├── SDImageFramePool.h │ │ │ ├── SDImageFramePool.m │ │ │ ├── SDImageIOAnimatedCoderInternal.h │ │ │ ├── SDInternalMacros.h │ │ │ ├── SDInternalMacros.m │ │ │ ├── SDWeakProxy.h │ │ │ ├── SDWeakProxy.m │ │ │ ├── SDWebImageTransitionInternal.h │ │ │ ├── SDmetamacros.h │ │ │ ├── UIColor+SDHexString.h │ │ │ └── UIColor+SDHexString.m │ │ └── WebImage/ │ │ ├── PrivacyInfo.xcprivacy │ │ └── SDWebImage.h │ ├── Target Support Files/ │ │ ├── CocoaAsyncSocket/ │ │ │ ├── CocoaAsyncSocket-Info.plist │ │ │ ├── CocoaAsyncSocket-dummy.m │ │ │ ├── CocoaAsyncSocket-prefix.pch │ │ │ ├── CocoaAsyncSocket-umbrella.h │ │ │ ├── CocoaAsyncSocket.debug.xcconfig │ │ │ ├── CocoaAsyncSocket.modulemap │ │ │ └── CocoaAsyncSocket.release.xcconfig │ │ ├── DZNEmptyDataSet/ │ │ │ ├── DZNEmptyDataSet-Info.plist │ │ │ ├── DZNEmptyDataSet-dummy.m │ │ │ ├── DZNEmptyDataSet-prefix.pch │ │ │ ├── DZNEmptyDataSet-umbrella.h │ │ │ ├── DZNEmptyDataSet.debug.xcconfig │ │ │ ├── DZNEmptyDataSet.modulemap │ │ │ └── DZNEmptyDataSet.release.xcconfig │ │ ├── Masonry/ │ │ │ ├── Masonry-Info.plist │ │ │ ├── Masonry-dummy.m │ │ │ ├── Masonry-prefix.pch │ │ │ ├── Masonry-umbrella.h │ │ │ ├── Masonry.debug.xcconfig │ │ │ ├── Masonry.modulemap │ │ │ └── Masonry.release.xcconfig │ │ ├── OCMock/ │ │ │ ├── OCMock-Info.plist │ │ │ ├── OCMock-dummy.m │ │ │ ├── OCMock-prefix.pch │ │ │ ├── OCMock-umbrella.h │ │ │ ├── OCMock.debug.xcconfig │ │ │ ├── OCMock.modulemap │ │ │ └── OCMock.release.xcconfig │ │ ├── Pods-iOS-Network-Stack-Dive/ │ │ │ ├── Pods-iOS-Network-Stack-Dive-Info.plist │ │ │ ├── Pods-iOS-Network-Stack-Dive-acknowledgements.markdown │ │ │ ├── Pods-iOS-Network-Stack-Dive-acknowledgements.plist │ │ │ ├── Pods-iOS-Network-Stack-Dive-dummy.m │ │ │ ├── Pods-iOS-Network-Stack-Dive-frameworks-Debug-input-files.xcfilelist │ │ │ ├── Pods-iOS-Network-Stack-Dive-frameworks-Debug-output-files.xcfilelist │ │ │ ├── Pods-iOS-Network-Stack-Dive-frameworks-Release-input-files.xcfilelist │ │ │ ├── Pods-iOS-Network-Stack-Dive-frameworks-Release-output-files.xcfilelist │ │ │ ├── Pods-iOS-Network-Stack-Dive-frameworks.sh │ │ │ ├── Pods-iOS-Network-Stack-Dive-umbrella.h │ │ │ ├── Pods-iOS-Network-Stack-Dive.debug.xcconfig │ │ │ ├── Pods-iOS-Network-Stack-Dive.modulemap │ │ │ └── Pods-iOS-Network-Stack-Dive.release.xcconfig │ │ ├── Pods-iOS-Network-Stack-DiveTests/ │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-Info.plist │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-acknowledgements.markdown │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-acknowledgements.plist │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-dummy.m │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-frameworks-Debug-input-files.xcfilelist │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-frameworks-Debug-output-files.xcfilelist │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-frameworks-Release-input-files.xcfilelist │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-frameworks-Release-output-files.xcfilelist │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-frameworks.sh │ │ │ ├── Pods-iOS-Network-Stack-DiveTests-umbrella.h │ │ │ ├── Pods-iOS-Network-Stack-DiveTests.debug.xcconfig │ │ │ ├── Pods-iOS-Network-Stack-DiveTests.modulemap │ │ │ └── Pods-iOS-Network-Stack-DiveTests.release.xcconfig │ │ ├── Reachability/ │ │ │ ├── Reachability-Info.plist │ │ │ ├── Reachability-dummy.m │ │ │ ├── Reachability-prefix.pch │ │ │ ├── Reachability-umbrella.h │ │ │ ├── Reachability.debug.xcconfig │ │ │ ├── Reachability.modulemap │ │ │ └── Reachability.release.xcconfig │ │ ├── ReactiveObjC/ │ │ │ ├── ReactiveObjC-Info.plist │ │ │ ├── ReactiveObjC-dummy.m │ │ │ ├── ReactiveObjC-prefix.pch │ │ │ ├── ReactiveObjC-umbrella.h │ │ │ ├── ReactiveObjC.debug.xcconfig │ │ │ ├── ReactiveObjC.modulemap │ │ │ └── ReactiveObjC.release.xcconfig │ │ ├── SDWebImage/ │ │ │ ├── ResourceBundle-SDWebImage-SDWebImage-Info.plist │ │ │ ├── SDWebImage-Info.plist │ │ │ ├── SDWebImage-dummy.m │ │ │ ├── SDWebImage-prefix.pch │ │ │ ├── SDWebImage-umbrella.h │ │ │ ├── SDWebImage.debug.xcconfig │ │ │ ├── SDWebImage.modulemap │ │ │ └── SDWebImage.release.xcconfig │ │ ├── Typhoon/ │ │ │ ├── Typhoon-Info.plist │ │ │ ├── Typhoon-dummy.m │ │ │ ├── Typhoon-prefix.pch │ │ │ ├── Typhoon-umbrella.h │ │ │ ├── Typhoon.debug.xcconfig │ │ │ ├── Typhoon.modulemap │ │ │ └── Typhoon.release.xcconfig │ │ └── YYModel/ │ │ ├── YYModel-Info.plist │ │ ├── YYModel-dummy.m │ │ ├── YYModel-prefix.pch │ │ ├── YYModel-umbrella.h │ │ ├── YYModel.debug.xcconfig │ │ ├── YYModel.modulemap │ │ └── YYModel.release.xcconfig │ ├── Typhoon/ │ │ ├── LICENSE │ │ ├── README.md │ │ └── Source/ │ │ ├── Configuration/ │ │ │ ├── ConfigPostProcessor/ │ │ │ │ ├── Internal/ │ │ │ │ │ └── TyphoonConfigPostProcessor+Internal.h │ │ │ │ ├── TyphoonConfigPostProcessor.h │ │ │ │ ├── TyphoonConfigPostProcessor.m │ │ │ │ ├── TyphoonConfiguration/ │ │ │ │ │ ├── TyphoonConfiguration.h │ │ │ │ │ ├── TyphoonJsonStyleConfiguration.h │ │ │ │ │ ├── TyphoonJsonStyleConfiguration.m │ │ │ │ │ ├── TyphoonPlistStyleConfiguration.h │ │ │ │ │ ├── TyphoonPlistStyleConfiguration.m │ │ │ │ │ ├── TyphoonPropertyStyleConfiguration.h │ │ │ │ │ └── TyphoonPropertyStyleConfiguration.m │ │ │ │ ├── TyphoonDefinition+Config.h │ │ │ │ └── TyphoonDefinition+Config.m │ │ │ ├── DefinitionOptionConfiguration/ │ │ │ │ ├── Matcher/ │ │ │ │ │ ├── TyphoonOptionMatcher+Internal.h │ │ │ │ │ ├── TyphoonOptionMatcher.h │ │ │ │ │ └── TyphoonOptionMatcher.m │ │ │ │ ├── TyphoonDefinition+Option.h │ │ │ │ └── TyphoonDefinition+Option.m │ │ │ ├── GlobalConfigResolver/ │ │ │ │ ├── TyphoonGlobalConfigCollector.h │ │ │ │ └── TyphoonGlobalConfigCollector.m │ │ │ ├── Resource/ │ │ │ │ ├── TyphoonBundleResource.h │ │ │ │ ├── TyphoonBundleResource.m │ │ │ │ ├── TyphoonPathResource.h │ │ │ │ ├── TyphoonPathResource.m │ │ │ │ └── TyphoonResource.h │ │ │ ├── Startup/ │ │ │ │ ├── TyphoonStartup.h │ │ │ │ └── TyphoonStartup.m │ │ │ ├── TyphoonAbstractDetachableComponentFactoryPostProcessor.h │ │ │ ├── TyphoonAbstractDetachableComponentFactoryPostProcessor.m │ │ │ ├── TyphoonDefinitionPostProcessor.h │ │ │ ├── TyphoonInstancePostProcessor.h │ │ │ └── TyphoonOrdered.h │ │ ├── Definition/ │ │ │ ├── AutoInjection/ │ │ │ │ ├── TyphoonAutoInjection.h │ │ │ │ ├── TyphoonFactoryAutoInjectionPostProcessor.h │ │ │ │ ├── TyphoonFactoryAutoInjectionPostProcessor.m │ │ │ │ ├── TyphoonInjectedObject.h │ │ │ │ └── TyphoonInjectedObject.m │ │ │ ├── Infrastructure/ │ │ │ │ ├── TyphoonDefinition+Infrastructure.h │ │ │ │ └── TyphoonInjectionsEnumeration.h │ │ │ ├── Injections/ │ │ │ │ ├── TyphoonAbstractInjection.h │ │ │ │ ├── TyphoonAbstractInjection.m │ │ │ │ ├── TyphoonInject.h │ │ │ │ ├── TyphoonInject.m │ │ │ │ ├── TyphoonInjection.h │ │ │ │ ├── TyphoonInjectionByCollection.h │ │ │ │ ├── TyphoonInjectionByCollection.m │ │ │ │ ├── TyphoonInjectionByComponentFactory.h │ │ │ │ ├── TyphoonInjectionByComponentFactory.m │ │ │ │ ├── TyphoonInjectionByConfig.h │ │ │ │ ├── TyphoonInjectionByConfig.m │ │ │ │ ├── TyphoonInjectionByCurrentRuntimeArguments.h │ │ │ │ ├── TyphoonInjectionByCurrentRuntimeArguments.m │ │ │ │ ├── TyphoonInjectionByDictionary.h │ │ │ │ ├── TyphoonInjectionByDictionary.m │ │ │ │ ├── TyphoonInjectionByFactoryReference.h │ │ │ │ ├── TyphoonInjectionByFactoryReference.m │ │ │ │ ├── TyphoonInjectionByObjectFromString.h │ │ │ │ ├── TyphoonInjectionByObjectFromString.m │ │ │ │ ├── TyphoonInjectionByObjectInstance.h │ │ │ │ ├── TyphoonInjectionByObjectInstance.m │ │ │ │ ├── TyphoonInjectionByReference.h │ │ │ │ ├── TyphoonInjectionByReference.m │ │ │ │ ├── TyphoonInjectionByRuntimeArgument.h │ │ │ │ ├── TyphoonInjectionByRuntimeArgument.m │ │ │ │ ├── TyphoonInjectionByType.h │ │ │ │ ├── TyphoonInjectionByType.m │ │ │ │ ├── TyphoonInjectionContext.h │ │ │ │ ├── TyphoonInjectionContext.m │ │ │ │ ├── TyphoonInjections.h │ │ │ │ ├── TyphoonInjections.m │ │ │ │ ├── TyphoonParameterInjection.h │ │ │ │ └── TyphoonPropertyInjection.h │ │ │ ├── Internal/ │ │ │ │ ├── Collections+CustomInjection.h │ │ │ │ ├── Collections+CustomInjection.m │ │ │ │ ├── NSDictionary+CustomInjection.h │ │ │ │ ├── NSDictionary+CustomInjection.m │ │ │ │ ├── TyphoonBlockDefinition+InstanceBuilder.h │ │ │ │ ├── TyphoonBlockDefinition+InstanceBuilder.m │ │ │ │ ├── TyphoonBlockDefinition+Internal.h │ │ │ │ ├── TyphoonBlockDefinitionController.h │ │ │ │ ├── TyphoonBlockDefinitionController.m │ │ │ │ ├── TyphoonDefinition+InstanceBuilder.h │ │ │ │ ├── TyphoonDefinition+InstanceBuilder.m │ │ │ │ ├── TyphoonDefinition+Internal.h │ │ │ │ ├── TyphoonFactoryDefinition.h │ │ │ │ ├── TyphoonFactoryDefinition.m │ │ │ │ ├── TyphoonInjectionDefinition.h │ │ │ │ ├── TyphoonInjectionDefinition.m │ │ │ │ ├── TyphoonObjectWithCustomInjection.h │ │ │ │ ├── TyphoonReferenceDefinition.h │ │ │ │ └── TyphoonReferenceDefinition.m │ │ │ ├── Method/ │ │ │ │ ├── Internal/ │ │ │ │ │ ├── TyphoonMethod+InstanceBuilder.h │ │ │ │ │ └── TyphoonMethod+InstanceBuilder.m │ │ │ │ ├── TyphoonMethod.h │ │ │ │ └── TyphoonMethod.m │ │ │ ├── Namespacing/ │ │ │ │ ├── TyphoonDefinitionNamespace.h │ │ │ │ └── TyphoonDefinitionNamespace.m │ │ │ ├── TyphoonBlockDefinition.h │ │ │ ├── TyphoonBlockDefinition.m │ │ │ ├── TyphoonDefinition.h │ │ │ └── TyphoonDefinition.m │ │ ├── Factory/ │ │ │ ├── Assembly/ │ │ │ │ ├── TyphoonAssembly+TyphoonAssemblyFriend.h │ │ │ │ ├── TyphoonAssembly.h │ │ │ │ ├── TyphoonAssembly.m │ │ │ │ ├── TyphoonAssemblyAccessor.h │ │ │ │ └── TyphoonAssemblyAccessor.m │ │ │ ├── Hooks/ │ │ │ │ └── NSObject+FactoryHooks.h │ │ │ ├── Internal/ │ │ │ │ ├── NSInvocation+TCFCustomImplementation.h │ │ │ │ ├── NSInvocation+TCFCustomImplementation.m │ │ │ │ ├── NSInvocation+TCFInstanceBuilder.h │ │ │ │ ├── NSInvocation+TCFInstanceBuilder.m │ │ │ │ ├── NSInvocation+TCFUnwrapValues.h │ │ │ │ ├── NSInvocation+TCFUnwrapValues.m │ │ │ │ ├── NSInvocation+TCFWrapValues.h │ │ │ │ ├── NSInvocation+TCFWrapValues.m │ │ │ │ ├── NSMethodSignature+TCFUnwrapValues.h │ │ │ │ ├── NSMethodSignature+TCFUnwrapValues.m │ │ │ │ ├── NSValue+TCFUnwrapValues.h │ │ │ │ ├── NSValue+TCFUnwrapValues.m │ │ │ │ ├── TyphoonAssemblyAdviser.h │ │ │ │ ├── TyphoonAssemblyAdviser.m │ │ │ │ ├── TyphoonAssemblyBuilder+PlistProcessor.h │ │ │ │ ├── TyphoonAssemblyBuilder+PlistProcessor.m │ │ │ │ ├── TyphoonAssemblyBuilder.h │ │ │ │ ├── TyphoonAssemblyBuilder.m │ │ │ │ ├── TyphoonAssemblyDefinitionBuilder.h │ │ │ │ ├── TyphoonAssemblyDefinitionBuilder.m │ │ │ │ ├── TyphoonAssemblyPropertyInjectionPostProcessor.h │ │ │ │ ├── TyphoonAssemblyPropertyInjectionPostProcessor.m │ │ │ │ ├── TyphoonAssemblySelectorAdviser.h │ │ │ │ ├── TyphoonAssemblySelectorAdviser.m │ │ │ │ ├── TyphoonBlockComponentFactory.h │ │ │ │ ├── TyphoonBlockComponentFactory.m │ │ │ │ ├── TyphoonCallStack.h │ │ │ │ ├── TyphoonCallStack.m │ │ │ │ ├── TyphoonCircularDependencyTerminator.h │ │ │ │ ├── TyphoonCircularDependencyTerminator.m │ │ │ │ ├── TyphoonCollaboratingAssembliesCollector.h │ │ │ │ ├── TyphoonCollaboratingAssembliesCollector.m │ │ │ │ ├── TyphoonCollaboratingAssemblyPropertyEnumerator.h │ │ │ │ ├── TyphoonCollaboratingAssemblyPropertyEnumerator.m │ │ │ │ ├── TyphoonCollaboratingAssemblyProxy.h │ │ │ │ ├── TyphoonCollaboratingAssemblyProxy.m │ │ │ │ ├── TyphoonComponentFactory+InstanceBuilder.h │ │ │ │ ├── TyphoonComponentFactory+InstanceBuilder.m │ │ │ │ ├── TyphoonComponentFactory+TyphoonDefinitionRegisterer.h │ │ │ │ ├── TyphoonFactoryPropertyInjectionPostProcessor.h │ │ │ │ ├── TyphoonFactoryPropertyInjectionPostProcessor.m │ │ │ │ ├── TyphoonMemoryManagementUtils.h │ │ │ │ ├── TyphoonMemoryManagementUtils.m │ │ │ │ ├── TyphoonParentReferenceHydratingPostProcessor.h │ │ │ │ ├── TyphoonParentReferenceHydratingPostProcessor.m │ │ │ │ ├── TyphoonRuntimeArguments.h │ │ │ │ ├── TyphoonRuntimeArguments.m │ │ │ │ ├── TyphoonStackElement.h │ │ │ │ └── TyphoonStackElement.m │ │ │ ├── Pool/ │ │ │ │ ├── TyphoonComponentsPool.h │ │ │ │ ├── TyphoonWeakComponentsPool.h │ │ │ │ └── TyphoonWeakComponentsPool.m │ │ │ ├── TyphoonComponentFactory.h │ │ │ ├── TyphoonComponentFactory.m │ │ │ ├── TyphoonDefinitionRegisterer.h │ │ │ ├── TyphoonDefinitionRegisterer.m │ │ │ ├── TyphoonPreattachedComponentsRegisterer.h │ │ │ └── TyphoonPreattachedComponentsRegisterer.m │ │ ├── Test/ │ │ │ ├── Patcher/ │ │ │ │ ├── TyphoonPatcher.h │ │ │ │ └── TyphoonPatcher.m │ │ │ └── TestUtils/ │ │ │ ├── TyphoonTestUtils.h │ │ │ └── TyphoonTestUtils.m │ │ ├── TypeConversion/ │ │ │ ├── Converters/ │ │ │ │ ├── NSNullTypeConverter.h │ │ │ │ ├── NSNullTypeConverter.m │ │ │ │ ├── TyphoonNSNumberTypeConverter.h │ │ │ │ ├── TyphoonNSNumberTypeConverter.m │ │ │ │ ├── TyphoonNSURLTypeConverter.h │ │ │ │ ├── TyphoonNSURLTypeConverter.m │ │ │ │ ├── TyphoonPassThroughTypeConverter.h │ │ │ │ ├── TyphoonPassThroughTypeConverter.m │ │ │ │ ├── TyphoonPrimitiveTypeConverter.h │ │ │ │ └── TyphoonPrimitiveTypeConverter.m │ │ │ ├── Helpers/ │ │ │ │ ├── TyphoonColorConversionUtils.h │ │ │ │ └── TyphoonColorConversionUtils.m │ │ │ ├── TyphoonTypeConversionUtils.h │ │ │ ├── TyphoonTypeConversionUtils.m │ │ │ ├── TyphoonTypeConverter.h │ │ │ ├── TyphoonTypeConverterRegistry.h │ │ │ ├── TyphoonTypeConverterRegistry.m │ │ │ ├── TyphoonTypeDescriptor.h │ │ │ └── TyphoonTypeDescriptor.m │ │ ├── Typhoon+Infrastructure.h │ │ ├── Typhoon.h │ │ ├── Utils/ │ │ │ ├── NSArray+TyphoonManualEnumeration.h │ │ │ ├── NSArray+TyphoonManualEnumeration.m │ │ │ ├── NSObject+DeallocNotification.h │ │ │ ├── NSObject+DeallocNotification.m │ │ │ ├── NSObject+PropertyInjection.h │ │ │ ├── NSObject+PropertyInjection.m │ │ │ ├── NSObject+TyphoonIntrospectionUtils.h │ │ │ ├── NSObject+TyphoonIntrospectionUtils.m │ │ │ ├── Swizzle/ │ │ │ │ ├── TyphoonMethodSwizzler.h │ │ │ │ ├── TyphoonSwizzlerDefaultImpl.h │ │ │ │ └── TyphoonSwizzlerDefaultImpl.m │ │ │ ├── TyphoonIntrospectionUtils.h │ │ │ ├── TyphoonIntrospectionUtils.m │ │ │ ├── TyphoonLinkerCategoryBugFix.h │ │ │ ├── TyphoonSelector.h │ │ │ ├── TyphoonSelector.m │ │ │ └── TyphoonUtils.h │ │ ├── Vendor/ │ │ │ └── OCLogTemplate/ │ │ │ └── OCLogTemplate.h │ │ └── ios/ │ │ ├── Configuration/ │ │ │ ├── TyphoonViewControllerInjector.h │ │ │ └── TyphoonViewControllerInjector.m │ │ ├── Definition/ │ │ │ ├── TyphoonStoryboardDefinition.h │ │ │ ├── TyphoonStoryboardDefinition.m │ │ │ ├── TyphoonStoryboardDefinitionContext.h │ │ │ └── TyphoonStoryboardDefinitionContext.m │ │ ├── Nib/ │ │ │ ├── TyphoonNibLoader.h │ │ │ ├── TyphoonNibLoader.m │ │ │ ├── TyphoonViewControllerNibResolver.h │ │ │ └── TyphoonViewControllerNibResolver.m │ │ ├── Storyboard/ │ │ │ ├── Internal/ │ │ │ │ ├── UIView+TyphoonDefinitionKey.h │ │ │ │ ├── UIView+TyphoonDefinitionKey.m │ │ │ │ ├── UIViewController+TyphoonStoryboardIntegration.h │ │ │ │ └── UIViewController+TyphoonStoryboardIntegration.m │ │ │ ├── NSLayoutConstraint+TyphoonOutletTransfer.h │ │ │ ├── NSLayoutConstraint+TyphoonOutletTransfer.m │ │ │ ├── TyphoonComponentFactory+Storyboard.h │ │ │ ├── TyphoonComponentFactory+Storyboard.m │ │ │ ├── TyphoonDefinition+Storyboard.h │ │ │ ├── TyphoonDefinition+Storyboard.m │ │ │ ├── TyphoonLoadedView.h │ │ │ ├── TyphoonLoadedView.m │ │ │ ├── TyphoonStoryboard.h │ │ │ ├── TyphoonStoryboard.m │ │ │ ├── TyphoonStoryboardProvider.h │ │ │ ├── TyphoonStoryboardProvider.m │ │ │ ├── TyphoonStoryboardResolver.h │ │ │ ├── TyphoonStoryboardResolver.m │ │ │ ├── TyphoonViewControllerFactory.h │ │ │ ├── TyphoonViewControllerFactory.m │ │ │ ├── TyphoonViewHelpers.h │ │ │ ├── TyphoonViewHelpers.m │ │ │ ├── UIResponder+TyphoonOutletTransfer.h │ │ │ ├── UIResponder+TyphoonOutletTransfer.m │ │ │ ├── UIView+TyphoonOutletTransfer.h │ │ │ └── UIView+TyphoonOutletTransfer.m │ │ ├── TypeConversion/ │ │ │ └── Converters/ │ │ │ ├── TyphoonBundledImageTypeConverter.h │ │ │ ├── TyphoonBundledImageTypeConverter.m │ │ │ ├── TyphoonUIColorTypeConverter.h │ │ │ └── TyphoonUIColorTypeConverter.m │ │ └── TyphooniOS.h │ └── YYModel/ │ ├── LICENSE │ ├── README.md │ └── YYModel/ │ ├── NSObject+YYModel.h │ ├── NSObject+YYModel.m │ ├── YYClassInfo.h │ ├── YYClassInfo.m │ └── YYModel.h ├── README.en.md ├── README.md ├── iOS-Network-Stack-Dive/ │ ├── ArchitectureExtensions/ │ │ ├── AOP/ │ │ │ ├── LoggingAspect/ │ │ │ │ ├── TJPAspectCore.h │ │ │ │ ├── TJPAspectCore.m │ │ │ │ ├── TJPLogAspectInterface.h │ │ │ │ ├── TJPLogModel.h │ │ │ │ ├── TJPLogModel.m │ │ │ │ ├── TJPLogger.h │ │ │ │ ├── TJPLogger.m │ │ │ │ ├── TJPLoggerManager.h │ │ │ │ └── TJPLoggerManager.m │ │ │ ├── TJPLoggerViewController.h │ │ │ └── TJPLoggerViewController.m │ │ ├── NetworkMonitor/ │ │ │ ├── Collector/ │ │ │ │ ├── TJPMetricsCollector.h │ │ │ │ └── TJPMetricsCollector.m │ │ │ ├── Metrics/ │ │ │ │ ├── TJPConcreteSession+TJPMetrics.h │ │ │ │ ├── TJPConcreteSession+TJPMetrics.m │ │ │ │ ├── TJPConnectStateMachine+TJPMetrics.h │ │ │ │ ├── TJPConnectStateMachine+TJPMetrics.m │ │ │ │ ├── TJPConnectionManager+TJPMetrics.h │ │ │ │ ├── TJPConnectionManager+TJPMetrics.m │ │ │ │ ├── TJPDynamicHeartbeat+TJPMetrics.h │ │ │ │ ├── TJPDynamicHeartbeat+TJPMetrics.m │ │ │ │ ├── TJPMessageParser+TJPMetrics.h │ │ │ │ └── TJPMessageParser+TJPMetrics.m │ │ │ ├── MetricsHeader/ │ │ │ │ ├── TJPMetricsKeys.h │ │ │ │ └── TJPMetricsKeys.m │ │ │ ├── Reporter/ │ │ │ │ ├── TJPMetricsConsoleReporter.h │ │ │ │ └── TJPMetricsConsoleReporter.m │ │ │ ├── TJPNetworkMonitorViewController.h │ │ │ ├── TJPNetworkMonitorViewController.m │ │ │ ├── TJPSessionPacketMonitor.h │ │ │ └── TJPSessionPacketMonitor.m │ │ ├── Utility/ │ │ │ ├── Category/ │ │ │ │ ├── UIImage+TJPImageOrientation.h │ │ │ │ └── UIImage+TJPImageOrientation.m │ │ │ └── TJPUtility/ │ │ │ ├── TJPFPSLabel.h │ │ │ └── TJPFPSLabel.m │ │ └── VIPER-Integration/ │ │ ├── CustomTableViewDemo/ │ │ │ ├── TJPCustomTableViewDemoViewController.h │ │ │ ├── TJPCustomTableViewDemoViewController.m │ │ │ ├── TJPSectionTableViewDemoViewController.h │ │ │ ├── TJPSectionTableViewDemoViewController.m │ │ │ └── TJPViperBaseTableView/ │ │ │ ├── Cell/ │ │ │ │ ├── TJPBaseTableViewCell.h │ │ │ │ └── TJPBaseTableViewCell.m │ │ │ ├── Loading/ │ │ │ │ ├── TJPDefaultLoadingAnimation.h │ │ │ │ └── TJPDefaultLoadingAnimation.m │ │ │ ├── Model/ │ │ │ │ ├── TJPBaseCellModel.h │ │ │ │ ├── TJPBaseCellModel.m │ │ │ │ ├── TJPBaseSectionModel.h │ │ │ │ └── TJPBaseSectionModel.m │ │ │ ├── Protocol/ │ │ │ │ ├── TJPBaseCellModelProtocol.h │ │ │ │ ├── TJPBaseSectionModelProtocol.h │ │ │ │ ├── TJPBaseTableViewCellProtocol.h │ │ │ │ └── TJPBaseTableViewLoadingProtocol.h │ │ │ └── View/ │ │ │ ├── TJPBaseTableView.h │ │ │ └── TJPBaseTableView.m │ │ ├── DIContainer/ │ │ │ ├── TJPViperModuleAssembly.h │ │ │ ├── TJPViperModuleAssembly.m │ │ │ └── TJPViperModuleProvider.h │ │ ├── VIPER-Architecture/ │ │ │ ├── Entity/ │ │ │ │ ├── TJPNavigationModel.h │ │ │ │ └── TJPNavigationModel.m │ │ │ ├── Error/ │ │ │ │ ├── TJPViperDefaultErrorHandler.h │ │ │ │ ├── TJPViperDefaultErrorHandler.m │ │ │ │ ├── TJPViperErrorDefine.h │ │ │ │ ├── TJPViperErrorHandlingStrategy.h │ │ │ │ └── TJPViperErrorHandlingStrategy.m │ │ │ ├── Handler/ │ │ │ │ ├── TJPViperErrorHandlerDelegate.h │ │ │ │ └── TJPViperErrorHandlerProtocol.h │ │ │ ├── Interactor/ │ │ │ │ ├── TJPViperBaseInteractorImpl.h │ │ │ │ ├── TJPViperBaseInteractorImpl.m │ │ │ │ └── TJPViperBaseInteractorProtocol.h │ │ │ ├── Presenter/ │ │ │ │ ├── TJPViperBasePresenterImpl.h │ │ │ │ ├── TJPViperBasePresenterImpl.m │ │ │ │ └── TJPViperBasePresenterProtocol.h │ │ │ ├── Response/ │ │ │ │ ├── TJPBaseResponse.h │ │ │ │ └── TJPBaseResponse.m │ │ │ ├── Router/ │ │ │ │ ├── TJPNavigationCoordinator.h │ │ │ │ ├── TJPNavigationCoordinator.m │ │ │ │ ├── TJPNavigationDefines.h │ │ │ │ ├── TJPViewPresentHandler.h │ │ │ │ ├── TJPViewPresentHandler.m │ │ │ │ ├── TJPViewPushHandler.h │ │ │ │ ├── TJPViewPushHandler.m │ │ │ │ ├── TJPViperBaseRouterHandlerProtocol.h │ │ │ │ ├── TJPViperBaseRouterImpl.h │ │ │ │ └── TJPViperBaseRouterImpl.m │ │ │ ├── StateMachine/ │ │ │ │ ├── TJPViewControllerStateMachine.h │ │ │ │ └── TJPViewControllerStateMachine.m │ │ │ └── View/ │ │ │ ├── TJPViperBaseTableViewController.h │ │ │ ├── TJPViperBaseTableViewController.m │ │ │ └── TJPViperBaseViewControllerProtocol.h │ │ └── VIPER-Demo/ │ │ └── Demo/ │ │ ├── Cell/ │ │ │ ├── TJPAdCell.h │ │ │ ├── TJPAdCell.m │ │ │ ├── TJPImageCell.h │ │ │ ├── TJPImageCell.m │ │ │ ├── TJPNewsCell.h │ │ │ ├── TJPNewsCell.m │ │ │ ├── TJPProductCell.h │ │ │ ├── TJPProductCell.m │ │ │ ├── TJPUserDynamicCell.h │ │ │ ├── TJPUserDynamicCell.m │ │ │ ├── TJPVideoCell.h │ │ │ └── TJPVideoCell.m │ │ ├── Entity/ │ │ │ ├── TJPAdCellModel.h │ │ │ ├── TJPAdCellModel.m │ │ │ ├── TJPImageCellModel.h │ │ │ ├── TJPImageCellModel.m │ │ │ ├── TJPNewsCellModel.h │ │ │ ├── TJPNewsCellModel.m │ │ │ ├── TJPProductCellModel.h │ │ │ ├── TJPProductCellModel.m │ │ │ ├── TJPUserDynamicCellModel.h │ │ │ ├── TJPUserDynamicCellModel.m │ │ │ ├── TJPVideoCellModel.h │ │ │ └── TJPVideoCellModel.m │ │ ├── Interactor/ │ │ │ ├── TJPVIPERDemoInteractorImpl.h │ │ │ └── TJPVIPERDemoInteractorImpl.m │ │ ├── Presenter/ │ │ │ ├── TJPVIPERDemoPresenter.h │ │ │ └── TJPVIPERDemoPresenter.m │ │ ├── Respone/ │ │ │ ├── TJPFeedResponse.h │ │ │ ├── TJPFeedResponse.m │ │ │ ├── TJPPaginationInfo.h │ │ │ └── TJPPaginationInfo.m │ │ ├── Router/ │ │ │ ├── TJPVIPERDemoRouter.h │ │ │ └── TJPVIPERDemoRouter.m │ │ └── View/ │ │ ├── DetailVC/ │ │ │ ├── TJPNewsDetailTableViewController.h │ │ │ └── TJPNewsDetailTableViewController.m │ │ ├── TJPLikeCommentAreaView.h │ │ ├── TJPLikeCommentAreaView.m │ │ ├── TJPNineGridImageView.h │ │ ├── TJPNineGridImageView.m │ │ ├── TJPVIPERDemoDetailViewController.h │ │ ├── TJPVIPERDemoDetailViewController.m │ │ ├── TJPVIPERDemoViewController.h │ │ └── TJPVIPERDemoViewController.m │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ ├── CoreNetworkStack/ │ │ ├── TJPIMCore/ │ │ │ ├── Bulider/ │ │ │ │ ├── TJPMessageBuilder.h │ │ │ │ └── TJPMessageBuilder.m │ │ │ ├── Connect/ │ │ │ │ ├── TJPConnectionManager.h │ │ │ │ └── TJPConnectionManager.m │ │ │ ├── Container/ │ │ │ │ ├── TJPConcreteSession.h │ │ │ │ ├── TJPConcreteSession.m │ │ │ │ ├── TJPLightweightSessionPool.h │ │ │ │ ├── TJPLightweightSessionPool.m │ │ │ │ ├── TJPNetworkCoordinator.h │ │ │ │ └── TJPNetworkCoordinator.m │ │ │ ├── Context/ │ │ │ │ ├── TJPMessageContext.h │ │ │ │ └── TJPMessageContext.m │ │ │ ├── Error/ │ │ │ │ ├── TJPErrorUtil.h │ │ │ │ ├── TJPErrorUtil.m │ │ │ │ ├── TJPNETError.h │ │ │ │ ├── TJPNETError.m │ │ │ │ ├── TJPNETErrorHandler.h │ │ │ │ └── TJPNETErrorHandler.m │ │ │ ├── Heartbeat/ │ │ │ │ ├── TJPDynamicHeartbeat.h │ │ │ │ └── TJPDynamicHeartbeat.m │ │ │ ├── IMClient/ │ │ │ │ ├── TJPIMClient.h │ │ │ │ └── TJPIMClient.m │ │ │ ├── Log/ │ │ │ │ ├── TJPLogManager.h │ │ │ │ └── TJPLogManager.m │ │ │ ├── Message/ │ │ │ │ ├── TJPMessageFactory.h │ │ │ │ ├── TJPMessageFactory.m │ │ │ │ ├── TJPMessageManager.h │ │ │ │ ├── TJPMessageManager.m │ │ │ │ ├── TJPMessageSerializer.h │ │ │ │ ├── TJPMessageSerializer.m │ │ │ │ ├── TJPTextMessage.h │ │ │ │ └── TJPTextMessage.m │ │ │ ├── Network/ │ │ │ │ ├── TJPNetworkCondition.h │ │ │ │ └── TJPNetworkCondition.m │ │ │ ├── NetworkUtility/ │ │ │ │ ├── TJPNetworkDefine.h │ │ │ │ └── TJPNetworkErrorDefine.h │ │ │ ├── Parser/ │ │ │ │ ├── TJPMessageParser.h │ │ │ │ ├── TJPMessageParser.m │ │ │ │ ├── TJPParsedPacket.h │ │ │ │ ├── TJPParsedPacket.m │ │ │ │ ├── TJPRingBuffer.h │ │ │ │ └── TJPRingBuffer.m │ │ │ ├── Policy/ │ │ │ │ ├── TJPReconnectPolicy.h │ │ │ │ └── TJPReconnectPolicy.m │ │ │ ├── Protocol/ │ │ │ │ ├── TJPConnectionDelegate.h │ │ │ │ ├── TJPHeartbeatProtocol.h │ │ │ │ ├── TJPMessageManagerDelegate.h │ │ │ │ ├── TJPMessageManagerNetworkDelegate.h │ │ │ │ ├── TJPMessageProtocol.h │ │ │ │ ├── TJPSessionDelegate.h │ │ │ │ └── TJPSessionProtocol.h │ │ │ ├── Sequence/ │ │ │ │ ├── TJPSequenceManager.h │ │ │ │ └── TJPSequenceManager.m │ │ │ ├── TCP-Machine/ │ │ │ │ ├── TJPConnectStateMachine.h │ │ │ │ ├── TJPConnectStateMachine.m │ │ │ │ ├── TJPMessageStateMachine.h │ │ │ │ └── TJPMessageStateMachine.m │ │ │ └── Utility/ │ │ │ ├── TJPCoreTypes.h │ │ │ ├── TJPNetworkConfig.h │ │ │ ├── TJPNetworkConfig.m │ │ │ ├── TJPNetworkUtil.h │ │ │ └── TJPNetworkUtil.m │ │ ├── TransportLayer/ │ │ │ └── Tests/ │ │ │ ├── TJPMockFinalVersionTCPServer.h │ │ │ ├── TJPMockFinalVersionTCPServer.m │ │ │ ├── TJPMockTCPServer.h │ │ │ └── TJPMockTCPServer.m │ │ ├── V1_BasicFunction/ │ │ │ ├── TJPNetworkManagerV1.h │ │ │ └── TJPNetworkManagerV1.m │ │ └── V2_Concurrency/ │ │ ├── TJPConcurrentNetworkManager.h │ │ ├── TJPConcurrentNetworkManager.m │ │ └── TJPNetworkProtocol.h │ ├── Delegate/ │ │ ├── AppDelegate.h │ │ └── AppDelegate.m │ ├── Docs/ │ │ ├── ArchitectureExtensions/ │ │ │ └── AspectLoggerDesign.md │ │ ├── CoreNetworkStackDoc/ │ │ │ ├── FinalNetworkTests.md │ │ │ ├── HeartbeatKeepaliveDesign.md │ │ │ ├── NetworkMonitorDesign.md │ │ │ ├── ProtocolParseDesign.md │ │ │ ├── TJPNetworkManagerV2Design.md │ │ │ ├── TJPNetworkV3FinalDesign.md │ │ │ └── 轻量级多路复用架构设计.md │ │ ├── VIPER-Integration/ │ │ │ ├── VIPER-Design.md │ │ │ └── VIPER-RouterGuide.md │ │ └── Version/ │ │ └── v1.2.0版本内容.md │ ├── HomeVC/ │ │ ├── HomeViewController.h │ │ └── HomeViewController.m │ ├── Info.plist │ ├── Labs/ │ │ └── NetworkFundamentals/ │ │ └── Lab-Socket-API/ │ │ ├── StickyPacketDemo/ │ │ │ ├── SocketChatClient.h │ │ │ ├── SocketChatClient.m │ │ │ ├── SocketChatServer.h │ │ │ ├── SocketChatServer.m │ │ │ ├── StickPacketDemoController.h │ │ │ └── StickPacketDemoController.m │ │ └── StickyPacketSolution/ │ │ ├── SolutionStickyPacketClient.h │ │ ├── SolutionStickyPacketClient.m │ │ ├── SolutionStickyPacketServer.h │ │ ├── SolutionStickyPacketServer.m │ │ ├── StickPacketSolutionController.h │ │ └── StickPacketSolutionController.m │ ├── ProductionBridge/ │ │ └── VIPER-Sample/ │ │ └── MessageModule/ │ │ ├── Entity/ │ │ │ ├── TJPChatMessage.h │ │ │ └── TJPChatMessage.m │ │ ├── Manager/ │ │ │ ├── TJPMessageTimeoutManager.h │ │ │ └── TJPMessageTimeoutManager.m │ │ ├── TJPChatViewController.h │ │ ├── TJPChatViewController.m │ │ ├── Utility/ │ │ │ └── TJPChatMessageDefine.h │ │ └── View/ │ │ ├── TJPChatInputView.h │ │ ├── TJPChatInputView.m │ │ ├── TJPChatMessageCell.h │ │ ├── TJPChatMessageCell.m │ │ ├── TJPConnectionStatusView.h │ │ ├── TJPConnectionStatusView.m │ │ ├── TJPMessageStatusIndicator.h │ │ └── TJPMessageStatusIndicator.m │ ├── Resources/ │ │ ├── feedData.json │ │ ├── feedData_page2.json │ │ └── feedData_page3.json │ ├── Tools/ │ │ ├── Color/ │ │ │ ├── UIColor+TJPColor.h │ │ │ └── UIColor+TJPColor.m │ │ ├── MJRefresh/ │ │ │ ├── Base/ │ │ │ │ ├── MJRefreshAutoFooter.h │ │ │ │ ├── MJRefreshAutoFooter.m │ │ │ │ ├── MJRefreshBackFooter.h │ │ │ │ ├── MJRefreshBackFooter.m │ │ │ │ ├── MJRefreshComponent.h │ │ │ │ ├── MJRefreshComponent.m │ │ │ │ ├── MJRefreshFooter.h │ │ │ │ ├── MJRefreshFooter.m │ │ │ │ ├── MJRefreshHeader.h │ │ │ │ └── MJRefreshHeader.m │ │ │ ├── Custom/ │ │ │ │ ├── Footer/ │ │ │ │ │ ├── Auto/ │ │ │ │ │ │ ├── MJRefreshAutoGifFooter.h │ │ │ │ │ │ ├── MJRefreshAutoGifFooter.m │ │ │ │ │ │ ├── MJRefreshAutoNormalFooter.h │ │ │ │ │ │ ├── MJRefreshAutoNormalFooter.m │ │ │ │ │ │ ├── MJRefreshAutoStateFooter.h │ │ │ │ │ │ └── MJRefreshAutoStateFooter.m │ │ │ │ │ └── Back/ │ │ │ │ │ ├── MJRefreshBackGifFooter.h │ │ │ │ │ ├── MJRefreshBackGifFooter.m │ │ │ │ │ ├── MJRefreshBackNormalFooter.h │ │ │ │ │ ├── MJRefreshBackNormalFooter.m │ │ │ │ │ ├── MJRefreshBackStateFooter.h │ │ │ │ │ └── MJRefreshBackStateFooter.m │ │ │ │ └── Header/ │ │ │ │ ├── MJRefreshGifHeader.h │ │ │ │ ├── MJRefreshGifHeader.m │ │ │ │ ├── MJRefreshNormalHeader.h │ │ │ │ ├── MJRefreshNormalHeader.m │ │ │ │ ├── MJRefreshStateHeader.h │ │ │ │ └── MJRefreshStateHeader.m │ │ │ ├── MJRefresh.bundle/ │ │ │ │ ├── en.lproj/ │ │ │ │ │ └── Localizable.strings │ │ │ │ ├── zh-Hans.lproj/ │ │ │ │ │ └── Localizable.strings │ │ │ │ └── zh-Hant.lproj/ │ │ │ │ └── Localizable.strings │ │ │ ├── MJRefresh.h │ │ │ ├── MJRefreshConst.h │ │ │ ├── MJRefreshConst.m │ │ │ ├── NSBundle+MJRefresh.h │ │ │ ├── NSBundle+MJRefresh.m │ │ │ ├── UIScrollView+MJExtension.h │ │ │ ├── UIScrollView+MJExtension.m │ │ │ ├── UIScrollView+MJRefresh.h │ │ │ ├── UIScrollView+MJRefresh.m │ │ │ ├── UIView+MJExtension.h │ │ │ └── UIView+MJExtension.m │ │ ├── TJPCache/ │ │ │ ├── TJPCacheManager.h │ │ │ ├── TJPCacheManager.m │ │ │ ├── TJPCacheProtocol.h │ │ │ ├── TJPDiskCache.h │ │ │ ├── TJPDiskCache.m │ │ │ ├── TJPMemoryCache.h │ │ │ └── TJPMemoryCache.m │ │ └── TJPUITools/ │ │ ├── TJPToast.h │ │ └── TJPToast.m │ └── main.m ├── iOS-Network-Stack-Dive.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ └── contents.xcworkspacedata │ ├── xcshareddata/ │ │ └── xcschemes/ │ │ └── iOS-Network-Stack-Dive.xcscheme │ └── xcuserdata/ │ └── aarongtang.xcuserdatad/ │ └── xcschemes/ │ └── xcschememanagement.plist ├── iOS-Network-Stack-Dive.xcworkspace/ │ ├── contents.xcworkspacedata │ └── xcuserdata/ │ └── aarongtang.xcuserdatad/ │ └── xcdebugger/ │ └── Breakpoints_v2.xcbkptlist └── iOS-Network-Stack-DiveTests/ ├── ArchitectureExtensions/ │ ├── AOP/ │ │ └── TJPLoggerTests.m │ ├── Network/ │ │ └── TJPConnectStateMachineMetricsTest.m │ └── VIPER/ │ └── TJPNavigationCoordinatorTests.m ├── CoreNetworkStack/ │ └── TransportLayer/ │ ├── V1_BasicFunction/ │ │ └── TJPNetworkManagerTests.m │ ├── V2_Concurrency/ │ │ └── TJPNetworkManagerV2Tests.m │ └── V3_FinalProduct/ │ ├── TJPConcreteSessionTests.m │ ├── TJPConnectStateMachineTests.m │ ├── TJPDynamicHeartbeatTests.m │ ├── TJPMessageContextTests.m │ ├── TJPMessageParserTests.m │ ├── TJPNetworkConditionTests.m │ ├── TJPNetworkCoordinatorTests.m │ ├── TJPReconnectPolicyTests.m │ ├── TJPSequenceManagerTests.m │ └── TJPTJPNetworkUtilTests.m └── iOS_Network_Stack_DiveTests.m ================================================ FILE CONTENTS ================================================ ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2025 [唐佳鹏 or AarongTang] Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Podfile ================================================ # Uncomment the next line to define a global platform for your project # platform :ios, '9.0' target 'iOS-Network-Stack-Dive' do # Comment the next line if you don't want to use dynamic frameworks use_frameworks! # Pods for iOS-Network-Stack-Dive pod 'CocoaAsyncSocket' pod 'Reachability', '3.7.5' # pod 'libffi-ios-latest', :git => 'https://github.com/codeacmen/libffi-ios-latest.git', :tag => '3.4.0-fix' pod 'ReactiveObjC' pod 'Typhoon' #UI pod # pod 'MJRefresh' pod 'DZNEmptyDataSet' pod 'Masonry' pod 'SDWebImage' # pod 'YYKit' pod 'YYModel' target 'iOS-Network-Stack-DiveTests' do inherit! :search_paths #Mock pod pod 'OCMock' end end ================================================ FILE: Pods/CocoaAsyncSocket/LICENSE.txt ================================================ This library is in the public domain. However, not all organizations are allowed to use such a license. For example, Germany doesn't recognize the Public Domain and one is not allowed to use libraries under such license (or similar). Thus, the library is now dual licensed, and one is allowed to choose which license they would like to use. ################################################## License Option #1 : ################################################## Public Domain ################################################## License Option #2 : ################################################## Software License Agreement (BSD License) Copyright (c) 2017, Deusty, LLC All rights reserved. Redistribution and use of this software in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Neither the name of Deusty LLC nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission of Deusty LLC. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Pods/CocoaAsyncSocket/README.markdown ================================================ # CocoaAsyncSocket [![Build Status](https://travis-ci.org/robbiehanson/CocoaAsyncSocket.svg?branch=master)](https://travis-ci.org/robbiehanson/CocoaAsyncSocket) [![Version Status](https://img.shields.io/cocoapods/v/CocoaAsyncSocket.svg?style=flat)](http://cocoadocs.org/docsets/CocoaAsyncSocket) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Platform](http://img.shields.io/cocoapods/p/CocoaAsyncSocket.svg?style=flat)](http://cocoapods.org/?q=CocoaAsyncSocket) [![license Public Domain](https://img.shields.io/badge/license-Public%20Domain-orange.svg?style=flat)](https://en.wikipedia.org/wiki/Public_domain) CocoaAsyncSocket provides easy-to-use and powerful asynchronous socket libraries for macOS, iOS, and tvOS. The classes are described below. ## Installation #### CocoaPods Install using [CocoaPods](https://cocoapods.org) by adding this line to your Podfile: ````ruby use_frameworks! # Add this if you are targeting iOS 8+ or using Swift pod 'CocoaAsyncSocket' ```` #### Carthage CocoaAsyncSocket is [Carthage](https://github.com/Carthage/Carthage) compatible. To include it add the following line to your `Cartfile` ```bash github "robbiehanson/CocoaAsyncSocket" "master" ``` The project is currently configured to build for **iOS**, **tvOS** and **Mac**. After building with carthage the resultant frameworks will be stored in: * `Carthage/Build/iOS/CocoaAsyncSocket.framework` * `Carthage/Build/tvOS/CocoaAsyncSocket.framework` * `Carthage/Build/Mac/CocoaAsyncSocket.framework` Select the correct framework(s) and drag it into your project. #### Swift Package Manager Simply add the package dependency to your Package.swift and depend on "CocoaAsyncSocket" in the necessary targets: ```swift dependencies: [ .package(url: "https://github.com/robbiehanson/CocoaAsyncSocket", from: "7.6.4") ] ``` #### Manual You can also include it into your project by adding the source files directly, but you should probably be using a dependency manager to keep up to date. ### Importing Using Objective-C: ```obj-c // When using Clang Modules: @import CocoaAsyncSocket; // or when not: #import "GCDAsyncSocket.h" // for TCP #import "GCDAsyncUdpSocket.h" // for UDP ``` Using Swift: ```swift import CocoaAsyncSocket ``` ## TCP **GCDAsyncSocket** is a TCP/IP socket networking library built atop Grand Central Dispatch. Here are the key features available: - Native Objective-C, fully self-contained in one class.
_No need to muck around with sockets or streams. This class handles everything for you._ - Full delegate support
_Errors, connections, read completions, write completions, progress, and disconnections all result in a call to your delegate method._ - Queued non-blocking reads and writes, with optional timeouts.
_You tell it what to read or write, and it handles everything for you. Queueing, buffering, and searching for termination sequences within the stream - all handled for you automatically._ - Automatic socket acceptance.
_Spin up a server socket, tell it to accept connections, and it will call you with new instances of itself for each connection._ - Support for TCP streams over IPv4 and IPv6.
_Automatically connect to IPv4 or IPv6 hosts. Automatically accept incoming connections over both IPv4 and IPv6 with a single instance of this class. No more worrying about multiple sockets._ - Support for TLS / SSL
_Secure your socket with ease using just a single method call. Available for both client and server sockets._ - Fully GCD based and Thread-Safe
_It runs entirely within its own GCD dispatch_queue, and is completely thread-safe. Further, the delegate methods are all invoked asynchronously onto a dispatch_queue of your choosing. This means parallel operation of your socket code, and your delegate/processing code._ ## UDP **GCDAsyncUdpSocket** is a UDP/IP socket networking library built atop Grand Central Dispatch. Here are the key features available: - Native Objective-C, fully self-contained in one class.
_No need to muck around with low-level sockets. This class handles everything for you._ - Full delegate support.
_Errors, send completions, receive completions, and disconnections all result in a call to your delegate method._ - Queued non-blocking send and receive operations, with optional timeouts.
_You tell it what to send or receive, and it handles everything for you. Queueing, buffering, waiting and checking errno - all handled for you automatically._ - Support for IPv4 and IPv6.
_Automatically send/recv using IPv4 and/or IPv6. No more worrying about multiple sockets._ - Fully GCD based and Thread-Safe
_It runs entirely within its own GCD dispatch_queue, and is completely thread-safe. Further, the delegate methods are all invoked asynchronously onto a dispatch_queue of your choosing. This means parallel operation of your socket code, and your delegate/processing code._ *** For those new(ish) to networking, it's recommended you **[read the wiki](https://github.com/robbiehanson/CocoaAsyncSocket/wiki)**.
_Sockets might not work exactly like you think they do..._ **Still got questions?** Try the **[CocoaAsyncSocket Mailing List](https://groups.google.com/group/cocoaasyncsocket)**. *** Love the project? Wanna buy me a ☕️  ? (or a 🍺  😀 ): [![donation-bitcoin](https://bitpay.com/img/donate-sm.png)](https://onename.com/robbiehanson) [![donation-paypal](https://www.paypal.com/en_US/i/btn/btn_donate_SM.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=2M8C699FQ8AW2) ================================================ FILE: Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.h ================================================ // // GCDAsyncSocket.h // // This class is in the public domain. // Originally created by Robbie Hanson in Q3 2010. // Updated and maintained by Deusty LLC and the Apple development community. // // https://github.com/robbiehanson/CocoaAsyncSocket // #import #import #import #import #import #include // AF_INET, AF_INET6 @class GCDAsyncReadPacket; @class GCDAsyncWritePacket; @class GCDAsyncSocketPreBuffer; @protocol GCDAsyncSocketDelegate; NS_ASSUME_NONNULL_BEGIN extern NSString *const GCDAsyncSocketException; extern NSString *const GCDAsyncSocketErrorDomain; extern NSString *const GCDAsyncSocketQueueName; extern NSString *const GCDAsyncSocketThreadName; extern NSString *const GCDAsyncSocketManuallyEvaluateTrust; #if TARGET_OS_IPHONE extern NSString *const GCDAsyncSocketUseCFStreamForTLS; #endif #define GCDAsyncSocketSSLPeerName (NSString *)kCFStreamSSLPeerName #define GCDAsyncSocketSSLCertificates (NSString *)kCFStreamSSLCertificates #define GCDAsyncSocketSSLIsServer (NSString *)kCFStreamSSLIsServer extern NSString *const GCDAsyncSocketSSLPeerID; extern NSString *const GCDAsyncSocketSSLProtocolVersionMin; extern NSString *const GCDAsyncSocketSSLProtocolVersionMax; extern NSString *const GCDAsyncSocketSSLSessionOptionFalseStart; extern NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord; extern NSString *const GCDAsyncSocketSSLCipherSuites; extern NSString *const GCDAsyncSocketSSLALPN; #if !TARGET_OS_IPHONE extern NSString *const GCDAsyncSocketSSLDiffieHellmanParameters; #endif #define GCDAsyncSocketLoggingContext 65535 typedef NS_ERROR_ENUM(GCDAsyncSocketErrorDomain, GCDAsyncSocketError) { GCDAsyncSocketNoError = 0, // Never used GCDAsyncSocketBadConfigError, // Invalid configuration GCDAsyncSocketBadParamError, // Invalid parameter was passed GCDAsyncSocketConnectTimeoutError, // A connect operation timed out GCDAsyncSocketReadTimeoutError, // A read operation timed out GCDAsyncSocketWriteTimeoutError, // A write operation timed out GCDAsyncSocketReadMaxedOutError, // Reached set maxLength without completing GCDAsyncSocketClosedError, // The remote peer closed the connection GCDAsyncSocketOtherError, // Description provided in userInfo }; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @interface GCDAsyncSocket : NSObject /** * GCDAsyncSocket uses the standard delegate paradigm, * but executes all delegate callbacks on a given delegate dispatch queue. * This allows for maximum concurrency, while at the same time providing easy thread safety. * * You MUST set a delegate AND delegate dispatch queue before attempting to * use the socket, or you will get an error. * * The socket queue is optional. * If you pass NULL, GCDAsyncSocket will automatically create it's own socket queue. * If you choose to provide a socket queue, the socket queue must not be a concurrent queue. * If you choose to provide a socket queue, and the socket queue has a configured target queue, * then please see the discussion for the method markSocketQueueTargetQueue. * * The delegate queue and socket queue can optionally be the same. **/ - (instancetype)init; - (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; - (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; - (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq NS_DESIGNATED_INITIALIZER; /** * Create GCDAsyncSocket from already connect BSD socket file descriptor **/ + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD socketQueue:(nullable dispatch_queue_t)sq error:(NSError**)error; + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq error:(NSError**)error; + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq error:(NSError **)error; #pragma mark Configuration @property (atomic, weak, readwrite, nullable) id delegate; #if OS_OBJECT_USE_OBJC @property (atomic, strong, readwrite, nullable) dispatch_queue_t delegateQueue; #else @property (atomic, assign, readwrite, nullable) dispatch_queue_t delegateQueue; #endif - (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; - (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; /** * If you are setting the delegate to nil within the delegate's dealloc method, * you may need to use the synchronous versions below. **/ - (void)synchronouslySetDelegate:(nullable id)delegate; - (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; - (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; /** * By default, both IPv4 and IPv6 are enabled. * * For accepting incoming connections, this means GCDAsyncSocket automatically supports both protocols, * and can simulataneously accept incoming connections on either protocol. * * For outgoing connections, this means GCDAsyncSocket can connect to remote hosts running either protocol. * If a DNS lookup returns only IPv4 results, GCDAsyncSocket will automatically use IPv4. * If a DNS lookup returns only IPv6 results, GCDAsyncSocket will automatically use IPv6. * If a DNS lookup returns both IPv4 and IPv6 results, the preferred protocol will be chosen. * By default, the preferred protocol is IPv4, but may be configured as desired. **/ @property (atomic, assign, readwrite, getter=isIPv4Enabled) BOOL IPv4Enabled; @property (atomic, assign, readwrite, getter=isIPv6Enabled) BOOL IPv6Enabled; @property (atomic, assign, readwrite, getter=isIPv4PreferredOverIPv6) BOOL IPv4PreferredOverIPv6; /** * When connecting to both IPv4 and IPv6 using Happy Eyeballs (RFC 6555) https://tools.ietf.org/html/rfc6555 * this is the delay between connecting to the preferred protocol and the fallback protocol. * * Defaults to 300ms. **/ @property (atomic, assign, readwrite) NSTimeInterval alternateAddressDelay; /** * User data allows you to associate arbitrary information with the socket. * This data is not used internally by socket in any way. **/ @property (atomic, strong, readwrite, nullable) id userData; #pragma mark Accepting /** * Tells the socket to begin listening and accepting connections on the given port. * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, * and the socket:didAcceptNewSocket: delegate method will be invoked. * * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) **/ - (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr; /** * This method is the same as acceptOnPort:error: with the * additional option of specifying which interface to listen on. * * For example, you could specify that the socket should only accept connections over ethernet, * and not other interfaces such as wifi. * * The interface may be specified by name (e.g. "en1" or "lo0") or by IP address (e.g. "192.168.4.34"). * You may also use the special strings "localhost" or "loopback" to specify that * the socket only accept connections from the local machine. * * You can see the list of interfaces via the command line utility "ifconfig", * or programmatically via the getifaddrs() function. * * To accept connections on any interface pass nil, or simply use the acceptOnPort:error: method. **/ - (BOOL)acceptOnInterface:(nullable NSString *)interface port:(uint16_t)port error:(NSError **)errPtr; /** * Tells the socket to begin listening and accepting connections on the unix domain at the given url. * When a connection is accepted, a new instance of GCDAsyncSocket will be spawned to handle it, * and the socket:didAcceptNewSocket: delegate method will be invoked. * * The socket will listen on all available interfaces (e.g. wifi, ethernet, etc) **/ - (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr; #pragma mark Connecting /** * Connects to the given host and port. * * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: * and uses the default interface, and no timeout. **/ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; /** * Connects to the given host and port with an optional timeout. * * This method invokes connectToHost:onPort:viaInterface:withTimeout:error: and uses the default interface. **/ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; /** * Connects to the given host & port, via the optional interface, with an optional timeout. * * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). * The host may also be the special strings "localhost" or "loopback" to specify connecting * to a service on the local machine. * * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). * The interface may also be used to specify the local port (see below). * * To not time out use a negative time interval. * * This method will return NO if an error is detected, and set the error pointer (if one was given). * Possible errors would be a nil host, invalid interface, or socket is already connected. * * If no errors are detected, this method will start a background connect operation and immediately return YES. * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. * * Since this class supports queued reads and writes, you can immediately start reading and/or writing. * All read/write operations will be queued, and upon socket connection, * the operations will be dequeued and processed in order. * * The interface may optionally contain a port number at the end of the string, separated by a colon. * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". * To specify only local port: ":8082". * Please note this is an advanced feature, and is somewhat hidden on purpose. * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. * Local ports do NOT need to match remote ports. In fact, they almost never do. * This feature is here for networking professionals using very advanced techniques. **/ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port viaInterface:(nullable NSString *)interface withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; /** * Connects to the given address, specified as a sockaddr structure wrapped in a NSData object. * For example, a NSData object returned from NSNetService's addresses method. * * If you have an existing struct sockaddr you can convert it to a NSData object like so: * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; * * This method invokes connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr. **/ - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; /** * This method is the same as connectToAddress:error: with an additional timeout option. * To not time out use a negative time interval, or simply use the connectToAddress:error: method. **/ - (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; /** * Connects to the given address, using the specified interface and timeout. * * The address is specified as a sockaddr structure wrapped in a NSData object. * For example, a NSData object returned from NSNetService's addresses method. * * If you have an existing struct sockaddr you can convert it to a NSData object like so: * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; * * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). * The interface may also be used to specify the local port (see below). * * The timeout is optional. To not time out use a negative time interval. * * This method will return NO if an error is detected, and set the error pointer (if one was given). * Possible errors would be a nil host, invalid interface, or socket is already connected. * * If no errors are detected, this method will start a background connect operation and immediately return YES. * The delegate callbacks are used to notify you when the socket connects, or if the host was unreachable. * * Since this class supports queued reads and writes, you can immediately start reading and/or writing. * All read/write operations will be queued, and upon socket connection, * the operations will be dequeued and processed in order. * * The interface may optionally contain a port number at the end of the string, separated by a colon. * This allows you to specify the local port that should be used for the outgoing connection. (read paragraph to end) * To specify both interface and local port: "en1:8082" or "192.168.4.35:2424". * To specify only local port: ":8082". * Please note this is an advanced feature, and is somewhat hidden on purpose. * You should understand that 99.999% of the time you should NOT specify the local port for an outgoing connection. * If you think you need to, there is a very good chance you have a fundamental misunderstanding somewhere. * Local ports do NOT need to match remote ports. In fact, they almost never do. * This feature is here for networking professionals using very advanced techniques. **/ - (BOOL)connectToAddress:(NSData *)remoteAddr viaInterface:(nullable NSString *)interface withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; /** * Connects to the unix domain socket at the given url, using the specified timeout. */ - (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr; /** * Iterates over the given NetService's addresses in order, and invokes connectToAddress:error:. Stops at the * first invocation that succeeds and returns YES; otherwise returns NO. */ - (BOOL)connectToNetService:(NSNetService *)netService error:(NSError **)errPtr; #pragma mark Disconnecting /** * Disconnects immediately (synchronously). Any pending reads or writes are dropped. * * If the socket is not already disconnected, an invocation to the socketDidDisconnect:withError: delegate method * will be queued onto the delegateQueue asynchronously (behind any previously queued delegate methods). * In other words, the disconnected delegate method will be invoked sometime shortly after this method returns. * * Please note the recommended way of releasing a GCDAsyncSocket instance (e.g. in a dealloc method) * [asyncSocket setDelegate:nil]; * [asyncSocket disconnect]; * [asyncSocket release]; * * If you plan on disconnecting the socket, and then immediately asking it to connect again, * you'll likely want to do so like this: * [asyncSocket setDelegate:nil]; * [asyncSocket disconnect]; * [asyncSocket setDelegate:self]; * [asyncSocket connect...]; **/ - (void)disconnect; /** * Disconnects after all pending reads have completed. * After calling this, the read and write methods will do nothing. * The socket will disconnect even if there are still pending writes. **/ - (void)disconnectAfterReading; /** * Disconnects after all pending writes have completed. * After calling this, the read and write methods will do nothing. * The socket will disconnect even if there are still pending reads. **/ - (void)disconnectAfterWriting; /** * Disconnects after all pending reads and writes have completed. * After calling this, the read and write methods will do nothing. **/ - (void)disconnectAfterReadingAndWriting; #pragma mark Diagnostics /** * Returns whether the socket is disconnected or connected. * * A disconnected socket may be recycled. * That is, it can be used again for connecting or listening. * * If a socket is in the process of connecting, it may be neither disconnected nor connected. **/ @property (atomic, readonly) BOOL isDisconnected; @property (atomic, readonly) BOOL isConnected; /** * Returns the local or remote host and port to which this socket is connected, or nil and 0 if not connected. * The host will be an IP address. **/ @property (atomic, readonly, nullable) NSString *connectedHost; @property (atomic, readonly) uint16_t connectedPort; @property (atomic, readonly, nullable) NSURL *connectedUrl; @property (atomic, readonly, nullable) NSString *localHost; @property (atomic, readonly) uint16_t localPort; /** * Returns the local or remote address to which this socket is connected, * specified as a sockaddr structure wrapped in a NSData object. * * @seealso connectedHost * @seealso connectedPort * @seealso localHost * @seealso localPort **/ @property (atomic, readonly, nullable) NSData *connectedAddress; @property (atomic, readonly, nullable) NSData *localAddress; /** * Returns whether the socket is IPv4 or IPv6. * An accepting socket may be both. **/ @property (atomic, readonly) BOOL isIPv4; @property (atomic, readonly) BOOL isIPv6; /** * Returns whether or not the socket has been secured via SSL/TLS. * * See also the startTLS method. **/ @property (atomic, readonly) BOOL isSecure; #pragma mark Reading // The readData and writeData methods won't block (they are asynchronous). // // When a read is complete the socket:didReadData:withTag: delegate method is dispatched on the delegateQueue. // When a write is complete the socket:didWriteDataWithTag: delegate method is dispatched on the delegateQueue. // // You may optionally set a timeout for any read/write operation. (To not timeout, use a negative time interval.) // If a read/write opertion times out, the corresponding "socket:shouldTimeout..." delegate method // is called to optionally allow you to extend the timeout. // Upon a timeout, the "socket:didDisconnectWithError:" method is called // // The tag is for your convenience. // You can use it as an array index, step number, state id, pointer, etc. /** * Reads the first available bytes that become available on the socket. * * If the timeout value is negative, the read operation will not use a timeout. **/ - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Reads the first available bytes that become available on the socket. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer is nil, the socket will create a buffer for you. * * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing, and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while the socket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. **/ - (void)readDataWithTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag; /** * Reads the first available bytes that become available on the socket. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * A maximum of length bytes will be read. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer is nil, a buffer will automatically be created for you. * If maxLength is zero, no length restriction is enforced. * * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing, and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while the socket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. **/ - (void)readDataWithTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)length tag:(long)tag; /** * Reads the given number of bytes. * * If the timeout value is negative, the read operation will not use a timeout. * * If the length is 0, this method does nothing and the delegate is not called. **/ - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Reads the given number of bytes. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer is nil, a buffer will automatically be created for you. * * If the length is 0, this method does nothing and the delegate is not called. * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing, and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while AsyncSocket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. **/ - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag; /** * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. * * If the timeout value is negative, the read operation will not use a timeout. * * If you pass nil or zero-length data as the "data" parameter, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. * If you're developing your own custom protocol, be sure your separator can not occur naturally as * part of the data between separators. * For example, imagine you want to send several small documents over a socket. * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. * In this particular example, it would be better to use a protocol similar to HTTP with * a header that includes the length of the document. * Also be careful that your separator cannot occur naturally as part of the encoding for a character. * * The given data (separator) parameter should be immutable. * For performance reasons, the socket will retain it, not copy it. * So if it is immutable, don't modify it while the socket is using it. **/ - (void)readDataToData:(nullable NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer is nil, a buffer will automatically be created for you. * * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while the socket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. * * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. * If you're developing your own custom protocol, be sure your separator can not occur naturally as * part of the data between separators. * For example, imagine you want to send several small documents over a socket. * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. * In this particular example, it would be better to use a protocol similar to HTTP with * a header that includes the length of the document. * Also be careful that your separator cannot occur naturally as part of the encoding for a character. * * The given data (separator) parameter should be immutable. * For performance reasons, the socket will retain it, not copy it. * So if it is immutable, don't modify it while the socket is using it. **/ - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag; /** * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. * * If the timeout value is negative, the read operation will not use a timeout. * * If maxLength is zero, no length restriction is enforced. * Otherwise if maxLength bytes are read without completing the read, * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. * * If you pass nil or zero-length data as the "data" parameter, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * If you pass a maxLength parameter that is less than the length of the data parameter, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. * If you're developing your own custom protocol, be sure your separator can not occur naturally as * part of the data between separators. * For example, imagine you want to send several small documents over a socket. * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. * In this particular example, it would be better to use a protocol similar to HTTP with * a header that includes the length of the document. * Also be careful that your separator cannot occur naturally as part of the encoding for a character. * * The given data (separator) parameter should be immutable. * For performance reasons, the socket will retain it, not copy it. * So if it is immutable, don't modify it while the socket is using it. **/ - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag; /** * Reads bytes until (and including) the passed "data" parameter, which acts as a separator. * The bytes will be appended to the given byte buffer starting at the given offset. * The given buffer will automatically be increased in size if needed. * * If the timeout value is negative, the read operation will not use a timeout. * If the buffer is nil, a buffer will automatically be created for you. * * If maxLength is zero, no length restriction is enforced. * Otherwise if maxLength bytes are read without completing the read, * it is treated similarly to a timeout - the socket is closed with a GCDAsyncSocketReadMaxedOutError. * The read will complete successfully if exactly maxLength bytes are read and the given data is found at the end. * * If you pass a maxLength parameter that is less than the length of the data (separator) parameter, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * If the bufferOffset is greater than the length of the given buffer, * the method will do nothing (except maybe print a warning), and the delegate will not be called. * * If you pass a buffer, you must not alter it in any way while the socket is using it. * After completion, the data returned in socket:didReadData:withTag: will be a subset of the given buffer. * That is, it will reference the bytes that were appended to the given buffer via * the method [NSData dataWithBytesNoCopy:length:freeWhenDone:NO]. * * To read a line from the socket, use the line separator (e.g. CRLF for HTTP, see below) as the "data" parameter. * If you're developing your own custom protocol, be sure your separator can not occur naturally as * part of the data between separators. * For example, imagine you want to send several small documents over a socket. * Using CRLF as a separator is likely unwise, as a CRLF could easily exist within the documents. * In this particular example, it would be better to use a protocol similar to HTTP with * a header that includes the length of the document. * Also be careful that your separator cannot occur naturally as part of the encoding for a character. * * The given data (separator) parameter should be immutable. * For performance reasons, the socket will retain it, not copy it. * So if it is immutable, don't modify it while the socket is using it. **/ - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(nullable NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)length tag:(long)tag; /** * Returns progress of the current read, from 0.0 to 1.0, or NaN if no current read (use isnan() to check). * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. **/ - (float)progressOfReadReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; #pragma mark Writing /** * Writes data to the socket, and calls the delegate when finished. * * If you pass in nil or zero-length data, this method does nothing and the delegate will not be called. * If the timeout value is negative, the write operation will not use a timeout. * * Thread-Safety Note: * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while * the socket is writing it. In other words, it's not safe to alter the data until after the delegate method * socket:didWriteDataWithTag: is invoked signifying that this particular write operation has completed. * This is due to the fact that GCDAsyncSocket does NOT copy the data. It simply retains it. * This is for performance reasons. Often times, if NSMutableData is passed, it is because * a request/response was built up in memory. Copying this data adds an unwanted/unneeded overhead. * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket * completes writing the bytes (which is NOT immediately after this method returns, but rather at a later time * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. **/ - (void)writeData:(nullable NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Returns progress of the current write, from 0.0 to 1.0, or NaN if no current write (use isnan() to check). * The parameters "tag", "done" and "total" will be filled in if they aren't NULL. **/ - (float)progressOfWriteReturningTag:(nullable long *)tagPtr bytesDone:(nullable NSUInteger *)donePtr total:(nullable NSUInteger *)totalPtr; #pragma mark Security /** * Secures the connection using SSL/TLS. * * This method may be called at any time, and the TLS handshake will occur after all pending reads and writes * are finished. This allows one the option of sending a protocol dependent StartTLS message, and queuing * the upgrade to TLS at the same time, without having to wait for the write to finish. * Any reads or writes scheduled after this method is called will occur over the secured connection. * * ==== The available TOP-LEVEL KEYS are: * * - GCDAsyncSocketManuallyEvaluateTrust * The value must be of type NSNumber, encapsulating a BOOL value. * If you set this to YES, then the underlying SecureTransport system will not evaluate the SecTrustRef of the peer. * Instead it will pause at the moment evaulation would typically occur, * and allow us to handle the security evaluation however we see fit. * So GCDAsyncSocket will invoke the delegate method socket:shouldTrustPeer: passing the SecTrustRef. * * Note that if you set this option, then all other configuration keys are ignored. * Evaluation will be completely up to you during the socket:didReceiveTrust:completionHandler: delegate method. * * For more information on trust evaluation see: * Apple's Technical Note TN2232 - HTTPS Server Trust Evaluation * https://developer.apple.com/library/ios/technotes/tn2232/_index.html * * If unspecified, the default value is NO. * * - GCDAsyncSocketUseCFStreamForTLS (iOS only) * The value must be of type NSNumber, encapsulating a BOOL value. * By default GCDAsyncSocket will use the SecureTransport layer to perform encryption. * This gives us more control over the security protocol (many more configuration options), * plus it allows us to optimize things like sys calls and buffer allocation. * * However, if you absolutely must, you can instruct GCDAsyncSocket to use the old-fashioned encryption * technique by going through the CFStream instead. So instead of using SecureTransport, GCDAsyncSocket * will instead setup a CFRead/CFWriteStream. And then set the kCFStreamPropertySSLSettings property * (via CFReadStreamSetProperty / CFWriteStreamSetProperty) and will pass the given options to this method. * * Thus all the other keys in the given dictionary will be ignored by GCDAsyncSocket, * and will passed directly CFReadStreamSetProperty / CFWriteStreamSetProperty. * For more infomation on these keys, please see the documentation for kCFStreamPropertySSLSettings. * * If unspecified, the default value is NO. * * ==== The available CONFIGURATION KEYS are: * * - kCFStreamSSLPeerName * The value must be of type NSString. * It should match the name in the X.509 certificate given by the remote party. * See Apple's documentation for SSLSetPeerDomainName. * * - kCFStreamSSLCertificates * The value must be of type NSArray. * See Apple's documentation for SSLSetCertificate. * * - kCFStreamSSLIsServer * The value must be of type NSNumber, encapsulationg a BOOL value. * See Apple's documentation for SSLCreateContext for iOS. * This is optional for iOS. If not supplied, a NO value is the default. * This is not needed for Mac OS X, and the value is ignored. * * - GCDAsyncSocketSSLPeerID * The value must be of type NSData. * You must set this value if you want to use TLS session resumption. * See Apple's documentation for SSLSetPeerID. * * - GCDAsyncSocketSSLProtocolVersionMin * - GCDAsyncSocketSSLProtocolVersionMax * The value(s) must be of type NSNumber, encapsulting a SSLProtocol value. * See Apple's documentation for SSLSetProtocolVersionMin & SSLSetProtocolVersionMax. * See also the SSLProtocol typedef. * * - GCDAsyncSocketSSLSessionOptionFalseStart * The value must be of type NSNumber, encapsulating a BOOL value. * See Apple's documentation for kSSLSessionOptionFalseStart. * * - GCDAsyncSocketSSLSessionOptionSendOneByteRecord * The value must be of type NSNumber, encapsulating a BOOL value. * See Apple's documentation for kSSLSessionOptionSendOneByteRecord. * * - GCDAsyncSocketSSLCipherSuites * The values must be of type NSArray. * Each item within the array must be a NSNumber, encapsulating an SSLCipherSuite. * See Apple's documentation for SSLSetEnabledCiphers. * See also the SSLCipherSuite typedef. * * - GCDAsyncSocketSSLDiffieHellmanParameters (Mac OS X only) * The value must be of type NSData. * See Apple's documentation for SSLSetDiffieHellmanParams. * * ==== The following UNAVAILABLE KEYS are: (with throw an exception) * * - kCFStreamSSLAllowsAnyRoot (UNAVAILABLE) * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). * Corresponding deprecated method: SSLSetAllowsAnyRoot * * - kCFStreamSSLAllowsExpiredRoots (UNAVAILABLE) * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). * Corresponding deprecated method: SSLSetAllowsExpiredRoots * * - kCFStreamSSLAllowsExpiredCertificates (UNAVAILABLE) * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). * Corresponding deprecated method: SSLSetAllowsExpiredCerts * * - kCFStreamSSLValidatesCertificateChain (UNAVAILABLE) * You MUST use manual trust evaluation instead (see GCDAsyncSocketManuallyEvaluateTrust). * Corresponding deprecated method: SSLSetEnableCertVerify * * - kCFStreamSSLLevel (UNAVAILABLE) * You MUST use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMin instead. * Corresponding deprecated method: SSLSetProtocolVersionEnabled * * * Please refer to Apple's documentation for corresponding SSLFunctions. * * If you pass in nil or an empty dictionary, the default settings will be used. * * IMPORTANT SECURITY NOTE: * The default settings will check to make sure the remote party's certificate is signed by a * trusted 3rd party certificate agency (e.g. verisign) and that the certificate is not expired. * However it will not verify the name on the certificate unless you * give it a name to verify against via the kCFStreamSSLPeerName key. * The security implications of this are important to understand. * Imagine you are attempting to create a secure connection to MySecureServer.com, * but your socket gets directed to MaliciousServer.com because of a hacked DNS server. * If you simply use the default settings, and MaliciousServer.com has a valid certificate, * the default settings will not detect any problems since the certificate is valid. * To properly secure your connection in this particular scenario you * should set the kCFStreamSSLPeerName property to "MySecureServer.com". * * You can also perform additional validation in socketDidSecure. **/ - (void)startTLS:(nullable NSDictionary *)tlsSettings; #pragma mark Advanced /** * Traditionally sockets are not closed until the conversation is over. * However, it is technically possible for the remote enpoint to close its write stream. * Our socket would then be notified that there is no more data to be read, * but our socket would still be writeable and the remote endpoint could continue to receive our data. * * The argument for this confusing functionality stems from the idea that a client could shut down its * write stream after sending a request to the server, thus notifying the server there are to be no further requests. * In practice, however, this technique did little to help server developers. * * To make matters worse, from a TCP perspective there is no way to tell the difference from a read stream close * and a full socket close. They both result in the TCP stack receiving a FIN packet. The only way to tell * is by continuing to write to the socket. If it was only a read stream close, then writes will continue to work. * Otherwise an error will be occur shortly (when the remote end sends us a RST packet). * * In addition to the technical challenges and confusion, many high level socket/stream API's provide * no support for dealing with the problem. If the read stream is closed, the API immediately declares the * socket to be closed, and shuts down the write stream as well. In fact, this is what Apple's CFStream API does. * It might sound like poor design at first, but in fact it simplifies development. * * The vast majority of the time if the read stream is closed it's because the remote endpoint closed its socket. * Thus it actually makes sense to close the socket at this point. * And in fact this is what most networking developers want and expect to happen. * However, if you are writing a server that interacts with a plethora of clients, * you might encounter a client that uses the discouraged technique of shutting down its write stream. * If this is the case, you can set this property to NO, * and make use of the socketDidCloseReadStream delegate method. * * The default value is YES. **/ @property (atomic, assign, readwrite) BOOL autoDisconnectOnClosedReadStream; /** * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. * In most cases, the instance creates this queue itself. * However, to allow for maximum flexibility, the internal queue may be passed in the init method. * This allows for some advanced options such as controlling socket priority via target queues. * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. * * For example, imagine there are 2 queues: * dispatch_queue_t socketQueue; * dispatch_queue_t socketTargetQueue; * * If you do this (pseudo-code): * socketQueue.targetQueue = socketTargetQueue; * * Then all socketQueue operations will actually get run on the given socketTargetQueue. * This is fine and works great in most situations. * But if you run code directly from within the socketTargetQueue that accesses the socket, * you could potentially get deadlock. Imagine the following code: * * - (BOOL)socketHasSomething * { * __block BOOL result = NO; * dispatch_block_t block = ^{ * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; * } * if (is_executing_on_queue(socketQueue)) * block(); * else * dispatch_sync(socketQueue, block); * * return result; * } * * What happens if you call this method from the socketTargetQueue? The result is deadlock. * This is because the GCD API offers no mechanism to discover a queue's targetQueue. * Thus we have no idea if our socketQueue is configured with a targetQueue. * If we had this information, we could easily avoid deadlock. * But, since these API's are missing or unfeasible, you'll have to explicitly set it. * * IF you pass a socketQueue via the init method, * AND you've configured the passed socketQueue with a targetQueue, * THEN you should pass the end queue in the target hierarchy. * * For example, consider the following queue hierarchy: * socketQueue -> ipQueue -> moduleQueue * * This example demonstrates priority shaping within some server. * All incoming client connections from the same IP address are executed on the same target queue. * And all connections for a particular module are executed on the same target queue. * Thus, the priority of all networking for the entire module can be changed on the fly. * Additionally, networking traffic from a single IP cannot monopolize the module. * * Here's how you would accomplish something like that: * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock * { * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; * * dispatch_set_target_queue(socketQueue, ipQueue); * dispatch_set_target_queue(iqQueue, moduleQueue); * * return socketQueue; * } * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket * { * [clientConnections addObject:newSocket]; * [newSocket markSocketQueueTargetQueue:moduleQueue]; * } * * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. * This is often NOT the case, as such queues are used solely for execution shaping. **/ - (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; - (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; /** * It's not thread-safe to access certain variables from outside the socket's internal queue. * * For example, the socket file descriptor. * File descriptors are simply integers which reference an index in the per-process file table. * However, when one requests a new file descriptor (by opening a file or socket), * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. * So if we're not careful, the following could be possible: * * - Thread A invokes a method which returns the socket's file descriptor. * - The socket is closed via the socket's internal queue on thread B. * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. * - Thread A is now accessing/altering the file instead of the socket. * * In addition to this, other variables are not actually objects, * and thus cannot be retained/released or even autoreleased. * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. * * Although there are internal variables that make it difficult to maintain thread-safety, * it is important to provide access to these variables * to ensure this class can be used in a wide array of environments. * This method helps to accomplish this by invoking the current block on the socket's internal queue. * The methods below can be invoked from within the block to access * those generally thread-unsafe internal variables in a thread-safe manner. * The given block will be invoked synchronously on the socket's internal queue. * * If you save references to any protected variables and use them outside the block, you do so at your own peril. **/ - (void)performBlock:(dispatch_block_t)block; /** * These methods are only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Provides access to the socket's file descriptor(s). * If the socket is a server socket (is accepting incoming connections), * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. **/ - (int)socketFD; - (int)socket4FD; - (int)socket6FD; #if TARGET_OS_IPHONE /** * These methods are only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Provides access to the socket's internal CFReadStream/CFWriteStream. * * These streams are only used as workarounds for specific iOS shortcomings: * * - Apple has decided to keep the SecureTransport framework private is iOS. * This means the only supplied way to do SSL/TLS is via CFStream or some other API layered on top of it. * Thus, in order to provide SSL/TLS support on iOS we are forced to rely on CFStream, * instead of the preferred and faster and more powerful SecureTransport. * * - If a socket doesn't have backgrounding enabled, and that socket is closed while the app is backgrounded, * Apple only bothers to notify us via the CFStream API. * The faster and more powerful GCD API isn't notified properly in this case. * * See also: (BOOL)enableBackgroundingOnSocket **/ - (nullable CFReadStreamRef)readStream; - (nullable CFWriteStreamRef)writeStream; /** * This method is only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Configures the socket to allow it to operate when the iOS application has been backgrounded. * In other words, this method creates a read & write stream, and invokes: * * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); * * Returns YES if successful, NO otherwise. * * Note: Apple does not officially support backgrounding server sockets. * That is, if your socket is accepting incoming connections, Apple does not officially support * allowing iOS applications to accept incoming connections while an app is backgrounded. * * Example usage: * * - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port * { * [asyncSocket performBlock:^{ * [asyncSocket enableBackgroundingOnSocket]; * }]; * } **/ - (BOOL)enableBackgroundingOnSocket; #endif /** * This method is only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Provides access to the socket's SSLContext, if SSL/TLS has been started on the socket. **/ - (nullable SSLContextRef)sslContext; #pragma mark Utilities /** * The address lookup utility used by the class. * This method is synchronous, so it's recommended you use it on a background thread/queue. * * The special strings "localhost" and "loopback" return the loopback address for IPv4 and IPv6. * * @returns * A mutable array with all IPv4 and IPv6 addresses returned by getaddrinfo. * The addresses are specifically for TCP connections. * You can filter the addresses, if needed, using the other utility methods provided by the class. **/ + (nullable NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr; /** * Extracting host and port information from raw address data. **/ + (nullable NSString *)hostFromAddress:(NSData *)address; + (uint16_t)portFromAddress:(NSData *)address; + (BOOL)isIPv4Address:(NSData *)address; + (BOOL)isIPv6Address:(NSData *)address; + (BOOL)getHost:( NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr fromAddress:(NSData *)address; + (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(nullable uint16_t *)portPtr family:(nullable sa_family_t *)afPtr fromAddress:(NSData *)address; /** * A few common line separators, for use with the readDataToData:... methods. **/ + (NSData *)CRLFData; // 0x0D0A + (NSData *)CRData; // 0x0D + (NSData *)LFData; // 0x0A + (NSData *)ZeroData; // 0x00 @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @protocol GCDAsyncSocketDelegate @optional /** * This method is called immediately prior to socket:didAcceptNewSocket:. * It optionally allows a listening socket to specify the socketQueue for a new accepted socket. * If this method is not implemented, or returns NULL, the new accepted socket will create its own default queue. * * Since you cannot autorelease a dispatch_queue, * this method uses the "new" prefix in its name to specify that the returned queue has been retained. * * Thus you could do something like this in the implementation: * return dispatch_queue_create("MyQueue", NULL); * * If you are placing multiple sockets on the same queue, * then care should be taken to increment the retain count each time this method is invoked. * * For example, your implementation might look something like this: * dispatch_retain(myExistingQueue); * return myExistingQueue; **/ - (nullable dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock; /** * Called when a socket accepts a connection. * Another socket is automatically spawned to handle it. * * You must retain the newSocket if you wish to handle the connection. * Otherwise the newSocket instance will be released and the spawned connection will be closed. * * By default the new socket will have the same delegate and delegateQueue. * You may, of course, change this at any time. **/ - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket; /** * Called when a socket connects and is ready for reading and writing. * The host parameter will be an IP address, not a DNS name. **/ - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port; /** * Called when a socket connects and is ready for reading and writing. * The host parameter will be an IP address, not a DNS name. **/ - (void)socket:(GCDAsyncSocket *)sock didConnectToUrl:(NSURL *)url; /** * Called when a socket has completed reading the requested data into memory. * Not called if there is an error. **/ - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag; /** * Called when a socket has read in data, but has not yet completed the read. * This would occur if using readToData: or readToLength: methods. * It may be used for things such as updating progress bars. **/ - (void)socket:(GCDAsyncSocket *)sock didReadPartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; /** * Called when a socket has completed writing the requested data. Not called if there is an error. **/ - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag; /** * Called when a socket has written some data, but has not yet completed the entire write. * It may be used for things such as updating progress bars. **/ - (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag; /** * Called if a read operation has reached its timeout without completing. * This method allows you to optionally extend the timeout. * If you return a positive time interval (> 0) the read's timeout will be extended by the given amount. * If you don't implement this method, or return a non-positive time interval (<= 0) the read will timeout as usual. * * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. * The length parameter is the number of bytes that have been read so far for the read operation. * * Note that this method may be called multiple times for a single read if you return positive numbers. **/ - (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutReadWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length; /** * Called if a write operation has reached its timeout without completing. * This method allows you to optionally extend the timeout. * If you return a positive time interval (> 0) the write's timeout will be extended by the given amount. * If you don't implement this method, or return a non-positive time interval (<= 0) the write will timeout as usual. * * The elapsed parameter is the sum of the original timeout, plus any additions previously added via this method. * The length parameter is the number of bytes that have been written so far for the write operation. * * Note that this method may be called multiple times for a single write if you return positive numbers. **/ - (NSTimeInterval)socket:(GCDAsyncSocket *)sock shouldTimeoutWriteWithTag:(long)tag elapsed:(NSTimeInterval)elapsed bytesDone:(NSUInteger)length; /** * Conditionally called if the read stream closes, but the write stream may still be writeable. * * This delegate method is only called if autoDisconnectOnClosedReadStream has been set to NO. * See the discussion on the autoDisconnectOnClosedReadStream method for more information. **/ - (void)socketDidCloseReadStream:(GCDAsyncSocket *)sock; /** * Called when a socket disconnects with or without error. * * If you call the disconnect method, and the socket wasn't already disconnected, * then an invocation of this delegate method will be enqueued on the delegateQueue * before the disconnect method returns. * * Note: If the GCDAsyncSocket instance is deallocated while it is still connected, * and the delegate is not also deallocated, then this method will be invoked, * but the sock parameter will be nil. (It must necessarily be nil since it is no longer available.) * This is a generally rare, but is possible if one writes code like this: * * asyncSocket = nil; // I'm implicitly disconnecting the socket * * In this case it may preferrable to nil the delegate beforehand, like this: * * asyncSocket.delegate = nil; // Don't invoke my delegate method * asyncSocket = nil; // I'm implicitly disconnecting the socket * * Of course, this depends on how your state machine is configured. **/ - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(nullable NSError *)err; /** * Called after the socket has successfully completed SSL/TLS negotiation. * This method is not called unless you use the provided startTLS method. * * If a SSL/TLS negotiation fails (invalid certificate, etc) then the socket will immediately close, * and the socketDidDisconnect:withError: delegate method will be called with the specific SSL error code. **/ - (void)socketDidSecure:(GCDAsyncSocket *)sock; /** * Allows a socket delegate to hook into the TLS handshake and manually validate the peer it's connecting to. * * This is only called if startTLS is invoked with options that include: * - GCDAsyncSocketManuallyEvaluateTrust == YES * * Typically the delegate will use SecTrustEvaluate (and related functions) to properly validate the peer. * * Note from Apple's documentation: * Because [SecTrustEvaluate] might look on the network for certificates in the certificate chain, * [it] might block while attempting network access. You should never call it from your main thread; * call it only from within a function running on a dispatch queue or on a separate thread. * * Thus this method uses a completionHandler block rather than a normal return value. * The completionHandler block is thread-safe, and may be invoked from a background queue/thread. * It is safe to invoke the completionHandler block even if the socket has been closed. **/ - (void)socket:(GCDAsyncSocket *)sock didReceiveTrust:(SecTrustRef)trust completionHandler:(void (^)(BOOL shouldTrustPeer))completionHandler; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncSocket.m ================================================ // // GCDAsyncSocket.m // // This class is in the public domain. // Originally created by Robbie Hanson in Q4 2010. // Updated and maintained by Deusty LLC and the Apple development community. // // https://github.com/robbiehanson/CocoaAsyncSocket // #import "GCDAsyncSocket.h" #if TARGET_OS_IPHONE #import #endif #import #import #import #import #import #import #import #import #import #import #import #import #import #import #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). // For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC #endif #ifndef GCDAsyncSocketLoggingEnabled #define GCDAsyncSocketLoggingEnabled 0 #endif #if GCDAsyncSocketLoggingEnabled // Logging Enabled - See log level below // Logging uses the CocoaLumberjack framework (which is also GCD based). // https://github.com/robbiehanson/CocoaLumberjack // // It allows us to do a lot of logging without significantly slowing down the code. #import "DDLog.h" #define LogAsync YES #define LogContext GCDAsyncSocketLoggingContext #define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) #define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) #ifndef GCDAsyncSocketLogLevel #define GCDAsyncSocketLogLevel LOG_LEVEL_VERBOSE #endif // Log levels : off, error, warn, info, verbose static const int logLevel = GCDAsyncSocketLogLevel; #else // Logging Disabled #define LogError(frmt, ...) {} #define LogWarn(frmt, ...) {} #define LogInfo(frmt, ...) {} #define LogVerbose(frmt, ...) {} #define LogCError(frmt, ...) {} #define LogCWarn(frmt, ...) {} #define LogCInfo(frmt, ...) {} #define LogCVerbose(frmt, ...) {} #define LogTrace() {} #define LogCTrace(frmt, ...) {} #endif /** * Seeing a return statements within an inner block * can sometimes be mistaken for a return point of the enclosing method. * This makes inline blocks a bit easier to read. **/ #define return_from_block return /** * A socket file descriptor is really just an integer. * It represents the index of the socket within the kernel. * This makes invalid file descriptor comparisons easier to read. **/ #define SOCKET_NULL -1 NSString *const GCDAsyncSocketException = @"GCDAsyncSocketException"; NSString *const GCDAsyncSocketErrorDomain = @"GCDAsyncSocketErrorDomain"; NSString *const GCDAsyncSocketQueueName = @"GCDAsyncSocket"; NSString *const GCDAsyncSocketThreadName = @"GCDAsyncSocket-CFStream"; NSString *const GCDAsyncSocketManuallyEvaluateTrust = @"GCDAsyncSocketManuallyEvaluateTrust"; #if TARGET_OS_IPHONE NSString *const GCDAsyncSocketUseCFStreamForTLS = @"GCDAsyncSocketUseCFStreamForTLS"; #endif NSString *const GCDAsyncSocketSSLPeerID = @"GCDAsyncSocketSSLPeerID"; NSString *const GCDAsyncSocketSSLProtocolVersionMin = @"GCDAsyncSocketSSLProtocolVersionMin"; NSString *const GCDAsyncSocketSSLProtocolVersionMax = @"GCDAsyncSocketSSLProtocolVersionMax"; NSString *const GCDAsyncSocketSSLSessionOptionFalseStart = @"GCDAsyncSocketSSLSessionOptionFalseStart"; NSString *const GCDAsyncSocketSSLSessionOptionSendOneByteRecord = @"GCDAsyncSocketSSLSessionOptionSendOneByteRecord"; NSString *const GCDAsyncSocketSSLCipherSuites = @"GCDAsyncSocketSSLCipherSuites"; NSString *const GCDAsyncSocketSSLALPN = @"GCDAsyncSocketSSLALPN"; #if !TARGET_OS_IPHONE NSString *const GCDAsyncSocketSSLDiffieHellmanParameters = @"GCDAsyncSocketSSLDiffieHellmanParameters"; #endif enum GCDAsyncSocketFlags { kSocketStarted = 1 << 0, // If set, socket has been started (accepting/connecting) kConnected = 1 << 1, // If set, the socket is connected kForbidReadsWrites = 1 << 2, // If set, no new reads or writes are allowed kReadsPaused = 1 << 3, // If set, reads are paused due to possible timeout kWritesPaused = 1 << 4, // If set, writes are paused due to possible timeout kDisconnectAfterReads = 1 << 5, // If set, disconnect after no more reads are queued kDisconnectAfterWrites = 1 << 6, // If set, disconnect after no more writes are queued kSocketCanAcceptBytes = 1 << 7, // If set, we know socket can accept bytes. If unset, it's unknown. kReadSourceSuspended = 1 << 8, // If set, the read source is suspended kWriteSourceSuspended = 1 << 9, // If set, the write source is suspended kQueuedTLS = 1 << 10, // If set, we've queued an upgrade to TLS kStartingReadTLS = 1 << 11, // If set, we're waiting for TLS negotiation to complete kStartingWriteTLS = 1 << 12, // If set, we're waiting for TLS negotiation to complete kSocketSecure = 1 << 13, // If set, socket is using secure communication via SSL/TLS kSocketHasReadEOF = 1 << 14, // If set, we have read EOF from socket kReadStreamClosed = 1 << 15, // If set, we've read EOF plus prebuffer has been drained kDealloc = 1 << 16, // If set, the socket is being deallocated #if TARGET_OS_IPHONE kAddedStreamsToRunLoop = 1 << 17, // If set, CFStreams have been added to listener thread kUsingCFStreamForTLS = 1 << 18, // If set, we're forced to use CFStream instead of SecureTransport kSecureSocketHasBytesAvailable = 1 << 19, // If set, CFReadStream has notified us of bytes available #endif }; enum GCDAsyncSocketConfig { kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled kPreferIPv6 = 1 << 2, // If set, IPv6 is preferred over IPv4 kAllowHalfDuplexConnection = 1 << 3, // If set, the socket will stay open even if the read stream closes }; #if TARGET_OS_IPHONE static NSThread *cfstreamThread; // Used for CFStreams static uint64_t cfstreamThreadRetainCount; // setup & teardown static dispatch_queue_t cfstreamThreadSetupQueue; // setup & teardown #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * A PreBuffer is used when there is more data available on the socket * than is being requested by current read request. * In this case we slurp up all data from the socket (to minimize sys calls), * and store additional yet unread data in a "prebuffer". * * The prebuffer is entirely drained before we read from the socket again. * In other words, a large chunk of data is written is written to the prebuffer. * The prebuffer is then drained via a series of one or more reads (for subsequent read request(s)). * * A ring buffer was once used for this purpose. * But a ring buffer takes up twice as much memory as needed (double the size for mirroring). * In fact, it generally takes up more than twice the needed size as everything has to be rounded up to vm_page_size. * And since the prebuffer is always completely drained after being written to, a full ring buffer isn't needed. * * The current design is very simple and straight-forward, while also keeping memory requirements lower. **/ @interface GCDAsyncSocketPreBuffer : NSObject { uint8_t *preBuffer; size_t preBufferSize; uint8_t *readPointer; uint8_t *writePointer; } - (instancetype)initWithCapacity:(size_t)numBytes NS_DESIGNATED_INITIALIZER; - (void)ensureCapacityForWrite:(size_t)numBytes; - (size_t)availableBytes; - (uint8_t *)readBuffer; - (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr; - (size_t)availableSpace; - (uint8_t *)writeBuffer; - (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr; - (void)didRead:(size_t)bytesRead; - (void)didWrite:(size_t)bytesWritten; - (void)reset; @end @implementation GCDAsyncSocketPreBuffer // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { NSAssert(0, @"Use the designated initializer"); return nil; } - (instancetype)initWithCapacity:(size_t)numBytes { if ((self = [super init])) { preBufferSize = numBytes; preBuffer = malloc(preBufferSize); readPointer = preBuffer; writePointer = preBuffer; } return self; } - (void)dealloc { if (preBuffer) free(preBuffer); } - (void)ensureCapacityForWrite:(size_t)numBytes { size_t availableSpace = [self availableSpace]; if (numBytes > availableSpace) { size_t additionalBytes = numBytes - availableSpace; size_t newPreBufferSize = preBufferSize + additionalBytes; uint8_t *newPreBuffer = realloc(preBuffer, newPreBufferSize); size_t readPointerOffset = readPointer - preBuffer; size_t writePointerOffset = writePointer - preBuffer; preBuffer = newPreBuffer; preBufferSize = newPreBufferSize; readPointer = preBuffer + readPointerOffset; writePointer = preBuffer + writePointerOffset; } } - (size_t)availableBytes { return writePointer - readPointer; } - (uint8_t *)readBuffer { return readPointer; } - (void)getReadBuffer:(uint8_t **)bufferPtr availableBytes:(size_t *)availableBytesPtr { if (bufferPtr) *bufferPtr = readPointer; if (availableBytesPtr) *availableBytesPtr = [self availableBytes]; } - (void)didRead:(size_t)bytesRead { readPointer += bytesRead; if (readPointer == writePointer) { // The prebuffer has been drained. Reset pointers. readPointer = preBuffer; writePointer = preBuffer; } } - (size_t)availableSpace { return preBufferSize - (writePointer - preBuffer); } - (uint8_t *)writeBuffer { return writePointer; } - (void)getWriteBuffer:(uint8_t **)bufferPtr availableSpace:(size_t *)availableSpacePtr { if (bufferPtr) *bufferPtr = writePointer; if (availableSpacePtr) *availableSpacePtr = [self availableSpace]; } - (void)didWrite:(size_t)bytesWritten { writePointer += bytesWritten; } - (void)reset { readPointer = preBuffer; writePointer = preBuffer; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The GCDAsyncReadPacket encompasses the instructions for any given read. * The content of a read packet allows the code to determine if we're: * - reading to a certain length * - reading to a certain separator * - or simply reading the first chunk of available data **/ @interface GCDAsyncReadPacket : NSObject { @public NSMutableData *buffer; NSUInteger startOffset; NSUInteger bytesDone; NSUInteger maxLength; NSTimeInterval timeout; NSUInteger readLength; NSData *term; BOOL bufferOwner; NSUInteger originalBufferLength; long tag; } - (instancetype)initWithData:(NSMutableData *)d startOffset:(NSUInteger)s maxLength:(NSUInteger)m timeout:(NSTimeInterval)t readLength:(NSUInteger)l terminator:(NSData *)e tag:(long)i NS_DESIGNATED_INITIALIZER; - (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead; - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr; - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable; - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr; - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr; - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes; @end @implementation GCDAsyncReadPacket // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { NSAssert(0, @"Use the designated initializer"); return nil; } - (instancetype)initWithData:(NSMutableData *)d startOffset:(NSUInteger)s maxLength:(NSUInteger)m timeout:(NSTimeInterval)t readLength:(NSUInteger)l terminator:(NSData *)e tag:(long)i { if((self = [super init])) { bytesDone = 0; maxLength = m; timeout = t; readLength = l; term = [e copy]; tag = i; if (d) { buffer = d; startOffset = s; bufferOwner = NO; originalBufferLength = [d length]; } else { if (readLength > 0) buffer = [[NSMutableData alloc] initWithLength:readLength]; else buffer = [[NSMutableData alloc] initWithLength:0]; startOffset = 0; bufferOwner = YES; originalBufferLength = 0; } } return self; } /** * Increases the length of the buffer (if needed) to ensure a read of the given size will fit. **/ - (void)ensureCapacityForAdditionalDataOfLength:(NSUInteger)bytesToRead { NSUInteger buffSize = [buffer length]; NSUInteger buffUsed = startOffset + bytesDone; NSUInteger buffSpace = buffSize - buffUsed; if (bytesToRead > buffSpace) { NSUInteger buffInc = bytesToRead - buffSpace; [buffer increaseLengthBy:buffInc]; } } /** * This method is used when we do NOT know how much data is available to be read from the socket. * This method returns the default value unless it exceeds the specified readLength or maxLength. * * Furthermore, the shouldPreBuffer decision is based upon the packet type, * and whether the returned value would fit in the current buffer without requiring a resize of the buffer. **/ - (NSUInteger)optimalReadLengthWithDefault:(NSUInteger)defaultValue shouldPreBuffer:(BOOL *)shouldPreBufferPtr { NSUInteger result; if (readLength > 0) { // Read a specific length of data result = readLength - bytesDone; // There is no need to prebuffer since we know exactly how much data we need to read. // Even if the buffer isn't currently big enough to fit this amount of data, // it would have to be resized eventually anyway. if (shouldPreBufferPtr) *shouldPreBufferPtr = NO; } else { // Either reading until we find a specified terminator, // or we're simply reading all available data. // // In other words, one of: // // - readDataToData packet // - readDataWithTimeout packet if (maxLength > 0) result = MIN(defaultValue, (maxLength - bytesDone)); else result = defaultValue; // Since we don't know the size of the read in advance, // the shouldPreBuffer decision is based upon whether the returned value would fit // in the current buffer without requiring a resize of the buffer. // // This is because, in all likelyhood, the amount read from the socket will be less than the default value. // Thus we should avoid over-allocating the read buffer when we can simply use the pre-buffer instead. if (shouldPreBufferPtr) { NSUInteger buffSize = [buffer length]; NSUInteger buffUsed = startOffset + bytesDone; NSUInteger buffSpace = buffSize - buffUsed; if (buffSpace >= result) *shouldPreBufferPtr = NO; else *shouldPreBufferPtr = YES; } } return result; } /** * For read packets without a set terminator, returns the amount of data * that can be read without exceeding the readLength or maxLength. * * The given parameter indicates the number of bytes estimated to be available on the socket, * which is taken into consideration during the calculation. * * The given hint MUST be greater than zero. **/ - (NSUInteger)readLengthForNonTermWithHint:(NSUInteger)bytesAvailable { NSAssert(term == nil, @"This method does not apply to term reads"); NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); if (readLength > 0) { // Read a specific length of data return MIN(bytesAvailable, (readLength - bytesDone)); // No need to avoid resizing the buffer. // If the user provided their own buffer, // and told us to read a certain length of data that exceeds the size of the buffer, // then it is clear that our code will resize the buffer during the read operation. // // This method does not actually do any resizing. // The resizing will happen elsewhere if needed. } else { // Read all available data NSUInteger result = bytesAvailable; if (maxLength > 0) { result = MIN(result, (maxLength - bytesDone)); } // No need to avoid resizing the buffer. // If the user provided their own buffer, // and told us to read all available data without giving us a maxLength, // then it is clear that our code might resize the buffer during the read operation. // // This method does not actually do any resizing. // The resizing will happen elsewhere if needed. return result; } } /** * For read packets with a set terminator, returns the amount of data * that can be read without exceeding the maxLength. * * The given parameter indicates the number of bytes estimated to be available on the socket, * which is taken into consideration during the calculation. * * To optimize memory allocations, mem copies, and mem moves * the shouldPreBuffer boolean value will indicate if the data should be read into a prebuffer first, * or if the data can be read directly into the read packet's buffer. **/ - (NSUInteger)readLengthForTermWithHint:(NSUInteger)bytesAvailable shouldPreBuffer:(BOOL *)shouldPreBufferPtr { NSAssert(term != nil, @"This method does not apply to non-term reads"); NSAssert(bytesAvailable > 0, @"Invalid parameter: bytesAvailable"); NSUInteger result = bytesAvailable; if (maxLength > 0) { result = MIN(result, (maxLength - bytesDone)); } // Should the data be read into the read packet's buffer, or into a pre-buffer first? // // One would imagine the preferred option is the faster one. // So which one is faster? // // Reading directly into the packet's buffer requires: // 1. Possibly resizing packet buffer (malloc/realloc) // 2. Filling buffer (read) // 3. Searching for term (memcmp) // 4. Possibly copying overflow into prebuffer (malloc/realloc, memcpy) // // Reading into prebuffer first: // 1. Possibly resizing prebuffer (malloc/realloc) // 2. Filling buffer (read) // 3. Searching for term (memcmp) // 4. Copying underflow into packet buffer (malloc/realloc, memcpy) // 5. Removing underflow from prebuffer (memmove) // // Comparing the performance of the two we can see that reading // data into the prebuffer first is slower due to the extra memove. // // However: // The implementation of NSMutableData is open source via core foundation's CFMutableData. // Decreasing the length of a mutable data object doesn't cause a realloc. // In other words, the capacity of a mutable data object can grow, but doesn't shrink. // // This means the prebuffer will rarely need a realloc. // The packet buffer, on the other hand, may often need a realloc. // This is especially true if we are the buffer owner. // Furthermore, if we are constantly realloc'ing the packet buffer, // and then moving the overflow into the prebuffer, // then we're consistently over-allocating memory for each term read. // And now we get into a bit of a tradeoff between speed and memory utilization. // // The end result is that the two perform very similarly. // And we can answer the original question very simply by another means. // // If we can read all the data directly into the packet's buffer without resizing it first, // then we do so. Otherwise we use the prebuffer. if (shouldPreBufferPtr) { NSUInteger buffSize = [buffer length]; NSUInteger buffUsed = startOffset + bytesDone; if ((buffSize - buffUsed) >= result) *shouldPreBufferPtr = NO; else *shouldPreBufferPtr = YES; } return result; } /** * For read packets with a set terminator, * returns the amount of data that can be read from the given preBuffer, * without going over a terminator or the maxLength. * * It is assumed the terminator has not already been read. **/ - (NSUInteger)readLengthForTermWithPreBuffer:(GCDAsyncSocketPreBuffer *)preBuffer found:(BOOL *)foundPtr { NSAssert(term != nil, @"This method does not apply to non-term reads"); NSAssert([preBuffer availableBytes] > 0, @"Invoked with empty pre buffer!"); // We know that the terminator, as a whole, doesn't exist in our own buffer. // But it is possible that a _portion_ of it exists in our buffer. // So we're going to look for the terminator starting with a portion of our own buffer. // // Example: // // term length = 3 bytes // bytesDone = 5 bytes // preBuffer length = 5 bytes // // If we append the preBuffer to our buffer, // it would look like this: // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // --------------------- // // So we start our search here: // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // -------^-^-^--------- // // And move forwards... // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // ---------^-^-^------- // // Until we find the terminator or reach the end. // // --------------------- // |B|B|B|B|B|P|P|P|P|P| // ---------------^-^-^- BOOL found = NO; NSUInteger termLength = [term length]; NSUInteger preBufferLength = [preBuffer availableBytes]; if ((bytesDone + preBufferLength) < termLength) { // Not enough data for a full term sequence yet return preBufferLength; } NSUInteger maxPreBufferLength; if (maxLength > 0) { maxPreBufferLength = MIN(preBufferLength, (maxLength - bytesDone)); // Note: maxLength >= termLength } else { maxPreBufferLength = preBufferLength; } uint8_t seq[termLength]; const void *termBuf = [term bytes]; NSUInteger bufLen = MIN(bytesDone, (termLength - 1)); uint8_t *buf = (uint8_t *)[buffer mutableBytes] + startOffset + bytesDone - bufLen; NSUInteger preLen = termLength - bufLen; const uint8_t *pre = [preBuffer readBuffer]; NSUInteger loopCount = bufLen + maxPreBufferLength - termLength + 1; // Plus one. See example above. NSUInteger result = maxPreBufferLength; NSUInteger i; for (i = 0; i < loopCount; i++) { if (bufLen > 0) { // Combining bytes from buffer and preBuffer memcpy(seq, buf, bufLen); memcpy(seq + bufLen, pre, preLen); if (memcmp(seq, termBuf, termLength) == 0) { result = preLen; found = YES; break; } buf++; bufLen--; preLen++; } else { // Comparing directly from preBuffer if (memcmp(pre, termBuf, termLength) == 0) { NSUInteger preOffset = pre - [preBuffer readBuffer]; // pointer arithmetic result = preOffset + termLength; found = YES; break; } pre++; } } // There is no need to avoid resizing the buffer in this particular situation. if (foundPtr) *foundPtr = found; return result; } /** * For read packets with a set terminator, scans the packet buffer for the term. * It is assumed the terminator had not been fully read prior to the new bytes. * * If the term is found, the number of excess bytes after the term are returned. * If the term is not found, this method will return -1. * * Note: A return value of zero means the term was found at the very end. * * Prerequisites: * The given number of bytes have been added to the end of our buffer. * Our bytesDone variable has NOT been changed due to the prebuffered bytes. **/ - (NSInteger)searchForTermAfterPreBuffering:(ssize_t)numBytes { NSAssert(term != nil, @"This method does not apply to non-term reads"); // The implementation of this method is very similar to the above method. // See the above method for a discussion of the algorithm used here. uint8_t *buff = [buffer mutableBytes]; NSUInteger buffLength = bytesDone + numBytes; const void *termBuff = [term bytes]; NSUInteger termLength = [term length]; // Note: We are dealing with unsigned integers, // so make sure the math doesn't go below zero. NSUInteger i = ((buffLength - numBytes) >= termLength) ? (buffLength - numBytes - termLength + 1) : 0; while (i + termLength <= buffLength) { uint8_t *subBuffer = buff + startOffset + i; if (memcmp(subBuffer, termBuff, termLength) == 0) { return buffLength - (i + termLength); } i++; } return -1; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The GCDAsyncWritePacket encompasses the instructions for any given write. **/ @interface GCDAsyncWritePacket : NSObject { @public NSData *buffer; NSUInteger bytesDone; long tag; NSTimeInterval timeout; } - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i NS_DESIGNATED_INITIALIZER; @end @implementation GCDAsyncWritePacket // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { NSAssert(0, @"Use the designated initializer"); return nil; } - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i { if((self = [super init])) { buffer = d; // Retain not copy. For performance as documented in header file. bytesDone = 0; timeout = t; tag = i; } return self; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The GCDAsyncSpecialPacket encompasses special instructions for interruptions in the read/write queues. * This class my be altered to support more than just TLS in the future. **/ @interface GCDAsyncSpecialPacket : NSObject { @public NSDictionary *tlsSettings; } - (instancetype)initWithTLSSettings:(NSDictionary *)settings NS_DESIGNATED_INITIALIZER; @end @implementation GCDAsyncSpecialPacket // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { NSAssert(0, @"Use the designated initializer"); return nil; } - (instancetype)initWithTLSSettings:(NSDictionary *)settings { if((self = [super init])) { tlsSettings = [settings copy]; } return self; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation GCDAsyncSocket { uint32_t flags; uint16_t config; __weak id delegate; dispatch_queue_t delegateQueue; int socket4FD; int socket6FD; int socketUN; NSURL *socketUrl; int stateIndex; NSData * connectInterface4; NSData * connectInterface6; NSData * connectInterfaceUN; dispatch_queue_t socketQueue; dispatch_source_t accept4Source; dispatch_source_t accept6Source; dispatch_source_t acceptUNSource; dispatch_source_t connectTimer; dispatch_source_t readSource; dispatch_source_t writeSource; dispatch_source_t readTimer; dispatch_source_t writeTimer; NSMutableArray *readQueue; NSMutableArray *writeQueue; GCDAsyncReadPacket *currentRead; GCDAsyncWritePacket *currentWrite; unsigned long socketFDBytesAvailable; GCDAsyncSocketPreBuffer *preBuffer; #if TARGET_OS_IPHONE CFStreamClientContext streamContext; CFReadStreamRef readStream; CFWriteStreamRef writeStream; #endif SSLContextRef sslContext; GCDAsyncSocketPreBuffer *sslPreBuffer; size_t sslWriteCachedLength; OSStatus sslErrCode; OSStatus lastSSLHandshakeError; void *IsOnSocketQueueOrTargetQueueKey; id userData; NSTimeInterval alternateAddressDelay; } - (instancetype)init { return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; } - (instancetype)initWithSocketQueue:(dispatch_queue_t)sq { return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq { return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq { if((self = [super init])) { delegate = aDelegate; delegateQueue = dq; #if !OS_OBJECT_USE_OBJC if (dq) dispatch_retain(dq); #endif socket4FD = SOCKET_NULL; socket6FD = SOCKET_NULL; socketUN = SOCKET_NULL; socketUrl = nil; stateIndex = 0; if (sq) { NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), @"The given socketQueue parameter must not be a concurrent queue."); socketQueue = sq; #if !OS_OBJECT_USE_OBJC dispatch_retain(sq); #endif } else { socketQueue = dispatch_queue_create([GCDAsyncSocketQueueName UTF8String], NULL); } // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. // From the documentation: // // > Keys are only compared as pointers and are never dereferenced. // > Thus, you can use a pointer to a static variable for a specific subsystem or // > any other value that allows you to identify the value uniquely. // // We're just going to use the memory address of an ivar. // Specifically an ivar that is explicitly named for our purpose to make the code more readable. // // However, it feels tedious (and less readable) to include the "&" all the time: // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) // // So we're going to make it so it doesn't matter if we use the '&' or not, // by assigning the value of the ivar to the address of the ivar. // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; void *nonNullUnusedPointer = (__bridge void *)self; dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); readQueue = [[NSMutableArray alloc] initWithCapacity:5]; currentRead = nil; writeQueue = [[NSMutableArray alloc] initWithCapacity:5]; currentWrite = nil; preBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; alternateAddressDelay = 0.3; } return self; } - (void)dealloc { LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); // Set dealloc flag. // This is used by closeWithError to ensure we don't accidentally retain ourself. flags |= kDealloc; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { [self closeWithError:nil]; } else { dispatch_sync(socketQueue, ^{ [self closeWithError:nil]; }); } delegate = nil; #if !OS_OBJECT_USE_OBJC if (delegateQueue) dispatch_release(delegateQueue); #endif delegateQueue = NULL; #if !OS_OBJECT_USE_OBJC if (socketQueue) dispatch_release(socketQueue); #endif socketQueue = NULL; LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); } #pragma mark - + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD socketQueue:(nullable dispatch_queue_t)sq error:(NSError**)error { return [self socketFromConnectedSocketFD:socketFD delegate:nil delegateQueue:NULL socketQueue:sq error:error]; } + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq error:(NSError**)error { return [self socketFromConnectedSocketFD:socketFD delegate:aDelegate delegateQueue:dq socketQueue:NULL error:error]; } + (nullable instancetype)socketFromConnectedSocketFD:(int)socketFD delegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq error:(NSError* __autoreleasing *)error { __block BOOL errorOccured = NO; GCDAsyncSocket *socket = [[[self class] alloc] initWithDelegate:aDelegate delegateQueue:dq socketQueue:sq]; dispatch_sync(socket->socketQueue, ^{ @autoreleasepool { struct sockaddr addr; socklen_t addr_size = sizeof(struct sockaddr); int retVal = getpeername(socketFD, (struct sockaddr *)&addr, &addr_size); if (retVal) { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Attempt to create socket from socket FD failed. getpeername() failed", nil); NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; errorOccured = YES; if (error) *error = [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; return; } if (addr.sa_family == AF_INET) { socket->socket4FD = socketFD; } else if (addr.sa_family == AF_INET6) { socket->socket6FD = socketFD; } else { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketOtherError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Attempt to create socket from socket FD failed. socket FD is neither IPv4 nor IPv6", nil); NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; errorOccured = YES; if (error) *error = [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; return; } socket->flags = kSocketStarted; [socket didConnect:socket->stateIndex]; }}); return errorOccured? nil: socket; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Configuration //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (id)delegate { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return delegate; } else { __block id result; dispatch_sync(socketQueue, ^{ result = self->delegate; }); return result; } } - (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ self->delegate = newDelegate; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } else { if (synchronously) dispatch_sync(socketQueue, block); else dispatch_async(socketQueue, block); } } - (void)setDelegate:(id)newDelegate { [self setDelegate:newDelegate synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate { [self setDelegate:newDelegate synchronously:YES]; } - (dispatch_queue_t)delegateQueue { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return delegateQueue; } else { __block dispatch_queue_t result; dispatch_sync(socketQueue, ^{ result = self->delegateQueue; }); return result; } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ #if !OS_OBJECT_USE_OBJC if (self->delegateQueue) dispatch_release(self->delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif self->delegateQueue = newDelegateQueue; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } else { if (synchronously) dispatch_sync(socketQueue, block); else dispatch_async(socketQueue, block); } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegateQueue:newDelegateQueue synchronously:YES]; } - (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (delegatePtr) *delegatePtr = delegate; if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; } else { __block id dPtr = NULL; __block dispatch_queue_t dqPtr = NULL; dispatch_sync(socketQueue, ^{ dPtr = self->delegate; dqPtr = self->delegateQueue; }); if (delegatePtr) *delegatePtr = dPtr; if (delegateQueuePtr) *delegateQueuePtr = dqPtr; } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ self->delegate = newDelegate; #if !OS_OBJECT_USE_OBJC if (self->delegateQueue) dispatch_release(self->delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif self->delegateQueue = newDelegateQueue; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } else { if (synchronously) dispatch_sync(socketQueue, block); else dispatch_async(socketQueue, block); } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; } - (BOOL)isIPv4Enabled { // Note: YES means kIPv4Disabled is OFF if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kIPv4Disabled) == 0); } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = ((self->config & kIPv4Disabled) == 0); }); return result; } } - (void)setIPv4Enabled:(BOOL)flag { // Note: YES means kIPv4Disabled is OFF dispatch_block_t block = ^{ if (flag) self->config &= ~kIPv4Disabled; else self->config |= kIPv4Disabled; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (BOOL)isIPv6Enabled { // Note: YES means kIPv6Disabled is OFF if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kIPv6Disabled) == 0); } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = ((self->config & kIPv6Disabled) == 0); }); return result; } } - (void)setIPv6Enabled:(BOOL)flag { // Note: YES means kIPv6Disabled is OFF dispatch_block_t block = ^{ if (flag) self->config &= ~kIPv6Disabled; else self->config |= kIPv6Disabled; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (BOOL)isIPv4PreferredOverIPv6 { // Note: YES means kPreferIPv6 is OFF if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kPreferIPv6) == 0); } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = ((self->config & kPreferIPv6) == 0); }); return result; } } - (void)setIPv4PreferredOverIPv6:(BOOL)flag { // Note: YES means kPreferIPv6 is OFF dispatch_block_t block = ^{ if (flag) self->config &= ~kPreferIPv6; else self->config |= kPreferIPv6; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (NSTimeInterval) alternateAddressDelay { __block NSTimeInterval delay; dispatch_block_t block = ^{ delay = self->alternateAddressDelay; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return delay; } - (void) setAlternateAddressDelay:(NSTimeInterval)delay { dispatch_block_t block = ^{ self->alternateAddressDelay = delay; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (id)userData { __block id result = nil; dispatch_block_t block = ^{ result = self->userData; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (void)setUserData:(id)arbitraryUserData { dispatch_block_t block = ^{ if (self->userData != arbitraryUserData) { self->userData = arbitraryUserData; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Accepting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)acceptOnPort:(uint16_t)port error:(NSError **)errPtr { return [self acceptOnInterface:nil port:port error:errPtr]; } - (BOOL)acceptOnInterface:(NSString *)inInterface port:(uint16_t)port error:(NSError **)errPtr { LogTrace(); // Just in-case interface parameter is immutable. NSString *interface = [inInterface copy]; __block BOOL result = NO; __block NSError *err = nil; // CreateSocket Block // This block will be invoked within the dispatch block below. int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { int socketFD = socket(domain, SOCK_STREAM, 0); if (socketFD == SOCKET_NULL) { NSString *reason = @"Error in socket() function"; err = [self errorWithErrno:errno reason:reason]; return SOCKET_NULL; } int status; // Set socket options status = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (status == -1) { NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; err = [self errorWithErrno:errno reason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } int reuseOn = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); if (status == -1) { NSString *reason = @"Error enabling address reuse (setsockopt)"; err = [self errorWithErrno:errno reason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } // Bind socket status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); if (status == -1) { NSString *reason = @"Error in bind() function"; err = [self errorWithErrno:errno reason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } // Listen status = listen(socketFD, 1024); if (status == -1) { NSString *reason = @"Error in listen() function"; err = [self errorWithErrno:errno reason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } return socketFD; }; // Create dispatch block and run on socketQueue dispatch_block_t block = ^{ @autoreleasepool { if (self->delegate == nil) // Must have delegate set { NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; err = [self badConfigError:msg]; return_from_block; } if (self->delegateQueue == NULL) // Must have delegate queue set { NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; err = [self badConfigError:msg]; return_from_block; } BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; err = [self badConfigError:msg]; return_from_block; } if (![self isDisconnected]) // Must be disconnected { NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; err = [self badConfigError:msg]; return_from_block; } // Clear queues (spurious read/write requests post disconnect) [self->readQueue removeAllObjects]; [self->writeQueue removeAllObjects]; // Resolve interface from description NSMutableData *interface4 = nil; NSMutableData *interface6 = nil; [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:port]; if ((interface4 == nil) && (interface6 == nil)) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; err = [self badParamError:msg]; return_from_block; } if (isIPv4Disabled && (interface6 == nil)) { NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; err = [self badParamError:msg]; return_from_block; } if (isIPv6Disabled && (interface4 == nil)) { NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; err = [self badParamError:msg]; return_from_block; } BOOL enableIPv4 = !isIPv4Disabled && (interface4 != nil); BOOL enableIPv6 = !isIPv6Disabled && (interface6 != nil); // Create sockets, configure, bind, and listen if (enableIPv4) { LogVerbose(@"Creating IPv4 socket"); self->socket4FD = createSocket(AF_INET, interface4); if (self->socket4FD == SOCKET_NULL) { return_from_block; } } if (enableIPv6) { LogVerbose(@"Creating IPv6 socket"); if (enableIPv4 && (port == 0)) { // No specific port was specified, so we allowed the OS to pick an available port for us. // Now we need to make sure the IPv6 socket listens on the same port as the IPv4 socket. struct sockaddr_in6 *addr6 = (struct sockaddr_in6 *)[interface6 mutableBytes]; addr6->sin6_port = htons([self localPort4]); } self->socket6FD = createSocket(AF_INET6, interface6); if (self->socket6FD == SOCKET_NULL) { if (self->socket4FD != SOCKET_NULL) { LogVerbose(@"close(socket4FD)"); close(self->socket4FD); self->socket4FD = SOCKET_NULL; } return_from_block; } } // Create accept sources if (enableIPv4) { self->accept4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socket4FD, 0, self->socketQueue); int socketFD = self->socket4FD; dispatch_source_t acceptSource = self->accept4Source; __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(self->accept4Source, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; LogVerbose(@"event4Block"); unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); LogVerbose(@"numPendingConnections: %lu", numPendingConnections); while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); #pragma clang diagnostic pop }}); dispatch_source_set_cancel_handler(self->accept4Source, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(accept4Source)"); dispatch_release(acceptSource); #endif LogVerbose(@"close(socket4FD)"); close(socketFD); #pragma clang diagnostic pop }); LogVerbose(@"dispatch_resume(accept4Source)"); dispatch_resume(self->accept4Source); } if (enableIPv6) { self->accept6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socket6FD, 0, self->socketQueue); int socketFD = self->socket6FD; dispatch_source_t acceptSource = self->accept6Source; __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(self->accept6Source, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; LogVerbose(@"event6Block"); unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); LogVerbose(@"numPendingConnections: %lu", numPendingConnections); while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); #pragma clang diagnostic pop }}); dispatch_source_set_cancel_handler(self->accept6Source, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(accept6Source)"); dispatch_release(acceptSource); #endif LogVerbose(@"close(socket6FD)"); close(socketFD); #pragma clang diagnostic pop }); LogVerbose(@"dispatch_resume(accept6Source)"); dispatch_resume(self->accept6Source); } self->flags |= kSocketStarted; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (result == NO) { LogInfo(@"Error in accept: %@", err); if (errPtr) *errPtr = err; } return result; } - (BOOL)acceptOnUrl:(NSURL *)url error:(NSError **)errPtr { LogTrace(); __block BOOL result = NO; __block NSError *err = nil; // CreateSocket Block // This block will be invoked within the dispatch block below. int(^createSocket)(int, NSData*) = ^int (int domain, NSData *interfaceAddr) { int socketFD = socket(domain, SOCK_STREAM, 0); if (socketFD == SOCKET_NULL) { NSString *reason = @"Error in socket() function"; err = [self errorWithErrno:errno reason:reason]; return SOCKET_NULL; } int status; // Set socket options status = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (status == -1) { NSString *reason = @"Error enabling non-blocking IO on socket (fcntl)"; err = [self errorWithErrno:errno reason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } int reuseOn = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); if (status == -1) { NSString *reason = @"Error enabling address reuse (setsockopt)"; err = [self errorWithErrno:errno reason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } // Bind socket status = bind(socketFD, (const struct sockaddr *)[interfaceAddr bytes], (socklen_t)[interfaceAddr length]); if (status == -1) { NSString *reason = @"Error in bind() function"; err = [self errorWithErrno:errno reason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } // Listen status = listen(socketFD, 1024); if (status == -1) { NSString *reason = @"Error in listen() function"; err = [self errorWithErrno:errno reason:reason]; LogVerbose(@"close(socketFD)"); close(socketFD); return SOCKET_NULL; } return socketFD; }; // Create dispatch block and run on socketQueue dispatch_block_t block = ^{ @autoreleasepool { if (self->delegate == nil) // Must have delegate set { NSString *msg = @"Attempting to accept without a delegate. Set a delegate first."; err = [self badConfigError:msg]; return_from_block; } if (self->delegateQueue == NULL) // Must have delegate queue set { NSString *msg = @"Attempting to accept without a delegate queue. Set a delegate queue first."; err = [self badConfigError:msg]; return_from_block; } if (![self isDisconnected]) // Must be disconnected { NSString *msg = @"Attempting to accept while connected or accepting connections. Disconnect first."; err = [self badConfigError:msg]; return_from_block; } // Clear queues (spurious read/write requests post disconnect) [self->readQueue removeAllObjects]; [self->writeQueue removeAllObjects]; // Remove a previous socket NSError *error = nil; NSFileManager *fileManager = [NSFileManager defaultManager]; NSString *urlPath = url.path; if (urlPath && [fileManager fileExistsAtPath:urlPath]) { if (![fileManager removeItemAtURL:url error:&error]) { NSString *msg = @"Could not remove previous unix domain socket at given url."; err = [self otherError:msg]; return_from_block; } } // Resolve interface from description NSData *interface = [self getInterfaceAddressFromUrl:url]; if (interface == nil) { NSString *msg = @"Invalid unix domain url. Specify a valid file url that does not exist (e.g. \"file:///tmp/socket\")"; err = [self badParamError:msg]; return_from_block; } // Create sockets, configure, bind, and listen LogVerbose(@"Creating unix domain socket"); self->socketUN = createSocket(AF_UNIX, interface); if (self->socketUN == SOCKET_NULL) { return_from_block; } self->socketUrl = url; // Create accept sources self->acceptUNSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->socketUN, 0, self->socketQueue); int socketFD = self->socketUN; dispatch_source_t acceptSource = self->acceptUNSource; __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(self->acceptUNSource, ^{ @autoreleasepool { __strong GCDAsyncSocket *strongSelf = weakSelf; LogVerbose(@"eventUNBlock"); unsigned long i = 0; unsigned long numPendingConnections = dispatch_source_get_data(acceptSource); LogVerbose(@"numPendingConnections: %lu", numPendingConnections); while ([strongSelf doAccept:socketFD] && (++i < numPendingConnections)); }}); dispatch_source_set_cancel_handler(self->acceptUNSource, ^{ #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(acceptUNSource)"); dispatch_release(acceptSource); #endif LogVerbose(@"close(socketUN)"); close(socketFD); }); LogVerbose(@"dispatch_resume(acceptUNSource)"); dispatch_resume(self->acceptUNSource); self->flags |= kSocketStarted; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (result == NO) { LogInfo(@"Error in accept: %@", err); if (errPtr) *errPtr = err; } return result; } - (BOOL)doAccept:(int)parentSocketFD { LogTrace(); int socketType; int childSocketFD; NSData *childSocketAddress; if (parentSocketFD == socket4FD) { socketType = 0; struct sockaddr_in addr; socklen_t addrLen = sizeof(addr); childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); if (childSocketFD == -1) { LogWarn(@"Accept failed with error: %@", [self errnoError]); return NO; } childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; } else if (parentSocketFD == socket6FD) { socketType = 1; struct sockaddr_in6 addr; socklen_t addrLen = sizeof(addr); childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); if (childSocketFD == -1) { LogWarn(@"Accept failed with error: %@", [self errnoError]); return NO; } childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; } else // if (parentSocketFD == socketUN) { socketType = 2; struct sockaddr_un addr; socklen_t addrLen = sizeof(addr); childSocketFD = accept(parentSocketFD, (struct sockaddr *)&addr, &addrLen); if (childSocketFD == -1) { LogWarn(@"Accept failed with error: %@", [self errnoError]); return NO; } childSocketAddress = [NSData dataWithBytes:&addr length:addrLen]; } // Enable non-blocking IO on the socket int result = fcntl(childSocketFD, F_SETFL, O_NONBLOCK); if (result == -1) { LogWarn(@"Error enabling non-blocking IO on accepted socket (fcntl)"); LogVerbose(@"close(childSocketFD)"); close(childSocketFD); return NO; } // Prevent SIGPIPE signals int nosigpipe = 1; setsockopt(childSocketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); // Notify delegate if (delegateQueue) { __strong id theDelegate = delegate; dispatch_async(delegateQueue, ^{ @autoreleasepool { // Query delegate for custom socket queue dispatch_queue_t childSocketQueue = NULL; if ([theDelegate respondsToSelector:@selector(newSocketQueueForConnectionFromAddress:onSocket:)]) { childSocketQueue = [theDelegate newSocketQueueForConnectionFromAddress:childSocketAddress onSocket:self]; } // Create GCDAsyncSocket instance for accepted socket GCDAsyncSocket *acceptedSocket = [[[self class] alloc] initWithDelegate:theDelegate delegateQueue:self->delegateQueue socketQueue:childSocketQueue]; if (socketType == 0) acceptedSocket->socket4FD = childSocketFD; else if (socketType == 1) acceptedSocket->socket6FD = childSocketFD; else acceptedSocket->socketUN = childSocketFD; acceptedSocket->flags = (kSocketStarted | kConnected); // Setup read and write sources for accepted socket dispatch_async(acceptedSocket->socketQueue, ^{ @autoreleasepool { [acceptedSocket setupReadAndWriteSourcesForNewlyConnectedSocket:childSocketFD]; }}); // Notify delegate if ([theDelegate respondsToSelector:@selector(socket:didAcceptNewSocket:)]) { [theDelegate socket:self didAcceptNewSocket:acceptedSocket]; } // Release the socket queue returned from the delegate (it was retained by acceptedSocket) #if !OS_OBJECT_USE_OBJC if (childSocketQueue) dispatch_release(childSocketQueue); #endif // The accepted socket should have been retained by the delegate. // Otherwise it gets properly released when exiting the block. }}); } return YES; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Connecting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * This method runs through the various checks required prior to a connection attempt. * It is shared between the connectToHost and connectToAddress methods. * **/ - (BOOL)preConnectWithInterface:(NSString *)interface error:(NSError **)errPtr { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (delegate == nil) // Must have delegate set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; *errPtr = [self badConfigError:msg]; } return NO; } if (delegateQueue == NULL) // Must have delegate queue set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; *errPtr = [self badConfigError:msg]; } return NO; } if (![self isDisconnected]) // Must be disconnected { if (errPtr) { NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; *errPtr = [self badConfigError:msg]; } return NO; } BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { if (errPtr) { NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; *errPtr = [self badConfigError:msg]; } return NO; } if (interface) { NSMutableData *interface4 = nil; NSMutableData *interface6 = nil; [self getInterfaceAddress4:&interface4 address6:&interface6 fromDescription:interface port:0]; if ((interface4 == nil) && (interface6 == nil)) { if (errPtr) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; *errPtr = [self badParamError:msg]; } return NO; } if (isIPv4Disabled && (interface6 == nil)) { if (errPtr) { NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; *errPtr = [self badParamError:msg]; } return NO; } if (isIPv6Disabled && (interface4 == nil)) { if (errPtr) { NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; *errPtr = [self badParamError:msg]; } return NO; } connectInterface4 = interface4; connectInterface6 = interface6; } // Clear queues (spurious read/write requests post disconnect) [readQueue removeAllObjects]; [writeQueue removeAllObjects]; return YES; } - (BOOL)preConnectWithUrl:(NSURL *)url error:(NSError **)errPtr { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (delegate == nil) // Must have delegate set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate. Set a delegate first."; *errPtr = [self badConfigError:msg]; } return NO; } if (delegateQueue == NULL) // Must have delegate queue set { if (errPtr) { NSString *msg = @"Attempting to connect without a delegate queue. Set a delegate queue first."; *errPtr = [self badConfigError:msg]; } return NO; } if (![self isDisconnected]) // Must be disconnected { if (errPtr) { NSString *msg = @"Attempting to connect while connected or accepting connections. Disconnect first."; *errPtr = [self badConfigError:msg]; } return NO; } NSData *interface = [self getInterfaceAddressFromUrl:url]; if (interface == nil) { if (errPtr) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; *errPtr = [self badParamError:msg]; } return NO; } connectInterfaceUN = interface; // Clear queues (spurious read/write requests post disconnect) [readQueue removeAllObjects]; [writeQueue removeAllObjects]; return YES; } - (BOOL)connectToHost:(NSString*)host onPort:(uint16_t)port error:(NSError **)errPtr { return [self connectToHost:host onPort:port withTimeout:-1 error:errPtr]; } - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { return [self connectToHost:host onPort:port viaInterface:nil withTimeout:timeout error:errPtr]; } - (BOOL)connectToHost:(NSString *)inHost onPort:(uint16_t)port viaInterface:(NSString *)inInterface withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { LogTrace(); // Just in case immutable objects were passed NSString *host = [inHost copy]; NSString *interface = [inInterface copy]; __block BOOL result = NO; __block NSError *preConnectErr = nil; dispatch_block_t block = ^{ @autoreleasepool { // Check for problems with host parameter if ([host length] == 0) { NSString *msg = @"Invalid host parameter (nil or \"\"). Should be a domain name or IP address string."; preConnectErr = [self badParamError:msg]; return_from_block; } // Run through standard pre-connect checks if (![self preConnectWithInterface:interface error:&preConnectErr]) { return_from_block; } // We've made it past all the checks. // It's time to start the connection process. self->flags |= kSocketStarted; LogVerbose(@"Dispatching DNS lookup..."); // It's possible that the given host parameter is actually a NSMutableString. // So we want to copy it now, within this block that will be executed synchronously. // This way the asynchronous lookup block below doesn't have to worry about it changing. NSString *hostCpy = [host copy]; int aStateIndex = self->stateIndex; __weak GCDAsyncSocket *weakSelf = self; dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" NSError *lookupErr = nil; NSMutableArray *addresses = [[self class] lookupHost:hostCpy port:port error:&lookupErr]; __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; if (lookupErr) { dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { [strongSelf lookup:aStateIndex didFail:lookupErr]; }}); } else { NSData *address4 = nil; NSData *address6 = nil; for (NSData *address in addresses) { if (!address4 && [[self class] isIPv4Address:address]) { address4 = address; } else if (!address6 && [[self class] isIPv6Address:address]) { address6 = address; } } dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { [strongSelf lookup:aStateIndex didSucceedWithAddress4:address4 address6:address6]; }}); } #pragma clang diagnostic pop }}); [self startConnectTimeout:timeout]; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (errPtr) *errPtr = preConnectErr; return result; } - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr { return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:-1 error:errPtr]; } - (BOOL)connectToAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { return [self connectToAddress:remoteAddr viaInterface:nil withTimeout:timeout error:errPtr]; } - (BOOL)connectToAddress:(NSData *)inRemoteAddr viaInterface:(NSString *)inInterface withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { LogTrace(); // Just in case immutable objects were passed NSData *remoteAddr = [inRemoteAddr copy]; NSString *interface = [inInterface copy]; __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { // Check for problems with remoteAddr parameter NSData *address4 = nil; NSData *address6 = nil; if ([remoteAddr length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddr = (const struct sockaddr *)[remoteAddr bytes]; if (sockaddr->sa_family == AF_INET) { if ([remoteAddr length] == sizeof(struct sockaddr_in)) { address4 = remoteAddr; } } else if (sockaddr->sa_family == AF_INET6) { if ([remoteAddr length] == sizeof(struct sockaddr_in6)) { address6 = remoteAddr; } } } if ((address4 == nil) && (address6 == nil)) { NSString *msg = @"A valid IPv4 or IPv6 address was not given"; err = [self badParamError:msg]; return_from_block; } BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && (address4 != nil)) { NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; err = [self badParamError:msg]; return_from_block; } if (isIPv6Disabled && (address6 != nil)) { NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; err = [self badParamError:msg]; return_from_block; } // Run through standard pre-connect checks if (![self preConnectWithInterface:interface error:&err]) { return_from_block; } // We've made it past all the checks. // It's time to start the connection process. if (![self connectWithAddress4:address4 address6:address6 error:&err]) { return_from_block; } self->flags |= kSocketStarted; [self startConnectTimeout:timeout]; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (result == NO) { if (errPtr) *errPtr = err; } return result; } - (BOOL)connectToUrl:(NSURL *)url withTimeout:(NSTimeInterval)timeout error:(NSError **)errPtr { LogTrace(); __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { // Check for problems with host parameter if ([url.path length] == 0) { NSString *msg = @"Invalid unix domain socket url."; err = [self badParamError:msg]; return_from_block; } // Run through standard pre-connect checks if (![self preConnectWithUrl:url error:&err]) { return_from_block; } // We've made it past all the checks. // It's time to start the connection process. self->flags |= kSocketStarted; // Start the normal connection process NSError *connectError = nil; if (![self connectWithAddressUN:self->connectInterfaceUN error:&connectError]) { [self closeWithError:connectError]; return_from_block; } [self startConnectTimeout:timeout]; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (result == NO) { if (errPtr) *errPtr = err; } return result; } - (BOOL)connectToNetService:(NSNetService *)netService error:(NSError **)errPtr { NSArray* addresses = [netService addresses]; for (NSData* address in addresses) { BOOL result = [self connectToAddress:address error:errPtr]; if (result) { return YES; } } return NO; } - (void)lookup:(int)aStateIndex didSucceedWithAddress4:(NSData *)address4 address6:(NSData *)address6 { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(address4 || address6, @"Expected at least one valid address"); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring lookupDidSucceed, already disconnected"); // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } // Check for problems BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && (address6 == nil)) { NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address."; [self closeWithError:[self otherError:msg]]; return; } if (isIPv6Disabled && (address4 == nil)) { NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address."; [self closeWithError:[self otherError:msg]]; return; } // Start the normal connection process NSError *err = nil; if (![self connectWithAddress4:address4 address6:address6 error:&err]) { [self closeWithError:err]; } } /** * This method is called if the DNS lookup fails. * This method is executed on the socketQueue. * * Since the DNS lookup executed synchronously on a global concurrent queue, * the original connection request may have already been cancelled or timed-out by the time this method is invoked. * The lookupIndex tells us whether the lookup is still valid or not. **/ - (void)lookup:(int)aStateIndex didFail:(NSError *)error { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring lookup:didFail: - already disconnected"); // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } [self endConnectTimeout]; [self closeWithError:error]; } - (BOOL)bindSocket:(int)socketFD toInterface:(NSData *)connectInterface error:(NSError **)errPtr { // Bind the socket to the desired interface (if needed) if (connectInterface) { LogVerbose(@"Binding socket..."); if ([[self class] portFromAddress:connectInterface] > 0) { // Since we're going to be binding to a specific port, // we should turn on reuseaddr to allow us to override sockets in time_wait. int reuseOn = 1; setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); } const struct sockaddr *interfaceAddr = (const struct sockaddr *)[connectInterface bytes]; int result = bind(socketFD, interfaceAddr, (socklen_t)[connectInterface length]); if (result != 0) { if (errPtr) *errPtr = [self errorWithErrno:errno reason:@"Error in bind() function"]; return NO; } } return YES; } - (int)createSocket:(int)family connectInterface:(NSData *)connectInterface errPtr:(NSError **)errPtr { int socketFD = socket(family, SOCK_STREAM, 0); if (socketFD == SOCKET_NULL) { if (errPtr) *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; return socketFD; } if (![self bindSocket:socketFD toInterface:connectInterface error:errPtr]) { [self closeSocket:socketFD]; return SOCKET_NULL; } // Prevent SIGPIPE signals int nosigpipe = 1; setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); return socketFD; } - (void)connectSocket:(int)socketFD address:(NSData *)address stateIndex:(int)aStateIndex { // If there already is a socket connected, we close socketFD and return if (self.isConnected) { [self closeSocket:socketFD]; return; } // Start the connection process in a background queue __weak GCDAsyncSocket *weakSelf = self; dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" int result = connect(socketFD, (const struct sockaddr *)[address bytes], (socklen_t)[address length]); int err = errno; __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; dispatch_async(strongSelf->socketQueue, ^{ @autoreleasepool { if (strongSelf.isConnected) { [strongSelf closeSocket:socketFD]; return_from_block; } if (result == 0) { [self closeUnusedSocket:socketFD]; [strongSelf didConnect:aStateIndex]; } else { [strongSelf closeSocket:socketFD]; // If there are no more sockets trying to connect, we inform the error to the delegate if (strongSelf.socket4FD == SOCKET_NULL && strongSelf.socket6FD == SOCKET_NULL) { NSError *error = [strongSelf errorWithErrno:err reason:@"Error in connect() function"]; [strongSelf didNotConnect:aStateIndex error:error]; } } }}); #pragma clang diagnostic pop }); LogVerbose(@"Connecting..."); } - (void)closeSocket:(int)socketFD { if (socketFD != SOCKET_NULL && (socketFD == socket6FD || socketFD == socket4FD)) { close(socketFD); if (socketFD == socket4FD) { LogVerbose(@"close(socket4FD)"); socket4FD = SOCKET_NULL; } else if (socketFD == socket6FD) { LogVerbose(@"close(socket6FD)"); socket6FD = SOCKET_NULL; } } } - (void)closeUnusedSocket:(int)usedSocketFD { if (usedSocketFD != socket4FD) { [self closeSocket:socket4FD]; } else if (usedSocketFD != socket6FD) { [self closeSocket:socket6FD]; } } - (BOOL)connectWithAddress4:(NSData *)address4 address6:(NSData *)address6 error:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); LogVerbose(@"IPv4: %@:%hu", [[self class] hostFromAddress:address4], [[self class] portFromAddress:address4]); LogVerbose(@"IPv6: %@:%hu", [[self class] hostFromAddress:address6], [[self class] portFromAddress:address6]); // Determine socket type BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; // Create and bind the sockets if (address4) { LogVerbose(@"Creating IPv4 socket"); socket4FD = [self createSocket:AF_INET connectInterface:connectInterface4 errPtr:errPtr]; } if (address6) { LogVerbose(@"Creating IPv6 socket"); socket6FD = [self createSocket:AF_INET6 connectInterface:connectInterface6 errPtr:errPtr]; } if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) { return NO; } int socketFD, alternateSocketFD; NSData *address, *alternateAddress; if ((preferIPv6 && socket6FD != SOCKET_NULL) || socket4FD == SOCKET_NULL) { socketFD = socket6FD; alternateSocketFD = socket4FD; address = address6; alternateAddress = address4; } else { socketFD = socket4FD; alternateSocketFD = socket6FD; address = address4; alternateAddress = address6; } int aStateIndex = stateIndex; [self connectSocket:socketFD address:address stateIndex:aStateIndex]; if (alternateAddress) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(alternateAddressDelay * NSEC_PER_SEC)), socketQueue, ^{ [self connectSocket:alternateSocketFD address:alternateAddress stateIndex:aStateIndex]; }); } return YES; } - (BOOL)connectWithAddressUN:(NSData *)address error:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); // Create the socket int socketFD; LogVerbose(@"Creating unix domain socket"); socketUN = socket(AF_UNIX, SOCK_STREAM, 0); socketFD = socketUN; if (socketFD == SOCKET_NULL) { if (errPtr) *errPtr = [self errorWithErrno:errno reason:@"Error in socket() function"]; return NO; } // Bind the socket to the desired interface (if needed) LogVerbose(@"Binding socket..."); int reuseOn = 1; setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseOn, sizeof(reuseOn)); // const struct sockaddr *interfaceAddr = (const struct sockaddr *)[address bytes]; // // int result = bind(socketFD, interfaceAddr, (socklen_t)[address length]); // if (result != 0) // { // if (errPtr) // *errPtr = [self errnoErrorWithReason:@"Error in bind() function"]; // // return NO; // } // Prevent SIGPIPE signals int nosigpipe = 1; setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); // Start the connection process in a background queue int aStateIndex = stateIndex; dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ const struct sockaddr *addr = (const struct sockaddr *)[address bytes]; int result = connect(socketFD, addr, addr->sa_len); if (result == 0) { dispatch_async(self->socketQueue, ^{ @autoreleasepool { [self didConnect:aStateIndex]; }}); } else { // TODO: Bad file descriptor perror("connect"); NSError *error = [self errorWithErrno:errno reason:@"Error in connect() function"]; dispatch_async(self->socketQueue, ^{ @autoreleasepool { [self didNotConnect:aStateIndex error:error]; }}); } }); LogVerbose(@"Connecting..."); return YES; } - (void)didConnect:(int)aStateIndex { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring didConnect, already disconnected"); // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } flags |= kConnected; [self endConnectTimeout]; #if TARGET_OS_IPHONE // The endConnectTimeout method executed above incremented the stateIndex. aStateIndex = stateIndex; #endif // Setup read/write streams (as workaround for specific shortcomings in the iOS platform) // // Note: // There may be configuration options that must be set by the delegate before opening the streams. // The primary example is the kCFStreamNetworkServiceTypeVoIP flag, which only works on an unopened stream. // // Thus we wait until after the socket:didConnectToHost:port: delegate method has completed. // This gives the delegate time to properly configure the streams if needed. dispatch_block_t SetupStreamsPart1 = ^{ #if TARGET_OS_IPHONE if (![self createReadAndWriteStream]) { [self closeWithError:[self otherError:@"Error creating CFStreams"]]; return; } if (![self registerForStreamCallbacksIncludingReadWrite:NO]) { [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; return; } #endif }; dispatch_block_t SetupStreamsPart2 = ^{ #if TARGET_OS_IPHONE if (aStateIndex != self->stateIndex) { // The socket has been disconnected. return; } if (![self addStreamsToRunLoop]) { [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; return; } if (![self openStreams]) { [self closeWithError:[self otherError:@"Error creating CFStreams"]]; return; } #endif }; // Notify delegate NSString *host = [self connectedHost]; uint16_t port = [self connectedPort]; NSURL *url = [self connectedUrl]; __strong id theDelegate = delegate; if (delegateQueue && host != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToHost:port:)]) { SetupStreamsPart1(); dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didConnectToHost:host port:port]; dispatch_async(self->socketQueue, ^{ @autoreleasepool { SetupStreamsPart2(); }}); }}); } else if (delegateQueue && url != nil && [theDelegate respondsToSelector:@selector(socket:didConnectToUrl:)]) { SetupStreamsPart1(); dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didConnectToUrl:url]; dispatch_async(self->socketQueue, ^{ @autoreleasepool { SetupStreamsPart2(); }}); }}); } else { SetupStreamsPart1(); SetupStreamsPart2(); } // Get the connected socket int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; // Enable non-blocking IO on the socket int result = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (result == -1) { NSString *errMsg = @"Error enabling non-blocking IO on socket (fcntl)"; [self closeWithError:[self otherError:errMsg]]; return; } // Setup our read/write sources [self setupReadAndWriteSourcesForNewlyConnectedSocket:socketFD]; // Dequeue any pending read/write requests [self maybeDequeueRead]; [self maybeDequeueWrite]; } - (void)didNotConnect:(int)aStateIndex error:(NSError *)error { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring didNotConnect, already disconnected"); // The connect operation has been cancelled. // That is, socket was disconnected, or connection has already timed out. return; } [self closeWithError:error]; } - (void)startConnectTimeout:(NSTimeInterval)timeout { if (timeout >= 0.0) { connectTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(connectTimer, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; [strongSelf doConnectTimeout]; #pragma clang diagnostic pop }}); #if !OS_OBJECT_USE_OBJC dispatch_source_t theConnectTimer = connectTimer; dispatch_source_set_cancel_handler(connectTimer, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"dispatch_release(connectTimer)"); dispatch_release(theConnectTimer); #pragma clang diagnostic pop }); #endif dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); dispatch_source_set_timer(connectTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(connectTimer); } } - (void)endConnectTimeout { LogTrace(); if (connectTimer) { dispatch_source_cancel(connectTimer); connectTimer = NULL; } // Increment stateIndex. // This will prevent us from processing results from any related background asynchronous operations. // // Note: This should be called from close method even if connectTimer is NULL. // This is because one might disconnect a socket prior to a successful connection which had no timeout. stateIndex++; if (connectInterface4) { connectInterface4 = nil; } if (connectInterface6) { connectInterface6 = nil; } } - (void)doConnectTimeout { LogTrace(); [self endConnectTimeout]; [self closeWithError:[self connectTimeoutError]]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Disconnecting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)closeWithError:(NSError *)error { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); [self endConnectTimeout]; if (currentRead != nil) [self endCurrentRead]; if (currentWrite != nil) [self endCurrentWrite]; [readQueue removeAllObjects]; [writeQueue removeAllObjects]; [preBuffer reset]; #if TARGET_OS_IPHONE { if (readStream || writeStream) { [self removeStreamsFromRunLoop]; if (readStream) { CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL); CFReadStreamClose(readStream); CFRelease(readStream); readStream = NULL; } if (writeStream) { CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL); CFWriteStreamClose(writeStream); CFRelease(writeStream); writeStream = NULL; } } } #endif [sslPreBuffer reset]; sslErrCode = lastSSLHandshakeError = noErr; if (sslContext) { // Getting a linker error here about the SSLx() functions? // You need to add the Security Framework to your application. SSLClose(sslContext); #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) CFRelease(sslContext); #else SSLDisposeContext(sslContext); #endif sslContext = NULL; } // For some crazy reason (in my opinion), cancelling a dispatch source doesn't // invoke the cancel handler if the dispatch source is paused. // So we have to unpause the source if needed. // This allows the cancel handler to be run, which in turn releases the source and closes the socket. if (!accept4Source && !accept6Source && !acceptUNSource && !readSource && !writeSource) { LogVerbose(@"manually closing close"); if (socket4FD != SOCKET_NULL) { LogVerbose(@"close(socket4FD)"); close(socket4FD); socket4FD = SOCKET_NULL; } if (socket6FD != SOCKET_NULL) { LogVerbose(@"close(socket6FD)"); close(socket6FD); socket6FD = SOCKET_NULL; } if (socketUN != SOCKET_NULL) { LogVerbose(@"close(socketUN)"); close(socketUN); socketUN = SOCKET_NULL; unlink(socketUrl.path.fileSystemRepresentation); socketUrl = nil; } } else { if (accept4Source) { LogVerbose(@"dispatch_source_cancel(accept4Source)"); dispatch_source_cancel(accept4Source); // We never suspend accept4Source accept4Source = NULL; } if (accept6Source) { LogVerbose(@"dispatch_source_cancel(accept6Source)"); dispatch_source_cancel(accept6Source); // We never suspend accept6Source accept6Source = NULL; } if (acceptUNSource) { LogVerbose(@"dispatch_source_cancel(acceptUNSource)"); dispatch_source_cancel(acceptUNSource); // We never suspend acceptUNSource acceptUNSource = NULL; } if (readSource) { LogVerbose(@"dispatch_source_cancel(readSource)"); dispatch_source_cancel(readSource); [self resumeReadSource]; readSource = NULL; } if (writeSource) { LogVerbose(@"dispatch_source_cancel(writeSource)"); dispatch_source_cancel(writeSource); [self resumeWriteSource]; writeSource = NULL; } // The sockets will be closed by the cancel handlers of the corresponding source socket4FD = SOCKET_NULL; socket6FD = SOCKET_NULL; socketUN = SOCKET_NULL; } // If the client has passed the connect/accept method, then the connection has at least begun. // Notify delegate that it is now ending. BOOL shouldCallDelegate = (flags & kSocketStarted) ? YES : NO; BOOL isDeallocating = (flags & kDealloc) ? YES : NO; // Clear stored socket info and all flags (config remains as is) socketFDBytesAvailable = 0; flags = 0; sslWriteCachedLength = 0; if (shouldCallDelegate) { __strong id theDelegate = delegate; __strong id theSelf = isDeallocating ? nil : self; if (delegateQueue && [theDelegate respondsToSelector: @selector(socketDidDisconnect:withError:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socketDidDisconnect:theSelf withError:error]; }}); } } } - (void)disconnect { dispatch_block_t block = ^{ @autoreleasepool { if (self->flags & kSocketStarted) { [self closeWithError:nil]; } }}; // Synchronous disconnection, as documented in the header file if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); } - (void)disconnectAfterReading { dispatch_async(socketQueue, ^{ @autoreleasepool { if (self->flags & kSocketStarted) { self->flags |= (kForbidReadsWrites | kDisconnectAfterReads); [self maybeClose]; } }}); } - (void)disconnectAfterWriting { dispatch_async(socketQueue, ^{ @autoreleasepool { if (self->flags & kSocketStarted) { self->flags |= (kForbidReadsWrites | kDisconnectAfterWrites); [self maybeClose]; } }}); } - (void)disconnectAfterReadingAndWriting { dispatch_async(socketQueue, ^{ @autoreleasepool { if (self->flags & kSocketStarted) { self->flags |= (kForbidReadsWrites | kDisconnectAfterReads | kDisconnectAfterWrites); [self maybeClose]; } }}); } /** * Closes the socket if possible. * That is, if all writes have completed, and we're set to disconnect after writing, * or if all reads have completed, and we're set to disconnect after reading. **/ - (void)maybeClose { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); BOOL shouldClose = NO; if (flags & kDisconnectAfterReads) { if (([readQueue count] == 0) && (currentRead == nil)) { if (flags & kDisconnectAfterWrites) { if (([writeQueue count] == 0) && (currentWrite == nil)) { shouldClose = YES; } } else { shouldClose = YES; } } } else if (flags & kDisconnectAfterWrites) { if (([writeQueue count] == 0) && (currentWrite == nil)) { shouldClose = YES; } } if (shouldClose) { [self closeWithError:nil]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Errors //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (NSError *)badConfigError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadConfigError userInfo:userInfo]; } - (NSError *)badParamError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketBadParamError userInfo:userInfo]; } + (NSError *)gaiError:(int)gai_error { NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; } - (NSError *)errorWithErrno:(int)err reason:(NSString *)reason { NSString *errMsg = [NSString stringWithUTF8String:strerror(err)]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg, NSLocalizedFailureReasonErrorKey : reason}; return [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:userInfo]; } - (NSError *)errnoError { NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; } - (NSError *)sslError:(OSStatus)ssl_error { NSString *msg = @"Error code definition can be found in Apple's SecureTransport.h"; NSDictionary *userInfo = @{NSLocalizedRecoverySuggestionErrorKey : msg}; return [NSError errorWithDomain:@"kCFStreamErrorDomainSSL" code:ssl_error userInfo:userInfo]; } - (NSError *)connectTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketConnectTimeoutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Attempt to connect to host timed out", nil); NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketConnectTimeoutError userInfo:userInfo]; } /** * Returns a standard AsyncSocket maxed out error. **/ - (NSError *)readMaxedOutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadMaxedOutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Read operation reached set maximum length", nil); NSDictionary *info = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadMaxedOutError userInfo:info]; } /** * Returns a standard AsyncSocket write timeout error. **/ - (NSError *)readTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketReadTimeoutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Read operation timed out", nil); NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketReadTimeoutError userInfo:userInfo]; } /** * Returns a standard AsyncSocket write timeout error. **/ - (NSError *)writeTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketWriteTimeoutError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Write operation timed out", nil); NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketWriteTimeoutError userInfo:userInfo]; } - (NSError *)connectionClosedError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncSocketClosedError", @"GCDAsyncSocket", [NSBundle mainBundle], @"Socket closed by remote peer", nil); NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketClosedError userInfo:userInfo]; } - (NSError *)otherError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncSocketErrorDomain code:GCDAsyncSocketOtherError userInfo:userInfo]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Diagnostics //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)isDisconnected { __block BOOL result = NO; dispatch_block_t block = ^{ result = (self->flags & kSocketStarted) ? NO : YES; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (BOOL)isConnected { __block BOOL result = NO; dispatch_block_t block = ^{ result = (self->flags & kConnected) ? YES : NO; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (NSString *)connectedHost { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socket4FD != SOCKET_NULL) return [self connectedHostFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self connectedHostFromSocket6:socket6FD]; return nil; } else { __block NSString *result = nil; dispatch_sync(socketQueue, ^{ @autoreleasepool { if (self->socket4FD != SOCKET_NULL) result = [self connectedHostFromSocket4:self->socket4FD]; else if (self->socket6FD != SOCKET_NULL) result = [self connectedHostFromSocket6:self->socket6FD]; }}); return result; } } - (uint16_t)connectedPort { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socket4FD != SOCKET_NULL) return [self connectedPortFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self connectedPortFromSocket6:socket6FD]; return 0; } else { __block uint16_t result = 0; dispatch_sync(socketQueue, ^{ // No need for autorelease pool if (self->socket4FD != SOCKET_NULL) result = [self connectedPortFromSocket4:self->socket4FD]; else if (self->socket6FD != SOCKET_NULL) result = [self connectedPortFromSocket6:self->socket6FD]; }); return result; } } - (NSURL *)connectedUrl { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socketUN != SOCKET_NULL) return [self connectedUrlFromSocketUN:socketUN]; return nil; } else { __block NSURL *result = nil; dispatch_sync(socketQueue, ^{ @autoreleasepool { if (self->socketUN != SOCKET_NULL) result = [self connectedUrlFromSocketUN:self->socketUN]; }}); return result; } } - (NSString *)localHost { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socket4FD != SOCKET_NULL) return [self localHostFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self localHostFromSocket6:socket6FD]; return nil; } else { __block NSString *result = nil; dispatch_sync(socketQueue, ^{ @autoreleasepool { if (self->socket4FD != SOCKET_NULL) result = [self localHostFromSocket4:self->socket4FD]; else if (self->socket6FD != SOCKET_NULL) result = [self localHostFromSocket6:self->socket6FD]; }}); return result; } } - (uint16_t)localPort { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (socket4FD != SOCKET_NULL) return [self localPortFromSocket4:socket4FD]; if (socket6FD != SOCKET_NULL) return [self localPortFromSocket6:socket6FD]; return 0; } else { __block uint16_t result = 0; dispatch_sync(socketQueue, ^{ // No need for autorelease pool if (self->socket4FD != SOCKET_NULL) result = [self localPortFromSocket4:self->socket4FD]; else if (self->socket6FD != SOCKET_NULL) result = [self localPortFromSocket6:self->socket6FD]; }); return result; } } - (NSString *)connectedHost4 { if (socket4FD != SOCKET_NULL) return [self connectedHostFromSocket4:socket4FD]; return nil; } - (NSString *)connectedHost6 { if (socket6FD != SOCKET_NULL) return [self connectedHostFromSocket6:socket6FD]; return nil; } - (uint16_t)connectedPort4 { if (socket4FD != SOCKET_NULL) return [self connectedPortFromSocket4:socket4FD]; return 0; } - (uint16_t)connectedPort6 { if (socket6FD != SOCKET_NULL) return [self connectedPortFromSocket6:socket6FD]; return 0; } - (NSString *)localHost4 { if (socket4FD != SOCKET_NULL) return [self localHostFromSocket4:socket4FD]; return nil; } - (NSString *)localHost6 { if (socket6FD != SOCKET_NULL) return [self localHostFromSocket6:socket6FD]; return nil; } - (uint16_t)localPort4 { if (socket4FD != SOCKET_NULL) return [self localPortFromSocket4:socket4FD]; return 0; } - (uint16_t)localPort6 { if (socket6FD != SOCKET_NULL) return [self localPortFromSocket6:socket6FD]; return 0; } - (NSString *)connectedHostFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return nil; } return [[self class] hostFromSockaddr4:&sockaddr4]; } - (NSString *)connectedHostFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return nil; } return [[self class] hostFromSockaddr6:&sockaddr6]; } - (uint16_t)connectedPortFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getpeername(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return 0; } return [[self class] portFromSockaddr4:&sockaddr4]; } - (uint16_t)connectedPortFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getpeername(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return 0; } return [[self class] portFromSockaddr6:&sockaddr6]; } - (NSURL *)connectedUrlFromSocketUN:(int)socketFD { struct sockaddr_un sockaddr; socklen_t sockaddrlen = sizeof(sockaddr); if (getpeername(socketFD, (struct sockaddr *)&sockaddr, &sockaddrlen) < 0) { return 0; } return [[self class] urlFromSockaddrUN:&sockaddr]; } - (NSString *)localHostFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return nil; } return [[self class] hostFromSockaddr4:&sockaddr4]; } - (NSString *)localHostFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return nil; } return [[self class] hostFromSockaddr6:&sockaddr6]; } - (uint16_t)localPortFromSocket4:(int)socketFD { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) < 0) { return 0; } return [[self class] portFromSockaddr4:&sockaddr4]; } - (uint16_t)localPortFromSocket6:(int)socketFD { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) < 0) { return 0; } return [[self class] portFromSockaddr6:&sockaddr6]; } - (NSData *)connectedAddress { __block NSData *result = nil; dispatch_block_t block = ^{ if (self->socket4FD != SOCKET_NULL) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getpeername(self->socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; } } if (self->socket6FD != SOCKET_NULL) { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getpeername(self->socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; } } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (NSData *)localAddress { __block NSData *result = nil; dispatch_block_t block = ^{ if (self->socket4FD != SOCKET_NULL) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getsockname(self->socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr4 length:sockaddr4len]; } } if (self->socket6FD != SOCKET_NULL) { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getsockname(self->socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { result = [[NSData alloc] initWithBytes:&sockaddr6 length:sockaddr6len]; } } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (BOOL)isIPv4 { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return (socket4FD != SOCKET_NULL); } else { __block BOOL result = NO; dispatch_sync(socketQueue, ^{ result = (self->socket4FD != SOCKET_NULL); }); return result; } } - (BOOL)isIPv6 { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return (socket6FD != SOCKET_NULL); } else { __block BOOL result = NO; dispatch_sync(socketQueue, ^{ result = (self->socket6FD != SOCKET_NULL); }); return result; } } - (BOOL)isSecure { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return (flags & kSocketSecure) ? YES : NO; } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = (self->flags & kSocketSecure) ? YES : NO; }); return result; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Utilities //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * Finds the address of an interface description. * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). * * The interface description may optionally contain a port number at the end, separated by a colon. * If a non-zero port parameter is provided, any port number in the interface description is ignored. * * The returned value is a 'struct sockaddr' wrapped in an NSMutableData object. **/ - (void)getInterfaceAddress4:(NSMutableData **)interfaceAddr4Ptr address6:(NSMutableData **)interfaceAddr6Ptr fromDescription:(NSString *)interfaceDescription port:(uint16_t)port { NSMutableData *addr4 = nil; NSMutableData *addr6 = nil; NSString *interface = nil; NSArray *components = [interfaceDescription componentsSeparatedByString:@":"]; if ([components count] > 0) { NSString *temp = [components objectAtIndex:0]; if ([temp length] > 0) { interface = temp; } } if ([components count] > 1 && port == 0) { NSString *temp = [components objectAtIndex:1]; long portL = strtol([temp UTF8String], NULL, 10); if (portL > 0 && portL <= UINT16_MAX) { port = (uint16_t)portL; } } if (interface == nil) { // ANY address struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); sockaddr4.sin_len = sizeof(sockaddr4); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); sockaddr6.sin6_len = sizeof(sockaddr6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_any; addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } else if ([interface isEqualToString:@"localhost"] || [interface isEqualToString:@"loopback"]) { // LOOPBACK address struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); sockaddr4.sin_len = sizeof(sockaddr4); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); sockaddr6.sin6_len = sizeof(sockaddr6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_loopback; addr4 = [NSMutableData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSMutableData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } else { const char *iface = [interface UTF8String]; struct ifaddrs *addrs; const struct ifaddrs *cursor; if ((getifaddrs(&addrs) == 0)) { cursor = addrs; while (cursor != NULL) { if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) { // IPv4 struct sockaddr_in nativeAddr4; memcpy(&nativeAddr4, cursor->ifa_addr, sizeof(nativeAddr4)); if (strcmp(cursor->ifa_name, iface) == 0) { // Name match nativeAddr4.sin_port = htons(port); addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } else { char ip[INET_ADDRSTRLEN]; const char *conversion = inet_ntop(AF_INET, &nativeAddr4.sin_addr, ip, sizeof(ip)); if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match nativeAddr4.sin_port = htons(port); addr4 = [NSMutableData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } } } else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) { // IPv6 struct sockaddr_in6 nativeAddr6; memcpy(&nativeAddr6, cursor->ifa_addr, sizeof(nativeAddr6)); if (strcmp(cursor->ifa_name, iface) == 0) { // Name match nativeAddr6.sin6_port = htons(port); addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } else { char ip[INET6_ADDRSTRLEN]; const char *conversion = inet_ntop(AF_INET6, &nativeAddr6.sin6_addr, ip, sizeof(ip)); if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match nativeAddr6.sin6_port = htons(port); addr6 = [NSMutableData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } } } cursor = cursor->ifa_next; } freeifaddrs(addrs); } } if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; } - (NSData *)getInterfaceAddressFromUrl:(NSURL *)url { NSString *path = url.path; if (path.length == 0) { return nil; } struct sockaddr_un nativeAddr; nativeAddr.sun_family = AF_UNIX; strlcpy(nativeAddr.sun_path, path.fileSystemRepresentation, sizeof(nativeAddr.sun_path)); nativeAddr.sun_len = (unsigned char)SUN_LEN(&nativeAddr); NSData *interface = [NSData dataWithBytes:&nativeAddr length:sizeof(struct sockaddr_un)]; return interface; } - (void)setupReadAndWriteSourcesForNewlyConnectedSocket:(int)socketFD { readSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socketFD, 0, socketQueue); writeSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socketFD, 0, socketQueue); // Setup event handlers __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(readSource, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; LogVerbose(@"readEventBlock"); strongSelf->socketFDBytesAvailable = dispatch_source_get_data(strongSelf->readSource); LogVerbose(@"socketFDBytesAvailable: %lu", strongSelf->socketFDBytesAvailable); if (strongSelf->socketFDBytesAvailable > 0) [strongSelf doReadData]; else [strongSelf doReadEOF]; #pragma clang diagnostic pop }}); dispatch_source_set_event_handler(writeSource, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; LogVerbose(@"writeEventBlock"); strongSelf->flags |= kSocketCanAcceptBytes; [strongSelf doWriteData]; #pragma clang diagnostic pop }}); // Setup cancel handlers __block int socketFDRefCount = 2; #if !OS_OBJECT_USE_OBJC dispatch_source_t theReadSource = readSource; dispatch_source_t theWriteSource = writeSource; #endif dispatch_source_set_cancel_handler(readSource, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"readCancelBlock"); #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(readSource)"); dispatch_release(theReadSource); #endif if (--socketFDRefCount == 0) { LogVerbose(@"close(socketFD)"); close(socketFD); } #pragma clang diagnostic pop }); dispatch_source_set_cancel_handler(writeSource, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"writeCancelBlock"); #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(writeSource)"); dispatch_release(theWriteSource); #endif if (--socketFDRefCount == 0) { LogVerbose(@"close(socketFD)"); close(socketFD); } #pragma clang diagnostic pop }); // We will not be able to read until data arrives. // But we should be able to write immediately. socketFDBytesAvailable = 0; flags &= ~kReadSourceSuspended; LogVerbose(@"dispatch_resume(readSource)"); dispatch_resume(readSource); flags |= kSocketCanAcceptBytes; flags |= kWriteSourceSuspended; } - (BOOL)usingCFStreamForTLS { #if TARGET_OS_IPHONE if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) { // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. return YES; } #endif return NO; } - (BOOL)usingSecureTransportForTLS { // Invoking this method is equivalent to ![self usingCFStreamForTLS] (just more readable) #if TARGET_OS_IPHONE if ((flags & kSocketSecure) && (flags & kUsingCFStreamForTLS)) { // The startTLS method was given the GCDAsyncSocketUseCFStreamForTLS flag. return NO; } #endif return YES; } - (void)suspendReadSource { if (!(flags & kReadSourceSuspended)) { LogVerbose(@"dispatch_suspend(readSource)"); dispatch_suspend(readSource); flags |= kReadSourceSuspended; } } - (void)resumeReadSource { if (flags & kReadSourceSuspended) { LogVerbose(@"dispatch_resume(readSource)"); dispatch_resume(readSource); flags &= ~kReadSourceSuspended; } } - (void)suspendWriteSource { if (!(flags & kWriteSourceSuspended)) { LogVerbose(@"dispatch_suspend(writeSource)"); dispatch_suspend(writeSource); flags |= kWriteSourceSuspended; } } - (void)resumeWriteSource { if (flags & kWriteSourceSuspended) { LogVerbose(@"dispatch_resume(writeSource)"); dispatch_resume(writeSource); flags &= ~kWriteSourceSuspended; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Reading //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)readDataWithTimeout:(NSTimeInterval)timeout tag:(long)tag { [self readDataWithTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; } - (void)readDataWithTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag { [self readDataWithTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; } - (void)readDataWithTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)length tag:(long)tag { if (offset > [buffer length]) { LogWarn(@"Cannot read: offset > [buffer length]"); return; } GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:length timeout:timeout readLength:0 terminator:nil tag:tag]; dispatch_async(socketQueue, ^{ @autoreleasepool { LogTrace(); if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) { [self->readQueue addObject:packet]; [self maybeDequeueRead]; } }}); // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout tag:(long)tag { [self readDataToLength:length withTimeout:timeout buffer:nil bufferOffset:0 tag:tag]; } - (void)readDataToLength:(NSUInteger)length withTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag { if (length == 0) { LogWarn(@"Cannot read: length == 0"); return; } if (offset > [buffer length]) { LogWarn(@"Cannot read: offset > [buffer length]"); return; } GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:0 timeout:timeout readLength:length terminator:nil tag:tag]; dispatch_async(socketQueue, ^{ @autoreleasepool { LogTrace(); if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) { [self->readQueue addObject:packet]; [self maybeDequeueRead]; } }}); // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:0 tag:tag]; } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset tag:(long)tag { [self readDataToData:data withTimeout:timeout buffer:buffer bufferOffset:offset maxLength:0 tag:tag]; } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout maxLength:(NSUInteger)length tag:(long)tag { [self readDataToData:data withTimeout:timeout buffer:nil bufferOffset:0 maxLength:length tag:tag]; } - (void)readDataToData:(NSData *)data withTimeout:(NSTimeInterval)timeout buffer:(NSMutableData *)buffer bufferOffset:(NSUInteger)offset maxLength:(NSUInteger)maxLength tag:(long)tag { if ([data length] == 0) { LogWarn(@"Cannot read: [data length] == 0"); return; } if (offset > [buffer length]) { LogWarn(@"Cannot read: offset > [buffer length]"); return; } if (maxLength > 0 && maxLength < [data length]) { LogWarn(@"Cannot read: maxLength > 0 && maxLength < [data length]"); return; } GCDAsyncReadPacket *packet = [[GCDAsyncReadPacket alloc] initWithData:buffer startOffset:offset maxLength:maxLength timeout:timeout readLength:0 terminator:data tag:tag]; dispatch_async(socketQueue, ^{ @autoreleasepool { LogTrace(); if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) { [self->readQueue addObject:packet]; [self maybeDequeueRead]; } }}); // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } - (float)progressOfReadReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr { __block float result = 0.0F; dispatch_block_t block = ^{ if (!self->currentRead || ![self->currentRead isKindOfClass:[GCDAsyncReadPacket class]]) { // We're not reading anything right now. if (tagPtr != NULL) *tagPtr = 0; if (donePtr != NULL) *donePtr = 0; if (totalPtr != NULL) *totalPtr = 0; result = NAN; } else { // It's only possible to know the progress of our read if we're reading to a certain length. // If we're reading to data, we of course have no idea when the data will arrive. // If we're reading to timeout, then we have no idea when the next chunk of data will arrive. NSUInteger done = self->currentRead->bytesDone; NSUInteger total = self->currentRead->readLength; if (tagPtr != NULL) *tagPtr = self->currentRead->tag; if (donePtr != NULL) *donePtr = done; if (totalPtr != NULL) *totalPtr = total; if (total > 0) result = (float)done / (float)total; else result = 1.0F; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } /** * This method starts a new read, if needed. * * It is called when: * - a user requests a read * - after a read request has finished (to handle the next request) * - immediately after the socket opens to handle any pending requests * * This method also handles auto-disconnect post read/write completion. **/ - (void)maybeDequeueRead { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); // If we're not currently processing a read AND we have an available read stream if ((currentRead == nil) && (flags & kConnected)) { if ([readQueue count] > 0) { // Dequeue the next object in the write queue currentRead = [readQueue objectAtIndex:0]; [readQueue removeObjectAtIndex:0]; if ([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]]) { LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); // Attempt to start TLS flags |= kStartingReadTLS; // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set [self maybeStartTLS]; } else { LogVerbose(@"Dequeued GCDAsyncReadPacket"); // Setup read timer (if needed) [self setupReadTimerWithTimeout:currentRead->timeout]; // Immediately read, if possible [self doReadData]; } } else if (flags & kDisconnectAfterReads) { if (flags & kDisconnectAfterWrites) { if (([writeQueue count] == 0) && (currentWrite == nil)) { [self closeWithError:nil]; } } else { [self closeWithError:nil]; } } else if (flags & kSocketSecure) { [self flushSSLBuffers]; // Edge case: // // We just drained all data from the ssl buffers, // and all known data from the socket (socketFDBytesAvailable). // // If we didn't get any data from this process, // then we may have reached the end of the TCP stream. // // Be sure callbacks are enabled so we're notified about a disconnection. if ([preBuffer availableBytes] == 0) { if ([self usingCFStreamForTLS]) { // Callbacks never disabled } else { [self resumeReadSource]; } } } } } - (void)flushSSLBuffers { LogTrace(); NSAssert((flags & kSocketSecure), @"Cannot flush ssl buffers on non-secure socket"); if ([preBuffer availableBytes] > 0) { // Only flush the ssl buffers if the prebuffer is empty. // This is to avoid growing the prebuffer inifinitely large. return; } #if TARGET_OS_IPHONE if ([self usingCFStreamForTLS]) { if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) { LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); CFIndex defaultBytesToRead = (1024 * 4); [preBuffer ensureCapacityForWrite:defaultBytesToRead]; uint8_t *buffer = [preBuffer writeBuffer]; CFIndex result = CFReadStreamRead(readStream, buffer, defaultBytesToRead); LogVerbose(@"%@ - CFReadStreamRead(): result = %i", THIS_METHOD, (int)result); if (result > 0) { [preBuffer didWrite:result]; } flags &= ~kSecureSocketHasBytesAvailable; } return; } #endif __block NSUInteger estimatedBytesAvailable = 0; dispatch_block_t updateEstimatedBytesAvailable = ^{ // Figure out if there is any data available to be read // // socketFDBytesAvailable <- Number of encrypted bytes we haven't read from the bsd socket // [sslPreBuffer availableBytes] <- Number of encrypted bytes we've buffered from bsd socket // sslInternalBufSize <- Number of decrypted bytes SecureTransport has buffered // // We call the variable "estimated" because we don't know how many decrypted bytes we'll get // from the encrypted bytes in the sslPreBuffer. // However, we do know this is an upper bound on the estimation. estimatedBytesAvailable = self->socketFDBytesAvailable + [self->sslPreBuffer availableBytes]; size_t sslInternalBufSize = 0; SSLGetBufferedReadSize(self->sslContext, &sslInternalBufSize); estimatedBytesAvailable += sslInternalBufSize; }; updateEstimatedBytesAvailable(); if (estimatedBytesAvailable > 0) { LogVerbose(@"%@ - Flushing ssl buffers into prebuffer...", THIS_METHOD); BOOL done = NO; do { LogVerbose(@"%@ - estimatedBytesAvailable = %lu", THIS_METHOD, (unsigned long)estimatedBytesAvailable); // Make sure there's enough room in the prebuffer [preBuffer ensureCapacityForWrite:estimatedBytesAvailable]; // Read data into prebuffer uint8_t *buffer = [preBuffer writeBuffer]; size_t bytesRead = 0; OSStatus result = SSLRead(sslContext, buffer, (size_t)estimatedBytesAvailable, &bytesRead); LogVerbose(@"%@ - read from secure socket = %u", THIS_METHOD, (unsigned)bytesRead); if (bytesRead > 0) { [preBuffer didWrite:bytesRead]; } LogVerbose(@"%@ - prebuffer.length = %zu", THIS_METHOD, [preBuffer availableBytes]); if (result != noErr) { done = YES; } else { updateEstimatedBytesAvailable(); } } while (!done && estimatedBytesAvailable > 0); } } - (void)doReadData { LogTrace(); // This method is called on the socketQueue. // It might be called directly, or via the readSource when data is available to be read. if ((currentRead == nil) || (flags & kReadsPaused)) { LogVerbose(@"No currentRead or kReadsPaused"); // Unable to read at this time if (flags & kSocketSecure) { // Here's the situation: // // We have an established secure connection. // There may not be a currentRead, but there might be encrypted data sitting around for us. // When the user does get around to issuing a read, that encrypted data will need to be decrypted. // // So why make the user wait? // We might as well get a head start on decrypting some data now. // // The other reason we do this has to do with detecting a socket disconnection. // The SSL/TLS protocol has it's own disconnection handshake. // So when a secure socket is closed, a "goodbye" packet comes across the wire. // We want to make sure we read the "goodbye" packet so we can properly detect the TCP disconnection. [self flushSSLBuffers]; } if ([self usingCFStreamForTLS]) { // CFReadStream only fires once when there is available data. // It won't fire again until we've invoked CFReadStreamRead. } else { // If the readSource is firing, we need to pause it // or else it will continue to fire over and over again. // // If the readSource is not firing, // we want it to continue monitoring the socket. if (socketFDBytesAvailable > 0) { [self suspendReadSource]; } } return; } BOOL hasBytesAvailable = NO; unsigned long estimatedBytesAvailable = 0; if ([self usingCFStreamForTLS]) { #if TARGET_OS_IPHONE // Requested CFStream, rather than SecureTransport, for TLS (via GCDAsyncSocketUseCFStreamForTLS) estimatedBytesAvailable = 0; if ((flags & kSecureSocketHasBytesAvailable) && CFReadStreamHasBytesAvailable(readStream)) hasBytesAvailable = YES; else hasBytesAvailable = NO; #endif } else { estimatedBytesAvailable = socketFDBytesAvailable; if (flags & kSocketSecure) { // There are 2 buffers to be aware of here. // // We are using SecureTransport, a TLS/SSL security layer which sits atop TCP. // We issue a read to the SecureTranport API, which in turn issues a read to our SSLReadFunction. // Our SSLReadFunction then reads from the BSD socket and returns the encrypted data to SecureTransport. // SecureTransport then decrypts the data, and finally returns the decrypted data back to us. // // The first buffer is one we create. // SecureTransport often requests small amounts of data. // This has to do with the encypted packets that are coming across the TCP stream. // But it's non-optimal to do a bunch of small reads from the BSD socket. // So our SSLReadFunction reads all available data from the socket (optimizing the sys call) // and may store excess in the sslPreBuffer. estimatedBytesAvailable += [sslPreBuffer availableBytes]; // The second buffer is within SecureTransport. // As mentioned earlier, there are encrypted packets coming across the TCP stream. // SecureTransport needs the entire packet to decrypt it. // But if the entire packet produces X bytes of decrypted data, // and we only asked SecureTransport for X/2 bytes of data, // it must store the extra X/2 bytes of decrypted data for the next read. // // The SSLGetBufferedReadSize function will tell us the size of this internal buffer. // From the documentation: // // "This function does not block or cause any low-level read operations to occur." size_t sslInternalBufSize = 0; SSLGetBufferedReadSize(sslContext, &sslInternalBufSize); estimatedBytesAvailable += sslInternalBufSize; } hasBytesAvailable = (estimatedBytesAvailable > 0); } if ((hasBytesAvailable == NO) && ([preBuffer availableBytes] == 0)) { LogVerbose(@"No data available to read..."); // No data available to read. if (![self usingCFStreamForTLS]) { // Need to wait for readSource to fire and notify us of // available data in the socket's internal read buffer. [self resumeReadSource]; } return; } if (flags & kStartingReadTLS) { LogVerbose(@"Waiting for SSL/TLS handshake to complete"); // The readQueue is waiting for SSL/TLS handshake to complete. if (flags & kStartingWriteTLS) { if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) { // We are in the process of a SSL Handshake. // We were waiting for incoming data which has just arrived. [self ssl_continueSSLHandshake]; } } else { // We are still waiting for the writeQueue to drain and start the SSL/TLS process. // We now know data is available to read. if (![self usingCFStreamForTLS]) { // Suspend the read source or else it will continue to fire nonstop. [self suspendReadSource]; } } return; } BOOL done = NO; // Completed read operation NSError *error = nil; // Error occurred NSUInteger totalBytesReadForCurrentRead = 0; // // STEP 1 - READ FROM PREBUFFER // if ([preBuffer availableBytes] > 0) { // There are 3 types of read packets: // // 1) Read all available data. // 2) Read a specific length of data. // 3) Read up to a particular terminator. NSUInteger bytesToCopy; if (currentRead->term != nil) { // Read type #3 - read up to a terminator bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; } else { // Read type #1 or #2 bytesToCopy = [currentRead readLengthForNonTermWithHint:[preBuffer availableBytes]]; } // Make sure we have enough room in the buffer for our read. [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; // Copy bytes from prebuffer into packet buffer uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; memcpy(buffer, [preBuffer readBuffer], bytesToCopy); // Remove the copied bytes from the preBuffer [preBuffer didRead:bytesToCopy]; LogVerbose(@"copied(%lu) preBufferLength(%zu)", (unsigned long)bytesToCopy, [preBuffer availableBytes]); // Update totals currentRead->bytesDone += bytesToCopy; totalBytesReadForCurrentRead += bytesToCopy; // Check to see if the read operation is done if (currentRead->readLength > 0) { // Read type #2 - read a specific length of data done = (currentRead->bytesDone == currentRead->readLength); } else if (currentRead->term != nil) { // Read type #3 - read up to a terminator // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method if (!done && currentRead->maxLength > 0) { // We're not done and there's a set maxLength. // Have we reached that maxLength yet? if (currentRead->bytesDone >= currentRead->maxLength) { error = [self readMaxedOutError]; } } } else { // Read type #1 - read all available data // // We're done as soon as // - we've read all available data (in prebuffer and socket) // - we've read the maxLength of read packet. done = ((currentRead->maxLength > 0) && (currentRead->bytesDone == currentRead->maxLength)); } } // // STEP 2 - READ FROM SOCKET // BOOL socketEOF = (flags & kSocketHasReadEOF) ? YES : NO; // Nothing more to read via socket (end of file) BOOL waiting = !done && !error && !socketEOF && !hasBytesAvailable; // Ran out of data, waiting for more if (!done && !error && !socketEOF && hasBytesAvailable) { NSAssert(([preBuffer availableBytes] == 0), @"Invalid logic"); BOOL readIntoPreBuffer = NO; uint8_t *buffer = NULL; size_t bytesRead = 0; if (flags & kSocketSecure) { if ([self usingCFStreamForTLS]) { #if TARGET_OS_IPHONE // Using CFStream, rather than SecureTransport, for TLS NSUInteger defaultReadLength = (1024 * 32); NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength shouldPreBuffer:&readIntoPreBuffer]; // Make sure we have enough room in the buffer for our read. // // We are either reading directly into the currentRead->buffer, // or we're reading into the temporary preBuffer. if (readIntoPreBuffer) { [preBuffer ensureCapacityForWrite:bytesToRead]; buffer = [preBuffer writeBuffer]; } else { [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } // Read data into buffer CFIndex result = CFReadStreamRead(readStream, buffer, (CFIndex)bytesToRead); LogVerbose(@"CFReadStreamRead(): result = %i", (int)result); if (result < 0) { error = (__bridge_transfer NSError *)CFReadStreamCopyError(readStream); } else if (result == 0) { socketEOF = YES; } else { waiting = YES; bytesRead = (size_t)result; } // We only know how many decrypted bytes were read. // The actual number of bytes read was likely more due to the overhead of the encryption. // So we reset our flag, and rely on the next callback to alert us of more data. flags &= ~kSecureSocketHasBytesAvailable; #endif } else { // Using SecureTransport for TLS // // We know: // - how many bytes are available on the socket // - how many encrypted bytes are sitting in the sslPreBuffer // - how many decypted bytes are sitting in the sslContext // // But we do NOT know: // - how many encypted bytes are sitting in the sslContext // // So we play the regular game of using an upper bound instead. NSUInteger defaultReadLength = (1024 * 32); if (defaultReadLength < estimatedBytesAvailable) { defaultReadLength = estimatedBytesAvailable + (1024 * 16); } NSUInteger bytesToRead = [currentRead optimalReadLengthWithDefault:defaultReadLength shouldPreBuffer:&readIntoPreBuffer]; if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t bytesToRead = SIZE_MAX; } // Make sure we have enough room in the buffer for our read. // // We are either reading directly into the currentRead->buffer, // or we're reading into the temporary preBuffer. if (readIntoPreBuffer) { [preBuffer ensureCapacityForWrite:bytesToRead]; buffer = [preBuffer writeBuffer]; } else { [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } // The documentation from Apple states: // // "a read operation might return errSSLWouldBlock, // indicating that less data than requested was actually transferred" // // However, starting around 10.7, the function will sometimes return noErr, // even if it didn't read as much data as requested. So we need to watch out for that. OSStatus result; do { void *loop_buffer = buffer + bytesRead; size_t loop_bytesToRead = (size_t)bytesToRead - bytesRead; size_t loop_bytesRead = 0; result = SSLRead(sslContext, loop_buffer, loop_bytesToRead, &loop_bytesRead); LogVerbose(@"read from secure socket = %u", (unsigned)loop_bytesRead); bytesRead += loop_bytesRead; } while ((result == noErr) && (bytesRead < bytesToRead)); if (result != noErr) { if (result == errSSLWouldBlock) waiting = YES; else { if (result == errSSLClosedGraceful || result == errSSLClosedAbort) { // We've reached the end of the stream. // Handle this the same way we would an EOF from the socket. socketEOF = YES; sslErrCode = result; } else { error = [self sslError:result]; } } // It's possible that bytesRead > 0, even if the result was errSSLWouldBlock. // This happens when the SSLRead function is able to read some data, // but not the entire amount we requested. if (bytesRead <= 0) { bytesRead = 0; } } // Do not modify socketFDBytesAvailable. // It will be updated via the SSLReadFunction(). } } else { // Normal socket operation NSUInteger bytesToRead; // There are 3 types of read packets: // // 1) Read all available data. // 2) Read a specific length of data. // 3) Read up to a particular terminator. if (currentRead->term != nil) { // Read type #3 - read up to a terminator bytesToRead = [currentRead readLengthForTermWithHint:estimatedBytesAvailable shouldPreBuffer:&readIntoPreBuffer]; } else { // Read type #1 or #2 bytesToRead = [currentRead readLengthForNonTermWithHint:estimatedBytesAvailable]; } if (bytesToRead > SIZE_MAX) { // NSUInteger may be bigger than size_t (read param 3) bytesToRead = SIZE_MAX; } // Make sure we have enough room in the buffer for our read. // // We are either reading directly into the currentRead->buffer, // or we're reading into the temporary preBuffer. if (readIntoPreBuffer) { [preBuffer ensureCapacityForWrite:bytesToRead]; buffer = [preBuffer writeBuffer]; } else { [currentRead ensureCapacityForAdditionalDataOfLength:bytesToRead]; buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; } // Read data into buffer int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; ssize_t result = read(socketFD, buffer, (size_t)bytesToRead); LogVerbose(@"read from socket = %i", (int)result); if (result < 0) { if (errno == EWOULDBLOCK) waiting = YES; else error = [self errorWithErrno:errno reason:@"Error in read() function"]; socketFDBytesAvailable = 0; } else if (result == 0) { socketEOF = YES; socketFDBytesAvailable = 0; } else { bytesRead = result; if (bytesRead < bytesToRead) { // The read returned less data than requested. // This means socketFDBytesAvailable was a bit off due to timing, // because we read from the socket right when the readSource event was firing. socketFDBytesAvailable = 0; } else { if (socketFDBytesAvailable <= bytesRead) socketFDBytesAvailable = 0; else socketFDBytesAvailable -= bytesRead; } if (socketFDBytesAvailable == 0) { waiting = YES; } } } if (bytesRead > 0) { // Check to see if the read operation is done if (currentRead->readLength > 0) { // Read type #2 - read a specific length of data // // Note: We should never be using a prebuffer when we're reading a specific length of data. NSAssert(readIntoPreBuffer == NO, @"Invalid logic"); currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; done = (currentRead->bytesDone == currentRead->readLength); } else if (currentRead->term != nil) { // Read type #3 - read up to a terminator if (readIntoPreBuffer) { // We just read a big chunk of data into the preBuffer [preBuffer didWrite:bytesRead]; LogVerbose(@"read data into preBuffer - preBuffer.length = %zu", [preBuffer availableBytes]); // Search for the terminating sequence NSUInteger bytesToCopy = [currentRead readLengthForTermWithPreBuffer:preBuffer found:&done]; LogVerbose(@"copying %lu bytes from preBuffer", (unsigned long)bytesToCopy); // Ensure there's room on the read packet's buffer [currentRead ensureCapacityForAdditionalDataOfLength:bytesToCopy]; // Copy bytes from prebuffer into read buffer uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; memcpy(readBuf, [preBuffer readBuffer], bytesToCopy); // Remove the copied bytes from the prebuffer [preBuffer didRead:bytesToCopy]; LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); // Update totals currentRead->bytesDone += bytesToCopy; totalBytesReadForCurrentRead += bytesToCopy; // Our 'done' variable was updated via the readLengthForTermWithPreBuffer:found: method above } else { // We just read a big chunk of data directly into the packet's buffer. // We need to move any overflow into the prebuffer. NSInteger overflow = [currentRead searchForTermAfterPreBuffering:bytesRead]; if (overflow == 0) { // Perfect match! // Every byte we read stays in the read buffer, // and the last byte we read was the last byte of the term. currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; done = YES; } else if (overflow > 0) { // The term was found within the data that we read, // and there are extra bytes that extend past the end of the term. // We need to move these excess bytes out of the read packet and into the prebuffer. NSInteger underflow = bytesRead - overflow; // Copy excess data into preBuffer LogVerbose(@"copying %ld overflow bytes into preBuffer", (long)overflow); [preBuffer ensureCapacityForWrite:overflow]; uint8_t *overflowBuffer = buffer + underflow; memcpy([preBuffer writeBuffer], overflowBuffer, overflow); [preBuffer didWrite:overflow]; LogVerbose(@"preBuffer.length = %zu", [preBuffer availableBytes]); // Note: The completeCurrentRead method will trim the buffer for us. currentRead->bytesDone += underflow; totalBytesReadForCurrentRead += underflow; done = YES; } else { // The term was not found within the data that we read. currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; done = NO; } } if (!done && currentRead->maxLength > 0) { // We're not done and there's a set maxLength. // Have we reached that maxLength yet? if (currentRead->bytesDone >= currentRead->maxLength) { error = [self readMaxedOutError]; } } } else { // Read type #1 - read all available data if (readIntoPreBuffer) { // We just read a chunk of data into the preBuffer [preBuffer didWrite:bytesRead]; // Now copy the data into the read packet. // // Recall that we didn't read directly into the packet's buffer to avoid // over-allocating memory since we had no clue how much data was available to be read. // // Ensure there's room on the read packet's buffer [currentRead ensureCapacityForAdditionalDataOfLength:bytesRead]; // Copy bytes from prebuffer into read buffer uint8_t *readBuf = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset + currentRead->bytesDone; memcpy(readBuf, [preBuffer readBuffer], bytesRead); // Remove the copied bytes from the prebuffer [preBuffer didRead:bytesRead]; // Update totals currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; } else { currentRead->bytesDone += bytesRead; totalBytesReadForCurrentRead += bytesRead; } done = YES; } } // if (bytesRead > 0) } // if (!done && !error && !socketEOF && hasBytesAvailable) if (!done && currentRead->readLength == 0 && currentRead->term == nil) { // Read type #1 - read all available data // // We might arrive here if we read data from the prebuffer but not from the socket. done = (totalBytesReadForCurrentRead > 0); } // Check to see if we're done, or if we've made progress if (done) { [self completeCurrentRead]; if (!error && (!socketEOF || [preBuffer availableBytes] > 0)) { [self maybeDequeueRead]; } } else if (totalBytesReadForCurrentRead > 0) { // We're not done read type #2 or #3 yet, but we have read in some bytes // // We ensure that `waiting` is set in order to resume the readSource (if it is suspended). It is // possible to reach this point and `waiting` not be set, if the current read's length is // sufficiently large. In that case, we may have read to some upperbound successfully, but // that upperbound could be smaller than the desired length. waiting = YES; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadPartialDataOfLength:tag:)]) { long theReadTag = currentRead->tag; dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didReadPartialDataOfLength:totalBytesReadForCurrentRead tag:theReadTag]; }}); } } // Check for errors if (error) { [self closeWithError:error]; } else if (socketEOF) { [self doReadEOF]; } else if (waiting) { if (![self usingCFStreamForTLS]) { // Monitor the socket for readability (if we're not already doing so) [self resumeReadSource]; } } // Do not add any code here without first adding return statements in the error cases above. } - (void)doReadEOF { LogTrace(); // This method may be called more than once. // If the EOF is read while there is still data in the preBuffer, // then this method may be called continually after invocations of doReadData to see if it's time to disconnect. flags |= kSocketHasReadEOF; if (flags & kSocketSecure) { // If the SSL layer has any buffered data, flush it into the preBuffer now. [self flushSSLBuffers]; } BOOL shouldDisconnect = NO; NSError *error = nil; if ((flags & kStartingReadTLS) || (flags & kStartingWriteTLS)) { // We received an EOF during or prior to startTLS. // The SSL/TLS handshake is now impossible, so this is an unrecoverable situation. shouldDisconnect = YES; if ([self usingSecureTransportForTLS]) { error = [self sslError:errSSLClosedAbort]; } } else if (flags & kReadStreamClosed) { // The preBuffer has already been drained. // The config allows half-duplex connections. // We've previously checked the socket, and it appeared writeable. // So we marked the read stream as closed and notified the delegate. // // As per the half-duplex contract, the socket will be closed when a write fails, // or when the socket is manually closed. shouldDisconnect = NO; } else if ([preBuffer availableBytes] > 0) { LogVerbose(@"Socket reached EOF, but there is still data available in prebuffer"); // Although we won't be able to read any more data from the socket, // there is existing data that has been prebuffered that we can read. shouldDisconnect = NO; } else if (config & kAllowHalfDuplexConnection) { // We just received an EOF (end of file) from the socket's read stream. // This means the remote end of the socket (the peer we're connected to) // has explicitly stated that it will not be sending us any more data. // // Query the socket to see if it is still writeable. (Perhaps the peer will continue reading data from us) int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; struct pollfd pfd[1]; pfd[0].fd = socketFD; pfd[0].events = POLLOUT; pfd[0].revents = 0; poll(pfd, 1, 0); if (pfd[0].revents & POLLOUT) { // Socket appears to still be writeable shouldDisconnect = NO; flags |= kReadStreamClosed; // Notify the delegate that we're going half-duplex __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidCloseReadStream:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socketDidCloseReadStream:self]; }}); } } else { shouldDisconnect = YES; } } else { shouldDisconnect = YES; } if (shouldDisconnect) { if (error == nil) { if ([self usingSecureTransportForTLS]) { if (sslErrCode != noErr && sslErrCode != errSSLClosedGraceful) { error = [self sslError:sslErrCode]; } else { error = [self connectionClosedError]; } } else { error = [self connectionClosedError]; } } [self closeWithError:error]; } else { if (![self usingCFStreamForTLS]) { // Suspend the read source (if needed) [self suspendReadSource]; } } } - (void)completeCurrentRead { LogTrace(); NSAssert(currentRead, @"Trying to complete current read when there is no current read."); NSData *result = nil; if (currentRead->bufferOwner) { // We created the buffer on behalf of the user. // Trim our buffer to be the proper size. [currentRead->buffer setLength:currentRead->bytesDone]; result = currentRead->buffer; } else { // We did NOT create the buffer. // The buffer is owned by the caller. // Only trim the buffer if we had to increase its size. if ([currentRead->buffer length] > currentRead->originalBufferLength) { NSUInteger readSize = currentRead->startOffset + currentRead->bytesDone; NSUInteger origSize = currentRead->originalBufferLength; NSUInteger buffSize = MAX(readSize, origSize); [currentRead->buffer setLength:buffSize]; } uint8_t *buffer = (uint8_t *)[currentRead->buffer mutableBytes] + currentRead->startOffset; result = [NSData dataWithBytesNoCopy:buffer length:currentRead->bytesDone freeWhenDone:NO]; } __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReadData:withTag:)]) { GCDAsyncReadPacket *theRead = currentRead; // Ensure currentRead retained since result may not own buffer dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didReadData:result withTag:theRead->tag]; }}); } [self endCurrentRead]; } - (void)endCurrentRead { if (readTimer) { dispatch_source_cancel(readTimer); readTimer = NULL; } currentRead = nil; } - (void)setupReadTimerWithTimeout:(NSTimeInterval)timeout { if (timeout >= 0.0) { readTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(readTimer, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; [strongSelf doReadTimeout]; #pragma clang diagnostic pop }}); #if !OS_OBJECT_USE_OBJC dispatch_source_t theReadTimer = readTimer; dispatch_source_set_cancel_handler(readTimer, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"dispatch_release(readTimer)"); dispatch_release(theReadTimer); #pragma clang diagnostic pop }); #endif dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(readTimer); } } - (void)doReadTimeout { // This is a little bit tricky. // Ideally we'd like to synchronously query the delegate about a timeout extension. // But if we do so synchronously we risk a possible deadlock. // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. flags |= kReadsPaused; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutReadWithTag:elapsed:bytesDone:)]) { GCDAsyncReadPacket *theRead = currentRead; dispatch_async(delegateQueue, ^{ @autoreleasepool { NSTimeInterval timeoutExtension = 0.0; timeoutExtension = [theDelegate socket:self shouldTimeoutReadWithTag:theRead->tag elapsed:theRead->timeout bytesDone:theRead->bytesDone]; dispatch_async(self->socketQueue, ^{ @autoreleasepool { [self doReadTimeoutWithExtension:timeoutExtension]; }}); }}); } else { [self doReadTimeoutWithExtension:0.0]; } } - (void)doReadTimeoutWithExtension:(NSTimeInterval)timeoutExtension { if (currentRead) { if (timeoutExtension > 0.0) { currentRead->timeout += timeoutExtension; // Reschedule the timer dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); dispatch_source_set_timer(readTimer, tt, DISPATCH_TIME_FOREVER, 0); // Unpause reads, and continue flags &= ~kReadsPaused; [self doReadData]; } else { LogVerbose(@"ReadTimeout"); [self closeWithError:[self readTimeoutError]]; } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Writing //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)writeData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { if ([data length] == 0) return; GCDAsyncWritePacket *packet = [[GCDAsyncWritePacket alloc] initWithData:data timeout:timeout tag:tag]; dispatch_async(socketQueue, ^{ @autoreleasepool { LogTrace(); if ((self->flags & kSocketStarted) && !(self->flags & kForbidReadsWrites)) { [self->writeQueue addObject:packet]; [self maybeDequeueWrite]; } }}); // Do not rely on the block being run in order to release the packet, // as the queue might get released without the block completing. } - (float)progressOfWriteReturningTag:(long *)tagPtr bytesDone:(NSUInteger *)donePtr total:(NSUInteger *)totalPtr { __block float result = 0.0F; dispatch_block_t block = ^{ if (!self->currentWrite || ![self->currentWrite isKindOfClass:[GCDAsyncWritePacket class]]) { // We're not writing anything right now. if (tagPtr != NULL) *tagPtr = 0; if (donePtr != NULL) *donePtr = 0; if (totalPtr != NULL) *totalPtr = 0; result = NAN; } else { NSUInteger done = self->currentWrite->bytesDone; NSUInteger total = [self->currentWrite->buffer length]; if (tagPtr != NULL) *tagPtr = self->currentWrite->tag; if (donePtr != NULL) *donePtr = done; if (totalPtr != NULL) *totalPtr = total; result = (float)done / (float)total; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } /** * Conditionally starts a new write. * * It is called when: * - a user requests a write * - after a write request has finished (to handle the next request) * - immediately after the socket opens to handle any pending requests * * This method also handles auto-disconnect post read/write completion. **/ - (void)maybeDequeueWrite { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); // If we're not currently processing a write AND we have an available write stream if ((currentWrite == nil) && (flags & kConnected)) { if ([writeQueue count] > 0) { // Dequeue the next object in the write queue currentWrite = [writeQueue objectAtIndex:0]; [writeQueue removeObjectAtIndex:0]; if ([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]]) { LogVerbose(@"Dequeued GCDAsyncSpecialPacket"); // Attempt to start TLS flags |= kStartingWriteTLS; // This method won't do anything unless both kStartingReadTLS and kStartingWriteTLS are set [self maybeStartTLS]; } else { LogVerbose(@"Dequeued GCDAsyncWritePacket"); // Setup write timer (if needed) [self setupWriteTimerWithTimeout:currentWrite->timeout]; // Immediately write, if possible [self doWriteData]; } } else if (flags & kDisconnectAfterWrites) { if (flags & kDisconnectAfterReads) { if (([readQueue count] == 0) && (currentRead == nil)) { [self closeWithError:nil]; } } else { [self closeWithError:nil]; } } } } - (void)doWriteData { LogTrace(); // This method is called by the writeSource via the socketQueue if ((currentWrite == nil) || (flags & kWritesPaused)) { LogVerbose(@"No currentWrite or kWritesPaused"); // Unable to write at this time if ([self usingCFStreamForTLS]) { // CFWriteStream only fires once when there is available data. // It won't fire again until we've invoked CFWriteStreamWrite. } else { // If the writeSource is firing, we need to pause it // or else it will continue to fire over and over again. if (flags & kSocketCanAcceptBytes) { [self suspendWriteSource]; } } return; } if (!(flags & kSocketCanAcceptBytes)) { LogVerbose(@"No space available to write..."); // No space available to write. if (![self usingCFStreamForTLS]) { // Need to wait for writeSource to fire and notify us of // available space in the socket's internal write buffer. [self resumeWriteSource]; } return; } if (flags & kStartingWriteTLS) { LogVerbose(@"Waiting for SSL/TLS handshake to complete"); // The writeQueue is waiting for SSL/TLS handshake to complete. if (flags & kStartingReadTLS) { if ([self usingSecureTransportForTLS] && lastSSLHandshakeError == errSSLWouldBlock) { // We are in the process of a SSL Handshake. // We were waiting for available space in the socket's internal OS buffer to continue writing. [self ssl_continueSSLHandshake]; } } else { // We are still waiting for the readQueue to drain and start the SSL/TLS process. // We now know we can write to the socket. if (![self usingCFStreamForTLS]) { // Suspend the write source or else it will continue to fire nonstop. [self suspendWriteSource]; } } return; } // Note: This method is not called if currentWrite is a GCDAsyncSpecialPacket (startTLS packet) BOOL waiting = NO; NSError *error = nil; size_t bytesWritten = 0; if (flags & kSocketSecure) { if ([self usingCFStreamForTLS]) { #if TARGET_OS_IPHONE // // Writing data using CFStream (over internal TLS) // const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) { bytesToWrite = SIZE_MAX; } CFIndex result = CFWriteStreamWrite(writeStream, buffer, (CFIndex)bytesToWrite); LogVerbose(@"CFWriteStreamWrite(%lu) = %li", (unsigned long)bytesToWrite, result); if (result < 0) { error = (__bridge_transfer NSError *)CFWriteStreamCopyError(writeStream); } else { bytesWritten = (size_t)result; // We always set waiting to true in this scenario. // CFStream may have altered our underlying socket to non-blocking. // Thus if we attempt to write without a callback, we may end up blocking our queue. waiting = YES; } #endif } else { // We're going to use the SSLWrite function. // // OSStatus SSLWrite(SSLContextRef context, const void *data, size_t dataLength, size_t *processed) // // Parameters: // context - An SSL session context reference. // data - A pointer to the buffer of data to write. // dataLength - The amount, in bytes, of data to write. // processed - On return, the length, in bytes, of the data actually written. // // It sounds pretty straight-forward, // but there are a few caveats you should be aware of. // // The SSLWrite method operates in a non-obvious (and rather annoying) manner. // According to the documentation: // // Because you may configure the underlying connection to operate in a non-blocking manner, // a write operation might return errSSLWouldBlock, indicating that less data than requested // was actually transferred. In this case, you should repeat the call to SSLWrite until some // other result is returned. // // This sounds perfect, but when our SSLWriteFunction returns errSSLWouldBlock, // then the SSLWrite method returns (with the proper errSSLWouldBlock return value), // but it sets processed to dataLength !! // // In other words, if the SSLWrite function doesn't completely write all the data we tell it to, // then it doesn't tell us how many bytes were actually written. So, for example, if we tell it to // write 256 bytes then it might actually write 128 bytes, but then report 0 bytes written. // // You might be wondering: // If the SSLWrite function doesn't tell us how many bytes were written, // then how in the world are we supposed to update our parameters (buffer & bytesToWrite) // for the next time we invoke SSLWrite? // // The answer is that SSLWrite cached all the data we told it to write, // and it will push out that data next time we call SSLWrite. // If we call SSLWrite with new data, it will push out the cached data first, and then the new data. // If we call SSLWrite with empty data, then it will simply push out the cached data. // // For this purpose we're going to break large writes into a series of smaller writes. // This allows us to report progress back to the delegate. OSStatus result; BOOL hasCachedDataToWrite = (sslWriteCachedLength > 0); BOOL hasNewDataToWrite = YES; if (hasCachedDataToWrite) { size_t processed = 0; result = SSLWrite(sslContext, NULL, 0, &processed); if (result == noErr) { bytesWritten = sslWriteCachedLength; sslWriteCachedLength = 0; if ([currentWrite->buffer length] == (currentWrite->bytesDone + bytesWritten)) { // We've written all data for the current write. hasNewDataToWrite = NO; } } else { if (result == errSSLWouldBlock) { waiting = YES; } else { error = [self sslError:result]; } // Can't write any new data since we were unable to write the cached data. hasNewDataToWrite = NO; } } if (hasNewDataToWrite) { const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone + bytesWritten; NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone - bytesWritten; if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) { bytesToWrite = SIZE_MAX; } size_t bytesRemaining = bytesToWrite; BOOL keepLooping = YES; while (keepLooping) { const size_t sslMaxBytesToWrite = 32768; size_t sslBytesToWrite = MIN(bytesRemaining, sslMaxBytesToWrite); size_t sslBytesWritten = 0; result = SSLWrite(sslContext, buffer, sslBytesToWrite, &sslBytesWritten); if (result == noErr) { buffer += sslBytesWritten; bytesWritten += sslBytesWritten; bytesRemaining -= sslBytesWritten; keepLooping = (bytesRemaining > 0); } else { if (result == errSSLWouldBlock) { waiting = YES; sslWriteCachedLength = sslBytesToWrite; } else { error = [self sslError:result]; } keepLooping = NO; } } // while (keepLooping) } // if (hasNewDataToWrite) } } else { // // Writing data directly over raw socket // int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; const uint8_t *buffer = (const uint8_t *)[currentWrite->buffer bytes] + currentWrite->bytesDone; NSUInteger bytesToWrite = [currentWrite->buffer length] - currentWrite->bytesDone; if (bytesToWrite > SIZE_MAX) // NSUInteger may be bigger than size_t (write param 3) { bytesToWrite = SIZE_MAX; } ssize_t result = write(socketFD, buffer, (size_t)bytesToWrite); LogVerbose(@"wrote to socket = %zd", result); // Check results if (result < 0) { if (errno == EWOULDBLOCK) { waiting = YES; } else { error = [self errorWithErrno:errno reason:@"Error in write() function"]; } } else { bytesWritten = result; } } // We're done with our writing. // If we explictly ran into a situation where the socket told us there was no room in the buffer, // then we immediately resume listening for notifications. // // We must do this before we dequeue another write, // as that may in turn invoke this method again. // // Note that if CFStream is involved, it may have maliciously put our socket in blocking mode. if (waiting) { flags &= ~kSocketCanAcceptBytes; if (![self usingCFStreamForTLS]) { [self resumeWriteSource]; } } // Check our results BOOL done = NO; if (bytesWritten > 0) { // Update total amount read for the current write currentWrite->bytesDone += bytesWritten; LogVerbose(@"currentWrite->bytesDone = %lu", (unsigned long)currentWrite->bytesDone); // Is packet done? done = (currentWrite->bytesDone == [currentWrite->buffer length]); } if (done) { [self completeCurrentWrite]; if (!error) { dispatch_async(socketQueue, ^{ @autoreleasepool{ [self maybeDequeueWrite]; }}); } } else { // We were unable to finish writing the data, // so we're waiting for another callback to notify us of available space in the lower-level output buffer. if (!waiting && !error) { // This would be the case if our write was able to accept some data, but not all of it. flags &= ~kSocketCanAcceptBytes; if (![self usingCFStreamForTLS]) { [self resumeWriteSource]; } } if (bytesWritten > 0) { // We're not done with the entire write, but we have written some bytes __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWritePartialDataOfLength:tag:)]) { long theWriteTag = currentWrite->tag; dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didWritePartialDataOfLength:bytesWritten tag:theWriteTag]; }}); } } } // Check for errors if (error) { [self closeWithError:[self errorWithErrno:errno reason:@"Error in write() function"]]; } // Do not add any code here without first adding a return statement in the error case above. } - (void)completeCurrentWrite { LogTrace(); NSAssert(currentWrite, @"Trying to complete current write when there is no current write."); __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didWriteDataWithTag:)]) { long theWriteTag = currentWrite->tag; dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didWriteDataWithTag:theWriteTag]; }}); } [self endCurrentWrite]; } - (void)endCurrentWrite { if (writeTimer) { dispatch_source_cancel(writeTimer); writeTimer = NULL; } currentWrite = nil; } - (void)setupWriteTimerWithTimeout:(NSTimeInterval)timeout { if (timeout >= 0.0) { writeTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); __weak GCDAsyncSocket *weakSelf = self; dispatch_source_set_event_handler(writeTimer, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf == nil) return_from_block; [strongSelf doWriteTimeout]; #pragma clang diagnostic pop }}); #if !OS_OBJECT_USE_OBJC dispatch_source_t theWriteTimer = writeTimer; dispatch_source_set_cancel_handler(writeTimer, ^{ #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" LogVerbose(@"dispatch_release(writeTimer)"); dispatch_release(theWriteTimer); #pragma clang diagnostic pop }); #endif dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(writeTimer); } } - (void)doWriteTimeout { // This is a little bit tricky. // Ideally we'd like to synchronously query the delegate about a timeout extension. // But if we do so synchronously we risk a possible deadlock. // So instead we have to do so asynchronously, and callback to ourselves from within the delegate block. flags |= kWritesPaused; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:shouldTimeoutWriteWithTag:elapsed:bytesDone:)]) { GCDAsyncWritePacket *theWrite = currentWrite; dispatch_async(delegateQueue, ^{ @autoreleasepool { NSTimeInterval timeoutExtension = 0.0; timeoutExtension = [theDelegate socket:self shouldTimeoutWriteWithTag:theWrite->tag elapsed:theWrite->timeout bytesDone:theWrite->bytesDone]; dispatch_async(self->socketQueue, ^{ @autoreleasepool { [self doWriteTimeoutWithExtension:timeoutExtension]; }}); }}); } else { [self doWriteTimeoutWithExtension:0.0]; } } - (void)doWriteTimeoutWithExtension:(NSTimeInterval)timeoutExtension { if (currentWrite) { if (timeoutExtension > 0.0) { currentWrite->timeout += timeoutExtension; // Reschedule the timer dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeoutExtension * NSEC_PER_SEC)); dispatch_source_set_timer(writeTimer, tt, DISPATCH_TIME_FOREVER, 0); // Unpause writes, and continue flags &= ~kWritesPaused; [self doWriteData]; } else { LogVerbose(@"WriteTimeout"); [self closeWithError:[self writeTimeoutError]]; } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Security //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)startTLS:(NSDictionary *)tlsSettings { LogTrace(); if (tlsSettings == nil) { // Passing nil/NULL to CFReadStreamSetProperty will appear to work the same as passing an empty dictionary, // but causes problems if we later try to fetch the remote host's certificate. // // To be exact, it causes the following to return NULL instead of the normal result: // CFReadStreamCopyProperty(readStream, kCFStreamPropertySSLPeerCertificates) // // So we use an empty dictionary instead, which works perfectly. tlsSettings = [NSDictionary dictionary]; } GCDAsyncSpecialPacket *packet = [[GCDAsyncSpecialPacket alloc] initWithTLSSettings:tlsSettings]; dispatch_async(socketQueue, ^{ @autoreleasepool { if ((self->flags & kSocketStarted) && !(self->flags & kQueuedTLS) && !(self->flags & kForbidReadsWrites)) { [self->readQueue addObject:packet]; [self->writeQueue addObject:packet]; self->flags |= kQueuedTLS; [self maybeDequeueRead]; [self maybeDequeueWrite]; } }}); } - (void)maybeStartTLS { // We can't start TLS until: // - All queued reads prior to the user calling startTLS are complete // - All queued writes prior to the user calling startTLS are complete // // We'll know these conditions are met when both kStartingReadTLS and kStartingWriteTLS are set if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) { BOOL useSecureTransport = YES; #if TARGET_OS_IPHONE { GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; NSDictionary *tlsSettings = @{}; if (tlsPacket) { tlsSettings = tlsPacket->tlsSettings; } NSNumber *value = [tlsSettings objectForKey:GCDAsyncSocketUseCFStreamForTLS]; if (value && [value boolValue]) useSecureTransport = NO; } #endif if (useSecureTransport) { [self ssl_startTLS]; } else { #if TARGET_OS_IPHONE [self cf_startTLS]; #endif } } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Security via SecureTransport //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (OSStatus)sslReadWithBuffer:(void *)buffer length:(size_t *)bufferLength { LogVerbose(@"sslReadWithBuffer:%p length:%lu", buffer, (unsigned long)*bufferLength); if ((socketFDBytesAvailable == 0) && ([sslPreBuffer availableBytes] == 0)) { LogVerbose(@"%@ - No data available to read...", THIS_METHOD); // No data available to read. // // Need to wait for readSource to fire and notify us of // available data in the socket's internal read buffer. [self resumeReadSource]; *bufferLength = 0; return errSSLWouldBlock; } size_t totalBytesRead = 0; size_t totalBytesLeftToBeRead = *bufferLength; BOOL done = NO; BOOL socketError = NO; // // STEP 1 : READ FROM SSL PRE BUFFER // size_t sslPreBufferLength = [sslPreBuffer availableBytes]; if (sslPreBufferLength > 0) { LogVerbose(@"%@: Reading from SSL pre buffer...", THIS_METHOD); size_t bytesToCopy; if (sslPreBufferLength > totalBytesLeftToBeRead) bytesToCopy = totalBytesLeftToBeRead; else bytesToCopy = sslPreBufferLength; LogVerbose(@"%@: Copying %zu bytes from sslPreBuffer", THIS_METHOD, bytesToCopy); memcpy(buffer, [sslPreBuffer readBuffer], bytesToCopy); [sslPreBuffer didRead:bytesToCopy]; LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); totalBytesRead += bytesToCopy; totalBytesLeftToBeRead -= bytesToCopy; done = (totalBytesLeftToBeRead == 0); if (done) LogVerbose(@"%@: Complete", THIS_METHOD); } // // STEP 2 : READ FROM SOCKET // if (!done && (socketFDBytesAvailable > 0)) { LogVerbose(@"%@: Reading from socket...", THIS_METHOD); int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; BOOL readIntoPreBuffer; size_t bytesToRead; uint8_t *buf; if (socketFDBytesAvailable > totalBytesLeftToBeRead) { // Read all available data from socket into sslPreBuffer. // Then copy requested amount into dataBuffer. LogVerbose(@"%@: Reading into sslPreBuffer...", THIS_METHOD); [sslPreBuffer ensureCapacityForWrite:socketFDBytesAvailable]; readIntoPreBuffer = YES; bytesToRead = (size_t)socketFDBytesAvailable; buf = [sslPreBuffer writeBuffer]; } else { // Read available data from socket directly into dataBuffer. LogVerbose(@"%@: Reading directly into dataBuffer...", THIS_METHOD); readIntoPreBuffer = NO; bytesToRead = totalBytesLeftToBeRead; buf = (uint8_t *)buffer + totalBytesRead; } ssize_t result = read(socketFD, buf, bytesToRead); LogVerbose(@"%@: read from socket = %zd", THIS_METHOD, result); if (result < 0) { LogVerbose(@"%@: read errno = %i", THIS_METHOD, errno); if (errno != EWOULDBLOCK) { socketError = YES; } socketFDBytesAvailable = 0; } else if (result == 0) { LogVerbose(@"%@: read EOF", THIS_METHOD); socketError = YES; socketFDBytesAvailable = 0; } else { size_t bytesReadFromSocket = result; if (socketFDBytesAvailable > bytesReadFromSocket) socketFDBytesAvailable -= bytesReadFromSocket; else socketFDBytesAvailable = 0; if (readIntoPreBuffer) { [sslPreBuffer didWrite:bytesReadFromSocket]; size_t bytesToCopy = MIN(totalBytesLeftToBeRead, bytesReadFromSocket); LogVerbose(@"%@: Copying %zu bytes out of sslPreBuffer", THIS_METHOD, bytesToCopy); memcpy((uint8_t *)buffer + totalBytesRead, [sslPreBuffer readBuffer], bytesToCopy); [sslPreBuffer didRead:bytesToCopy]; totalBytesRead += bytesToCopy; totalBytesLeftToBeRead -= bytesToCopy; LogVerbose(@"%@: sslPreBuffer.length = %zu", THIS_METHOD, [sslPreBuffer availableBytes]); } else { totalBytesRead += bytesReadFromSocket; totalBytesLeftToBeRead -= bytesReadFromSocket; } done = (totalBytesLeftToBeRead == 0); if (done) LogVerbose(@"%@: Complete", THIS_METHOD); } } *bufferLength = totalBytesRead; if (done) return noErr; if (socketError) return errSSLClosedAbort; return errSSLWouldBlock; } - (OSStatus)sslWriteWithBuffer:(const void *)buffer length:(size_t *)bufferLength { if (!(flags & kSocketCanAcceptBytes)) { // Unable to write. // // Need to wait for writeSource to fire and notify us of // available space in the socket's internal write buffer. [self resumeWriteSource]; *bufferLength = 0; return errSSLWouldBlock; } size_t bytesToWrite = *bufferLength; size_t bytesWritten = 0; BOOL done = NO; BOOL socketError = NO; int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; ssize_t result = write(socketFD, buffer, bytesToWrite); if (result < 0) { if (errno != EWOULDBLOCK) { socketError = YES; } flags &= ~kSocketCanAcceptBytes; } else if (result == 0) { flags &= ~kSocketCanAcceptBytes; } else { bytesWritten = result; done = (bytesWritten == bytesToWrite); } *bufferLength = bytesWritten; if (done) return noErr; if (socketError) return errSSLClosedAbort; return errSSLWouldBlock; } static OSStatus SSLReadFunction(SSLConnectionRef connection, void *data, size_t *dataLength) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); return [asyncSocket sslReadWithBuffer:data length:dataLength]; } static OSStatus SSLWriteFunction(SSLConnectionRef connection, const void *data, size_t *dataLength) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)connection; NSCAssert(dispatch_get_specific(asyncSocket->IsOnSocketQueueOrTargetQueueKey), @"What the deuce?"); return [asyncSocket sslWriteWithBuffer:data length:dataLength]; } - (void)ssl_startTLS { LogTrace(); LogVerbose(@"Starting TLS (via SecureTransport)..."); OSStatus status; GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; if (tlsPacket == nil) // Code to quiet the analyzer { NSAssert(NO, @"Logic error"); [self closeWithError:[self otherError:@"Logic error"]]; return; } NSDictionary *tlsSettings = tlsPacket->tlsSettings; // Create SSLContext, and setup IO callbacks and connection ref NSNumber *isServerNumber = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLIsServer]; BOOL isServer = [isServerNumber boolValue]; #if TARGET_OS_IPHONE || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 1080) { if (isServer) sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLServerSide, kSSLStreamType); else sslContext = SSLCreateContext(kCFAllocatorDefault, kSSLClientSide, kSSLStreamType); if (sslContext == NULL) { [self closeWithError:[self otherError:@"Error in SSLCreateContext"]]; return; } } #else // (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) { status = SSLNewContext(isServer, &sslContext); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLNewContext"]]; return; } } #endif status = SSLSetIOFuncs(sslContext, &SSLReadFunction, &SSLWriteFunction); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetIOFuncs"]]; return; } status = SSLSetConnection(sslContext, (__bridge SSLConnectionRef)self); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetConnection"]]; return; } NSNumber *shouldManuallyEvaluateTrust = [tlsSettings objectForKey:GCDAsyncSocketManuallyEvaluateTrust]; if ([shouldManuallyEvaluateTrust boolValue]) { if (isServer) { [self closeWithError:[self otherError:@"Manual trust validation is not supported for server sockets"]]; return; } status = SSLSetSessionOption(sslContext, kSSLSessionOptionBreakOnServerAuth, true); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetSessionOption"]]; return; } #if !TARGET_OS_IPHONE && (__MAC_OS_X_VERSION_MIN_REQUIRED < 1080) // Note from Apple's documentation: // // It is only necessary to call SSLSetEnableCertVerify on the Mac prior to OS X 10.8. // On OS X 10.8 and later setting kSSLSessionOptionBreakOnServerAuth always disables the // built-in trust evaluation. All versions of iOS behave like OS X 10.8 and thus // SSLSetEnableCertVerify is not available on that platform at all. status = SSLSetEnableCertVerify(sslContext, NO); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetEnableCertVerify"]]; return; } #endif } // Configure SSLContext from given settings // // Checklist: // 1. kCFStreamSSLPeerName // 2. kCFStreamSSLCertificates // 3. GCDAsyncSocketSSLPeerID // 4. GCDAsyncSocketSSLProtocolVersionMin // 5. GCDAsyncSocketSSLProtocolVersionMax // 6. GCDAsyncSocketSSLSessionOptionFalseStart // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord // 8. GCDAsyncSocketSSLCipherSuites // 9. GCDAsyncSocketSSLDiffieHellmanParameters (Mac) // 10. GCDAsyncSocketSSLALPN // // Deprecated (throw error): // 10. kCFStreamSSLAllowsAnyRoot // 11. kCFStreamSSLAllowsExpiredRoots // 12. kCFStreamSSLAllowsExpiredCertificates // 13. kCFStreamSSLValidatesCertificateChain // 14. kCFStreamSSLLevel NSObject *value; // 1. kCFStreamSSLPeerName value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLPeerName]; if ([value isKindOfClass:[NSString class]]) { NSString *peerName = (NSString *)value; const char *peer = [peerName UTF8String]; size_t peerLen = strlen(peer); status = SSLSetPeerDomainName(sslContext, peer, peerLen); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetPeerDomainName"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for kCFStreamSSLPeerName. Value must be of type NSString."); [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLPeerName."]]; return; } // 2. kCFStreamSSLCertificates value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLCertificates]; if ([value isKindOfClass:[NSArray class]]) { NSArray *certs = (NSArray *)value; status = SSLSetCertificate(sslContext, (__bridge CFArrayRef)certs); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetCertificate"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for kCFStreamSSLCertificates. Value must be of type NSArray."); [self closeWithError:[self otherError:@"Invalid value for kCFStreamSSLCertificates."]]; return; } // 3. GCDAsyncSocketSSLPeerID value = [tlsSettings objectForKey:GCDAsyncSocketSSLPeerID]; if ([value isKindOfClass:[NSData class]]) { NSData *peerIdData = (NSData *)value; status = SSLSetPeerID(sslContext, [peerIdData bytes], [peerIdData length]); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetPeerID"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLPeerID. Value must be of type NSData." @" (You can convert strings to data using a method like" @" [string dataUsingEncoding:NSUTF8StringEncoding])"); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLPeerID."]]; return; } // 4. GCDAsyncSocketSSLProtocolVersionMin value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMin]; if ([value isKindOfClass:[NSNumber class]]) { SSLProtocol minProtocol = (SSLProtocol)[(NSNumber *)value intValue]; if (minProtocol != kSSLProtocolUnknown) { status = SSLSetProtocolVersionMin(sslContext, minProtocol); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMin"]]; return; } } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMin. Value must be of type NSNumber."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMin."]]; return; } // 5. GCDAsyncSocketSSLProtocolVersionMax value = [tlsSettings objectForKey:GCDAsyncSocketSSLProtocolVersionMax]; if ([value isKindOfClass:[NSNumber class]]) { SSLProtocol maxProtocol = (SSLProtocol)[(NSNumber *)value intValue]; if (maxProtocol != kSSLProtocolUnknown) { status = SSLSetProtocolVersionMax(sslContext, maxProtocol); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetProtocolVersionMax"]]; return; } } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLProtocolVersionMax. Value must be of type NSNumber."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLProtocolVersionMax."]]; return; } // 6. GCDAsyncSocketSSLSessionOptionFalseStart value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionFalseStart]; if ([value isKindOfClass:[NSNumber class]]) { NSNumber *falseStart = (NSNumber *)value; status = SSLSetSessionOption(sslContext, kSSLSessionOptionFalseStart, [falseStart boolValue]); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionFalseStart)"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart. Value must be of type NSNumber."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionFalseStart."]]; return; } // 7. GCDAsyncSocketSSLSessionOptionSendOneByteRecord value = [tlsSettings objectForKey:GCDAsyncSocketSSLSessionOptionSendOneByteRecord]; if ([value isKindOfClass:[NSNumber class]]) { NSNumber *oneByteRecord = (NSNumber *)value; status = SSLSetSessionOption(sslContext, kSSLSessionOptionSendOneByteRecord, [oneByteRecord boolValue]); if (status != noErr) { [self closeWithError: [self otherError:@"Error in SSLSetSessionOption (kSSLSessionOptionSendOneByteRecord)"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord." @" Value must be of type NSNumber."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLSessionOptionSendOneByteRecord."]]; return; } // 8. GCDAsyncSocketSSLCipherSuites value = [tlsSettings objectForKey:GCDAsyncSocketSSLCipherSuites]; if ([value isKindOfClass:[NSArray class]]) { NSArray *cipherSuites = (NSArray *)value; NSUInteger numberCiphers = [cipherSuites count]; SSLCipherSuite ciphers[numberCiphers]; NSUInteger cipherIndex; for (cipherIndex = 0; cipherIndex < numberCiphers; cipherIndex++) { NSNumber *cipherObject = [cipherSuites objectAtIndex:cipherIndex]; ciphers[cipherIndex] = (SSLCipherSuite)[cipherObject unsignedIntValue]; } status = SSLSetEnabledCiphers(sslContext, ciphers, numberCiphers); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetEnabledCiphers"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLCipherSuites. Value must be of type NSArray."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLCipherSuites."]]; return; } // 9. GCDAsyncSocketSSLDiffieHellmanParameters #if !TARGET_OS_IPHONE value = [tlsSettings objectForKey:GCDAsyncSocketSSLDiffieHellmanParameters]; if ([value isKindOfClass:[NSData class]]) { NSData *diffieHellmanData = (NSData *)value; status = SSLSetDiffieHellmanParams(sslContext, [diffieHellmanData bytes], [diffieHellmanData length]); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetDiffieHellmanParams"]]; return; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters. Value must be of type NSData."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLDiffieHellmanParameters."]]; return; } #endif // 10. kCFStreamSSLCertificates value = [tlsSettings objectForKey:GCDAsyncSocketSSLALPN]; if ([value isKindOfClass:[NSArray class]]) { if (@available(iOS 11.0, macOS 10.13, tvOS 11.0, *)) { CFArrayRef protocols = (__bridge CFArrayRef)((NSArray *) value); status = SSLSetALPNProtocols(sslContext, protocols); if (status != noErr) { [self closeWithError:[self otherError:@"Error in SSLSetALPNProtocols"]]; return; } } else { NSAssert(NO, @"Security option unavailable - GCDAsyncSocketSSLALPN" @" - iOS 11.0, macOS 10.13 required"); [self closeWithError:[self otherError:@"Security option unavailable - GCDAsyncSocketSSLALPN"]]; } } else if (value) { NSAssert(NO, @"Invalid value for GCDAsyncSocketSSLALPN. Value must be of type NSArray."); [self closeWithError:[self otherError:@"Invalid value for GCDAsyncSocketSSLALPN."]]; return; } // DEPRECATED checks // 10. kCFStreamSSLAllowsAnyRoot #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsAnyRoot]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsAnyRoot" @" - You must use manual trust evaluation"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsAnyRoot"]]; return; } // 11. kCFStreamSSLAllowsExpiredRoots #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredRoots]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredRoots" @" - You must use manual trust evaluation"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredRoots"]]; return; } // 12. kCFStreamSSLValidatesCertificateChain #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLValidatesCertificateChain]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLValidatesCertificateChain" @" - You must use manual trust evaluation"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLValidatesCertificateChain"]]; return; } // 13. kCFStreamSSLAllowsExpiredCertificates #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLAllowsExpiredCertificates]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates" @" - You must use manual trust evaluation"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLAllowsExpiredCertificates"]]; return; } // 14. kCFStreamSSLLevel #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" value = [tlsSettings objectForKey:(__bridge NSString *)kCFStreamSSLLevel]; #pragma clang diagnostic pop if (value) { NSAssert(NO, @"Security option unavailable - kCFStreamSSLLevel" @" - You must use GCDAsyncSocketSSLProtocolVersionMin & GCDAsyncSocketSSLProtocolVersionMax"); [self closeWithError:[self otherError:@"Security option unavailable - kCFStreamSSLLevel"]]; return; } // Setup the sslPreBuffer // // Any data in the preBuffer needs to be moved into the sslPreBuffer, // as this data is now part of the secure read stream. sslPreBuffer = [[GCDAsyncSocketPreBuffer alloc] initWithCapacity:(1024 * 4)]; size_t preBufferLength = [preBuffer availableBytes]; if (preBufferLength > 0) { [sslPreBuffer ensureCapacityForWrite:preBufferLength]; memcpy([sslPreBuffer writeBuffer], [preBuffer readBuffer], preBufferLength); [preBuffer didRead:preBufferLength]; [sslPreBuffer didWrite:preBufferLength]; } sslErrCode = lastSSLHandshakeError = noErr; // Start the SSL Handshake process [self ssl_continueSSLHandshake]; } - (void)ssl_continueSSLHandshake { LogTrace(); // If the return value is noErr, the session is ready for normal secure communication. // If the return value is errSSLWouldBlock, the SSLHandshake function must be called again. // If the return value is errSSLServerAuthCompleted, we ask delegate if we should trust the // server and then call SSLHandshake again to resume the handshake or close the connection // errSSLPeerBadCert SSL error. // Otherwise, the return value indicates an error code. OSStatus status = SSLHandshake(sslContext); lastSSLHandshakeError = status; if (status == noErr) { LogVerbose(@"SSLHandshake complete"); flags &= ~kStartingReadTLS; flags &= ~kStartingWriteTLS; flags |= kSocketSecure; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socketDidSecure:self]; }}); } [self endCurrentRead]; [self endCurrentWrite]; [self maybeDequeueRead]; [self maybeDequeueWrite]; } else if (status == errSSLPeerAuthCompleted) { LogVerbose(@"SSLHandshake peerAuthCompleted - awaiting delegate approval"); __block SecTrustRef trust = NULL; status = SSLCopyPeerTrust(sslContext, &trust); if (status != noErr) { [self closeWithError:[self sslError:status]]; return; } int aStateIndex = stateIndex; dispatch_queue_t theSocketQueue = socketQueue; __weak GCDAsyncSocket *weakSelf = self; void (^comletionHandler)(BOOL) = ^(BOOL shouldTrust){ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" dispatch_async(theSocketQueue, ^{ @autoreleasepool { if (trust) { CFRelease(trust); trust = NULL; } __strong GCDAsyncSocket *strongSelf = weakSelf; if (strongSelf) { [strongSelf ssl_shouldTrustPeer:shouldTrust stateIndex:aStateIndex]; } }}); #pragma clang diagnostic pop }}; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socket:didReceiveTrust:completionHandler:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socket:self didReceiveTrust:trust completionHandler:comletionHandler]; }}); } else { if (trust) { CFRelease(trust); trust = NULL; } NSString *msg = @"GCDAsyncSocketManuallyEvaluateTrust specified in tlsSettings," @" but delegate doesn't implement socket:shouldTrustPeer:"; [self closeWithError:[self otherError:msg]]; return; } } else if (status == errSSLWouldBlock) { LogVerbose(@"SSLHandshake continues..."); // Handshake continues... // // This method will be called again from doReadData or doWriteData. } else { [self closeWithError:[self sslError:status]]; } } - (void)ssl_shouldTrustPeer:(BOOL)shouldTrust stateIndex:(int)aStateIndex { LogTrace(); if (aStateIndex != stateIndex) { LogInfo(@"Ignoring ssl_shouldTrustPeer - invalid state (maybe disconnected)"); // One of the following is true // - the socket was disconnected // - the startTLS operation timed out // - the completionHandler was already invoked once return; } // Increment stateIndex to ensure completionHandler can only be called once. stateIndex++; if (shouldTrust) { NSAssert(lastSSLHandshakeError == errSSLPeerAuthCompleted, @"ssl_shouldTrustPeer called when last error is %d and not errSSLPeerAuthCompleted", (int)lastSSLHandshakeError); [self ssl_continueSSLHandshake]; } else { [self closeWithError:[self sslError:errSSLPeerBadCert]]; } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Security via CFStream //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #if TARGET_OS_IPHONE - (void)cf_finishSSLHandshake { LogTrace(); if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) { flags &= ~kStartingReadTLS; flags &= ~kStartingWriteTLS; flags |= kSocketSecure; __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(socketDidSecure:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate socketDidSecure:self]; }}); } [self endCurrentRead]; [self endCurrentWrite]; [self maybeDequeueRead]; [self maybeDequeueWrite]; } } - (void)cf_abortSSLHandshake:(NSError *)error { LogTrace(); if ((flags & kStartingReadTLS) && (flags & kStartingWriteTLS)) { flags &= ~kStartingReadTLS; flags &= ~kStartingWriteTLS; [self closeWithError:error]; } } - (void)cf_startTLS { LogTrace(); LogVerbose(@"Starting TLS (via CFStream)..."); if ([preBuffer availableBytes] > 0) { NSString *msg = @"Invalid TLS transition. Handshake has already been read from socket."; [self closeWithError:[self otherError:msg]]; return; } [self suspendReadSource]; [self suspendWriteSource]; socketFDBytesAvailable = 0; flags &= ~kSocketCanAcceptBytes; flags &= ~kSecureSocketHasBytesAvailable; flags |= kUsingCFStreamForTLS; if (![self createReadAndWriteStream]) { [self closeWithError:[self otherError:@"Error in CFStreamCreatePairWithSocket"]]; return; } if (![self registerForStreamCallbacksIncludingReadWrite:YES]) { [self closeWithError:[self otherError:@"Error in CFStreamSetClient"]]; return; } if (![self addStreamsToRunLoop]) { [self closeWithError:[self otherError:@"Error in CFStreamScheduleWithRunLoop"]]; return; } NSAssert([currentRead isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid read packet for startTLS"); NSAssert([currentWrite isKindOfClass:[GCDAsyncSpecialPacket class]], @"Invalid write packet for startTLS"); GCDAsyncSpecialPacket *tlsPacket = (GCDAsyncSpecialPacket *)currentRead; CFDictionaryRef tlsSettings = (__bridge CFDictionaryRef)tlsPacket->tlsSettings; // Getting an error concerning kCFStreamPropertySSLSettings ? // You need to add the CFNetwork framework to your iOS application. BOOL r1 = CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, tlsSettings); BOOL r2 = CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, tlsSettings); // For some reason, starting around the time of iOS 4.3, // the first call to set the kCFStreamPropertySSLSettings will return true, // but the second will return false. // // Order doesn't seem to matter. // So you could call CFReadStreamSetProperty and then CFWriteStreamSetProperty, or you could reverse the order. // Either way, the first call will return true, and the second returns false. // // Interestingly, this doesn't seem to affect anything. // Which is not altogether unusual, as the documentation seems to suggest that (for many settings) // setting it on one side of the stream automatically sets it for the other side of the stream. // // Although there isn't anything in the documentation to suggest that the second attempt would fail. // // Furthermore, this only seems to affect streams that are negotiating a security upgrade. // In other words, the socket gets connected, there is some back-and-forth communication over the unsecure // connection, and then a startTLS is issued. // So this mostly affects newer protocols (XMPP, IMAP) as opposed to older protocols (HTTPS). if (!r1 && !r2) // Yes, the && is correct - workaround for apple bug. { [self closeWithError:[self otherError:@"Error in CFStreamSetProperty"]]; return; } if (![self openStreams]) { [self closeWithError:[self otherError:@"Error in CFStreamOpen"]]; return; } LogVerbose(@"Waiting for SSL Handshake to complete..."); } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark CFStream //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #if TARGET_OS_IPHONE + (void)ignore:(id)_ {} + (void)startCFStreamThreadIfNeeded { LogTrace(); static dispatch_once_t predicate; dispatch_once(&predicate, ^{ cfstreamThreadRetainCount = 0; cfstreamThreadSetupQueue = dispatch_queue_create("GCDAsyncSocket-CFStreamThreadSetup", DISPATCH_QUEUE_SERIAL); }); dispatch_sync(cfstreamThreadSetupQueue, ^{ @autoreleasepool { if (++cfstreamThreadRetainCount == 1) { cfstreamThread = [[NSThread alloc] initWithTarget:self selector:@selector(cfstreamThread:) object:nil]; [cfstreamThread start]; } }}); } + (void)stopCFStreamThreadIfNeeded { LogTrace(); // The creation of the cfstreamThread is relatively expensive. // So we'd like to keep it available for recycling. // However, there's a tradeoff here, because it shouldn't remain alive forever. // So what we're going to do is use a little delay before taking it down. // This way it can be reused properly in situations where multiple sockets are continually in flux. int delayInSeconds = 30; dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC)); dispatch_after(when, cfstreamThreadSetupQueue, ^{ @autoreleasepool { #pragma clang diagnostic push #pragma clang diagnostic warning "-Wimplicit-retain-self" if (cfstreamThreadRetainCount == 0) { LogWarn(@"Logic error concerning cfstreamThread start / stop"); return_from_block; } if (--cfstreamThreadRetainCount == 0) { [cfstreamThread cancel]; // set isCancelled flag // wake up the thread [[self class] performSelector:@selector(ignore:) onThread:cfstreamThread withObject:[NSNull null] waitUntilDone:NO]; cfstreamThread = nil; } #pragma clang diagnostic pop }}); } + (void)cfstreamThread:(id)unused { @autoreleasepool { [[NSThread currentThread] setName:GCDAsyncSocketThreadName]; LogInfo(@"CFStreamThread: Started"); // We can't run the run loop unless it has an associated input source or a timer. // So we'll just create a timer that will never fire - unless the server runs for decades. [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] target:self selector:@selector(ignore:) userInfo:nil repeats:YES]; NSThread *currentThread = [NSThread currentThread]; NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop]; BOOL isCancelled = [currentThread isCancelled]; while (!isCancelled && [currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]) { isCancelled = [currentThread isCancelled]; } LogInfo(@"CFStreamThread: Stopped"); }} + (void)scheduleCFStreams:(GCDAsyncSocket *)asyncSocket { LogTrace(); NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); CFRunLoopRef runLoop = CFRunLoopGetCurrent(); if (asyncSocket->readStream) CFReadStreamScheduleWithRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); if (asyncSocket->writeStream) CFWriteStreamScheduleWithRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); } + (void)unscheduleCFStreams:(GCDAsyncSocket *)asyncSocket { LogTrace(); NSAssert([NSThread currentThread] == cfstreamThread, @"Invoked on wrong thread"); CFRunLoopRef runLoop = CFRunLoopGetCurrent(); if (asyncSocket->readStream) CFReadStreamUnscheduleFromRunLoop(asyncSocket->readStream, runLoop, kCFRunLoopDefaultMode); if (asyncSocket->writeStream) CFWriteStreamUnscheduleFromRunLoop(asyncSocket->writeStream, runLoop, kCFRunLoopDefaultMode); } static void CFReadStreamCallback (CFReadStreamRef stream, CFStreamEventType type, void *pInfo) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; switch(type) { case kCFStreamEventHasBytesAvailable: { dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); if (asyncSocket->readStream != stream) return_from_block; if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. // (A callback related to the tcp stream, but not to the SSL layer). if (CFReadStreamHasBytesAvailable(asyncSocket->readStream)) { asyncSocket->flags |= kSecureSocketHasBytesAvailable; [asyncSocket cf_finishSSLHandshake]; } } else { asyncSocket->flags |= kSecureSocketHasBytesAvailable; [asyncSocket doReadData]; } }}); break; } default: { NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); if (error == nil && type == kCFStreamEventEndEncountered) { error = [asyncSocket connectionClosedError]; } dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFReadStreamCallback - Other"); if (asyncSocket->readStream != stream) return_from_block; if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { [asyncSocket cf_abortSSLHandshake:error]; } else { [asyncSocket closeWithError:error]; } }}); break; } } } static void CFWriteStreamCallback (CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) { GCDAsyncSocket *asyncSocket = (__bridge GCDAsyncSocket *)pInfo; switch(type) { case kCFStreamEventCanAcceptBytes: { dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); if (asyncSocket->writeStream != stream) return_from_block; if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { // If we set kCFStreamPropertySSLSettings before we opened the streams, this might be a lie. // (A callback related to the tcp stream, but not to the SSL layer). if (CFWriteStreamCanAcceptBytes(asyncSocket->writeStream)) { asyncSocket->flags |= kSocketCanAcceptBytes; [asyncSocket cf_finishSSLHandshake]; } } else { asyncSocket->flags |= kSocketCanAcceptBytes; [asyncSocket doWriteData]; } }}); break; } default: { NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); if (error == nil && type == kCFStreamEventEndEncountered) { error = [asyncSocket connectionClosedError]; } dispatch_async(asyncSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFWriteStreamCallback - Other"); if (asyncSocket->writeStream != stream) return_from_block; if ((asyncSocket->flags & kStartingReadTLS) && (asyncSocket->flags & kStartingWriteTLS)) { [asyncSocket cf_abortSSLHandshake:error]; } else { [asyncSocket closeWithError:error]; } }}); break; } } } - (BOOL)createReadAndWriteStream { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (readStream || writeStream) { // Streams already created return YES; } int socketFD = (socket4FD != SOCKET_NULL) ? socket4FD : (socket6FD != SOCKET_NULL) ? socket6FD : socketUN; if (socketFD == SOCKET_NULL) { // Cannot create streams without a file descriptor return NO; } if (![self isConnected]) { // Cannot create streams until file descriptor is connected return NO; } LogVerbose(@"Creating read and write stream..."); CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socketFD, &readStream, &writeStream); // The kCFStreamPropertyShouldCloseNativeSocket property should be false by default (for our case). // But let's not take any chances. if (readStream) CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); if (writeStream) CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); if ((readStream == NULL) || (writeStream == NULL)) { LogWarn(@"Unable to create read and write stream..."); if (readStream) { CFReadStreamClose(readStream); CFRelease(readStream); readStream = NULL; } if (writeStream) { CFWriteStreamClose(writeStream); CFRelease(writeStream); writeStream = NULL; } return NO; } return YES; } - (BOOL)registerForStreamCallbacksIncludingReadWrite:(BOOL)includeReadWrite { LogVerbose(@"%@ %@", THIS_METHOD, (includeReadWrite ? @"YES" : @"NO")); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); streamContext.version = 0; streamContext.info = (__bridge void *)(self); streamContext.retain = nil; streamContext.release = nil; streamContext.copyDescription = nil; CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; if (includeReadWrite) readStreamEvents |= kCFStreamEventHasBytesAvailable; if (!CFReadStreamSetClient(readStream, readStreamEvents, &CFReadStreamCallback, &streamContext)) { return NO; } CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; if (includeReadWrite) writeStreamEvents |= kCFStreamEventCanAcceptBytes; if (!CFWriteStreamSetClient(writeStream, writeStreamEvents, &CFWriteStreamCallback, &streamContext)) { return NO; } return YES; } - (BOOL)addStreamsToRunLoop { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); if (!(flags & kAddedStreamsToRunLoop)) { LogVerbose(@"Adding streams to runloop..."); [[self class] startCFStreamThreadIfNeeded]; dispatch_sync(cfstreamThreadSetupQueue, ^{ [[self class] performSelector:@selector(scheduleCFStreams:) onThread:cfstreamThread withObject:self waitUntilDone:YES]; }); flags |= kAddedStreamsToRunLoop; } return YES; } - (void)removeStreamsFromRunLoop { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); if (flags & kAddedStreamsToRunLoop) { LogVerbose(@"Removing streams from runloop..."); dispatch_sync(cfstreamThreadSetupQueue, ^{ [[self class] performSelector:@selector(unscheduleCFStreams:) onThread:cfstreamThread withObject:self waitUntilDone:YES]; }); [[self class] stopCFStreamThreadIfNeeded]; flags &= ~kAddedStreamsToRunLoop; } } - (BOOL)openStreams { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert((readStream != NULL && writeStream != NULL), @"Read/Write stream is null"); CFStreamStatus readStatus = CFReadStreamGetStatus(readStream); CFStreamStatus writeStatus = CFWriteStreamGetStatus(writeStream); if ((readStatus == kCFStreamStatusNotOpen) || (writeStatus == kCFStreamStatusNotOpen)) { LogVerbose(@"Opening read and write stream..."); BOOL r1 = CFReadStreamOpen(readStream); BOOL r2 = CFWriteStreamOpen(writeStream); if (!r1 || !r2) { LogError(@"Error in CFStreamOpen"); return NO; } } return YES; } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Advanced //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * See header file for big discussion of this method. **/ - (BOOL)autoDisconnectOnClosedReadStream { // Note: YES means kAllowHalfDuplexConnection is OFF if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return ((config & kAllowHalfDuplexConnection) == 0); } else { __block BOOL result; dispatch_sync(socketQueue, ^{ result = ((self->config & kAllowHalfDuplexConnection) == 0); }); return result; } } /** * See header file for big discussion of this method. **/ - (void)setAutoDisconnectOnClosedReadStream:(BOOL)flag { // Note: YES means kAllowHalfDuplexConnection is OFF dispatch_block_t block = ^{ if (flag) self->config &= ~kAllowHalfDuplexConnection; else self->config |= kAllowHalfDuplexConnection; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } /** * See header file for big discussion of this method. **/ - (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue { void *nonNullUnusedPointer = (__bridge void *)self; dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); } /** * See header file for big discussion of this method. **/ - (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue { dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); } /** * See header file for big discussion of this method. **/ - (void)performBlock:(dispatch_block_t)block { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); } /** * Questions? Have you read the header file? **/ - (int)socketFD { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return SOCKET_NULL; } if (socket4FD != SOCKET_NULL) return socket4FD; else return socket6FD; } /** * Questions? Have you read the header file? **/ - (int)socket4FD { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return SOCKET_NULL; } return socket4FD; } /** * Questions? Have you read the header file? **/ - (int)socket6FD { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return SOCKET_NULL; } return socket6FD; } #if TARGET_OS_IPHONE /** * Questions? Have you read the header file? **/ - (CFReadStreamRef)readStream { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NULL; } if (readStream == NULL) [self createReadAndWriteStream]; return readStream; } /** * Questions? Have you read the header file? **/ - (CFWriteStreamRef)writeStream { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NULL; } if (writeStream == NULL) [self createReadAndWriteStream]; return writeStream; } - (BOOL)enableBackgroundingOnSocketWithCaveat:(BOOL)caveat { if (![self createReadAndWriteStream]) { // Error occurred creating streams (perhaps socket isn't open) return NO; } BOOL r1, r2; LogVerbose(@"Enabling backgrouding on socket"); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" r1 = CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); r2 = CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); #pragma clang diagnostic pop if (!r1 || !r2) { return NO; } if (!caveat) { if (![self openStreams]) { return NO; } } return YES; } /** * Questions? Have you read the header file? **/ - (BOOL)enableBackgroundingOnSocket { LogTrace(); if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NO; } return [self enableBackgroundingOnSocketWithCaveat:NO]; } - (BOOL)enableBackgroundingOnSocketWithCaveat // Deprecated in iOS 4.??? { // This method was created as a workaround for a bug in iOS. // Apple has since fixed this bug. // I'm not entirely sure which version of iOS they fixed it in... LogTrace(); if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NO; } return [self enableBackgroundingOnSocketWithCaveat:YES]; } #endif - (SSLContextRef)sslContext { if (!dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@ - Method only available from within the context of a performBlock: invocation", THIS_METHOD); return NULL; } return sslContext; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Class Utilities //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + (NSMutableArray *)lookupHost:(NSString *)host port:(uint16_t)port error:(NSError **)errPtr { LogTrace(); NSMutableArray *addresses = nil; NSError *error = nil; if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) { // Use LOOPBACK address struct sockaddr_in nativeAddr4; nativeAddr4.sin_len = sizeof(struct sockaddr_in); nativeAddr4.sin_family = AF_INET; nativeAddr4.sin_port = htons(port); nativeAddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); memset(&(nativeAddr4.sin_zero), 0, sizeof(nativeAddr4.sin_zero)); struct sockaddr_in6 nativeAddr6; nativeAddr6.sin6_len = sizeof(struct sockaddr_in6); nativeAddr6.sin6_family = AF_INET6; nativeAddr6.sin6_port = htons(port); nativeAddr6.sin6_flowinfo = 0; nativeAddr6.sin6_addr = in6addr_loopback; nativeAddr6.sin6_scope_id = 0; // Wrap the native address structures NSData *address4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; NSData *address6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; addresses = [NSMutableArray arrayWithCapacity:2]; [addresses addObject:address4]; [addresses addObject:address6]; } else { NSString *portStr = [NSString stringWithFormat:@"%hu", port]; struct addrinfo hints, *res, *res0; memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_STREAM; hints.ai_protocol = IPPROTO_TCP; int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); if (gai_error) { error = [self gaiError:gai_error]; } else { NSUInteger capacity = 0; for (res = res0; res; res = res->ai_next) { if (res->ai_family == AF_INET || res->ai_family == AF_INET6) { capacity++; } } addresses = [NSMutableArray arrayWithCapacity:capacity]; for (res = res0; res; res = res->ai_next) { if (res->ai_family == AF_INET) { // Found IPv4 address. // Wrap the native address structure, and add to results. NSData *address4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; [addresses addObject:address4]; } else if (res->ai_family == AF_INET6) { // Fixes connection issues with IPv6 // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 // Found IPv6 address. // Wrap the native address structure, and add to results. struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)(void *)res->ai_addr; in_port_t *portPtr = &sockaddr->sin6_port; if ((portPtr != NULL) && (*portPtr == 0)) { *portPtr = htons(port); } NSData *address6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; [addresses addObject:address6]; } } freeaddrinfo(res0); if ([addresses count] == 0) { error = [self gaiError:EAI_FAIL]; } } } if (errPtr) *errPtr = error; return addresses; } + (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { char addrBuf[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { char addrBuf[INET6_ADDRSTRLEN]; if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { return ntohs(pSockaddr4->sin_port); } + (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { return ntohs(pSockaddr6->sin6_port); } + (NSURL *)urlFromSockaddrUN:(const struct sockaddr_un *)pSockaddr { NSString *path = [NSString stringWithUTF8String:pSockaddr->sun_path]; return [NSURL fileURLWithPath:path]; } + (NSString *)hostFromAddress:(NSData *)address { NSString *host; if ([self getHost:&host port:NULL fromAddress:address]) return host; else return nil; } + (uint16_t)portFromAddress:(NSData *)address { uint16_t port; if ([self getHost:NULL port:&port fromAddress:address]) return port; else return 0; } + (BOOL)isIPv4Address:(NSData *)address { if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddrX = [address bytes]; if (sockaddrX->sa_family == AF_INET) { return YES; } } return NO; } + (BOOL)isIPv6Address:(NSData *)address { if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddrX = [address bytes]; if (sockaddrX->sa_family == AF_INET6) { return YES; } } return NO; } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address { return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(sa_family_t *)afPtr fromAddress:(NSData *)address { if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *sockaddrX = [address bytes]; if (sockaddrX->sa_family == AF_INET) { if ([address length] >= sizeof(struct sockaddr_in)) { struct sockaddr_in sockaddr4; memcpy(&sockaddr4, sockaddrX, sizeof(sockaddr4)); if (hostPtr) *hostPtr = [self hostFromSockaddr4:&sockaddr4]; if (portPtr) *portPtr = [self portFromSockaddr4:&sockaddr4]; if (afPtr) *afPtr = AF_INET; return YES; } } else if (sockaddrX->sa_family == AF_INET6) { if ([address length] >= sizeof(struct sockaddr_in6)) { struct sockaddr_in6 sockaddr6; memcpy(&sockaddr6, sockaddrX, sizeof(sockaddr6)); if (hostPtr) *hostPtr = [self hostFromSockaddr6:&sockaddr6]; if (portPtr) *portPtr = [self portFromSockaddr6:&sockaddr6]; if (afPtr) *afPtr = AF_INET6; return YES; } } } return NO; } + (NSData *)CRLFData { return [NSData dataWithBytes:"\x0D\x0A" length:2]; } + (NSData *)CRData { return [NSData dataWithBytes:"\x0D" length:1]; } + (NSData *)LFData { return [NSData dataWithBytes:"\x0A" length:1]; } + (NSData *)ZeroData { return [NSData dataWithBytes:"" length:1]; } @end ================================================ FILE: Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.h ================================================ // // GCDAsyncUdpSocket // // This class is in the public domain. // Originally created by Robbie Hanson of Deusty LLC. // Updated and maintained by Deusty LLC and the Apple development community. // // https://github.com/robbiehanson/CocoaAsyncSocket // #import #import #import #import NS_ASSUME_NONNULL_BEGIN extern NSString *const GCDAsyncUdpSocketException; extern NSString *const GCDAsyncUdpSocketErrorDomain; extern NSString *const GCDAsyncUdpSocketQueueName; extern NSString *const GCDAsyncUdpSocketThreadName; typedef NS_ERROR_ENUM(GCDAsyncUdpSocketErrorDomain, GCDAsyncUdpSocketError) { GCDAsyncUdpSocketNoError = 0, // Never used GCDAsyncUdpSocketBadConfigError, // Invalid configuration GCDAsyncUdpSocketBadParamError, // Invalid parameter was passed GCDAsyncUdpSocketSendTimeoutError, // A send operation timed out GCDAsyncUdpSocketClosedError, // The socket was closed GCDAsyncUdpSocketOtherError, // Description provided in userInfo }; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @class GCDAsyncUdpSocket; @protocol GCDAsyncUdpSocketDelegate @optional /** * By design, UDP is a connectionless protocol, and connecting is not needed. * However, you may optionally choose to connect to a particular host for reasons * outlined in the documentation for the various connect methods listed above. * * This method is called if one of the connect methods are invoked, and the connection is successful. **/ - (void)udpSocket:(GCDAsyncUdpSocket *)sock didConnectToAddress:(NSData *)address; /** * By design, UDP is a connectionless protocol, and connecting is not needed. * However, you may optionally choose to connect to a particular host for reasons * outlined in the documentation for the various connect methods listed above. * * This method is called if one of the connect methods are invoked, and the connection fails. * This may happen, for example, if a domain name is given for the host and the domain name is unable to be resolved. **/ - (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotConnect:(NSError * _Nullable)error; /** * Called when the datagram with the given tag has been sent. **/ - (void)udpSocket:(GCDAsyncUdpSocket *)sock didSendDataWithTag:(long)tag; /** * Called if an error occurs while trying to send a datagram. * This could be due to a timeout, or something more serious such as the data being too large to fit in a sigle packet. **/ - (void)udpSocket:(GCDAsyncUdpSocket *)sock didNotSendDataWithTag:(long)tag dueToError:(NSError * _Nullable)error; /** * Called when the socket has received the requested datagram. **/ - (void)udpSocket:(GCDAsyncUdpSocket *)sock didReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(nullable id)filterContext; /** * Called when the socket is closed. **/ - (void)udpSocketDidClose:(GCDAsyncUdpSocket *)sock withError:(NSError * _Nullable)error; @end /** * You may optionally set a receive filter for the socket. * A filter can provide several useful features: * * 1. Many times udp packets need to be parsed. * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. * The end result is a parallel socket io, datagram parsing, and packet processing. * * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. * The filter can prevent such packets from arriving at the delegate. * And because the filter can run in its own independent queue, this doesn't slow down the delegate. * * - Since the udp protocol does not guarantee delivery, udp packets may be lost. * Many protocols built atop udp thus provide various resend/re-request algorithms. * This sometimes results in duplicate packets arriving. * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. * * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. * Such packets need to be ignored. * * 3. Sometimes traffic shapers are needed to simulate real world environments. * A filter allows you to write custom code to simulate such environments. * The ability to code this yourself is especially helpful when your simulated environment * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), * or the system tools to handle this aren't available (e.g. on a mobile device). * * @param data - The packet that was received. * @param address - The address the data was received from. * See utilities section for methods to extract info from address. * @param context - Out parameter you may optionally set, which will then be passed to the delegate method. * For example, filter block can parse the data and then, * pass the parsed data to the delegate. * * @returns - YES if the received packet should be passed onto the delegate. * NO if the received packet should be discarded, and not reported to the delegete. * * Example: * * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { * * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; * * *context = response; * return (response != nil); * }; * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; * **/ typedef BOOL (^GCDAsyncUdpSocketReceiveFilterBlock)(NSData *data, NSData *address, id __nullable * __nonnull context); /** * You may optionally set a send filter for the socket. * A filter can provide several interesting possibilities: * * 1. Optional caching of resolved addresses for domain names. * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. * * 2. Reusable modules of code for bandwidth monitoring. * * 3. Sometimes traffic shapers are needed to simulate real world environments. * A filter allows you to write custom code to simulate such environments. * The ability to code this yourself is especially helpful when your simulated environment * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), * or the system tools to handle this aren't available (e.g. on a mobile device). * * @param data - The packet that was received. * @param address - The address the data was received from. * See utilities section for methods to extract info from address. * @param tag - The tag that was passed in the send method. * * @returns - YES if the packet should actually be sent over the socket. * NO if the packet should be silently dropped (not sent over the socket). * * Regardless of the return value, the delegate will be informed that the packet was successfully sent. * **/ typedef BOOL (^GCDAsyncUdpSocketSendFilterBlock)(NSData *data, NSData *address, long tag); @interface GCDAsyncUdpSocket : NSObject /** * GCDAsyncUdpSocket uses the standard delegate paradigm, * but executes all delegate callbacks on a given delegate dispatch queue. * This allows for maximum concurrency, while at the same time providing easy thread safety. * * You MUST set a delegate AND delegate dispatch queue before attempting to * use the socket, or you will get an error. * * The socket queue is optional. * If you pass NULL, GCDAsyncSocket will automatically create its own socket queue. * If you choose to provide a socket queue, the socket queue must not be a concurrent queue, * then please see the discussion for the method markSocketQueueTargetQueue. * * The delegate queue and socket queue can optionally be the same. **/ - (instancetype)init; - (instancetype)initWithSocketQueue:(nullable dispatch_queue_t)sq; - (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq; - (instancetype)initWithDelegate:(nullable id)aDelegate delegateQueue:(nullable dispatch_queue_t)dq socketQueue:(nullable dispatch_queue_t)sq NS_DESIGNATED_INITIALIZER; #pragma mark Configuration - (nullable id)delegate; - (void)setDelegate:(nullable id)delegate; - (void)synchronouslySetDelegate:(nullable id)delegate; - (nullable dispatch_queue_t)delegateQueue; - (void)setDelegateQueue:(nullable dispatch_queue_t)delegateQueue; - (void)synchronouslySetDelegateQueue:(nullable dispatch_queue_t)delegateQueue; - (void)getDelegate:(id __nullable * __nullable)delegatePtr delegateQueue:(dispatch_queue_t __nullable * __nullable)delegateQueuePtr; - (void)setDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; - (void)synchronouslySetDelegate:(nullable id)delegate delegateQueue:(nullable dispatch_queue_t)delegateQueue; /** * By default, both IPv4 and IPv6 are enabled. * * This means GCDAsyncUdpSocket automatically supports both protocols, * and can send to IPv4 or IPv6 addresses, * as well as receive over IPv4 and IPv6. * * For operations that require DNS resolution, GCDAsyncUdpSocket supports both IPv4 and IPv6. * If a DNS lookup returns only IPv4 results, GCDAsyncUdpSocket will automatically use IPv4. * If a DNS lookup returns only IPv6 results, GCDAsyncUdpSocket will automatically use IPv6. * If a DNS lookup returns both IPv4 and IPv6 results, then the protocol used depends on the configured preference. * If IPv4 is preferred, then IPv4 is used. * If IPv6 is preferred, then IPv6 is used. * If neutral, then the first IP version in the resolved array will be used. * * Starting with Mac OS X 10.7 Lion and iOS 5, the default IP preference is neutral. * On prior systems the default IP preference is IPv4. **/ - (BOOL)isIPv4Enabled; - (void)setIPv4Enabled:(BOOL)flag; - (BOOL)isIPv6Enabled; - (void)setIPv6Enabled:(BOOL)flag; - (BOOL)isIPv4Preferred; - (BOOL)isIPv6Preferred; - (BOOL)isIPVersionNeutral; - (void)setPreferIPv4; - (void)setPreferIPv6; - (void)setIPVersionNeutral; /** * Gets/Sets the maximum size of the buffer that will be allocated for receive operations. * The default maximum size is 65535 bytes. * * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. * * Since the OS/GCD notifies us of the size of each received UDP packet, * the actual allocated buffer size for each packet is exact. * And in practice the size of UDP packets is generally much smaller than the max. * Indeed most protocols will send and receive packets of only a few bytes, * or will set a limit on the size of packets to prevent fragmentation in the IP layer. * * If you set the buffer size too small, the sockets API in the OS will silently discard * any extra data, and you will not be notified of the error. **/ - (uint16_t)maxReceiveIPv4BufferSize; - (void)setMaxReceiveIPv4BufferSize:(uint16_t)max; - (uint32_t)maxReceiveIPv6BufferSize; - (void)setMaxReceiveIPv6BufferSize:(uint32_t)max; /** * Gets/Sets the maximum size of the buffer that will be allocated for send operations. * The default maximum size is 65535 bytes. * * Given that a typical link MTU is 1500 bytes, a large UDP datagram will have to be * fragmented, and that’s both expensive and risky (if one fragment goes missing, the * entire datagram is lost). You are much better off sending a large number of smaller * UDP datagrams, preferably using a path MTU algorithm to avoid fragmentation. * * You must set it before the sockt is created otherwise it won't work. * **/ - (uint16_t)maxSendBufferSize; - (void)setMaxSendBufferSize:(uint16_t)max; /** * User data allows you to associate arbitrary information with the socket. * This data is not used internally in any way. **/ - (nullable id)userData; - (void)setUserData:(nullable id)arbitraryUserData; #pragma mark Diagnostics /** * Returns the local address info for the socket. * * The localAddress method returns a sockaddr structure wrapped in a NSData object. * The localHost method returns the human readable IP address as a string. * * Note: Address info may not be available until after the socket has been binded, connected * or until after data has been sent. **/ - (nullable NSData *)localAddress; - (nullable NSString *)localHost; - (uint16_t)localPort; - (nullable NSData *)localAddress_IPv4; - (nullable NSString *)localHost_IPv4; - (uint16_t)localPort_IPv4; - (nullable NSData *)localAddress_IPv6; - (nullable NSString *)localHost_IPv6; - (uint16_t)localPort_IPv6; /** * Returns the remote address info for the socket. * * The connectedAddress method returns a sockaddr structure wrapped in a NSData object. * The connectedHost method returns the human readable IP address as a string. * * Note: Since UDP is connectionless by design, connected address info * will not be available unless the socket is explicitly connected to a remote host/port. * If the socket is not connected, these methods will return nil / 0. **/ - (nullable NSData *)connectedAddress; - (nullable NSString *)connectedHost; - (uint16_t)connectedPort; /** * Returns whether or not this socket has been connected to a single host. * By design, UDP is a connectionless protocol, and connecting is not needed. * If connected, the socket will only be able to send/receive data to/from the connected host. **/ - (BOOL)isConnected; /** * Returns whether or not this socket has been closed. * The only way a socket can be closed is if you explicitly call one of the close methods. **/ - (BOOL)isClosed; /** * Returns whether or not this socket is IPv4. * * By default this will be true, unless: * - IPv4 is disabled (via setIPv4Enabled:) * - The socket is explicitly bound to an IPv6 address * - The socket is connected to an IPv6 address **/ - (BOOL)isIPv4; /** * Returns whether or not this socket is IPv6. * * By default this will be true, unless: * - IPv6 is disabled (via setIPv6Enabled:) * - The socket is explicitly bound to an IPv4 address * _ The socket is connected to an IPv4 address * * This method will also return false on platforms that do not support IPv6. * Note: The iPhone does not currently support IPv6. **/ - (BOOL)isIPv6; #pragma mark Binding /** * Binds the UDP socket to the given port. * Binding should be done for server sockets that receive data prior to sending it. * Client sockets can skip binding, * as the OS will automatically assign the socket an available port when it starts sending data. * * You may optionally pass a port number of zero to immediately bind the socket, * yet still allow the OS to automatically assign an available port. * * You cannot bind a socket after its been connected. * You can only bind a socket once. * You can still connect a socket (if desired) after binding. * * On success, returns YES. * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. **/ - (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr; /** * Binds the UDP socket to the given port and optional interface. * Binding should be done for server sockets that receive data prior to sending it. * Client sockets can skip binding, * as the OS will automatically assign the socket an available port when it starts sending data. * * You may optionally pass a port number of zero to immediately bind the socket, * yet still allow the OS to automatically assign an available port. * * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). * You may also use the special strings "localhost" or "loopback" to specify that * the socket only accept packets from the local machine. * * You cannot bind a socket after its been connected. * You can only bind a socket once. * You can still connect a socket (if desired) after binding. * * On success, returns YES. * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. **/ - (BOOL)bindToPort:(uint16_t)port interface:(nullable NSString *)interface error:(NSError **)errPtr; /** * Binds the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. * * If you have an existing struct sockaddr you can convert it to a NSData object like so: * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; * * Binding should be done for server sockets that receive data prior to sending it. * Client sockets can skip binding, * as the OS will automatically assign the socket an available port when it starts sending data. * * You cannot bind a socket after its been connected. * You can only bind a socket once. * You can still connect a socket (if desired) after binding. * * On success, returns YES. * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass NULL for errPtr. **/ - (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr; #pragma mark Connecting /** * Connects the UDP socket to the given host and port. * By design, UDP is a connectionless protocol, and connecting is not needed. * * Choosing to connect to a specific host/port has the following effect: * - You will only be able to send data to the connected host/port. * - You will only be able to receive data from the connected host/port. * - You will receive ICMP messages that come from the connected host/port, such as "connection refused". * * The actual process of connecting a UDP socket does not result in any communication on the socket. * It simply changes the internal state of the socket. * * You cannot bind a socket after it has been connected. * You can only connect a socket once. * * The host may be a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). * * This method is asynchronous as it requires a DNS lookup to resolve the given host name. * If an obvious error is detected, this method immediately returns NO and sets errPtr. * If you don't care about the error, you can pass nil for errPtr. * Otherwise, this method returns YES and begins the asynchronous connection process. * The result of the asynchronous connection process will be reported via the delegate methods. **/ - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr; /** * Connects the UDP socket to the given address, specified as a sockaddr structure wrapped in a NSData object. * * If you have an existing struct sockaddr you can convert it to a NSData object like so: * struct sockaddr sa -> NSData *dsa = [NSData dataWithBytes:&remoteAddr length:remoteAddr.sa_len]; * struct sockaddr *sa -> NSData *dsa = [NSData dataWithBytes:remoteAddr length:remoteAddr->sa_len]; * * By design, UDP is a connectionless protocol, and connecting is not needed. * * Choosing to connect to a specific address has the following effect: * - You will only be able to send data to the connected address. * - You will only be able to receive data from the connected address. * - You will receive ICMP messages that come from the connected address, such as "connection refused". * * Connecting a UDP socket does not result in any communication on the socket. * It simply changes the internal state of the socket. * * You cannot bind a socket after its been connected. * You can only connect a socket once. * * On success, returns YES. * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. * * Note: Unlike the connectToHost:onPort:error: method, this method does not require a DNS lookup. * Thus when this method returns, the connection has either failed or fully completed. * In other words, this method is synchronous, unlike the asynchronous connectToHost::: method. * However, for compatibility and simplification of delegate code, if this method returns YES * then the corresponding delegate method (udpSocket:didConnectToHost:port:) is still invoked. **/ - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr; #pragma mark Multicast /** * Join multicast group. * Group should be an IP address (eg @"225.228.0.1"). * * On success, returns YES. * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. **/ - (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr; /** * Join multicast group. * Group should be an IP address (eg @"225.228.0.1"). * The interface may be a name (e.g. "en1" or "lo0") or the corresponding IP address (e.g. "192.168.4.35"). * * On success, returns YES. * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. **/ - (BOOL)joinMulticastGroup:(NSString *)group onInterface:(nullable NSString *)interface error:(NSError **)errPtr; - (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr; - (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(nullable NSString *)interface error:(NSError **)errPtr; /** * Send multicast on a specified interface. * For IPv4, interface should be the the IP address of the interface (eg @"192.168.10.1"). * For IPv6, interface should be the a network interface name (eg @"en0"). * * On success, returns YES. * Otherwise returns NO, and sets errPtr. If you don't care about the error, you can pass nil for errPtr. **/ - (BOOL)sendIPv4MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr; - (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr; #pragma mark Reuse Port /** * By default, only one socket can be bound to a given IP address + port at a time. * To enable multiple processes to simultaneously bind to the same address+port, * you need to enable this functionality in the socket. All processes that wish to * use the address+port simultaneously must all enable reuse port on the socket * bound to that port. **/ - (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr; #pragma mark Broadcast /** * By default, the underlying socket in the OS will not allow you to send broadcast messages. * In order to send broadcast messages, you need to enable this functionality in the socket. * * A broadcast is a UDP message to addresses like "192.168.255.255" or "255.255.255.255" that is * delivered to every host on the network. * The reason this is generally disabled by default (by the OS) is to prevent * accidental broadcast messages from flooding the network. **/ - (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr; #pragma mark Sending /** * Asynchronously sends the given data, with the given timeout and tag. * * This method may only be used with a connected socket. * Recall that connecting is optional for a UDP socket. * For connected sockets, data can only be sent to the connected address. * For non-connected sockets, the remote destination is specified for each packet. * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. * * @param data * The data to send. * If data is nil or zero-length, this method does nothing. * If passing NSMutableData, please read the thread-safety notice below. * * @param timeout * The timeout for the send opeartion. * If the timeout value is negative, the send operation will not use a timeout. * * @param tag * The tag is for your convenience. * It is not sent or received over the socket in any manner what-so-ever. * It is reported back as a parameter in the udpSocket:didSendDataWithTag: * or udpSocket:didNotSendDataWithTag:dueToError: methods. * You can use it as an array index, state id, type constant, etc. * * * Thread-Safety Note: * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying * that this particular send operation has completed. * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. * It simply retains it for performance reasons. * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. * Copying this data adds an unwanted/unneeded overhead. * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. **/ - (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Asynchronously sends the given data, with the given timeout and tag, to the given host and port. * * This method cannot be used with a connected socket. * Recall that connecting is optional for a UDP socket. * For connected sockets, data can only be sent to the connected address. * For non-connected sockets, the remote destination is specified for each packet. * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. * * @param data * The data to send. * If data is nil or zero-length, this method does nothing. * If passing NSMutableData, please read the thread-safety notice below. * * @param host * The destination to send the udp packet to. * May be specified as a domain name (e.g. "deusty.com") or an IP address string (e.g. "192.168.0.2"). * You may also use the convenience strings of "loopback" or "localhost". * * @param port * The port of the host to send to. * * @param timeout * The timeout for the send opeartion. * If the timeout value is negative, the send operation will not use a timeout. * * @param tag * The tag is for your convenience. * It is not sent or received over the socket in any manner what-so-ever. * It is reported back as a parameter in the udpSocket:didSendDataWithTag: * or udpSocket:didNotSendDataWithTag:dueToError: methods. * You can use it as an array index, state id, type constant, etc. * * * Thread-Safety Note: * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying * that this particular send operation has completed. * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. * It simply retains it for performance reasons. * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. * Copying this data adds an unwanted/unneeded overhead. * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. **/ - (void)sendData:(NSData *)data toHost:(NSString *)host port:(uint16_t)port withTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * Asynchronously sends the given data, with the given timeout and tag, to the given address. * * This method cannot be used with a connected socket. * Recall that connecting is optional for a UDP socket. * For connected sockets, data can only be sent to the connected address. * For non-connected sockets, the remote destination is specified for each packet. * For more information about optionally connecting udp sockets, see the documentation for the connect methods above. * * @param data * The data to send. * If data is nil or zero-length, this method does nothing. * If passing NSMutableData, please read the thread-safety notice below. * * @param remoteAddr * The address to send the data to (specified as a sockaddr structure wrapped in a NSData object). * * @param timeout * The timeout for the send opeartion. * If the timeout value is negative, the send operation will not use a timeout. * * @param tag * The tag is for your convenience. * It is not sent or received over the socket in any manner what-so-ever. * It is reported back as a parameter in the udpSocket:didSendDataWithTag: * or udpSocket:didNotSendDataWithTag:dueToError: methods. * You can use it as an array index, state id, type constant, etc. * * * Thread-Safety Note: * If the given data parameter is mutable (NSMutableData) then you MUST NOT alter the data while * the socket is sending it. In other words, it's not safe to alter the data until after the delegate method * udpSocket:didSendDataWithTag: or udpSocket:didNotSendDataWithTag:dueToError: is invoked signifying * that this particular send operation has completed. * This is due to the fact that GCDAsyncUdpSocket does NOT copy the data. * It simply retains it for performance reasons. * Often times, if NSMutableData is passed, it is because a request/response was built up in memory. * Copying this data adds an unwanted/unneeded overhead. * If you need to write data from an immutable buffer, and you need to alter the buffer before the socket * completes sending the bytes (which is NOT immediately after this method returns, but rather at a later time * when the delegate method notifies you), then you should first copy the bytes, and pass the copy to this method. **/ - (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag; /** * You may optionally set a send filter for the socket. * A filter can provide several interesting possibilities: * * 1. Optional caching of resolved addresses for domain names. * The cache could later be consulted, resulting in fewer system calls to getaddrinfo. * * 2. Reusable modules of code for bandwidth monitoring. * * 3. Sometimes traffic shapers are needed to simulate real world environments. * A filter allows you to write custom code to simulate such environments. * The ability to code this yourself is especially helpful when your simulated environment * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), * or the system tools to handle this aren't available (e.g. on a mobile device). * * For more information about GCDAsyncUdpSocketSendFilterBlock, see the documentation for its typedef. * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. * * Note: This method invokes setSendFilter:withQueue:isAsynchronous: (documented below), * passing YES for the isAsynchronous parameter. **/ - (void)setSendFilter:(nullable GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue; /** * The receive filter can be run via dispatch_async or dispatch_sync. * Most typical situations call for asynchronous operation. * * However, there are a few situations in which synchronous operation is preferred. * Such is the case when the filter is extremely minimal and fast. * This is because dispatch_sync is faster than dispatch_async. * * If you choose synchronous operation, be aware of possible deadlock conditions. * Since the socket queue is executing your block via dispatch_sync, * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. * For example, you can't query properties on the socket. **/ - (void)setSendFilter:(nullable GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue isAsynchronous:(BOOL)isAsynchronous; #pragma mark Receiving /** * There are two modes of operation for receiving packets: one-at-a-time & continuous. * * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, * where your state machine may not always be ready to process incoming packets. * * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. * Receiving packets continuously is better suited to real-time streaming applications. * * You may switch back and forth between one-at-a-time mode and continuous mode. * If the socket is currently in continuous mode, calling this method will switch it to one-at-a-time mode. * * When a packet is received (and not filtered by the optional receive filter), * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. * * If the socket is able to begin receiving packets, this method returns YES. * Otherwise it returns NO, and sets the errPtr with appropriate error information. * * An example error: * You created a udp socket to act as a server, and immediately called receive. * You forgot to first bind the socket to a port number, and received a error with a message like: * "Must bind socket before you can receive data." **/ - (BOOL)receiveOnce:(NSError **)errPtr; /** * There are two modes of operation for receiving packets: one-at-a-time & continuous. * * In one-at-a-time mode, you call receiveOnce everytime your delegate is ready to process an incoming udp packet. * Receiving packets one-at-a-time may be better suited for implementing certain state machine code, * where your state machine may not always be ready to process incoming packets. * * In continuous mode, the delegate is invoked immediately everytime incoming udp packets are received. * Receiving packets continuously is better suited to real-time streaming applications. * * You may switch back and forth between one-at-a-time mode and continuous mode. * If the socket is currently in one-at-a-time mode, calling this method will switch it to continuous mode. * * For every received packet (not filtered by the optional receive filter), * the delegate method (udpSocket:didReceiveData:fromAddress:withFilterContext:) is invoked. * * If the socket is able to begin receiving packets, this method returns YES. * Otherwise it returns NO, and sets the errPtr with appropriate error information. * * An example error: * You created a udp socket to act as a server, and immediately called receive. * You forgot to first bind the socket to a port number, and received a error with a message like: * "Must bind socket before you can receive data." **/ - (BOOL)beginReceiving:(NSError **)errPtr; /** * If the socket is currently receiving (beginReceiving has been called), this method pauses the receiving. * That is, it won't read any more packets from the underlying OS socket until beginReceiving is called again. * * Important Note: * GCDAsyncUdpSocket may be running in parallel with your code. * That is, your delegate is likely running on a separate thread/dispatch_queue. * When you invoke this method, GCDAsyncUdpSocket may have already dispatched delegate methods to be invoked. * Thus, if those delegate methods have already been dispatch_async'd, * your didReceive delegate method may still be invoked after this method has been called. * You should be aware of this, and program defensively. **/ - (void)pauseReceiving; /** * You may optionally set a receive filter for the socket. * This receive filter may be set to run in its own queue (independent of delegate queue). * * A filter can provide several useful features. * * 1. Many times udp packets need to be parsed. * Since the filter can run in its own independent queue, you can parallelize this parsing quite easily. * The end result is a parallel socket io, datagram parsing, and packet processing. * * 2. Many times udp packets are discarded because they are duplicate/unneeded/unsolicited. * The filter can prevent such packets from arriving at the delegate. * And because the filter can run in its own independent queue, this doesn't slow down the delegate. * * - Since the udp protocol does not guarantee delivery, udp packets may be lost. * Many protocols built atop udp thus provide various resend/re-request algorithms. * This sometimes results in duplicate packets arriving. * A filter may allow you to architect the duplicate detection code to run in parallel to normal processing. * * - Since the udp socket may be connectionless, its possible for unsolicited packets to arrive. * Such packets need to be ignored. * * 3. Sometimes traffic shapers are needed to simulate real world environments. * A filter allows you to write custom code to simulate such environments. * The ability to code this yourself is especially helpful when your simulated environment * is more complicated than simple traffic shaping (e.g. simulating a cone port restricted router), * or the system tools to handle this aren't available (e.g. on a mobile device). * * Example: * * GCDAsyncUdpSocketReceiveFilterBlock filter = ^BOOL (NSData *data, NSData *address, id *context) { * * MyProtocolMessage *msg = [MyProtocol parseMessage:data]; * * *context = response; * return (response != nil); * }; * [udpSocket setReceiveFilter:filter withQueue:myParsingQueue]; * * For more information about GCDAsyncUdpSocketReceiveFilterBlock, see the documentation for its typedef. * To remove a previously set filter, invoke this method and pass a nil filterBlock and NULL filterQueue. * * Note: This method invokes setReceiveFilter:withQueue:isAsynchronous: (documented below), * passing YES for the isAsynchronous parameter. **/ - (void)setReceiveFilter:(nullable GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue; /** * The receive filter can be run via dispatch_async or dispatch_sync. * Most typical situations call for asynchronous operation. * * However, there are a few situations in which synchronous operation is preferred. * Such is the case when the filter is extremely minimal and fast. * This is because dispatch_sync is faster than dispatch_async. * * If you choose synchronous operation, be aware of possible deadlock conditions. * Since the socket queue is executing your block via dispatch_sync, * then you cannot perform any tasks which may invoke dispatch_sync on the socket queue. * For example, you can't query properties on the socket. **/ - (void)setReceiveFilter:(nullable GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(nullable dispatch_queue_t)filterQueue isAsynchronous:(BOOL)isAsynchronous; #pragma mark Closing /** * Immediately closes the underlying socket. * Any pending send operations are discarded. * * The GCDAsyncUdpSocket instance may optionally be used again. * (it will setup/configure/use another unnderlying BSD socket). **/ - (void)close; /** * Closes the underlying socket after all pending send operations have been sent. * * The GCDAsyncUdpSocket instance may optionally be used again. * (it will setup/configure/use another unnderlying BSD socket). **/ - (void)closeAfterSending; #pragma mark Advanced /** * GCDAsyncSocket maintains thread safety by using an internal serial dispatch_queue. * In most cases, the instance creates this queue itself. * However, to allow for maximum flexibility, the internal queue may be passed in the init method. * This allows for some advanced options such as controlling socket priority via target queues. * However, when one begins to use target queues like this, they open the door to some specific deadlock issues. * * For example, imagine there are 2 queues: * dispatch_queue_t socketQueue; * dispatch_queue_t socketTargetQueue; * * If you do this (pseudo-code): * socketQueue.targetQueue = socketTargetQueue; * * Then all socketQueue operations will actually get run on the given socketTargetQueue. * This is fine and works great in most situations. * But if you run code directly from within the socketTargetQueue that accesses the socket, * you could potentially get deadlock. Imagine the following code: * * - (BOOL)socketHasSomething * { * __block BOOL result = NO; * dispatch_block_t block = ^{ * result = [self someInternalMethodToBeRunOnlyOnSocketQueue]; * } * if (is_executing_on_queue(socketQueue)) * block(); * else * dispatch_sync(socketQueue, block); * * return result; * } * * What happens if you call this method from the socketTargetQueue? The result is deadlock. * This is because the GCD API offers no mechanism to discover a queue's targetQueue. * Thus we have no idea if our socketQueue is configured with a targetQueue. * If we had this information, we could easily avoid deadlock. * But, since these API's are missing or unfeasible, you'll have to explicitly set it. * * IF you pass a socketQueue via the init method, * AND you've configured the passed socketQueue with a targetQueue, * THEN you should pass the end queue in the target hierarchy. * * For example, consider the following queue hierarchy: * socketQueue -> ipQueue -> moduleQueue * * This example demonstrates priority shaping within some server. * All incoming client connections from the same IP address are executed on the same target queue. * And all connections for a particular module are executed on the same target queue. * Thus, the priority of all networking for the entire module can be changed on the fly. * Additionally, networking traffic from a single IP cannot monopolize the module. * * Here's how you would accomplish something like that: * - (dispatch_queue_t)newSocketQueueForConnectionFromAddress:(NSData *)address onSocket:(GCDAsyncSocket *)sock * { * dispatch_queue_t socketQueue = dispatch_queue_create("", NULL); * dispatch_queue_t ipQueue = [self ipQueueForAddress:address]; * * dispatch_set_target_queue(socketQueue, ipQueue); * dispatch_set_target_queue(iqQueue, moduleQueue); * * return socketQueue; * } * - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket * { * [clientConnections addObject:newSocket]; * [newSocket markSocketQueueTargetQueue:moduleQueue]; * } * * Note: This workaround is ONLY needed if you intend to execute code directly on the ipQueue or moduleQueue. * This is often NOT the case, as such queues are used solely for execution shaping. **/ - (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreConfiguredTargetQueue; - (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketQueuesPreviouslyConfiguredTargetQueue; /** * It's not thread-safe to access certain variables from outside the socket's internal queue. * * For example, the socket file descriptor. * File descriptors are simply integers which reference an index in the per-process file table. * However, when one requests a new file descriptor (by opening a file or socket), * the file descriptor returned is guaranteed to be the lowest numbered unused descriptor. * So if we're not careful, the following could be possible: * * - Thread A invokes a method which returns the socket's file descriptor. * - The socket is closed via the socket's internal queue on thread B. * - Thread C opens a file, and subsequently receives the file descriptor that was previously the socket's FD. * - Thread A is now accessing/altering the file instead of the socket. * * In addition to this, other variables are not actually objects, * and thus cannot be retained/released or even autoreleased. * An example is the sslContext, of type SSLContextRef, which is actually a malloc'd struct. * * Although there are internal variables that make it difficult to maintain thread-safety, * it is important to provide access to these variables * to ensure this class can be used in a wide array of environments. * This method helps to accomplish this by invoking the current block on the socket's internal queue. * The methods below can be invoked from within the block to access * those generally thread-unsafe internal variables in a thread-safe manner. * The given block will be invoked synchronously on the socket's internal queue. * * If you save references to any protected variables and use them outside the block, you do so at your own peril. **/ - (void)performBlock:(dispatch_block_t)block; /** * These methods are only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Provides access to the socket's file descriptor(s). * If the socket isn't connected, or explicity bound to a particular interface, * it might actually have multiple internal socket file descriptors - one for IPv4 and one for IPv6. **/ - (int)socketFD; - (int)socket4FD; - (int)socket6FD; #if TARGET_OS_IPHONE /** * These methods are only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Returns (creating if necessary) a CFReadStream/CFWriteStream for the internal socket. * * Generally GCDAsyncUdpSocket doesn't use CFStream. (It uses the faster GCD API's.) * However, if you need one for any reason, * these methods are a convenient way to get access to a safe instance of one. **/ - (nullable CFReadStreamRef)readStream; - (nullable CFWriteStreamRef)writeStream; /** * This method is only available from within the context of a performBlock: invocation. * See the documentation for the performBlock: method above. * * Configures the socket to allow it to operate when the iOS application has been backgrounded. * In other words, this method creates a read & write stream, and invokes: * * CFReadStreamSetProperty(readStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); * CFWriteStreamSetProperty(writeStream, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); * * Returns YES if successful, NO otherwise. * * Example usage: * * [asyncUdpSocket performBlock:^{ * [asyncUdpSocket enableBackgroundingOnSocket]; * }]; * * * NOTE : Apple doesn't currently support backgrounding UDP sockets. (Only TCP for now). **/ //- (BOOL)enableBackgroundingOnSockets; #endif #pragma mark Utilities /** * Extracting host/port/family information from raw address data. **/ + (nullable NSString *)hostFromAddress:(NSData *)address; + (uint16_t)portFromAddress:(NSData *)address; + (int)familyFromAddress:(NSData *)address; + (BOOL)isIPv4Address:(NSData *)address; + (BOOL)isIPv6Address:(NSData *)address; + (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(uint16_t * __nullable)portPtr fromAddress:(NSData *)address; + (BOOL)getHost:(NSString * __nullable * __nullable)hostPtr port:(uint16_t * __nullable)portPtr family:(int * __nullable)afPtr fromAddress:(NSData *)address; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/CocoaAsyncSocket/Source/GCD/GCDAsyncUdpSocket.m ================================================ // // GCDAsyncUdpSocket // // This class is in the public domain. // Originally created by Robbie Hanson of Deusty LLC. // Updated and maintained by Deusty LLC and the Apple development community. // // https://github.com/robbiehanson/CocoaAsyncSocket // #import "GCDAsyncUdpSocket.h" #if ! __has_feature(objc_arc) #warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC). // For more information see: https://github.com/robbiehanson/CocoaAsyncSocket/wiki/ARC #endif #if TARGET_OS_IPHONE #import #import #endif #import #import #import #import #import #import #import #if 0 // Logging Enabled - See log level below // Logging uses the CocoaLumberjack framework (which is also GCD based). // https://github.com/robbiehanson/CocoaLumberjack // // It allows us to do a lot of logging without significantly slowing down the code. #import "DDLog.h" #define LogAsync NO #define LogContext 65535 #define LogObjc(flg, frmt, ...) LOG_OBJC_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogC(flg, frmt, ...) LOG_C_MAYBE(LogAsync, logLevel, flg, LogContext, frmt, ##__VA_ARGS__) #define LogError(frmt, ...) LogObjc(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogWarn(frmt, ...) LogObjc(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogInfo(frmt, ...) LogObjc(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogVerbose(frmt, ...) LogObjc(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCError(frmt, ...) LogC(LOG_FLAG_ERROR, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCWarn(frmt, ...) LogC(LOG_FLAG_WARN, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCInfo(frmt, ...) LogC(LOG_FLAG_INFO, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogCVerbose(frmt, ...) LogC(LOG_FLAG_VERBOSE, (@"%@: " frmt), THIS_FILE, ##__VA_ARGS__) #define LogTrace() LogObjc(LOG_FLAG_VERBOSE, @"%@: %@", THIS_FILE, THIS_METHOD) #define LogCTrace() LogC(LOG_FLAG_VERBOSE, @"%@: %s", THIS_FILE, __FUNCTION__) // Log levels : off, error, warn, info, verbose static const int logLevel = LOG_LEVEL_VERBOSE; #else // Logging Disabled #define LogError(frmt, ...) {} #define LogWarn(frmt, ...) {} #define LogInfo(frmt, ...) {} #define LogVerbose(frmt, ...) {} #define LogCError(frmt, ...) {} #define LogCWarn(frmt, ...) {} #define LogCInfo(frmt, ...) {} #define LogCVerbose(frmt, ...) {} #define LogTrace() {} #define LogCTrace(frmt, ...) {} #endif /** * Seeing a return statements within an inner block * can sometimes be mistaken for a return point of the enclosing method. * This makes inline blocks a bit easier to read. **/ #define return_from_block return /** * A socket file descriptor is really just an integer. * It represents the index of the socket within the kernel. * This makes invalid file descriptor comparisons easier to read. **/ #define SOCKET_NULL -1 /** * Just to type less code. **/ #define AutoreleasedBlock(block) ^{ @autoreleasepool { block(); }} @class GCDAsyncUdpSendPacket; NSString *const GCDAsyncUdpSocketException = @"GCDAsyncUdpSocketException"; NSString *const GCDAsyncUdpSocketErrorDomain = @"GCDAsyncUdpSocketErrorDomain"; NSString *const GCDAsyncUdpSocketQueueName = @"GCDAsyncUdpSocket"; NSString *const GCDAsyncUdpSocketThreadName = @"GCDAsyncUdpSocket-CFStream"; enum GCDAsyncUdpSocketFlags { kDidCreateSockets = 1 << 0, // If set, the sockets have been created. kDidBind = 1 << 1, // If set, bind has been called. kConnecting = 1 << 2, // If set, a connection attempt is in progress. kDidConnect = 1 << 3, // If set, socket is connected. kReceiveOnce = 1 << 4, // If set, one-at-a-time receive is enabled kReceiveContinuous = 1 << 5, // If set, continuous receive is enabled kIPv4Deactivated = 1 << 6, // If set, socket4 was closed due to bind or connect on IPv6. kIPv6Deactivated = 1 << 7, // If set, socket6 was closed due to bind or connect on IPv4. kSend4SourceSuspended = 1 << 8, // If set, send4Source is suspended. kSend6SourceSuspended = 1 << 9, // If set, send6Source is suspended. kReceive4SourceSuspended = 1 << 10, // If set, receive4Source is suspended. kReceive6SourceSuspended = 1 << 11, // If set, receive6Source is suspended. kSock4CanAcceptBytes = 1 << 12, // If set, we know socket4 can accept bytes. If unset, it's unknown. kSock6CanAcceptBytes = 1 << 13, // If set, we know socket6 can accept bytes. If unset, it's unknown. kForbidSendReceive = 1 << 14, // If set, no new send or receive operations are allowed to be queued. kCloseAfterSends = 1 << 15, // If set, close as soon as no more sends are queued. kFlipFlop = 1 << 16, // Used to alternate between IPv4 and IPv6 sockets. #if TARGET_OS_IPHONE kAddedStreamListener = 1 << 17, // If set, CFStreams have been added to listener thread #endif }; enum GCDAsyncUdpSocketConfig { kIPv4Disabled = 1 << 0, // If set, IPv4 is disabled kIPv6Disabled = 1 << 1, // If set, IPv6 is disabled kPreferIPv4 = 1 << 2, // If set, IPv4 is preferred over IPv6 kPreferIPv6 = 1 << 3, // If set, IPv6 is preferred over IPv4 }; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @interface GCDAsyncUdpSocket () { #if __has_feature(objc_arc_weak) __weak id delegate; #else __unsafe_unretained id delegate; #endif dispatch_queue_t delegateQueue; GCDAsyncUdpSocketReceiveFilterBlock receiveFilterBlock; dispatch_queue_t receiveFilterQueue; BOOL receiveFilterAsync; GCDAsyncUdpSocketSendFilterBlock sendFilterBlock; dispatch_queue_t sendFilterQueue; BOOL sendFilterAsync; uint32_t flags; uint16_t config; uint16_t max4ReceiveSize; uint32_t max6ReceiveSize; uint16_t maxSendSize; int socket4FD; int socket6FD; dispatch_queue_t socketQueue; dispatch_source_t send4Source; dispatch_source_t send6Source; dispatch_source_t receive4Source; dispatch_source_t receive6Source; dispatch_source_t sendTimer; GCDAsyncUdpSendPacket *currentSend; NSMutableArray *sendQueue; unsigned long socket4FDBytesAvailable; unsigned long socket6FDBytesAvailable; uint32_t pendingFilterOperations; NSData *cachedLocalAddress4; NSString *cachedLocalHost4; uint16_t cachedLocalPort4; NSData *cachedLocalAddress6; NSString *cachedLocalHost6; uint16_t cachedLocalPort6; NSData *cachedConnectedAddress; NSString *cachedConnectedHost; uint16_t cachedConnectedPort; int cachedConnectedFamily; void *IsOnSocketQueueOrTargetQueueKey; #if TARGET_OS_IPHONE CFStreamClientContext streamContext; CFReadStreamRef readStream4; CFReadStreamRef readStream6; CFWriteStreamRef writeStream4; CFWriteStreamRef writeStream6; #endif id userData; } - (void)resumeSend4Source; - (void)resumeSend6Source; - (void)resumeReceive4Source; - (void)resumeReceive6Source; - (void)closeSockets; - (void)maybeConnect; - (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr; - (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr; - (void)maybeDequeueSend; - (void)doPreSend; - (void)doSend; - (void)endCurrentSend; - (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout; - (void)doReceive; - (void)doReceiveEOF; - (void)closeWithError:(NSError *)error; - (BOOL)performMulticastRequest:(int)requestType forGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr; #if TARGET_OS_IPHONE - (BOOL)createReadAndWriteStreams:(NSError **)errPtr; - (BOOL)registerForStreamCallbacks:(NSError **)errPtr; - (BOOL)addStreamsToRunLoop:(NSError **)errPtr; - (BOOL)openStreams:(NSError **)errPtr; - (void)removeStreamsFromRunLoop; - (void)closeReadAndWriteStreams; #endif + (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4; + (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6; + (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4; + (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6; #if TARGET_OS_IPHONE // Forward declaration + (void)listenerThread:(id)unused; #endif @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * The GCDAsyncUdpSendPacket encompasses the instructions for a single send/write. **/ @interface GCDAsyncUdpSendPacket : NSObject { @public NSData *buffer; NSTimeInterval timeout; long tag; BOOL resolveInProgress; BOOL filterInProgress; NSArray *resolvedAddresses; NSError *resolveError; NSData *address; int addressFamily; } - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i NS_DESIGNATED_INITIALIZER; @end @implementation GCDAsyncUdpSendPacket // Cover the superclass' designated initializer - (instancetype)init NS_UNAVAILABLE { NSAssert(0, @"Use the designated initializer"); return nil; } - (instancetype)initWithData:(NSData *)d timeout:(NSTimeInterval)t tag:(long)i { if ((self = [super init])) { buffer = d; timeout = t; tag = i; resolveInProgress = NO; } return self; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @interface GCDAsyncUdpSpecialPacket : NSObject { @public // uint8_t type; BOOL resolveInProgress; NSArray *addresses; NSError *error; } - (instancetype)init NS_DESIGNATED_INITIALIZER; @end @implementation GCDAsyncUdpSpecialPacket - (instancetype)init { self = [super init]; return self; } @end //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark - //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @implementation GCDAsyncUdpSocket - (instancetype)init { LogTrace(); return [self initWithDelegate:nil delegateQueue:NULL socketQueue:NULL]; } - (instancetype)initWithSocketQueue:(dispatch_queue_t)sq { LogTrace(); return [self initWithDelegate:nil delegateQueue:NULL socketQueue:sq]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq { LogTrace(); return [self initWithDelegate:aDelegate delegateQueue:dq socketQueue:NULL]; } - (instancetype)initWithDelegate:(id)aDelegate delegateQueue:(dispatch_queue_t)dq socketQueue:(dispatch_queue_t)sq { LogTrace(); if ((self = [super init])) { delegate = aDelegate; if (dq) { delegateQueue = dq; #if !OS_OBJECT_USE_OBJC dispatch_retain(delegateQueue); #endif } max4ReceiveSize = 65535; max6ReceiveSize = 65535; maxSendSize = 65535; socket4FD = SOCKET_NULL; socket6FD = SOCKET_NULL; if (sq) { NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), @"The given socketQueue parameter must not be a concurrent queue."); NSAssert(sq != dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), @"The given socketQueue parameter must not be a concurrent queue."); socketQueue = sq; #if !OS_OBJECT_USE_OBJC dispatch_retain(socketQueue); #endif } else { socketQueue = dispatch_queue_create([GCDAsyncUdpSocketQueueName UTF8String], NULL); } // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter. // From the documentation: // // > Keys are only compared as pointers and are never dereferenced. // > Thus, you can use a pointer to a static variable for a specific subsystem or // > any other value that allows you to identify the value uniquely. // // We're just going to use the memory address of an ivar. // Specifically an ivar that is explicitly named for our purpose to make the code more readable. // // However, it feels tedious (and less readable) to include the "&" all the time: // dispatch_get_specific(&IsOnSocketQueueOrTargetQueueKey) // // So we're going to make it so it doesn't matter if we use the '&' or not, // by assigning the value of the ivar to the address of the ivar. // Thus: IsOnSocketQueueOrTargetQueueKey == &IsOnSocketQueueOrTargetQueueKey; IsOnSocketQueueOrTargetQueueKey = &IsOnSocketQueueOrTargetQueueKey; void *nonNullUnusedPointer = (__bridge void *)self; dispatch_queue_set_specific(socketQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); currentSend = nil; sendQueue = [[NSMutableArray alloc] initWithCapacity:5]; #if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; #endif } return self; } - (void)dealloc { LogInfo(@"%@ - %@ (start)", THIS_METHOD, self); #if TARGET_OS_IPHONE [[NSNotificationCenter defaultCenter] removeObserver:self]; #endif if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { [self closeWithError:nil]; } else { dispatch_sync(socketQueue, ^{ [self closeWithError:nil]; }); } delegate = nil; #if !OS_OBJECT_USE_OBJC if (delegateQueue) dispatch_release(delegateQueue); #endif delegateQueue = NULL; #if !OS_OBJECT_USE_OBJC if (socketQueue) dispatch_release(socketQueue); #endif socketQueue = NULL; LogInfo(@"%@ - %@ (finish)", THIS_METHOD, self); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Configuration //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (id)delegate { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return delegate; } else { __block id result = nil; dispatch_sync(socketQueue, ^{ result = self->delegate; }); return result; } } - (void)setDelegate:(id)newDelegate synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ self->delegate = newDelegate; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } else { if (synchronously) dispatch_sync(socketQueue, block); else dispatch_async(socketQueue, block); } } - (void)setDelegate:(id)newDelegate { [self setDelegate:newDelegate synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate { [self setDelegate:newDelegate synchronously:YES]; } - (dispatch_queue_t)delegateQueue { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { return delegateQueue; } else { __block dispatch_queue_t result = NULL; dispatch_sync(socketQueue, ^{ result = self->delegateQueue; }); return result; } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ #if !OS_OBJECT_USE_OBJC if (self->delegateQueue) dispatch_release(self->delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif self->delegateQueue = newDelegateQueue; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } else { if (synchronously) dispatch_sync(socketQueue, block); else dispatch_async(socketQueue, block); } } - (void)setDelegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegateQueue:newDelegateQueue synchronously:YES]; } - (void)getDelegate:(id *)delegatePtr delegateQueue:(dispatch_queue_t *)delegateQueuePtr { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { if (delegatePtr) *delegatePtr = delegate; if (delegateQueuePtr) *delegateQueuePtr = delegateQueue; } else { __block id dPtr = NULL; __block dispatch_queue_t dqPtr = NULL; dispatch_sync(socketQueue, ^{ dPtr = self->delegate; dqPtr = self->delegateQueue; }); if (delegatePtr) *delegatePtr = dPtr; if (delegateQueuePtr) *delegateQueuePtr = dqPtr; } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue synchronously:(BOOL)synchronously { dispatch_block_t block = ^{ self->delegate = newDelegate; #if !OS_OBJECT_USE_OBJC if (self->delegateQueue) dispatch_release(self->delegateQueue); if (newDelegateQueue) dispatch_retain(newDelegateQueue); #endif self->delegateQueue = newDelegateQueue; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { block(); } else { if (synchronously) dispatch_sync(socketQueue, block); else dispatch_async(socketQueue, block); } } - (void)setDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:NO]; } - (void)synchronouslySetDelegate:(id)newDelegate delegateQueue:(dispatch_queue_t)newDelegateQueue { [self setDelegate:newDelegate delegateQueue:newDelegateQueue synchronously:YES]; } - (BOOL)isIPv4Enabled { // Note: YES means kIPv4Disabled is OFF __block BOOL result = NO; dispatch_block_t block = ^{ result = ((self->config & kIPv4Disabled) == 0); }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (void)setIPv4Enabled:(BOOL)flag { // Note: YES means kIPv4Disabled is OFF dispatch_block_t block = ^{ LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); if (flag) self->config &= ~kIPv4Disabled; else self->config |= kIPv4Disabled; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (BOOL)isIPv6Enabled { // Note: YES means kIPv6Disabled is OFF __block BOOL result = NO; dispatch_block_t block = ^{ result = ((self->config & kIPv6Disabled) == 0); }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (void)setIPv6Enabled:(BOOL)flag { // Note: YES means kIPv6Disabled is OFF dispatch_block_t block = ^{ LogVerbose(@"%@ %@", THIS_METHOD, (flag ? @"YES" : @"NO")); if (flag) self->config &= ~kIPv6Disabled; else self->config |= kIPv6Disabled; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (BOOL)isIPv4Preferred { __block BOOL result = NO; dispatch_block_t block = ^{ result = (self->config & kPreferIPv4) ? YES : NO; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (BOOL)isIPv6Preferred { __block BOOL result = NO; dispatch_block_t block = ^{ result = (self->config & kPreferIPv6) ? YES : NO; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (BOOL)isIPVersionNeutral { __block BOOL result = NO; dispatch_block_t block = ^{ result = (self->config & (kPreferIPv4 | kPreferIPv6)) == 0; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (void)setPreferIPv4 { dispatch_block_t block = ^{ LogTrace(); self->config |= kPreferIPv4; self->config &= ~kPreferIPv6; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (void)setPreferIPv6 { dispatch_block_t block = ^{ LogTrace(); self->config &= ~kPreferIPv4; self->config |= kPreferIPv6; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (void)setIPVersionNeutral { dispatch_block_t block = ^{ LogTrace(); self->config &= ~kPreferIPv4; self->config &= ~kPreferIPv6; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (uint16_t)maxReceiveIPv4BufferSize { __block uint16_t result = 0; dispatch_block_t block = ^{ result = self->max4ReceiveSize; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (void)setMaxReceiveIPv4BufferSize:(uint16_t)max { dispatch_block_t block = ^{ LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); self->max4ReceiveSize = max; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (uint32_t)maxReceiveIPv6BufferSize { __block uint32_t result = 0; dispatch_block_t block = ^{ result = self->max6ReceiveSize; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (void)setMaxReceiveIPv6BufferSize:(uint32_t)max { dispatch_block_t block = ^{ LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); self->max6ReceiveSize = max; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (void)setMaxSendBufferSize:(uint16_t)max { dispatch_block_t block = ^{ LogVerbose(@"%@ %u", THIS_METHOD, (unsigned)max); self->maxSendSize = max; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (uint16_t)maxSendBufferSize { __block uint16_t result = 0; dispatch_block_t block = ^{ result = self->maxSendSize; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (id)userData { __block id result = nil; dispatch_block_t block = ^{ result = self->userData; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (void)setUserData:(id)arbitraryUserData { dispatch_block_t block = ^{ if (self->userData != arbitraryUserData) { self->userData = arbitraryUserData; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Delegate Helpers //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)notifyDidConnectToAddress:(NSData *)anAddress { LogTrace(); __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didConnectToAddress:)]) { NSData *address = [anAddress copy]; // In case param is NSMutableData dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate udpSocket:self didConnectToAddress:address]; }}); } } - (void)notifyDidNotConnect:(NSError *)error { LogTrace(); __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotConnect:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate udpSocket:self didNotConnect:error]; }}); } } - (void)notifyDidSendDataWithTag:(long)tag { LogTrace(); __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didSendDataWithTag:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate udpSocket:self didSendDataWithTag:tag]; }}); } } - (void)notifyDidNotSendDataWithTag:(long)tag dueToError:(NSError *)error { LogTrace(); __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocket:didNotSendDataWithTag:dueToError:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate udpSocket:self didNotSendDataWithTag:tag dueToError:error]; }}); } } - (void)notifyDidReceiveData:(NSData *)data fromAddress:(NSData *)address withFilterContext:(id)context { LogTrace(); SEL selector = @selector(udpSocket:didReceiveData:fromAddress:withFilterContext:); __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:selector]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate udpSocket:self didReceiveData:data fromAddress:address withFilterContext:context]; }}); } } - (void)notifyDidCloseWithError:(NSError *)error { LogTrace(); __strong id theDelegate = delegate; if (delegateQueue && [theDelegate respondsToSelector:@selector(udpSocketDidClose:withError:)]) { dispatch_async(delegateQueue, ^{ @autoreleasepool { [theDelegate udpSocketDidClose:self withError:error]; }}); } } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Errors //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (NSError *)badConfigError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketBadConfigError userInfo:userInfo]; } - (NSError *)badParamError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketBadParamError userInfo:userInfo]; } - (NSError *)gaiError:(int)gai_error { NSString *errMsg = [NSString stringWithCString:gai_strerror(gai_error) encoding:NSASCIIStringEncoding]; NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:@"kCFStreamErrorDomainNetDB" code:gai_error userInfo:userInfo]; } - (NSError *)errnoErrorWithReason:(NSString *)reason { NSString *errMsg = [NSString stringWithUTF8String:strerror(errno)]; NSDictionary *userInfo; if (reason) userInfo = @{NSLocalizedDescriptionKey : errMsg, NSLocalizedFailureReasonErrorKey : reason}; else userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo:userInfo]; } - (NSError *)errnoError { return [self errnoErrorWithReason:nil]; } /** * Returns a standard send timeout error. **/ - (NSError *)sendTimeoutError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketSendTimeoutError", @"GCDAsyncUdpSocket", [NSBundle mainBundle], @"Send operation timed out", nil); NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketSendTimeoutError userInfo:userInfo]; } - (NSError *)socketClosedError { NSString *errMsg = NSLocalizedStringWithDefaultValue(@"GCDAsyncUdpSocketClosedError", @"GCDAsyncUdpSocket", [NSBundle mainBundle], @"Socket closed", nil); NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketClosedError userInfo:userInfo]; } - (NSError *)otherError:(NSString *)errMsg { NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg}; return [NSError errorWithDomain:GCDAsyncUdpSocketErrorDomain code:GCDAsyncUdpSocketOtherError userInfo:userInfo]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Utilities //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)preOp:(NSError **)errPtr { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (delegate == nil) // Must have delegate set { if (errPtr) { NSString *msg = @"Attempting to use socket without a delegate. Set a delegate first."; *errPtr = [self badConfigError:msg]; } return NO; } if (delegateQueue == NULL) // Must have delegate queue set { if (errPtr) { NSString *msg = @"Attempting to use socket without a delegate queue. Set a delegate queue first."; *errPtr = [self badConfigError:msg]; } return NO; } return YES; } /** * This method executes on a global concurrent queue. * When complete, it executes the given completion block on the socketQueue. **/ - (void)asyncResolveHost:(NSString *)aHost port:(uint16_t)port withCompletionBlock:(void (^)(NSArray *addresses, NSError *error))completionBlock { LogTrace(); // Check parameter(s) if (aHost == nil) { NSString *msg = @"The host param is nil. Should be domain name or IP address string."; NSError *error = [self badParamError:msg]; // We should still use dispatch_async since this method is expected to be asynchronous dispatch_async(socketQueue, ^{ @autoreleasepool { completionBlock(nil, error); }}); return; } // It's possible that the given aHost parameter is actually a NSMutableString. // So we want to copy it now, within this block that will be executed synchronously. // This way the asynchronous lookup block below doesn't have to worry about it changing. NSString *host = [aHost copy]; dispatch_queue_t globalConcurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(globalConcurrentQueue, ^{ @autoreleasepool { NSMutableArray *addresses = [NSMutableArray arrayWithCapacity:2]; NSError *error = nil; if ([host isEqualToString:@"localhost"] || [host isEqualToString:@"loopback"]) { // Use LOOPBACK address struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); sockaddr4.sin_len = sizeof(struct sockaddr_in); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); sockaddr6.sin6_len = sizeof(struct sockaddr_in6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_loopback; // Wrap the native address structures and add to list [addresses addObject:[NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]]; [addresses addObject:[NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]]; } else { NSString *portStr = [NSString stringWithFormat:@"%hu", port]; struct addrinfo hints, *res, *res0; memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_protocol = IPPROTO_UDP; int gai_error = getaddrinfo([host UTF8String], [portStr UTF8String], &hints, &res0); if (gai_error) { error = [self gaiError:gai_error]; } else { for(res = res0; res; res = res->ai_next) { if (res->ai_family == AF_INET) { // Found IPv4 address // Wrap the native address structure and add to list [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; } else if (res->ai_family == AF_INET6) { // Fixes connection issues with IPv6, it is the same solution for udp socket. // https://github.com/robbiehanson/CocoaAsyncSocket/issues/429#issuecomment-222477158 struct sockaddr_in6 *sockaddr = (struct sockaddr_in6 *)(void *)res->ai_addr; in_port_t *portPtr = &sockaddr->sin6_port; if ((portPtr != NULL) && (*portPtr == 0)) { *portPtr = htons(port); } // Found IPv6 address // Wrap the native address structure and add to list [addresses addObject:[NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]]; } } freeaddrinfo(res0); if ([addresses count] == 0) { error = [self gaiError:EAI_FAIL]; } } } dispatch_async(self->socketQueue, ^{ @autoreleasepool { completionBlock(addresses, error); }}); }}); } /** * This method picks an address from the given list of addresses. * The address picked depends upon which protocols are disabled, deactived, & preferred. * * Returns the address family (AF_INET or AF_INET6) of the picked address, * or AF_UNSPEC and the corresponding error is there's a problem. **/ - (int)getAddress:(NSData **)addressPtr error:(NSError **)errorPtr fromAddresses:(NSArray *)addresses { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert([addresses count] > 0, @"Expected at least one address"); int resultAF = AF_UNSPEC; NSData *resultAddress = nil; NSError *resultError = nil; // Check for problems BOOL resolvedIPv4Address = NO; BOOL resolvedIPv6Address = NO; for (NSData *address in addresses) { switch ([[self class] familyFromAddress:address]) { case AF_INET : resolvedIPv4Address = YES; break; case AF_INET6 : resolvedIPv6Address = YES; break; default : NSAssert(NO, @"Addresses array contains invalid address"); } } BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && !resolvedIPv6Address) { NSString *msg = @"IPv4 has been disabled and DNS lookup found no IPv6 address(es)."; resultError = [self otherError:msg]; if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; return resultAF; } if (isIPv6Disabled && !resolvedIPv4Address) { NSString *msg = @"IPv6 has been disabled and DNS lookup found no IPv4 address(es)."; resultError = [self otherError:msg]; if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; return resultAF; } BOOL isIPv4Deactivated = (flags & kIPv4Deactivated) ? YES : NO; BOOL isIPv6Deactivated = (flags & kIPv6Deactivated) ? YES : NO; if (isIPv4Deactivated && !resolvedIPv6Address) { NSString *msg = @"IPv4 has been deactivated due to bind/connect, and DNS lookup found no IPv6 address(es)."; resultError = [self otherError:msg]; if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; return resultAF; } if (isIPv6Deactivated && !resolvedIPv4Address) { NSString *msg = @"IPv6 has been deactivated due to bind/connect, and DNS lookup found no IPv4 address(es)."; resultError = [self otherError:msg]; if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; return resultAF; } // Extract first IPv4 and IPv6 address in list BOOL ipv4WasFirstInList = YES; NSData *address4 = nil; NSData *address6 = nil; for (NSData *address in addresses) { int af = [[self class] familyFromAddress:address]; if (af == AF_INET) { if (address4 == nil) { address4 = address; if (address6) break; else ipv4WasFirstInList = YES; } } else // af == AF_INET6 { if (address6 == nil) { address6 = address; if (address4) break; else ipv4WasFirstInList = NO; } } } // Determine socket type BOOL preferIPv4 = (config & kPreferIPv4) ? YES : NO; BOOL preferIPv6 = (config & kPreferIPv6) ? YES : NO; BOOL useIPv4 = ((preferIPv4 && address4) || (address6 == nil)); BOOL useIPv6 = ((preferIPv6 && address6) || (address4 == nil)); NSAssert(!(preferIPv4 && preferIPv6), @"Invalid config state"); NSAssert(!(useIPv4 && useIPv6), @"Invalid logic"); if (useIPv4 || (!useIPv6 && ipv4WasFirstInList)) { resultAF = AF_INET; resultAddress = address4; } else { resultAF = AF_INET6; resultAddress = address6; } if (addressPtr) *addressPtr = resultAddress; if (errorPtr) *errorPtr = resultError; return resultAF; } /** * Finds the address(es) of an interface description. * An inteface description may be an interface name (en0, en1, lo0) or corresponding IP (192.168.4.34). **/ - (void)convertIntefaceDescription:(NSString *)interfaceDescription port:(uint16_t)port intoAddress4:(NSData **)interfaceAddr4Ptr address6:(NSData **)interfaceAddr6Ptr { NSData *addr4 = nil; NSData *addr6 = nil; if (interfaceDescription == nil) { // ANY address struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); sockaddr4.sin_len = sizeof(sockaddr4); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_ANY); struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); sockaddr6.sin6_len = sizeof(sockaddr6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_any; addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } else if ([interfaceDescription isEqualToString:@"localhost"] || [interfaceDescription isEqualToString:@"loopback"]) { // LOOPBACK address struct sockaddr_in sockaddr4; memset(&sockaddr4, 0, sizeof(sockaddr4)); sockaddr4.sin_len = sizeof(struct sockaddr_in); sockaddr4.sin_family = AF_INET; sockaddr4.sin_port = htons(port); sockaddr4.sin_addr.s_addr = htonl(INADDR_LOOPBACK); struct sockaddr_in6 sockaddr6; memset(&sockaddr6, 0, sizeof(sockaddr6)); sockaddr6.sin6_len = sizeof(struct sockaddr_in6); sockaddr6.sin6_family = AF_INET6; sockaddr6.sin6_port = htons(port); sockaddr6.sin6_addr = in6addr_loopback; addr4 = [NSData dataWithBytes:&sockaddr4 length:sizeof(sockaddr4)]; addr6 = [NSData dataWithBytes:&sockaddr6 length:sizeof(sockaddr6)]; } else { const char *iface = [interfaceDescription UTF8String]; struct ifaddrs *addrs; const struct ifaddrs *cursor; if ((getifaddrs(&addrs) == 0)) { cursor = addrs; while (cursor != NULL) { if ((addr4 == nil) && (cursor->ifa_addr->sa_family == AF_INET)) { // IPv4 struct sockaddr_in *addr = (struct sockaddr_in *)(void *)cursor->ifa_addr; if (strcmp(cursor->ifa_name, iface) == 0) { // Name match struct sockaddr_in nativeAddr4 = *addr; nativeAddr4.sin_port = htons(port); addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } else { char ip[INET_ADDRSTRLEN]; const char *conversion; conversion = inet_ntop(AF_INET, &addr->sin_addr, ip, sizeof(ip)); if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match struct sockaddr_in nativeAddr4 = *addr; nativeAddr4.sin_port = htons(port); addr4 = [NSData dataWithBytes:&nativeAddr4 length:sizeof(nativeAddr4)]; } } } else if ((addr6 == nil) && (cursor->ifa_addr->sa_family == AF_INET6)) { // IPv6 const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; if (strcmp(cursor->ifa_name, iface) == 0) { // Name match struct sockaddr_in6 nativeAddr6 = *addr; nativeAddr6.sin6_port = htons(port); addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } else { char ip[INET6_ADDRSTRLEN]; const char *conversion; conversion = inet_ntop(AF_INET6, &addr->sin6_addr, ip, sizeof(ip)); if ((conversion != NULL) && (strcmp(ip, iface) == 0)) { // IP match struct sockaddr_in6 nativeAddr6 = *addr; nativeAddr6.sin6_port = htons(port); addr6 = [NSData dataWithBytes:&nativeAddr6 length:sizeof(nativeAddr6)]; } } } cursor = cursor->ifa_next; } freeifaddrs(addrs); } } if (interfaceAddr4Ptr) *interfaceAddr4Ptr = addr4; if (interfaceAddr6Ptr) *interfaceAddr6Ptr = addr6; } /** * Converts a numeric hostname into its corresponding address. * The hostname is expected to be an IPv4 or IPv6 address represented as a human-readable string. (e.g. 192.168.4.34) **/ - (void)convertNumericHost:(NSString *)numericHost port:(uint16_t)port intoAddress4:(NSData **)addr4Ptr address6:(NSData **)addr6Ptr { NSData *addr4 = nil; NSData *addr6 = nil; if (numericHost) { NSString *portStr = [NSString stringWithFormat:@"%hu", port]; struct addrinfo hints, *res, *res0; memset(&hints, 0, sizeof(hints)); hints.ai_family = PF_UNSPEC; hints.ai_socktype = SOCK_DGRAM; hints.ai_protocol = IPPROTO_UDP; hints.ai_flags = AI_NUMERICHOST; // No name resolution should be attempted if (getaddrinfo([numericHost UTF8String], [portStr UTF8String], &hints, &res0) == 0) { for (res = res0; res; res = res->ai_next) { if ((addr4 == nil) && (res->ai_family == AF_INET)) { // Found IPv4 address // Wrap the native address structure addr4 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; } else if ((addr6 == nil) && (res->ai_family == AF_INET6)) { // Found IPv6 address // Wrap the native address structure addr6 = [NSData dataWithBytes:res->ai_addr length:res->ai_addrlen]; } } freeaddrinfo(res0); } } if (addr4Ptr) *addr4Ptr = addr4; if (addr6Ptr) *addr6Ptr = addr6; } - (BOOL)isConnectedToAddress4:(NSData *)someAddr4 { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(flags & kDidConnect, @"Not connected"); NSAssert(cachedConnectedAddress, @"Expected cached connected address"); if (cachedConnectedFamily != AF_INET) { return NO; } const struct sockaddr_in *sSockaddr4 = (const struct sockaddr_in *)[someAddr4 bytes]; const struct sockaddr_in *cSockaddr4 = (const struct sockaddr_in *)[cachedConnectedAddress bytes]; if (memcmp(&sSockaddr4->sin_addr, &cSockaddr4->sin_addr, sizeof(struct in_addr)) != 0) { return NO; } if (memcmp(&sSockaddr4->sin_port, &cSockaddr4->sin_port, sizeof(in_port_t)) != 0) { return NO; } return YES; } - (BOOL)isConnectedToAddress6:(NSData *)someAddr6 { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(flags & kDidConnect, @"Not connected"); NSAssert(cachedConnectedAddress, @"Expected cached connected address"); if (cachedConnectedFamily != AF_INET6) { return NO; } const struct sockaddr_in6 *sSockaddr6 = (const struct sockaddr_in6 *)[someAddr6 bytes]; const struct sockaddr_in6 *cSockaddr6 = (const struct sockaddr_in6 *)[cachedConnectedAddress bytes]; if (memcmp(&sSockaddr6->sin6_addr, &cSockaddr6->sin6_addr, sizeof(struct in6_addr)) != 0) { return NO; } if (memcmp(&sSockaddr6->sin6_port, &cSockaddr6->sin6_port, sizeof(in_port_t)) != 0) { return NO; } return YES; } - (unsigned int)indexOfInterfaceAddr4:(NSData *)interfaceAddr4 { if (interfaceAddr4 == nil) return 0; if ([interfaceAddr4 length] != sizeof(struct sockaddr_in)) return 0; int result = 0; const struct sockaddr_in *ifaceAddr = (const struct sockaddr_in *)[interfaceAddr4 bytes]; struct ifaddrs *addrs; const struct ifaddrs *cursor; if ((getifaddrs(&addrs) == 0)) { cursor = addrs; while (cursor != NULL) { if (cursor->ifa_addr->sa_family == AF_INET) { // IPv4 const struct sockaddr_in *addr = (const struct sockaddr_in *)(const void *)cursor->ifa_addr; if (memcmp(&addr->sin_addr, &ifaceAddr->sin_addr, sizeof(struct in_addr)) == 0) { result = if_nametoindex(cursor->ifa_name); break; } } cursor = cursor->ifa_next; } freeifaddrs(addrs); } return result; } - (unsigned int)indexOfInterfaceAddr6:(NSData *)interfaceAddr6 { if (interfaceAddr6 == nil) return 0; if ([interfaceAddr6 length] != sizeof(struct sockaddr_in6)) return 0; int result = 0; const struct sockaddr_in6 *ifaceAddr = (const struct sockaddr_in6 *)[interfaceAddr6 bytes]; struct ifaddrs *addrs; const struct ifaddrs *cursor; if ((getifaddrs(&addrs) == 0)) { cursor = addrs; while (cursor != NULL) { if (cursor->ifa_addr->sa_family == AF_INET6) { // IPv6 const struct sockaddr_in6 *addr = (const struct sockaddr_in6 *)(const void *)cursor->ifa_addr; if (memcmp(&addr->sin6_addr, &ifaceAddr->sin6_addr, sizeof(struct in6_addr)) == 0) { result = if_nametoindex(cursor->ifa_name); break; } } cursor = cursor->ifa_next; } freeifaddrs(addrs); } return result; } - (void)setupSendAndReceiveSourcesForSocket4 { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); send4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket4FD, 0, socketQueue); receive4Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket4FD, 0, socketQueue); // Setup event handlers dispatch_source_set_event_handler(send4Source, ^{ @autoreleasepool { LogVerbose(@"send4EventBlock"); LogVerbose(@"dispatch_source_get_data(send4Source) = %lu", dispatch_source_get_data(send4Source)); self->flags |= kSock4CanAcceptBytes; // If we're ready to send data, do so immediately. // Otherwise pause the send source or it will continue to fire over and over again. if (self->currentSend == nil) { LogVerbose(@"Nothing to send"); [self suspendSend4Source]; } else if (self->currentSend->resolveInProgress) { LogVerbose(@"currentSend - waiting for address resolve"); [self suspendSend4Source]; } else if (self->currentSend->filterInProgress) { LogVerbose(@"currentSend - waiting on sendFilter"); [self suspendSend4Source]; } else { [self doSend]; } }}); dispatch_source_set_event_handler(receive4Source, ^{ @autoreleasepool { LogVerbose(@"receive4EventBlock"); self->socket4FDBytesAvailable = dispatch_source_get_data(self->receive4Source); LogVerbose(@"socket4FDBytesAvailable: %lu", socket4FDBytesAvailable); if (self->socket4FDBytesAvailable > 0) [self doReceive]; else [self doReceiveEOF]; }}); // Setup cancel handlers __block int socketFDRefCount = 2; int theSocketFD = socket4FD; #if !OS_OBJECT_USE_OBJC dispatch_source_t theSendSource = send4Source; dispatch_source_t theReceiveSource = receive4Source; #endif dispatch_source_set_cancel_handler(send4Source, ^{ LogVerbose(@"send4CancelBlock"); #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(send4Source)"); dispatch_release(theSendSource); #endif if (--socketFDRefCount == 0) { LogVerbose(@"close(socket4FD)"); close(theSocketFD); } }); dispatch_source_set_cancel_handler(receive4Source, ^{ LogVerbose(@"receive4CancelBlock"); #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(receive4Source)"); dispatch_release(theReceiveSource); #endif if (--socketFDRefCount == 0) { LogVerbose(@"close(socket4FD)"); close(theSocketFD); } }); // We will not be able to receive until the socket is bound to a port, // either explicitly via bind, or implicitly by connect or by sending data. // // But we should be able to send immediately. socket4FDBytesAvailable = 0; flags |= kSock4CanAcceptBytes; flags |= kSend4SourceSuspended; flags |= kReceive4SourceSuspended; } - (void)setupSendAndReceiveSourcesForSocket6 { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); send6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_WRITE, socket6FD, 0, socketQueue); receive6Source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, socket6FD, 0, socketQueue); // Setup event handlers dispatch_source_set_event_handler(send6Source, ^{ @autoreleasepool { LogVerbose(@"send6EventBlock"); LogVerbose(@"dispatch_source_get_data(send6Source) = %lu", dispatch_source_get_data(send6Source)); self->flags |= kSock6CanAcceptBytes; // If we're ready to send data, do so immediately. // Otherwise pause the send source or it will continue to fire over and over again. if (self->currentSend == nil) { LogVerbose(@"Nothing to send"); [self suspendSend6Source]; } else if (self->currentSend->resolveInProgress) { LogVerbose(@"currentSend - waiting for address resolve"); [self suspendSend6Source]; } else if (self->currentSend->filterInProgress) { LogVerbose(@"currentSend - waiting on sendFilter"); [self suspendSend6Source]; } else { [self doSend]; } }}); dispatch_source_set_event_handler(receive6Source, ^{ @autoreleasepool { LogVerbose(@"receive6EventBlock"); self->socket6FDBytesAvailable = dispatch_source_get_data(self->receive6Source); LogVerbose(@"socket6FDBytesAvailable: %lu", socket6FDBytesAvailable); if (self->socket6FDBytesAvailable > 0) [self doReceive]; else [self doReceiveEOF]; }}); // Setup cancel handlers __block int socketFDRefCount = 2; int theSocketFD = socket6FD; #if !OS_OBJECT_USE_OBJC dispatch_source_t theSendSource = send6Source; dispatch_source_t theReceiveSource = receive6Source; #endif dispatch_source_set_cancel_handler(send6Source, ^{ LogVerbose(@"send6CancelBlock"); #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(send6Source)"); dispatch_release(theSendSource); #endif if (--socketFDRefCount == 0) { LogVerbose(@"close(socket6FD)"); close(theSocketFD); } }); dispatch_source_set_cancel_handler(receive6Source, ^{ LogVerbose(@"receive6CancelBlock"); #if !OS_OBJECT_USE_OBJC LogVerbose(@"dispatch_release(receive6Source)"); dispatch_release(theReceiveSource); #endif if (--socketFDRefCount == 0) { LogVerbose(@"close(socket6FD)"); close(theSocketFD); } }); // We will not be able to receive until the socket is bound to a port, // either explicitly via bind, or implicitly by connect or by sending data. // // But we should be able to send immediately. socket6FDBytesAvailable = 0; flags |= kSock6CanAcceptBytes; flags |= kSend6SourceSuspended; flags |= kReceive6SourceSuspended; } - (BOOL)createSocket4:(BOOL)useIPv4 socket6:(BOOL)useIPv6 error:(NSError * __autoreleasing *)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(((flags & kDidCreateSockets) == 0), @"Sockets have already been created"); // CreateSocket Block // This block will be invoked below. int(^createSocket)(int) = ^int (int domain) { int socketFD = socket(domain, SOCK_DGRAM, 0); if (socketFD == SOCKET_NULL) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in socket() function"]; return SOCKET_NULL; } int status; // Set socket options status = fcntl(socketFD, F_SETFL, O_NONBLOCK); if (status == -1) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error enabling non-blocking IO on socket (fcntl)"]; close(socketFD); return SOCKET_NULL; } int reuseaddr = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_REUSEADDR, &reuseaddr, sizeof(reuseaddr)); if (status == -1) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error enabling address reuse (setsockopt)"]; close(socketFD); return SOCKET_NULL; } int nosigpipe = 1; status = setsockopt(socketFD, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); if (status == -1) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error disabling sigpipe (setsockopt)"]; close(socketFD); return SOCKET_NULL; } /** * The theoretical maximum size of any IPv4 UDP packet is UINT16_MAX = 65535. * The theoretical maximum size of any IPv6 UDP packet is UINT32_MAX = 4294967295. * * The default maximum size of the UDP buffer in iOS is 9216 bytes. * * This is the reason of #222(GCD does not necessarily return the size of an entire UDP packet) and * #535(GCDAsyncUDPSocket can not send data when data is greater than 9K) * * * Enlarge the maximum size of UDP packet. * I can not ensure the protocol type now so that the max size is set to 65535 :) **/ status = setsockopt(socketFD, SOL_SOCKET, SO_SNDBUF, (const char*)&self->maxSendSize, sizeof(int)); if (status == -1) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error setting send buffer size (setsockopt)"]; close(socketFD); return SOCKET_NULL; } status = setsockopt(socketFD, SOL_SOCKET, SO_RCVBUF, (const char*)&self->maxSendSize, sizeof(int)); if (status == -1) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error setting receive buffer size (setsockopt)"]; close(socketFD); return SOCKET_NULL; } return socketFD; }; // Create sockets depending upon given configuration. if (useIPv4) { LogVerbose(@"Creating IPv4 socket"); socket4FD = createSocket(AF_INET); if (socket4FD == SOCKET_NULL) { // errPtr set in local createSocket() block return NO; } } if (useIPv6) { LogVerbose(@"Creating IPv6 socket"); socket6FD = createSocket(AF_INET6); if (socket6FD == SOCKET_NULL) { // errPtr set in local createSocket() block if (socket4FD != SOCKET_NULL) { close(socket4FD); socket4FD = SOCKET_NULL; } return NO; } } // Setup send and receive sources if (useIPv4) [self setupSendAndReceiveSourcesForSocket4]; if (useIPv6) [self setupSendAndReceiveSourcesForSocket6]; flags |= kDidCreateSockets; return YES; } - (BOOL)createSockets:(NSError **)errPtr { LogTrace(); BOOL useIPv4 = [self isIPv4Enabled]; BOOL useIPv6 = [self isIPv6Enabled]; return [self createSocket4:useIPv4 socket6:useIPv6 error:errPtr]; } - (void)suspendSend4Source { if (send4Source && !(flags & kSend4SourceSuspended)) { LogVerbose(@"dispatch_suspend(send4Source)"); dispatch_suspend(send4Source); flags |= kSend4SourceSuspended; } } - (void)suspendSend6Source { if (send6Source && !(flags & kSend6SourceSuspended)) { LogVerbose(@"dispatch_suspend(send6Source)"); dispatch_suspend(send6Source); flags |= kSend6SourceSuspended; } } - (void)resumeSend4Source { if (send4Source && (flags & kSend4SourceSuspended)) { LogVerbose(@"dispatch_resume(send4Source)"); dispatch_resume(send4Source); flags &= ~kSend4SourceSuspended; } } - (void)resumeSend6Source { if (send6Source && (flags & kSend6SourceSuspended)) { LogVerbose(@"dispatch_resume(send6Source)"); dispatch_resume(send6Source); flags &= ~kSend6SourceSuspended; } } - (void)suspendReceive4Source { if (receive4Source && !(flags & kReceive4SourceSuspended)) { LogVerbose(@"dispatch_suspend(receive4Source)"); dispatch_suspend(receive4Source); flags |= kReceive4SourceSuspended; } } - (void)suspendReceive6Source { if (receive6Source && !(flags & kReceive6SourceSuspended)) { LogVerbose(@"dispatch_suspend(receive6Source)"); dispatch_suspend(receive6Source); flags |= kReceive6SourceSuspended; } } - (void)resumeReceive4Source { if (receive4Source && (flags & kReceive4SourceSuspended)) { LogVerbose(@"dispatch_resume(receive4Source)"); dispatch_resume(receive4Source); flags &= ~kReceive4SourceSuspended; } } - (void)resumeReceive6Source { if (receive6Source && (flags & kReceive6SourceSuspended)) { LogVerbose(@"dispatch_resume(receive6Source)"); dispatch_resume(receive6Source); flags &= ~kReceive6SourceSuspended; } } - (void)closeSocket4 { if (socket4FD != SOCKET_NULL) { LogVerbose(@"dispatch_source_cancel(send4Source)"); dispatch_source_cancel(send4Source); LogVerbose(@"dispatch_source_cancel(receive4Source)"); dispatch_source_cancel(receive4Source); // For some crazy reason (in my opinion), cancelling a dispatch source doesn't // invoke the cancel handler if the dispatch source is paused. // So we have to unpause the source if needed. // This allows the cancel handler to be run, which in turn releases the source and closes the socket. [self resumeSend4Source]; [self resumeReceive4Source]; // The sockets will be closed by the cancel handlers of the corresponding source send4Source = NULL; receive4Source = NULL; socket4FD = SOCKET_NULL; // Clear socket states socket4FDBytesAvailable = 0; flags &= ~kSock4CanAcceptBytes; // Clear cached info cachedLocalAddress4 = nil; cachedLocalHost4 = nil; cachedLocalPort4 = 0; } } - (void)closeSocket6 { if (socket6FD != SOCKET_NULL) { LogVerbose(@"dispatch_source_cancel(send6Source)"); dispatch_source_cancel(send6Source); LogVerbose(@"dispatch_source_cancel(receive6Source)"); dispatch_source_cancel(receive6Source); // For some crazy reason (in my opinion), cancelling a dispatch source doesn't // invoke the cancel handler if the dispatch source is paused. // So we have to unpause the source if needed. // This allows the cancel handler to be run, which in turn releases the source and closes the socket. [self resumeSend6Source]; [self resumeReceive6Source]; send6Source = NULL; receive6Source = NULL; // The sockets will be closed by the cancel handlers of the corresponding source socket6FD = SOCKET_NULL; // Clear socket states socket6FDBytesAvailable = 0; flags &= ~kSock6CanAcceptBytes; // Clear cached info cachedLocalAddress6 = nil; cachedLocalHost6 = nil; cachedLocalPort6 = 0; } } - (void)closeSockets { [self closeSocket4]; [self closeSocket6]; flags &= ~kDidCreateSockets; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Diagnostics //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)getLocalAddress:(NSData **)dataPtr host:(NSString **)hostPtr port:(uint16_t *)portPtr forSocket:(int)socketFD withFamily:(int)socketFamily { NSData *data = nil; NSString *host = nil; uint16_t port = 0; if (socketFamily == AF_INET) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getsockname(socketFD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; host = [[self class] hostFromSockaddr4:&sockaddr4]; port = [[self class] portFromSockaddr4:&sockaddr4]; } else { LogWarn(@"Error in getsockname: %@", [self errnoError]); } } else if (socketFamily == AF_INET6) { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getsockname(socketFD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; host = [[self class] hostFromSockaddr6:&sockaddr6]; port = [[self class] portFromSockaddr6:&sockaddr6]; } else { LogWarn(@"Error in getsockname: %@", [self errnoError]); } } if (dataPtr) *dataPtr = data; if (hostPtr) *hostPtr = host; if (portPtr) *portPtr = port; return (data != nil); } - (void)maybeUpdateCachedLocalAddress4Info { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if ( cachedLocalAddress4 || ((flags & kDidBind) == 0) || (socket4FD == SOCKET_NULL) ) { return; } NSData *address = nil; NSString *host = nil; uint16_t port = 0; if ([self getLocalAddress:&address host:&host port:&port forSocket:socket4FD withFamily:AF_INET]) { cachedLocalAddress4 = address; cachedLocalHost4 = host; cachedLocalPort4 = port; } } - (void)maybeUpdateCachedLocalAddress6Info { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if ( cachedLocalAddress6 || ((flags & kDidBind) == 0) || (socket6FD == SOCKET_NULL) ) { return; } NSData *address = nil; NSString *host = nil; uint16_t port = 0; if ([self getLocalAddress:&address host:&host port:&port forSocket:socket6FD withFamily:AF_INET6]) { cachedLocalAddress6 = address; cachedLocalHost6 = host; cachedLocalPort6 = port; } } - (NSData *)localAddress { __block NSData *result = nil; dispatch_block_t block = ^{ if (self->socket4FD != SOCKET_NULL) { [self maybeUpdateCachedLocalAddress4Info]; result = self->cachedLocalAddress4; } else { [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalAddress6; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (NSString *)localHost { __block NSString *result = nil; dispatch_block_t block = ^{ if (self->socket4FD != SOCKET_NULL) { [self maybeUpdateCachedLocalAddress4Info]; result = self->cachedLocalHost4; } else { [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalHost6; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (uint16_t)localPort { __block uint16_t result = 0; dispatch_block_t block = ^{ if (self->socket4FD != SOCKET_NULL) { [self maybeUpdateCachedLocalAddress4Info]; result = self->cachedLocalPort4; } else { [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalPort6; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (NSData *)localAddress_IPv4 { __block NSData *result = nil; dispatch_block_t block = ^{ [self maybeUpdateCachedLocalAddress4Info]; result = self->cachedLocalAddress4; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (NSString *)localHost_IPv4 { __block NSString *result = nil; dispatch_block_t block = ^{ [self maybeUpdateCachedLocalAddress4Info]; result = self->cachedLocalHost4; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (uint16_t)localPort_IPv4 { __block uint16_t result = 0; dispatch_block_t block = ^{ [self maybeUpdateCachedLocalAddress4Info]; result = self->cachedLocalPort4; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (NSData *)localAddress_IPv6 { __block NSData *result = nil; dispatch_block_t block = ^{ [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalAddress6; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (NSString *)localHost_IPv6 { __block NSString *result = nil; dispatch_block_t block = ^{ [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalHost6; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (uint16_t)localPort_IPv6 { __block uint16_t result = 0; dispatch_block_t block = ^{ [self maybeUpdateCachedLocalAddress6Info]; result = self->cachedLocalPort6; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (void)maybeUpdateCachedConnectedAddressInfo { NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (cachedConnectedAddress || (flags & kDidConnect) == 0) { return; } NSData *data = nil; NSString *host = nil; uint16_t port = 0; int family = AF_UNSPEC; if (socket4FD != SOCKET_NULL) { struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); if (getpeername(socket4FD, (struct sockaddr *)&sockaddr4, &sockaddr4len) == 0) { data = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; host = [[self class] hostFromSockaddr4:&sockaddr4]; port = [[self class] portFromSockaddr4:&sockaddr4]; family = AF_INET; } else { LogWarn(@"Error in getpeername: %@", [self errnoError]); } } else if (socket6FD != SOCKET_NULL) { struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); if (getpeername(socket6FD, (struct sockaddr *)&sockaddr6, &sockaddr6len) == 0) { data = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; host = [[self class] hostFromSockaddr6:&sockaddr6]; port = [[self class] portFromSockaddr6:&sockaddr6]; family = AF_INET6; } else { LogWarn(@"Error in getpeername: %@", [self errnoError]); } } cachedConnectedAddress = data; cachedConnectedHost = host; cachedConnectedPort = port; cachedConnectedFamily = family; } - (NSData *)connectedAddress { __block NSData *result = nil; dispatch_block_t block = ^{ [self maybeUpdateCachedConnectedAddressInfo]; result = self->cachedConnectedAddress; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (NSString *)connectedHost { __block NSString *result = nil; dispatch_block_t block = ^{ [self maybeUpdateCachedConnectedAddressInfo]; result = self->cachedConnectedHost; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (uint16_t)connectedPort { __block uint16_t result = 0; dispatch_block_t block = ^{ [self maybeUpdateCachedConnectedAddressInfo]; result = self->cachedConnectedPort; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, AutoreleasedBlock(block)); return result; } - (BOOL)isConnected { __block BOOL result = NO; dispatch_block_t block = ^{ result = (self->flags & kDidConnect) ? YES : NO; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (BOOL)isClosed { __block BOOL result = YES; dispatch_block_t block = ^{ result = (self->flags & kDidCreateSockets) ? NO : YES; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (BOOL)isIPv4 { __block BOOL result = NO; dispatch_block_t block = ^{ if (self->flags & kDidCreateSockets) { result = (self->socket4FD != SOCKET_NULL); } else { result = [self isIPv4Enabled]; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } - (BOOL)isIPv6 { __block BOOL result = NO; dispatch_block_t block = ^{ if (self->flags & kDidCreateSockets) { result = (self->socket6FD != SOCKET_NULL); } else { result = [self isIPv6Enabled]; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Binding //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * This method runs through the various checks required prior to a bind attempt. * It is shared between the various bind methods. **/ - (BOOL)preBind:(NSError **)errPtr { if (![self preOp:errPtr]) { return NO; } if (flags & kDidBind) { if (errPtr) { NSString *msg = @"Cannot bind a socket more than once."; *errPtr = [self badConfigError:msg]; } return NO; } if ((flags & kConnecting) || (flags & kDidConnect)) { if (errPtr) { NSString *msg = @"Cannot bind after connecting. If needed, bind first, then connect."; *errPtr = [self badConfigError:msg]; } return NO; } BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { if (errPtr) { NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; *errPtr = [self badConfigError:msg]; } return NO; } return YES; } - (BOOL)bindToPort:(uint16_t)port error:(NSError **)errPtr { return [self bindToPort:port interface:nil error:errPtr]; } - (BOOL)bindToPort:(uint16_t)port interface:(NSString *)interface error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { // Run through sanity checks if (![self preBind:&err]) { return_from_block; } // Check the given interface NSData *interface4 = nil; NSData *interface6 = nil; [self convertIntefaceDescription:interface port:port intoAddress4:&interface4 address6:&interface6]; if ((interface4 == nil) && (interface6 == nil)) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; err = [self badParamError:msg]; return_from_block; } BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && (interface6 == nil)) { NSString *msg = @"IPv4 has been disabled and specified interface doesn't support IPv6."; err = [self badParamError:msg]; return_from_block; } if (isIPv6Disabled && (interface4 == nil)) { NSString *msg = @"IPv6 has been disabled and specified interface doesn't support IPv4."; err = [self badParamError:msg]; return_from_block; } // Determine protocol(s) BOOL useIPv4 = !isIPv4Disabled && (interface4 != nil); BOOL useIPv6 = !isIPv6Disabled && (interface6 != nil); // Create the socket(s) if needed if ((self->flags & kDidCreateSockets) == 0) { if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) { return_from_block; } } // Bind the socket(s) LogVerbose(@"Binding socket to port(%hu) interface(%@)", port, interface); if (useIPv4) { int status = bind(self->socket4FD, (const struct sockaddr *)[interface4 bytes], (socklen_t)[interface4 length]); if (status == -1) { [self closeSockets]; NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; return_from_block; } } if (useIPv6) { int status = bind(self->socket6FD, (const struct sockaddr *)[interface6 bytes], (socklen_t)[interface6 length]); if (status == -1) { [self closeSockets]; NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; return_from_block; } } // Update flags self->flags |= kDidBind; if (!useIPv4) self->flags |= kIPv4Deactivated; if (!useIPv6) self->flags |= kIPv6Deactivated; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (err) LogError(@"Error binding to port/interface: %@", err); if (errPtr) *errPtr = err; return result; } - (BOOL)bindToAddress:(NSData *)localAddr error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { // Run through sanity checks if (![self preBind:&err]) { return_from_block; } // Check the given address int addressFamily = [[self class] familyFromAddress:localAddr]; if (addressFamily == AF_UNSPEC) { NSString *msg = @"A valid IPv4 or IPv6 address was not given"; err = [self badParamError:msg]; return_from_block; } NSData *localAddr4 = (addressFamily == AF_INET) ? localAddr : nil; NSData *localAddr6 = (addressFamily == AF_INET6) ? localAddr : nil; BOOL isIPv4Disabled = (self->config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (self->config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && localAddr4) { NSString *msg = @"IPv4 has been disabled and an IPv4 address was passed."; err = [self badParamError:msg]; return_from_block; } if (isIPv6Disabled && localAddr6) { NSString *msg = @"IPv6 has been disabled and an IPv6 address was passed."; err = [self badParamError:msg]; return_from_block; } // Determine protocol(s) BOOL useIPv4 = !isIPv4Disabled && (localAddr4 != nil); BOOL useIPv6 = !isIPv6Disabled && (localAddr6 != nil); // Create the socket(s) if needed if ((self->flags & kDidCreateSockets) == 0) { if (![self createSocket4:useIPv4 socket6:useIPv6 error:&err]) { return_from_block; } } // Bind the socket(s) if (useIPv4) { LogVerbose(@"Binding socket to address(%@:%hu)", [[self class] hostFromAddress:localAddr4], [[self class] portFromAddress:localAddr4]); int status = bind(self->socket4FD, (const struct sockaddr *)[localAddr4 bytes], (socklen_t)[localAddr4 length]); if (status == -1) { [self closeSockets]; NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; return_from_block; } } else { LogVerbose(@"Binding socket to address(%@:%hu)", [[self class] hostFromAddress:localAddr6], [[self class] portFromAddress:localAddr6]); int status = bind(self->socket6FD, (const struct sockaddr *)[localAddr6 bytes], (socklen_t)[localAddr6 length]); if (status == -1) { [self closeSockets]; NSString *reason = @"Error in bind() function"; err = [self errnoErrorWithReason:reason]; return_from_block; } } // Update flags self->flags |= kDidBind; if (!useIPv4) self->flags |= kIPv4Deactivated; if (!useIPv6) self->flags |= kIPv6Deactivated; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (err) LogError(@"Error binding to address: %@", err); if (errPtr) *errPtr = err; return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Connecting //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * This method runs through the various checks required prior to a connect attempt. * It is shared between the various connect methods. **/ - (BOOL)preConnect:(NSError **)errPtr { if (![self preOp:errPtr]) { return NO; } if ((flags & kConnecting) || (flags & kDidConnect)) { if (errPtr) { NSString *msg = @"Cannot connect a socket more than once."; *errPtr = [self badConfigError:msg]; } return NO; } BOOL isIPv4Disabled = (config & kIPv4Disabled) ? YES : NO; BOOL isIPv6Disabled = (config & kIPv6Disabled) ? YES : NO; if (isIPv4Disabled && isIPv6Disabled) // Must have IPv4 or IPv6 enabled { if (errPtr) { NSString *msg = @"Both IPv4 and IPv6 have been disabled. Must enable at least one protocol first."; *errPtr = [self badConfigError:msg]; } return NO; } return YES; } - (BOOL)connectToHost:(NSString *)host onPort:(uint16_t)port error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { // Run through sanity checks. if (![self preConnect:&err]) { return_from_block; } // Check parameter(s) if (host == nil) { NSString *msg = @"The host param is nil. Should be domain name or IP address string."; err = [self badParamError:msg]; return_from_block; } // Create the socket(s) if needed if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) { return_from_block; } } // Create special connect packet GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; packet->resolveInProgress = YES; // Start asynchronous DNS resolve for host:port on background queue LogVerbose(@"Dispatching DNS resolve for connect..."); [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, // and immediately returns. Once the async resolve task completes, // this block is executed on our socketQueue. packet->resolveInProgress = NO; packet->addresses = addresses; packet->error = error; [self maybeConnect]; }]; // Updates flags, add connect packet to send queue, and pump send queue self->flags |= kConnecting; [self->sendQueue addObject:packet]; [self maybeDequeueSend]; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (err) LogError(@"Error connecting to host/port: %@", err); if (errPtr) *errPtr = err; return result; } - (BOOL)connectToAddress:(NSData *)remoteAddr error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { // Run through sanity checks. if (![self preConnect:&err]) { return_from_block; } // Check parameter(s) if (remoteAddr == nil) { NSString *msg = @"The address param is nil. Should be a valid address."; err = [self badParamError:msg]; return_from_block; } // Create the socket(s) if needed if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) { return_from_block; } } // The remoteAddr parameter could be of type NSMutableData. // So we copy it to be safe. NSData *address = [remoteAddr copy]; NSArray *addresses = [NSArray arrayWithObject:address]; GCDAsyncUdpSpecialPacket *packet = [[GCDAsyncUdpSpecialPacket alloc] init]; packet->addresses = addresses; // Updates flags, add connect packet to send queue, and pump send queue self->flags |= kConnecting; [self->sendQueue addObject:packet]; [self maybeDequeueSend]; result = YES; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (err) LogError(@"Error connecting to address: %@", err); if (errPtr) *errPtr = err; return result; } - (void)maybeConnect { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); BOOL sendQueueReady = [currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]; if (sendQueueReady) { GCDAsyncUdpSpecialPacket *connectPacket = (GCDAsyncUdpSpecialPacket *)currentSend; if (connectPacket->resolveInProgress) { LogVerbose(@"Waiting for DNS resolve..."); } else { if (connectPacket->error) { [self notifyDidNotConnect:connectPacket->error]; } else { NSData *address = nil; NSError *error = nil; int addressFamily = [self getAddress:&address error:&error fromAddresses:connectPacket->addresses]; // Perform connect BOOL result = NO; switch (addressFamily) { case AF_INET : result = [self connectWithAddress4:address error:&error]; break; case AF_INET6 : result = [self connectWithAddress6:address error:&error]; break; } if (result) { flags |= kDidBind; flags |= kDidConnect; cachedConnectedAddress = address; cachedConnectedHost = [[self class] hostFromAddress:address]; cachedConnectedPort = [[self class] portFromAddress:address]; cachedConnectedFamily = addressFamily; [self notifyDidConnectToAddress:address]; } else { [self notifyDidNotConnect:error]; } } flags &= ~kConnecting; [self endCurrentSend]; [self maybeDequeueSend]; } } } - (BOOL)connectWithAddress4:(NSData *)address4 error:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); int status = connect(socket4FD, (const struct sockaddr *)[address4 bytes], (socklen_t)[address4 length]); if (status != 0) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; return NO; } [self closeSocket6]; flags |= kIPv6Deactivated; return YES; } - (BOOL)connectWithAddress6:(NSData *)address6 error:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); int status = connect(socket6FD, (const struct sockaddr *)[address6 bytes], (socklen_t)[address6 length]); if (status != 0) { if (errPtr) *errPtr = [self errnoErrorWithReason:@"Error in connect() function"]; return NO; } [self closeSocket4]; flags |= kIPv4Deactivated; return YES; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Multicast //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)preJoin:(NSError **)errPtr { if (![self preOp:errPtr]) { return NO; } if (!(flags & kDidBind)) { if (errPtr) { NSString *msg = @"Must bind a socket before joining a multicast group."; *errPtr = [self badConfigError:msg]; } return NO; } if ((flags & kConnecting) || (flags & kDidConnect)) { if (errPtr) { NSString *msg = @"Cannot join a multicast group if connected."; *errPtr = [self badConfigError:msg]; } return NO; } return YES; } - (BOOL)joinMulticastGroup:(NSString *)group error:(NSError **)errPtr { return [self joinMulticastGroup:group onInterface:nil error:errPtr]; } - (BOOL)joinMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr { // IP_ADD_MEMBERSHIP == IPV6_JOIN_GROUP return [self performMulticastRequest:IP_ADD_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; } - (BOOL)leaveMulticastGroup:(NSString *)group error:(NSError **)errPtr { return [self leaveMulticastGroup:group onInterface:nil error:errPtr]; } - (BOOL)leaveMulticastGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr { // IP_DROP_MEMBERSHIP == IPV6_LEAVE_GROUP return [self performMulticastRequest:IP_DROP_MEMBERSHIP forGroup:group onInterface:interface error:errPtr]; } - (BOOL)performMulticastRequest:(int)requestType forGroup:(NSString *)group onInterface:(NSString *)interface error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { // Run through sanity checks if (![self preJoin:&err]) { return_from_block; } // Convert group to address NSData *groupAddr4 = nil; NSData *groupAddr6 = nil; [self convertNumericHost:group port:0 intoAddress4:&groupAddr4 address6:&groupAddr6]; if ((groupAddr4 == nil) && (groupAddr6 == nil)) { NSString *msg = @"Unknown group. Specify valid group IP address."; err = [self badParamError:msg]; return_from_block; } // Convert interface to address NSData *interfaceAddr4 = nil; NSData *interfaceAddr6 = nil; [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; if ((interfaceAddr4 == nil) && (interfaceAddr6 == nil)) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\") or IP address."; err = [self badParamError:msg]; return_from_block; } // Perform join if ((self->socket4FD != SOCKET_NULL) && groupAddr4 && interfaceAddr4) { const struct sockaddr_in *nativeGroup = (const struct sockaddr_in *)[groupAddr4 bytes]; const struct sockaddr_in *nativeIface = (const struct sockaddr_in *)[interfaceAddr4 bytes]; struct ip_mreq imreq; imreq.imr_multiaddr = nativeGroup->sin_addr; imreq.imr_interface = nativeIface->sin_addr; int status = setsockopt(self->socket4FD, IPPROTO_IP, requestType, (const void *)&imreq, sizeof(imreq)); if (status != 0) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; return_from_block; } // Using IPv4 only [self closeSocket6]; result = YES; } else if ((self->socket6FD != SOCKET_NULL) && groupAddr6 && interfaceAddr6) { const struct sockaddr_in6 *nativeGroup = (const struct sockaddr_in6 *)[groupAddr6 bytes]; struct ipv6_mreq imreq; imreq.ipv6mr_multiaddr = nativeGroup->sin6_addr; imreq.ipv6mr_interface = [self indexOfInterfaceAddr6:interfaceAddr6]; int status = setsockopt(self->socket6FD, IPPROTO_IPV6, requestType, (const void *)&imreq, sizeof(imreq)); if (status != 0) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; return_from_block; } // Using IPv6 only [self closeSocket4]; result = YES; } else { NSString *msg = @"Socket, group, and interface do not have matching IP versions"; err = [self badParamError:msg]; return_from_block; } }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (errPtr) *errPtr = err; return result; } - (BOOL)sendIPv4MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { if (![self preOp:&err]) { return_from_block; } if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) { return_from_block; } } // Convert interface to address NSData *interfaceAddr4 = nil; NSData *interfaceAddr6 = nil; [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; if (interfaceAddr4 == nil) { NSString *msg = @"Unknown interface. Specify valid interface by IP address."; err = [self badParamError:msg]; return_from_block; } if (self->socket4FD != SOCKET_NULL) { const struct sockaddr_in *nativeIface = (struct sockaddr_in *)[interfaceAddr4 bytes]; struct in_addr interface_addr = nativeIface->sin_addr; int status = setsockopt(self->socket4FD, IPPROTO_IP, IP_MULTICAST_IF, &interface_addr, sizeof(interface_addr)); if (status != 0) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; return_from_block; result = YES; } } }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (errPtr) *errPtr = err; return result; } - (BOOL)sendIPv6MulticastOnInterface:(NSString*)interface error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { if (![self preOp:&err]) { return_from_block; } if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) { return_from_block; } } // Convert interface to address NSData *interfaceAddr4 = nil; NSData *interfaceAddr6 = nil; [self convertIntefaceDescription:interface port:0 intoAddress4:&interfaceAddr4 address6:&interfaceAddr6]; if (interfaceAddr6 == nil) { NSString *msg = @"Unknown interface. Specify valid interface by name (e.g. \"en1\")."; err = [self badParamError:msg]; return_from_block; } if ((self->socket6FD != SOCKET_NULL)) { uint32_t scope_id = [self indexOfInterfaceAddr6:interfaceAddr6]; int status = setsockopt(self->socket6FD, IPPROTO_IPV6, IPV6_MULTICAST_IF, &scope_id, sizeof(scope_id)); if (status != 0) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; return_from_block; } result = YES; } }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (errPtr) *errPtr = err; return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Reuse port //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)enableReusePort:(BOOL)flag error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { if (![self preOp:&err]) { return_from_block; } if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) { return_from_block; } } int value = flag ? 1 : 0; if (self->socket4FD != SOCKET_NULL) { int error = setsockopt(self->socket4FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); if (error) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; return_from_block; } result = YES; } if (self->socket6FD != SOCKET_NULL) { int error = setsockopt(self->socket6FD, SOL_SOCKET, SO_REUSEPORT, (const void *)&value, sizeof(value)); if (error) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; return_from_block; } result = YES; } }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (errPtr) *errPtr = err; return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Broadcast //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)enableBroadcast:(BOOL)flag error:(NSError **)errPtr { __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ @autoreleasepool { if (![self preOp:&err]) { return_from_block; } if ((self->flags & kDidCreateSockets) == 0) { if (![self createSockets:&err]) { return_from_block; } } if (self->socket4FD != SOCKET_NULL) { int value = flag ? 1 : 0; int error = setsockopt(self->socket4FD, SOL_SOCKET, SO_BROADCAST, (const void *)&value, sizeof(value)); if (error) { err = [self errnoErrorWithReason:@"Error in setsockopt() function"]; return_from_block; } result = YES; } // IPv6 does not implement broadcast, the ability to send a packet to all hosts on the attached link. // The same effect can be achieved by sending a packet to the link-local all hosts multicast group. }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (errPtr) *errPtr = err; return result; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Sending //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)sendData:(NSData *)data withTag:(long)tag { [self sendData:data withTimeout:-1.0 tag:tag]; } - (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { LogTrace(); if ([data length] == 0) { LogWarn(@"Ignoring attempt to send nil/empty data."); return; } GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; dispatch_async(socketQueue, ^{ @autoreleasepool { [self->sendQueue addObject:packet]; [self maybeDequeueSend]; }}); } - (void)sendData:(NSData *)data toHost:(NSString *)host port:(uint16_t)port withTimeout:(NSTimeInterval)timeout tag:(long)tag { LogTrace(); if ([data length] == 0) { LogWarn(@"Ignoring attempt to send nil/empty data."); return; } GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; packet->resolveInProgress = YES; [self asyncResolveHost:host port:port withCompletionBlock:^(NSArray *addresses, NSError *error) { // The asyncResolveHost:port:: method asynchronously dispatches a task onto the global concurrent queue, // and immediately returns. Once the async resolve task completes, // this block is executed on our socketQueue. packet->resolveInProgress = NO; packet->resolvedAddresses = addresses; packet->resolveError = error; if (packet == self->currentSend) { LogVerbose(@"currentSend - address resolved"); [self doPreSend]; } }]; dispatch_async(socketQueue, ^{ @autoreleasepool { [self->sendQueue addObject:packet]; [self maybeDequeueSend]; }}); } - (void)sendData:(NSData *)data toAddress:(NSData *)remoteAddr withTimeout:(NSTimeInterval)timeout tag:(long)tag { LogTrace(); if ([data length] == 0) { LogWarn(@"Ignoring attempt to send nil/empty data."); return; } GCDAsyncUdpSendPacket *packet = [[GCDAsyncUdpSendPacket alloc] initWithData:data timeout:timeout tag:tag]; packet->addressFamily = [GCDAsyncUdpSocket familyFromAddress:remoteAddr]; packet->address = remoteAddr; dispatch_async(socketQueue, ^{ @autoreleasepool { [self->sendQueue addObject:packet]; [self maybeDequeueSend]; }}); } - (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue { [self setSendFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; } - (void)setSendFilter:(GCDAsyncUdpSocketSendFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue isAsynchronous:(BOOL)isAsynchronous { GCDAsyncUdpSocketSendFilterBlock newFilterBlock = NULL; dispatch_queue_t newFilterQueue = NULL; if (filterBlock) { NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); newFilterBlock = [filterBlock copy]; newFilterQueue = filterQueue; #if !OS_OBJECT_USE_OBJC dispatch_retain(newFilterQueue); #endif } dispatch_block_t block = ^{ #if !OS_OBJECT_USE_OBJC if (self->sendFilterQueue) dispatch_release(self->sendFilterQueue); #endif self->sendFilterBlock = newFilterBlock; self->sendFilterQueue = newFilterQueue; self->sendFilterAsync = isAsynchronous; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (void)maybeDequeueSend { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); // If we don't have a send operation already in progress if (currentSend == nil) { // Create the sockets if needed if ((flags & kDidCreateSockets) == 0) { NSError *err = nil; if (![self createSockets:&err]) { [self closeWithError:err]; return; } } while ([sendQueue count] > 0) { // Dequeue the next object in the queue currentSend = [sendQueue objectAtIndex:0]; [sendQueue removeObjectAtIndex:0]; if ([currentSend isKindOfClass:[GCDAsyncUdpSpecialPacket class]]) { [self maybeConnect]; return; // The maybeConnect method, if it connects, will invoke this method again } else if (currentSend->resolveError) { // Notify delegate [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:currentSend->resolveError]; // Clear currentSend currentSend = nil; continue; } else { // Start preprocessing checks on the send packet [self doPreSend]; break; } } if ((currentSend == nil) && (flags & kCloseAfterSends)) { [self closeWithError:nil]; } } } /** * This method is called after a sendPacket has been dequeued. * It performs various preprocessing checks on the packet, * and queries the sendFilter (if set) to determine if the packet can be sent. * * If the packet passes all checks, it will be passed on to the doSend method. **/ - (void)doPreSend { LogTrace(); // // 1. Check for problems with send packet // BOOL waitingForResolve = NO; NSError *error = nil; if (flags & kDidConnect) { // Connected socket if (currentSend->resolveInProgress || currentSend->resolvedAddresses || currentSend->resolveError) { NSString *msg = @"Cannot specify destination of packet for connected socket"; error = [self badConfigError:msg]; } else { currentSend->address = cachedConnectedAddress; currentSend->addressFamily = cachedConnectedFamily; } } else { // Non-Connected socket if (currentSend->resolveInProgress) { // We're waiting for the packet's destination to be resolved. waitingForResolve = YES; } else if (currentSend->resolveError) { error = currentSend->resolveError; } else if (currentSend->address == nil) { if (currentSend->resolvedAddresses == nil) { NSString *msg = @"You must specify destination of packet for a non-connected socket"; error = [self badConfigError:msg]; } else { // Pick the proper address to use (out of possibly several resolved addresses) NSData *address = nil; int addressFamily = AF_UNSPEC; addressFamily = [self getAddress:&address error:&error fromAddresses:currentSend->resolvedAddresses]; currentSend->address = address; currentSend->addressFamily = addressFamily; } } } if (waitingForResolve) { // We're waiting for the packet's destination to be resolved. LogVerbose(@"currentSend - waiting for address resolve"); if (flags & kSock4CanAcceptBytes) { [self suspendSend4Source]; } if (flags & kSock6CanAcceptBytes) { [self suspendSend6Source]; } return; } if (error) { // Unable to send packet due to some error. // Notify delegate and move on. [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:error]; [self endCurrentSend]; [self maybeDequeueSend]; return; } // // 2. Query sendFilter (if applicable) // if (sendFilterBlock && sendFilterQueue) { // Query sendFilter if (sendFilterAsync) { // Scenario 1 of 3 - Need to asynchronously query sendFilter currentSend->filterInProgress = YES; GCDAsyncUdpSendPacket *sendPacket = currentSend; dispatch_async(sendFilterQueue, ^{ @autoreleasepool { BOOL allowed = self->sendFilterBlock(sendPacket->buffer, sendPacket->address, sendPacket->tag); dispatch_async(self->socketQueue, ^{ @autoreleasepool { sendPacket->filterInProgress = NO; if (sendPacket == self->currentSend) { if (allowed) { [self doSend]; } else { LogVerbose(@"currentSend - silently dropped by sendFilter"); [self notifyDidSendDataWithTag:self->currentSend->tag]; [self endCurrentSend]; [self maybeDequeueSend]; } } }}); }}); } else { // Scenario 2 of 3 - Need to synchronously query sendFilter __block BOOL allowed = YES; dispatch_sync(sendFilterQueue, ^{ @autoreleasepool { allowed = self->sendFilterBlock(self->currentSend->buffer, self->currentSend->address, self->currentSend->tag); }}); if (allowed) { [self doSend]; } else { LogVerbose(@"currentSend - silently dropped by sendFilter"); [self notifyDidSendDataWithTag:currentSend->tag]; [self endCurrentSend]; [self maybeDequeueSend]; } } } else // if (!sendFilterBlock || !sendFilterQueue) { // Scenario 3 of 3 - No sendFilter. Just go straight into sending. [self doSend]; } } /** * This method performs the actual sending of data in the currentSend packet. * It should only be called if the **/ - (void)doSend { LogTrace(); NSAssert(currentSend != nil, @"Invalid logic"); // Perform the actual send ssize_t result = 0; if (flags & kDidConnect) { // Connected socket const void *buffer = [currentSend->buffer bytes]; size_t length = (size_t)[currentSend->buffer length]; if (currentSend->addressFamily == AF_INET) { result = send(socket4FD, buffer, length, 0); LogVerbose(@"send(socket4FD) = %d", result); } else { result = send(socket6FD, buffer, length, 0); LogVerbose(@"send(socket6FD) = %d", result); } } else { // Non-Connected socket const void *buffer = [currentSend->buffer bytes]; size_t length = (size_t)[currentSend->buffer length]; const void *dst = [currentSend->address bytes]; socklen_t dstSize = (socklen_t)[currentSend->address length]; if (currentSend->addressFamily == AF_INET) { result = sendto(socket4FD, buffer, length, 0, dst, dstSize); LogVerbose(@"sendto(socket4FD) = %d", result); } else { result = sendto(socket6FD, buffer, length, 0, dst, dstSize); LogVerbose(@"sendto(socket6FD) = %d", result); } } // If the socket wasn't bound before, it is now if ((flags & kDidBind) == 0) { flags |= kDidBind; } // Check the results. // // From the send() & sendto() manpage: // // Upon successful completion, the number of bytes which were sent is returned. // Otherwise, -1 is returned and the global variable errno is set to indicate the error. BOOL waitingForSocket = NO; NSError *socketError = nil; if (result == 0) { waitingForSocket = YES; } else if (result < 0) { if (errno == EAGAIN) waitingForSocket = YES; else socketError = [self errnoErrorWithReason:@"Error in send() function."]; } if (waitingForSocket) { // Not enough room in the underlying OS socket send buffer. // Wait for a notification of available space. LogVerbose(@"currentSend - waiting for socket"); if (!(flags & kSock4CanAcceptBytes)) { [self resumeSend4Source]; } if (!(flags & kSock6CanAcceptBytes)) { [self resumeSend6Source]; } if ((sendTimer == NULL) && (currentSend->timeout >= 0.0)) { // Unable to send packet right away. // Start timer to timeout the send operation. [self setupSendTimerWithTimeout:currentSend->timeout]; } } else if (socketError) { [self closeWithError:socketError]; } else // done { [self notifyDidSendDataWithTag:currentSend->tag]; [self endCurrentSend]; [self maybeDequeueSend]; } } /** * Releases all resources associated with the currentSend. **/ - (void)endCurrentSend { if (sendTimer) { dispatch_source_cancel(sendTimer); #if !OS_OBJECT_USE_OBJC dispatch_release(sendTimer); #endif sendTimer = NULL; } currentSend = nil; } /** * Performs the operations to timeout the current send operation, and move on. **/ - (void)doSendTimeout { LogTrace(); [self notifyDidNotSendDataWithTag:currentSend->tag dueToError:[self sendTimeoutError]]; [self endCurrentSend]; [self maybeDequeueSend]; } /** * Sets up a timer that fires to timeout the current send operation. * This method should only be called once per send packet. **/ - (void)setupSendTimerWithTimeout:(NSTimeInterval)timeout { NSAssert(sendTimer == NULL, @"Invalid logic"); NSAssert(timeout >= 0.0, @"Invalid logic"); LogTrace(); sendTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, socketQueue); dispatch_source_set_event_handler(sendTimer, ^{ @autoreleasepool { [self doSendTimeout]; }}); dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)); dispatch_source_set_timer(sendTimer, tt, DISPATCH_TIME_FOREVER, 0); dispatch_resume(sendTimer); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Receiving //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (BOOL)receiveOnce:(NSError **)errPtr { LogTrace(); __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ if ((self->flags & kReceiveOnce) == 0) { if ((self->flags & kDidCreateSockets) == 0) { NSString *msg = @"Must bind socket before you can receive data. " @"You can do this explicitly via bind, or implicitly via connect or by sending data."; err = [self badConfigError:msg]; return_from_block; } self->flags |= kReceiveOnce; // Enable self->flags &= ~kReceiveContinuous; // Disable dispatch_async(self->socketQueue, ^{ @autoreleasepool { [self doReceive]; }}); } result = YES; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (err) LogError(@"Error in beginReceiving: %@", err); if (errPtr) *errPtr = err; return result; } - (BOOL)beginReceiving:(NSError **)errPtr { LogTrace(); __block BOOL result = NO; __block NSError *err = nil; dispatch_block_t block = ^{ if ((self->flags & kReceiveContinuous) == 0) { if ((self->flags & kDidCreateSockets) == 0) { NSString *msg = @"Must bind socket before you can receive data. " @"You can do this explicitly via bind, or implicitly via connect or by sending data."; err = [self badConfigError:msg]; return_from_block; } self->flags |= kReceiveContinuous; // Enable self->flags &= ~kReceiveOnce; // Disable dispatch_async(self->socketQueue, ^{ @autoreleasepool { [self doReceive]; }}); } result = YES; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); if (err) LogError(@"Error in beginReceiving: %@", err); if (errPtr) *errPtr = err; return result; } - (void)pauseReceiving { LogTrace(); dispatch_block_t block = ^{ self->flags &= ~kReceiveOnce; // Disable self->flags &= ~kReceiveContinuous; // Disable if (self->socket4FDBytesAvailable > 0) { [self suspendReceive4Source]; } if (self->socket6FDBytesAvailable > 0) { [self suspendReceive6Source]; } }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue { [self setReceiveFilter:filterBlock withQueue:filterQueue isAsynchronous:YES]; } - (void)setReceiveFilter:(GCDAsyncUdpSocketReceiveFilterBlock)filterBlock withQueue:(dispatch_queue_t)filterQueue isAsynchronous:(BOOL)isAsynchronous { GCDAsyncUdpSocketReceiveFilterBlock newFilterBlock = NULL; dispatch_queue_t newFilterQueue = NULL; if (filterBlock) { NSAssert(filterQueue, @"Must provide a dispatch_queue in which to run the filter block."); newFilterBlock = [filterBlock copy]; newFilterQueue = filterQueue; #if !OS_OBJECT_USE_OBJC dispatch_retain(newFilterQueue); #endif } dispatch_block_t block = ^{ #if !OS_OBJECT_USE_OBJC if (self->receiveFilterQueue) dispatch_release(self->receiveFilterQueue); #endif self->receiveFilterBlock = newFilterBlock; self->receiveFilterQueue = newFilterQueue; self->receiveFilterAsync = isAsynchronous; }; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } - (void)doReceive { LogTrace(); if ((flags & (kReceiveOnce | kReceiveContinuous)) == 0) { LogVerbose(@"Receiving is paused..."); if (socket4FDBytesAvailable > 0) { [self suspendReceive4Source]; } if (socket6FDBytesAvailable > 0) { [self suspendReceive6Source]; } return; } if ((flags & kReceiveOnce) && (pendingFilterOperations > 0)) { LogVerbose(@"Receiving is temporarily paused (pending filter operations)..."); if (socket4FDBytesAvailable > 0) { [self suspendReceive4Source]; } if (socket6FDBytesAvailable > 0) { [self suspendReceive6Source]; } return; } if ((socket4FDBytesAvailable == 0) && (socket6FDBytesAvailable == 0)) { LogVerbose(@"No data available to receive..."); if (socket4FDBytesAvailable == 0) { [self resumeReceive4Source]; } if (socket6FDBytesAvailable == 0) { [self resumeReceive6Source]; } return; } // Figure out if we should receive on socket4 or socket6 BOOL doReceive4; if (flags & kDidConnect) { // Connected socket doReceive4 = (socket4FD != SOCKET_NULL); } else { // Non-Connected socket if (socket4FDBytesAvailable > 0) { if (socket6FDBytesAvailable > 0) { // Bytes available on socket4 & socket6 doReceive4 = (flags & kFlipFlop) ? YES : NO; flags ^= kFlipFlop; // flags = flags xor kFlipFlop; (toggle flip flop bit) } else { // Bytes available on socket4, but not socket6 doReceive4 = YES; } } else { // Bytes available on socket6, but not socket4 doReceive4 = NO; } } // Perform socket IO ssize_t result = 0; NSData *data = nil; NSData *addr4 = nil; NSData *addr6 = nil; if (doReceive4) { NSAssert(socket4FDBytesAvailable > 0, @"Invalid logic"); LogVerbose(@"Receiving on IPv4"); struct sockaddr_in sockaddr4; socklen_t sockaddr4len = sizeof(sockaddr4); // #222: GCD does not necessarily return the size of an entire UDP packet // from dispatch_source_get_data(), so we must use the maximum packet size. size_t bufSize = max4ReceiveSize; void *buf = malloc(bufSize); result = recvfrom(socket4FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr4, &sockaddr4len); LogVerbose(@"recvfrom(socket4FD) = %i", (int)result); if (result > 0) { if ((size_t)result >= socket4FDBytesAvailable) socket4FDBytesAvailable = 0; else socket4FDBytesAvailable -= result; if ((size_t)result != bufSize) { buf = realloc(buf, result); } data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; addr4 = [NSData dataWithBytes:&sockaddr4 length:sockaddr4len]; } else { LogVerbose(@"recvfrom(socket4FD) = %@", [self errnoError]); socket4FDBytesAvailable = 0; free(buf); } } else { NSAssert(socket6FDBytesAvailable > 0, @"Invalid logic"); LogVerbose(@"Receiving on IPv6"); struct sockaddr_in6 sockaddr6; socklen_t sockaddr6len = sizeof(sockaddr6); // #222: GCD does not necessarily return the size of an entire UDP packet // from dispatch_source_get_data(), so we must use the maximum packet size. size_t bufSize = max6ReceiveSize; void *buf = malloc(bufSize); result = recvfrom(socket6FD, buf, bufSize, 0, (struct sockaddr *)&sockaddr6, &sockaddr6len); LogVerbose(@"recvfrom(socket6FD) -> %i", (int)result); if (result > 0) { if ((size_t)result >= socket6FDBytesAvailable) socket6FDBytesAvailable = 0; else socket6FDBytesAvailable -= result; if ((size_t)result != bufSize) { buf = realloc(buf, result); } data = [NSData dataWithBytesNoCopy:buf length:result freeWhenDone:YES]; addr6 = [NSData dataWithBytes:&sockaddr6 length:sockaddr6len]; } else { LogVerbose(@"recvfrom(socket6FD) = %@", [self errnoError]); socket6FDBytesAvailable = 0; free(buf); } } BOOL waitingForSocket = NO; BOOL notifiedDelegate = NO; BOOL ignored = NO; NSError *socketError = nil; if (result == 0) { waitingForSocket = YES; } else if (result < 0) { if (errno == EAGAIN) waitingForSocket = YES; else socketError = [self errnoErrorWithReason:@"Error in recvfrom() function"]; } else { if (flags & kDidConnect) { if (addr4 && ![self isConnectedToAddress4:addr4]) ignored = YES; if (addr6 && ![self isConnectedToAddress6:addr6]) ignored = YES; } NSData *addr = (addr4 != nil) ? addr4 : addr6; if (!ignored) { if (receiveFilterBlock && receiveFilterQueue) { // Run data through filter, and if approved, notify delegate __block id filterContext = nil; __block BOOL allowed = NO; if (receiveFilterAsync) { pendingFilterOperations++; dispatch_async(receiveFilterQueue, ^{ @autoreleasepool { allowed = self->receiveFilterBlock(data, addr, &filterContext); // Transition back to socketQueue to get the current delegate / delegateQueue dispatch_async(self->socketQueue, ^{ @autoreleasepool { self->pendingFilterOperations--; if (allowed) { [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; } else { LogVerbose(@"received packet silently dropped by receiveFilter"); } if (self->flags & kReceiveOnce) { if (allowed) { // The delegate has been notified, // so our receive once operation has completed. self->flags &= ~kReceiveOnce; } else if (self->pendingFilterOperations == 0) { // All pending filter operations have completed, // and none were allowed through. // Our receive once operation hasn't completed yet. [self doReceive]; } } }}); }}); } else // if (!receiveFilterAsync) { dispatch_sync(receiveFilterQueue, ^{ @autoreleasepool { allowed = self->receiveFilterBlock(data, addr, &filterContext); }}); if (allowed) { [self notifyDidReceiveData:data fromAddress:addr withFilterContext:filterContext]; notifiedDelegate = YES; } else { LogVerbose(@"received packet silently dropped by receiveFilter"); ignored = YES; } } } else // if (!receiveFilterBlock || !receiveFilterQueue) { [self notifyDidReceiveData:data fromAddress:addr withFilterContext:nil]; notifiedDelegate = YES; } } } if (waitingForSocket) { // Wait for a notification of available data. if (socket4FDBytesAvailable == 0) { [self resumeReceive4Source]; } if (socket6FDBytesAvailable == 0) { [self resumeReceive6Source]; } } else if (socketError) { [self closeWithError:socketError]; } else { if (flags & kReceiveContinuous) { // Continuous receive mode [self doReceive]; } else { // One-at-a-time receive mode if (notifiedDelegate) { // The delegate has been notified (no set filter). // So our receive once operation has completed. flags &= ~kReceiveOnce; } else if (ignored) { [self doReceive]; } else { // Waiting on asynchronous receive filter... } } } } - (void)doReceiveEOF { LogTrace(); [self closeWithError:[self socketClosedError]]; } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Closing //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// - (void)closeWithError:(NSError *)error { LogVerbose(@"closeWithError: %@", error); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (currentSend) [self endCurrentSend]; [sendQueue removeAllObjects]; // If a socket has been created, we should notify the delegate. BOOL shouldCallDelegate = (flags & kDidCreateSockets) ? YES : NO; // Close all sockets, send/receive sources, cfstreams, etc #if TARGET_OS_IPHONE [self removeStreamsFromRunLoop]; [self closeReadAndWriteStreams]; #endif [self closeSockets]; // Clear all flags (config remains as is) flags = 0; if (shouldCallDelegate) { [self notifyDidCloseWithError:error]; } } - (void)close { LogTrace(); dispatch_block_t block = ^{ @autoreleasepool { [self closeWithError:nil]; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); } - (void)closeAfterSending { LogTrace(); dispatch_block_t block = ^{ @autoreleasepool { self->flags |= kCloseAfterSends; if (self->currentSend == nil && [self->sendQueue count] == 0) { [self closeWithError:nil]; } }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark CFStream //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #if TARGET_OS_IPHONE static NSThread *listenerThread; + (void)ignore:(id)_ {} + (void)startListenerThreadIfNeeded { static dispatch_once_t predicate; dispatch_once(&predicate, ^{ listenerThread = [[NSThread alloc] initWithTarget:self selector:@selector(listenerThread:) object:nil]; [listenerThread start]; }); } + (void)listenerThread:(id)unused { @autoreleasepool { [[NSThread currentThread] setName:GCDAsyncUdpSocketThreadName]; LogInfo(@"ListenerThread: Started"); // We can't run the run loop unless it has an associated input source or a timer. // So we'll just create a timer that will never fire - unless the server runs for a decades. [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow] target:self selector:@selector(ignore:) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] run]; LogInfo(@"ListenerThread: Stopped"); } } + (void)addStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket { LogTrace(); NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); CFRunLoopRef runLoop = CFRunLoopGetCurrent(); if (asyncUdpSocket->readStream4) CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); if (asyncUdpSocket->readStream6) CFReadStreamScheduleWithRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); if (asyncUdpSocket->writeStream4) CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); if (asyncUdpSocket->writeStream6) CFWriteStreamScheduleWithRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); } + (void)removeStreamListener:(GCDAsyncUdpSocket *)asyncUdpSocket { LogTrace(); NSAssert([NSThread currentThread] == listenerThread, @"Invoked on wrong thread"); CFRunLoopRef runLoop = CFRunLoopGetCurrent(); if (asyncUdpSocket->readStream4) CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream4, runLoop, kCFRunLoopDefaultMode); if (asyncUdpSocket->readStream6) CFReadStreamUnscheduleFromRunLoop(asyncUdpSocket->readStream6, runLoop, kCFRunLoopDefaultMode); if (asyncUdpSocket->writeStream4) CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream4, runLoop, kCFRunLoopDefaultMode); if (asyncUdpSocket->writeStream6) CFWriteStreamUnscheduleFromRunLoop(asyncUdpSocket->writeStream6, runLoop, kCFRunLoopDefaultMode); } static void CFReadStreamCallback(CFReadStreamRef stream, CFStreamEventType type, void *pInfo) { @autoreleasepool { GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; switch(type) { case kCFStreamEventOpenCompleted: { LogCVerbose(@"CFReadStreamCallback - Open"); break; } case kCFStreamEventHasBytesAvailable: { LogCVerbose(@"CFReadStreamCallback - HasBytesAvailable"); break; } case kCFStreamEventErrorOccurred: case kCFStreamEventEndEncountered: { NSError *error = (__bridge_transfer NSError *)CFReadStreamCopyError(stream); if (error == nil && type == kCFStreamEventEndEncountered) { error = [asyncUdpSocket socketClosedError]; } dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFReadStreamCallback - %@", (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); if (stream != asyncUdpSocket->readStream4 && stream != asyncUdpSocket->readStream6 ) { LogCVerbose(@"CFReadStreamCallback - Ignored"); return_from_block; } [asyncUdpSocket closeWithError:error]; }}); break; } default: { LogCError(@"CFReadStreamCallback - UnknownType: %i", (int)type); } } } } static void CFWriteStreamCallback(CFWriteStreamRef stream, CFStreamEventType type, void *pInfo) { @autoreleasepool { GCDAsyncUdpSocket *asyncUdpSocket = (__bridge GCDAsyncUdpSocket *)pInfo; switch(type) { case kCFStreamEventOpenCompleted: { LogCVerbose(@"CFWriteStreamCallback - Open"); break; } case kCFStreamEventCanAcceptBytes: { LogCVerbose(@"CFWriteStreamCallback - CanAcceptBytes"); break; } case kCFStreamEventErrorOccurred: case kCFStreamEventEndEncountered: { NSError *error = (__bridge_transfer NSError *)CFWriteStreamCopyError(stream); if (error == nil && type == kCFStreamEventEndEncountered) { error = [asyncUdpSocket socketClosedError]; } dispatch_async(asyncUdpSocket->socketQueue, ^{ @autoreleasepool { LogCVerbose(@"CFWriteStreamCallback - %@", (type == kCFStreamEventErrorOccurred) ? @"Error" : @"EndEncountered"); if (stream != asyncUdpSocket->writeStream4 && stream != asyncUdpSocket->writeStream6 ) { LogCVerbose(@"CFWriteStreamCallback - Ignored"); return_from_block; } [asyncUdpSocket closeWithError:error]; }}); break; } default: { LogCError(@"CFWriteStreamCallback - UnknownType: %i", (int)type); } } } } - (BOOL)createReadAndWriteStreams:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSError *err = nil; if (readStream4 || writeStream4 || readStream6 || writeStream6) { // Streams already created return YES; } if (socket4FD == SOCKET_NULL && socket6FD == SOCKET_NULL) { err = [self otherError:@"Cannot create streams without a file descriptor"]; goto Failed; } // Create streams LogVerbose(@"Creating read and write stream(s)..."); if (socket4FD != SOCKET_NULL) { CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket4FD, &readStream4, &writeStream4); if (!readStream4 || !writeStream4) { err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv4]"]; goto Failed; } } if (socket6FD != SOCKET_NULL) { CFStreamCreatePairWithSocket(NULL, (CFSocketNativeHandle)socket6FD, &readStream6, &writeStream6); if (!readStream6 || !writeStream6) { err = [self otherError:@"Error in CFStreamCreatePairWithSocket() [IPv6]"]; goto Failed; } } // Ensure the CFStream's don't close our underlying socket CFReadStreamSetProperty(readStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); CFWriteStreamSetProperty(writeStream4, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); CFReadStreamSetProperty(readStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); CFWriteStreamSetProperty(writeStream6, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanFalse); return YES; Failed: if (readStream4) { CFReadStreamClose(readStream4); CFRelease(readStream4); readStream4 = NULL; } if (writeStream4) { CFWriteStreamClose(writeStream4); CFRelease(writeStream4); writeStream4 = NULL; } if (readStream6) { CFReadStreamClose(readStream6); CFRelease(readStream6); readStream6 = NULL; } if (writeStream6) { CFWriteStreamClose(writeStream6); CFRelease(writeStream6); writeStream6 = NULL; } if (errPtr) *errPtr = err; return NO; } - (BOOL)registerForStreamCallbacks:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); NSError *err = nil; streamContext.version = 0; streamContext.info = (__bridge void *)self; streamContext.retain = nil; streamContext.release = nil; streamContext.copyDescription = nil; CFOptionFlags readStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; CFOptionFlags writeStreamEvents = kCFStreamEventErrorOccurred | kCFStreamEventEndEncountered; // readStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventHasBytesAvailable); // writeStreamEvents |= (kCFStreamEventOpenCompleted | kCFStreamEventCanAcceptBytes); if (socket4FD != SOCKET_NULL) { if (readStream4 == NULL || writeStream4 == NULL) { err = [self otherError:@"Read/Write stream4 is null"]; goto Failed; } BOOL r1 = CFReadStreamSetClient(readStream4, readStreamEvents, &CFReadStreamCallback, &streamContext); BOOL r2 = CFWriteStreamSetClient(writeStream4, writeStreamEvents, &CFWriteStreamCallback, &streamContext); if (!r1 || !r2) { err = [self otherError:@"Error in CFStreamSetClient(), [IPv4]"]; goto Failed; } } if (socket6FD != SOCKET_NULL) { if (readStream6 == NULL || writeStream6 == NULL) { err = [self otherError:@"Read/Write stream6 is null"]; goto Failed; } BOOL r1 = CFReadStreamSetClient(readStream6, readStreamEvents, &CFReadStreamCallback, &streamContext); BOOL r2 = CFWriteStreamSetClient(writeStream6, writeStreamEvents, &CFWriteStreamCallback, &streamContext); if (!r1 || !r2) { err = [self otherError:@"Error in CFStreamSetClient() [IPv6]"]; goto Failed; } } return YES; Failed: if (readStream4) { CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); } if (writeStream4) { CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); } if (readStream6) { CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); } if (writeStream6) { CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); } if (errPtr) *errPtr = err; return NO; } - (BOOL)addStreamsToRunLoop:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); if (!(flags & kAddedStreamListener)) { [[self class] startListenerThreadIfNeeded]; [[self class] performSelector:@selector(addStreamListener:) onThread:listenerThread withObject:self waitUntilDone:YES]; flags |= kAddedStreamListener; } return YES; } - (BOOL)openStreams:(NSError **)errPtr { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); NSAssert(readStream4 || writeStream4 || readStream6 || writeStream6, @"Read/Write streams are null"); NSError *err = nil; if (socket4FD != SOCKET_NULL) { BOOL r1 = CFReadStreamOpen(readStream4); BOOL r2 = CFWriteStreamOpen(writeStream4); if (!r1 || !r2) { err = [self otherError:@"Error in CFStreamOpen() [IPv4]"]; goto Failed; } } if (socket6FD != SOCKET_NULL) { BOOL r1 = CFReadStreamOpen(readStream6); BOOL r2 = CFWriteStreamOpen(writeStream6); if (!r1 || !r2) { err = [self otherError:@"Error in CFStreamOpen() [IPv6]"]; goto Failed; } } return YES; Failed: if (errPtr) *errPtr = err; return NO; } - (void)removeStreamsFromRunLoop { LogTrace(); NSAssert(dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey), @"Must be dispatched on socketQueue"); if (flags & kAddedStreamListener) { [[self class] performSelector:@selector(removeStreamListener:) onThread:listenerThread withObject:self waitUntilDone:YES]; flags &= ~kAddedStreamListener; } } - (void)closeReadAndWriteStreams { LogTrace(); if (readStream4) { CFReadStreamSetClient(readStream4, kCFStreamEventNone, NULL, NULL); CFReadStreamClose(readStream4); CFRelease(readStream4); readStream4 = NULL; } if (writeStream4) { CFWriteStreamSetClient(writeStream4, kCFStreamEventNone, NULL, NULL); CFWriteStreamClose(writeStream4); CFRelease(writeStream4); writeStream4 = NULL; } if (readStream6) { CFReadStreamSetClient(readStream6, kCFStreamEventNone, NULL, NULL); CFReadStreamClose(readStream6); CFRelease(readStream6); readStream6 = NULL; } if (writeStream6) { CFWriteStreamSetClient(writeStream6, kCFStreamEventNone, NULL, NULL); CFWriteStreamClose(writeStream6); CFRelease(writeStream6); writeStream6 = NULL; } } #endif #if TARGET_OS_IPHONE - (void)applicationWillEnterForeground:(NSNotification *)notification { LogTrace(); // If the application was backgrounded, then iOS may have shut down our sockets. // So we take a quick look to see if any of them received an EOF. dispatch_block_t block = ^{ @autoreleasepool { [self resumeReceive4Source]; [self resumeReceive6Source]; }}; if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_async(socketQueue, block); } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Advanced //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// /** * See header file for big discussion of this method. **/ - (void)markSocketQueueTargetQueue:(dispatch_queue_t)socketNewTargetQueue { void *nonNullUnusedPointer = (__bridge void *)self; dispatch_queue_set_specific(socketNewTargetQueue, IsOnSocketQueueOrTargetQueueKey, nonNullUnusedPointer, NULL); } /** * See header file for big discussion of this method. **/ - (void)unmarkSocketQueueTargetQueue:(dispatch_queue_t)socketOldTargetQueue { dispatch_queue_set_specific(socketOldTargetQueue, IsOnSocketQueueOrTargetQueueKey, NULL, NULL); } - (void)performBlock:(dispatch_block_t)block { if (dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) block(); else dispatch_sync(socketQueue, block); } - (int)socketFD { if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", THIS_FILE, THIS_METHOD); return SOCKET_NULL; } if (socket4FD != SOCKET_NULL) return socket4FD; else return socket6FD; } - (int)socket4FD { if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", THIS_FILE, THIS_METHOD); return SOCKET_NULL; } return socket4FD; } - (int)socket6FD { if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", THIS_FILE, THIS_METHOD); return SOCKET_NULL; } return socket6FD; } #if TARGET_OS_IPHONE - (CFReadStreamRef)readStream { if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", THIS_FILE, THIS_METHOD); return NULL; } NSError *err = nil; if (![self createReadAndWriteStreams:&err]) { LogError(@"Error creating CFStream(s): %@", err); return NULL; } // Todo... if (readStream4) return readStream4; else return readStream6; } - (CFWriteStreamRef)writeStream { if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", THIS_FILE, THIS_METHOD); return NULL; } NSError *err = nil; if (![self createReadAndWriteStreams:&err]) { LogError(@"Error creating CFStream(s): %@", err); return NULL; } if (writeStream4) return writeStream4; else return writeStream6; } - (BOOL)enableBackgroundingOnSockets { if (! dispatch_get_specific(IsOnSocketQueueOrTargetQueueKey)) { LogWarn(@"%@: %@ - Method only available from within the context of a performBlock: invocation", THIS_FILE, THIS_METHOD); return NO; } // Why is this commented out? // See comments below. // NSError *err = nil; // if (![self createReadAndWriteStreams:&err]) // { // LogError(@"Error creating CFStream(s): %@", err); // return NO; // } // // LogVerbose(@"Enabling backgrouding on socket"); // // BOOL r1, r2; // // if (readStream4 && writeStream4) // { // r1 = CFReadStreamSetProperty(readStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); // r2 = CFWriteStreamSetProperty(writeStream4, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); // // if (!r1 || !r2) // { // LogError(@"Error setting voip type (IPv4)"); // return NO; // } // } // // if (readStream6 && writeStream6) // { // r1 = CFReadStreamSetProperty(readStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); // r2 = CFWriteStreamSetProperty(writeStream6, kCFStreamNetworkServiceType, kCFStreamNetworkServiceTypeVoIP); // // if (!r1 || !r2) // { // LogError(@"Error setting voip type (IPv6)"); // return NO; // } // } // // return YES; // The above code will actually appear to work. // The methods will return YES, and everything will appear fine. // // One tiny problem: the sockets will still get closed when the app gets backgrounded. // // Apple does not officially support backgrounding UDP sockets. return NO; } #endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// #pragma mark Class Methods //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + (NSString *)hostFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { char addrBuf[INET_ADDRSTRLEN]; if (inet_ntop(AF_INET, &pSockaddr4->sin_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (NSString *)hostFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { char addrBuf[INET6_ADDRSTRLEN]; if (inet_ntop(AF_INET6, &pSockaddr6->sin6_addr, addrBuf, (socklen_t)sizeof(addrBuf)) == NULL) { addrBuf[0] = '\0'; } return [NSString stringWithCString:addrBuf encoding:NSASCIIStringEncoding]; } + (uint16_t)portFromSockaddr4:(const struct sockaddr_in *)pSockaddr4 { return ntohs(pSockaddr4->sin_port); } + (uint16_t)portFromSockaddr6:(const struct sockaddr_in6 *)pSockaddr6 { return ntohs(pSockaddr6->sin6_port); } + (NSString *)hostFromAddress:(NSData *)address { NSString *host = nil; [self getHost:&host port:NULL family:NULL fromAddress:address]; return host; } + (uint16_t)portFromAddress:(NSData *)address { uint16_t port = 0; [self getHost:NULL port:&port family:NULL fromAddress:address]; return port; } + (int)familyFromAddress:(NSData *)address { int af = AF_UNSPEC; [self getHost:NULL port:NULL family:&af fromAddress:address]; return af; } + (BOOL)isIPv4Address:(NSData *)address { int af = AF_UNSPEC; [self getHost:NULL port:NULL family:&af fromAddress:address]; return (af == AF_INET); } + (BOOL)isIPv6Address:(NSData *)address { int af = AF_UNSPEC; [self getHost:NULL port:NULL family:&af fromAddress:address]; return (af == AF_INET6); } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr fromAddress:(NSData *)address { return [self getHost:hostPtr port:portPtr family:NULL fromAddress:address]; } + (BOOL)getHost:(NSString **)hostPtr port:(uint16_t *)portPtr family:(int *)afPtr fromAddress:(NSData *)address { if ([address length] >= sizeof(struct sockaddr)) { const struct sockaddr *addrX = (const struct sockaddr *)[address bytes]; if (addrX->sa_family == AF_INET) { if ([address length] >= sizeof(struct sockaddr_in)) { const struct sockaddr_in *addr4 = (const struct sockaddr_in *)(const void *)addrX; if (hostPtr) *hostPtr = [self hostFromSockaddr4:addr4]; if (portPtr) *portPtr = [self portFromSockaddr4:addr4]; if (afPtr) *afPtr = AF_INET; return YES; } } else if (addrX->sa_family == AF_INET6) { if ([address length] >= sizeof(struct sockaddr_in6)) { const struct sockaddr_in6 *addr6 = (const struct sockaddr_in6 *)(const void *)addrX; if (hostPtr) *hostPtr = [self hostFromSockaddr6:addr6]; if (portPtr) *portPtr = [self portFromSockaddr6:addr6]; if (afPtr) *afPtr = AF_INET6; return YES; } } } if (hostPtr) *hostPtr = nil; if (portPtr) *portPtr = 0; if (afPtr) *afPtr = AF_UNSPEC; return NO; } @end ================================================ FILE: Pods/DZNEmptyDataSet/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2016 Ignacio Romero Zurbuchen iromero@dzen.cl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Pods/DZNEmptyDataSet/README.md ================================================ DZNEmptyDataSet ================= [![Pod Version](http://img.shields.io/cocoapods/v/DZNEmptyDataSet.svg)](http://cocoadocs.org/docsets/DZNEmptyDataSet/) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![License](http://img.shields.io/badge/license-MIT-blue.svg)](http://opensource.org/licenses/MIT) ### Projects using this library [Add your project to the list here](https://github.com/dzenbot/DZNEmptyDataSet/wiki/Projects-using-DZNEmptyDataSet) and provide a (320px wide) render of the result. ### The Empty Data Set Pattern Also known as *[Empty State](http://emptystat.es/)* or *[Blank Slate](http://patternry.com/p=blank-slate/)*. Most applications show lists of content (data sets), which many turn out to be empty at one point, specially for new users with blank accounts. Empty screens create confusion by not being clear about what's going on, if there is an error/bug or if the user is supposed to do something within your app to be able to consume the content. Please read this very interesting article about [*Designing For The Empty States*](http://tympanus.net/codrops/2013/01/09/designing-for-the-empty-states/). ![Screenshots_Row1](https://raw.githubusercontent.com/dzenbot/UITableView-DataSet/master/Examples/Applications/Screenshots/Screenshots_row1.png) ![Screenshots_Row2](https://raw.githubusercontent.com/dzenbot/UITableView-DataSet/master/Examples/Applications/Screenshots/Screenshots_row2.png) (*These are real life examples, available in the 'Applications' sample project*) **[Empty Data Sets](http://pttrns.com/?did=1&scid=30)** are helpful for: * Avoiding white-screens and communicating to your users why the screen is empty. * Calling to action (particularly as an onboarding process). * Avoiding other interruptive mechanisms like showing error alerts. * Being consistent and improving the user experience. * Delivering a brand presence. ### Features * Compatible with UITableView and UICollectionView. Also compatible with UISearchDisplayController and UIScrollView. * Gives multiple possibilities of layout and appearance, by showing an image and/or title label and/or description label and/or button. * Uses NSAttributedString for easier appearance customisation. * Uses auto-layout to automagically center the content to the tableview, with auto-rotation support. Also accepts custom vertical and horizontal alignment. * Background color customisation. * Allows tap gesture on the whole tableview rectangle (useful for resigning first responder or similar actions). * For more advanced customisation, it allows a custom view. * Compatible with Storyboard. * Compatible with iOS 6 or later. * Compatible with iPhone and iPad. * **App Store ready** This library has been designed in a way where you won't need to extend UITableView or UICollectionView class. It will still work when using UITableViewController or UICollectionViewController. By just conforming to DZNEmptyDataSetSource & DZNEmptyDataSetDelegate, you will be able to fully customize the content and appearance of the empty states for your application. ## Installation Available in [CocoaPods](http://cocoapods.org/?q=DZNEmptyDataSet) ```ruby pod 'DZNEmptyDataSet' ``` To integrate DZNEmptyDataSet into your Xcode project using Carthage, specify it in your `Cartfile`: ```ruby github "dzenbot/DZNEmptyDataSet" ``` ## How to use For complete documentation, [visit CocoaPods' auto-generated doc](http://cocoadocs.org/docsets/DZNEmptyDataSet/) ### Import ```objc #import "UIScrollView+EmptyDataSet.h" ``` Unless you are importing as a framework, then do: ```objc #import "" ``` ### Protocol Conformance Conform to datasource and/or delegate. ```objc @interface MainViewController : UITableViewController - (void)viewDidLoad { [super viewDidLoad]; self.tableView.emptyDataSetSource = self; self.tableView.emptyDataSetDelegate = self; // A little trick for removing the cell separators self.tableView.tableFooterView = [UIView new]; } ``` ### Data Source Implementation Return the content you want to show on the empty state, and take advantage of NSAttributedString features to customise the text appearance. The image for the empty state: ```objc - (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView { return [UIImage imageNamed:@"empty_placeholder"]; } ``` The image view animation ```objc - (CAAnimation *)imageAnimationForEmptyDataSet:(UIScrollView *)scrollView { CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath: @"transform"]; animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI_2, 0.0, 0.0, 1.0)]; animation.duration = 0.25; animation.cumulative = YES; animation.repeatCount = MAXFLOAT; return animation; } ``` The attributed string for the title of the empty state: ```objc - (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView { NSString *text = @"Please Allow Photo Access"; NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:18.0f], NSForegroundColorAttributeName: [UIColor darkGrayColor]}; return [[NSAttributedString alloc] initWithString:text attributes:attributes]; } ``` The attributed string for the description of the empty state: ```objc - (NSAttributedString *)descriptionForEmptyDataSet:(UIScrollView *)scrollView { NSString *text = @"This allows you to share photos from your library and save photos to your camera roll."; NSMutableParagraphStyle *paragraph = [NSMutableParagraphStyle new]; paragraph.lineBreakMode = NSLineBreakByWordWrapping; paragraph.alignment = NSTextAlignmentCenter; NSDictionary *attributes = @{NSFontAttributeName: [UIFont systemFontOfSize:14.0f], NSForegroundColorAttributeName: [UIColor lightGrayColor], NSParagraphStyleAttributeName: paragraph}; return [[NSAttributedString alloc] initWithString:text attributes:attributes]; } ``` The attributed string to be used for the specified button state: ```objc - (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state { NSDictionary *attributes = @{NSFontAttributeName: [UIFont boldSystemFontOfSize:17.0f]}; return [[NSAttributedString alloc] initWithString:@"Continue" attributes:attributes]; } ``` or the image to be used for the specified button state: ```objc - (UIImage *)buttonImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state { return [UIImage imageNamed:@"button_image"]; } ``` The background color for the empty state: ```objc - (UIColor *)backgroundColorForEmptyDataSet:(UIScrollView *)scrollView { return [UIColor whiteColor]; } ``` If you need a more complex layout, you can return a custom view instead: ```objc - (UIView *)customViewForEmptyDataSet:(UIScrollView *)scrollView { UIActivityIndicatorView *activityView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; [activityView startAnimating]; return activityView; } ``` Additionally, you can also adjust the vertical alignment of the content view (ie: useful when there is tableHeaderView visible): ```objc - (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView { return -self.tableView.tableHeaderView.frame.size.height/2.0f; } ``` Finally, you can separate components from each other (default separation is 11 pts): ```objc - (CGFloat)spaceHeightForEmptyDataSet:(UIScrollView *)scrollView { return 20.0f; } ``` ### Delegate Implementation Return the behaviours you would expect from the empty states, and receive the user events. Asks to know if the empty state should be rendered and displayed (Default is YES) : ```objc - (BOOL)emptyDataSetShouldDisplay:(UIScrollView *)scrollView { return YES; } ``` Asks for interaction permission (Default is YES) : ```objc - (BOOL)emptyDataSetShouldAllowTouch:(UIScrollView *)scrollView { return YES; } ``` Asks for scrolling permission (Default is NO) : ```objc - (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView { return YES; } ``` Asks for image view animation permission (Default is NO) : ```objc - (BOOL) emptyDataSetShouldAllowImageViewAnimate:(UIScrollView *)scrollView { return YES; } ``` Notifies when the dataset view was tapped: ```objc - (void)emptyDataSet:(UIScrollView *)scrollView didTapView:(UIView *)view { // Do something } ``` Notifies when the data set call to action button was tapped: ```objc - (void)emptyDataSet:(UIScrollView *)scrollView didTapButton:(UIButton *)button { // Do something } ``` ### Refresh layout If you need to refresh the empty state layout, simply call: ```objc [self.tableView reloadData]; ``` or ```objc [self.collectionView reloadData]; ``` depending of which you are using. ### Force layout update You can also call `[self.tableView reloadEmptyDataSet]` to invalidate the current empty state layout and trigger a layout update, bypassing `-reloadData`. This might be useful if you have a lot of logic on your data source that you want to avoid calling, when not needed. `[self.scrollView reloadEmptyDataSet]` is the only way to refresh content when using with UIScrollView. ## Sample projects #### Applications This project replicates several popular application's empty states (~20) with their exact content and appearance, such as Airbnb, Dropbox, Facebook, Foursquare, and many others. See how easy and flexible it is to customize the appearance of your empty states. You can also use this project as a playground to test things. #### Countries This project shows a list of the world countries loaded from CoreData. It uses NSFecthedResultController for filtering search. When searching and no content is matched, a simple empty state is shown. See how to interact between the UITableViewDataSource and the DZNEmptyDataSetSource protocols, while using a typical CoreData stack. #### Colors This project is a simple example of how this library also works with UICollectionView and UISearchDisplayController, while using Storyboards. ## Collaboration Feel free to collaborate with ideas, issues and/or pull requests. ## License (The MIT License) Copyright (c) 2016 Ignacio Romero Zurbuchen iromero@dzen.cl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Pods/DZNEmptyDataSet/Source/UIScrollView+EmptyDataSet.h ================================================ // // UIScrollView+EmptyDataSet.h // DZNEmptyDataSet // https://github.com/dzenbot/DZNEmptyDataSet // // Created by Ignacio Romero Zurbuchen on 6/20/14. // Copyright (c) 2016 DZN Labs. All rights reserved. // Licence: MIT-Licence // #import @protocol DZNEmptyDataSetSource; @protocol DZNEmptyDataSetDelegate; #define DZNEmptyDataSetDeprecated(instead) DEPRECATED_MSG_ATTRIBUTE(" Use " # instead " instead") /** A drop-in UITableView/UICollectionView superclass category for showing empty datasets whenever the view has no content to display. @discussion It will work automatically, by just conforming to DZNEmptyDataSetSource, and returning the data you want to show. */ @interface UIScrollView (EmptyDataSet) /** The empty datasets data source. */ @property (nonatomic, weak) IBOutlet id emptyDataSetSource; /** The empty datasets delegate. */ @property (nonatomic, weak) IBOutlet id emptyDataSetDelegate; /** YES if any empty dataset is visible. */ @property (nonatomic, readonly, getter = isEmptyDataSetVisible) BOOL emptyDataSetVisible; /** Reloads the empty dataset content receiver. @discussion Call this method to force all the data to refresh. Calling -reloadData is similar, but this forces only the empty dataset to reload, not the entire table view or collection view. */ - (void)reloadEmptyDataSet; @end /** The object that acts as the data source of the empty datasets. @discussion The data source must adopt the DZNEmptyDataSetSource protocol. The data source is not retained. All data source methods are optional. */ @protocol DZNEmptyDataSetSource @optional /** Asks the data source for the title of the dataset. The dataset uses a fixed font style by default, if no attributes are set. If you want a different font style, return a attributed string. @param scrollView A scrollView subclass informing the data source. @return An attributed string for the dataset title, combining font, text color, text pararaph style, etc. */ - (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView; /** Asks the data source for the description of the dataset. The dataset uses a fixed font style by default, if no attributes are set. If you want a different font style, return a attributed string. @param scrollView A scrollView subclass informing the data source. @return An attributed string for the dataset description text, combining font, text color, text pararaph style, etc. */ - (NSAttributedString *)descriptionForEmptyDataSet:(UIScrollView *)scrollView; /** Asks the data source for the image of the dataset. @param scrollView A scrollView subclass informing the data source. @return An image for the dataset. */ - (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView; /** Asks the data source for a tint color of the image dataset. Default is nil. @param scrollView A scrollView subclass object informing the data source. @return A color to tint the image of the dataset. */ - (UIColor *)imageTintColorForEmptyDataSet:(UIScrollView *)scrollView; /** * Asks the data source for the image animation of the dataset. * * @param scrollView A scrollView subclass object informing the delegate. * * @return image animation */ - (CAAnimation *) imageAnimationForEmptyDataSet:(UIScrollView *) scrollView; /** Asks the data source for the title to be used for the specified button state. The dataset uses a fixed font style by default, if no attributes are set. If you want a different font style, return a attributed string. @param scrollView A scrollView subclass object informing the data source. @param state The state that uses the specified title. The possible values are described in UIControlState. @return An attributed string for the dataset button title, combining font, text color, text pararaph style, etc. */ - (NSAttributedString *)buttonTitleForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state; /** Asks the data source for the image to be used for the specified button state. This method will override buttonTitleForEmptyDataSet:forState: and present the image only without any text. @param scrollView A scrollView subclass object informing the data source. @param state The state that uses the specified title. The possible values are described in UIControlState. @return An image for the dataset button imageview. */ - (UIImage *)buttonImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state; /** Asks the data source for a background image to be used for the specified button state. There is no default style for this call. @param scrollView A scrollView subclass informing the data source. @param state The state that uses the specified image. The values are described in UIControlState. @return An attributed string for the dataset button title, combining font, text color, text pararaph style, etc. */ - (UIImage *)buttonBackgroundImageForEmptyDataSet:(UIScrollView *)scrollView forState:(UIControlState)state; /** Asks the data source for the background color of the dataset. Default is clear color. @param scrollView A scrollView subclass object informing the data source. @return A color to be applied to the dataset background view. */ - (UIColor *)backgroundColorForEmptyDataSet:(UIScrollView *)scrollView; /** Asks the data source for a custom view to be displayed instead of the default views such as labels, imageview and button. Default is nil. Use this method to show an activity view indicator for loading feedback, or for complete custom empty data set. Returning a custom view will ignore -offsetForEmptyDataSet and -spaceHeightForEmptyDataSet configurations. @param scrollView A scrollView subclass object informing the delegate. @return The custom view. */ - (UIView *)customViewForEmptyDataSet:(UIScrollView *)scrollView; /** Asks the data source for a offset for vertical and horizontal alignment of the content. Default is CGPointZero. @param scrollView A scrollView subclass object informing the delegate. @return The offset for vertical and horizontal alignment. */ - (CGPoint)offsetForEmptyDataSet:(UIScrollView *)scrollView DZNEmptyDataSetDeprecated(-verticalOffsetForEmptyDataSet:); - (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView; /** Asks the data source for a vertical space between elements. Default is 11 pts. @param scrollView A scrollView subclass object informing the delegate. @return The space height between elements. */ - (CGFloat)spaceHeightForEmptyDataSet:(UIScrollView *)scrollView; @end /** The object that acts as the delegate of the empty datasets. @discussion The delegate can adopt the DZNEmptyDataSetDelegate protocol. The delegate is not retained. All delegate methods are optional. @discussion All delegate methods are optional. Use this delegate for receiving action callbacks. */ @protocol DZNEmptyDataSetDelegate @optional /** Asks the delegate to know if the empty dataset should fade in when displayed. Default is YES. @param scrollView A scrollView subclass object informing the delegate. @return YES if the empty dataset should fade in. */ - (BOOL)emptyDataSetShouldFadeIn:(UIScrollView *)scrollView; /** Asks the delegate to know if the empty dataset should still be displayed when the amount of items is more than 0. Default is NO @param scrollView A scrollView subclass object informing the delegate. @return YES if empty dataset should be forced to display */ - (BOOL)emptyDataSetShouldBeForcedToDisplay:(UIScrollView *)scrollView; /** Asks the delegate to know if the empty dataset should be rendered and displayed. Default is YES. @param scrollView A scrollView subclass object informing the delegate. @return YES if the empty dataset should show. */ - (BOOL)emptyDataSetShouldDisplay:(UIScrollView *)scrollView; /** Asks the delegate for touch permission. Default is YES. @param scrollView A scrollView subclass object informing the delegate. @return YES if the empty dataset receives touch gestures. */ - (BOOL)emptyDataSetShouldAllowTouch:(UIScrollView *)scrollView; /** Asks the delegate for scroll permission. Default is NO. @param scrollView A scrollView subclass object informing the delegate. @return YES if the empty dataset is allowed to be scrollable. */ - (BOOL)emptyDataSetShouldAllowScroll:(UIScrollView *)scrollView; /** Asks the delegate for image view animation permission. Default is NO. Make sure to return a valid CAAnimation object from imageAnimationForEmptyDataSet: @param scrollView A scrollView subclass object informing the delegate. @return YES if the empty dataset is allowed to animate */ - (BOOL)emptyDataSetShouldAnimateImageView:(UIScrollView *)scrollView; /** Tells the delegate that the empty dataset view was tapped. Use this method either to resignFirstResponder of a textfield or searchBar. @param scrollView A scrollView subclass informing the delegate. */ - (void)emptyDataSetDidTapView:(UIScrollView *)scrollView DZNEmptyDataSetDeprecated(-emptyDataSet:didTapView:); /** Tells the delegate that the action button was tapped. @param scrollView A scrollView subclass informing the delegate. */ - (void)emptyDataSetDidTapButton:(UIScrollView *)scrollView DZNEmptyDataSetDeprecated(-emptyDataSet:didTapButton:); /** Tells the delegate that the empty dataset view was tapped. Use this method either to resignFirstResponder of a textfield or searchBar. @param scrollView A scrollView subclass informing the delegate. @param view the view tapped by the user */ - (void)emptyDataSet:(UIScrollView *)scrollView didTapView:(UIView *)view; /** Tells the delegate that the action button was tapped. @param scrollView A scrollView subclass informing the delegate. @param button the button tapped by the user */ - (void)emptyDataSet:(UIScrollView *)scrollView didTapButton:(UIButton *)button; /** Tells the delegate that the empty data set will appear. @param scrollView A scrollView subclass informing the delegate. */ - (void)emptyDataSetWillAppear:(UIScrollView *)scrollView; /** Tells the delegate that the empty data set did appear. @param scrollView A scrollView subclass informing the delegate. */ - (void)emptyDataSetDidAppear:(UIScrollView *)scrollView; /** Tells the delegate that the empty data set will disappear. @param scrollView A scrollView subclass informing the delegate. */ - (void)emptyDataSetWillDisappear:(UIScrollView *)scrollView; /** Tells the delegate that the empty data set did disappear. @param scrollView A scrollView subclass informing the delegate. */ - (void)emptyDataSetDidDisappear:(UIScrollView *)scrollView; @end #undef DZNEmptyDataSetDeprecated ================================================ FILE: Pods/DZNEmptyDataSet/Source/UIScrollView+EmptyDataSet.m ================================================ // // UIScrollView+EmptyDataSet.m // DZNEmptyDataSet // https://github.com/dzenbot/DZNEmptyDataSet // // Created by Ignacio Romero Zurbuchen on 6/20/14. // Copyright (c) 2016 DZN Labs. All rights reserved. // Licence: MIT-Licence // #import "UIScrollView+EmptyDataSet.h" #import @interface UIView (DZNConstraintBasedLayoutExtensions) - (NSLayoutConstraint *)equallyRelatedConstraintWithView:(UIView *)view attribute:(NSLayoutAttribute)attribute; @end @interface DZNWeakObjectContainer : NSObject @property (nonatomic, readonly, weak) id weakObject; - (instancetype)initWithWeakObject:(id)object; @end @interface DZNEmptyDataSetView : UIView @property (nonatomic, readonly) UIView *contentView; @property (nonatomic, readonly) UILabel *titleLabel; @property (nonatomic, readonly) UILabel *detailLabel; @property (nonatomic, readonly) UIImageView *imageView; @property (nonatomic, readonly) UIButton *button; @property (nonatomic, strong) UIView *customView; @property (nonatomic, strong) UITapGestureRecognizer *tapGesture; @property (nonatomic, assign) CGFloat verticalOffset; @property (nonatomic, assign) CGFloat verticalSpace; @property (nonatomic, assign) BOOL fadeInOnDisplay; - (void)setupConstraints; - (void)prepareForReuse; @end #pragma mark - UIScrollView+EmptyDataSet static char const * const kEmptyDataSetSource = "emptyDataSetSource"; static char const * const kEmptyDataSetDelegate = "emptyDataSetDelegate"; static char const * const kEmptyDataSetView = "emptyDataSetView"; #define kEmptyImageViewAnimationKey @"com.dzn.emptyDataSet.imageViewAnimation" @interface UIScrollView () @property (nonatomic, readonly) DZNEmptyDataSetView *emptyDataSetView; @end @implementation UIScrollView (DZNEmptyDataSet) #pragma mark - Getters (Public) - (id)emptyDataSetSource { DZNWeakObjectContainer *container = objc_getAssociatedObject(self, kEmptyDataSetSource); return container.weakObject; } - (id)emptyDataSetDelegate { DZNWeakObjectContainer *container = objc_getAssociatedObject(self, kEmptyDataSetDelegate); return container.weakObject; } - (BOOL)isEmptyDataSetVisible { UIView *view = objc_getAssociatedObject(self, kEmptyDataSetView); return view ? !view.hidden : NO; } #pragma mark - Getters (Private) - (DZNEmptyDataSetView *)emptyDataSetView { DZNEmptyDataSetView *view = objc_getAssociatedObject(self, kEmptyDataSetView); if (!view) { view = [DZNEmptyDataSetView new]; view.autoresizingMask = UIViewAutoresizingFlexibleWidth|UIViewAutoresizingFlexibleHeight; view.hidden = YES; view.tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(dzn_didTapContentView:)]; view.tapGesture.delegate = self; [view addGestureRecognizer:view.tapGesture]; [self setEmptyDataSetView:view]; } return view; } - (BOOL)dzn_canDisplay { if (self.emptyDataSetSource && [self.emptyDataSetSource conformsToProtocol:@protocol(DZNEmptyDataSetSource)]) { if ([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]] || [self isKindOfClass:[UIScrollView class]]) { return YES; } } return NO; } - (NSInteger)dzn_itemsCount { NSInteger items = 0; // UIScollView doesn't respond to 'dataSource' so let's exit if (![self respondsToSelector:@selector(dataSource)]) { return items; } // UITableView support if ([self isKindOfClass:[UITableView class]]) { UITableView *tableView = (UITableView *)self; id dataSource = tableView.dataSource; NSInteger sections = 1; if (dataSource && [dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { sections = [dataSource numberOfSectionsInTableView:tableView]; } if (dataSource && [dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) { for (NSInteger section = 0; section < sections; section++) { items += [dataSource tableView:tableView numberOfRowsInSection:section]; } } } // UICollectionView support else if ([self isKindOfClass:[UICollectionView class]]) { UICollectionView *collectionView = (UICollectionView *)self; id dataSource = collectionView.dataSource; NSInteger sections = 1; if (dataSource && [dataSource respondsToSelector:@selector(numberOfSectionsInCollectionView:)]) { sections = [dataSource numberOfSectionsInCollectionView:collectionView]; } if (dataSource && [dataSource respondsToSelector:@selector(collectionView:numberOfItemsInSection:)]) { for (NSInteger section = 0; section < sections; section++) { items += [dataSource collectionView:collectionView numberOfItemsInSection:section]; } } } return items; } #pragma mark - Data Source Getters - (NSAttributedString *)dzn_titleLabelString { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(titleForEmptyDataSet:)]) { NSAttributedString *string = [self.emptyDataSetSource titleForEmptyDataSet:self]; if (string) NSAssert([string isKindOfClass:[NSAttributedString class]], @"You must return a valid NSAttributedString object for -titleForEmptyDataSet:"); return string; } return nil; } - (NSAttributedString *)dzn_detailLabelString { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(descriptionForEmptyDataSet:)]) { NSAttributedString *string = [self.emptyDataSetSource descriptionForEmptyDataSet:self]; if (string) NSAssert([string isKindOfClass:[NSAttributedString class]], @"You must return a valid NSAttributedString object for -descriptionForEmptyDataSet:"); return string; } return nil; } - (UIImage *)dzn_image { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(imageForEmptyDataSet:)]) { UIImage *image = [self.emptyDataSetSource imageForEmptyDataSet:self]; if (image) NSAssert([image isKindOfClass:[UIImage class]], @"You must return a valid UIImage object for -imageForEmptyDataSet:"); return image; } return nil; } - (CAAnimation *)dzn_imageAnimation { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(imageAnimationForEmptyDataSet:)]) { CAAnimation *imageAnimation = [self.emptyDataSetSource imageAnimationForEmptyDataSet:self]; if (imageAnimation) NSAssert([imageAnimation isKindOfClass:[CAAnimation class]], @"You must return a valid CAAnimation object for -imageAnimationForEmptyDataSet:"); return imageAnimation; } return nil; } - (UIColor *)dzn_imageTintColor { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(imageTintColorForEmptyDataSet:)]) { UIColor *color = [self.emptyDataSetSource imageTintColorForEmptyDataSet:self]; if (color) NSAssert([color isKindOfClass:[UIColor class]], @"You must return a valid UIColor object for -imageTintColorForEmptyDataSet:"); return color; } return nil; } - (NSAttributedString *)dzn_buttonTitleForState:(UIControlState)state { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(buttonTitleForEmptyDataSet:forState:)]) { NSAttributedString *string = [self.emptyDataSetSource buttonTitleForEmptyDataSet:self forState:state]; if (string) NSAssert([string isKindOfClass:[NSAttributedString class]], @"You must return a valid NSAttributedString object for -buttonTitleForEmptyDataSet:forState:"); return string; } return nil; } - (UIImage *)dzn_buttonImageForState:(UIControlState)state { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(buttonImageForEmptyDataSet:forState:)]) { UIImage *image = [self.emptyDataSetSource buttonImageForEmptyDataSet:self forState:state]; if (image) NSAssert([image isKindOfClass:[UIImage class]], @"You must return a valid UIImage object for -buttonImageForEmptyDataSet:forState:"); return image; } return nil; } - (UIImage *)dzn_buttonBackgroundImageForState:(UIControlState)state { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(buttonBackgroundImageForEmptyDataSet:forState:)]) { UIImage *image = [self.emptyDataSetSource buttonBackgroundImageForEmptyDataSet:self forState:state]; if (image) NSAssert([image isKindOfClass:[UIImage class]], @"You must return a valid UIImage object for -buttonBackgroundImageForEmptyDataSet:forState:"); return image; } return nil; } - (UIColor *)dzn_dataSetBackgroundColor { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(backgroundColorForEmptyDataSet:)]) { UIColor *color = [self.emptyDataSetSource backgroundColorForEmptyDataSet:self]; if (color) NSAssert([color isKindOfClass:[UIColor class]], @"You must return a valid UIColor object for -backgroundColorForEmptyDataSet:"); return color; } return [UIColor clearColor]; } - (UIView *)dzn_customView { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(customViewForEmptyDataSet:)]) { UIView *view = [self.emptyDataSetSource customViewForEmptyDataSet:self]; if (view) NSAssert([view isKindOfClass:[UIView class]], @"You must return a valid UIView object for -customViewForEmptyDataSet:"); return view; } return nil; } - (CGFloat)dzn_verticalOffset { CGFloat offset = 0.0; if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(verticalOffsetForEmptyDataSet:)]) { offset = [self.emptyDataSetSource verticalOffsetForEmptyDataSet:self]; } return offset; } - (CGFloat)dzn_verticalSpace { if (self.emptyDataSetSource && [self.emptyDataSetSource respondsToSelector:@selector(spaceHeightForEmptyDataSet:)]) { return [self.emptyDataSetSource spaceHeightForEmptyDataSet:self]; } return 0.0; } #pragma mark - Delegate Getters & Events (Private) - (BOOL)dzn_shouldFadeIn { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldFadeIn:)]) { return [self.emptyDataSetDelegate emptyDataSetShouldFadeIn:self]; } return YES; } - (BOOL)dzn_shouldDisplay { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldDisplay:)]) { return [self.emptyDataSetDelegate emptyDataSetShouldDisplay:self]; } return YES; } - (BOOL)dzn_shouldBeForcedToDisplay { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldBeForcedToDisplay:)]) { return [self.emptyDataSetDelegate emptyDataSetShouldBeForcedToDisplay:self]; } return NO; } - (BOOL)dzn_isTouchAllowed { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldAllowTouch:)]) { return [self.emptyDataSetDelegate emptyDataSetShouldAllowTouch:self]; } return YES; } - (BOOL)dzn_isScrollAllowed { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldAllowScroll:)]) { return [self.emptyDataSetDelegate emptyDataSetShouldAllowScroll:self]; } return NO; } - (BOOL)dzn_isImageViewAnimateAllowed { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetShouldAnimateImageView:)]) { return [self.emptyDataSetDelegate emptyDataSetShouldAnimateImageView:self]; } return NO; } - (void)dzn_willAppear { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetWillAppear:)]) { [self.emptyDataSetDelegate emptyDataSetWillAppear:self]; } } - (void)dzn_didAppear { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidAppear:)]) { [self.emptyDataSetDelegate emptyDataSetDidAppear:self]; } } - (void)dzn_willDisappear { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetWillDisappear:)]) { [self.emptyDataSetDelegate emptyDataSetWillDisappear:self]; } } - (void)dzn_didDisappear { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidDisappear:)]) { [self.emptyDataSetDelegate emptyDataSetDidDisappear:self]; } } - (void)dzn_didTapContentView:(id)sender { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSet:didTapView:)]) { [self.emptyDataSetDelegate emptyDataSet:self didTapView:sender]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" else if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidTapView:)]) { [self.emptyDataSetDelegate emptyDataSetDidTapView:self]; } #pragma clang diagnostic pop } - (void)dzn_didTapDataButton:(id)sender { if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSet:didTapButton:)]) { [self.emptyDataSetDelegate emptyDataSet:self didTapButton:sender]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" else if (self.emptyDataSetDelegate && [self.emptyDataSetDelegate respondsToSelector:@selector(emptyDataSetDidTapButton:)]) { [self.emptyDataSetDelegate emptyDataSetDidTapButton:self]; } #pragma clang diagnostic pop } #pragma mark - Setters (Public) - (void)setEmptyDataSetSource:(id)datasource { if (!datasource || ![self dzn_canDisplay]) { [self dzn_invalidate]; } objc_setAssociatedObject(self, kEmptyDataSetSource, [[DZNWeakObjectContainer alloc] initWithWeakObject:datasource], OBJC_ASSOCIATION_RETAIN_NONATOMIC); // We add method sizzling for injecting -dzn_reloadData implementation to the native -reloadData implementation [self swizzleIfPossible:@selector(reloadData)]; // Exclusively for UITableView, we also inject -dzn_reloadData to -endUpdates if ([self isKindOfClass:[UITableView class]]) { [self swizzleIfPossible:@selector(endUpdates)]; } } - (void)setEmptyDataSetDelegate:(id)delegate { if (!delegate) { [self dzn_invalidate]; } objc_setAssociatedObject(self, kEmptyDataSetDelegate, [[DZNWeakObjectContainer alloc] initWithWeakObject:delegate], OBJC_ASSOCIATION_RETAIN_NONATOMIC); } #pragma mark - Setters (Private) - (void)setEmptyDataSetView:(DZNEmptyDataSetView *)view { objc_setAssociatedObject(self, kEmptyDataSetView, view, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } #pragma mark - Reload APIs (Public) - (void)reloadEmptyDataSet { [self dzn_reloadEmptyDataSet]; } #pragma mark - Reload APIs (Private) - (void)dzn_reloadEmptyDataSet { if (![self dzn_canDisplay]) { return; } if (([self dzn_shouldDisplay] && [self dzn_itemsCount] == 0) || [self dzn_shouldBeForcedToDisplay]) { // Notifies that the empty dataset view will appear [self dzn_willAppear]; DZNEmptyDataSetView *view = self.emptyDataSetView; if (!view.superview) { // Send the view all the way to the back, in case a header and/or footer is present, as well as for sectionHeaders or any other content if (([self isKindOfClass:[UITableView class]] || [self isKindOfClass:[UICollectionView class]]) && self.subviews.count > 1) { [self insertSubview:view atIndex:0]; } else { [self addSubview:view]; } } // Removing view resetting the view and its constraints it very important to guarantee a good state [view prepareForReuse]; UIView *customView = [self dzn_customView]; // If a non-nil custom view is available, let's configure it instead if (customView) { view.customView = customView; } else { // Get the data from the data source NSAttributedString *titleLabelString = [self dzn_titleLabelString]; NSAttributedString *detailLabelString = [self dzn_detailLabelString]; UIImage *buttonImage = [self dzn_buttonImageForState:UIControlStateNormal]; NSAttributedString *buttonTitle = [self dzn_buttonTitleForState:UIControlStateNormal]; UIImage *image = [self dzn_image]; UIColor *imageTintColor = [self dzn_imageTintColor]; UIImageRenderingMode renderingMode = imageTintColor ? UIImageRenderingModeAlwaysTemplate : UIImageRenderingModeAlwaysOriginal; view.verticalSpace = [self dzn_verticalSpace]; // Configure Image if (image) { if ([image respondsToSelector:@selector(imageWithRenderingMode:)]) { view.imageView.image = [image imageWithRenderingMode:renderingMode]; view.imageView.tintColor = imageTintColor; } else { // iOS 6 fallback: insert code to convert imaged if needed view.imageView.image = image; } } // Configure title label if (titleLabelString) { view.titleLabel.attributedText = titleLabelString; } // Configure detail label if (detailLabelString) { view.detailLabel.attributedText = detailLabelString; } // Configure button if (buttonImage) { [view.button setImage:buttonImage forState:UIControlStateNormal]; [view.button setImage:[self dzn_buttonImageForState:UIControlStateHighlighted] forState:UIControlStateHighlighted]; } else if (buttonTitle) { [view.button setAttributedTitle:buttonTitle forState:UIControlStateNormal]; [view.button setAttributedTitle:[self dzn_buttonTitleForState:UIControlStateHighlighted] forState:UIControlStateHighlighted]; [view.button setBackgroundImage:[self dzn_buttonBackgroundImageForState:UIControlStateNormal] forState:UIControlStateNormal]; [view.button setBackgroundImage:[self dzn_buttonBackgroundImageForState:UIControlStateHighlighted] forState:UIControlStateHighlighted]; } } // Configure offset view.verticalOffset = [self dzn_verticalOffset]; // Configure the empty dataset view view.backgroundColor = [self dzn_dataSetBackgroundColor]; view.hidden = NO; view.clipsToBounds = YES; // Configure empty dataset userInteraction permission view.userInteractionEnabled = [self dzn_isTouchAllowed]; // Configure empty dataset fade in display view.fadeInOnDisplay = [self dzn_shouldFadeIn]; [view setupConstraints]; [UIView performWithoutAnimation:^{ [view layoutIfNeeded]; }]; // Configure scroll permission self.scrollEnabled = [self dzn_isScrollAllowed]; // Configure image view animation if ([self dzn_isImageViewAnimateAllowed]) { CAAnimation *animation = [self dzn_imageAnimation]; if (animation) { [self.emptyDataSetView.imageView.layer addAnimation:animation forKey:kEmptyImageViewAnimationKey]; } } else if ([self.emptyDataSetView.imageView.layer animationForKey:kEmptyImageViewAnimationKey]) { [self.emptyDataSetView.imageView.layer removeAnimationForKey:kEmptyImageViewAnimationKey]; } // Notifies that the empty dataset view did appear [self dzn_didAppear]; } else if (self.isEmptyDataSetVisible) { [self dzn_invalidate]; } } - (void)dzn_invalidate { // Notifies that the empty dataset view will disappear [self dzn_willDisappear]; if (self.emptyDataSetView) { [self.emptyDataSetView prepareForReuse]; [self.emptyDataSetView removeFromSuperview]; [self setEmptyDataSetView:nil]; } self.scrollEnabled = YES; // Notifies that the empty dataset view did disappear [self dzn_didDisappear]; } #pragma mark - Method Swizzling static NSMutableDictionary *_impLookupTable; static NSString *const DZNSwizzleInfoPointerKey = @"pointer"; static NSString *const DZNSwizzleInfoOwnerKey = @"owner"; static NSString *const DZNSwizzleInfoSelectorKey = @"selector"; // Based on Bryce Buchanan's swizzling technique http://blog.newrelic.com/2014/04/16/right-way-to-swizzle/ // And Juzzin's ideas https://github.com/juzzin/JUSEmptyViewController void dzn_original_implementation(id self, SEL _cmd) { // Fetch original implementation from lookup table Class baseClass = dzn_baseClassToSwizzleForTarget(self); NSString *key = dzn_implementationKey(baseClass, _cmd); NSDictionary *swizzleInfo = [_impLookupTable objectForKey:key]; NSValue *impValue = [swizzleInfo valueForKey:DZNSwizzleInfoPointerKey]; IMP impPointer = [impValue pointerValue]; // We then inject the additional implementation for reloading the empty dataset // Doing it before calling the original implementation does update the 'isEmptyDataSetVisible' flag on time. [self dzn_reloadEmptyDataSet]; // If found, call original implementation if (impPointer) { ((void(*)(id,SEL))impPointer)(self,_cmd); } } NSString *dzn_implementationKey(Class class, SEL selector) { if (!class || !selector) { return nil; } NSString *className = NSStringFromClass([class class]); NSString *selectorName = NSStringFromSelector(selector); return [NSString stringWithFormat:@"%@_%@",className,selectorName]; } Class dzn_baseClassToSwizzleForTarget(id target) { if ([target isKindOfClass:[UITableView class]]) { return [UITableView class]; } else if ([target isKindOfClass:[UICollectionView class]]) { return [UICollectionView class]; } else if ([target isKindOfClass:[UIScrollView class]]) { return [UIScrollView class]; } return nil; } - (void)swizzleIfPossible:(SEL)selector { // Check if the target responds to selector if (![self respondsToSelector:selector]) { return; } // Create the lookup table if (!_impLookupTable) { _impLookupTable = [[NSMutableDictionary alloc] initWithCapacity:3]; // 3 represent the supported base classes } // We make sure that setImplementation is called once per class kind, UITableView or UICollectionView. for (NSDictionary *info in [_impLookupTable allValues]) { Class class = [info objectForKey:DZNSwizzleInfoOwnerKey]; NSString *selectorName = [info objectForKey:DZNSwizzleInfoSelectorKey]; if ([selectorName isEqualToString:NSStringFromSelector(selector)]) { if ([self isKindOfClass:class]) { return; } } } Class baseClass = dzn_baseClassToSwizzleForTarget(self); NSString *key = dzn_implementationKey(baseClass, selector); NSValue *impValue = [[_impLookupTable objectForKey:key] valueForKey:DZNSwizzleInfoPointerKey]; // If the implementation for this class already exist, skip!! if (impValue || !key || !baseClass) { return; } // Swizzle by injecting additional implementation Method method = class_getInstanceMethod(baseClass, selector); IMP dzn_newImplementation = method_setImplementation(method, (IMP)dzn_original_implementation); // Store the new implementation in the lookup table NSDictionary *swizzledInfo = @{DZNSwizzleInfoOwnerKey: baseClass, DZNSwizzleInfoSelectorKey: NSStringFromSelector(selector), DZNSwizzleInfoPointerKey: [NSValue valueWithPointer:dzn_newImplementation]}; [_impLookupTable setObject:swizzledInfo forKey:key]; } #pragma mark - UIGestureRecognizerDelegate Methods - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { if ([gestureRecognizer.view isEqual:self.emptyDataSetView]) { return [self dzn_isTouchAllowed]; } return [super gestureRecognizerShouldBegin:gestureRecognizer]; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { UIGestureRecognizer *tapGesture = self.emptyDataSetView.tapGesture; if ([gestureRecognizer isEqual:tapGesture] || [otherGestureRecognizer isEqual:tapGesture]) { return YES; } // defer to emptyDataSetDelegate's implementation if available if ( (self.emptyDataSetDelegate != (id)self) && [self.emptyDataSetDelegate respondsToSelector:@selector(gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer:)]) { return [(id)self.emptyDataSetDelegate gestureRecognizer:gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:otherGestureRecognizer]; } return NO; } @end #pragma mark - DZNEmptyDataSetView @interface DZNEmptyDataSetView () @end @implementation DZNEmptyDataSetView @synthesize contentView = _contentView; @synthesize titleLabel = _titleLabel, detailLabel = _detailLabel, imageView = _imageView, button = _button; #pragma mark - Initialization Methods - (instancetype)init { self = [super init]; if (self) { [self addSubview:self.contentView]; } return self; } - (void)didMoveToSuperview { self.frame = self.superview.bounds; void(^fadeInBlock)(void) = ^{_contentView.alpha = 1.0;}; if (self.fadeInOnDisplay) { [UIView animateWithDuration:0.25 animations:fadeInBlock completion:NULL]; } else { fadeInBlock(); } } #pragma mark - Getters - (UIView *)contentView { if (!_contentView) { _contentView = [UIView new]; _contentView.translatesAutoresizingMaskIntoConstraints = NO; _contentView.backgroundColor = [UIColor clearColor]; _contentView.userInteractionEnabled = YES; _contentView.alpha = 0; } return _contentView; } - (UIImageView *)imageView { if (!_imageView) { _imageView = [UIImageView new]; _imageView.translatesAutoresizingMaskIntoConstraints = NO; _imageView.backgroundColor = [UIColor clearColor]; _imageView.contentMode = UIViewContentModeScaleAspectFit; _imageView.userInteractionEnabled = NO; _imageView.accessibilityIdentifier = @"empty set background image"; [_contentView addSubview:_imageView]; } return _imageView; } - (UILabel *)titleLabel { if (!_titleLabel) { _titleLabel = [UILabel new]; _titleLabel.translatesAutoresizingMaskIntoConstraints = NO; _titleLabel.backgroundColor = [UIColor clearColor]; _titleLabel.font = [UIFont systemFontOfSize:27.0]; _titleLabel.textColor = [UIColor colorWithWhite:0.6 alpha:1.0]; _titleLabel.textAlignment = NSTextAlignmentCenter; _titleLabel.lineBreakMode = NSLineBreakByWordWrapping; _titleLabel.numberOfLines = 0; _titleLabel.accessibilityIdentifier = @"empty set title"; [_contentView addSubview:_titleLabel]; } return _titleLabel; } - (UILabel *)detailLabel { if (!_detailLabel) { _detailLabel = [UILabel new]; _detailLabel.translatesAutoresizingMaskIntoConstraints = NO; _detailLabel.backgroundColor = [UIColor clearColor]; _detailLabel.font = [UIFont systemFontOfSize:17.0]; _detailLabel.textColor = [UIColor colorWithWhite:0.6 alpha:1.0]; _detailLabel.textAlignment = NSTextAlignmentCenter; _detailLabel.lineBreakMode = NSLineBreakByWordWrapping; _detailLabel.numberOfLines = 0; _detailLabel.accessibilityIdentifier = @"empty set detail label"; [_contentView addSubview:_detailLabel]; } return _detailLabel; } - (UIButton *)button { if (!_button) { _button = [UIButton buttonWithType:UIButtonTypeCustom]; _button.translatesAutoresizingMaskIntoConstraints = NO; _button.backgroundColor = [UIColor clearColor]; _button.contentHorizontalAlignment = UIControlContentHorizontalAlignmentCenter; _button.contentVerticalAlignment = UIControlContentVerticalAlignmentCenter; _button.accessibilityIdentifier = @"empty set button"; [_button addTarget:self action:@selector(didTapButton:) forControlEvents:UIControlEventTouchUpInside]; [_contentView addSubview:_button]; } return _button; } - (BOOL)canShowImage { return (_imageView.image && _imageView.superview); } - (BOOL)canShowTitle { return (_titleLabel.attributedText.string.length > 0 && _titleLabel.superview); } - (BOOL)canShowDetail { return (_detailLabel.attributedText.string.length > 0 && _detailLabel.superview); } - (BOOL)canShowButton { if ([_button attributedTitleForState:UIControlStateNormal].string.length > 0 || [_button imageForState:UIControlStateNormal]) { return (_button.superview != nil); } return NO; } #pragma mark - Setters - (void)setCustomView:(UIView *)view { if (!view) { return; } if (_customView) { [_customView removeFromSuperview]; _customView = nil; } _customView = view; _customView.translatesAutoresizingMaskIntoConstraints = NO; [self.contentView addSubview:_customView]; } #pragma mark - Action Methods - (void)didTapButton:(id)sender { SEL selector = NSSelectorFromString(@"dzn_didTapDataButton:"); if ([self.superview respondsToSelector:selector]) { [self.superview performSelector:selector withObject:sender afterDelay:0.0f]; } } - (void)removeAllConstraints { [self removeConstraints:self.constraints]; [_contentView removeConstraints:_contentView.constraints]; } - (void)prepareForReuse { [self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)]; _titleLabel = nil; _detailLabel = nil; _imageView = nil; _button = nil; _customView = nil; [self removeAllConstraints]; } #pragma mark - Auto-Layout Configuration - (void)setupConstraints { // First, configure the content view constaints // The content view must alway be centered to its superview NSLayoutConstraint *centerXConstraint = [self equallyRelatedConstraintWithView:self.contentView attribute:NSLayoutAttributeCenterX]; NSLayoutConstraint *centerYConstraint = [self equallyRelatedConstraintWithView:self.contentView attribute:NSLayoutAttributeCenterY]; [self addConstraint:centerXConstraint]; [self addConstraint:centerYConstraint]; [self addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[contentView]|" options:0 metrics:nil views:@{@"contentView": self.contentView}]]; // When a custom offset is available, we adjust the vertical constraints' constants if (self.verticalOffset != 0 && self.constraints.count > 0) { centerYConstraint.constant = self.verticalOffset; } // If applicable, set the custom view's constraints if (_customView) { [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|[customView]|" options:0 metrics:nil views:@{@"customView":_customView}]]; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|[customView]|" options:0 metrics:nil views:@{@"customView":_customView}]]; } else { CGFloat width = CGRectGetWidth(self.frame) ? : CGRectGetWidth([UIScreen mainScreen].bounds); CGFloat padding = roundf(width/16.0); CGFloat verticalSpace = self.verticalSpace ? : 11.0; // Default is 11 pts NSMutableArray *subviewStrings = [NSMutableArray array]; NSMutableDictionary *views = [NSMutableDictionary dictionary]; NSDictionary *metrics = @{@"padding": @(padding)}; // Assign the image view's horizontal constraints if (_imageView.superview) { [subviewStrings addObject:@"imageView"]; views[[subviewStrings lastObject]] = _imageView; [self.contentView addConstraint:[self.contentView equallyRelatedConstraintWithView:_imageView attribute:NSLayoutAttributeCenterX]]; } // Assign the title label's horizontal constraints if ([self canShowTitle]) { [subviewStrings addObject:@"titleLabel"]; views[[subviewStrings lastObject]] = _titleLabel; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(padding@750)-[titleLabel(>=0)]-(padding@750)-|" options:0 metrics:metrics views:views]]; } // or removes from its superview else { [_titleLabel removeFromSuperview]; _titleLabel = nil; } // Assign the detail label's horizontal constraints if ([self canShowDetail]) { [subviewStrings addObject:@"detailLabel"]; views[[subviewStrings lastObject]] = _detailLabel; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(padding@750)-[detailLabel(>=0)]-(padding@750)-|" options:0 metrics:metrics views:views]]; } // or removes from its superview else { [_detailLabel removeFromSuperview]; _detailLabel = nil; } // Assign the button's horizontal constraints if ([self canShowButton]) { [subviewStrings addObject:@"button"]; views[[subviewStrings lastObject]] = _button; [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-(padding@750)-[button(>=0)]-(padding@750)-|" options:0 metrics:metrics views:views]]; } // or removes from its superview else { [_button removeFromSuperview]; _button = nil; } NSMutableString *verticalFormat = [NSMutableString new]; // Build a dynamic string format for the vertical constraints, adding a margin between each element. Default is 11 pts. for (int i = 0; i < subviewStrings.count; i++) { NSString *string = subviewStrings[i]; [verticalFormat appendFormat:@"[%@]", string]; if (i < subviewStrings.count-1) { [verticalFormat appendFormat:@"-(%.f@750)-", verticalSpace]; } } // Assign the vertical constraints to the content view if (verticalFormat.length > 0) { [self.contentView addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:[NSString stringWithFormat:@"V:|%@|", verticalFormat] options:0 metrics:metrics views:views]]; } } } - (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { UIView *hitView = [super hitTest:point withEvent:event]; // Return any UIControl instance such as buttons, segmented controls, switches, etc. if ([hitView isKindOfClass:[UIControl class]]) { return hitView; } // Return either the contentView or customView if ([hitView isEqual:_contentView] || [hitView isEqual:_customView]) { return hitView; } return nil; } @end #pragma mark - UIView+DZNConstraintBasedLayoutExtensions @implementation UIView (DZNConstraintBasedLayoutExtensions) - (NSLayoutConstraint *)equallyRelatedConstraintWithView:(UIView *)view attribute:(NSLayoutAttribute)attribute { return [NSLayoutConstraint constraintWithItem:view attribute:attribute relatedBy:NSLayoutRelationEqual toItem:self attribute:attribute multiplier:1.0 constant:0.0]; } @end #pragma mark - DZNWeakObjectContainer @implementation DZNWeakObjectContainer - (instancetype)initWithWeakObject:(id)object { self = [super init]; if (self) { _weakObject = object; } return self; } @end ================================================ FILE: Pods/Masonry/LICENSE ================================================ Copyright (c) 2011-2012 Masonry Team - https://github.com/Masonry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Pods/Masonry/Masonry/MASCompositeConstraint.h ================================================ // // MASCompositeConstraint.h // Masonry // // Created by Jonas Budelmann on 21/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASConstraint.h" #import "MASUtilities.h" /** * A group of MASConstraint objects */ @interface MASCompositeConstraint : MASConstraint /** * Creates a composite with a predefined array of children * * @param children child MASConstraints * * @return a composite constraint */ - (id)initWithChildren:(NSArray *)children; @end ================================================ FILE: Pods/Masonry/Masonry/MASCompositeConstraint.m ================================================ // // MASCompositeConstraint.m // Masonry // // Created by Jonas Budelmann on 21/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASCompositeConstraint.h" #import "MASConstraint+Private.h" @interface MASCompositeConstraint () @property (nonatomic, strong) id mas_key; @property (nonatomic, strong) NSMutableArray *childConstraints; @end @implementation MASCompositeConstraint - (id)initWithChildren:(NSArray *)children { self = [super init]; if (!self) return nil; _childConstraints = [children mutableCopy]; for (MASConstraint *constraint in _childConstraints) { constraint.delegate = self; } return self; } #pragma mark - MASConstraintDelegate - (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint { NSUInteger index = [self.childConstraints indexOfObject:constraint]; NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint); [self.childConstraints replaceObjectAtIndex:index withObject:replacementConstraint]; } - (MASConstraint *)constraint:(MASConstraint __unused *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { id strongDelegate = self.delegate; MASConstraint *newConstraint = [strongDelegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute]; newConstraint.delegate = self; [self.childConstraints addObject:newConstraint]; return newConstraint; } #pragma mark - NSLayoutConstraint multiplier proxies - (MASConstraint * (^)(CGFloat))multipliedBy { return ^id(CGFloat multiplier) { for (MASConstraint *constraint in self.childConstraints) { constraint.multipliedBy(multiplier); } return self; }; } - (MASConstraint * (^)(CGFloat))dividedBy { return ^id(CGFloat divider) { for (MASConstraint *constraint in self.childConstraints) { constraint.dividedBy(divider); } return self; }; } #pragma mark - MASLayoutPriority proxy - (MASConstraint * (^)(MASLayoutPriority))priority { return ^id(MASLayoutPriority priority) { for (MASConstraint *constraint in self.childConstraints) { constraint.priority(priority); } return self; }; } #pragma mark - NSLayoutRelation proxy - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { return ^id(id attr, NSLayoutRelation relation) { for (MASConstraint *constraint in self.childConstraints.copy) { constraint.equalToWithRelation(attr, relation); } return self; }; } #pragma mark - attribute chaining - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { [self constraint:self addConstraintWithLayoutAttribute:layoutAttribute]; return self; } #pragma mark - Animator proxy #if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV) - (MASConstraint *)animator { for (MASConstraint *constraint in self.childConstraints) { [constraint animator]; } return self; } #endif #pragma mark - debug helpers - (MASConstraint * (^)(id))key { return ^id(id key) { self.mas_key = key; int i = 0; for (MASConstraint *constraint in self.childConstraints) { constraint.key([NSString stringWithFormat:@"%@[%d]", key, i++]); } return self; }; } #pragma mark - NSLayoutConstraint constant setters - (void)setInsets:(MASEdgeInsets)insets { for (MASConstraint *constraint in self.childConstraints) { constraint.insets = insets; } } - (void)setInset:(CGFloat)inset { for (MASConstraint *constraint in self.childConstraints) { constraint.inset = inset; } } - (void)setOffset:(CGFloat)offset { for (MASConstraint *constraint in self.childConstraints) { constraint.offset = offset; } } - (void)setSizeOffset:(CGSize)sizeOffset { for (MASConstraint *constraint in self.childConstraints) { constraint.sizeOffset = sizeOffset; } } - (void)setCenterOffset:(CGPoint)centerOffset { for (MASConstraint *constraint in self.childConstraints) { constraint.centerOffset = centerOffset; } } #pragma mark - MASConstraint - (void)activate { for (MASConstraint *constraint in self.childConstraints) { [constraint activate]; } } - (void)deactivate { for (MASConstraint *constraint in self.childConstraints) { [constraint deactivate]; } } - (void)install { for (MASConstraint *constraint in self.childConstraints) { constraint.updateExisting = self.updateExisting; [constraint install]; } } - (void)uninstall { for (MASConstraint *constraint in self.childConstraints) { [constraint uninstall]; } } @end ================================================ FILE: Pods/Masonry/Masonry/MASConstraint+Private.h ================================================ // // MASConstraint+Private.h // Masonry // // Created by Nick Tymchenko on 29/04/14. // Copyright (c) 2014 cloudling. All rights reserved. // #import "MASConstraint.h" @protocol MASConstraintDelegate; @interface MASConstraint () /** * Whether or not to check for an existing constraint instead of adding constraint */ @property (nonatomic, assign) BOOL updateExisting; /** * Usually MASConstraintMaker but could be a parent MASConstraint */ @property (nonatomic, weak) id delegate; /** * Based on a provided value type, is equal to calling: * NSNumber - setOffset: * NSValue with CGPoint - setPointOffset: * NSValue with CGSize - setSizeOffset: * NSValue with MASEdgeInsets - setInsets: */ - (void)setLayoutConstantWithValue:(NSValue *)value; @end @interface MASConstraint (Abstract) /** * Sets the constraint relation to given NSLayoutRelation * returns a block which accepts one of the following: * MASViewAttribute, UIView, NSValue, NSArray * see readme for more details. */ - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation; /** * Override to set a custom chaining behaviour */ - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute; @end @protocol MASConstraintDelegate /** * Notifies the delegate when the constraint needs to be replaced with another constraint. For example * A MASViewConstraint may turn into a MASCompositeConstraint when an array is passed to one of the equality blocks */ - (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint; - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute; @end ================================================ FILE: Pods/Masonry/Masonry/MASConstraint.h ================================================ // // MASConstraint.h // Masonry // // Created by Jonas Budelmann on 22/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASUtilities.h" /** * Enables Constraints to be created with chainable syntax * Constraint can represent single NSLayoutConstraint (MASViewConstraint) * or a group of NSLayoutConstraints (MASComposisteConstraint) */ @interface MASConstraint : NSObject // Chaining Support /** * Modifies the NSLayoutConstraint constant, * only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following * NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight */ - (MASConstraint * (^)(MASEdgeInsets insets))insets; /** * Modifies the NSLayoutConstraint constant, * only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following * NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight */ - (MASConstraint * (^)(CGFloat inset))inset; /** * Modifies the NSLayoutConstraint constant, * only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following * NSLayoutAttributeWidth, NSLayoutAttributeHeight */ - (MASConstraint * (^)(CGSize offset))sizeOffset; /** * Modifies the NSLayoutConstraint constant, * only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following * NSLayoutAttributeCenterX, NSLayoutAttributeCenterY */ - (MASConstraint * (^)(CGPoint offset))centerOffset; /** * Modifies the NSLayoutConstraint constant */ - (MASConstraint * (^)(CGFloat offset))offset; /** * Modifies the NSLayoutConstraint constant based on a value type */ - (MASConstraint * (^)(NSValue *value))valueOffset; /** * Sets the NSLayoutConstraint multiplier property */ - (MASConstraint * (^)(CGFloat multiplier))multipliedBy; /** * Sets the NSLayoutConstraint multiplier to 1.0/dividedBy */ - (MASConstraint * (^)(CGFloat divider))dividedBy; /** * Sets the NSLayoutConstraint priority to a float or MASLayoutPriority */ - (MASConstraint * (^)(MASLayoutPriority priority))priority; /** * Sets the NSLayoutConstraint priority to MASLayoutPriorityLow */ - (MASConstraint * (^)(void))priorityLow; /** * Sets the NSLayoutConstraint priority to MASLayoutPriorityMedium */ - (MASConstraint * (^)(void))priorityMedium; /** * Sets the NSLayoutConstraint priority to MASLayoutPriorityHigh */ - (MASConstraint * (^)(void))priorityHigh; /** * Sets the constraint relation to NSLayoutRelationEqual * returns a block which accepts one of the following: * MASViewAttribute, UIView, NSValue, NSArray * see readme for more details. */ - (MASConstraint * (^)(id attr))equalTo; /** * Sets the constraint relation to NSLayoutRelationGreaterThanOrEqual * returns a block which accepts one of the following: * MASViewAttribute, UIView, NSValue, NSArray * see readme for more details. */ - (MASConstraint * (^)(id attr))greaterThanOrEqualTo; /** * Sets the constraint relation to NSLayoutRelationLessThanOrEqual * returns a block which accepts one of the following: * MASViewAttribute, UIView, NSValue, NSArray * see readme for more details. */ - (MASConstraint * (^)(id attr))lessThanOrEqualTo; /** * Optional semantic property which has no effect but improves the readability of constraint */ - (MASConstraint *)with; /** * Optional semantic property which has no effect but improves the readability of constraint */ - (MASConstraint *)and; /** * Creates a new MASCompositeConstraint with the called attribute and reciever */ - (MASConstraint *)left; - (MASConstraint *)top; - (MASConstraint *)right; - (MASConstraint *)bottom; - (MASConstraint *)leading; - (MASConstraint *)trailing; - (MASConstraint *)width; - (MASConstraint *)height; - (MASConstraint *)centerX; - (MASConstraint *)centerY; - (MASConstraint *)baseline; #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) - (MASConstraint *)firstBaseline; - (MASConstraint *)lastBaseline; #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) - (MASConstraint *)leftMargin; - (MASConstraint *)rightMargin; - (MASConstraint *)topMargin; - (MASConstraint *)bottomMargin; - (MASConstraint *)leadingMargin; - (MASConstraint *)trailingMargin; - (MASConstraint *)centerXWithinMargins; - (MASConstraint *)centerYWithinMargins; #endif /** * Sets the constraint debug name */ - (MASConstraint * (^)(id key))key; // NSLayoutConstraint constant Setters // for use outside of mas_updateConstraints/mas_makeConstraints blocks /** * Modifies the NSLayoutConstraint constant, * only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following * NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight */ - (void)setInsets:(MASEdgeInsets)insets; /** * Modifies the NSLayoutConstraint constant, * only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following * NSLayoutAttributeTop, NSLayoutAttributeLeft, NSLayoutAttributeBottom, NSLayoutAttributeRight */ - (void)setInset:(CGFloat)inset; /** * Modifies the NSLayoutConstraint constant, * only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following * NSLayoutAttributeWidth, NSLayoutAttributeHeight */ - (void)setSizeOffset:(CGSize)sizeOffset; /** * Modifies the NSLayoutConstraint constant, * only affects MASConstraints in which the first item's NSLayoutAttribute is one of the following * NSLayoutAttributeCenterX, NSLayoutAttributeCenterY */ - (void)setCenterOffset:(CGPoint)centerOffset; /** * Modifies the NSLayoutConstraint constant */ - (void)setOffset:(CGFloat)offset; // NSLayoutConstraint Installation support #if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV) /** * Whether or not to go through the animator proxy when modifying the constraint */ @property (nonatomic, copy, readonly) MASConstraint *animator; #endif /** * Activates an NSLayoutConstraint if it's supported by an OS. * Invokes install otherwise. */ - (void)activate; /** * Deactivates previously installed/activated NSLayoutConstraint. */ - (void)deactivate; /** * Creates a NSLayoutConstraint and adds it to the appropriate view. */ - (void)install; /** * Removes previously installed NSLayoutConstraint */ - (void)uninstall; @end /** * Convenience auto-boxing macros for MASConstraint methods. * * Defining MAS_SHORTHAND_GLOBALS will turn on auto-boxing for default syntax. * A potential drawback of this is that the unprefixed macros will appear in global scope. */ #define mas_equalTo(...) equalTo(MASBoxValue((__VA_ARGS__))) #define mas_greaterThanOrEqualTo(...) greaterThanOrEqualTo(MASBoxValue((__VA_ARGS__))) #define mas_lessThanOrEqualTo(...) lessThanOrEqualTo(MASBoxValue((__VA_ARGS__))) #define mas_offset(...) valueOffset(MASBoxValue((__VA_ARGS__))) #ifdef MAS_SHORTHAND_GLOBALS #define equalTo(...) mas_equalTo(__VA_ARGS__) #define greaterThanOrEqualTo(...) mas_greaterThanOrEqualTo(__VA_ARGS__) #define lessThanOrEqualTo(...) mas_lessThanOrEqualTo(__VA_ARGS__) #define offset(...) mas_offset(__VA_ARGS__) #endif @interface MASConstraint (AutoboxingSupport) /** * Aliases to corresponding relation methods (for shorthand macros) * Also needed to aid autocompletion */ - (MASConstraint * (^)(id attr))mas_equalTo; - (MASConstraint * (^)(id attr))mas_greaterThanOrEqualTo; - (MASConstraint * (^)(id attr))mas_lessThanOrEqualTo; /** * A dummy method to aid autocompletion */ - (MASConstraint * (^)(id offset))mas_offset; @end ================================================ FILE: Pods/Masonry/Masonry/MASConstraint.m ================================================ // // MASConstraint.m // Masonry // // Created by Nick Tymchenko on 1/20/14. // #import "MASConstraint.h" #import "MASConstraint+Private.h" #define MASMethodNotImplemented() \ @throw [NSException exceptionWithName:NSInternalInconsistencyException \ reason:[NSString stringWithFormat:@"You must override %@ in a subclass.", NSStringFromSelector(_cmd)] \ userInfo:nil] @implementation MASConstraint #pragma mark - Init - (id)init { NSAssert(![self isMemberOfClass:[MASConstraint class]], @"MASConstraint is an abstract class, you should not instantiate it directly."); return [super init]; } #pragma mark - NSLayoutRelation proxies - (MASConstraint * (^)(id))equalTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationEqual); }; } - (MASConstraint * (^)(id))mas_equalTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationEqual); }; } - (MASConstraint * (^)(id))greaterThanOrEqualTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationGreaterThanOrEqual); }; } - (MASConstraint * (^)(id))mas_greaterThanOrEqualTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationGreaterThanOrEqual); }; } - (MASConstraint * (^)(id))lessThanOrEqualTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationLessThanOrEqual); }; } - (MASConstraint * (^)(id))mas_lessThanOrEqualTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationLessThanOrEqual); }; } #pragma mark - MASLayoutPriority proxies - (MASConstraint * (^)(void))priorityLow { return ^id{ self.priority(MASLayoutPriorityDefaultLow); return self; }; } - (MASConstraint * (^)(void))priorityMedium { return ^id{ self.priority(MASLayoutPriorityDefaultMedium); return self; }; } - (MASConstraint * (^)(void))priorityHigh { return ^id{ self.priority(MASLayoutPriorityDefaultHigh); return self; }; } #pragma mark - NSLayoutConstraint constant proxies - (MASConstraint * (^)(MASEdgeInsets))insets { return ^id(MASEdgeInsets insets){ self.insets = insets; return self; }; } - (MASConstraint * (^)(CGFloat))inset { return ^id(CGFloat inset){ self.inset = inset; return self; }; } - (MASConstraint * (^)(CGSize))sizeOffset { return ^id(CGSize offset) { self.sizeOffset = offset; return self; }; } - (MASConstraint * (^)(CGPoint))centerOffset { return ^id(CGPoint offset) { self.centerOffset = offset; return self; }; } - (MASConstraint * (^)(CGFloat))offset { return ^id(CGFloat offset){ self.offset = offset; return self; }; } - (MASConstraint * (^)(NSValue *value))valueOffset { return ^id(NSValue *offset) { NSAssert([offset isKindOfClass:NSValue.class], @"expected an NSValue offset, got: %@", offset); [self setLayoutConstantWithValue:offset]; return self; }; } - (MASConstraint * (^)(id offset))mas_offset { // Will never be called due to macro return nil; } #pragma mark - NSLayoutConstraint constant setter - (void)setLayoutConstantWithValue:(NSValue *)value { if ([value isKindOfClass:NSNumber.class]) { self.offset = [(NSNumber *)value doubleValue]; } else if (strcmp(value.objCType, @encode(CGPoint)) == 0) { CGPoint point; [value getValue:&point]; self.centerOffset = point; } else if (strcmp(value.objCType, @encode(CGSize)) == 0) { CGSize size; [value getValue:&size]; self.sizeOffset = size; } else if (strcmp(value.objCType, @encode(MASEdgeInsets)) == 0) { MASEdgeInsets insets; [value getValue:&insets]; self.insets = insets; } else { NSAssert(NO, @"attempting to set layout constant with unsupported value: %@", value); } } #pragma mark - Semantic properties - (MASConstraint *)with { return self; } - (MASConstraint *)and { return self; } #pragma mark - Chaining - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute __unused)layoutAttribute { MASMethodNotImplemented(); } - (MASConstraint *)left { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]; } - (MASConstraint *)top { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop]; } - (MASConstraint *)right { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRight]; } - (MASConstraint *)bottom { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottom]; } - (MASConstraint *)leading { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading]; } - (MASConstraint *)trailing { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailing]; } - (MASConstraint *)width { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth]; } - (MASConstraint *)height { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight]; } - (MASConstraint *)centerX { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterX]; } - (MASConstraint *)centerY { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterY]; } - (MASConstraint *)baseline { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBaseline]; } #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) - (MASConstraint *)firstBaseline { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeFirstBaseline]; } - (MASConstraint *)lastBaseline { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLastBaseline]; } #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) - (MASConstraint *)leftMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeftMargin]; } - (MASConstraint *)rightMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRightMargin]; } - (MASConstraint *)topMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTopMargin]; } - (MASConstraint *)bottomMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottomMargin]; } - (MASConstraint *)leadingMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeadingMargin]; } - (MASConstraint *)trailingMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailingMargin]; } - (MASConstraint *)centerXWithinMargins { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterXWithinMargins]; } - (MASConstraint *)centerYWithinMargins { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterYWithinMargins]; } #endif #pragma mark - Abstract - (MASConstraint * (^)(CGFloat multiplier))multipliedBy { MASMethodNotImplemented(); } - (MASConstraint * (^)(CGFloat divider))dividedBy { MASMethodNotImplemented(); } - (MASConstraint * (^)(MASLayoutPriority priority))priority { MASMethodNotImplemented(); } - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { MASMethodNotImplemented(); } - (MASConstraint * (^)(id key))key { MASMethodNotImplemented(); } - (void)setInsets:(MASEdgeInsets __unused)insets { MASMethodNotImplemented(); } - (void)setInset:(CGFloat __unused)inset { MASMethodNotImplemented(); } - (void)setSizeOffset:(CGSize __unused)sizeOffset { MASMethodNotImplemented(); } - (void)setCenterOffset:(CGPoint __unused)centerOffset { MASMethodNotImplemented(); } - (void)setOffset:(CGFloat __unused)offset { MASMethodNotImplemented(); } #if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV) - (MASConstraint *)animator { MASMethodNotImplemented(); } #endif - (void)activate { MASMethodNotImplemented(); } - (void)deactivate { MASMethodNotImplemented(); } - (void)install { MASMethodNotImplemented(); } - (void)uninstall { MASMethodNotImplemented(); } @end ================================================ FILE: Pods/Masonry/Masonry/MASConstraintMaker.h ================================================ // // MASConstraintMaker.h // Masonry // // Created by Jonas Budelmann on 20/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASConstraint.h" #import "MASUtilities.h" typedef NS_OPTIONS(NSInteger, MASAttribute) { MASAttributeLeft = 1 << NSLayoutAttributeLeft, MASAttributeRight = 1 << NSLayoutAttributeRight, MASAttributeTop = 1 << NSLayoutAttributeTop, MASAttributeBottom = 1 << NSLayoutAttributeBottom, MASAttributeLeading = 1 << NSLayoutAttributeLeading, MASAttributeTrailing = 1 << NSLayoutAttributeTrailing, MASAttributeWidth = 1 << NSLayoutAttributeWidth, MASAttributeHeight = 1 << NSLayoutAttributeHeight, MASAttributeCenterX = 1 << NSLayoutAttributeCenterX, MASAttributeCenterY = 1 << NSLayoutAttributeCenterY, MASAttributeBaseline = 1 << NSLayoutAttributeBaseline, #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) MASAttributeFirstBaseline = 1 << NSLayoutAttributeFirstBaseline, MASAttributeLastBaseline = 1 << NSLayoutAttributeLastBaseline, #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) MASAttributeLeftMargin = 1 << NSLayoutAttributeLeftMargin, MASAttributeRightMargin = 1 << NSLayoutAttributeRightMargin, MASAttributeTopMargin = 1 << NSLayoutAttributeTopMargin, MASAttributeBottomMargin = 1 << NSLayoutAttributeBottomMargin, MASAttributeLeadingMargin = 1 << NSLayoutAttributeLeadingMargin, MASAttributeTrailingMargin = 1 << NSLayoutAttributeTrailingMargin, MASAttributeCenterXWithinMargins = 1 << NSLayoutAttributeCenterXWithinMargins, MASAttributeCenterYWithinMargins = 1 << NSLayoutAttributeCenterYWithinMargins, #endif }; /** * Provides factory methods for creating MASConstraints. * Constraints are collected until they are ready to be installed * */ @interface MASConstraintMaker : NSObject /** * The following properties return a new MASViewConstraint * with the first item set to the makers associated view and the appropriate MASViewAttribute */ @property (nonatomic, strong, readonly) MASConstraint *left; @property (nonatomic, strong, readonly) MASConstraint *top; @property (nonatomic, strong, readonly) MASConstraint *right; @property (nonatomic, strong, readonly) MASConstraint *bottom; @property (nonatomic, strong, readonly) MASConstraint *leading; @property (nonatomic, strong, readonly) MASConstraint *trailing; @property (nonatomic, strong, readonly) MASConstraint *width; @property (nonatomic, strong, readonly) MASConstraint *height; @property (nonatomic, strong, readonly) MASConstraint *centerX; @property (nonatomic, strong, readonly) MASConstraint *centerY; @property (nonatomic, strong, readonly) MASConstraint *baseline; #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) @property (nonatomic, strong, readonly) MASConstraint *firstBaseline; @property (nonatomic, strong, readonly) MASConstraint *lastBaseline; #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) @property (nonatomic, strong, readonly) MASConstraint *leftMargin; @property (nonatomic, strong, readonly) MASConstraint *rightMargin; @property (nonatomic, strong, readonly) MASConstraint *topMargin; @property (nonatomic, strong, readonly) MASConstraint *bottomMargin; @property (nonatomic, strong, readonly) MASConstraint *leadingMargin; @property (nonatomic, strong, readonly) MASConstraint *trailingMargin; @property (nonatomic, strong, readonly) MASConstraint *centerXWithinMargins; @property (nonatomic, strong, readonly) MASConstraint *centerYWithinMargins; #endif /** * Returns a block which creates a new MASCompositeConstraint with the first item set * to the makers associated view and children corresponding to the set bits in the * MASAttribute parameter. Combine multiple attributes via binary-or. */ @property (nonatomic, strong, readonly) MASConstraint *(^attributes)(MASAttribute attrs); /** * Creates a MASCompositeConstraint with type MASCompositeConstraintTypeEdges * which generates the appropriate MASViewConstraint children (top, left, bottom, right) * with the first item set to the makers associated view */ @property (nonatomic, strong, readonly) MASConstraint *edges; /** * Creates a MASCompositeConstraint with type MASCompositeConstraintTypeSize * which generates the appropriate MASViewConstraint children (width, height) * with the first item set to the makers associated view */ @property (nonatomic, strong, readonly) MASConstraint *size; /** * Creates a MASCompositeConstraint with type MASCompositeConstraintTypeCenter * which generates the appropriate MASViewConstraint children (centerX, centerY) * with the first item set to the makers associated view */ @property (nonatomic, strong, readonly) MASConstraint *center; /** * Whether or not to check for an existing constraint instead of adding constraint */ @property (nonatomic, assign) BOOL updateExisting; /** * Whether or not to remove existing constraints prior to installing */ @property (nonatomic, assign) BOOL removeExisting; /** * initialises the maker with a default view * * @param view any MASConstraint are created with this view as the first item * * @return a new MASConstraintMaker */ - (id)initWithView:(MAS_VIEW *)view; /** * Calls install method on any MASConstraints which have been created by this maker * * @return an array of all the installed MASConstraints */ - (NSArray *)install; - (MASConstraint * (^)(dispatch_block_t))group; @end ================================================ FILE: Pods/Masonry/Masonry/MASConstraintMaker.m ================================================ // // MASConstraintMaker.m // Masonry // // Created by Jonas Budelmann on 20/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASConstraintMaker.h" #import "MASViewConstraint.h" #import "MASCompositeConstraint.h" #import "MASConstraint+Private.h" #import "MASViewAttribute.h" #import "View+MASAdditions.h" @interface MASConstraintMaker () @property (nonatomic, weak) MAS_VIEW *view; @property (nonatomic, strong) NSMutableArray *constraints; @end @implementation MASConstraintMaker - (id)initWithView:(MAS_VIEW *)view { self = [super init]; if (!self) return nil; self.view = view; self.constraints = NSMutableArray.new; return self; } - (NSArray *)install { if (self.removeExisting) { NSArray *installedConstraints = [MASViewConstraint installedConstraintsForView:self.view]; for (MASConstraint *constraint in installedConstraints) { [constraint uninstall]; } } NSArray *constraints = self.constraints.copy; for (MASConstraint *constraint in constraints) { constraint.updateExisting = self.updateExisting; [constraint install]; } [self.constraints removeAllObjects]; return constraints; } #pragma mark - MASConstraintDelegate - (void)constraint:(MASConstraint *)constraint shouldBeReplacedWithConstraint:(MASConstraint *)replacementConstraint { NSUInteger index = [self.constraints indexOfObject:constraint]; NSAssert(index != NSNotFound, @"Could not find constraint %@", constraint); [self.constraints replaceObjectAtIndex:index withObject:replacementConstraint]; } - (MASConstraint *)constraint:(MASConstraint *)constraint addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { MASViewAttribute *viewAttribute = [[MASViewAttribute alloc] initWithView:self.view layoutAttribute:layoutAttribute]; MASViewConstraint *newConstraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:viewAttribute]; if ([constraint isKindOfClass:MASViewConstraint.class]) { //replace with composite constraint NSArray *children = @[constraint, newConstraint]; MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self; [self constraint:constraint shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } if (!constraint) { newConstraint.delegate = self; [self.constraints addObject:newConstraint]; } return newConstraint; } - (MASConstraint *)addConstraintWithAttributes:(MASAttribute)attrs { __unused MASAttribute anyAttribute = (MASAttributeLeft | MASAttributeRight | MASAttributeTop | MASAttributeBottom | MASAttributeLeading | MASAttributeTrailing | MASAttributeWidth | MASAttributeHeight | MASAttributeCenterX | MASAttributeCenterY | MASAttributeBaseline #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) | MASAttributeFirstBaseline | MASAttributeLastBaseline #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) | MASAttributeLeftMargin | MASAttributeRightMargin | MASAttributeTopMargin | MASAttributeBottomMargin | MASAttributeLeadingMargin | MASAttributeTrailingMargin | MASAttributeCenterXWithinMargins | MASAttributeCenterYWithinMargins #endif ); NSAssert((attrs & anyAttribute) != 0, @"You didn't pass any attribute to make.attributes(...)"); NSMutableArray *attributes = [NSMutableArray array]; if (attrs & MASAttributeLeft) [attributes addObject:self.view.mas_left]; if (attrs & MASAttributeRight) [attributes addObject:self.view.mas_right]; if (attrs & MASAttributeTop) [attributes addObject:self.view.mas_top]; if (attrs & MASAttributeBottom) [attributes addObject:self.view.mas_bottom]; if (attrs & MASAttributeLeading) [attributes addObject:self.view.mas_leading]; if (attrs & MASAttributeTrailing) [attributes addObject:self.view.mas_trailing]; if (attrs & MASAttributeWidth) [attributes addObject:self.view.mas_width]; if (attrs & MASAttributeHeight) [attributes addObject:self.view.mas_height]; if (attrs & MASAttributeCenterX) [attributes addObject:self.view.mas_centerX]; if (attrs & MASAttributeCenterY) [attributes addObject:self.view.mas_centerY]; if (attrs & MASAttributeBaseline) [attributes addObject:self.view.mas_baseline]; #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) if (attrs & MASAttributeFirstBaseline) [attributes addObject:self.view.mas_firstBaseline]; if (attrs & MASAttributeLastBaseline) [attributes addObject:self.view.mas_lastBaseline]; #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) if (attrs & MASAttributeLeftMargin) [attributes addObject:self.view.mas_leftMargin]; if (attrs & MASAttributeRightMargin) [attributes addObject:self.view.mas_rightMargin]; if (attrs & MASAttributeTopMargin) [attributes addObject:self.view.mas_topMargin]; if (attrs & MASAttributeBottomMargin) [attributes addObject:self.view.mas_bottomMargin]; if (attrs & MASAttributeLeadingMargin) [attributes addObject:self.view.mas_leadingMargin]; if (attrs & MASAttributeTrailingMargin) [attributes addObject:self.view.mas_trailingMargin]; if (attrs & MASAttributeCenterXWithinMargins) [attributes addObject:self.view.mas_centerXWithinMargins]; if (attrs & MASAttributeCenterYWithinMargins) [attributes addObject:self.view.mas_centerYWithinMargins]; #endif NSMutableArray *children = [NSMutableArray arrayWithCapacity:attributes.count]; for (MASViewAttribute *a in attributes) { [children addObject:[[MASViewConstraint alloc] initWithFirstViewAttribute:a]]; } MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children]; constraint.delegate = self; [self.constraints addObject:constraint]; return constraint; } #pragma mark - standard Attributes - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { return [self constraint:nil addConstraintWithLayoutAttribute:layoutAttribute]; } - (MASConstraint *)left { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeft]; } - (MASConstraint *)top { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTop]; } - (MASConstraint *)right { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRight]; } - (MASConstraint *)bottom { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottom]; } - (MASConstraint *)leading { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeading]; } - (MASConstraint *)trailing { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailing]; } - (MASConstraint *)width { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeWidth]; } - (MASConstraint *)height { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeHeight]; } - (MASConstraint *)centerX { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterX]; } - (MASConstraint *)centerY { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterY]; } - (MASConstraint *)baseline { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBaseline]; } - (MASConstraint *(^)(MASAttribute))attributes { return ^(MASAttribute attrs){ return [self addConstraintWithAttributes:attrs]; }; } #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) - (MASConstraint *)firstBaseline { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeFirstBaseline]; } - (MASConstraint *)lastBaseline { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLastBaseline]; } #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) - (MASConstraint *)leftMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeftMargin]; } - (MASConstraint *)rightMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeRightMargin]; } - (MASConstraint *)topMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTopMargin]; } - (MASConstraint *)bottomMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeBottomMargin]; } - (MASConstraint *)leadingMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeLeadingMargin]; } - (MASConstraint *)trailingMargin { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeTrailingMargin]; } - (MASConstraint *)centerXWithinMargins { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterXWithinMargins]; } - (MASConstraint *)centerYWithinMargins { return [self addConstraintWithLayoutAttribute:NSLayoutAttributeCenterYWithinMargins]; } #endif #pragma mark - composite Attributes - (MASConstraint *)edges { return [self addConstraintWithAttributes:MASAttributeTop | MASAttributeLeft | MASAttributeRight | MASAttributeBottom]; } - (MASConstraint *)size { return [self addConstraintWithAttributes:MASAttributeWidth | MASAttributeHeight]; } - (MASConstraint *)center { return [self addConstraintWithAttributes:MASAttributeCenterX | MASAttributeCenterY]; } #pragma mark - grouping - (MASConstraint *(^)(dispatch_block_t group))group { return ^id(dispatch_block_t group) { NSInteger previousCount = self.constraints.count; group(); NSArray *children = [self.constraints subarrayWithRange:NSMakeRange(previousCount, self.constraints.count - previousCount)]; MASCompositeConstraint *constraint = [[MASCompositeConstraint alloc] initWithChildren:children]; constraint.delegate = self; return constraint; }; } @end ================================================ FILE: Pods/Masonry/Masonry/MASLayoutConstraint.h ================================================ // // MASLayoutConstraint.h // Masonry // // Created by Jonas Budelmann on 3/08/13. // Copyright (c) 2013 Jonas Budelmann. All rights reserved. // #import "MASUtilities.h" /** * When you are debugging or printing the constraints attached to a view this subclass * makes it easier to identify which constraints have been created via Masonry */ @interface MASLayoutConstraint : NSLayoutConstraint /** * a key to associate with this constraint */ @property (nonatomic, strong) id mas_key; @end ================================================ FILE: Pods/Masonry/Masonry/MASLayoutConstraint.m ================================================ // // MASLayoutConstraint.m // Masonry // // Created by Jonas Budelmann on 3/08/13. // Copyright (c) 2013 Jonas Budelmann. All rights reserved. // #import "MASLayoutConstraint.h" @implementation MASLayoutConstraint @end ================================================ FILE: Pods/Masonry/Masonry/MASUtilities.h ================================================ // // MASUtilities.h // Masonry // // Created by Jonas Budelmann on 19/08/13. // Copyright (c) 2013 Jonas Budelmann. All rights reserved. // #import #if TARGET_OS_IPHONE || TARGET_OS_TV #import #define MAS_VIEW UIView #define MAS_VIEW_CONTROLLER UIViewController #define MASEdgeInsets UIEdgeInsets typedef UILayoutPriority MASLayoutPriority; static const MASLayoutPriority MASLayoutPriorityRequired = UILayoutPriorityRequired; static const MASLayoutPriority MASLayoutPriorityDefaultHigh = UILayoutPriorityDefaultHigh; static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 500; static const MASLayoutPriority MASLayoutPriorityDefaultLow = UILayoutPriorityDefaultLow; static const MASLayoutPriority MASLayoutPriorityFittingSizeLevel = UILayoutPriorityFittingSizeLevel; #elif TARGET_OS_MAC #import #define MAS_VIEW NSView #define MASEdgeInsets NSEdgeInsets typedef NSLayoutPriority MASLayoutPriority; static const MASLayoutPriority MASLayoutPriorityRequired = NSLayoutPriorityRequired; static const MASLayoutPriority MASLayoutPriorityDefaultHigh = NSLayoutPriorityDefaultHigh; static const MASLayoutPriority MASLayoutPriorityDragThatCanResizeWindow = NSLayoutPriorityDragThatCanResizeWindow; static const MASLayoutPriority MASLayoutPriorityDefaultMedium = 501; static const MASLayoutPriority MASLayoutPriorityWindowSizeStayPut = NSLayoutPriorityWindowSizeStayPut; static const MASLayoutPriority MASLayoutPriorityDragThatCannotResizeWindow = NSLayoutPriorityDragThatCannotResizeWindow; static const MASLayoutPriority MASLayoutPriorityDefaultLow = NSLayoutPriorityDefaultLow; static const MASLayoutPriority MASLayoutPriorityFittingSizeCompression = NSLayoutPriorityFittingSizeCompression; #endif /** * Allows you to attach keys to objects matching the variable names passed. * * view1.mas_key = @"view1", view2.mas_key = @"view2"; * * is equivalent to: * * MASAttachKeys(view1, view2); */ #define MASAttachKeys(...) \ { \ NSDictionary *keyPairs = NSDictionaryOfVariableBindings(__VA_ARGS__); \ for (id key in keyPairs.allKeys) { \ id obj = keyPairs[key]; \ NSAssert([obj respondsToSelector:@selector(setMas_key:)], \ @"Cannot attach mas_key to %@", obj); \ [obj setMas_key:key]; \ } \ } /** * Used to create object hashes * Based on http://www.mikeash.com/pyblog/friday-qa-2010-06-18-implementing-equality-and-hashing.html */ #define MAS_NSUINT_BIT (CHAR_BIT * sizeof(NSUInteger)) #define MAS_NSUINTROTATE(val, howmuch) ((((NSUInteger)val) << howmuch) | (((NSUInteger)val) >> (MAS_NSUINT_BIT - howmuch))) /** * Given a scalar or struct value, wraps it in NSValue * Based on EXPObjectify: https://github.com/specta/expecta */ static inline id _MASBoxValue(const char *type, ...) { va_list v; va_start(v, type); id obj = nil; if (strcmp(type, @encode(id)) == 0) { id actual = va_arg(v, id); obj = actual; } else if (strcmp(type, @encode(CGPoint)) == 0) { CGPoint actual = (CGPoint)va_arg(v, CGPoint); obj = [NSValue value:&actual withObjCType:type]; } else if (strcmp(type, @encode(CGSize)) == 0) { CGSize actual = (CGSize)va_arg(v, CGSize); obj = [NSValue value:&actual withObjCType:type]; } else if (strcmp(type, @encode(MASEdgeInsets)) == 0) { MASEdgeInsets actual = (MASEdgeInsets)va_arg(v, MASEdgeInsets); obj = [NSValue value:&actual withObjCType:type]; } else if (strcmp(type, @encode(double)) == 0) { double actual = (double)va_arg(v, double); obj = [NSNumber numberWithDouble:actual]; } else if (strcmp(type, @encode(float)) == 0) { float actual = (float)va_arg(v, double); obj = [NSNumber numberWithFloat:actual]; } else if (strcmp(type, @encode(int)) == 0) { int actual = (int)va_arg(v, int); obj = [NSNumber numberWithInt:actual]; } else if (strcmp(type, @encode(long)) == 0) { long actual = (long)va_arg(v, long); obj = [NSNumber numberWithLong:actual]; } else if (strcmp(type, @encode(long long)) == 0) { long long actual = (long long)va_arg(v, long long); obj = [NSNumber numberWithLongLong:actual]; } else if (strcmp(type, @encode(short)) == 0) { short actual = (short)va_arg(v, int); obj = [NSNumber numberWithShort:actual]; } else if (strcmp(type, @encode(char)) == 0) { char actual = (char)va_arg(v, int); obj = [NSNumber numberWithChar:actual]; } else if (strcmp(type, @encode(bool)) == 0) { bool actual = (bool)va_arg(v, int); obj = [NSNumber numberWithBool:actual]; } else if (strcmp(type, @encode(unsigned char)) == 0) { unsigned char actual = (unsigned char)va_arg(v, unsigned int); obj = [NSNumber numberWithUnsignedChar:actual]; } else if (strcmp(type, @encode(unsigned int)) == 0) { unsigned int actual = (unsigned int)va_arg(v, unsigned int); obj = [NSNumber numberWithUnsignedInt:actual]; } else if (strcmp(type, @encode(unsigned long)) == 0) { unsigned long actual = (unsigned long)va_arg(v, unsigned long); obj = [NSNumber numberWithUnsignedLong:actual]; } else if (strcmp(type, @encode(unsigned long long)) == 0) { unsigned long long actual = (unsigned long long)va_arg(v, unsigned long long); obj = [NSNumber numberWithUnsignedLongLong:actual]; } else if (strcmp(type, @encode(unsigned short)) == 0) { unsigned short actual = (unsigned short)va_arg(v, unsigned int); obj = [NSNumber numberWithUnsignedShort:actual]; } va_end(v); return obj; } #define MASBoxValue(value) _MASBoxValue(@encode(__typeof__((value))), (value)) ================================================ FILE: Pods/Masonry/Masonry/MASViewAttribute.h ================================================ // // MASViewAttribute.h // Masonry // // Created by Jonas Budelmann on 21/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASUtilities.h" /** * An immutable tuple which stores the view and the related NSLayoutAttribute. * Describes part of either the left or right hand side of a constraint equation */ @interface MASViewAttribute : NSObject /** * The view which the reciever relates to. Can be nil if item is not a view. */ @property (nonatomic, weak, readonly) MAS_VIEW *view; /** * The item which the reciever relates to. */ @property (nonatomic, weak, readonly) id item; /** * The attribute which the reciever relates to */ @property (nonatomic, assign, readonly) NSLayoutAttribute layoutAttribute; /** * Convenience initializer. */ - (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute; /** * The designated initializer. */ - (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute; /** * Determine whether the layoutAttribute is a size attribute * * @return YES if layoutAttribute is equal to NSLayoutAttributeWidth or NSLayoutAttributeHeight */ - (BOOL)isSizeAttribute; @end ================================================ FILE: Pods/Masonry/Masonry/MASViewAttribute.m ================================================ // // MASViewAttribute.m // Masonry // // Created by Jonas Budelmann on 21/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASViewAttribute.h" @implementation MASViewAttribute - (id)initWithView:(MAS_VIEW *)view layoutAttribute:(NSLayoutAttribute)layoutAttribute { self = [self initWithView:view item:view layoutAttribute:layoutAttribute]; return self; } - (id)initWithView:(MAS_VIEW *)view item:(id)item layoutAttribute:(NSLayoutAttribute)layoutAttribute { self = [super init]; if (!self) return nil; _view = view; _item = item; _layoutAttribute = layoutAttribute; return self; } - (BOOL)isSizeAttribute { return self.layoutAttribute == NSLayoutAttributeWidth || self.layoutAttribute == NSLayoutAttributeHeight; } - (BOOL)isEqual:(MASViewAttribute *)viewAttribute { if ([viewAttribute isKindOfClass:self.class]) { return self.view == viewAttribute.view && self.layoutAttribute == viewAttribute.layoutAttribute; } return [super isEqual:viewAttribute]; } - (NSUInteger)hash { return MAS_NSUINTROTATE([self.view hash], MAS_NSUINT_BIT / 2) ^ self.layoutAttribute; } @end ================================================ FILE: Pods/Masonry/Masonry/MASViewConstraint.h ================================================ // // MASViewConstraint.h // Masonry // // Created by Jonas Budelmann on 20/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASViewAttribute.h" #import "MASConstraint.h" #import "MASLayoutConstraint.h" #import "MASUtilities.h" /** * A single constraint. * Contains the attributes neccessary for creating a NSLayoutConstraint and adding it to the appropriate view */ @interface MASViewConstraint : MASConstraint /** * First item/view and first attribute of the NSLayoutConstraint */ @property (nonatomic, strong, readonly) MASViewAttribute *firstViewAttribute; /** * Second item/view and second attribute of the NSLayoutConstraint */ @property (nonatomic, strong, readonly) MASViewAttribute *secondViewAttribute; /** * initialises the MASViewConstraint with the first part of the equation * * @param firstViewAttribute view.mas_left, view.mas_width etc. * * @return a new view constraint */ - (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute; /** * Returns all MASViewConstraints installed with this view as a first item. * * @param view A view to retrieve constraints for. * * @return An array of MASViewConstraints. */ + (NSArray *)installedConstraintsForView:(MAS_VIEW *)view; @end ================================================ FILE: Pods/Masonry/Masonry/MASViewConstraint.m ================================================ // // MASViewConstraint.m // Masonry // // Created by Jonas Budelmann on 20/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASViewConstraint.h" #import "MASConstraint+Private.h" #import "MASCompositeConstraint.h" #import "MASLayoutConstraint.h" #import "View+MASAdditions.h" #import @interface MAS_VIEW (MASConstraints) @property (nonatomic, readonly) NSMutableSet *mas_installedConstraints; @end @implementation MAS_VIEW (MASConstraints) static char kInstalledConstraintsKey; - (NSMutableSet *)mas_installedConstraints { NSMutableSet *constraints = objc_getAssociatedObject(self, &kInstalledConstraintsKey); if (!constraints) { constraints = [NSMutableSet set]; objc_setAssociatedObject(self, &kInstalledConstraintsKey, constraints, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return constraints; } @end @interface MASViewConstraint () @property (nonatomic, strong, readwrite) MASViewAttribute *secondViewAttribute; @property (nonatomic, weak) MAS_VIEW *installedView; @property (nonatomic, weak) MASLayoutConstraint *layoutConstraint; @property (nonatomic, assign) NSLayoutRelation layoutRelation; @property (nonatomic, assign) MASLayoutPriority layoutPriority; @property (nonatomic, assign) CGFloat layoutMultiplier; @property (nonatomic, assign) CGFloat layoutConstant; @property (nonatomic, assign) BOOL hasLayoutRelation; @property (nonatomic, strong) id mas_key; @property (nonatomic, assign) BOOL useAnimator; @end @implementation MASViewConstraint - (id)initWithFirstViewAttribute:(MASViewAttribute *)firstViewAttribute { self = [super init]; if (!self) return nil; _firstViewAttribute = firstViewAttribute; self.layoutPriority = MASLayoutPriorityRequired; self.layoutMultiplier = 1; return self; } #pragma mark - NSCoping - (id)copyWithZone:(NSZone __unused *)zone { MASViewConstraint *constraint = [[MASViewConstraint alloc] initWithFirstViewAttribute:self.firstViewAttribute]; constraint.layoutConstant = self.layoutConstant; constraint.layoutRelation = self.layoutRelation; constraint.layoutPriority = self.layoutPriority; constraint.layoutMultiplier = self.layoutMultiplier; constraint.delegate = self.delegate; return constraint; } #pragma mark - Public + (NSArray *)installedConstraintsForView:(MAS_VIEW *)view { return [view.mas_installedConstraints allObjects]; } #pragma mark - Private - (void)setLayoutConstant:(CGFloat)layoutConstant { _layoutConstant = layoutConstant; #if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV) if (self.useAnimator) { [self.layoutConstraint.animator setConstant:layoutConstant]; } else { self.layoutConstraint.constant = layoutConstant; } #else self.layoutConstraint.constant = layoutConstant; #endif } - (void)setLayoutRelation:(NSLayoutRelation)layoutRelation { _layoutRelation = layoutRelation; self.hasLayoutRelation = YES; } - (BOOL)supportsActiveProperty { return [self.layoutConstraint respondsToSelector:@selector(isActive)]; } - (BOOL)isActive { BOOL active = YES; if ([self supportsActiveProperty]) { active = [self.layoutConstraint isActive]; } return active; } - (BOOL)hasBeenInstalled { return (self.layoutConstraint != nil) && [self isActive]; } - (void)setSecondViewAttribute:(id)secondViewAttribute { if ([secondViewAttribute isKindOfClass:NSValue.class]) { [self setLayoutConstantWithValue:secondViewAttribute]; } else if ([secondViewAttribute isKindOfClass:MAS_VIEW.class]) { _secondViewAttribute = [[MASViewAttribute alloc] initWithView:secondViewAttribute layoutAttribute:self.firstViewAttribute.layoutAttribute]; } else if ([secondViewAttribute isKindOfClass:MASViewAttribute.class]) { _secondViewAttribute = secondViewAttribute; } else { NSAssert(NO, @"attempting to add unsupported attribute: %@", secondViewAttribute); } } #pragma mark - NSLayoutConstraint multiplier proxies - (MASConstraint * (^)(CGFloat))multipliedBy { return ^id(CGFloat multiplier) { NSAssert(!self.hasBeenInstalled, @"Cannot modify constraint multiplier after it has been installed"); self.layoutMultiplier = multiplier; return self; }; } - (MASConstraint * (^)(CGFloat))dividedBy { return ^id(CGFloat divider) { NSAssert(!self.hasBeenInstalled, @"Cannot modify constraint multiplier after it has been installed"); self.layoutMultiplier = 1.0/divider; return self; }; } #pragma mark - MASLayoutPriority proxy - (MASConstraint * (^)(MASLayoutPriority))priority { return ^id(MASLayoutPriority priority) { NSAssert(!self.hasBeenInstalled, @"Cannot modify constraint priority after it has been installed"); self.layoutPriority = priority; return self; }; } #pragma mark - NSLayoutRelation proxy - (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { return ^id(id attribute, NSLayoutRelation relation) { if ([attribute isKindOfClass:NSArray.class]) { NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation"); NSMutableArray *children = NSMutableArray.new; for (id attr in attribute) { MASViewConstraint *viewConstraint = [self copy]; viewConstraint.layoutRelation = relation; viewConstraint.secondViewAttribute = attr; [children addObject:viewConstraint]; } MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children]; compositeConstraint.delegate = self.delegate; [self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint; } else { NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation"); self.layoutRelation = relation; self.secondViewAttribute = attribute; return self; } }; } #pragma mark - Semantic properties - (MASConstraint *)with { return self; } - (MASConstraint *)and { return self; } #pragma mark - attribute chaining - (MASConstraint *)addConstraintWithLayoutAttribute:(NSLayoutAttribute)layoutAttribute { NSAssert(!self.hasLayoutRelation, @"Attributes should be chained before defining the constraint relation"); return [self.delegate constraint:self addConstraintWithLayoutAttribute:layoutAttribute]; } #pragma mark - Animator proxy #if TARGET_OS_MAC && !(TARGET_OS_IPHONE || TARGET_OS_TV) - (MASConstraint *)animator { self.useAnimator = YES; return self; } #endif #pragma mark - debug helpers - (MASConstraint * (^)(id))key { return ^id(id key) { self.mas_key = key; return self; }; } #pragma mark - NSLayoutConstraint constant setters - (void)setInsets:(MASEdgeInsets)insets { NSLayoutAttribute layoutAttribute = self.firstViewAttribute.layoutAttribute; switch (layoutAttribute) { case NSLayoutAttributeLeft: case NSLayoutAttributeLeading: self.layoutConstant = insets.left; break; case NSLayoutAttributeTop: self.layoutConstant = insets.top; break; case NSLayoutAttributeBottom: self.layoutConstant = -insets.bottom; break; case NSLayoutAttributeRight: case NSLayoutAttributeTrailing: self.layoutConstant = -insets.right; break; default: break; } } - (void)setInset:(CGFloat)inset { [self setInsets:(MASEdgeInsets){.top = inset, .left = inset, .bottom = inset, .right = inset}]; } - (void)setOffset:(CGFloat)offset { self.layoutConstant = offset; } - (void)setSizeOffset:(CGSize)sizeOffset { NSLayoutAttribute layoutAttribute = self.firstViewAttribute.layoutAttribute; switch (layoutAttribute) { case NSLayoutAttributeWidth: self.layoutConstant = sizeOffset.width; break; case NSLayoutAttributeHeight: self.layoutConstant = sizeOffset.height; break; default: break; } } - (void)setCenterOffset:(CGPoint)centerOffset { NSLayoutAttribute layoutAttribute = self.firstViewAttribute.layoutAttribute; switch (layoutAttribute) { case NSLayoutAttributeCenterX: self.layoutConstant = centerOffset.x; break; case NSLayoutAttributeCenterY: self.layoutConstant = centerOffset.y; break; default: break; } } #pragma mark - MASConstraint - (void)activate { [self install]; } - (void)deactivate { [self uninstall]; } - (void)install { if (self.hasBeenInstalled) { return; } if ([self supportsActiveProperty] && self.layoutConstraint) { self.layoutConstraint.active = YES; [self.firstViewAttribute.view.mas_installedConstraints addObject:self]; return; } MAS_VIEW *firstLayoutItem = self.firstViewAttribute.item; NSLayoutAttribute firstLayoutAttribute = self.firstViewAttribute.layoutAttribute; MAS_VIEW *secondLayoutItem = self.secondViewAttribute.item; NSLayoutAttribute secondLayoutAttribute = self.secondViewAttribute.layoutAttribute; // alignment attributes must have a secondViewAttribute // therefore we assume that is refering to superview // eg make.left.equalTo(@10) if (!self.firstViewAttribute.isSizeAttribute && !self.secondViewAttribute) { secondLayoutItem = self.firstViewAttribute.view.superview; secondLayoutAttribute = firstLayoutAttribute; } MASLayoutConstraint *layoutConstraint = [MASLayoutConstraint constraintWithItem:firstLayoutItem attribute:firstLayoutAttribute relatedBy:self.layoutRelation toItem:secondLayoutItem attribute:secondLayoutAttribute multiplier:self.layoutMultiplier constant:self.layoutConstant]; layoutConstraint.priority = self.layoutPriority; layoutConstraint.mas_key = self.mas_key; if (self.secondViewAttribute.view) { MAS_VIEW *closestCommonSuperview = [self.firstViewAttribute.view mas_closestCommonSuperview:self.secondViewAttribute.view]; NSAssert(closestCommonSuperview, @"couldn't find a common superview for %@ and %@", self.firstViewAttribute.view, self.secondViewAttribute.view); self.installedView = closestCommonSuperview; } else if (self.firstViewAttribute.isSizeAttribute) { self.installedView = self.firstViewAttribute.view; } else { self.installedView = self.firstViewAttribute.view.superview; } MASLayoutConstraint *existingConstraint = nil; if (self.updateExisting) { existingConstraint = [self layoutConstraintSimilarTo:layoutConstraint]; } if (existingConstraint) { // just update the constant existingConstraint.constant = layoutConstraint.constant; self.layoutConstraint = existingConstraint; } else { [self.installedView addConstraint:layoutConstraint]; self.layoutConstraint = layoutConstraint; [firstLayoutItem.mas_installedConstraints addObject:self]; } } - (MASLayoutConstraint *)layoutConstraintSimilarTo:(MASLayoutConstraint *)layoutConstraint { // check if any constraints are the same apart from the only mutable property constant // go through constraints in reverse as we do not want to match auto-resizing or interface builder constraints // and they are likely to be added first. for (NSLayoutConstraint *existingConstraint in self.installedView.constraints.reverseObjectEnumerator) { if (![existingConstraint isKindOfClass:MASLayoutConstraint.class]) continue; if (existingConstraint.firstItem != layoutConstraint.firstItem) continue; if (existingConstraint.secondItem != layoutConstraint.secondItem) continue; if (existingConstraint.firstAttribute != layoutConstraint.firstAttribute) continue; if (existingConstraint.secondAttribute != layoutConstraint.secondAttribute) continue; if (existingConstraint.relation != layoutConstraint.relation) continue; if (existingConstraint.multiplier != layoutConstraint.multiplier) continue; if (existingConstraint.priority != layoutConstraint.priority) continue; return (id)existingConstraint; } return nil; } - (void)uninstall { if ([self supportsActiveProperty]) { self.layoutConstraint.active = NO; [self.firstViewAttribute.view.mas_installedConstraints removeObject:self]; return; } [self.installedView removeConstraint:self.layoutConstraint]; self.layoutConstraint = nil; self.installedView = nil; [self.firstViewAttribute.view.mas_installedConstraints removeObject:self]; } @end ================================================ FILE: Pods/Masonry/Masonry/Masonry.h ================================================ // // Masonry.h // Masonry // // Created by Jonas Budelmann on 20/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import //! Project version number for Masonry. FOUNDATION_EXPORT double MasonryVersionNumber; //! Project version string for Masonry. FOUNDATION_EXPORT const unsigned char MasonryVersionString[]; #import "MASUtilities.h" #import "View+MASAdditions.h" #import "View+MASShorthandAdditions.h" #import "ViewController+MASAdditions.h" #import "NSArray+MASAdditions.h" #import "NSArray+MASShorthandAdditions.h" #import "MASConstraint.h" #import "MASCompositeConstraint.h" #import "MASViewAttribute.h" #import "MASViewConstraint.h" #import "MASConstraintMaker.h" #import "MASLayoutConstraint.h" #import "NSLayoutConstraint+MASDebugAdditions.h" ================================================ FILE: Pods/Masonry/Masonry/NSArray+MASAdditions.h ================================================ // // NSArray+MASAdditions.h // // // Created by Daniel Hammond on 11/26/13. // // #import "MASUtilities.h" #import "MASConstraintMaker.h" #import "MASViewAttribute.h" typedef NS_ENUM(NSUInteger, MASAxisType) { MASAxisTypeHorizontal, MASAxisTypeVertical }; @interface NSArray (MASAdditions) /** * Creates a MASConstraintMaker with each view in the callee. * Any constraints defined are added to the view or the appropriate superview once the block has finished executing on each view * * @param block scope within which you can build up the constraints which you wish to apply to each view. * * @return Array of created MASConstraints */ - (NSArray *)mas_makeConstraints:(void (NS_NOESCAPE ^)(MASConstraintMaker *make))block; /** * Creates a MASConstraintMaker with each view in the callee. * Any constraints defined are added to each view or the appropriate superview once the block has finished executing on each view. * If an existing constraint exists then it will be updated instead. * * @param block scope within which you can build up the constraints which you wish to apply to each view. * * @return Array of created/updated MASConstraints */ - (NSArray *)mas_updateConstraints:(void (NS_NOESCAPE ^)(MASConstraintMaker *make))block; /** * Creates a MASConstraintMaker with each view in the callee. * Any constraints defined are added to each view or the appropriate superview once the block has finished executing on each view. * All constraints previously installed for the views will be removed. * * @param block scope within which you can build up the constraints which you wish to apply to each view. * * @return Array of created/updated MASConstraints */ - (NSArray *)mas_remakeConstraints:(void (NS_NOESCAPE ^)(MASConstraintMaker *make))block; /** * distribute with fixed spacing * * @param axisType which axis to distribute items along * @param fixedSpacing the spacing between each item * @param leadSpacing the spacing before the first item and the container * @param tailSpacing the spacing after the last item and the container */ - (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedSpacing:(CGFloat)fixedSpacing leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing; /** * distribute with fixed item size * * @param axisType which axis to distribute items along * @param fixedItemLength the fixed length of each item * @param leadSpacing the spacing before the first item and the container * @param tailSpacing the spacing after the last item and the container */ - (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedItemLength:(CGFloat)fixedItemLength leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing; @end ================================================ FILE: Pods/Masonry/Masonry/NSArray+MASAdditions.m ================================================ // // NSArray+MASAdditions.m // // // Created by Daniel Hammond on 11/26/13. // // #import "NSArray+MASAdditions.h" #import "View+MASAdditions.h" @implementation NSArray (MASAdditions) - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *make))block { NSMutableArray *constraints = [NSMutableArray array]; for (MAS_VIEW *view in self) { NSAssert([view isKindOfClass:[MAS_VIEW class]], @"All objects in the array must be views"); [constraints addObjectsFromArray:[view mas_makeConstraints:block]]; } return constraints; } - (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *make))block { NSMutableArray *constraints = [NSMutableArray array]; for (MAS_VIEW *view in self) { NSAssert([view isKindOfClass:[MAS_VIEW class]], @"All objects in the array must be views"); [constraints addObjectsFromArray:[view mas_updateConstraints:block]]; } return constraints; } - (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block { NSMutableArray *constraints = [NSMutableArray array]; for (MAS_VIEW *view in self) { NSAssert([view isKindOfClass:[MAS_VIEW class]], @"All objects in the array must be views"); [constraints addObjectsFromArray:[view mas_remakeConstraints:block]]; } return constraints; } - (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedSpacing:(CGFloat)fixedSpacing leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing { if (self.count < 2) { NSAssert(self.count>1,@"views to distribute need to bigger than one"); return; } MAS_VIEW *tempSuperView = [self mas_commonSuperviewOfViews]; if (axisType == MASAxisTypeHorizontal) { MAS_VIEW *prev; for (int i = 0; i < self.count; i++) { MAS_VIEW *v = self[i]; [v mas_makeConstraints:^(MASConstraintMaker *make) { if (prev) { make.width.equalTo(prev); make.left.equalTo(prev.mas_right).offset(fixedSpacing); if (i == self.count - 1) {//last one make.right.equalTo(tempSuperView).offset(-tailSpacing); } } else {//first one make.left.equalTo(tempSuperView).offset(leadSpacing); } }]; prev = v; } } else { MAS_VIEW *prev; for (int i = 0; i < self.count; i++) { MAS_VIEW *v = self[i]; [v mas_makeConstraints:^(MASConstraintMaker *make) { if (prev) { make.height.equalTo(prev); make.top.equalTo(prev.mas_bottom).offset(fixedSpacing); if (i == self.count - 1) {//last one make.bottom.equalTo(tempSuperView).offset(-tailSpacing); } } else {//first one make.top.equalTo(tempSuperView).offset(leadSpacing); } }]; prev = v; } } } - (void)mas_distributeViewsAlongAxis:(MASAxisType)axisType withFixedItemLength:(CGFloat)fixedItemLength leadSpacing:(CGFloat)leadSpacing tailSpacing:(CGFloat)tailSpacing { if (self.count < 2) { NSAssert(self.count>1,@"views to distribute need to bigger than one"); return; } MAS_VIEW *tempSuperView = [self mas_commonSuperviewOfViews]; if (axisType == MASAxisTypeHorizontal) { MAS_VIEW *prev; for (int i = 0; i < self.count; i++) { MAS_VIEW *v = self[i]; [v mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(@(fixedItemLength)); if (prev) { if (i == self.count - 1) {//last one make.right.equalTo(tempSuperView).offset(-tailSpacing); } else { CGFloat offset = (1-(i/((CGFloat)self.count-1)))*(fixedItemLength+leadSpacing)-i*tailSpacing/(((CGFloat)self.count-1)); make.right.equalTo(tempSuperView).multipliedBy(i/((CGFloat)self.count-1)).with.offset(offset); } } else {//first one make.left.equalTo(tempSuperView).offset(leadSpacing); } }]; prev = v; } } else { MAS_VIEW *prev; for (int i = 0; i < self.count; i++) { MAS_VIEW *v = self[i]; [v mas_makeConstraints:^(MASConstraintMaker *make) { make.height.equalTo(@(fixedItemLength)); if (prev) { if (i == self.count - 1) {//last one make.bottom.equalTo(tempSuperView).offset(-tailSpacing); } else { CGFloat offset = (1-(i/((CGFloat)self.count-1)))*(fixedItemLength+leadSpacing)-i*tailSpacing/(((CGFloat)self.count-1)); make.bottom.equalTo(tempSuperView).multipliedBy(i/((CGFloat)self.count-1)).with.offset(offset); } } else {//first one make.top.equalTo(tempSuperView).offset(leadSpacing); } }]; prev = v; } } } - (MAS_VIEW *)mas_commonSuperviewOfViews { MAS_VIEW *commonSuperview = nil; MAS_VIEW *previousView = nil; for (id object in self) { if ([object isKindOfClass:[MAS_VIEW class]]) { MAS_VIEW *view = (MAS_VIEW *)object; if (previousView) { commonSuperview = [view mas_closestCommonSuperview:commonSuperview]; } else { commonSuperview = view; } previousView = view; } } NSAssert(commonSuperview, @"Can't constrain views that do not share a common superview. Make sure that all the views in this array have been added into the same view hierarchy."); return commonSuperview; } @end ================================================ FILE: Pods/Masonry/Masonry/NSArray+MASShorthandAdditions.h ================================================ // // NSArray+MASShorthandAdditions.h // Masonry // // Created by Jonas Budelmann on 22/07/13. // Copyright (c) 2013 Jonas Budelmann. All rights reserved. // #import "NSArray+MASAdditions.h" #ifdef MAS_SHORTHAND /** * Shorthand array additions without the 'mas_' prefixes, * only enabled if MAS_SHORTHAND is defined */ @interface NSArray (MASShorthandAdditions) - (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *make))block; - (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *make))block; - (NSArray *)remakeConstraints:(void(^)(MASConstraintMaker *make))block; @end @implementation NSArray (MASShorthandAdditions) - (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *))block { return [self mas_makeConstraints:block]; } - (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *))block { return [self mas_updateConstraints:block]; } - (NSArray *)remakeConstraints:(void(^)(MASConstraintMaker *))block { return [self mas_remakeConstraints:block]; } @end #endif ================================================ FILE: Pods/Masonry/Masonry/NSLayoutConstraint+MASDebugAdditions.h ================================================ // // NSLayoutConstraint+MASDebugAdditions.h // Masonry // // Created by Jonas Budelmann on 3/08/13. // Copyright (c) 2013 Jonas Budelmann. All rights reserved. // #import "MASUtilities.h" /** * makes debug and log output of NSLayoutConstraints more readable */ @interface NSLayoutConstraint (MASDebugAdditions) @end ================================================ FILE: Pods/Masonry/Masonry/NSLayoutConstraint+MASDebugAdditions.m ================================================ // // NSLayoutConstraint+MASDebugAdditions.m // Masonry // // Created by Jonas Budelmann on 3/08/13. // Copyright (c) 2013 Jonas Budelmann. All rights reserved. // #import "NSLayoutConstraint+MASDebugAdditions.h" #import "MASConstraint.h" #import "MASLayoutConstraint.h" @implementation NSLayoutConstraint (MASDebugAdditions) #pragma mark - description maps + (NSDictionary *)layoutRelationDescriptionsByValue { static dispatch_once_t once; static NSDictionary *descriptionMap; dispatch_once(&once, ^{ descriptionMap = @{ @(NSLayoutRelationEqual) : @"==", @(NSLayoutRelationGreaterThanOrEqual) : @">=", @(NSLayoutRelationLessThanOrEqual) : @"<=", }; }); return descriptionMap; } + (NSDictionary *)layoutAttributeDescriptionsByValue { static dispatch_once_t once; static NSDictionary *descriptionMap; dispatch_once(&once, ^{ descriptionMap = @{ @(NSLayoutAttributeTop) : @"top", @(NSLayoutAttributeLeft) : @"left", @(NSLayoutAttributeBottom) : @"bottom", @(NSLayoutAttributeRight) : @"right", @(NSLayoutAttributeLeading) : @"leading", @(NSLayoutAttributeTrailing) : @"trailing", @(NSLayoutAttributeWidth) : @"width", @(NSLayoutAttributeHeight) : @"height", @(NSLayoutAttributeCenterX) : @"centerX", @(NSLayoutAttributeCenterY) : @"centerY", @(NSLayoutAttributeBaseline) : @"baseline", #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) @(NSLayoutAttributeFirstBaseline) : @"firstBaseline", @(NSLayoutAttributeLastBaseline) : @"lastBaseline", #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) @(NSLayoutAttributeLeftMargin) : @"leftMargin", @(NSLayoutAttributeRightMargin) : @"rightMargin", @(NSLayoutAttributeTopMargin) : @"topMargin", @(NSLayoutAttributeBottomMargin) : @"bottomMargin", @(NSLayoutAttributeLeadingMargin) : @"leadingMargin", @(NSLayoutAttributeTrailingMargin) : @"trailingMargin", @(NSLayoutAttributeCenterXWithinMargins) : @"centerXWithinMargins", @(NSLayoutAttributeCenterYWithinMargins) : @"centerYWithinMargins", #endif }; }); return descriptionMap; } + (NSDictionary *)layoutPriorityDescriptionsByValue { static dispatch_once_t once; static NSDictionary *descriptionMap; dispatch_once(&once, ^{ #if TARGET_OS_IPHONE || TARGET_OS_TV descriptionMap = @{ @(MASLayoutPriorityDefaultHigh) : @"high", @(MASLayoutPriorityDefaultLow) : @"low", @(MASLayoutPriorityDefaultMedium) : @"medium", @(MASLayoutPriorityRequired) : @"required", @(MASLayoutPriorityFittingSizeLevel) : @"fitting size", }; #elif TARGET_OS_MAC descriptionMap = @{ @(MASLayoutPriorityDefaultHigh) : @"high", @(MASLayoutPriorityDragThatCanResizeWindow) : @"drag can resize window", @(MASLayoutPriorityDefaultMedium) : @"medium", @(MASLayoutPriorityWindowSizeStayPut) : @"window size stay put", @(MASLayoutPriorityDragThatCannotResizeWindow) : @"drag cannot resize window", @(MASLayoutPriorityDefaultLow) : @"low", @(MASLayoutPriorityFittingSizeCompression) : @"fitting size", @(MASLayoutPriorityRequired) : @"required", }; #endif }); return descriptionMap; } #pragma mark - description override + (NSString *)descriptionForObject:(id)obj { if ([obj respondsToSelector:@selector(mas_key)] && [obj mas_key]) { return [NSString stringWithFormat:@"%@:%@", [obj class], [obj mas_key]]; } return [NSString stringWithFormat:@"%@:%p", [obj class], obj]; } - (NSString *)description { NSMutableString *description = [[NSMutableString alloc] initWithString:@"<"]; [description appendString:[self.class descriptionForObject:self]]; [description appendFormat:@" %@", [self.class descriptionForObject:self.firstItem]]; if (self.firstAttribute != NSLayoutAttributeNotAnAttribute) { [description appendFormat:@".%@", self.class.layoutAttributeDescriptionsByValue[@(self.firstAttribute)]]; } [description appendFormat:@" %@", self.class.layoutRelationDescriptionsByValue[@(self.relation)]]; if (self.secondItem) { [description appendFormat:@" %@", [self.class descriptionForObject:self.secondItem]]; } if (self.secondAttribute != NSLayoutAttributeNotAnAttribute) { [description appendFormat:@".%@", self.class.layoutAttributeDescriptionsByValue[@(self.secondAttribute)]]; } if (self.multiplier != 1) { [description appendFormat:@" * %g", self.multiplier]; } if (self.secondAttribute == NSLayoutAttributeNotAnAttribute) { [description appendFormat:@" %g", self.constant]; } else { if (self.constant) { [description appendFormat:@" %@ %g", (self.constant < 0 ? @"-" : @"+"), ABS(self.constant)]; } } if (self.priority != MASLayoutPriorityRequired) { [description appendFormat:@" ^%@", self.class.layoutPriorityDescriptionsByValue[@(self.priority)] ?: [NSNumber numberWithDouble:self.priority]]; } [description appendString:@">"]; return description; } @end ================================================ FILE: Pods/Masonry/Masonry/View+MASAdditions.h ================================================ // // UIView+MASAdditions.h // Masonry // // Created by Jonas Budelmann on 20/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "MASUtilities.h" #import "MASConstraintMaker.h" #import "MASViewAttribute.h" /** * Provides constraint maker block * and convience methods for creating MASViewAttribute which are view + NSLayoutAttribute pairs */ @interface MAS_VIEW (MASAdditions) /** * following properties return a new MASViewAttribute with current view and appropriate NSLayoutAttribute */ @property (nonatomic, strong, readonly) MASViewAttribute *mas_left; @property (nonatomic, strong, readonly) MASViewAttribute *mas_top; @property (nonatomic, strong, readonly) MASViewAttribute *mas_right; @property (nonatomic, strong, readonly) MASViewAttribute *mas_bottom; @property (nonatomic, strong, readonly) MASViewAttribute *mas_leading; @property (nonatomic, strong, readonly) MASViewAttribute *mas_trailing; @property (nonatomic, strong, readonly) MASViewAttribute *mas_width; @property (nonatomic, strong, readonly) MASViewAttribute *mas_height; @property (nonatomic, strong, readonly) MASViewAttribute *mas_centerX; @property (nonatomic, strong, readonly) MASViewAttribute *mas_centerY; @property (nonatomic, strong, readonly) MASViewAttribute *mas_baseline; @property (nonatomic, strong, readonly) MASViewAttribute *(^mas_attribute)(NSLayoutAttribute attr); #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) @property (nonatomic, strong, readonly) MASViewAttribute *mas_firstBaseline; @property (nonatomic, strong, readonly) MASViewAttribute *mas_lastBaseline; #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) @property (nonatomic, strong, readonly) MASViewAttribute *mas_leftMargin; @property (nonatomic, strong, readonly) MASViewAttribute *mas_rightMargin; @property (nonatomic, strong, readonly) MASViewAttribute *mas_topMargin; @property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomMargin; @property (nonatomic, strong, readonly) MASViewAttribute *mas_leadingMargin; @property (nonatomic, strong, readonly) MASViewAttribute *mas_trailingMargin; @property (nonatomic, strong, readonly) MASViewAttribute *mas_centerXWithinMargins; @property (nonatomic, strong, readonly) MASViewAttribute *mas_centerYWithinMargins; #endif #if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000) @property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuide API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideTop API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideBottom API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideLeft API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, strong, readonly) MASViewAttribute *mas_safeAreaLayoutGuideRight API_AVAILABLE(ios(11.0),tvos(11.0)); #endif /** * a key to associate with this view */ @property (nonatomic, strong) id mas_key; /** * Finds the closest common superview between this view and another view * * @param view other view * * @return returns nil if common superview could not be found */ - (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view; /** * Creates a MASConstraintMaker with the callee view. * Any constraints defined are added to the view or the appropriate superview once the block has finished executing * * @param block scope within which you can build up the constraints which you wish to apply to the view. * * @return Array of created MASConstraints */ - (NSArray *)mas_makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block; /** * Creates a MASConstraintMaker with the callee view. * Any constraints defined are added to the view or the appropriate superview once the block has finished executing. * If an existing constraint exists then it will be updated instead. * * @param block scope within which you can build up the constraints which you wish to apply to the view. * * @return Array of created/updated MASConstraints */ - (NSArray *)mas_updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block; /** * Creates a MASConstraintMaker with the callee view. * Any constraints defined are added to the view or the appropriate superview once the block has finished executing. * All constraints previously installed for the view will be removed. * * @param block scope within which you can build up the constraints which you wish to apply to the view. * * @return Array of created/updated MASConstraints */ - (NSArray *)mas_remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *make))block; @end ================================================ FILE: Pods/Masonry/Masonry/View+MASAdditions.m ================================================ // // UIView+MASAdditions.m // Masonry // // Created by Jonas Budelmann on 20/07/13. // Copyright (c) 2013 cloudling. All rights reserved. // #import "View+MASAdditions.h" #import @implementation MAS_VIEW (MASAdditions) - (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; block(constraintMaker); return [constraintMaker install]; } - (NSArray *)mas_updateConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; constraintMaker.updateExisting = YES; block(constraintMaker); return [constraintMaker install]; } - (NSArray *)mas_remakeConstraints:(void(^)(MASConstraintMaker *make))block { self.translatesAutoresizingMaskIntoConstraints = NO; MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self]; constraintMaker.removeExisting = YES; block(constraintMaker); return [constraintMaker install]; } #pragma mark - NSLayoutAttribute properties - (MASViewAttribute *)mas_left { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeft]; } - (MASViewAttribute *)mas_top { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTop]; } - (MASViewAttribute *)mas_right { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeRight]; } - (MASViewAttribute *)mas_bottom { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeBottom]; } - (MASViewAttribute *)mas_leading { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeading]; } - (MASViewAttribute *)mas_trailing { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTrailing]; } - (MASViewAttribute *)mas_width { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeWidth]; } - (MASViewAttribute *)mas_height { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeHeight]; } - (MASViewAttribute *)mas_centerX { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterX]; } - (MASViewAttribute *)mas_centerY { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterY]; } - (MASViewAttribute *)mas_baseline { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeBaseline]; } - (MASViewAttribute *(^)(NSLayoutAttribute))mas_attribute { return ^(NSLayoutAttribute attr) { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:attr]; }; } #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) - (MASViewAttribute *)mas_firstBaseline { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeFirstBaseline]; } - (MASViewAttribute *)mas_lastBaseline { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLastBaseline]; } #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) - (MASViewAttribute *)mas_leftMargin { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeftMargin]; } - (MASViewAttribute *)mas_rightMargin { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeRightMargin]; } - (MASViewAttribute *)mas_topMargin { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTopMargin]; } - (MASViewAttribute *)mas_bottomMargin { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeBottomMargin]; } - (MASViewAttribute *)mas_leadingMargin { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeLeadingMargin]; } - (MASViewAttribute *)mas_trailingMargin { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeTrailingMargin]; } - (MASViewAttribute *)mas_centerXWithinMargins { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterXWithinMargins]; } - (MASViewAttribute *)mas_centerYWithinMargins { return [[MASViewAttribute alloc] initWithView:self layoutAttribute:NSLayoutAttributeCenterYWithinMargins]; } #endif #if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000) - (MASViewAttribute *)mas_safeAreaLayoutGuide { return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeBottom]; } - (MASViewAttribute *)mas_safeAreaLayoutGuideTop { return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeTop]; } - (MASViewAttribute *)mas_safeAreaLayoutGuideBottom { return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeBottom]; } - (MASViewAttribute *)mas_safeAreaLayoutGuideLeft { return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeLeft]; } - (MASViewAttribute *)mas_safeAreaLayoutGuideRight { return [[MASViewAttribute alloc] initWithView:self item:self.safeAreaLayoutGuide layoutAttribute:NSLayoutAttributeRight]; } #endif #pragma mark - associated properties - (id)mas_key { return objc_getAssociatedObject(self, @selector(mas_key)); } - (void)setMas_key:(id)key { objc_setAssociatedObject(self, @selector(mas_key), key, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } #pragma mark - heirachy - (instancetype)mas_closestCommonSuperview:(MAS_VIEW *)view { MAS_VIEW *closestCommonSuperview = nil; MAS_VIEW *secondViewSuperview = view; while (!closestCommonSuperview && secondViewSuperview) { MAS_VIEW *firstViewSuperview = self; while (!closestCommonSuperview && firstViewSuperview) { if (secondViewSuperview == firstViewSuperview) { closestCommonSuperview = secondViewSuperview; } firstViewSuperview = firstViewSuperview.superview; } secondViewSuperview = secondViewSuperview.superview; } return closestCommonSuperview; } @end ================================================ FILE: Pods/Masonry/Masonry/View+MASShorthandAdditions.h ================================================ // // UIView+MASShorthandAdditions.h // Masonry // // Created by Jonas Budelmann on 22/07/13. // Copyright (c) 2013 Jonas Budelmann. All rights reserved. // #import "View+MASAdditions.h" #ifdef MAS_SHORTHAND /** * Shorthand view additions without the 'mas_' prefixes, * only enabled if MAS_SHORTHAND is defined */ @interface MAS_VIEW (MASShorthandAdditions) @property (nonatomic, strong, readonly) MASViewAttribute *left; @property (nonatomic, strong, readonly) MASViewAttribute *top; @property (nonatomic, strong, readonly) MASViewAttribute *right; @property (nonatomic, strong, readonly) MASViewAttribute *bottom; @property (nonatomic, strong, readonly) MASViewAttribute *leading; @property (nonatomic, strong, readonly) MASViewAttribute *trailing; @property (nonatomic, strong, readonly) MASViewAttribute *width; @property (nonatomic, strong, readonly) MASViewAttribute *height; @property (nonatomic, strong, readonly) MASViewAttribute *centerX; @property (nonatomic, strong, readonly) MASViewAttribute *centerY; @property (nonatomic, strong, readonly) MASViewAttribute *baseline; @property (nonatomic, strong, readonly) MASViewAttribute *(^attribute)(NSLayoutAttribute attr); #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) @property (nonatomic, strong, readonly) MASViewAttribute *firstBaseline; @property (nonatomic, strong, readonly) MASViewAttribute *lastBaseline; #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) @property (nonatomic, strong, readonly) MASViewAttribute *leftMargin; @property (nonatomic, strong, readonly) MASViewAttribute *rightMargin; @property (nonatomic, strong, readonly) MASViewAttribute *topMargin; @property (nonatomic, strong, readonly) MASViewAttribute *bottomMargin; @property (nonatomic, strong, readonly) MASViewAttribute *leadingMargin; @property (nonatomic, strong, readonly) MASViewAttribute *trailingMargin; @property (nonatomic, strong, readonly) MASViewAttribute *centerXWithinMargins; @property (nonatomic, strong, readonly) MASViewAttribute *centerYWithinMargins; #endif #if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000) @property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideTop API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideBottom API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideLeft API_AVAILABLE(ios(11.0),tvos(11.0)); @property (nonatomic, strong, readonly) MASViewAttribute *safeAreaLayoutGuideRight API_AVAILABLE(ios(11.0),tvos(11.0)); #endif - (NSArray *)makeConstraints:(void(^)(MASConstraintMaker *make))block; - (NSArray *)updateConstraints:(void(^)(MASConstraintMaker *make))block; - (NSArray *)remakeConstraints:(void(^)(MASConstraintMaker *make))block; @end #define MAS_ATTR_FORWARD(attr) \ - (MASViewAttribute *)attr { \ return [self mas_##attr]; \ } @implementation MAS_VIEW (MASShorthandAdditions) MAS_ATTR_FORWARD(top); MAS_ATTR_FORWARD(left); MAS_ATTR_FORWARD(bottom); MAS_ATTR_FORWARD(right); MAS_ATTR_FORWARD(leading); MAS_ATTR_FORWARD(trailing); MAS_ATTR_FORWARD(width); MAS_ATTR_FORWARD(height); MAS_ATTR_FORWARD(centerX); MAS_ATTR_FORWARD(centerY); MAS_ATTR_FORWARD(baseline); #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) || (__MAC_OS_X_VERSION_MIN_REQUIRED >= 101100) MAS_ATTR_FORWARD(firstBaseline); MAS_ATTR_FORWARD(lastBaseline); #endif #if (__IPHONE_OS_VERSION_MIN_REQUIRED >= 80000) || (__TV_OS_VERSION_MIN_REQUIRED >= 9000) MAS_ATTR_FORWARD(leftMargin); MAS_ATTR_FORWARD(rightMargin); MAS_ATTR_FORWARD(topMargin); MAS_ATTR_FORWARD(bottomMargin); MAS_ATTR_FORWARD(leadingMargin); MAS_ATTR_FORWARD(trailingMargin); MAS_ATTR_FORWARD(centerXWithinMargins); MAS_ATTR_FORWARD(centerYWithinMargins); #endif #if (__IPHONE_OS_VERSION_MAX_ALLOWED >= 110000) || (__TV_OS_VERSION_MAX_ALLOWED >= 110000) MAS_ATTR_FORWARD(safeAreaLayoutGuideTop); MAS_ATTR_FORWARD(safeAreaLayoutGuideBottom); MAS_ATTR_FORWARD(safeAreaLayoutGuideLeft); MAS_ATTR_FORWARD(safeAreaLayoutGuideRight); #endif - (MASViewAttribute *(^)(NSLayoutAttribute))attribute { return [self mas_attribute]; } - (NSArray *)makeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block { return [self mas_makeConstraints:block]; } - (NSArray *)updateConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block { return [self mas_updateConstraints:block]; } - (NSArray *)remakeConstraints:(void(NS_NOESCAPE ^)(MASConstraintMaker *))block { return [self mas_remakeConstraints:block]; } @end #endif ================================================ FILE: Pods/Masonry/Masonry/ViewController+MASAdditions.h ================================================ // // UIViewController+MASAdditions.h // Masonry // // Created by Craig Siemens on 2015-06-23. // // #import "MASUtilities.h" #import "MASConstraintMaker.h" #import "MASViewAttribute.h" #ifdef MAS_VIEW_CONTROLLER @interface MAS_VIEW_CONTROLLER (MASAdditions) /** * following properties return a new MASViewAttribute with appropriate UILayoutGuide and NSLayoutAttribute */ @property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuide; @property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuide; @property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuideTop; @property (nonatomic, strong, readonly) MASViewAttribute *mas_topLayoutGuideBottom; @property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuideTop; @property (nonatomic, strong, readonly) MASViewAttribute *mas_bottomLayoutGuideBottom; @end #endif ================================================ FILE: Pods/Masonry/Masonry/ViewController+MASAdditions.m ================================================ // // UIViewController+MASAdditions.m // Masonry // // Created by Craig Siemens on 2015-06-23. // // #import "ViewController+MASAdditions.h" #ifdef MAS_VIEW_CONTROLLER @implementation MAS_VIEW_CONTROLLER (MASAdditions) - (MASViewAttribute *)mas_topLayoutGuide { return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeBottom]; } - (MASViewAttribute *)mas_topLayoutGuideTop { return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeTop]; } - (MASViewAttribute *)mas_topLayoutGuideBottom { return [[MASViewAttribute alloc] initWithView:self.view item:self.topLayoutGuide layoutAttribute:NSLayoutAttributeBottom]; } - (MASViewAttribute *)mas_bottomLayoutGuide { return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeTop]; } - (MASViewAttribute *)mas_bottomLayoutGuideTop { return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeTop]; } - (MASViewAttribute *)mas_bottomLayoutGuideBottom { return [[MASViewAttribute alloc] initWithView:self.view item:self.bottomLayoutGuide layoutAttribute:NSLayoutAttributeBottom]; } @end #endif ================================================ FILE: Pods/Masonry/README.md ================================================ # Masonry [![Build Status](https://travis-ci.org/SnapKit/Masonry.svg?branch=master)](https://travis-ci.org/SnapKit/Masonry) [![Coverage Status](https://img.shields.io/coveralls/SnapKit/Masonry.svg?style=flat-square)](https://coveralls.io/r/SnapKit/Masonry) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![Pod Version](https://img.shields.io/cocoapods/v/Masonry.svg?style=flat) **Masonry is still actively maintained, we are committed to fixing bugs and merging good quality PRs from the wider community. However if you're using Swift in your project, we recommend using [SnapKit](https://github.com/SnapKit/SnapKit) as it provides better type safety with a simpler API.** Masonry is a light-weight layout framework which wraps AutoLayout with a nicer syntax. Masonry has its own layout DSL which provides a chainable way of describing your NSLayoutConstraints which results in layout code that is more concise and readable. Masonry supports iOS and Mac OS X. For examples take a look at the **Masonry iOS Examples** project in the Masonry workspace. You will need to run `pod install` after downloading. ## What's wrong with NSLayoutConstraints? Under the hood Auto Layout is a powerful and flexible way of organising and laying out your views. However creating constraints from code is verbose and not very descriptive. Imagine a simple example in which you want to have a view fill its superview but inset by 10 pixels on every side ```obj-c UIView *superview = self.view; UIView *view1 = [[UIView alloc] init]; view1.translatesAutoresizingMaskIntoConstraints = NO; view1.backgroundColor = [UIColor greenColor]; [superview addSubview:view1]; UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10); [superview addConstraints:@[ //view1 constraints [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeTop multiplier:1.0 constant:padding.top], [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeLeft relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeLeft multiplier:1.0 constant:padding.left], [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-padding.bottom], [NSLayoutConstraint constraintWithItem:view1 attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:superview attribute:NSLayoutAttributeRight multiplier:1 constant:-padding.right], ]]; ``` Even with such a simple example the code needed is quite verbose and quickly becomes unreadable when you have more than 2 or 3 views. Another option is to use Visual Format Language (VFL), which is a bit less long winded. However the ASCII type syntax has its own pitfalls and its also a bit harder to animate as `NSLayoutConstraint constraintsWithVisualFormat:` returns an array. ## Prepare to meet your Maker! Heres the same constraints created using MASConstraintMaker ```obj-c UIEdgeInsets padding = UIEdgeInsetsMake(10, 10, 10, 10); [view1 mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(superview.mas_top).with.offset(padding.top); //with is an optional semantic filler make.left.equalTo(superview.mas_left).with.offset(padding.left); make.bottom.equalTo(superview.mas_bottom).with.offset(-padding.bottom); make.right.equalTo(superview.mas_right).with.offset(-padding.right); }]; ``` Or even shorter ```obj-c [view1 mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(superview).with.insets(padding); }]; ``` Also note in the first example we had to add the constraints to the superview `[superview addConstraints:...`. Masonry however will automagically add constraints to the appropriate view. Masonry will also call `view1.translatesAutoresizingMaskIntoConstraints = NO;` for you. ## Not all things are created equal > `.equalTo` equivalent to **NSLayoutRelationEqual** > `.lessThanOrEqualTo` equivalent to **NSLayoutRelationLessThanOrEqual** > `.greaterThanOrEqualTo` equivalent to **NSLayoutRelationGreaterThanOrEqual** These three equality constraints accept one argument which can be any of the following: #### 1. MASViewAttribute ```obj-c make.centerX.lessThanOrEqualTo(view2.mas_left); ``` MASViewAttribute | NSLayoutAttribute ------------------------- | -------------------------- view.mas_left | NSLayoutAttributeLeft view.mas_right | NSLayoutAttributeRight view.mas_top | NSLayoutAttributeTop view.mas_bottom | NSLayoutAttributeBottom view.mas_leading | NSLayoutAttributeLeading view.mas_trailing | NSLayoutAttributeTrailing view.mas_width | NSLayoutAttributeWidth view.mas_height | NSLayoutAttributeHeight view.mas_centerX | NSLayoutAttributeCenterX view.mas_centerY | NSLayoutAttributeCenterY view.mas_baseline | NSLayoutAttributeBaseline #### 2. UIView/NSView if you want view.left to be greater than or equal to label.left : ```obj-c //these two constraints are exactly the same make.left.greaterThanOrEqualTo(label); make.left.greaterThanOrEqualTo(label.mas_left); ``` #### 3. NSNumber Auto Layout allows width and height to be set to constant values. if you want to set view to have a minimum and maximum width you could pass a number to the equality blocks: ```obj-c //width >= 200 && width <= 400 make.width.greaterThanOrEqualTo(@200); make.width.lessThanOrEqualTo(@400) ``` However Auto Layout does not allow alignment attributes such as left, right, centerY etc to be set to constant values. So if you pass a NSNumber for these attributes Masonry will turn these into constraints relative to the view’s superview ie: ```obj-c //creates view.left = view.superview.left + 10 make.left.lessThanOrEqualTo(@10) ``` Instead of using NSNumber, you can use primitives and structs to build your constraints, like so: ```obj-c make.top.mas_equalTo(42); make.height.mas_equalTo(20); make.size.mas_equalTo(CGSizeMake(50, 100)); make.edges.mas_equalTo(UIEdgeInsetsMake(10, 0, 10, 0)); make.left.mas_equalTo(view).mas_offset(UIEdgeInsetsMake(10, 0, 10, 0)); ``` By default, macros which support [autoboxing](https://en.wikipedia.org/wiki/Autoboxing#Autoboxing) are prefixed with `mas_`. Unprefixed versions are available by defining `MAS_SHORTHAND_GLOBALS` before importing Masonry. #### 4. NSArray An array of a mixture of any of the previous types ```obj-c make.height.equalTo(@[view1.mas_height, view2.mas_height]); make.height.equalTo(@[view1, view2]); make.left.equalTo(@[view1, @100, view3.right]); ```` ## Learn to prioritize > `.priority` allows you to specify an exact priority > `.priorityHigh` equivalent to **UILayoutPriorityDefaultHigh** > `.priorityMedium` is half way between high and low > `.priorityLow` equivalent to **UILayoutPriorityDefaultLow** Priorities are can be tacked on to the end of a constraint chain like so: ```obj-c make.left.greaterThanOrEqualTo(label.mas_left).with.priorityLow(); make.top.equalTo(label.mas_top).with.priority(600); ``` ## Composition, composition, composition Masonry also gives you a few convenience methods which create multiple constraints at the same time. These are called MASCompositeConstraints #### edges ```obj-c // make top, left, bottom, right equal view2 make.edges.equalTo(view2); // make top = superview.top + 5, left = superview.left + 10, // bottom = superview.bottom - 15, right = superview.right - 20 make.edges.equalTo(superview).insets(UIEdgeInsetsMake(5, 10, 15, 20)) ``` #### size ```obj-c // make width and height greater than or equal to titleLabel make.size.greaterThanOrEqualTo(titleLabel) // make width = superview.width + 100, height = superview.height - 50 make.size.equalTo(superview).sizeOffset(CGSizeMake(100, -50)) ``` #### center ```obj-c // make centerX and centerY = button1 make.center.equalTo(button1) // make centerX = superview.centerX - 5, centerY = superview.centerY + 10 make.center.equalTo(superview).centerOffset(CGPointMake(-5, 10)) ``` You can chain view attributes for increased readability: ```obj-c // All edges but the top should equal those of the superview make.left.right.and.bottom.equalTo(superview); make.top.equalTo(otherView); ``` ## Hold on for dear life Sometimes you need modify existing constraints in order to animate or remove/replace constraints. In Masonry there are a few different approaches to updating constraints. #### 1. References You can hold on to a reference of a particular constraint by assigning the result of a constraint make expression to a local variable or a class property. You could also reference multiple constraints by storing them away in an array. ```obj-c // in public/private interface @property (nonatomic, strong) MASConstraint *topConstraint; ... // when making constraints [view1 mas_makeConstraints:^(MASConstraintMaker *make) { self.topConstraint = make.top.equalTo(superview.mas_top).with.offset(padding.top); make.left.equalTo(superview.mas_left).with.offset(padding.left); }]; ... // then later you can call [self.topConstraint uninstall]; ``` #### 2. mas_updateConstraints Alternatively if you are only updating the constant value of the constraint you can use the convience method `mas_updateConstraints` instead of `mas_makeConstraints` ```obj-c // this is Apple's recommended place for adding/updating constraints // this method can get called multiple times in response to setNeedsUpdateConstraints // which can be called by UIKit internally or in your code if you need to trigger an update to your constraints - (void)updateConstraints { [self.growingButton mas_updateConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self); make.width.equalTo(@(self.buttonSize.width)).priorityLow(); make.height.equalTo(@(self.buttonSize.height)).priorityLow(); make.width.lessThanOrEqualTo(self); make.height.lessThanOrEqualTo(self); }]; //according to apple super should be called at end of method [super updateConstraints]; } ``` ### 3. mas_remakeConstraints `mas_updateConstraints` is useful for updating a set of constraints, but doing anything beyond updating constant values can get exhausting. That's where `mas_remakeConstraints` comes in. `mas_remakeConstraints` is similar to `mas_updateConstraints`, but instead of updating constant values, it will remove all of its constraints before installing them again. This lets you provide different constraints without having to keep around references to ones which you want to remove. ```obj-c - (void)changeButtonPosition { [self.button mas_remakeConstraints:^(MASConstraintMaker *make) { make.size.equalTo(self.buttonSize); if (topLeft) { make.top.and.left.offset(10); } else { make.bottom.and.right.offset(-10); } }]; } ``` You can find more detailed examples of all three approaches in the **Masonry iOS Examples** project. ## When the ^&*!@ hits the fan! Laying out your views doesn't always goto plan. So when things literally go pear shaped, you don't want to be looking at console output like this: ```obj-c Unable to simultaneously satisfy constraints.....blah blah blah.... ( "=5000)]>", "", "", "" ) Will attempt to recover by breaking constraint =5000)]> ``` Masonry adds a category to NSLayoutConstraint which overrides the default implementation of `- (NSString *)description`. Now you can give meaningful names to views and constraints, and also easily pick out the constraints created by Masonry. which means your console output can now look like this: ```obj-c Unable to simultaneously satisfy constraints......blah blah blah.... ( "", "= 5000>", "", "" ) Will attempt to recover by breaking constraint = 5000> ``` For an example of how to set this up take a look at the **Masonry iOS Examples** project in the Masonry workspace. ## Where should I create my constraints? ```objc @implementation DIYCustomView - (id)init { self = [super init]; if (!self) return nil; // --- Create your views here --- self.button = [[UIButton alloc] init]; return self; } // tell UIKit that you are using AutoLayout + (BOOL)requiresConstraintBasedLayout { return YES; } // this is Apple's recommended place for adding/updating constraints - (void)updateConstraints { // --- remake/update constraints here [self.button remakeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(@(self.buttonSize.width)); make.height.equalTo(@(self.buttonSize.height)); }]; //according to apple super should be called at end of method [super updateConstraints]; } - (void)didTapButton:(UIButton *)button { // --- Do your changes ie change variables that affect your layout etc --- self.buttonSize = CGSize(200, 200); // tell constraints they need updating [self setNeedsUpdateConstraints]; } @end ``` ## Installation Use the [orsome](http://www.youtube.com/watch?v=YaIZF8uUTtk) [CocoaPods](http://github.com/CocoaPods/CocoaPods). In your Podfile >`pod 'Masonry'` If you want to use masonry without all those pesky 'mas_' prefixes. Add #define MAS_SHORTHAND to your prefix.pch before importing Masonry >`#define MAS_SHORTHAND` Get busy Masoning >`#import "Masonry.h"` ## Code Snippets Copy the included code snippets to ``~/Library/Developer/Xcode/UserData/CodeSnippets`` to write your masonry blocks at lightning speed! `mas_make` -> ` [<#view#> mas_makeConstraints:^(MASConstraintMaker *make) { <#code#> }];` `mas_update` -> ` [<#view#> mas_updateConstraints:^(MASConstraintMaker *make) { <#code#> }];` `mas_remake` -> ` [<#view#> mas_remakeConstraints:^(MASConstraintMaker *make) { <#code#> }];` ## Features * Not limited to subset of Auto Layout. Anything NSLayoutConstraint can do, Masonry can do too! * Great debug support, give your views and constraints meaningful names. * Constraints read like sentences. * No crazy macro magic. Masonry won't pollute the global namespace with macros. * Not string or dictionary based and hence you get compile time checking. ## TODO * Eye candy * Mac example project * More tests and examples ================================================ FILE: Pods/OCMock/License.txt ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: Pods/OCMock/README.md ================================================ OCMock ====== ![Build Status](https://github.com/erikdoe/ocmock/actions/workflows/build-and-test.yaml/badge.svg?branch=master) OCMock is an Objective-C implementation of mock objects. For downloads, documentation, and support please visit [ocmock.org](http://ocmock.org/). ================================================ FILE: Pods/OCMock/Source/OCMock/NSInvocation+OCMAdditions.h ================================================ /* * Copyright (c) 2006-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface NSInvocation(OCMAdditions) + (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)arguments; - (void)retainObjectArgumentsExcludingObject:(id)objectToExclude; - (id)getArgumentAtIndexAsObject:(NSInteger)argIndex; - (NSString *)invocationDescription; - (NSString *)argumentDescriptionAtIndex:(NSInteger)argIndex; - (NSString *)objectDescriptionAtIndex:(NSInteger)anInt; - (NSString *)charDescriptionAtIndex:(NSInteger)anInt; - (NSString *)unsignedCharDescriptionAtIndex:(NSInteger)anInt; - (NSString *)intDescriptionAtIndex:(NSInteger)anInt; - (NSString *)unsignedIntDescriptionAtIndex:(NSInteger)anInt; - (NSString *)shortDescriptionAtIndex:(NSInteger)anInt; - (NSString *)unsignedShortDescriptionAtIndex:(NSInteger)anInt; - (NSString *)longDescriptionAtIndex:(NSInteger)anInt; - (NSString *)unsignedLongDescriptionAtIndex:(NSInteger)anInt; - (NSString *)longLongDescriptionAtIndex:(NSInteger)anInt; - (NSString *)unsignedLongLongDescriptionAtIndex:(NSInteger)anInt; - (NSString *)doubleDescriptionAtIndex:(NSInteger)anInt; - (NSString *)floatDescriptionAtIndex:(NSInteger)anInt; - (NSString *)structDescriptionAtIndex:(NSInteger)anInt; - (NSString *)pointerDescriptionAtIndex:(NSInteger)anInt; - (NSString *)cStringDescriptionAtIndex:(NSInteger)anInt; - (NSString *)selectorDescriptionAtIndex:(NSInteger)anInt; - (BOOL)methodIsInInitFamily; - (BOOL)methodIsInCreateFamily; @end ================================================ FILE: Pods/OCMock/Source/OCMock/NSInvocation+OCMAdditions.m ================================================ /* * Copyright (c) 2006-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "NSInvocation+OCMAdditions.h" #import "NSMethodSignature+OCMAdditions.h" #import "OCMArg.h" #import "OCMFunctionsPrivate.h" #if(TARGET_OS_OSX && (!defined(__MAC_10_10) || __MAC_OS_X_VERSION_MIN_REQUIRED < __MAC_10_10)) || \ (TARGET_OS_IPHONE && (!defined(__IPHONE_8_0) || __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0)) static BOOL OCMObjectIsClass(id object) { return class_isMetaClass(object_getClass(object)); } #define object_isClass OCMObjectIsClass #endif static NSString *const OCMArgAnyPointerDescription = @"<[OCMArg anyPointer]>"; @implementation NSInvocation(OCMAdditions) + (NSInvocation *)invocationForBlock:(id)block withArguments:(NSArray *)arguments { NSMethodSignature *sig = [NSMethodSignature signatureForBlock:block]; NSInvocation *inv = [self invocationWithMethodSignature:sig]; NSUInteger numArgsRequired = sig.numberOfArguments - 1; if((arguments != nil) && ([arguments count] != numArgsRequired)) [NSException raise:NSInvalidArgumentException format:@"Specified too few arguments for block; expected %lu arguments.", (unsigned long)numArgsRequired]; for(NSUInteger i = 0, j = 1; i < numArgsRequired; ++i, ++j) { id arg = [arguments objectAtIndex:i]; [inv setArgumentWithObject:arg atIndex:j]; } return inv; } static NSString *const OCMRetainedObjectArgumentsKey = @"OCMRetainedObjectArgumentsKey"; - (void)retainObjectArgumentsExcludingObject:(id)objectToExclude { if(objc_getAssociatedObject(self, OCMRetainedObjectArgumentsKey) != nil) { // looks like we've retained the arguments already; do nothing else return; } NSMutableArray *retainedArguments = [[NSMutableArray alloc] init]; id target = [self target]; if((target != nil) && (target != objectToExclude) && !object_isClass(target)) { // Bad things will happen if the target is a block since it's not being // copied. There isn't a very good way to tell if an invocation's target // is a block though (the argument type at index 0 is always "@" even if // the target is a Class or block), and in practice it's OK since you // can't mock a block. [retainedArguments addObject:target]; } NSUInteger numberOfArguments = [[self methodSignature] numberOfArguments]; for(NSUInteger index = 2; index < numberOfArguments; index++) { const char *argumentType = [[self methodSignature] getArgumentTypeAtIndex:index]; if(OCMIsObjectType(argumentType)) { id argument; [self getArgument:&argument atIndex:index]; if((argument != nil) && (argument != objectToExclude)) { if(OCMIsBlockType(argumentType) && OCMIsBlock(argument)) { // The argument's type is block and the passed argument is a block. In this // case we can't retain the argument because it might be stack block, which // must be copied. Further, non-escaping blocks have a lifetime that is stack- // based and they treat copy/release as a no-op. Keeping a reference to these // would result in a dangling pointer, which is why they are ignored here. // Note: even when the argument's type is block the argument could be // something else, e.g. an instance of OCMConstraint. Such cases are handled // like regular objects in the last else branch below. if(OCMIsNonEscapingBlock(argument) == NO) { id blockArgument = [argument copy]; [retainedArguments addObject:blockArgument]; [blockArgument release]; } } else if(OCMIsClassType(argumentType) && object_isClass(argument)) { // The argument's type is class and the passed argument is a class. In this // case do not retain the argument. Note: Even though the type is class the // argument could be a non-class, e.g. an instance of OCMArg. } else { [retainedArguments addObject:argument]; } } } } objc_setAssociatedObject(self, OCMRetainedObjectArgumentsKey, retainedArguments, OBJC_ASSOCIATION_RETAIN); [retainedArguments release]; } - (void)setArgumentWithObject:(id)arg atIndex:(NSInteger)idx { const char *typeEncoding = [[self methodSignature] getArgumentTypeAtIndex:idx]; if((arg == nil) || ([arg respondsToSelector:@selector(isKindOfClass:)] && [arg isKindOfClass:[NSNull class]])) { if(OCMIsPointerType(typeEncoding)) { void *nullPtr = NULL; [self setArgument:&nullPtr atIndex:idx]; } else if(typeEncoding[0] == '@') { id nilObj = nil; [self setArgument:&nilObj atIndex:idx]; } else if(OCMNumberTypeForObjCType(typeEncoding)) { NSUInteger argSize; NSGetSizeAndAlignment(typeEncoding, &argSize, NULL); void *argBuffer = calloc(1, argSize); [self setArgument:argBuffer atIndex:idx]; free(argBuffer); } else { [NSException raise:NSInvalidArgumentException format:@"Unable to create default value for type '%s'.", typeEncoding]; } } else if(OCMIsObjectType(typeEncoding)) { [self setArgument:&arg atIndex:idx]; } else { if(![arg isKindOfClass:[NSValue class]]) [NSException raise:NSInvalidArgumentException format:@"Argument '%@' should be boxed in NSValue.", arg]; char const *valEncoding = [arg objCType]; /// @note Here we allow any data pointer to be passed as a void pointer and /// any numerical types to be passed as arguments to the block. BOOL takesVoidPtr = !strcmp(typeEncoding, "^v") && valEncoding[0] == '^'; BOOL takesNumber = OCMNumberTypeForObjCType(typeEncoding) && OCMNumberTypeForObjCType(valEncoding); if(!takesVoidPtr && !takesNumber && !OCMEqualTypesAllowingOpaqueStructs(typeEncoding, valEncoding)) [NSException raise:NSInvalidArgumentException format:@"Argument type mismatch; type of argument required is '%s' but type of value provided is '%s'", typeEncoding, valEncoding]; NSUInteger argSize; NSGetSizeAndAlignment(typeEncoding, &argSize, NULL); void *argBuffer = malloc(argSize); [arg getValue:argBuffer]; [self setArgument:argBuffer atIndex:idx]; free(argBuffer); } } - (id)getArgumentAtIndexAsObject:(NSInteger)argIndex { const char *argType = OCMTypeWithoutQualifiers([[self methodSignature] getArgumentTypeAtIndex:(NSUInteger)argIndex]); if((strlen(argType) > 1) && (strchr("{^", argType[0]) == NULL) && (strcmp("@?", argType) != 0)) [NSException raise:NSInvalidArgumentException format:@"Cannot handle argument type '%s'.", argType]; if(OCMIsObjectType(argType)) { id value; [self getArgument:&value atIndex:argIndex]; return value; } switch(argType[0]) { case ':': { SEL s = (SEL)0; [self getArgument:&s atIndex:argIndex]; return [NSValue valueWithBytes:&s objCType:":"]; } case 'i': { int value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 's': { short value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'l': { long value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'q': { long long value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'c': { char value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'C': { unsigned char value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'I': { unsigned int value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'S': { unsigned short value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'L': { unsigned long value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'Q': { unsigned long long value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'f': { float value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'd': { double value; [self getArgument:&value atIndex:argIndex]; return @(value); } case 'D': { long double value; [self getArgument:&value atIndex:argIndex]; return [NSValue valueWithBytes:&value objCType:@encode(__typeof__(value))]; } case 'B': { bool value; [self getArgument:&value atIndex:argIndex]; return @(value); } case '^': case '*': { void *value = NULL; [self getArgument:&value atIndex:argIndex]; return [NSValue valueWithPointer:value]; } case '{': // structure { NSUInteger argSize; NSGetSizeAndAlignment([[self methodSignature] getArgumentTypeAtIndex:(NSUInteger)argIndex], &argSize, NULL); if(argSize == 0) // TODO: Can this happen? Is frameLength a good choice in that case? argSize = [[self methodSignature] frameLength]; NSMutableData *argumentData = [[[NSMutableData alloc] initWithLength:argSize] autorelease]; [self getArgument:[argumentData mutableBytes] atIndex:argIndex]; return [NSValue valueWithBytes:[argumentData bytes] objCType:argType]; } } [NSException raise:NSInvalidArgumentException format:@"Argument type '%s' not supported", argType]; return nil; } - (NSString *)invocationDescription { NSMethodSignature *methodSignature = [self methodSignature]; NSUInteger numberOfArgs = [methodSignature numberOfArguments]; if(numberOfArgs == 2) return NSStringFromSelector([self selector]); NSArray *selectorParts = [NSStringFromSelector([self selector]) componentsSeparatedByString:@":"]; NSMutableString *description = [[NSMutableString alloc] init]; NSUInteger i; for(i = 2; i < numberOfArgs; i++) { [description appendFormat:@"%@%@:", (i > 2 ? @" " : @""), [selectorParts objectAtIndex:(i - 2)]]; [description appendString:[self argumentDescriptionAtIndex:(NSInteger)i]]; } return [description autorelease]; } - (NSString *)argumentDescriptionAtIndex:(NSInteger)argIndex { const char *argType = OCMTypeWithoutQualifiers([[self methodSignature] getArgumentTypeAtIndex:(NSUInteger)argIndex]); switch(*argType) { case '@': return [self objectDescriptionAtIndex:argIndex]; case 'B': return [self boolDescriptionAtIndex:argIndex]; case 'c': return [self charDescriptionAtIndex:argIndex]; case 'C': return [self unsignedCharDescriptionAtIndex:argIndex]; case 'i': return [self intDescriptionAtIndex:argIndex]; case 'I': return [self unsignedIntDescriptionAtIndex:argIndex]; case 's': return [self shortDescriptionAtIndex:argIndex]; case 'S': return [self unsignedShortDescriptionAtIndex:argIndex]; case 'l': return [self longDescriptionAtIndex:argIndex]; case 'L': return [self unsignedLongDescriptionAtIndex:argIndex]; case 'q': return [self longLongDescriptionAtIndex:argIndex]; case 'Q': return [self unsignedLongLongDescriptionAtIndex:argIndex]; case 'd': return [self doubleDescriptionAtIndex:argIndex]; case 'f': return [self floatDescriptionAtIndex:argIndex]; case 'D': return [self longDoubleDescriptionAtIndex:argIndex]; case '{': return [self structDescriptionAtIndex:argIndex]; case '^': return [self pointerDescriptionAtIndex:argIndex]; case '*': return [self cStringDescriptionAtIndex:argIndex]; case ':': return [self selectorDescriptionAtIndex:argIndex]; default: return [@""]; // avoid confusion with trigraphs... } } - (NSString *)objectDescriptionAtIndex:(NSInteger)anInt { id object; [self getArgument:&object atIndex:anInt]; if(object == nil) return @"nil"; else if(![object isProxy] && [object isKindOfClass:[NSString class]]) return [NSString stringWithFormat:@"@\"%@\"", [object description]]; else // The description cannot be nil, if it is then replace it return [object description] ?: @""; } - (NSString *)boolDescriptionAtIndex:(NSInteger)anInt { bool value; [self getArgument:&value atIndex:anInt]; return value ? @"YES" : @"NO"; } - (NSString *)charDescriptionAtIndex:(NSInteger)anInt { unsigned char buffer[128]; memset(buffer, 0x0, 128); [self getArgument:&buffer atIndex:anInt]; // If there's only one character in the buffer, and it's 0 or 1, then we have a BOOL if(buffer[1] == '\0' && (buffer[0] == 0 || buffer[0] == 1)) return (buffer[0] == 1 ? @"YES" : @"NO"); else return [NSString stringWithFormat:@"'%c'", *buffer]; } - (NSString *)unsignedCharDescriptionAtIndex:(NSInteger)anInt { unsigned char buffer[128]; memset(buffer, 0x0, 128); [self getArgument:&buffer atIndex:anInt]; return [NSString stringWithFormat:@"'%c'", *buffer]; } - (NSString *)intDescriptionAtIndex:(NSInteger)anInt { int intValue; [self getArgument:&intValue atIndex:anInt]; return [NSString stringWithFormat:@"%d", intValue]; } - (NSString *)unsignedIntDescriptionAtIndex:(NSInteger)anInt { unsigned int intValue; [self getArgument:&intValue atIndex:anInt]; return [NSString stringWithFormat:@"%d", intValue]; } - (NSString *)shortDescriptionAtIndex:(NSInteger)anInt { short shortValue; [self getArgument:&shortValue atIndex:anInt]; return [NSString stringWithFormat:@"%hi", shortValue]; } - (NSString *)unsignedShortDescriptionAtIndex:(NSInteger)anInt { unsigned short shortValue; [self getArgument:&shortValue atIndex:anInt]; return [NSString stringWithFormat:@"%hu", shortValue]; } - (NSString *)longDescriptionAtIndex:(NSInteger)anInt { long longValue; [self getArgument:&longValue atIndex:anInt]; return [NSString stringWithFormat:@"%ld", longValue]; } - (NSString *)unsignedLongDescriptionAtIndex:(NSInteger)anInt { unsigned long longValue; [self getArgument:&longValue atIndex:anInt]; return [NSString stringWithFormat:@"%lu", longValue]; } - (NSString *)longLongDescriptionAtIndex:(NSInteger)anInt { long long longLongValue; [self getArgument:&longLongValue atIndex:anInt]; return [NSString stringWithFormat:@"%qi", longLongValue]; } - (NSString *)unsignedLongLongDescriptionAtIndex:(NSInteger)anInt { unsigned long long longLongValue; [self getArgument:&longLongValue atIndex:anInt]; return [NSString stringWithFormat:@"%qu", longLongValue]; } - (NSString *)doubleDescriptionAtIndex:(NSInteger)anInt { double doubleValue; [self getArgument:&doubleValue atIndex:anInt]; return [NSString stringWithFormat:@"%f", doubleValue]; } - (NSString *)floatDescriptionAtIndex:(NSInteger)anInt { float floatValue; [self getArgument:&floatValue atIndex:anInt]; return [NSString stringWithFormat:@"%f", floatValue]; } - (NSString *)longDoubleDescriptionAtIndex:(NSInteger)anInt { long double longDoubleValue; [self getArgument:&longDoubleValue atIndex:anInt]; return [NSString stringWithFormat:@"%Lf", longDoubleValue]; } - (NSString *)structDescriptionAtIndex:(NSInteger)anInt { return [NSString stringWithFormat:@"(%@)", [[self getArgumentAtIndexAsObject:anInt] description]]; } - (NSString *)pointerDescriptionAtIndex:(NSInteger)anInt { void *buffer; [self getArgument:&buffer atIndex:anInt]; if(buffer == [OCMArg anyPointer]) return OCMArgAnyPointerDescription; else return [NSString stringWithFormat:@"%p", buffer]; } - (NSString *)cStringDescriptionAtIndex:(NSInteger)anInt { char *cStringPtr; [self getArgument:&cStringPtr atIndex:anInt]; if(cStringPtr == [OCMArg anyPointer]) { return OCMArgAnyPointerDescription; } else { char buffer[104]; strlcpy(buffer, cStringPtr, sizeof(buffer)); strlcpy(buffer + 100, "...", (sizeof(buffer) - 100)); return [NSString stringWithFormat:@"\"%s\"", buffer]; } } - (NSString *)selectorDescriptionAtIndex:(NSInteger)anInt { SEL selectorValue; [self getArgument:&selectorValue atIndex:anInt]; return [NSString stringWithFormat:@"@selector(%@)", NSStringFromSelector(selectorValue)]; } - (BOOL)isMethodFamily:(NSString *)family { // Definitions here: https://clang.llvm.org/docs/AutomaticReferenceCounting.html#method-families NSMethodSignature *signature = [self methodSignature]; if(OCMIsObjectType(signature.methodReturnType) == NO) { return NO; } NSString *selString = NSStringFromSelector([self selector]); NSRange underscoreRange = [selString rangeOfString:@"^_*" options:NSRegularExpressionSearch]; selString = [selString substringFromIndex:NSMaxRange(underscoreRange)]; if([selString hasPrefix:family] == NO) { return NO; } NSUInteger familyLength = [family length]; if(([selString length] > familyLength) && ([[NSCharacterSet lowercaseLetterCharacterSet] characterIsMember:[selString characterAtIndex:familyLength]])) { return NO; } return YES; } - (BOOL)methodIsInInitFamily { return [self isMethodFamily:@"init"]; } - (BOOL)methodIsInCreateFamily { return [self isMethodFamily:@"alloc"] || [self isMethodFamily:@"copy"] || [self isMethodFamily:@"mutableCopy"] || [self isMethodFamily:@"new"]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface NSMethodSignature(OCMAdditions) + (NSMethodSignature *)signatureForDynamicPropertyAccessedWithSelector:(SEL)selector inClass:(Class)aClass; + (NSMethodSignature *)signatureForBlock:(id)block; - (BOOL)usesSpecialStructureReturn; - (NSString *)fullTypeString; - (const char *)fullObjCTypes; @end ================================================ FILE: Pods/OCMock/Source/OCMock/NSMethodSignature+OCMAdditions.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "NSMethodSignature+OCMAdditions.h" #import "OCMFunctionsPrivate.h" @implementation NSMethodSignature(OCMAdditions) #pragma mark Signatures for dynamic properties + (NSMethodSignature *)signatureForDynamicPropertyAccessedWithSelector:(SEL)selector inClass:(Class)aClass { BOOL isGetter = YES; objc_property_t property = [self propertyMatchingSelector:selector inClass:aClass isGetter:&isGetter]; if(property == NULL) return nil; const char *propertyAttributesString = property_getAttributes(property); NSArray *propertyAttributes = [[NSString stringWithCString:propertyAttributesString encoding:NSASCIIStringEncoding] componentsSeparatedByString:@","]; NSString *typeStr = nil; BOOL isDynamic = NO; for(NSString *attribute in propertyAttributes) { if([attribute isEqualToString:@"D"]) isDynamic = YES; else if([attribute hasPrefix:@"T"]) typeStr = [attribute substringFromIndex:1]; } if(!isDynamic) return nil; NSRange r = [typeStr rangeOfString:@"\""]; // incomplete workaround to deal with structs if(r.location != NSNotFound) typeStr = [typeStr substringToIndex:r.location]; NSString *sigStringFormat = isGetter ? @"%@@:" : @"v@:%@"; const char *sigCString = [[NSString stringWithFormat:sigStringFormat, typeStr] cStringUsingEncoding:NSASCIIStringEncoding]; return [NSMethodSignature signatureWithObjCTypes:sigCString]; } + (objc_property_t)propertyMatchingSelector:(SEL)selector inClass:(Class)aClass isGetter:(BOOL *)isGetterPtr { NSString *propertyName = NSStringFromSelector(selector); // first try selector as is aassuming it's a getter objc_property_t property = class_getProperty(aClass, [propertyName cStringUsingEncoding:NSASCIIStringEncoding]); if(property != NULL) { *isGetterPtr = YES; return property; } // try setter next if selector starts with "set" if([propertyName hasPrefix:@"set"]) { propertyName = [propertyName substringFromIndex:@"set".length]; propertyName = [propertyName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:[[propertyName substringToIndex:1] lowercaseString]]; if([propertyName hasSuffix:@":"]) propertyName = [propertyName substringToIndex:[propertyName length] - 1]; property = class_getProperty(aClass, [propertyName cStringUsingEncoding:NSASCIIStringEncoding]); if(property != NULL) { *isGetterPtr = NO; return property; } } // search through properties with custom getter/setter that corresponds to selector unsigned int propertiesCount = 0; objc_property_t *allProperties = class_copyPropertyList(aClass, &propertiesCount); for(unsigned int i = 0; i < propertiesCount; i++) { NSArray *propertyAttributes = [[NSString stringWithCString:property_getAttributes(allProperties[i]) encoding:NSASCIIStringEncoding] componentsSeparatedByString:@","]; for(NSString *attribute in propertyAttributes) { if(([attribute hasPrefix:@"G"] || [attribute hasPrefix:@"S"]) && [[attribute substringFromIndex:1] isEqualToString:propertyName]) { *isGetterPtr = ![attribute hasPrefix:@"S"]; property = allProperties[i]; i = propertiesCount; break; } } } free(allProperties); return property; } #pragma mark Signatures for blocks + (NSMethodSignature *)signatureForBlock:(id)block { /* For a more complete implementation of parsing the block data structure see: * * https://github.com/ebf/CTObjectiveCRuntimeAdditions/tree/master/CTObjectiveCRuntimeAdditions/CTObjectiveCRuntimeAdditions */ struct OCMBlockDef *blockRef = (__bridge struct OCMBlockDef *)block; if(!(blockRef->flags & OCMBlockDescriptionFlagsHasSignature)) return nil; void *signatureLocation = blockRef->descriptor; signatureLocation += sizeof(unsigned long int); signatureLocation += sizeof(unsigned long int); if(blockRef->flags & OCMBlockDescriptionFlagsHasCopyDispose) { signatureLocation += sizeof(void (*)(void *dst, void *src)); signatureLocation += sizeof(void (*)(void *src)); } const char *signature = (*(const char **)signatureLocation); return [NSMethodSignature signatureWithObjCTypes:signature]; } #pragma mark Extended attributes - (BOOL)usesSpecialStructureReturn { const char *types = OCMTypeWithoutQualifiers([self methodReturnType]); if((types == NULL) || (types[0] != '{')) return NO; /* In some cases structures are returned by ref. The rules are complex and depend on the architecture, see: http://sealiesoftware.com/blog/archive/2008/10/30/objc_explain_objc_msgSend_stret.html http://developer.apple.com/library/mac/#documentation/DeveloperTools/Conceptual/LowLevelABI/000-Introduction/introduction.html https://github.com/atgreen/libffi/blob/master/src/x86/ffi64.c http://www.uclibc.org/docs/psABI-x86_64.pdf http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042e/IHI0042E_aapcs.pdf NSMethodSignature knows the details but has no API to return it, though it is in the debugDescription. Horribly kludgy. */ NSRange range = [[self debugDescription] rangeOfString:@"is special struct return? YES"]; return range.length > 0; } - (NSString *)fullTypeString { NSMutableString *typeString = [NSMutableString string]; [typeString appendFormat:@"%s", [self methodReturnType]]; for(NSUInteger i = 0; i < [self numberOfArguments]; i++) [typeString appendFormat:@"%s", [self getArgumentTypeAtIndex:i]]; return typeString; } - (const char *)fullObjCTypes { return [[self fullTypeString] UTF8String]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @class OCObserverMockObject; @interface NSNotificationCenter(OCMAdditions) - (void)addMockObserver:(OCObserverMockObject *)notificationObserver name:(NSString *)notificationName object:(id)notificationSender; @end ================================================ FILE: Pods/OCMock/Source/OCMock/NSNotificationCenter+OCMAdditions.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "NSNotificationCenter+OCMAdditions.h" #import "OCObserverMockObject.h" @implementation NSNotificationCenter(OCMAdditions) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (void)addMockObserver:(OCObserverMockObject *)notificationObserver name:(NSString *)notificationName object:(id)notificationSender { [notificationObserver autoRemoveFromCenter:self]; [self addObserver:notificationObserver selector:@selector(handleNotification:) name:notificationName object:notificationSender]; } #pragma clang diagnostic pop @end ================================================ FILE: Pods/OCMock/Source/OCMock/NSObject+OCMAdditions.h ================================================ /* * Copyright (c) 2013-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface NSObject(OCMAdditions) + (IMP)instanceMethodForwarderForSelector:(SEL)aSelector; + (void)enumerateMethodsInClass:(Class)aClass usingBlock:(void (^)(Class cls, SEL sel))aBlock; @end ================================================ FILE: Pods/OCMock/Source/OCMock/NSObject+OCMAdditions.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import #import "NSMethodSignature+OCMAdditions.h" #import "NSObject+OCMAdditions.h" @implementation NSObject(OCMAdditions) + (IMP)instanceMethodForwarderForSelector:(SEL)aSelector { #ifndef __arm64__ static NSMutableDictionary *_OCMReturnTypeCache; if(_OCMReturnTypeCache == nil) _OCMReturnTypeCache = [[NSMutableDictionary alloc] init]; BOOL needsStructureReturn; void *rawCacheKey[2] = { (void *)self, aSelector }; NSData *cacheKey = [NSData dataWithBytes:rawCacheKey length:sizeof(rawCacheKey)]; NSNumber *cachedValue = [_OCMReturnTypeCache objectForKey:cacheKey]; if(cachedValue == nil) { NSMethodSignature *sig = [self instanceMethodSignatureForSelector:aSelector]; needsStructureReturn = [sig usesSpecialStructureReturn]; [_OCMReturnTypeCache setObject:@(needsStructureReturn) forKey:cacheKey]; } else { needsStructureReturn = [cachedValue boolValue]; } if(needsStructureReturn) return (IMP)_objc_msgForward_stret; #endif return _objc_msgForward; } + (void)enumerateMethodsInClass:(Class)aClass usingBlock:(void (^)(Class cls, SEL sel))aBlock { for(Class cls = aClass; cls != nil; cls = class_getSuperclass(cls)) { Method *methodList = class_copyMethodList(cls, NULL); if(methodList == NULL) continue; for(Method *mPtr = methodList; *mPtr != NULL; mPtr++) { SEL sel = method_getName(*mPtr); aBlock(cls, sel); } free(methodList); } } @end ================================================ FILE: Pods/OCMock/Source/OCMock/NSValue+OCMAdditions.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface NSValue(OCMAdditions) - (BOOL)getBytes:(void *)outputBuf objCType:(const char *)targetType; @end ================================================ FILE: Pods/OCMock/Source/OCMock/NSValue+OCMAdditions.m ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "NSValue+OCMAdditions.h" #import "OCMFunctionsPrivate.h" @implementation NSValue(OCMAdditions) static NSNumber *OCMNumberForValue(NSValue *value) { #define CREATE_NUM(_type) ({ _type _v; [value getValue:&_v]; @(_v); }) switch([value objCType][0]) { case 'c': return CREATE_NUM(char); case 'C': return CREATE_NUM(unsigned char); case 'B': return CREATE_NUM(bool); case 's': return CREATE_NUM(short); case 'S': return CREATE_NUM(unsigned short); case 'i': return CREATE_NUM(int); case 'I': return CREATE_NUM(unsigned int); case 'l': return CREATE_NUM(long); case 'L': return CREATE_NUM(unsigned long); case 'q': return CREATE_NUM(long long); case 'Q': return CREATE_NUM(unsigned long long); case 'f': return CREATE_NUM(float); case 'd': return CREATE_NUM(double); default: return nil; } } - (BOOL)getBytes:(void *)outputBuf objCType:(const char *)targetType { /* * See if they are similar number types, and if we can convert losslessly between them. * For the most part, we set things up to use CFNumberGetValue, which returns false if * conversion will be lossy. */ CFNumberType inputType = OCMNumberTypeForObjCType([self objCType]); CFNumberType outputType = OCMNumberTypeForObjCType(targetType); if(inputType == 0 || outputType == 0) // one or both are non-number types return NO; NSNumber *inputNumber = [self isKindOfClass:[NSNumber class]] ? (NSNumber *)self : OCMNumberForValue(self); /* * Due to some legacy, back-compatible requirements in CFNumber.c, CFNumberGetValue can return true for * some conversions which should not be allowed (by reading source, conversions from integer types to * 8-bit or 16-bit integer types). So, check ourselves. */ long long min; long long max; long long val = [inputNumber longLongValue]; switch(targetType[0]) { case 'B': case 'c': min = CHAR_MIN; max = CHAR_MAX; break; case 'C': min = 0; max = UCHAR_MAX; break; case 's': min = SHRT_MIN; max = SHRT_MAX; break; case 'S': min = 0; max = USHRT_MAX; break; default: min = LLONG_MIN; max = LLONG_MAX; break; } if(val < min || val > max) return NO; /* Get the number, and return NO if the value was out of range or conversion was lossy */ return CFNumberGetValue((CFNumberRef)inputNumber, outputType, outputBuf); } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCClassMockObject.h ================================================ /* * Copyright (c) 2005-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMockObject.h" @interface OCClassMockObject : OCMockObject { Class mockedClass; Class originalMetaClass; Class classCreatedForNewMetaClass; } - (id)initWithClass:(Class)aClass; - (Class)mockedClass; - (Class)mockObjectClass; // since -class returns the mockedClass - (void)assertClassIsSupported:(Class)aClass; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCClassMockObject.m ================================================ /* * Copyright (c) 2005-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "NSMethodSignature+OCMAdditions.h" #import "NSObject+OCMAdditions.h" #import "OCClassMockObject.h" #import "OCMFunctionsPrivate.h" #import "OCMInvocationStub.h" @interface NSObject (OCMClassMockingSupport) + (BOOL)supportsMocking:(NSString **)reason; @end @implementation OCClassMockObject #pragma mark Initialisers, description, accessors, etc. - (id)initWithClass:(Class)aClass { [self assertClassIsSupported:aClass]; [super init]; mockedClass = aClass; [self prepareClassForClassMethodMocking]; return self; } - (void)dealloc { [self stopMocking]; [super dealloc]; } - (NSString *)description { return [NSString stringWithFormat:@"OCClassMockObject(%@)", NSStringFromClass(mockedClass)]; } - (Class)mockedClass { return mockedClass; } - (void)assertClassIsSupported:(Class)aClass { if(aClass == Nil) [NSException raise:NSInvalidArgumentException format:@"Class cannot be Nil."]; if([aClass respondsToSelector:@selector(supportsMocking:)]) { NSString *reason = nil; if(![aClass supportsMocking:&reason]) [NSException raise:NSInvalidArgumentException format:@"Class %@ does not support mocking: %@", aClass, reason]; } } #pragma mark Extending/overriding superclass behaviour - (void)stopMocking { if(originalMetaClass != nil) { [self stopMockingClassMethods]; } if(classCreatedForNewMetaClass != nil) { OCMDisposeSubclass(classCreatedForNewMetaClass); classCreatedForNewMetaClass = nil; } [super stopMocking]; } - (void)stopMockingClassMethods { OCMSetAssociatedMockForClass(nil, mockedClass); object_setClass(mockedClass, originalMetaClass); originalMetaClass = nil; /* created meta class will be disposed later because partial mocks create another subclass depending on it */ } - (void)addStub:(OCMInvocationStub *)aStub { [super addStub:aStub]; if([aStub recordedAsClassMethod]) [self setupForwarderForClassMethodSelector:[[aStub recordedInvocation] selector]]; } #pragma mark Class method mocking - (void)prepareClassForClassMethodMocking { /* the runtime and OCMock depend on string and array; we don't intercept methods on them to avoid endless loops */ if([[mockedClass class] isSubclassOfClass:[NSString class]] || [[mockedClass class] isSubclassOfClass:[NSArray class]]) return; /* trying to replace class methods on NSManagedObject and subclasses of it doesn't work; see #339 */ if([mockedClass isSubclassOfClass:objc_getClass("NSManagedObject")]) return; /* if there is another mock for this exact class, stop it */ id otherMock = OCMGetAssociatedMockForClass(mockedClass, NO); if(otherMock != nil) [otherMock stopMockingClassMethods]; OCMSetAssociatedMockForClass(self, mockedClass); /* dynamically create a subclass and use its meta class as the meta class for the mocked class */ classCreatedForNewMetaClass = OCMCreateSubclass(mockedClass, mockedClass); originalMetaClass = object_getClass(mockedClass); id newMetaClass = object_getClass(classCreatedForNewMetaClass); /* create a dummy initialize method */ Method myDummyInitializeMethod = class_getInstanceMethod([self mockObjectClass], @selector(initializeForClassObject)); const char *initializeTypes = method_getTypeEncoding(myDummyInitializeMethod); IMP myDummyInitializeIMP = method_getImplementation(myDummyInitializeMethod); class_addMethod(newMetaClass, @selector(initialize), myDummyInitializeIMP, initializeTypes); object_setClass(mockedClass, newMetaClass); // only after dummy initialize is installed (iOS9) /* point forwardInvocation: of the object to the implementation in the mock */ Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForClassObject:)); IMP myForwardIMP = method_getImplementation(myForwardMethod); class_addMethod(newMetaClass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod)); /* adding forwarder for most class methods (instance methods on meta class) to allow for verify after run */ NSArray *methodsNotToForward = @[ @"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"isBlock", @"instanceMethodForwarderForSelector:", @"instanceMethodSignatureForSelector:", @"resolveClassMethod:" ]; void (^setupForwarderFiltered)(Class, SEL) = ^(Class cls, SEL sel) { if((cls == object_getClass([NSObject class])) || (cls == [NSObject class]) || (cls == object_getClass(cls))) return; if(OCMIsApplePrivateMethod(cls, sel)) return; if([methodsNotToForward containsObject:NSStringFromSelector(sel)]) return; @try { [self setupForwarderForClassMethodSelector:sel]; } @catch(NSException *e) { // ignore for now } }; [NSObject enumerateMethodsInClass:originalMetaClass usingBlock:setupForwarderFiltered]; } - (void)setupForwarderForClassMethodSelector:(SEL)selector { SEL aliasSelector = OCMAliasForOriginalSelector(selector); if(class_getClassMethod(mockedClass, aliasSelector) != NULL) return; Method originalMethod = class_getClassMethod(mockedClass, selector); IMP originalIMP = method_getImplementation(originalMethod); const char *types = method_getTypeEncoding(originalMethod); Class metaClass = object_getClass(mockedClass); IMP forwarderIMP = [originalMetaClass instanceMethodForwarderForSelector:selector]; class_addMethod(metaClass, aliasSelector, originalIMP, types); class_replaceMethod(metaClass, selector, forwarderIMP, types); } - (void)forwardInvocationForClassObject:(NSInvocation *)anInvocation { // in here "self" is a reference to the real class, not the mock OCClassMockObject *mock = OCMGetAssociatedMockForClass((Class)self, YES); if(mock == nil) { [NSException raise:NSInternalInconsistencyException format:@"No mock for class %@", NSStringFromClass((Class)self)]; } if([mock handleInvocation:anInvocation] == NO) { [anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])]; [anInvocation invoke]; } } - (void)initializeForClassObject { // we really just want to have an implementation so that the superclass's is not called } #pragma mark Proxy API - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSMethodSignature *signature = [mockedClass instanceMethodSignatureForSelector:aSelector]; if(signature == nil) { signature = [NSMethodSignature signatureForDynamicPropertyAccessedWithSelector:aSelector inClass:mockedClass]; } return signature; } - (Class)mockObjectClass { return [super class]; } - (Class)class { return mockedClass; } - (BOOL)respondsToSelector:(SEL)selector { return [mockedClass instancesRespondToSelector:selector]; } - (BOOL)isKindOfClass:(Class)aClass { return [mockedClass isSubclassOfClass:aClass]; } - (BOOL)conformsToProtocol:(Protocol *)aProtocol { Class clazz = mockedClass; while(clazz != nil) { if(class_conformsToProtocol(clazz, aProtocol)) { return YES; } clazz = class_getSuperclass(clazz); } return NO; } @end #pragma mark - /* taken from: `class-dump -f isNS /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator7.0.sdk/System/Library/Frameworks/CoreFoundation.framework` @ interface NSObject (__NSIsKinds) - (_Bool)isNSValue__; - (_Bool)isNSTimeZone__; - (_Bool)isNSString__; - (_Bool)isNSSet__; - (_Bool)isNSOrderedSet__; - (_Bool)isNSNumber__; - (_Bool)isNSDictionary__; - (_Bool)isNSDate__; - (_Bool)isNSData__; - (_Bool)isNSArray__; */ @implementation OCClassMockObject(NSIsKindsImplementation) - (BOOL)isNSValue__ { return [mockedClass isSubclassOfClass:[NSValue class]]; } - (BOOL)isNSTimeZone__ { return [mockedClass isSubclassOfClass:[NSTimeZone class]]; } - (BOOL)isNSSet__ { return [mockedClass isSubclassOfClass:[NSSet class]]; } - (BOOL)isNSOrderedSet__ { return [mockedClass isSubclassOfClass:[NSOrderedSet class]]; } - (BOOL)isNSNumber__ { return [mockedClass isSubclassOfClass:[NSNumber class]]; } - (BOOL)isNSDate__ { return [mockedClass isSubclassOfClass:[NSDate class]]; } - (BOOL)isNSString__ { return [mockedClass isSubclassOfClass:[NSString class]]; } - (BOOL)isNSDictionary__ { return [mockedClass isSubclassOfClass:[NSDictionary class]]; } - (BOOL)isNSData__ { return [mockedClass isSubclassOfClass:[NSData class]]; } - (BOOL)isNSArray__ { return [mockedClass isSubclassOfClass:[NSArray class]]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMArg.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMArg : NSObject // constraining arguments + (id)any; + (SEL)anySelector; + (void *)anyPointer; + (id __autoreleasing *)anyObjectRef; + (id)isNil; + (id)isNotNil; + (id)isEqual:(id)value; + (id)isNotEqual:(id)value; + (id)isKindOfClass:(Class)cls; + (id)checkWithSelector:(SEL)selector onObject:(id)anObject; + (id)checkWithBlock:(BOOL (^)(id obj))block; // manipulating arguments + (id *)setTo:(id)value; + (void *)setToValue:(NSValue *)value; + (id)invokeBlock; + (id)invokeBlockWithArgs:(id)first, ... NS_REQUIRES_NIL_TERMINATION; + (id)defaultValue; // internal use only + (id)resolveSpecialValues:(NSValue *)value; @end #define OCMOCK_ANY [OCMArg any] #define OCMOCK_VALUE(variable) \ ({ __typeof__(variable) __v = (variable); [NSValue value:&__v withObjCType:@encode(__typeof__(__v))]; }) ================================================ FILE: Pods/OCMock/Source/OCMock/OCMArg.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "OCMArg.h" #import "OCMBlockArgCaller.h" #import "OCMConstraint.h" #import "OCMPassByRefSetter.h" @implementation OCMArg + (id)any { return [OCMAnyConstraint constraint]; } + (void *)anyPointer { return (void *)0x01234567; } + (id __autoreleasing *)anyObjectRef { return (id *)[self anyPointer]; } + (SEL)anySelector { return NSSelectorFromString(@"aSelectorThatMatchesAnySelector"); } + (id)isNil { return [OCMIsNilConstraint constraint]; } + (id)isNotNil { return [OCMIsNotNilConstraint constraint]; } + (id)isEqual:(id)value { return value; } + (id)isNotEqual:(id)value { OCMIsNotEqualConstraint *constraint = [OCMIsNotEqualConstraint constraint]; constraint->testValue = value; return constraint; } + (id)isKindOfClass:(Class)cls { return [[[OCMBlockConstraint alloc] initWithConstraintBlock:^BOOL(id obj) { return [obj isKindOfClass:cls]; }] autorelease]; } + (id)checkWithSelector:(SEL)selector onObject:(id)anObject { return [OCMConstraint constraintWithSelector:selector onObject:anObject]; } + (id)checkWithBlock:(BOOL (^)(id))block { return [[[OCMBlockConstraint alloc] initWithConstraintBlock:block] autorelease]; } + (id *)setTo:(id)value { return (id *)[[[OCMPassByRefSetter alloc] initWithValue:value] autorelease]; } + (void *)setToValue:(NSValue *)value { return (id *)[[[OCMPassByRefSetter alloc] initWithValue:value] autorelease]; } + (id)invokeBlock { return [[[OCMBlockArgCaller alloc] init] autorelease]; } + (id)invokeBlockWithArgs:(id)first, ... NS_REQUIRES_NIL_TERMINATION { NSMutableArray *params = [NSMutableArray array]; va_list args; if(first) { [params addObject:first]; va_start(args, first); id obj; while((obj = va_arg(args, id))) { [params addObject:obj]; } va_end(args); } return [[[OCMBlockArgCaller alloc] initWithBlockArguments:params] autorelease]; } + (id)defaultValue { return [NSNull null]; } + (id)resolveSpecialValues:(NSValue *)value { const char *type = [value objCType]; if(type[0] == '^') { void *pointer = [value pointerValue]; if(pointer == [self anyPointer]) return [OCMArg any]; if((pointer != NULL) && [OCMPassByRefSetter isPassByRefSetterInstance:pointer]) return (id)pointer; } else if(type[0] == ':') { SEL selector; [value getValue:&selector]; if(selector == NSSelectorFromString(@"aSelectorThatMatchesAnySelector")) return [OCMArg any]; } return value; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMArgAction.h ================================================ /* * Copyright (c) 2015-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMArgAction : NSObject - (void)handleArgument:(id)argument; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMArgAction.m ================================================ /* * Copyright (c) 2015-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMArgAction.h" @implementation OCMArgAction - (void)handleArgument:(id)argument { } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMBlockArgCaller.h ================================================ /* * Copyright (c) 2015-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMArgAction.h" @interface OCMBlockArgCaller : OCMArgAction { NSArray *arguments; } - (instancetype)initWithBlockArguments:(NSArray *)someArgs; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMBlockArgCaller.m ================================================ /* * Copyright (c) 2015-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMBlockArgCaller.h" #import "NSInvocation+OCMAdditions.h" @implementation OCMBlockArgCaller - (instancetype)initWithBlockArguments:(NSArray *)someArgs { self = [super init]; if(self) { arguments = [someArgs copy]; } return self; } - (void)dealloc { [arguments release]; [super dealloc]; } - (id)copyWithZone:(NSZone *)zone { return [self retain]; } - (void)handleArgument:(id)aBlock { if(aBlock) { NSInvocation *inv = [NSInvocation invocationForBlock:aBlock withArguments:arguments]; [inv invokeWithTarget:aBlock]; } } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMBlockCaller.h ================================================ /* * Copyright (c) 2010-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMBlockCaller : NSObject { void (^block)(NSInvocation *); } - (id)initWithCallBlock:(void (^)(NSInvocation *))theBlock; - (void)handleInvocation:(NSInvocation *)anInvocation; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMBlockCaller.m ================================================ /* * Copyright (c) 2010-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMBlockCaller.h" @implementation OCMBlockCaller - (id)initWithCallBlock:(void (^)(NSInvocation *))theBlock { if((self = [super init])) { block = [theBlock copy]; } return self; } - (void)dealloc { [block release]; [super dealloc]; } - (void)handleInvocation:(NSInvocation *)anInvocation { if(block != nil) { block(anInvocation); } } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMObjectReturnValueProvider.h" @interface OCMBoxedReturnValueProvider : OCMObjectReturnValueProvider { } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMBoxedReturnValueProvider.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMBoxedReturnValueProvider.h" #import "NSValue+OCMAdditions.h" #import "OCMFunctionsPrivate.h" @implementation OCMBoxedReturnValueProvider - (void)handleInvocation:(NSInvocation *)anInvocation { NSUInteger valueSize = 0; NSValue *returnValueAsNSValue = (NSValue *)returnValue; NSGetSizeAndAlignment([returnValueAsNSValue objCType], &valueSize, NULL); char valueBuffer[valueSize]; [returnValueAsNSValue getValue:valueBuffer]; const char *returnType = [[anInvocation methodSignature] methodReturnType]; if([self isMethodReturnType:returnType compatibleWithValueType:[returnValueAsNSValue objCType] value:valueBuffer valueSize:valueSize]) { [anInvocation setReturnValue:valueBuffer]; } else if([returnValueAsNSValue getBytes:valueBuffer objCType:returnType]) { [anInvocation setReturnValue:valueBuffer]; } else { [NSException raise:NSInvalidArgumentException format:@"Return value cannot be used for method; method signature declares '%s' but value is '%s'.", returnType, [returnValueAsNSValue objCType]]; } } - (BOOL)isMethodReturnType:(const char *)returnType compatibleWithValueType:(const char *)valueType value:(const void *)value valueSize:(size_t)valueSize { /* Same types are obviously compatible */ if(strcmp(returnType, valueType) == 0) return YES; /* Special treatment for nil and Nil */ if(strcmp(returnType, @encode(id)) == 0 || strcmp(returnType, @encode(Class)) == 0) return OCMIsNilValue(valueType, value, valueSize); return OCMEqualTypesAllowingOpaqueStructs(returnType, valueType); } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMConstraint.h ================================================ /* * Copyright (c) 2007-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMConstraint : NSObject + (instancetype)constraint; - (BOOL)evaluate:(id)value; // if you are looking for any, isNil, etc, they have moved to OCMArg // try to use [OCMArg checkWith...] instead of the constraintWith... methods below + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject; + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue; @end @interface OCMAnyConstraint : OCMConstraint @end @interface OCMIsNilConstraint : OCMConstraint @end @interface OCMIsNotNilConstraint : OCMConstraint @end @interface OCMIsNotEqualConstraint : OCMConstraint { @public id testValue; } @end @interface OCMInvocationConstraint : OCMConstraint { @public NSInvocation *invocation; } @end @interface OCMBlockConstraint : OCMConstraint { BOOL (^block)(id); } - (instancetype)initWithConstraintBlock:(BOOL (^)(id))block; @end #ifndef OCM_DISABLE_SHORT_SYNTAX #define CONSTRAINT(aSelector) [OCMConstraint constraintWithSelector:aSelector onObject:self] #define CONSTRAINTV(aSelector, aValue) [OCMConstraint constraintWithSelector:aSelector onObject:self withValue:(aValue)] #endif ================================================ FILE: Pods/OCMock/Source/OCMock/OCMConstraint.m ================================================ /* * Copyright (c) 2007-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "OCMConstraint.h" @implementation OCMConstraint + (instancetype)constraint { return [[[self alloc] init] autorelease]; } - (BOOL)evaluate:(id)value { return NO; } - (id)copyWithZone:(struct _NSZone *)zone __unused { return [self retain]; } + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject { OCMInvocationConstraint *constraint = [OCMInvocationConstraint constraint]; NSMethodSignature *signature = [anObject methodSignatureForSelector:aSelector]; if(signature == nil) [NSException raise:NSInvalidArgumentException format:@"Unknown selector %@ used in constraint.", NSStringFromSelector(aSelector)]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:anObject]; [invocation setSelector:aSelector]; constraint->invocation = invocation; return constraint; } + (instancetype)constraintWithSelector:(SEL)aSelector onObject:(id)anObject withValue:(id)aValue { OCMInvocationConstraint *constraint = (OCMInvocationConstraint *)[self constraintWithSelector:aSelector onObject:anObject]; if([[constraint->invocation methodSignature] numberOfArguments] < 4) [NSException raise:NSInvalidArgumentException format:@"Constraint with value requires selector with two arguments."]; [constraint->invocation setArgument:&aValue atIndex:3]; return constraint; } @end #pragma mark - @implementation OCMAnyConstraint - (BOOL)evaluate:(id)value { return YES; } @end #pragma mark - @implementation OCMIsNilConstraint - (BOOL)evaluate:(id)value { return value == nil; } @end #pragma mark - @implementation OCMIsNotNilConstraint - (BOOL)evaluate:(id)value { return value != nil; } @end #pragma mark - @implementation OCMIsNotEqualConstraint - (BOOL)evaluate:(id)value { return ![value isEqual:testValue]; } @end #pragma mark - @implementation OCMInvocationConstraint - (BOOL)evaluate:(id)value { [invocation setArgument:&value atIndex:2]; // should test if constraint takes arg [invocation invoke]; BOOL returnValue; [invocation getReturnValue:&returnValue]; return returnValue; } @end #pragma mark - @implementation OCMBlockConstraint - (instancetype)initWithConstraintBlock:(BOOL (^)(id))aBlock { if((self = [super init])) { block = [aBlock copy]; } return self; } - (void)dealloc { [block release]; [super dealloc]; } - (BOOL)evaluate:(id)value { return block ? block(value) : NO; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMObjectReturnValueProvider.h" extern NSString *OCMStubbedException; @interface OCMExceptionReturnValueProvider : OCMObjectReturnValueProvider { } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMExceptionReturnValueProvider.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMExceptionReturnValueProvider.h" @implementation OCMExceptionReturnValueProvider NSString *OCMStubbedException = @"OCMStubbedException"; - (void)handleInvocation:(NSInvocation *)anInvocation { [[NSException exceptionWithName:OCMStubbedException reason:@"Exception stubbed in test." userInfo:@{ @"exception" : returnValue }] raise]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMExpectationRecorder.h ================================================ /* * Copyright (c) 2004-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMStubRecorder.h" @interface OCMExpectationRecorder : OCMStubRecorder - (id)never; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMExpectationRecorder.m ================================================ /* * Copyright (c) 2004-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMExpectationRecorder.h" #import "OCMInvocationExpectation.h" #import "OCMockObject.h" @implementation OCMExpectationRecorder #pragma mark Initialisers, description, accessors, etc. - (id)init { self = [super init]; [invocationMatcher release]; invocationMatcher = [[OCMInvocationExpectation alloc] init]; return self; } - (OCMInvocationExpectation *)expectation { return (OCMInvocationExpectation *)invocationMatcher; } #pragma mark Modifying the expectation - (id)never { [[self expectation] setMatchAndReject:YES]; return self; } #pragma mark Finishing recording - (void)forwardInvocation:(NSInvocation *)anInvocation { [super forwardInvocation:anInvocation]; [mockObject addExpectation:[self expectation]]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMFunctions.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #if defined(__cplusplus) #define OCMOCK_EXTERN extern "C" #else #define OCMOCK_EXTERN extern #endif OCMOCK_EXTERN BOOL OCMIsObjectType(const char *objCType); OCMOCK_EXTERN BOOL OCMIsSubclassOfMockClass(Class cls); ================================================ FILE: Pods/OCMock/Source/OCMock/OCMFunctions.m ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #if !defined(OCM_DISABLE_XCTEST_FEATURES) #import #endif #import "OCClassMockObject.h" #import "OCMFunctionsPrivate.h" #import "OCMLocation.h" #import "OCPartialMockObject.h" #pragma mark Known private API @interface NSException(OCMKnownExceptionMethods) + (NSException *)failureInFile:(NSString *)file atLine:(int)line withDescription:(NSString *)formatString, ...; @end @interface NSObject(OCMKnownTestCaseMethods) - (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *)file atLine:(NSUInteger)line expected:(BOOL)expected; - (void)failWithException:(NSException *)exception; @end #pragma mark Functions related to ObjC type system const char *OCMTypeWithoutQualifiers(const char *objCType) { // In certain cases byref appears to just pass the "R" and not the "@" as expected. This is // likely a bug in the compiler since byref is basically a dead keyword at this point. That // being said, this will protect us, and returns what would be the expected type. if(strcmp(objCType, "R") == 0) return "@"; while(objCType[0] && strchr("rnNoORV", objCType[0]) != NULL) objCType += 1; return objCType; } static BOOL OCMIsUnqualifiedClassType(const char *unqualifiedObjCType) { return (strcmp(unqualifiedObjCType, @encode(Class)) == 0); } static BOOL OCMIsUnqualifiedBlockType(const char *unqualifiedObjCType) { char blockType[] = @encode(void (^)(void)); if(strcmp(unqualifiedObjCType, blockType) == 0) return YES; // sometimes block argument/return types are tacked onto the type, in angle brackets if(strncmp(unqualifiedObjCType, blockType, sizeof(blockType) - 1) == 0 && unqualifiedObjCType[sizeof(blockType) - 1] == '<') return YES; return NO; } BOOL OCMIsClassType(const char *objCType) { return OCMIsUnqualifiedClassType(OCMTypeWithoutQualifiers(objCType)); } BOOL OCMIsBlockType(const char *objCType) { return OCMIsUnqualifiedBlockType(OCMTypeWithoutQualifiers(objCType)); } BOOL OCMIsObjectType(const char *objCType) { const char *unqualifiedObjCType = OCMTypeWithoutQualifiers(objCType); char objectType[] = @encode(id); if(strcmp(unqualifiedObjCType, objectType) == 0 || OCMIsUnqualifiedClassType(unqualifiedObjCType)) return YES; // sometimes the name of an object's class is tacked onto the type, in double quotes if(strncmp(unqualifiedObjCType, objectType, sizeof(objectType) - 1) == 0 && unqualifiedObjCType[sizeof(objectType) - 1] == '"') return YES; // if the returnType is a typedef to an object, it has the form ^{OriginClass=#} NSString *regexString = @"^\\^\\{(.*)=#.*\\}"; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexString options:0 error:NULL]; NSString *type = [NSString stringWithCString:unqualifiedObjCType encoding:NSASCIIStringEncoding]; if([regex numberOfMatchesInString:type options:0 range:NSMakeRange(0, type.length)] > 0) return YES; // if the return type is a block we treat it like an object return OCMIsUnqualifiedBlockType(unqualifiedObjCType); } BOOL OCMIsPointerType(const char *objCType) { const char *unqualifiedObjCType = OCMTypeWithoutQualifiers(objCType); if(unqualifiedObjCType[0] == '^') return YES; if(unqualifiedObjCType[0] == '*') return YES; return NO; } CFNumberType OCMNumberTypeForObjCType(const char *objcType) { switch(objcType[0]) { case 'c': return kCFNumberCharType; case 'C': return kCFNumberCharType; case 'B': return kCFNumberCharType; case 's': return kCFNumberShortType; case 'S': return kCFNumberShortType; case 'i': return kCFNumberIntType; case 'I': return kCFNumberIntType; case 'l': return kCFNumberLongType; case 'L': return kCFNumberLongType; case 'q': return kCFNumberLongLongType; case 'Q': return kCFNumberLongLongType; case 'f': return kCFNumberFloatType; case 'd': return kCFNumberDoubleType; default: return 0; } } static BOOL ParseStructType(const char *type, const char **typeEnd, const char **typeNameEnd, const char **typeEqualSign) { if(type[0] != '{' && type[0] != '(') return NO; *typeNameEnd = NULL; *typeEqualSign = NULL; const char endChar = type[0] == '{' ? '}' : ')'; for(const char *ptr = type + 1; *ptr; ++ptr) { switch(*ptr) { case '(': case '{': { const char *subTypeEnd; const char *subTypeNameEnd; const char *subTypeEqualSign; if(!ParseStructType(ptr, &subTypeEnd, &subTypeNameEnd, &subTypeEqualSign)) return NO; ptr = subTypeEnd; break; } case '=': { if(!*typeEqualSign) { *typeNameEnd = ptr; *typeEqualSign = ptr; } break; } case ')': case '}': { if(*ptr == endChar) { *typeEnd = ptr; if(!*typeNameEnd) *typeNameEnd = ptr; return YES; } break; } default: break; } } return NO; } /* * Sometimes an external type is an opaque struct (which will have an @encode of "{structName}" * or "{structName=}") but the actual method return type, or property type, will know the contents * of the struct (so will have an objcType of say "{structName=iiSS}". This function will determine * those are equal provided they have the same structure name, otherwise everything else will be * compared textually. This can happen particularly for pointers to such structures, which still * encode what is being pointed to. * * In addition, this funtion will consider structures with unknown names, encoded as "{?=}, equal to * structures with any name. This means that "{?=dd}" and "{foo=dd}", and even "{?=}" and "{foo=dd}", * are considered equal. * * For some types some runtime functions throw exceptions, which is why we wrap this in an * exception handler just below. */ static BOOL OCMEqualTypesAllowingOpaqueStructsInternal(const char *type1, const char *type2) { type1 = OCMTypeWithoutQualifiers(type1); type2 = OCMTypeWithoutQualifiers(type2); switch(type1[0]) { case '{': case '(': { if(type2[0] != type1[0]) return NO; const char *type1End; const char *type1NameEnd; const char *type1EqualSign; if(!ParseStructType(type1, &type1End, &type1NameEnd, &type1EqualSign)) return NO; const char *type2End; const char *type2NameEnd; const char *type2EqualSign; if(!ParseStructType(type2, &type2End, &type2NameEnd, &type2EqualSign)) return NO; /* Opaque types either don't have an equals sign (just the name and the end brace), or * empty content after the equals sign. * We want that to compare the same as a type of the same name but with the content. */ BOOL type1Opaque = (type1EqualSign == NULL || type1EqualSign + 1 == type1End); BOOL type2Opaque = (type2EqualSign == NULL || type2EqualSign + 2 == type2End); intptr_t type1NameLen = type1NameEnd - type1; intptr_t type2NameLen = type2NameEnd - type2; /* If the names are not equal and neither of the names is a question mark, return NO */ if((type1NameLen != type2NameLen || strncmp(type1, type2, type1NameLen)) && !((type1NameLen == 2) && (type1[1] == '?')) && !((type2NameLen == 2) && (type2[1] == '?')) && !(type1NameLen == 1 || type2NameLen == 1)) return NO; /* If the same name, and at least one is opaque, that is close enough. */ if(type1Opaque || type2Opaque) return YES; /* Otherwise, compare all the elements. Use NSGetSizeAndAlignment to walk through the struct elements. */ type1 = type1EqualSign + 1; type2 = type2EqualSign + 1; while(type1 != type1End && *type1) { if(!OCMEqualTypesAllowingOpaqueStructs(type1, type2)) return NO; if(*type1 != '{' && *type1 != '(') { type1 = NSGetSizeAndAlignment(type1, NULL, NULL); type2 = NSGetSizeAndAlignment(type2, NULL, NULL); } else { const char *subType1End; const char *subType1NameEnd; const char *subType1EqualSign; if(!ParseStructType(type1, &subType1End, &subType1NameEnd, &subType1EqualSign)) return NO; const char *subType2End; const char *subType2NameEnd; const char *subType2EqualSign; if(!ParseStructType(type2, &subType2End, &subType2NameEnd, &subType2EqualSign)) return NO; type1 = subType1End + 1; type2 = subType2End + 1; } } return YES; } case '^': /* for a pointer, make sure the other is a pointer, then recursively compare the rest */ if(type2[0] != type1[0]) return NO; return OCMEqualTypesAllowingOpaqueStructs(type1 + 1, type2 + 1); case '?': return type2[0] == '?'; case '\0': return type2[0] == '\0'; default: { // Move the type pointers past the current types, then compare that region const char *afterType1 = NSGetSizeAndAlignment(type1, NULL, NULL); const char *afterType2 = NSGetSizeAndAlignment(type2, NULL, NULL); intptr_t type1Len = afterType1 - type1; intptr_t type2Len = afterType2 - type2; return (type1Len == type2Len && (strncmp(type1, type2, type1Len) == 0)); } } } BOOL OCMEqualTypesAllowingOpaqueStructs(const char *type1, const char *type2) { @try { return OCMEqualTypesAllowingOpaqueStructsInternal(type1, type2); } @catch(NSException *e) { /* Probably a bitfield or something that NSGetSizeAndAlignment chokes on, oh well */ return NO; } } BOOL OCMIsNilValue(const char *objectCType, const void *value, size_t valueSize) { // First, check value itself for(size_t i = 0; i < valueSize; i++) if(((const char *)value)[i] != 0) return NO; // Depending on the compilation settings of the file where the return value gets recorded, // nil and Nil get potentially different encodings. Check all known encodings. if((strcmp(objectCType, @encode(void *)) == 0) || // Standard Objective-C (strcmp(objectCType, @encode(int)) == 0) || // 32 bit C++ (before nullptr) (strcmp(objectCType, @encode(long long)) == 0) || // 64 bit C++ (before nullptr) (strcmp(objectCType, @encode(char *)) == 0)) // C++ with nullptr return YES; return NO; } BOOL OCMIsAppleBaseClass(Class cls) { return (cls == [NSObject class]) || (cls == [NSProxy class]); } BOOL OCMIsApplePrivateMethod(Class cls, SEL sel) { NSString *className = NSStringFromClass(cls); NSString *selName = NSStringFromSelector(sel); return ([className hasPrefix:@"NS"] || [className hasPrefix:@"UI"]) && ([selName hasPrefix:@"_"] || [selName hasSuffix:@"_"]); } BOOL OCMIsBlock(id potentialBlock) { static Class blockClass; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ blockClass = [^{} class]; Class nsObjectClass = [NSObject class]; while([blockClass superclass] != nsObjectClass) { blockClass = [blockClass superclass]; NSCAssert(blockClass != nil, @"Blocks are expected to inherit from NSObject."); } }); return [potentialBlock isKindOfClass:blockClass]; } BOOL OCMIsNonEscapingBlock(id block) { struct OCMBlockDef *blockRef = (__bridge struct OCMBlockDef *)block; return OCMIsBlock(block) && (blockRef->flags & OCMBlockIsNoEscape) != 0; } #pragma mark Creating and disposing classes static NSString *const OCMSubclassPrefix = @"OCMock_"; Class OCMCreateSubclass(Class class, void *ref) { const char *className = [[NSString stringWithFormat:@"%@%@-%p-%u", OCMSubclassPrefix, NSStringFromClass(class), ref, arc4random()] UTF8String]; Class subclass = objc_allocateClassPair(class, className, 0); objc_registerClassPair(subclass); return subclass; } void OCMDisposeSubclass(Class cls) { if(!OCMIsMockSubclass(cls)) { [NSException raise:NSInvalidArgumentException format:@"Not a mock subclass; found %@\nThe subclass dynamically created by OCMock has been replaced by another class. This can happen when KVO or CoreData create their own dynamic subclass after OCMock created its subclass.\nYou will need to reorder initialization and/or teardown so that classes are created and disposed of in the right order.", NSStringFromClass(cls)]; } objc_disposeClassPair(cls); } BOOL OCMIsMockSubclass(Class cls) { return [NSStringFromClass(cls) hasPrefix:OCMSubclassPrefix]; } BOOL OCMIsSubclassOfMockClass(Class cls) { for(; cls != nil; cls = class_getSuperclass(cls)) { if(OCMIsMockSubclass(cls)) return YES; } return NO; } #pragma mark Alias for renaming real methods static NSString *const OCMRealMethodAliasPrefix = @"ocmock_replaced_"; static const char *const OCMRealMethodAliasPrefixCString = "ocmock_replaced_"; BOOL OCMIsAliasSelector(SEL selector) { return [NSStringFromSelector(selector) hasPrefix:OCMRealMethodAliasPrefix]; } SEL OCMAliasForOriginalSelector(SEL selector) { char aliasName[2048]; const char *originalName = sel_getName(selector); strlcpy(aliasName, OCMRealMethodAliasPrefixCString, sizeof(aliasName)); strlcat(aliasName, originalName, sizeof(aliasName)); return sel_registerName(aliasName); } SEL OCMOriginalSelectorForAlias(SEL selector) { if(!OCMIsAliasSelector(selector)) [NSException raise:NSInvalidArgumentException format:@"Not an alias selector; found %@", NSStringFromSelector(selector)]; NSString *string = NSStringFromSelector(selector); return NSSelectorFromString([string substringFromIndex:[OCMRealMethodAliasPrefix length]]); } #pragma mark Wrappers around associative references static NSString *const OCMClassMethodMockObjectKey = @"OCMClassMethodMockObjectKey"; void OCMSetAssociatedMockForClass(OCClassMockObject *mock, Class aClass) { if((mock != nil) && (objc_getAssociatedObject(aClass, OCMClassMethodMockObjectKey) != nil)) [NSException raise:NSInternalInconsistencyException format:@"Another mock is already associated with class %@", NSStringFromClass(aClass)]; objc_setAssociatedObject(aClass, OCMClassMethodMockObjectKey, mock, OBJC_ASSOCIATION_ASSIGN); } OCClassMockObject *OCMGetAssociatedMockForClass(Class aClass, BOOL includeSuperclasses) { OCClassMockObject *mock = nil; do { mock = objc_getAssociatedObject(aClass, OCMClassMethodMockObjectKey); aClass = class_getSuperclass(aClass); } while((mock == nil) && (aClass != nil) && includeSuperclasses); return mock; } static NSString *const OCMPartialMockObjectKey = @"OCMPartialMockObjectKey"; void OCMSetAssociatedMockForObject(OCClassMockObject *mock, id anObject) { if((mock != nil) && (objc_getAssociatedObject(anObject, OCMPartialMockObjectKey) != nil)) [NSException raise:NSInternalInconsistencyException format:@"Another mock is already associated with object %@", anObject]; objc_setAssociatedObject(anObject, OCMPartialMockObjectKey, mock, OBJC_ASSOCIATION_ASSIGN); } OCPartialMockObject *OCMGetAssociatedMockForObject(id anObject) { return objc_getAssociatedObject(anObject, OCMPartialMockObjectKey); } #pragma mark Functions related to IDE error reporting void OCMReportFailure(OCMLocation *loc, NSString *description) { id testCase = [loc testCase]; #ifdef __IPHONE_14_0 // this is actually a test for Xcode 12; see issue #472 #if !defined(OCM_DISABLE_XCTEST_FEATURES) if((testCase != nil) && [testCase respondsToSelector:@selector(recordIssue:)]) { XCTSourceCodeLocation *xctloc = [[[XCTSourceCodeLocation alloc] initWithFilePath:[loc file] lineNumber:[loc line]] autorelease]; XCTSourceCodeContext *xctctx = [[[XCTSourceCodeContext alloc] initWithLocation:xctloc] autorelease]; XCTIssue *issue = [[[XCTIssue alloc] initWithType:XCTIssueTypeAssertionFailure compactDescription:description detailedDescription:nil sourceCodeContext:xctctx associatedError:nil attachments:[NSArray array]] autorelease]; [testCase recordIssue:issue]; } else #endif #endif if((testCase != nil) && [testCase respondsToSelector:@selector(recordFailureWithDescription:inFile:atLine:expected:)]) { [testCase recordFailureWithDescription:description inFile:[loc file] atLine:[loc line] expected:NO]; } else if((testCase != nil) && [testCase respondsToSelector:@selector(failWithException:)]) { NSException *exception = nil; if([NSException instancesRespondToSelector:@selector(failureInFile:atLine:withDescription:)]) { exception = [NSException failureInFile:[loc file] atLine:(int)[loc line] withDescription:description]; } else { NSString *reason = [NSString stringWithFormat:@"%@:%lu %@", [loc file], (unsigned long)[loc line], description]; exception = [NSException exceptionWithName:@"OCMockTestFailure" reason:reason userInfo:nil]; } [testCase failWithException:exception]; } else if(loc != nil) { NSLog(@"%@:%lu %@", [loc file], (unsigned long)[loc line], description); NSString *reason = [NSString stringWithFormat:@"%@:%lu %@", [loc file], (unsigned long)[loc line], description]; [[NSException exceptionWithName:@"OCMockTestFailure" reason:reason userInfo:nil] raise]; } else { NSLog(@"%@", description); [[NSException exceptionWithName:@"OCMockTestFailure" reason:description userInfo:nil] raise]; } } ================================================ FILE: Pods/OCMock/Source/OCMock/OCMFunctionsPrivate.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @class OCMLocation; @class OCClassMockObject; @class OCPartialMockObject; BOOL OCMIsClassType(const char *objCType); BOOL OCMIsBlockType(const char *objCType); BOOL OCMIsObjectType(const char *objCType); BOOL OCMIsPointerType(const char *objCType); const char *OCMTypeWithoutQualifiers(const char *objCType); BOOL OCMEqualTypesAllowingOpaqueStructs(const char *type1, const char *type2); CFNumberType OCMNumberTypeForObjCType(const char *objcType); BOOL OCMIsNilValue(const char *objectCType, const void *value, size_t valueSize); BOOL OCMIsAppleBaseClass(Class cls); BOOL OCMIsApplePrivateMethod(Class cls, SEL sel); Class OCMCreateSubclass(Class cls, void *ref); BOOL OCMIsMockSubclass(Class cls); void OCMDisposeSubclass(Class cls); BOOL OCMIsAliasSelector(SEL selector); SEL OCMAliasForOriginalSelector(SEL selector); SEL OCMOriginalSelectorForAlias(SEL selector); void OCMSetAssociatedMockForClass(OCClassMockObject *mock, Class aClass); OCClassMockObject *OCMGetAssociatedMockForClass(Class aClass, BOOL includeSuperclasses); void OCMSetAssociatedMockForObject(OCClassMockObject *mock, id anObject); OCPartialMockObject *OCMGetAssociatedMockForObject(id anObject); void OCMReportFailure(OCMLocation *loc, NSString *description); BOOL OCMIsBlock(id potentialBlock); BOOL OCMIsNonEscapingBlock(id block); struct OCMBlockDef { void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock int flags; int reserved; void (*invoke)(void *, ...); struct block_descriptor { unsigned long int reserved; // NULL unsigned long int size; // sizeof(struct Block_literal_1) // optional helper functions void (*copy_helper)(void *dst, void *src); // IFF (1<<25) void (*dispose_helper)(void *src); // IFF (1<<25) // required ABI.2010.3.16 const char *signature; // IFF (1<<30) } *descriptor; }; enum { OCMBlockIsNoEscape = (1 << 23), OCMBlockDescriptionFlagsHasCopyDispose = (1 << 25), OCMBlockDescriptionFlagsHasSignature = (1 << 30) }; ================================================ FILE: Pods/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMIndirectReturnValueProvider : NSObject { id provider; SEL selector; } - (id)initWithProvider:(id)aProvider andSelector:(SEL)aSelector; - (void)handleInvocation:(NSInvocation *)anInvocation; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMIndirectReturnValueProvider.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMIndirectReturnValueProvider.h" @implementation OCMIndirectReturnValueProvider - (id)initWithProvider:(id)aProvider andSelector:(SEL)aSelector { if((self = [super init])) { provider = [aProvider retain]; selector = aSelector; } return self; } - (void)dealloc { [provider release]; [super dealloc]; } - (void)handleInvocation:(NSInvocation *)anInvocation { id originalTarget = [anInvocation target]; SEL originalSelector = [anInvocation selector]; [anInvocation setTarget:provider]; [anInvocation setSelector:selector]; [anInvocation invoke]; [anInvocation setTarget:originalTarget]; [anInvocation setSelector:originalSelector]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMInvocationExpectation.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMInvocationStub.h" @interface OCMInvocationExpectation : OCMInvocationStub { BOOL matchAndReject; BOOL isSatisfied; } - (void)setMatchAndReject:(BOOL)flag; - (BOOL)isMatchAndReject; - (BOOL)isSatisfied; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMInvocationExpectation.m ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "NSInvocation+OCMAdditions.h" #import "OCMInvocationExpectation.h" @implementation OCMInvocationExpectation - (void)setMatchAndReject:(BOOL)flag { matchAndReject = flag; if(matchAndReject) isSatisfied = YES; } - (BOOL)isMatchAndReject { return matchAndReject; } - (BOOL)isSatisfied { return isSatisfied; } - (void)addInvocationAction:(id)anAction { if(matchAndReject) { [NSException raise:NSInternalInconsistencyException format:@"%@: cannot add action to a reject stub; got %@", [self description], anAction]; } [super addInvocationAction:anAction]; } - (void)handleInvocation:(NSInvocation *)anInvocation { if(matchAndReject) { isSatisfied = NO; [NSException raise:NSInternalInconsistencyException format:@"%@: explicitly disallowed method invoked: %@", [self description], [anInvocation invocationDescription]]; } else { [super handleInvocation:anInvocation]; isSatisfied = YES; } } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMInvocationMatcher.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMInvocationMatcher : NSObject { NSInvocation *recordedInvocation; BOOL recordedAsClassMethod; BOOL ignoreNonObjectArgs; } - (void)setInvocation:(NSInvocation *)anInvocation; - (NSInvocation *)recordedInvocation; - (void)setRecordedAsClassMethod:(BOOL)flag; - (BOOL)recordedAsClassMethod; - (void)setIgnoreNonObjectArgs:(BOOL)flag; - (BOOL)matchesSelector:(SEL)aSelector; - (BOOL)matchesInvocation:(NSInvocation *)anInvocation; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMInvocationMatcher.m ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "NSInvocation+OCMAdditions.h" #import "OCMInvocationMatcher.h" #import "OCMArg.h" #import "OCMConstraint.h" #import "OCMFunctionsPrivate.h" #import "OCMPassByRefSetter.h" @interface NSObject (HCMatcherDummy) - (BOOL)matches:(id)item; @end @implementation OCMInvocationMatcher - (void)dealloc { [recordedInvocation release]; [super dealloc]; } - (void)setInvocation:(NSInvocation *)anInvocation { [recordedInvocation release]; // Don't do a regular -retainArguments on the invocation that we use for matching. NSInvocation // effectively does an strcpy on char* arguments which messes up matching them literally and blows // up with anyPointer (in strlen since it's not actually a C string). Also on the off-chance that // anInvocation contains self as an argument, -retainArguments would create a retain cycle. [anInvocation retainObjectArgumentsExcludingObject:self]; recordedInvocation = [anInvocation retain]; } - (void)setRecordedAsClassMethod:(BOOL)flag { recordedAsClassMethod = flag; } - (BOOL)recordedAsClassMethod { return recordedAsClassMethod; } - (void)setIgnoreNonObjectArgs:(BOOL)flag { ignoreNonObjectArgs = flag; } - (NSString *)description { return [recordedInvocation invocationDescription]; } - (NSInvocation *)recordedInvocation { return recordedInvocation; } - (BOOL)matchesSelector:(SEL)sel { if(sel == [recordedInvocation selector]) return YES; if(OCMIsAliasSelector(sel) && OCMOriginalSelectorForAlias(sel) == [recordedInvocation selector]) return YES; return NO; } - (BOOL)matchesInvocation:(NSInvocation *)anInvocation { id target = [anInvocation target]; BOOL isClassMethodInvocation = (target != nil) && (target == [target class]); if(isClassMethodInvocation != recordedAsClassMethod) return NO; if(![self matchesSelector:[anInvocation selector]]) return NO; NSMethodSignature *signature = [recordedInvocation methodSignature]; NSUInteger n = [signature numberOfArguments]; for(NSUInteger i = 2; i < n; i++) { if(ignoreNonObjectArgs && !OCMIsObjectType([signature getArgumentTypeAtIndex:i])) { continue; } id recordedArg = [recordedInvocation getArgumentAtIndexAsObject:i]; id passedArg = [anInvocation getArgumentAtIndexAsObject:i]; if([recordedArg isProxy]) { if(![recordedArg isEqual:passedArg]) return NO; continue; } if([recordedArg isKindOfClass:[NSValue class]]) recordedArg = [OCMArg resolveSpecialValues:recordedArg]; if([recordedArg isKindOfClass:[OCMConstraint class]]) { if([recordedArg evaluate:passedArg] == NO) return NO; } else if([recordedArg isKindOfClass:[OCMArgAction class]]) { // ignore, will be dealt with in handleInvocation: where applicable } else if([recordedArg conformsToProtocol:objc_getProtocol("HCMatcher")]) { if([recordedArg matches:passedArg] == NO) return NO; } else { if(([recordedArg class] == [NSNumber class]) && ([(NSNumber *)recordedArg compare:(NSNumber *)passedArg] != NSOrderedSame)) return NO; if(([recordedArg isEqual:passedArg] == NO) && !((recordedArg == nil) && (passedArg == nil))) return NO; } } return YES; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMInvocationStub.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMInvocationMatcher.h" @interface OCMInvocationStub : OCMInvocationMatcher { NSMutableArray *invocationActions; } - (void)addInvocationAction:(id)anAction; - (NSArray *)invocationActions; - (void)handleInvocation:(NSInvocation *)anInvocation; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMInvocationStub.m ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "NSInvocation+OCMAdditions.h" #import "OCMInvocationStub.h" #import "OCMArg.h" #import "OCMArgAction.h" #define UNSET_RETURN_VALUE_MARKER ((id)0x01234567) @implementation OCMInvocationStub - (id)init { self = [super init]; invocationActions = [[NSMutableArray alloc] init]; return self; } - (void)dealloc { [invocationActions release]; [super dealloc]; } - (void)addInvocationAction:(id)anAction { [invocationActions addObject:anAction]; } - (NSArray *)invocationActions { return invocationActions; } - (void)handleInvocation:(NSInvocation *)anInvocation { [self invokeArgActionsForInvocation:anInvocation]; id target = [anInvocation target]; BOOL isInInitFamily = [anInvocation methodIsInInitFamily]; BOOL isInCreateFamily = isInInitFamily ? NO : [anInvocation methodIsInCreateFamily]; if(isInInitFamily || isInCreateFamily) { id returnVal = UNSET_RETURN_VALUE_MARKER; [anInvocation setReturnValue:&returnVal]; [self invokeActionsForInvocation:anInvocation]; [anInvocation getReturnValue:&returnVal]; if(returnVal == UNSET_RETURN_VALUE_MARKER) [NSException raise:NSInvalidArgumentException format:@"%@ was stubbed but no return value set. A return value is required for all alloc/copy/new/mutablecopy/init methods. If you intended to return nil, make this explicit with .andReturn(nil)", NSStringFromSelector([anInvocation selector])]; if(isInCreateFamily) { // methods that "create" an object return it with an extra retain count [returnVal retain]; } if(isInInitFamily) { // init family methods "consume" self and retain their return value. Do the retain // first in case the return value and self are the same. The analyzer doesn't // understand this; see #456 for details. In this case we also need to do something // harmless with target or else the analyzer will complain about it not being used. [returnVal retain]; #ifndef __clang_analyzer__ [target release]; #else [target class]; #endif } } else { [self invokeActionsForInvocation:anInvocation]; } } - (void)invokeArgActionsForInvocation:(NSInvocation *)anInvocation { NSMethodSignature *signature = [recordedInvocation methodSignature]; NSUInteger n = [signature numberOfArguments]; for(NSUInteger i = 2; i < n; i++) { id recordedArg = [recordedInvocation getArgumentAtIndexAsObject:i]; id passedArg = [anInvocation getArgumentAtIndexAsObject:i]; if([recordedArg isProxy]) continue; if([recordedArg isKindOfClass:[NSValue class]]) recordedArg = [OCMArg resolveSpecialValues:recordedArg]; if([recordedArg isKindOfClass:[OCMArgAction class]]) [recordedArg handleArgument:passedArg]; } } - (void)invokeActionsForInvocation:(NSInvocation *)anInvocation { [invocationActions makeObjectsPerformSelector:@selector(handleInvocation:) withObject:anInvocation]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMLocation.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import @interface OCMLocation : NSObject { id testCase; NSString *file; NSUInteger line; } + (instancetype)locationWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; - (instancetype)initWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine; - (id)testCase; - (NSString *)file; - (NSUInteger)line; @end OCMOCK_EXTERN OCMLocation *OCMMakeLocation(id testCase, const char *file, int line); ================================================ FILE: Pods/OCMock/Source/OCMock/OCMLocation.m ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMLocation.h" @implementation OCMLocation + (instancetype)locationWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine { return [[[OCMLocation alloc] initWithTestCase:aTestCase file:aFile line:aLine] autorelease]; } - (instancetype)initWithTestCase:(id)aTestCase file:(NSString *)aFile line:(NSUInteger)aLine { if((self = [super init])) { testCase = aTestCase; file = [aFile retain]; line = aLine; } return self; } - (void)dealloc { [file release]; [super dealloc]; } - (id)testCase { return testCase; } - (NSString *)file { return file; } - (NSUInteger)line { return line; } @end OCMLocation *OCMMakeLocation(id testCase, const char *fileCString, int line) { return [OCMLocation locationWithTestCase:testCase file:[NSString stringWithUTF8String:fileCString] line:line]; } ================================================ FILE: Pods/OCMock/Source/OCMock/OCMMacroState.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @class OCMLocation; @class OCMQuantifier; @class OCMRecorder; @class OCMStubRecorder; @class OCMockObject; @interface OCMMacroState : NSObject { OCMRecorder *recorder; BOOL invocationDidThrow; } + (void)beginStubMacro; + (OCMStubRecorder *)endStubMacro; + (void)beginExpectMacro; + (OCMStubRecorder *)endExpectMacro; + (void)beginRejectMacro; + (OCMStubRecorder *)endRejectMacro; + (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation; + (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation withQuantifier:(OCMQuantifier *)quantifier; + (void)endVerifyMacro; + (OCMMacroState *)globalState; - (void)setRecorder:(OCMRecorder *)aRecorder; - (OCMRecorder *)recorder; - (void)switchToClassMethod; - (void)setInvocationDidThrow:(BOOL)flag; - (BOOL)invocationDidThrow; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMMacroState.m ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMMacroState.h" #import "OCMExpectationRecorder.h" #import "OCMVerifier.h" @implementation OCMMacroState static NSString *const OCMGlobalStateKey = @"OCMGlobalStateKey"; #pragma mark Methods to begin/end macros + (void)beginStubMacro { OCMStubRecorder *recorder = [[[OCMStubRecorder alloc] init] autorelease]; OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; [macroState release]; } + (OCMStubRecorder *)endStubMacro { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; OCMMacroState *globalState = threadDictionary[OCMGlobalStateKey]; OCMStubRecorder *recorder = [[(OCMStubRecorder *)[globalState recorder] retain] autorelease]; BOOL didThrow = [globalState invocationDidThrow]; [threadDictionary removeObjectForKey:OCMGlobalStateKey]; if(didThrow == NO && [recorder didRecordInvocation] == NO) { [NSException raise:NSInternalInconsistencyException format:@"Did not record an invocation in OCMStub/OCMExpect/OCMReject.\n" @"Possible causes are:\n" @"- The receiver is not a mock object.\n" @"- The selector conflicts with a selector implemented by OCMStubRecorder/OCMExpectationRecorder."]; } return recorder; } + (void)beginExpectMacro { OCMExpectationRecorder *recorder = [[[OCMExpectationRecorder alloc] init] autorelease]; OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; [macroState release]; } + (OCMStubRecorder *)endExpectMacro { return [self endStubMacro]; } + (void)beginRejectMacro { OCMExpectationRecorder *recorder = [[[OCMExpectationRecorder alloc] init] autorelease]; OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; [macroState release]; } + (OCMStubRecorder *)endRejectMacro { OCMMacroState *globalState = [NSThread currentThread].threadDictionary[OCMGlobalStateKey]; // Calling never after the invocation to avoid running afoul of ARC's expectations on // return values from init methods. [(OCMExpectationRecorder *)[globalState recorder] never]; return [self endStubMacro]; } + (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation { return [self beginVerifyMacroAtLocation:aLocation withQuantifier:nil]; } + (void)beginVerifyMacroAtLocation:(OCMLocation *)aLocation withQuantifier:(OCMQuantifier *)quantifier { OCMVerifier *recorder = [[[OCMVerifier alloc] init] autorelease]; [recorder setLocation:aLocation]; [recorder setQuantifier:quantifier]; OCMMacroState *macroState = [[OCMMacroState alloc] initWithRecorder:recorder]; [NSThread currentThread].threadDictionary[OCMGlobalStateKey] = macroState; [macroState release]; } + (void)endVerifyMacro { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; OCMMacroState *globalState = threadDictionary[OCMGlobalStateKey]; OCMVerifier *verifier = [[(OCMVerifier *)[globalState recorder] retain] autorelease]; BOOL didThrow = [globalState invocationDidThrow]; [threadDictionary removeObjectForKey:OCMGlobalStateKey]; if(didThrow == NO && [verifier didRecordInvocation] == NO) { [NSException raise:NSInternalInconsistencyException format:@"Did not record an invocation in OCMVerify.\n" @"Possible causes are:\n" @"- The receiver is not a mock object.\n" @"- The selector conflicts with a selector implemented by OCMVerifier."]; } } #pragma mark Accessing global state + (OCMMacroState *)globalState { return [NSThread currentThread].threadDictionary[OCMGlobalStateKey]; } #pragma mark Init, dealloc, accessors - (id)initWithRecorder:(OCMRecorder *)aRecorder { if((self = [super init])) { recorder = [aRecorder retain]; } return self; } - (void)dealloc { [recorder release]; if([NSThread currentThread].threadDictionary[OCMGlobalStateKey] == self) [NSException raise:NSInternalInconsistencyException format:@"Unexpected dealloc while set as the global state"]; [super dealloc]; } - (void)setRecorder:(OCMRecorder *)aRecorder { [recorder autorelease]; recorder = [aRecorder retain]; } - (OCMRecorder *)recorder { return recorder; } - (void)setInvocationDidThrow:(BOOL)flag { invocationDidThrow = flag; } - (BOOL)invocationDidThrow { return invocationDidThrow; } #pragma mark Changing the recorder - (void)switchToClassMethod { [recorder classMethod]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.h ================================================ /* * Copyright (c) 2019-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMNonRetainingObjectReturnValueProvider : NSObject { id returnValue; } - (instancetype)initWithValue:(id)aValue; - (void)handleInvocation:(NSInvocation *)anInvocation; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMNonRetainingObjectReturnValueProvider.m ================================================ /* * Copyright (c) 2019-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "NSInvocation+OCMAdditions.h" #import "OCMNonRetainingObjectReturnValueProvider.h" #import "OCMFunctions.h" @implementation OCMNonRetainingObjectReturnValueProvider - (instancetype)initWithValue:(id)aValue { if((self = [super init])) returnValue = aValue; return self; } - (void)handleInvocation:(NSInvocation *)anInvocation { if(!OCMIsObjectType([[anInvocation methodSignature] methodReturnType])) { @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Expected invocation with object return type. Did you mean to use andReturnValue: instead?" userInfo:nil]; } [anInvocation setReturnValue:&returnValue]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMNotificationPoster.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMNotificationPoster : NSObject { NSNotification *notification; } - (id)initWithNotification:(id)aNotification; - (void)handleInvocation:(NSInvocation *)anInvocation; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMNotificationPoster.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMNotificationPoster.h" @implementation OCMNotificationPoster - (id)initWithNotification:(id)aNotification { if((self = [super init])) { notification = [aNotification retain]; } return self; } - (void)dealloc { [notification release]; [super dealloc]; } - (void)handleInvocation:(NSInvocation *)anInvocation { [[NSNotificationCenter defaultCenter] postNotification:notification]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMObjectReturnValueProvider.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMNonRetainingObjectReturnValueProvider.h" @interface OCMObjectReturnValueProvider : OCMNonRetainingObjectReturnValueProvider @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMObjectReturnValueProvider.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMObjectReturnValueProvider.h" @implementation OCMObjectReturnValueProvider - (instancetype)initWithValue:(id)aValue { if((self = [super initWithValue:aValue])) [returnValue retain]; return self; } - (void)dealloc { [returnValue release]; [super dealloc]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMObserverRecorder.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMObserverRecorder : NSObject { NSNotification *recordedNotification; } - (NSNotification *)notificationWithName:(NSString *)name object:(id)sender; - (NSNotification *)notificationWithName:(NSString *)name object:(id)sender userInfo:(NSDictionary *)userInfo; - (BOOL)matchesNotification:(NSNotification *)aNotification; - (BOOL)argument:(id)expectedArg matchesArgument:(id)observedArg; - (BOOL)didRecordInvocation __used; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMObserverRecorder.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "OCMObserverRecorder.h" #import "OCMConstraint.h" @interface NSObject (HCMatcherDummy) - (BOOL)matches:(id)item; @end #pragma mark - @implementation OCMObserverRecorder #pragma mark Initialisers, description, accessors, etc. - (void)dealloc { [recordedNotification release]; [super dealloc]; } - (BOOL)didRecordInvocation { return YES; // Needed for macro use, and recorder can only end up in macro state if it was used. } #pragma mark Recording - (NSNotification *)notificationWithName:(NSString *)name object:(id)sender { recordedNotification = [[NSNotification notificationWithName:name object:sender] retain]; return nil; } - (NSNotification *)notificationWithName:(NSString *)name object:(id)sender userInfo:(NSDictionary *)userInfo { recordedNotification = [[NSNotification notificationWithName:name object:sender userInfo:userInfo] retain]; return nil; } #pragma mark Verification - (BOOL)matchesNotification:(NSNotification *)aNotification { return [self argument:[recordedNotification name] matchesArgument:[aNotification name]] && [self argument:[recordedNotification object] matchesArgument:[aNotification object]] && [self argument:[recordedNotification userInfo] matchesArgument:[aNotification userInfo]]; } - (BOOL)argument:(id)expectedArg matchesArgument:(id)observedArg { if([expectedArg isKindOfClass:[OCMConstraint class]]) { return [expectedArg evaluate:observedArg]; } else if([expectedArg conformsToProtocol:objc_getProtocol("HCMatcher")]) { return [expectedArg matches:observedArg]; } else if(expectedArg == observedArg) { return YES; } else if(expectedArg == nil || observedArg == nil) { return NO; } else { return [expectedArg isEqual:observedArg]; } } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMPassByRefSetter.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMArgAction.h" @interface OCMPassByRefSetter : OCMArgAction { id value; } - (id)initWithValue:(id)value; + (BOOL)isPassByRefSetterInstance:(void *)ptr; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMPassByRefSetter.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMPassByRefSetter.h" @implementation OCMPassByRefSetter // Stores a reference to all OCMPassByRefSetter instances so that OCMArg can // check for any given pointer whether its an OCMPassByRefSetter without having // to get the class for the pointer (see #503). The pointers are stored without // reference count. static NSHashTable *_OCMPassByRefSetterInstances = NULL; + (void)initialize { if(self == [OCMPassByRefSetter class]) { _OCMPassByRefSetterInstances = [[NSHashTable hashTableWithOptions:NSPointerFunctionsOpaqueMemory | NSPointerFunctionsOpaquePersonality] retain]; } } + (BOOL)isPassByRefSetterInstance:(void *)ptr { @synchronized(_OCMPassByRefSetterInstances) { return NSHashGet(_OCMPassByRefSetterInstances, ptr) != NULL; } } - (id)initWithValue:(id)aValue { if((self = [super init])) { value = [aValue retain]; @synchronized(_OCMPassByRefSetterInstances) { NSHashInsertKnownAbsent(_OCMPassByRefSetterInstances, self); } } return self; } - (void)dealloc { [value release]; @synchronized(_OCMPassByRefSetterInstances) { NSHashRemove(_OCMPassByRefSetterInstances, self); } [super dealloc]; } - (void)handleArgument:(id)arg { void *pointerValue = [arg pointerValue]; if(pointerValue != NULL) { if([value isKindOfClass:[NSValue class]]) [(NSValue *)value getValue:pointerValue]; else *(id *)pointerValue = value; } } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMQuantifier.h ================================================ /* * Copyright (c) 2016-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMQuantifier : NSObject { NSUInteger expectedCount; } + (instancetype)never; + (instancetype)exactly:(NSUInteger)count; + (instancetype)atLeast:(NSUInteger)count; + (instancetype)atMost:(NSUInteger)count; - (BOOL)isValidCount:(NSUInteger)count; - (NSString *)description; @end #define OCMNever() ([OCMQuantifier never]) #define OCMTimes(n) ([OCMQuantifier exactly:(n)]) #define OCMAtLeast(n) ([OCMQuantifier atLeast:(n)]) #define OCMAtMost(n) ([OCMQuantifier atMost:(n)]) #ifndef OCM_DISABLE_SHORT_QSYNTAX #define never() OCMNever() #define times(n) OCMTimes(n) #define atLeast(n) OCMAtLeast(n) #define atMost(n) OCMAtMost(n) #endif ================================================ FILE: Pods/OCMock/Source/OCMock/OCMQuantifier.m ================================================ /* * Copyright (c) 2016-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMQuantifier.h" #import "OCMMacroState.h" #import "OCMVerifier.h" @interface OCMExactCountQuantifier : OCMQuantifier @end @interface OCMAtLeastQuantifier : OCMQuantifier @end @interface OCMAtMostQuantifier : OCMQuantifier @end @implementation OCMQuantifier + (instancetype)exactly:(NSUInteger)count { return [[[OCMExactCountQuantifier alloc] initWithCount:count] autorelease]; } + (instancetype)never { return [self exactly:0]; } + (instancetype)atLeast:(NSUInteger)count { return [[[OCMAtLeastQuantifier alloc] initWithCount:count] autorelease]; } + (instancetype)atMost:(NSUInteger)count { return [[[OCMAtMostQuantifier alloc] initWithCount:count] autorelease]; } - (instancetype)initWithCount:(NSUInteger)count { if((self = [super init]) != nil) { expectedCount = count; [(OCMVerifier *)[[OCMMacroState globalState] recorder] setQuantifier:self]; } return self; } - (BOOL)isValidCount:(NSUInteger)count { return NO; } - (NSString *)description { switch(expectedCount) { case 0: return @"never"; case 1: return @"once"; default: return [NSString stringWithFormat:@"%lu times", (unsigned long)expectedCount]; } } @end @implementation OCMExactCountQuantifier - (BOOL)isValidCount:(NSUInteger)count { return count == expectedCount; } @end @implementation OCMAtLeastQuantifier - (instancetype)initWithCount:(NSUInteger)count { if(count == 0) @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Count for an at-least quantifier cannot be zero." userInfo:nil]; return [super initWithCount:count]; } - (BOOL)isValidCount:(NSUInteger)count { return count >= expectedCount; } - (NSString *)description { return [@"at least " stringByAppendingString:[super description]]; } @end @implementation OCMAtMostQuantifier - (instancetype)initWithCount:(NSUInteger)count { if(count == 0) @throw [NSException exceptionWithName:NSInvalidArgumentException reason:@"Count for an at-most quantifier cannot be zero. Use never or exactly-zero quantifier instead." userInfo:nil]; return [super initWithCount:count]; } - (BOOL)isValidCount:(NSUInteger)count { return count <= expectedCount; } - (NSString *)description { return [@"at most " stringByAppendingString:[super description]]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMRealObjectForwarder.h ================================================ /* * Copyright (c) 2010-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @interface OCMRealObjectForwarder : NSObject { } - (void)handleInvocation:(NSInvocation *)anInvocation; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMRealObjectForwarder.m ================================================ /* * Copyright (c) 2010-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "NSInvocation+OCMAdditions.h" #import "OCMRealObjectForwarder.h" #import "OCMFunctionsPrivate.h" #import "OCPartialMockObject.h" @implementation OCMRealObjectForwarder - (void)handleInvocation:(NSInvocation *)anInvocation { id invocationTarget = [anInvocation target]; BOOL isInInitFamily = [anInvocation methodIsInInitFamily]; BOOL isInCreateFamily = isInInitFamily ? NO : [anInvocation methodIsInCreateFamily]; [anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])]; if([invocationTarget isProxy]) { if(!class_getInstanceMethod([invocationTarget mockObjectClass], @selector(realObject))) [NSException raise:NSInternalInconsistencyException format:@"Method andForwardToRealObject can only be used with partial mocks and class methods."]; NSObject *realObject = [(OCPartialMockObject *)invocationTarget realObject]; [anInvocation setTarget:realObject]; if(isInInitFamily) { // The init method of the real object will "consume" self, but because the method was // invoked on the mock and not the real object a corresponding retain is missing; so // we do this here. The analyzer doesn't understand this; see #456 for details. #ifndef __clang_analyzer__ [realObject retain]; #endif } } [anInvocation invoke]; if(isInInitFamily || isInCreateFamily) { // After invoking the method on the real object the return value's retain count is correct, // but because we have a chain of handlers for an invocation and we handle the retain count // adjustments at the end in the stub, we undo the additional retains here. id returnVal; [anInvocation getReturnValue:&returnVal]; [returnVal autorelease]; } } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMRecorder.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @class OCMockObject; @class OCMInvocationMatcher; @interface OCMRecorder : NSProxy { OCMockObject *mockObject; OCMInvocationMatcher *invocationMatcher; BOOL didRecordInvocation; BOOL shouldReturnMockFromInit; } - (instancetype)init; - (instancetype)initWithMockObject:(OCMockObject *)aMockObject; - (void)setMockObject:(OCMockObject *)aMockObject; - (void)setShouldReturnMockFromInit:(BOOL)flag; - (OCMInvocationMatcher *)invocationMatcher; - (BOOL)didRecordInvocation; - (id)classMethod; - (id)ignoringNonObjectArgs; @end @interface OCMRecorder (Properties) #define ignoringNonObjectArgs() _ignoringNonObjectArgs() @property(nonatomic, readonly) OCMRecorder * (^_ignoringNonObjectArgs)(void); @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMRecorder.m ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import #import "NSInvocation+OCMAdditions.h" #import "OCClassMockObject.h" #import "OCMInvocationMatcher.h" #import "OCMRecorder.h" @implementation OCMRecorder - (instancetype)init { // no super, we're inheriting from NSProxy didRecordInvocation = NO; shouldReturnMockFromInit = NO; return self; } - (instancetype)initWithMockObject:(OCMockObject *)aMockObject { [self init]; [self setMockObject:aMockObject]; return self; } - (void)setMockObject:(OCMockObject *)aMockObject { mockObject = aMockObject; } - (void)setShouldReturnMockFromInit:(BOOL)flag { shouldReturnMockFromInit = flag; } - (void)dealloc { [invocationMatcher release]; [super dealloc]; } - (NSString *)description { return [invocationMatcher description]; } - (OCMInvocationMatcher *)invocationMatcher { return invocationMatcher; } - (BOOL)didRecordInvocation { return didRecordInvocation; } #pragma mark Modifying the matcher - (id)classMethod { // should we handle the case where this is called with a mock that isn't a class mock? [invocationMatcher setRecordedAsClassMethod:YES]; return self; } - (id)ignoringNonObjectArgs { [invocationMatcher setIgnoreNonObjectArgs:YES]; return self; } #pragma mark Recording the actual invocation - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { if([invocationMatcher recordedAsClassMethod]) return [[(OCClassMockObject *)mockObject mockedClass] methodSignatureForSelector:aSelector]; NSMethodSignature *signature = [mockObject methodSignatureForSelector:aSelector]; if(signature == nil) { // if we're a working with a class mock and there is a class method, auto-switch if(([object_getClass(mockObject) isSubclassOfClass:[OCClassMockObject class]]) && ([[(OCClassMockObject *)mockObject mockedClass] respondsToSelector:aSelector])) { [self classMethod]; signature = [self methodSignatureForSelector:aSelector]; } } return signature; } - (void)forwardInvocation:(NSInvocation *)anInvocation { [anInvocation setTarget:nil]; didRecordInvocation = YES; [invocationMatcher setInvocation:anInvocation]; // Code with ARC may retain the receiver of an init method before invoking it. In that case it // relies on the init method returning an object it can release. So, we must set the correct // return value here. Normally, the correct return value is the recorder but sometimes it's the // mock. The decision is easier to make in the mock, which is why the mock sets a flag in the // recorder and we simply use the flag here. if([anInvocation methodIsInInitFamily]) { id returnValue = shouldReturnMockFromInit ? (id)mockObject : (id)self; [anInvocation setReturnValue:&returnValue]; } } - (void)doesNotRecognizeSelector:(SEL)aSelector __used { [NSException raise:NSInvalidArgumentException format:@"%@: cannot stub/expect/verify method '%@' because no such method exists in the mocked class.", mockObject, NSStringFromSelector(aSelector)]; } @end @implementation OCMRecorder (Properties) @dynamic _ignoringNonObjectArgs; - (OCMRecorder *(^)(void))_ignoringNonObjectArgs { id (^theBlock)(void) = ^(void) { return [self ignoringNonObjectArgs]; }; return [[theBlock copy] autorelease]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMStubRecorder.h ================================================ /* * Copyright (c) 2004-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import #import #if !defined(OCM_DISABLE_XCTEST_FEATURES) @class XCTestExpectation; #endif @interface OCMStubRecorder : OCMRecorder - (id)andReturn:(id)anObject; - (id)andReturnValue:(NSValue *)aValue; - (id)andThrow:(NSException *)anException; - (id)andPost:(NSNotification *)aNotification; - (id)andCall:(SEL)selector onObject:(id)anObject; - (id)andDo:(void (^)(NSInvocation *invocation))block; - (id)andForwardToRealObject; #if !defined(OCM_DISABLE_XCTEST_FEATURES) - (id)andFulfill:(XCTestExpectation *)expectation; #endif @end @interface OCMStubRecorder (Properties) #define andReturn(aValue) _andReturn(({ \ __typeof__(aValue) _val = (aValue); \ NSValue *_nsval = [NSValue value:&_val withObjCType:@encode(__typeof__(_val))]; \ if (OCMIsObjectType(@encode(__typeof(_val)))) { \ objc_setAssociatedObject(_nsval, "OCMAssociatedBoxedValue", *(__unsafe_unretained id *) (void *) &_val, OBJC_ASSOCIATION_RETAIN); \ } \ _nsval; \ })) @property (nonatomic, readonly) OCMStubRecorder *(^ _andReturn)(NSValue *); #define andThrow(anException) _andThrow(anException) @property (nonatomic, readonly) OCMStubRecorder *(^ _andThrow)(NSException *); #define andPost(aNotification) _andPost(aNotification) @property (nonatomic, readonly) OCMStubRecorder *(^ _andPost)(NSNotification *); #define andCall(anObject, aSelector) _andCall(anObject, aSelector) @property (nonatomic, readonly) OCMStubRecorder *(^ _andCall)(id, SEL); #define andDo(aBlock) _andDo(aBlock) @property (nonatomic, readonly) OCMStubRecorder *(^ _andDo)(void (^)(NSInvocation *)); #define andForwardToRealObject() _andForwardToRealObject() @property (nonatomic, readonly) OCMStubRecorder *(^ _andForwardToRealObject)(void); #if !defined(OCM_DISABLE_XCTEST_FEATURES) #define andFulfill(anExpectation) _andFulfill(anExpectation) @property (nonatomic, readonly) OCMStubRecorder *(^ _andFulfill)(XCTestExpectation *); #endif @property (nonatomic, readonly) OCMStubRecorder *(^ _ignoringNonObjectArgs)(void); #define andBreak() _andDo(^(NSInvocation *_invocation) \ { \ __builtin_debugtrap(); \ }) \ #define andLog(_format, ...) _andDo(^(NSInvocation *_invocation) \ { \ NSLog(_format, ##__VA_ARGS__); \ }) \ @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMStubRecorder.m ================================================ /* * Copyright (c) 2004-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMStubRecorder.h" #import "OCClassMockObject.h" #import "OCMBlockCaller.h" #import "OCMBoxedReturnValueProvider.h" #import "OCMExceptionReturnValueProvider.h" #import "OCMIndirectReturnValueProvider.h" #import "OCMInvocationStub.h" #import "OCMNotificationPoster.h" #import "OCMRealObjectForwarder.h" #if !defined(OCM_DISABLE_XCTEST_FEATURES) #import #endif @implementation OCMStubRecorder #pragma mark Initialisers, description, accessors, etc. - (id)init { if(invocationMatcher != nil) [NSException raise:NSInternalInconsistencyException format:@"** Method init invoked twice on stub recorder. Are you trying to mock the init method? This is currently not supported."]; self = [super init]; invocationMatcher = [[OCMInvocationStub alloc] init]; return self; } - (OCMInvocationStub *)stub { return (OCMInvocationStub *)invocationMatcher; } #pragma mark Recording invocation actions - (id)andReturn:(id)anObject { id action; if(anObject == mockObject) { action = [[[OCMNonRetainingObjectReturnValueProvider alloc] initWithValue:anObject] autorelease]; } else { action = [[[OCMObjectReturnValueProvider alloc] initWithValue:anObject] autorelease]; } [[self stub] addInvocationAction:action]; return self; } - (id)andReturnValue:(NSValue *)aValue { [[self stub] addInvocationAction:[[[OCMBoxedReturnValueProvider alloc] initWithValue:aValue] autorelease]]; return self; } - (id)andThrow:(NSException *)anException { [[self stub] addInvocationAction:[[[OCMExceptionReturnValueProvider alloc] initWithValue:anException] autorelease]]; return self; } - (id)andPost:(NSNotification *)aNotification { [[self stub] addInvocationAction:[[[OCMNotificationPoster alloc] initWithNotification:aNotification] autorelease]]; return self; } - (id)andCall:(SEL)selector onObject:(id)anObject { [[self stub] addInvocationAction:[[[OCMIndirectReturnValueProvider alloc] initWithProvider:anObject andSelector:selector] autorelease]]; return self; } - (id)andDo:(void (^)(NSInvocation *))aBlock { [[self stub] addInvocationAction:[[[OCMBlockCaller alloc] initWithCallBlock:aBlock] autorelease]]; return self; } - (id)andForwardToRealObject { [[self stub] addInvocationAction:[[[OCMRealObjectForwarder alloc] init] autorelease]]; return self; } #if !defined(OCM_DISABLE_XCTEST_FEATURES) - (id)andFulfill:(XCTestExpectation *)expectation { return [self andDo:^(NSInvocation *invocation) { [expectation fulfill]; }]; } #endif #pragma mark Finishing recording - (void)forwardInvocation:(NSInvocation *)anInvocation { [super forwardInvocation:anInvocation]; [mockObject addStub:[self stub]]; } @end @implementation OCMStubRecorder (Properties) @dynamic _andReturn; - (OCMStubRecorder * (^)(NSValue *))_andReturn { id (^theBlock)(id) = ^(NSValue *aValue) { if(OCMIsObjectType([aValue objCType])) { id objValue = nil; [aValue getValue:&objValue]; // TODO: deprecated but replacement available in 10.13 only return [self andReturn:objValue]; } else { return [self andReturnValue:aValue]; } }; return (id)[[theBlock copy] autorelease]; } @dynamic _andThrow; - (OCMStubRecorder * (^)(NSException *))_andThrow { id (^theBlock)(id) = ^(NSException *anException) { return [self andThrow:anException]; }; return (id)[[theBlock copy] autorelease]; } @dynamic _andPost; - (OCMStubRecorder * (^)(NSNotification *))_andPost { id (^theBlock)(id) = ^(NSNotification *aNotification) { return [self andPost:aNotification]; }; return (id)[[theBlock copy] autorelease]; } @dynamic _andCall; - (OCMStubRecorder * (^)(id, SEL))_andCall { id (^theBlock)(id, SEL) = ^(id anObject, SEL aSelector) { return [self andCall:aSelector onObject:anObject]; }; return (id)[[theBlock copy] autorelease]; } @dynamic _andDo; - (OCMStubRecorder * (^)(void (^)(NSInvocation *)))_andDo { id (^theBlock)(void (^)(NSInvocation *)) = ^(void (^blockToCall)(NSInvocation *)) { return [self andDo:blockToCall]; }; return (id)[[theBlock copy] autorelease]; } @dynamic _andForwardToRealObject; - (OCMStubRecorder * (^)(void))_andForwardToRealObject { id (^theBlock)(void) = ^(void) { return [self andForwardToRealObject]; }; return (id)[[theBlock copy] autorelease]; } #if !defined(OCM_DISABLE_XCTEST_FEATURES) @dynamic _andFulfill; - (OCMStubRecorder * (^)(XCTestExpectation *))_andFulfill { id (^theBlock)(XCTestExpectation *) = ^(XCTestExpectation *expectation) { return [self andFulfill:expectation]; }; return (id)[[theBlock copy] autorelease]; } #endif @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMVerifier.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @class OCMLocation; @class OCMQuantifier; @interface OCMVerifier : OCMRecorder @property(strong) OCMLocation *location; @property(strong) OCMQuantifier *quantifier; - (id)withQuantifier:(OCMQuantifier *)quantifier; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMVerifier.m ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMVerifier.h" #import "OCMInvocationMatcher.h" #import "OCMLocation.h" #import "OCMQuantifier.h" #import "OCMockObject.h" @implementation OCMVerifier - (id)init { if(invocationMatcher != nil) [NSException raise:NSInternalInconsistencyException format:@"** Method init invoked twice on verifier. Are you trying to verify the init method? This is currently not supported."]; if((self = [super init])) { invocationMatcher = [[OCMInvocationMatcher alloc] init]; } return self; } - (id)withQuantifier:(OCMQuantifier *)quantifier { [self setQuantifier:quantifier]; return self; } - (void)forwardInvocation:(NSInvocation *)anInvocation { [super forwardInvocation:anInvocation]; [mockObject verifyInvocation:invocationMatcher withQuantifier:self.quantifier atLocation:self.location]; } - (void)dealloc { [_location release]; [_quantifier release]; [super dealloc]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMock.h ================================================ /* * Copyright (c) 2004-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import #import #import #import #import #import #import #import #import #import #import ================================================ FILE: Pods/OCMock/Source/OCMock/OCMockMacros.h ================================================ /* * Copyright (c) 2014-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #ifdef OCM_DISABLE_SHORT_SYNTAX #define OCM_DISABLE_SHORT_QSYNTAX #endif #define OCMClassMock(cls) [OCMockObject niceMockForClass:cls] #define OCMStrictClassMock(cls) [OCMockObject mockForClass:cls] #define OCMProtocolMock(protocol) [OCMockObject niceMockForProtocol:protocol] #define OCMStrictProtocolMock(protocol) [OCMockObject mockForProtocol:protocol] #define OCMPartialMock(obj) [OCMockObject partialMockForObject:obj] #define OCMObserverMock() [OCMockObject observerMock] #define OCMStub(invocation) \ ({ \ _OCMSilenceWarnings( \ [OCMMacroState beginStubMacro]; \ OCMStubRecorder *recorder = nil; \ @try{ \ invocation; \ }@catch(...){ \ [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ recorder = [OCMMacroState endStubMacro]; \ } \ recorder; \ ); \ }) #define OCMExpect(invocation) \ ({ \ _OCMSilenceWarnings( \ [OCMMacroState beginExpectMacro]; \ OCMStubRecorder *recorder = nil; \ @try{ \ invocation; \ }@catch(...){ \ [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ recorder = [OCMMacroState endExpectMacro]; \ } \ recorder; \ ); \ }) #define OCMReject(invocation) \ ({ \ _OCMSilenceWarnings( \ [OCMMacroState beginRejectMacro]; \ OCMStubRecorder *recorder = nil; \ @try{ \ invocation; \ }@catch(...){ \ [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ recorder = [OCMMacroState endRejectMacro]; \ } \ recorder; \ ); \ }) #define OCMClassMethod(invocation) \ _OCMSilenceWarnings( \ [[OCMMacroState globalState] switchToClassMethod]; \ invocation; \ ); #ifndef OCM_DISABLE_SHORT_SYNTAX #define ClassMethod(invocation) OCMClassMethod(invocation) #endif #define OCMVerifyAll(mock) [(OCMockObject *)mock verifyAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)] #define OCMVerifyAllWithDelay(mock, delay) [(OCMockObject *)mock verifyWithDelay:delay atLocation:OCMMakeLocation(self, __FILE__, __LINE__)] #define _OCMVerify(invocation) \ ({ \ _OCMSilenceWarnings( \ [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__)]; \ @try{ \ invocation; \ }@catch(...){ \ [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ [OCMMacroState endVerifyMacro]; \ } \ ); \ }) #define _OCMVerifyWithQuantifier(quantifier, invocation) \ ({ \ _OCMSilenceWarnings( \ [OCMMacroState beginVerifyMacroAtLocation:OCMMakeLocation(self, __FILE__, __LINE__) withQuantifier:quantifier]; \ @try{ \ invocation; \ }@catch(...){ \ [[OCMMacroState globalState] setInvocationDidThrow:YES]; \ /* NOLINTNEXTLINE(google-objc-avoid-throwing-exception) */ \ @throw; \ }@finally{ \ [OCMMacroState endVerifyMacro]; \ } \ ); \ }) // explanation for macros below here: https://stackoverflow.com/questions/3046889/optional-parameters-with-c-macros #define _OCMVerify_1(A) _OCMVerify(A) #define _OCMVerify_2(A,B) _OCMVerifyWithQuantifier(A, B) #define _OCMVerify_X(x,A,B,FUNC, ...) FUNC #define OCMVerify(...) _OCMVerify_X(,##__VA_ARGS__, _OCMVerify_2(__VA_ARGS__), _OCMVerify_1(__VA_ARGS__)) #define _OCMSilenceWarnings(macro) \ ({ \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wunused-value\"") \ _Pragma("clang diagnostic ignored \"-Wunused-getter-return-value\"") \ _Pragma("clang diagnostic ignored \"-Wstrict-selector-match\"") \ macro \ _Pragma("clang diagnostic pop") \ }) ================================================ FILE: Pods/OCMock/Source/OCMock/OCMockObject.h ================================================ /* * Copyright (c) 2004-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @class OCMLocation; @class OCMQuantifier; @class OCMInvocationStub; @class OCMStubRecorder; @class OCMInvocationMatcher; @class OCMInvocationExpectation; @interface OCMockObject : NSProxy { BOOL isNice; BOOL expectationOrderMatters; NSMutableArray *stubs; NSMutableArray *expectations; NSMutableArray *exceptions; NSMutableArray *invocations; } + (id)mockForClass:(Class)aClass; + (id)mockForProtocol:(Protocol *)aProtocol; + (id)partialMockForObject:(NSObject *)anObject; + (id)niceMockForClass:(Class)aClass; + (id)niceMockForProtocol:(Protocol *)aProtocol; + (id)observerMock __deprecated_msg("Please use XCTNSNotificationExpectation instead."); - (instancetype)init; - (void)setExpectationOrderMatters:(BOOL)flag; - (id)stub; - (id)expect; - (id)reject; - (id)verify; - (id)verifyAtLocation:(OCMLocation *)location; - (void)verifyWithDelay:(NSTimeInterval)delay; - (void)verifyWithDelay:(NSTimeInterval)delay atLocation:(OCMLocation *)location; - (void)stopMocking; // internal use only - (void)addStub:(OCMInvocationStub *)aStub; - (void)addExpectation:(OCMInvocationExpectation *)anExpectation; - (void)addInvocation:(NSInvocation *)anInvocation; - (BOOL)handleInvocation:(NSInvocation *)anInvocation; - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation; - (BOOL)handleSelector:(SEL)sel; - (void)verifyInvocation:(OCMInvocationMatcher *)matcher; - (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location; - (void)verifyInvocation:(OCMInvocationMatcher *)matcher withQuantifier:(OCMQuantifier *)quantifier atLocation:(OCMLocation *)location; - (NSString *)descriptionForVerificationFailureWithMatcher:(OCMInvocationMatcher *)matcher quantifier:(OCMQuantifier *)quantifier invocationCount:(NSUInteger)count; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCMockObject.m ================================================ /* * Copyright (c) 2004-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "NSInvocation+OCMAdditions.h" #import "OCMockObject.h" #import "OCClassMockObject.h" #import "OCMExceptionReturnValueProvider.h" #import "OCMExpectationRecorder.h" #import "OCMFunctionsPrivate.h" #import "OCMInvocationExpectation.h" #import "OCMLocation.h" #import "OCMMacroState.h" #import "OCMQuantifier.h" #import "OCMVerifier.h" #import "OCObserverMockObject.h" #import "OCPartialMockObject.h" #import "OCProtocolMockObject.h" @implementation OCMockObject #pragma mark Class initialisation + (void)initialize { if([[NSInvocation class] instanceMethodSignatureForSelector:@selector(getArgumentAtIndexAsObject:)] == NULL) [NSException raise:NSInternalInconsistencyException format:@"** Expected method not present; the method getArgumentAtIndexAsObject: is not implemented by NSInvocation. If you see this exception it is likely that you are using the static library version of OCMock and your project is not configured correctly to load categories from static libraries. Did you forget to add the -ObjC linker flag?"]; } #pragma mark Factory methods + (id)mockForClass:(Class)aClass { return [[[OCClassMockObject alloc] initWithClass:aClass] autorelease]; } + (id)mockForProtocol:(Protocol *)aProtocol { return [[[OCProtocolMockObject alloc] initWithProtocol:aProtocol] autorelease]; } + (id)partialMockForObject:(NSObject *)anObject { return [[[OCPartialMockObject alloc] initWithObject:anObject] autorelease]; } + (id)niceMockForClass:(Class)aClass { return [self _makeNice:[self mockForClass:aClass]]; } + (id)niceMockForProtocol:(Protocol *)aProtocol { return [self _makeNice:[self mockForProtocol:aProtocol]]; } + (id)_makeNice:(OCMockObject *)mock { mock->isNice = YES; return mock; } + (id)observerMock { return [[[OCObserverMockObject alloc] init] autorelease]; } #pragma mark Initialisers, description, accessors, etc. - (instancetype)init { // Check whether init is called a second time, which can happen when stubbing alloc/init. Note // that you only really stub the alloc method. Init cannot be stubbed. Invocations of init // will always end up here, and we return self. If init is invoked inside a macro that's an // error, which will be detected in the init method of the recorder. if(stubs != nil) { // check if we are called from inside a macro OCMRecorder *recorder = [[OCMMacroState globalState] recorder]; if(recorder != nil) { [recorder setMockObject:self]; return (id)[[recorder retain] init]; } return self; } if([self class] == [OCMockObject class]) { [NSException raise:NSInternalInconsistencyException format:@"*** Cannot create instances of OCMockObject. Please use one of the subclasses."]; } // no [super init], we're inheriting from NSProxy expectationOrderMatters = NO; stubs = [[NSMutableArray alloc] init]; expectations = [[NSMutableArray alloc] init]; exceptions = [[NSMutableArray alloc] init]; invocations = [[NSMutableArray alloc] init]; return self; } - (void)dealloc { [stubs release]; [expectations release]; [exceptions release]; [invocations release]; [super dealloc]; } - (NSString *)description { return @"OCMockObject"; } - (void)addStub:(OCMInvocationStub *)aStub { [self assertInvocationsArrayIsPresent]; @synchronized(stubs) { [stubs addObject:aStub]; } } - (OCMInvocationStub *)stubForInvocation:(NSInvocation *)anInvocation { @synchronized(stubs) { for(OCMInvocationStub *stub in stubs) if([stub matchesInvocation:anInvocation]) return stub; return nil; } } - (void)addExpectation:(OCMInvocationExpectation *)anExpectation { @synchronized(expectations) { [expectations addObject:anExpectation]; } } - (void)assertInvocationsArrayIsPresent { if(invocations == nil) { [NSException raise:NSInternalInconsistencyException format:@"** Cannot use mock object %@ at %p. This error usually occurs when a mock object is used after stopMocking has been called on it. In most cases it is not necessary to call stopMocking. If you know you have to, please make sure that the mock object is not used afterwards.", [self description], (void *)self]; } } - (void)addInvocation:(NSInvocation *)anInvocation { @synchronized(invocations) { // We can't do a normal retain arguments on anInvocation because its target/arguments/return // value could be self. That would produce a retain cycle self->invocations->anInvocation->self. // However we need to retain everything on anInvocation that isn't self because we expect them to // stick around after this method returns. Use our special method to retain just what's needed. // This still doesn't completely prevent retain cycles since any of the arguments could have a // strong reference to self. Those will have to be broken with manual calls to -stopMocking. [anInvocation retainObjectArgumentsExcludingObject:self]; [invocations addObject:anInvocation]; } } #pragma mark Public API - (void)setExpectationOrderMatters:(BOOL)flag { expectationOrderMatters = flag; } - (void)stopMocking { // invocations can contain objects that clients expect to be deallocated by now, // and they can also have a strong reference to self, creating a retain cycle. Get // rid of all of the invocations to hopefully let their objects deallocate, and to // break any retain cycles involving self. @synchronized(invocations) { [invocations removeAllObjects]; [invocations autorelease]; invocations = nil; } } - (id)stub { return [[[OCMStubRecorder alloc] initWithMockObject:self] autorelease]; } - (id)expect { return [[[OCMExpectationRecorder alloc] initWithMockObject:self] autorelease]; } - (id)reject { return [[self expect] never]; } - (id)verify { return [self verifyAtLocation:nil]; } - (id)verifyAtLocation:(OCMLocation *)location { NSMutableArray *unsatisfiedExpectations = [NSMutableArray array]; @synchronized(expectations) { for(OCMInvocationExpectation *e in expectations) { if(![e isSatisfied]) [unsatisfiedExpectations addObject:e]; } } if([unsatisfiedExpectations count] == 1) { NSString *description = [NSString stringWithFormat:@"%@: expected method was not invoked: %@", [self description], [[unsatisfiedExpectations objectAtIndex:0] description]]; OCMReportFailure(location, description); } else if([unsatisfiedExpectations count] > 0) { NSString *description = [NSString stringWithFormat:@"%@: %@ expected methods were not invoked: %@", [self description], @([unsatisfiedExpectations count]), [self _stubDescriptions:YES]]; OCMReportFailure(location, description); } OCMInvocationExpectation *firstException = nil; @synchronized(exceptions) { firstException = [exceptions.firstObject retain]; } if(firstException) { NSString *description = [NSString stringWithFormat:@"%@: %@ (This is a strict mock failure that was ignored when it actually occurred.)", [self description], [firstException description]]; OCMReportFailure(location, description); } [firstException release]; return [[[OCMVerifier alloc] initWithMockObject:self] autorelease]; } - (void)verifyWithDelay:(NSTimeInterval)delay { [self verifyWithDelay:delay atLocation:nil]; } - (void)verifyWithDelay:(NSTimeInterval)delay atLocation:(OCMLocation *)location { NSTimeInterval step = 0.01; while(delay > 0) { @synchronized(expectations) { BOOL allExpectationsAreMatchAndReject = YES; for(OCMInvocationExpectation *expectation in expectations) { if(![expectation isMatchAndReject]) { allExpectationsAreMatchAndReject = NO; break; } } if(allExpectationsAreMatchAndReject) break; } [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:MIN(step, delay)]]; delay -= step; step *= 2; } [self verifyAtLocation:location]; } #pragma mark Verify after running - (void)verifyInvocation:(OCMInvocationMatcher *)matcher { [self verifyInvocation:matcher atLocation:nil]; } - (void)verifyInvocation:(OCMInvocationMatcher *)matcher atLocation:(OCMLocation *)location { [self verifyInvocation:matcher withQuantifier:nil atLocation:location]; } - (void)verifyInvocation:(OCMInvocationMatcher *)matcher withQuantifier:(OCMQuantifier *)quantifier atLocation:(OCMLocation *)location { NSUInteger count = 0; [self assertInvocationsArrayIsPresent]; @synchronized(invocations) { for(NSInvocation *invocation in invocations) { if([matcher matchesInvocation:invocation]) count += 1; } } if(quantifier == nil) quantifier = [OCMQuantifier atLeast:1]; if(![quantifier isValidCount:count]) { NSString *description = [self descriptionForVerificationFailureWithMatcher:matcher quantifier:quantifier invocationCount:count]; OCMReportFailure(location, description); } } - (NSString *)descriptionForVerificationFailureWithMatcher:(OCMInvocationMatcher *)matcher quantifier:(OCMQuantifier *)quantifier invocationCount:(NSUInteger)count { NSString *actualDescription = nil; switch(count) { case 0: actualDescription = @"not invoked"; break; case 1: actualDescription = @"invoked once"; break; default: actualDescription = [NSString stringWithFormat:@"invoked %lu times", (unsigned long)count]; break; } return [NSString stringWithFormat:@"%@: Method `%@` was %@; but was expected %@.", [self description], [matcher description], actualDescription, [quantifier description]]; } #pragma mark Handling invocations - (id)forwardingTargetForSelector:(SEL)aSelector { if([OCMMacroState globalState] != nil) { OCMRecorder *recorder = [[OCMMacroState globalState] recorder]; [recorder setMockObject:self]; // In order for ARC to work correctly, the recorder has to set up return values for // methods in the init family of methods. If the mock forwards a method to the recorder // that it will record, i.e. a method that the recorder does not implement, then the // recorder must set the mock as the return value. Otherwise it must use itself. [recorder setShouldReturnMockFromInit:(class_getInstanceMethod(object_getClass(recorder), aSelector) == NO)]; return recorder; } return nil; } - (BOOL)handleSelector:(SEL)sel { @synchronized(stubs) { for(OCMInvocationStub *recorder in stubs) if([recorder matchesSelector:sel]) return YES; } return NO; } - (void)forwardInvocation:(NSInvocation *)anInvocation { @try { if([self handleInvocation:anInvocation] == NO) [self handleUnRecordedInvocation:anInvocation]; } @catch(NSException *e) { if([[e name] isEqualToString:OCMStubbedException]) { e = [[e userInfo] objectForKey:@"exception"]; } else { // add non-stubbed method to list of exceptions to be re-raised in verify @synchronized(exceptions) { [exceptions addObject:e]; } } [e raise]; } } - (BOOL)handleInvocation:(NSInvocation *)anInvocation { [self assertInvocationsArrayIsPresent]; [self addInvocation:anInvocation]; OCMInvocationStub *stub = [self stubForInvocation:anInvocation]; if(stub == nil) return NO; // Retain the stub in case it ends up being removed because we still need it at the end for handleInvocation: [stub retain]; BOOL removeStub = NO; @synchronized(expectations) { if([expectations containsObject:stub]) { OCMInvocationExpectation *expectation = [self _nextExpectedInvocation]; if(expectationOrderMatters && (expectation != stub)) { [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@\n\texpected:\t%@", [self description], [stub description], [[expectations objectAtIndex:0] description]]; } // We can't check isSatisfied yet, since the stub won't be satisfied until we call // handleInvocation: since we'll still have the current expectation in the expectations array, which // will cause an exception if expectationOrderMatters is YES and we're not ready for any future // expected methods to be called yet if(![(OCMInvocationExpectation *)stub isMatchAndReject]) { [expectations removeObject:stub]; removeStub = YES; } } } if(removeStub) { @synchronized(stubs) { [stubs removeObject:stub]; } } @try { [stub handleInvocation:anInvocation]; } @finally { [stub release]; } return YES; } // Must be synchronized on expectations when calling this method. - (OCMInvocationExpectation *)_nextExpectedInvocation { for(OCMInvocationExpectation *expectation in expectations) if(![expectation isMatchAndReject]) return expectation; return nil; } - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation { if(isNice == NO) { [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected method invoked: %@ %@", [self description], [anInvocation invocationDescription], [self _stubDescriptions:NO]]; } } - (void)doesNotRecognizeSelector:(SEL)aSelector __unused { if([OCMMacroState globalState] != nil) { // we can't do anything clever with the macro state because we must raise an exception here [NSException raise:NSInvalidArgumentException format:@"%@: Cannot stub/expect/verify method '%@' because no such method exists in the mocked class.", [self description], NSStringFromSelector(aSelector)]; } else { [NSException raise:NSInvalidArgumentException format:@"-[%@ %@]: unrecognized selector sent to instance %p", [self description], NSStringFromSelector(aSelector), (void *)self]; } } #pragma mark Helper methods - (NSString *)_stubDescriptions:(BOOL)onlyExpectations { NSMutableString *outputString = [NSMutableString string]; NSArray *stubsCopy = nil; @synchronized(stubs) { stubsCopy = [stubs copy]; } for(OCMStubRecorder *stub in stubsCopy) { BOOL expectationsContainStub = NO; @synchronized(expectations) { expectationsContainStub = [expectations containsObject:stub]; } NSString *prefix = @""; if(onlyExpectations) { if(expectationsContainStub == NO) continue; } else { if(expectationsContainStub) prefix = @"expected:\t"; else prefix = @"stubbed:\t"; } [outputString appendFormat:@"\n\t%@%@", prefix, [stub description]]; } [stubsCopy release]; return outputString; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCObserverMockObject.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import @class OCMLocation; __deprecated_msg("Please use XCTNSNotificationExpectation instead.") @interface OCObserverMockObject : NSObject { BOOL expectationOrderMatters; NSMutableArray *recorders; NSMutableArray *centers; } - (void)setExpectationOrderMatters:(BOOL)flag; - (id)expect; - (void)verify; - (void)verifyAtLocation:(OCMLocation *)location; - (void)handleNotification:(NSNotification *)aNotification; // internal use - (void)autoRemoveFromCenter:(NSNotificationCenter *)aCenter; - (NSNotification *)notificationWithName:(NSString *)name object:(id)sender; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCObserverMockObject.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCObserverMockObject.h" #import "OCMFunctionsPrivate.h" #import "OCMLocation.h" #import "OCMMacroState.h" #import "OCMObserverRecorder.h" #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" @implementation OCObserverMockObject #pragma clang diagnostic pop #pragma mark Initialisers, description, accessors, etc. - (id)init { if((self = [super init])) { recorders = [[NSMutableArray alloc] init]; centers = [[NSMutableArray alloc] init]; } return self; } - (id)retain { return [super retain]; } - (void)dealloc { for(NSNotificationCenter *c in centers) [c removeObserver:self]; [centers release]; [recorders release]; [super dealloc]; } - (NSString *)description { return @"OCObserverMockObject"; } - (void)setExpectationOrderMatters:(BOOL)flag { expectationOrderMatters = flag; } - (void)autoRemoveFromCenter:(NSNotificationCenter *)aCenter { @synchronized(centers) { [centers addObject:aCenter]; } } #pragma mark Public API - (id)expect { OCMObserverRecorder *recorder = [[[OCMObserverRecorder alloc] init] autorelease]; @synchronized(recorders) { [recorders addObject:recorder]; } return recorder; } - (void)verify { [self verifyAtLocation:nil]; } - (void)verifyAtLocation:(OCMLocation *)location { @synchronized(recorders) { if([recorders count] == 1) { NSString *description = [NSString stringWithFormat:@"%@: expected notification was not observed: %@", [self description], [[recorders lastObject] description]]; OCMReportFailure(location, description); } else if([recorders count] > 0) { NSString *description = [NSString stringWithFormat:@"%@ : %@ expected notifications were not observed.", [self description], @([recorders count])]; OCMReportFailure(location, description); } } } #pragma mark Receiving recording requests via macro // This is a bit of a hack. The methods simply assume that when they are called from within a macro that it's // the OCMExpect macro. That creates a recorder for mock objects, which we cannot use here. So, we overwrite // it with a newly allocated recorder. - (NSNotification *)notificationWithName:(NSString *)name object:(id)sender { if([OCMMacroState globalState] != nil) { id recorder = [self expect]; [[OCMMacroState globalState] setRecorder:recorder]; return [recorder notificationWithName:name object:sender]; } return nil; } - (NSNotification *)notificationWithName:(NSString *)name object:(id)sender userInfo:(NSDictionary *)userInfo { if([OCMMacroState globalState] != nil) { id recorder = [self expect]; [[OCMMacroState globalState] setRecorder:recorder]; return [recorder notificationWithName:name object:sender userInfo:userInfo]; } return nil; } #pragma mark Receiving notifications - (void)handleNotification:(NSNotification *)aNotification { @synchronized(recorders) { NSUInteger i, limit; limit = expectationOrderMatters ? 1 : [recorders count]; for(i = 0; i < limit; i++) { if([[recorders objectAtIndex:i] matchesNotification:aNotification]) { [recorders removeObjectAtIndex:i]; return; } } } [NSException raise:NSInternalInconsistencyException format:@"%@: unexpected notification observed: %@", [self description], [aNotification description]]; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCPartialMockObject.h ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCClassMockObject.h" @interface OCPartialMockObject : OCClassMockObject { NSObject *realObject; NSInvocation *invocationFromMock; } - (id)initWithObject:(NSObject *)anObject; - (NSObject *)realObject; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCPartialMockObject.m ================================================ /* * Copyright (c) 2009-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "NSInvocation+OCMAdditions.h" #import "NSMethodSignature+OCMAdditions.h" #import "NSObject+OCMAdditions.h" #import "OCPartialMockObject.h" #import "OCMFunctionsPrivate.h" #import "OCMInvocationStub.h" @implementation OCPartialMockObject #pragma mark Initialisers, description, accessors, etc. - (id)initWithObject:(NSObject *)anObject { if(anObject == nil) [NSException raise:NSInvalidArgumentException format:@"Object cannot be nil."]; if([anObject isProxy]) [NSException raise:NSInvalidArgumentException format:@"OCMock does not support partially mocking subclasses of NSProxy."]; Class const class = [self classToSubclassForObject:anObject]; [super initWithClass:class]; realObject = [anObject retain]; [self prepareObjectForInstanceMethodMocking]; return self; } - (NSString *)description { return [NSString stringWithFormat:@"OCPartialMockObject(%@)", NSStringFromClass(mockedClass)]; } - (NSObject *)realObject { return realObject; } #pragma mark Helper methods - (void)assertClassIsSupported:(Class)class { [super assertClassIsSupported:class]; NSString *classname = NSStringFromClass(class); NSString *reason = nil; if([classname hasPrefix:@"__NSTagged"] || [classname hasPrefix:@"NSTagged"]) reason = [NSString stringWithFormat:@"OCMock does not support partially mocking tagged classes; got %@", classname]; else if([classname hasPrefix:@"__NSCF"]) reason = [NSString stringWithFormat:@"OCMock does not support partially mocking toll-free bridged classes; got %@", classname]; if(reason != nil) [[NSException exceptionWithName:NSInvalidArgumentException reason:reason userInfo:nil] raise]; } - (Class)classToSubclassForObject:(id)object { if([object observationInfo] != NULL) { // Special treatment for objects that are observed with KVO. The KVO implementation sets // a subclass for such objects and it overrides the -class method to return the original // class. If we base our subclass on the KVO subclass, as returned by object_getClass(), // crashes will occur. So, we take the real class instead. Unfortunately, this removes // any observers set up before. NSLog(@"Warning: Creating a partial mock for %@. This object has observers, which will now stop receiving KVO notifications. If you want to receive KVO notifications, create the partial mock first, and then register the observer.", object); return [object class]; } return object_getClass(object); } #pragma mark Extending/overriding superclass behaviour - (void)stopMocking { if(realObject != nil) { Class partialMockClass = object_getClass(realObject); OCMSetAssociatedMockForObject(nil, realObject); object_setClass(realObject, [self mockedClass]); [realObject release]; realObject = nil; OCMDisposeSubclass(partialMockClass); } [super stopMocking]; } - (void)addStub:(OCMInvocationStub *)aStub { [super addStub:aStub]; if(![aStub recordedAsClassMethod]) [self setupForwarderForSelector:[[aStub recordedInvocation] selector]]; } - (void)addInvocation:(NSInvocation *)anInvocation { // If the mock invokes a method on the real object we end up here a second time, but because // the mock has added the invocation already we do not want to add it again. if((invocationFromMock == nil) || ([anInvocation selector] != [invocationFromMock selector])) [super addInvocation:anInvocation]; } - (void)handleUnRecordedInvocation:(NSInvocation *)anInvocation { // In the case of an init that is called on a mock we must return the mock instance and // not the realObject if the underlying init returns the realObject because at the call site // ARC will have retained the target and the release/retain count must balance. If we return // the realObject, then realObject will be over released and the mock will leak. Equally if // we are called on the realObject we need to make sure not to return the mock. id targetReceivingInit = nil; if([anInvocation methodIsInInitFamily]) { targetReceivingInit = [anInvocation target]; [realObject retain]; } invocationFromMock = anInvocation; [anInvocation invokeWithTarget:realObject]; invocationFromMock = nil; if(targetReceivingInit) { id returnVal; [anInvocation getReturnValue:&returnVal]; if(returnVal == realObject) { [anInvocation setReturnValue:&self]; [realObject release]; [self retain]; } #ifndef __clang_analyzer__ // see #456 for details [targetReceivingInit release]; #endif } } #pragma mark Subclass management - (void)prepareObjectForInstanceMethodMocking { OCMSetAssociatedMockForObject(self, realObject); /* dynamically create a subclass and set it as the class of the object */ Class subclass = OCMCreateSubclass(mockedClass, realObject); object_setClass(realObject, subclass); /* point forwardInvocation: of the object to the implementation in the mock */ Method myForwardMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardInvocationForRealObject:)); IMP myForwardIMP = method_getImplementation(myForwardMethod); class_addMethod(subclass, @selector(forwardInvocation:), myForwardIMP, method_getTypeEncoding(myForwardMethod)); /* do the same for forwardingTargetForSelector, remember existing imp with alias selector */ Method myForwardingTargetMethod = class_getInstanceMethod([self mockObjectClass], @selector(forwardingTargetForSelectorForRealObject:)); IMP myForwardingTargetIMP = method_getImplementation(myForwardingTargetMethod); IMP originalForwardingTargetIMP = [mockedClass instanceMethodForSelector:@selector(forwardingTargetForSelector:)]; class_addMethod(subclass, @selector(forwardingTargetForSelector:), myForwardingTargetIMP, method_getTypeEncoding(myForwardingTargetMethod)); class_addMethod(subclass, @selector(ocmock_replaced_forwardingTargetForSelector:), originalForwardingTargetIMP, method_getTypeEncoding(myForwardingTargetMethod)); /* We also override the -class method to return the original class */ Method myObjectClassMethod = class_getInstanceMethod([self mockObjectClass], @selector(classForRealObject)); const char *objectClassTypes = method_getTypeEncoding(myObjectClassMethod); IMP myObjectClassImp = method_getImplementation(myObjectClassMethod); class_addMethod(subclass, @selector(class), myObjectClassImp, objectClassTypes); /* Adding forwarder for most instance methods to allow for verify after run */ NSArray *methodsNotToForward = @[ @"class", @"forwardingTargetForSelector:", @"methodSignatureForSelector:", @"forwardInvocation:", @"allowsWeakReference", @"retainWeakReference", @"isBlock", @"retainCount", @"retain", @"release", @"autorelease", @".cxx_construct", @".cxx_destruct" ]; void (^setupForwarderFiltered)(Class, SEL) = ^(Class cls, SEL sel) { if(OCMIsAppleBaseClass(cls) || OCMIsApplePrivateMethod(cls, sel)) return; if([methodsNotToForward containsObject:NSStringFromSelector(sel)]) return; @try { [self setupForwarderForSelector:sel]; } @catch(NSException *e) { // ignore for now } }; [NSObject enumerateMethodsInClass:mockedClass usingBlock:setupForwarderFiltered]; } - (void)setupForwarderForSelector:(SEL)sel { SEL aliasSelector = OCMAliasForOriginalSelector(sel); if(class_getInstanceMethod(object_getClass(realObject), aliasSelector) != NULL) return; Method originalMethod = class_getInstanceMethod(mockedClass, sel); /* Might be NULL if the selector is forwarded to another class */ IMP originalIMP = (originalMethod != NULL) ? method_getImplementation(originalMethod) : NULL; const char *types = (originalMethod != NULL) ? method_getTypeEncoding(originalMethod) : NULL; // TODO: check the fallback implementation is actually sufficient if(types == NULL) types = ([[mockedClass instanceMethodSignatureForSelector:sel] fullObjCTypes]); Class subclass = object_getClass([self realObject]); IMP forwarderIMP = [mockedClass instanceMethodForwarderForSelector:sel]; class_replaceMethod(subclass, sel, forwarderIMP, types); class_addMethod(subclass, aliasSelector, originalIMP, types); } // Implementation of the -class method; return the Class that was reported with [realObject class] prior to mocking - (Class)classForRealObject { // in here "self" is a reference to the real object, not the mock OCPartialMockObject *mock = OCMGetAssociatedMockForObject(self); if(mock == nil) [NSException raise:NSInternalInconsistencyException format:@"No partial mock for object %p", self]; return [mock mockedClass]; } - (id)forwardingTargetForSelectorForRealObject:(SEL)sel { // in here "self" is a reference to the real object, not the mock OCPartialMockObject *mock = OCMGetAssociatedMockForObject(self); if(mock == nil) [NSException raise:NSInternalInconsistencyException format:@"No partial mock for object %p", self]; if([mock handleSelector:sel]) return self; return [self ocmock_replaced_forwardingTargetForSelector:sel]; } // Make the compiler happy in -forwardingTargetForSelectorForRealObject: because it can't find the message… - (id)ocmock_replaced_forwardingTargetForSelector:(SEL)sel { return nil; } - (void)forwardInvocationForRealObject:(NSInvocation *)anInvocation { // in here "self" is a reference to the real object, not the mock OCPartialMockObject *mock = OCMGetAssociatedMockForObject(self); if(mock == nil) [NSException raise:NSInternalInconsistencyException format:@"No partial mock for object %p", self]; if([mock handleInvocation:anInvocation] == NO) { [anInvocation setSelector:OCMAliasForOriginalSelector([anInvocation selector])]; [anInvocation invoke]; } } #pragma mark Verification handling - (NSString *)descriptionForVerificationFailureWithMatcher:(OCMInvocationMatcher *)matcher quantifier:(OCMQuantifier *)quantifier invocationCount:(NSUInteger)count { SEL matcherSel = [[matcher recordedInvocation] selector]; __block BOOL stubbingMightHelp = NO; [NSObject enumerateMethodsInClass:mockedClass usingBlock:^(Class cls, SEL sel) { if(sel == matcherSel) stubbingMightHelp = OCMIsAppleBaseClass(cls) || OCMIsApplePrivateMethod(cls, sel); }]; NSString *description = [super descriptionForVerificationFailureWithMatcher:matcher quantifier:quantifier invocationCount:count]; if(stubbingMightHelp) { description = [description stringByAppendingFormat:@" Adding a stub for the method may resolve the issue, e.g. `OCMStub([mockObject %@]).andForwardToRealObject()`", [matcher description]]; } return description; } @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCProtocolMockObject.h ================================================ /* * Copyright (c) 2005-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import "OCMockObject.h" @interface OCProtocolMockObject : OCMockObject { Protocol *mockedProtocol; } - (id)initWithProtocol:(Protocol *)aProtocol; @end ================================================ FILE: Pods/OCMock/Source/OCMock/OCProtocolMockObject.m ================================================ /* * Copyright (c) 2005-2021 Erik Doernenburg and contributors * * Licensed under the Apache License, Version 2.0 (the "License"); you may * not use these files except in compliance with the License. You may obtain * a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations * under the License. */ #import #import "OCProtocolMockObject.h" @implementation OCProtocolMockObject #pragma mark Initialisers, description, accessors, etc. - (id)initWithProtocol:(Protocol *)aProtocol { if(aProtocol == nil) [NSException raise:NSInvalidArgumentException format:@"Protocol cannot be nil."]; [super init]; mockedProtocol = aProtocol; return self; } - (NSString *)description { const char *name = protocol_getName(mockedProtocol); return [NSString stringWithFormat:@"OCProtocolMockObject(%s)", name]; } #pragma mark Proxy API - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { struct { BOOL isRequired; BOOL isInstance; } opts[4] = { {YES, YES}, {NO, YES}, {YES, NO}, {NO, NO} }; for(int i = 0; i < 4; i++) { struct objc_method_description methodDescription = protocol_getMethodDescription(mockedProtocol, aSelector, opts[i].isRequired, opts[i].isInstance); if(methodDescription.name != NULL) return [NSMethodSignature signatureWithObjCTypes:methodDescription.types]; } return nil; } - (BOOL)conformsToProtocol:(Protocol *)aProtocol { return protocol_conformsToProtocol(mockedProtocol, aProtocol); } - (BOOL)respondsToSelector:(SEL)selector { return ([self methodSignatureForSelector:selector] != nil); } @end ================================================ FILE: Pods/Pods.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 77; objects = { /* Begin PBXBuildFile section */ 000A3A1711489B7F1981551FB9D5FF42 /* TyphoonFactoryDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = F813C3F784E8340DED279F0AC918CD92 /* TyphoonFactoryDefinition.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 009A101C6D6749EEE990C935A028904B /* UICollectionReusableView+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 87D6C46EF50641EB979304EC92ADD341 /* UICollectionReusableView+RACSignalSupport.m */; }; 00DAE48C9A4FBCD1FCAA922CA57B45F9 /* SDWebImageDownloaderRequestModifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 4BE4EBEC7840EB2B41361CE32163EA16 /* SDWebImageDownloaderRequestModifier.m */; }; 01337B28102993C3FDD41D9A2E0AFAB2 /* RACCompoundDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B2D3D98B9F08D80C8134D1C5346BA57 /* RACCompoundDisposable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 01AD9C4D5168F89B9844CB3E46072DE7 /* NSObject+RACDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = B2042CA0D12A3305ACABBBC22E60BE48 /* NSObject+RACDescription.m */; }; 01B2E81FBD9A6D179150C9130B2C1807 /* UIRefreshControl+RACCommandSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 9267DAB90224A45DA4C991474C964341 /* UIRefreshControl+RACCommandSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 026C6439062428052B58FA8D41877DB1 /* OCMBlockArgCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 1610B6558D631E16BFCF0FD099283BDA /* OCMBlockArgCaller.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 0298EAC75B6300AD806B86273DE13278 /* TyphoonStoryboardResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = 004DB416A9430FE442A136525FC3D93D /* TyphoonStoryboardResolver.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 02BDC4A577B91415561618A54DCB721E /* TyphoonPatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F8278A4894BA9FD972FEC5FCB716F39 /* TyphoonPatcher.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 037B74553C43333B8693ACA61DB05C78 /* OCMRealObjectForwarder.h in Headers */ = {isa = PBXBuildFile; fileRef = 45BD3FAB4630B2990E6B8F1BC78D9D8F /* OCMRealObjectForwarder.h */; settings = {ATTRIBUTES = (Project, ); }; }; 03815ACD5E64B92672AE8AA3A5A2EE27 /* OCMArg.h in Headers */ = {isa = PBXBuildFile; fileRef = FB65E6E7D6717ADB26576EA05A351211 /* OCMArg.h */; settings = {ATTRIBUTES = (Public, ); }; }; 03A1468408B50A609EF3669C178B99D4 /* OCMInvocationExpectation.m in Sources */ = {isa = PBXBuildFile; fileRef = 80863B62A376A21783063A91F74E52C6 /* OCMInvocationExpectation.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 03E6A871ACAE01F6AD2BFD0079FC0664 /* TyphoonNSURLTypeConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = 98854B7F25ABB6BE45BB69C58166AA5E /* TyphoonNSURLTypeConverter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 042D40751BD2F51FBE9FECD4707CBBE9 /* SDDeviceHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = 92A096C440891899FBCBB081ED6C8474 /* SDDeviceHelper.h */; settings = {ATTRIBUTES = (Private, ); }; }; 0453019EC6578A67B82CF569EC765546 /* SDFileAttributeHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = BAF592AE664D056D895A926E0F7D0261 /* SDFileAttributeHelper.h */; settings = {ATTRIBUTES = (Private, ); }; }; 04743C039FBAF096D2EFAF6CBBA24BE0 /* TyphoonFactoryAutoInjectionPostProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = F0E38DEA6A48AB6D0B5359119070CCD3 /* TyphoonFactoryAutoInjectionPostProcessor.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 0510DC02DA8CA59E8F2E373AF813BDB6 /* TyphoonBundledImageTypeConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = B7B1F42980891C0D9253DA6E3EEDE467 /* TyphoonBundledImageTypeConverter.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 054C3B2C429E6F3BDCDA2A480D917F51 /* RACMulticastConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 2E023F08FB04D7249EE7E6B1E84A12DC /* RACMulticastConnection.m */; }; 055CCD518759FBBD7DFD0B7C079A428B /* NSObject+FactoryHooks.h in Headers */ = {isa = PBXBuildFile; fileRef = 3B0FD681CCD09F3DE9E1B8663D3F1CC2 /* NSObject+FactoryHooks.h */; settings = {ATTRIBUTES = (Public, ); }; }; 05E2B7C1DB7528A0BBEA1521BE0DBAF1 /* MASViewAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = E928F67B2793E36DB0271AE60B7C1747 /* MASViewAttribute.h */; settings = {ATTRIBUTES = (Public, ); }; }; 05F22F8B6A39ADEE5F919070E6B2B869 /* UITextField+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 1428E07DEC90DAD4F228BB35096D20BF /* UITextField+RACSignalSupport.m */; }; 06C4E233E7977DB81A24482E69B2D7D7 /* UIImage+Transform.m in Sources */ = {isa = PBXBuildFile; fileRef = 026AD1F39E6F56A1374F84466603518D /* UIImage+Transform.m */; }; 07BC2019BAA4D31E62D7624C8E4F22E8 /* TyphoonBlockComponentFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 533ECD8A38FC36466554EF43B205AD18 /* TyphoonBlockComponentFactory.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 07C6B68367DDC6BCD78E44FFBCB8F379 /* TyphoonAbstractDetachableComponentFactoryPostProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = B2A8FF4E1BDBA3D59428BAC8FAA55FFD /* TyphoonAbstractDetachableComponentFactoryPostProcessor.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 08495F2DAFA36769FC16758CFDAC1F67 /* GCDAsyncUdpSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 7C4D52064EBE28BCC66250183EE0A11E /* GCDAsyncUdpSocket.m */; }; 08699A51E1CCDD2B8DD889A284ABE884 /* RACEXTScope.h in Headers */ = {isa = PBXBuildFile; fileRef = BCD5C7A53B74E13B1712A968B6AD9302 /* RACEXTScope.h */; settings = {ATTRIBUTES = (Public, ); }; }; 086E51FFE1FBD5D2C964EC8F7C1C1F5A /* Typhoon-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C35F5A9D87E1BF9E90AD737FA10D987 /* Typhoon-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 089F3C4BAA46A37EC5763DD312771021 /* SDImageIOAnimatedCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = C3E573EB2C906BB6045C58BC27C4CFB2 /* SDImageIOAnimatedCoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 08D50C5AC969A3701B6F9137CF3A10F1 /* UIImage+ForceDecode.m in Sources */ = {isa = PBXBuildFile; fileRef = 1083669FB5354500A46F16186007AE49 /* UIImage+ForceDecode.m */; }; 08E8B7ECC821E18E4B5B4F746E7337CD /* UIRefreshControl+RACCommandSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 78AC2407E9C7F573D2B8F2765AD61982 /* UIRefreshControl+RACCommandSupport.m */; }; 09A2ACBC8CE1761652EAA20886AEFE10 /* SDImageCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = E1B9673A5D86AD933259E18FA172B615 /* SDImageCoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 09C8A4EE1CC2BC62C3BC5EAC4F9E3BFD /* TyphoonInject.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A672A6EEB28F3A0ADB373CDCB3697AE /* TyphoonInject.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 09E7EED01CBCF945678E3D23B23EC0DD /* TyphoonConfigPostProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A3B0D97818AFB9EC630C3B522130631 /* TyphoonConfigPostProcessor.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 0B0E6CECDF516BC83756C1D5515A725B /* SDAsyncBlockOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 977CC2B85F6C66B4997C1A77F749621E /* SDAsyncBlockOperation.m */; }; 0B222396072B294B46CBB818B208CC48 /* TyphoonComponentFactory+InstanceBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 05C311CA83E10EC09FC2A8CAAF090A30 /* TyphoonComponentFactory+InstanceBuilder.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 0BF77394EAA86673E7948ECD9871D89F /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D93216F983293A53686F68A6B9A39E70 /* Security.framework */; }; 0C2B2878E7CF92392FFBFBD12C748255 /* TyphoonBundleResource.m in Sources */ = {isa = PBXBuildFile; fileRef = 15B1114AA6F4793852697F0F588E7204 /* TyphoonBundleResource.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 0C6956D9B1BAF797C380C6AEA4C23A5D /* RACBehaviorSubject.h in Headers */ = {isa = PBXBuildFile; fileRef = 16BEA40224A6DCCBB7E983D78F3CE027 /* RACBehaviorSubject.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0C994CE9E9CC8E7B31B2C862DB66E501 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = B663AA0974236ABB5B164B3F2FCA9874 /* OCMNonRetainingObjectReturnValueProvider.h */; settings = {ATTRIBUTES = (Project, ); }; }; 0CD72127DA641AD9C10CF2DB2D352C6D /* CocoaAsyncSocket-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 1FFAB3AC801B7EA6B022EBB03D54462C /* CocoaAsyncSocket-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0D45C478B6225E26D568D6CA97DA18FA /* OCClassMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = B7CC02F4E9B33CCE0CE749A8F5F1ACA7 /* OCClassMockObject.h */; settings = {ATTRIBUTES = (Project, ); }; }; 0D535DEFAB38F82F523FA2970DDE9A88 /* OCMNotificationPoster.h in Headers */ = {isa = PBXBuildFile; fileRef = B5D03644E567590DEDC2D0AE3425D98D /* OCMNotificationPoster.h */; settings = {ATTRIBUTES = (Project, ); }; }; 0DB61F661491104AF5F8778F810C37D4 /* RACSubscriber.m in Sources */ = {isa = PBXBuildFile; fileRef = 1291349B3C35F79CB6F631FD1CDCC1B0 /* RACSubscriber.m */; }; 0DF1C43C7B27B093F8BDF7B951233D19 /* TyphoonSelector.m in Sources */ = {isa = PBXBuildFile; fileRef = 95E34BD0B0987191F26A0B35F90CF6E5 /* TyphoonSelector.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 0E391D08B6652D83393D3E86C70B9BEB /* TyphoonInjectionByObjectInstance.m in Sources */ = {isa = PBXBuildFile; fileRef = 67345DA89FFA1B9FC73EA47478CB886D /* TyphoonInjectionByObjectInstance.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 0E6C53D91EE72B414A29D19423842652 /* OCMRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 1F18C60D29A81CE3566D4A38800979FA /* OCMRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0E8A6DC318277807A45CB21C40672C3F /* RACSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BCF3C4AEB69B5B155617E83726EB0A0 /* RACSignal.m */; }; 0EE4D51D6E6CB670272FED77C64D87EE /* OCObserverMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 8F54C727953E14DD511A46A9F90E8937 /* OCObserverMockObject.h */; settings = {ATTRIBUTES = (Project, ); }; }; 0F0B53B1D332733C46244D44DAB58508 /* TyphoonColorConversionUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 42BB4F99181287983D693A457D56DE96 /* TyphoonColorConversionUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0F1D0F5DCC8C94A4C684DF846D14F436 /* SDWebImagePrefetcher.m in Sources */ = {isa = PBXBuildFile; fileRef = E14AF6E304C6098B7A146E1BAFC26C6C /* SDWebImagePrefetcher.m */; }; 0F307CCAE2D45876CCD0983763D5D45B /* RACEmptySignal.h in Headers */ = {isa = PBXBuildFile; fileRef = D8706CB35453DAA97FE6E56A07BF5E79 /* RACEmptySignal.h */; settings = {ATTRIBUTES = (Private, ); }; }; 0F5ACCF4A4F36FAC65E045A9F87E3003 /* TyphoonInjectionByObjectFromString.m in Sources */ = {isa = PBXBuildFile; fileRef = 419694111A274B662810AE1FECA72F35 /* TyphoonInjectionByObjectFromString.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 0F76408EFB678D11DB62752B64017A40 /* TyphoonPropertyInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = F3F7B76572B953FCBB908A0E7605EB53 /* TyphoonPropertyInjection.h */; settings = {ATTRIBUTES = (Public, ); }; }; 0FF9F459ED16719292443A4C99B52B20 /* SDImageCache.m in Sources */ = {isa = PBXBuildFile; fileRef = E597546B5AFBBDBBDE17E78E1E1EC111 /* SDImageCache.m */; }; 0FFA63B0103710B3ACDAE72B33A73200 /* NSInvocation+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = D66DFDCCBA63EB04C097EA2E0C475EE5 /* NSInvocation+OCMAdditions.h */; settings = {ATTRIBUTES = (Project, ); }; }; 10017B43AC38C3A89D7AC1376C6E7066 /* SDImageLoadersManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 0851ED90E0FAF2464FCE1AB36209BB60 /* SDImageLoadersManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1046610A51620E727140217FEC6DD5BF /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; 10B1F7A3301C951B9F48B9B8F0CB1B31 /* Pods-iOS-Network-Stack-DiveTests-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 1051F60A0834845519249BBEEB7669EF /* Pods-iOS-Network-Stack-DiveTests-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 10F5D29724DC5FC71BFB33A0B6EB3D6B /* TyphoonInjectionByRuntimeArgument.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E607FCD32089EA9D313F72664F0B17B /* TyphoonInjectionByRuntimeArgument.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 110880E9AFA349C846476980F8AF8086 /* TyphoonTypeConversionUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 06B502FEF6AC1E2D1DECEF63D08CC04F /* TyphoonTypeConversionUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; 11422CB222C72A6EF77DA96D152FA6F8 /* UIBarButtonItem+RACCommandSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 63342C5E18062111D90810C1BF11878B /* UIBarButtonItem+RACCommandSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 127034388C945656BD5A7C979DA4029E /* TyphoonInjectionByCurrentRuntimeArguments.h in Headers */ = {isa = PBXBuildFile; fileRef = C0DFA61DA615C88D4F562DF9D047B134 /* TyphoonInjectionByCurrentRuntimeArguments.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12A631453F473C22F8936BF263BAD23E /* TyphoonJsonStyleConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 821F3BAC1C291831ECD436462BB57063 /* TyphoonJsonStyleConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12A67ABB2A4FE1BCF978075EBC33EB90 /* TyphoonInstancePostProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 1061DB1631D08DF649DC362D1104033F /* TyphoonInstancePostProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 12BE89AC12766C6CE239FD4EA3543599 /* UIView+TyphoonOutletTransfer.m in Sources */ = {isa = PBXBuildFile; fileRef = 8065BF1B5F4AFDB278D6AB4C732153BE /* UIView+TyphoonOutletTransfer.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 12DEDFBDD6A19E45734C9BF576DB4379 /* RACSerialDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = 26BFC051219DBE2ECE65E6DD31A177F9 /* RACSerialDisposable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 133E972891174F621EB1C605A111C61E /* TyphoonComponentFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 2EE6EC22E733D815C1F5084910F7491B /* TyphoonComponentFactory.h */; settings = {ATTRIBUTES = (Public, ); }; }; 138463D749696B86F3324AC0882A64EB /* GCDAsyncSocket.m in Sources */ = {isa = PBXBuildFile; fileRef = 88A89A507382B833489D6DB8D75E6DE2 /* GCDAsyncSocket.m */; }; 1444DFD1492DECA5C8C0760EF0FB6CA6 /* NSDictionary+RACSequenceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CE2A8DCEE78DE1933B81C994103BE63 /* NSDictionary+RACSequenceAdditions.m */; }; 14676CDFFDE06FF960179AA34C474EEE /* RACStringSequence.h in Headers */ = {isa = PBXBuildFile; fileRef = 8A14CECA21E108EE12451203537A6B99 /* RACStringSequence.h */; settings = {ATTRIBUTES = (Public, ); }; }; 147A5463FF6FFE40426B898B60344085 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5EFE23AB30614DC2B36B61715A6990FD /* CFNetwork.framework */; }; 14CA284AC4FF1EED75E785641EE98034 /* SDImageCacheConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 8AC5ECC34DB25CB5305086B539A17417 /* SDImageCacheConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 150373979C0DF8F34C788F000C2BE265 /* TyphoonInjectionByConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 765309DEC91CA062A4172B75C508325C /* TyphoonInjectionByConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 15B27182B591769C57B55544260DC886 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; 165F1C9CBD621828C788A3018D0426C5 /* SDImageAPNGCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = BD5A68EEDD637AF3394D147538906789 /* SDImageAPNGCoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 16D7DCB7CC985C33EEC41B371C029C84 /* SDWebImage-SDWebImage in Resources */ = {isa = PBXBuildFile; fileRef = CF1281E58AA1045D4B7F33FC56691C42 /* SDWebImage-SDWebImage */; }; 1708C1D28B421C4AD310426D1695CE77 /* SDAnimatedImage.m in Sources */ = {isa = PBXBuildFile; fileRef = E5F9720FC7B55FFB33E76C326A7DDE5E /* SDAnimatedImage.m */; }; 17134C3258CDB162F0507654B177CAE3 /* RACUnit.h in Headers */ = {isa = PBXBuildFile; fileRef = B28FA990E304B0E57E1599F3DDAC5301 /* RACUnit.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1725041850D30E89E549FD343C9B1073 /* NSMethodSignature+TCFUnwrapValues.h in Headers */ = {isa = PBXBuildFile; fileRef = CAE4756369959CF3EB303367CDDEBAFD /* NSMethodSignature+TCFUnwrapValues.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1754DD5511A7BF462B116F70B0D4006A /* SDWebImageOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F47471F1705425CC58FB3536B619557 /* SDWebImageOperation.m */; }; 179413BD936A98D571223DDA39AB9786 /* UIActionSheet+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 42AC4C9E32B2DF0DF2366BB6D08859AE /* UIActionSheet+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17A4935F78766E41FC29D1D668146EF3 /* RACTupleSequence.h in Headers */ = {isa = PBXBuildFile; fileRef = 21F7FF6AAD9960A7E34FA700AF4A2F1D /* RACTupleSequence.h */; settings = {ATTRIBUTES = (Public, ); }; }; 17D77D58509A819B927EFD3454361B4D /* NSNotificationCenter+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 06E2166895FA1466EACF8506DAA383DB /* NSNotificationCenter+OCMAdditions.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 1830558A4D2D63C8E76BC3136D8213F9 /* UIImage+ExtendedCacheData.h in Headers */ = {isa = PBXBuildFile; fileRef = 909F75C790C08B1A24A71FF138EA5C7B /* UIImage+ExtendedCacheData.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1839437DDEA6EB16A5239F19484FDC2F /* NSIndexSet+RACSequenceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 14752B57BED25D3E2474FA3AFEB5ECD7 /* NSIndexSet+RACSequenceAdditions.m */; }; 18660FA595DBE133BB784E813A7122A8 /* SDImageHEICCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 588908A9A5F504D890248C42BDD5B35C /* SDImageHEICCoder.m */; }; 18AD90784D549657DF51BC8377DA3085 /* SDWebImageDownloaderResponseModifier.h in Headers */ = {isa = PBXBuildFile; fileRef = BAFC1AB1A830CD29F44C74C0B71EF450 /* SDWebImageDownloaderResponseModifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; 196AAABF9ECA51DE0971598C883B4834 /* TyphoonNibLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = B49E72E762657E3284FA9D7A026A49E6 /* TyphoonNibLoader.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 19AC17E22E91595F6E0D5FF22AF7E834 /* TyphoonCollaboratingAssembliesCollector.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D69D7A45A56F3986C52DF0088F96C35 /* TyphoonCollaboratingAssembliesCollector.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 19B35F7B7E03A4884DF2861528AA40E1 /* TyphoonTypeDescriptor.h in Headers */ = {isa = PBXBuildFile; fileRef = E87C4CECC849A38798AC17F0D2EDBDC7 /* TyphoonTypeDescriptor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1A51CF9668347C9AB7F0DA8A9D149399 /* RACSerialDisposable.m in Sources */ = {isa = PBXBuildFile; fileRef = 0BB3D4EE11AB02C35A56E26CD538EA8D /* RACSerialDisposable.m */; }; 1A5A36F12519F4D3B9A2502D3A733381 /* Typhoon-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 9BE05A31C373E76C2393361CF1E8AF9C /* Typhoon-dummy.m */; }; 1A8F4D7FCCFAA0A692DB10B31D3C4A70 /* UITableViewHeaderFooterView+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 744D46F0AC450056AAE46AA71A878857 /* UITableViewHeaderFooterView+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1B6CE67196EE181E6B56788EFC7E00D3 /* SDImageGIFCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0AE0533E46155704FC6E99D900ECFF97 /* SDImageGIFCoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1BC44E2FDD197D5210A23C9CCF1A906B /* SDWebImageCompat.m in Sources */ = {isa = PBXBuildFile; fileRef = 198733CC5474A54E6311C06F7EF78E54 /* SDWebImageCompat.m */; }; 1BE3F2879E492B7F9A5AEBA3E3766100 /* NSDictionary+CustomInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = 12EC94936DE4F7A5029D489A3E4FB32E /* NSDictionary+CustomInjection.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1C6A9BE75AF46B08D04DEBB8DA4D687A /* NSObject+RACSelectorSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 8386D6BB6CF3F2D21ED967CC288819B0 /* NSObject+RACSelectorSignal.m */; }; 1C8B70C74291A3076746C3B18781568E /* SDImageCachesManagerOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 91A1F2245A7DAF44FC87AADAFA9E9BA9 /* SDImageCachesManagerOperation.h */; settings = {ATTRIBUTES = (Private, ); }; }; 1CB52E450D48ACF5A2F1232D03755A1D /* TyphoonInjectionByCurrentRuntimeArguments.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A273691556F81E2F190244D0347ED31 /* TyphoonInjectionByCurrentRuntimeArguments.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 1CCD65F93AB302520C82949625A48CE7 /* TyphoonInjections.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F3AD4FDCF5DACA1727D2EDD1CDAA03F /* TyphoonInjections.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1CDFABA9B2A06A4238E706017BE1F38A /* RACScopedDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = 5A66CEA5D0C3EE038662C0D25CA04F50 /* RACScopedDisposable.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1E21419F413F6D4CA470254D9A0349BA /* TyphoonViewControllerNibResolver.h in Headers */ = {isa = PBXBuildFile; fileRef = 8732F0D1FA8AA62E703DE7973280BA55 /* TyphoonViewControllerNibResolver.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1E23DAF8BFF220AA548435F8987B5EC8 /* TyphoonAssemblyAdviser.h in Headers */ = {isa = PBXBuildFile; fileRef = 37E635EECAF9EE9AFD5F838C93C42334 /* TyphoonAssemblyAdviser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1E4A2A24D1FA36114E4A37633FC546D3 /* NSObject+DeallocNotification.m in Sources */ = {isa = PBXBuildFile; fileRef = 5C7C6F0CA41AC6FA5A0EAD49FADBE0DE /* NSObject+DeallocNotification.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0 -DOS_OBJECT_USE_OBJC=0"; }; }; 1F255700661347843AD0580B1AA7D6A5 /* RACScheduler+Subclass.h in Headers */ = {isa = PBXBuildFile; fileRef = 183FD7641AE552C24F25EC96C491FCBF /* RACScheduler+Subclass.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1F9CDA98F3994B1BE2B535BDF2D073E5 /* Pods-iOS-Network-Stack-Dive-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E1DD37617D3B108328AB936760E6BDD /* Pods-iOS-Network-Stack-Dive-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 1FD7A52353875571B01C3E7FD9845744 /* OCMConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A12D240C381406917C3EFB576E33710 /* OCMConstraint.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 20199A7A56B3DF111724477B175701D0 /* Pods-iOS-Network-Stack-DiveTests-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = E526E2D4A9BDEA51AAF7B2A5AC6A46FA /* Pods-iOS-Network-Stack-DiveTests-dummy.m */; }; 207791430D1E92DF86EA8E7CE685BE30 /* RACSignalSequence.m in Sources */ = {isa = PBXBuildFile; fileRef = 5354038C92F8485F19D1D15D3BF5FFFC /* RACSignalSequence.m */; }; 20D618EF3EA5E3BE96DA24D36E3CA9EF /* SDAsyncBlockOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 50B9C0A664812C3E14A3224A605C9402 /* SDAsyncBlockOperation.h */; settings = {ATTRIBUTES = (Private, ); }; }; 215534F06D3902AE9E7D62DC1718C568 /* TyphoonConfigPostProcessor+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B16EB1B90C8121FC99C6D634CE916AA /* TyphoonConfigPostProcessor+Internal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2226813C530D9A755E29C9F35308E33C /* TyphoonTestUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EA1F7903B377E9FFA5C2028618BA7439 /* TyphoonTestUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; 225F924255C41D53958FEB795EB51FDE /* RACmetamacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 950CD6BE6707ED534FF448D9CC40FA3D /* RACmetamacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; 228CDD92F991FA023D6F06B69327A028 /* OCMInvocationMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DB0A4F8CE90B57FB6935727ABCA7CE2 /* OCMInvocationMatcher.h */; settings = {ATTRIBUTES = (Project, ); }; }; 22C00E6FDBCBBEB33AE6CDC8DFA60571 /* TyphoonPrimitiveTypeConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = 620115CAB08E9FF9898971DB60F30BA6 /* TyphoonPrimitiveTypeConverter.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 22F72E872A783A8AB4670B5037787DD6 /* NSArray+RACSequenceAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 43154AF288B8222D79172AC422866E6C /* NSArray+RACSequenceAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 244415898475C361F53B52BC1A34407E /* RACKVOProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 856CE023A29559B385271D8014E85BC3 /* RACKVOProxy.m */; }; 2483B32E63F6D95EFD66313968EDE3E7 /* TyphoonInjectionByRuntimeArgument.h in Headers */ = {isa = PBXBuildFile; fileRef = 6C1ADE2AAE0ED64E1AC780F3A8C2B911 /* TyphoonInjectionByRuntimeArgument.h */; settings = {ATTRIBUTES = (Public, ); }; }; 24D5DB6DC75B9301C8A56C067549ED18 /* TyphoonLinkerCategoryBugFix.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B7043029F68EFD2C4174A964B0551C6 /* TyphoonLinkerCategoryBugFix.h */; settings = {ATTRIBUTES = (Public, ); }; }; 24E8E4ED0B5D988E3346E6638619F4E4 /* SDImageFrame.m in Sources */ = {isa = PBXBuildFile; fileRef = 9CD76B64F3F84AD0EA28EDA5BE8A26BE /* SDImageFrame.m */; }; 255702D1853E23E30B6847F6A26402F9 /* RACBlockTrampoline.m in Sources */ = {isa = PBXBuildFile; fileRef = E37027B951A73179B2F4337FF41422D7 /* RACBlockTrampoline.m */; }; 259D1784222B56F16D06111003ABD61C /* TyphoonCircularDependencyTerminator.m in Sources */ = {isa = PBXBuildFile; fileRef = 04CD8685467C1425C560A1E9C0AA43B7 /* TyphoonCircularDependencyTerminator.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 25C3A2BA3CEF4D94C5C34CD8FE3FFAE2 /* UIButton+RACCommandSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 6499637B2B14B146BF05E82D9F524DB0 /* UIButton+RACCommandSupport.m */; }; 2609AEF9B7C24A507DAE3AA17E8ECA21 /* RACSubscriptionScheduler.h in Headers */ = {isa = PBXBuildFile; fileRef = 2E0DEBA6FAF64625746D89A660E075B9 /* RACSubscriptionScheduler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 27DAE8D358BD60E2886684E5A056E752 /* TyphoonDefinition+Config.h in Headers */ = {isa = PBXBuildFile; fileRef = 48F2519B307B02B43FC5B68399AF554F /* TyphoonDefinition+Config.h */; settings = {ATTRIBUTES = (Public, ); }; }; 28011D6203A46798D789585BB2F5B7F9 /* RACImmediateScheduler.h in Headers */ = {isa = PBXBuildFile; fileRef = 1443B87E860FFFC4733D9B16073E19F9 /* RACImmediateScheduler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 288D796F3F7B9F42690E24A3B1018B2C /* SDImageIOAnimatedCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 15DBB333129764191B36CFF037F62681 /* SDImageIOAnimatedCoder.m */; }; 28C71D0DD2723BBE5D5C74A64FE185A0 /* RACReturnSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = BFD56F486BA750FFE7B13856AC85FEA2 /* RACReturnSignal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 29489009B707CF4D904BEF00214A77C5 /* RACScheduler+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 0B2C8AD692D98D47C5658A9FE6A6DE4F /* RACScheduler+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 29939A199EE4BAE8976AEC88E59F2ABB /* CoreFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6D6E31E7C7B4D803BA05555FE0FF607 /* CoreFoundation.framework */; }; 299D0A26A874ADE845CBB1654CFB48EE /* TyphoonBlockDefinition+InstanceBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 955DB0048C92F2F02195AF9A7C000F04 /* TyphoonBlockDefinition+InstanceBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 29F7F0E98FD26A96364DBACD7D5F237A /* SDWebImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 5D1458925F5D672BF44A88E619165414 /* SDWebImageDownloader.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2A71217971D20B8AB2B3E50CC364417E /* TyphoonViewHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 02DBA7718F60FA5F5208D3C92DD2FAE1 /* TyphoonViewHelpers.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2A7799AF29FD31F1C0EAC4857E7511F2 /* TyphoonCallStack.h in Headers */ = {isa = PBXBuildFile; fileRef = 8F0A29B88BD1DA370225345A593E408F /* TyphoonCallStack.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2AB2A962A952CBC6F38DE2938E055B0E /* OCMMacroState.h in Headers */ = {isa = PBXBuildFile; fileRef = 7E53BFD7942CD993EE50F7FD12C87B2C /* OCMMacroState.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2AD6EE04D5EC7568C100BEE596D4C545 /* NSArray+TyphoonManualEnumeration.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CB4F2D27F00A3C400E970AB3C7AB6BC /* NSArray+TyphoonManualEnumeration.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 2B05197D9780DA9C2A6C74663FDC1BE8 /* UIButton+RACCommandSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = B05C75D2995DD598CD05DC64D113A11E /* UIButton+RACCommandSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2B0D8ABFCA7A2A99F4E3A2F35820DAAA /* TyphoonDefinitionNamespace.m in Sources */ = {isa = PBXBuildFile; fileRef = A5A8D22D51747CC57261A39513F63FBF /* TyphoonDefinitionNamespace.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 2B7DFF21453E38A19B45F8E11EF278E3 /* OCMLocation.h in Headers */ = {isa = PBXBuildFile; fileRef = FCD6A66B2012CDBD25E31AE588E07483 /* OCMLocation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2BEDD35426B348EC616C6A0939C6BD83 /* RACAnnotations.h in Headers */ = {isa = PBXBuildFile; fileRef = E49D958DD1C460DCA691F17881DE66D9 /* RACAnnotations.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2CFFDBFE764BA3B8B0B878BC3A0E7083 /* TyphoonStartup.m in Sources */ = {isa = PBXBuildFile; fileRef = DB337B4965F7DD6CACD10EBA48209055 /* TyphoonStartup.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 2D12D093F3425B423613BB39DC40A72A /* OCMFunctions.m in Sources */ = {isa = PBXBuildFile; fileRef = 9C26E7915BAEE3BDE02BCD9D2D389B9A /* OCMFunctions.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 2D873076AFF986FB88A07B056E6E1B45 /* UISwitch+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 9AE77ABB8D49E71EE7F0CFAFEC9710A4 /* UISwitch+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2DB0021F9583F06D433E0EA01CC4BD65 /* YYClassInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 33C36535BCF88C0F8CE4BEE165483F16 /* YYClassInfo.m */; }; 2DD65DE2BC1D3B6536ECC06F2BB1CB26 /* RACEmptySignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 2604998C21DB77EB38BB124802109F52 /* RACEmptySignal.m */; }; 2DDD48230ED9E8068C7E439D79B99A8E /* SDInternalMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 000F69D9699B233904DC78740C74688D /* SDInternalMacros.h */; settings = {ATTRIBUTES = (Private, ); }; }; 2EC9A7EDF03D4C66CB4DA9F65E1AC35B /* OCMInvocationStub.m in Sources */ = {isa = PBXBuildFile; fileRef = DF3E60E69433B185D69240ECF94CFCFB /* OCMInvocationStub.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 2EFF1EFA2FF0D2F1AE44A9DDBB932425 /* UIScrollView+EmptyDataSet.h in Headers */ = {isa = PBXBuildFile; fileRef = 1F40215065345DB127D2130E4F8AA842 /* UIScrollView+EmptyDataSet.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2F6D9BEA582A2DBB70A6C3B2FC2DB91E /* SDWebImageDownloaderResponseModifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 40F0A65B2C96B6E67B7417305F598D6D /* SDWebImageDownloaderResponseModifier.m */; }; 2FA3D210CCCBA41BC36F0D716F173F19 /* NSInvocation+TCFUnwrapValues.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C07DF0C9AD7EAEC76E6F3D297E58F66 /* NSInvocation+TCFUnwrapValues.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2FDCF9468E03C9B56AB0F7740669B4F5 /* NSValue+TCFUnwrapValues.m in Sources */ = {isa = PBXBuildFile; fileRef = DE4C0740ECC01D6ED27D9ABD7EA5172B /* NSValue+TCFUnwrapValues.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 2FEAE6F6465F609BC48C5F020306A6C5 /* Collections+CustomInjection.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CCB0069A6BAC8CD752488DF7B7D42B7 /* Collections+CustomInjection.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 3025FD4C72722EDAA7B5778B22735B62 /* OCMStubRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C3F45C3BA62C5E06E78277D0D81E73D /* OCMStubRecorder.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 30FE660831E11D2EA5791BB9D84A3978 /* UITableViewCell+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = FE568D9819827B0D7AC039F020E84BB1 /* UITableViewCell+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 317B814C899E76C4C794A92F42748AB5 /* NSObject+TyphoonIntrospectionUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = EAFD5F2E394EE2C4144D87785849CD68 /* NSObject+TyphoonIntrospectionUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3187FF0C251D1B78BE87F64F6F6E944A /* SDWebImageTransition.m in Sources */ = {isa = PBXBuildFile; fileRef = 7333039A34CFB7AC3CD117DC76BF5999 /* SDWebImageTransition.m */; }; 31C05D64D42CA917CEF5585183DD47B3 /* OCMInvocationExpectation.h in Headers */ = {isa = PBXBuildFile; fileRef = D400046FCD67C3ECE87A4FD7570DF9B7 /* OCMInvocationExpectation.h */; settings = {ATTRIBUTES = (Project, ); }; }; 31DC2EC78AD1F8241AE6051EF9E73B0A /* SDWebImageDefine.m in Sources */ = {isa = PBXBuildFile; fileRef = F2C354A6FCF564B6B35C09D98D9B276E /* SDWebImageDefine.m */; }; 31E03594824B9C1868700CB719A49612 /* NSUserDefaults+RACSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 41BDCD9D2400DA765DCB894C8509679B /* NSUserDefaults+RACSupport.m */; }; 320DE42AF3CFE11FF785FEB1A7E6547B /* SDImageFramePool.m in Sources */ = {isa = PBXBuildFile; fileRef = C5EE4FBC26F92ABB4464D8BED481D33A /* SDImageFramePool.m */; }; 32ACEDCEBE0507A82D6323114A1C74F1 /* UIImageView+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = E1DC9A972C497BFFC7B688A24BA28E98 /* UIImageView+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 32F2B91621A2F8F9AD7C8E2B224D73F6 /* SDWebImageDownloaderDecryptor.m in Sources */ = {isa = PBXBuildFile; fileRef = 6390D1A8353BED0692FDB19AF76851C0 /* SDWebImageDownloaderDecryptor.m */; }; 33BE7CF4354A6753F1CAB84BEC9F0575 /* NSMethodSignature+TCFUnwrapValues.m in Sources */ = {isa = PBXBuildFile; fileRef = 1C982E20E08BC60740455993A35E8A3F /* NSMethodSignature+TCFUnwrapValues.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 33D3587AF629B2FA21554DA002D6ACB8 /* SDImageCachesManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 1343632E0E45C40E0DBA77566514A62A /* SDImageCachesManager.m */; }; 3443028D46C9F3EF2BD8A7A22B4DC101 /* TyphoonStackElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D40DD7E6EAE2E8222553A0E28A69834 /* TyphoonStackElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34519026CD19276CF7FA8A6921FBD3A7 /* OCMFunctions.h in Headers */ = {isa = PBXBuildFile; fileRef = 0EB0DF190C36C4090D74F64BB54053A6 /* OCMFunctions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34AAA32786D16B879ED317280AD7C550 /* RACStream+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 8090506D09D6280408FEFDC474C367CF /* RACStream+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 34B28D4F0168194B6EFAC0520EB7A7F4 /* NSImage+Compatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = EE05E240495D85D96BA4CB6CB3310F63 /* NSImage+Compatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34C31624E93FB93E37E4C25D7E252A6A /* OCMVerifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 3C12837A69C57039BE360EAF15FE6C76 /* OCMVerifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; 34F0AC42EC0E17A78D8B472363FA732D /* NSSet+RACSequenceAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = E369629E03DEAACC1711DB17684549A9 /* NSSet+RACSequenceAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 35F8F16617EA4D58688C0633DF1ED887 /* UISwitch+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 83901A74DF9DB0A0733D4591727B3770 /* UISwitch+RACSignalSupport.m */; }; 3650C86E60D03E4678315B00B3910572 /* UIImagePickerController+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 2CD5CB6BE4742004BAEA7654CE3C6AF7 /* UIImagePickerController+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 36F4B09E7C71DCC5CEC6057814033C37 /* UIView+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = B0783913DE452E225655A280A31FEFA8 /* UIView+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3777CD89D444CBBB48AE323B303F3FC7 /* ImageIO.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E215B27470E9F7E028D285C07B15B9B /* ImageIO.framework */; }; 37B890ABDC7DD441E6AA662325D412E6 /* MASConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = 9630797851476E7C030A663FB01C053D /* MASConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; 38007D8B2D7B055D75C486183F0EADF3 /* RACScopedDisposable.m in Sources */ = {isa = PBXBuildFile; fileRef = A64869131A94BAF78EB608FAED187F90 /* RACScopedDisposable.m */; }; 385FEBB3BFA25B1AA8ECDFF703FD997A /* TyphoonComponentsPool.h in Headers */ = {isa = PBXBuildFile; fileRef = 65677A776ECF0D5458AAFDDA49ADDFC4 /* TyphoonComponentsPool.h */; settings = {ATTRIBUTES = (Public, ); }; }; 38938E604A7D708E6378A44063EF3512 /* UIImageView+WebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = A09A92A8038E8620B3B2A61D8A929180 /* UIImageView+WebCache.m */; }; 392AC16CCBF2A50096F0B3303E13ADA7 /* NSNotificationCenter+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = F89876C64AB0B6C50E8EE3A5E905CBB2 /* NSNotificationCenter+OCMAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 392E6367FB1A10EEA969ED00E460E4DD /* NSFileHandle+RACSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 795FD8B2FD7ED943A340C32633ED18F7 /* NSFileHandle+RACSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3A1AD84C0DC3C256418CC46739024E96 /* SDmetamacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 54AA8A2F8E3322A66F3449823D3B76B5 /* SDmetamacros.h */; settings = {ATTRIBUTES = (Private, ); }; }; 3ACE99697BE48783D60907A57A5F2FF1 /* TyphoonStoryboardDefinitionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D5383F6C03BB8925217003BD9A3A82C /* TyphoonStoryboardDefinitionContext.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 3AECFF92CB2DDFE798511330B5E8D704 /* OCMExpectationRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 437B0AD6B69A2013C65A324A3CBBF3CA /* OCMExpectationRecorder.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 3B46F3921BE0E2CCB23E716DD6D41D6D /* OCMArgAction.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DA3CE4677195B9C5B8F50C6E3B91D81 /* OCMArgAction.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 3B55DB273C7CD31AACE379298843C0A1 /* UIView+TyphoonDefinitionKey.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D713D070421A1E803471AA59CAE1B25 /* UIView+TyphoonDefinitionKey.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3B959E6505C82CA0FF804E07CA51492A /* NSEnumerator+RACSequenceAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 74E20E5BB34220E9282F27E20A9A567E /* NSEnumerator+RACSequenceAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3BFA92648AC1313CFD040750ACE5D0C1 /* RACSequence.m in Sources */ = {isa = PBXBuildFile; fileRef = 793AD3A27E2A7A7D6570D1D0B8E748CF /* RACSequence.m */; }; 3C0A1F9E0F5FDD72150F8A21A4672935 /* OCMQuantifier.m in Sources */ = {isa = PBXBuildFile; fileRef = E59E49F550CAE1C1C8665F6CCF893735 /* OCMQuantifier.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 3C2E1F3AF1E008F5610EDBB2258CA617 /* RACSignal+Operations.m in Sources */ = {isa = PBXBuildFile; fileRef = 55EB526211489C964AE450F84E636291 /* RACSignal+Operations.m */; }; 3C76848E9AA1EE9FB3CA1BAD6A217790 /* RACSubscriber.h in Headers */ = {isa = PBXBuildFile; fileRef = 143606750438523F029FB6DA44890C07 /* RACSubscriber.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C7815EEC599DD7D42FDEF19B2FF1563 /* SDWebImageOptionsProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 933EF5CE0D7C9A91603A2F91BDFD0871 /* SDWebImageOptionsProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C7EAECB8C573E714C818BA04EB33773 /* UIImage+MultiFormat.h in Headers */ = {isa = PBXBuildFile; fileRef = CEB064A038D098A7FFF817FFA5B9F340 /* UIImage+MultiFormat.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3C8F2F868D0C361CAF43E53CDB8EB631 /* SDWebImageCacheSerializer.h in Headers */ = {isa = PBXBuildFile; fileRef = D4142DA4BEFA74E0A9968C239127FF24 /* SDWebImageCacheSerializer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3CB27E5F83DADA61D461099B00ED2895 /* OCMock.h in Headers */ = {isa = PBXBuildFile; fileRef = 4987ACD1FED6CF90E9AE135E75E5AE57 /* OCMock.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3D0BBFEC1921CE71BC240DC18D8BE540 /* SDImageTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 073BF9A27C7F924FC6A5CF2E935DDDA8 /* SDImageTransformer.m */; }; 3D81A472030665B9BA7F4BF2E11A4541 /* TyphoonCollaboratingAssembliesCollector.h in Headers */ = {isa = PBXBuildFile; fileRef = F655091C80A455BB5F06459C5CC0EC71 /* TyphoonCollaboratingAssembliesCollector.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3D98D6C75AE8D46FEF6F5414F50815B2 /* OCMObjectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = A840AE48AD863CF06B48FE55C745EA9D /* OCMObjectReturnValueProvider.h */; settings = {ATTRIBUTES = (Project, ); }; }; 3DEB0926F884FA92604800641EF8BAC6 /* NSMethodSignature+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DEE925C4BC3366755D6B62640CCCFCAE /* NSMethodSignature+OCMAdditions.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 3E1D9FD4E15EAD00141A7A1342A0CC10 /* RACDynamicSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = 1F5DE94BB623F6C7A33F0A746E8F58DB /* RACDynamicSignal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 3E983180E84D6AC282EF25CDC8368D41 /* OCMIndirectReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 2EEE06C505E1457D30A4ACDA3820D377 /* OCMIndirectReturnValueProvider.h */; settings = {ATTRIBUTES = (Project, ); }; }; 3F9C3275F786E754C812482941E96F4A /* RACEXTRuntimeExtensions.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F26AA73AA98E00F8C916633F02D50F8 /* RACEXTRuntimeExtensions.h */; settings = {ATTRIBUTES = (Private, ); }; }; 3FF9873BBEDD1C8AA6EC43EBCB23D060 /* NSData+RACSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 0CEAF4D7D00B3A496611716400E24A3C /* NSData+RACSupport.m */; }; 40613D5600FE76DC9F8F377846577314 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; 416DA8B2997381F954DBA6E6A53DA4A2 /* NSData+ImageContentType.m in Sources */ = {isa = PBXBuildFile; fileRef = ECA70F67E1A45D6DB171A53CF98C8090 /* NSData+ImageContentType.m */; }; 422523173C30F2F9695A6635431DAF64 /* TyphooniOS.h in Headers */ = {isa = PBXBuildFile; fileRef = BD5D5509B774B3EA20B4EBDBD246DD8C /* TyphooniOS.h */; settings = {ATTRIBUTES = (Public, ); }; }; 425C9EA28FBEB7F7FC09A3F4A88C5955 /* SDWebImageError.m in Sources */ = {isa = PBXBuildFile; fileRef = ABBE463A361CCED6D1E02450518F512E /* SDWebImageError.m */; }; 42735B679585CBE859585C298BB91FC6 /* UIControl+RACSignalSupportPrivate.m in Sources */ = {isa = PBXBuildFile; fileRef = 93894FEF634076038AE57F0DA89A230A /* UIControl+RACSignalSupportPrivate.m */; }; 42DC6F6F01A9526A6A9687A6279E928B /* TyphoonCollaboratingAssemblyPropertyEnumerator.m in Sources */ = {isa = PBXBuildFile; fileRef = 62B6BED9AA97ED0F595CFB0D93C898E2 /* TyphoonCollaboratingAssemblyPropertyEnumerator.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 436764D67026FF2C796BC826CEC803DD /* OCMExceptionReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = E8561AC4257A4B8D0B0569EEEA2E4555 /* OCMExceptionReturnValueProvider.h */; settings = {ATTRIBUTES = (Project, ); }; }; 43BC3B34407AEA67381BAD180BCA1156 /* TyphoonPatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 2CD41B1FA71FB0AE97EB624502ACF0A1 /* TyphoonPatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; 445C3272BC1602F3CF1B140BC2A0B39C /* UIBarButtonItem+RACCommandSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 9DA14F675C32DB069C21840DFF0D5B75 /* UIBarButtonItem+RACCommandSupport.m */; }; 44AEABDD5FE11DCE9EE1F00ECBC340F5 /* TyphoonStoryboardDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D91285C522F06C05B537D395E259F59 /* TyphoonStoryboardDefinition.h */; settings = {ATTRIBUTES = (Public, ); }; }; 44CD842019B1CEA681F820F37A30B7C4 /* SDImageFramePool.h in Headers */ = {isa = PBXBuildFile; fileRef = 45FA653E23538B63DB444A6ECA2E3190 /* SDImageFramePool.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4571A0EA37DC84F39E3830D38A1531AB /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C5460C93594FE49E0DF49CC9F802F1F3 /* UIKit.framework */; }; 45A2252FE9A80B5B59B021395E19EE0C /* RACTuple.m in Sources */ = {isa = PBXBuildFile; fileRef = 1CB4CE5D415687F6A3499F360794DA04 /* RACTuple.m */; }; 45D4E85C7031F33EA83C956C73136DBF /* OCMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = A9267DE7A89C784B346A9CA842751576 /* OCMockObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; 45F861348548E0CD9D2C0781E04B3CEA /* RACPassthroughSubscriber.h in Headers */ = {isa = PBXBuildFile; fileRef = CB8729BC7993BA548B4D892BE63AEE83 /* RACPassthroughSubscriber.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4688743B7B845309486559EB7BD5D147 /* SDWebImageCompat.h in Headers */ = {isa = PBXBuildFile; fileRef = 24A117E496A0E15BE008AF6C24618D1F /* SDWebImageCompat.h */; settings = {ATTRIBUTES = (Public, ); }; }; 46FE7652F3F63388BF97D9C8B7015A6B /* RACUnarySequence.h in Headers */ = {isa = PBXBuildFile; fileRef = FB49799876CD2FF91D515D753BBB6D1F /* RACUnarySequence.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4775DBD4961BF6BA60BA6ADECCCF03DA /* OCMBoxedReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = ED4B3532992A08C1DE0094AFAED64A03 /* OCMBoxedReturnValueProvider.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 47987D5D284981A7B1FDA8EDAD4EA80E /* TyphoonInjectionByFactoryReference.m in Sources */ = {isa = PBXBuildFile; fileRef = DA6A12A58C1C3043FB0D92D39EF14EB7 /* TyphoonInjectionByFactoryReference.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 47BB76E62236FE1305B71F3D4484E52B /* TyphoonBlockDefinitionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 7C9741B2966A4017CFF5F1396954F520 /* TyphoonBlockDefinitionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 47E27473C6C3DAD243E76BBB9FAAC38A /* YYModel.h in Headers */ = {isa = PBXBuildFile; fileRef = 325FC278EFE1EDE9A708FE21611C503E /* YYModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4851743937896F7DBEC0BCC7A878682C /* NSObject+RACDeallocating.h in Headers */ = {isa = PBXBuildFile; fileRef = F383EDD80DA3184C9A032B89FD763BB7 /* NSObject+RACDeallocating.h */; settings = {ATTRIBUTES = (Public, ); }; }; 48916DE9521F627589300512ECC2D4A5 /* NSButton+WebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 0F07E5AA6AC318EED47D4865477C9372 /* NSButton+WebCache.m */; }; 4958D2D0D6DC37810FAB1105D10E90D3 /* UITextField+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 17092355C96C3E633D28031E208BC50C /* UITextField+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 49C540A33BC7DB59595582A72D92E8FF /* TyphoonStoryboardResolver.h in Headers */ = {isa = PBXBuildFile; fileRef = BC0D43AD044D143B02D4E1D5EA86BE86 /* TyphoonStoryboardResolver.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4B2C2AE16AE3DDA7417AFCF7952588F1 /* SDImageAssetManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 6ED217D4C98D62892AD92759A98D2928 /* SDImageAssetManager.h */; settings = {ATTRIBUTES = (Private, ); }; }; 4BCA5F8C6F0C73C76E656BA0E3EA858F /* RACTargetQueueScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = 0B91A0326C0D75CA3ED540C456F99FA7 /* RACTargetQueueScheduler.m */; }; 4BE76E020E0B05ADDB89CCA00CFB9933 /* NSObject+PropertyInjection.m in Sources */ = {isa = PBXBuildFile; fileRef = 69139A572FFAF2E425701DCD352635E3 /* NSObject+PropertyInjection.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 4C4C5CBDDABB5F5850DFE69DB34F6AD7 /* TyphoonInjectionByReference.m in Sources */ = {isa = PBXBuildFile; fileRef = DC81E927C43FEC142A408A3850366DA7 /* TyphoonInjectionByReference.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 4D2C79AB2D24CFEC864F08D913CE7692 /* SDImageCodersManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 85BADAC4F3DCD2CDEA7EE692773C47A3 /* SDImageCodersManager.m */; }; 4E104BAD7705D8D5D9F45D41535914EE /* OCMRealObjectForwarder.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E440830ABB9D3025B26A80937A2EFE9 /* OCMRealObjectForwarder.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 4E64E3ACB0070325287252B917BA9EF3 /* NSString+RACSequenceAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 06DAA5ED399AAB042406902A676EED50 /* NSString+RACSequenceAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4ED05DB3E43FF6AE1FA22130B2B50F05 /* UIImage+MemoryCacheCost.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D3D8BCFF92388848C3FD597F1895318 /* UIImage+MemoryCacheCost.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4ED8D4C89D65CD9BAFF98BA1EA8543BE /* RACPassthroughSubscriber.m in Sources */ = {isa = PBXBuildFile; fileRef = 472F840FC38C62F9E93AD1BEE1A70F95 /* RACPassthroughSubscriber.m */; }; 4F2506F20F6E778B71CC643C0C76F805 /* RACReplaySubject.m in Sources */ = {isa = PBXBuildFile; fileRef = EA318A2FA8725340866FC3083915FCFD /* RACReplaySubject.m */; }; 4F35842E1298A391EC638FE90727CC31 /* TyphoonInjectionByComponentFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 050CAE5EF6AFDE7FE32CEE7E030CDD0E /* TyphoonInjectionByComponentFactory.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 4F599FFE9C09DD9532BD46E758387116 /* TyphoonSwizzlerDefaultImpl.m in Sources */ = {isa = PBXBuildFile; fileRef = 95DC39645A666C2B0AF83523FAD30758 /* TyphoonSwizzlerDefaultImpl.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 4F650AA71665246CC359F05EA7B041D0 /* TyphoonBlockDefinition+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 1498864CCF5B387AE86A3D8C74065926 /* TyphoonBlockDefinition+Internal.h */; settings = {ATTRIBUTES = (Public, ); }; }; 4FE0E785F97144E411FCE8F30C51F7EA /* RACImmediateScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = F0C0E50FA94D388C9007312869F096D5 /* RACImmediateScheduler.m */; }; 5005432EAECE0BBCAE0487FB541489F7 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; 50559EFD7F12C78B1448D757DCC81218 /* RACGroupedSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 38B6C5CA78D03B16417EF54377D96D24 /* RACGroupedSignal.m */; }; 5111A0A0934551CD2B9DDB1A1CA79FA7 /* SDAnimatedImageRep.m in Sources */ = {isa = PBXBuildFile; fileRef = 0571A2C7BE7E0945F4212CD552F88BE1 /* SDAnimatedImageRep.m */; }; 5137D94F9CA1FAE507602788BC4988A2 /* OCProtocolMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 7F84B399A305F1F7EAFB02599E56A1B1 /* OCProtocolMockObject.h */; settings = {ATTRIBUTES = (Project, ); }; }; 5146309CBB46C9B12CC1648AF02C3A20 /* TyphoonParentReferenceHydratingPostProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 18FB517C4077E6929DB734C793782E40 /* TyphoonParentReferenceHydratingPostProcessor.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 521D285E66EFF69011341B410DE59767 /* TyphoonPropertyStyleConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 720CFEBFB23833C0091EFD8767731EA7 /* TyphoonPropertyStyleConfiguration.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 526485EF6D2B62B24DB59122FB94BD42 /* SDDeviceHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 7D1BD5E2CF0B45A47FC6BC88583E5B26 /* SDDeviceHelper.m */; }; 52B6E27260DC936E3AAE8F2F46CFB416 /* RACEXTKeyPathCoding.h in Headers */ = {isa = PBXBuildFile; fileRef = F4690548BF3F6C5A71CDC7BF7187EC20 /* RACEXTKeyPathCoding.h */; settings = {ATTRIBUTES = (Public, ); }; }; 52D27AEA262A912CA0A4F72D1BD32586 /* OCMPassByRefSetter.m in Sources */ = {isa = PBXBuildFile; fileRef = C14BDA045B4B1D88E3900EFD02FFA862 /* OCMPassByRefSetter.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 5308E660E723C11E7691D311FD59C459 /* SDDisplayLink.m in Sources */ = {isa = PBXBuildFile; fileRef = FB0CBD82662E0CBDB55A48F245646888 /* SDDisplayLink.m */; }; 53433003112C4FE271EC985803862B61 /* SDWebImageCacheKeyFilter.h in Headers */ = {isa = PBXBuildFile; fileRef = F5AEF10212D323726736313347CAA30B /* SDWebImageCacheKeyFilter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 53EF718B00DF8A17F05A50B6E478E799 /* TyphoonInjectionByType.h in Headers */ = {isa = PBXBuildFile; fileRef = 2C379332A0D51473ADCF63C80BF3B4C8 /* TyphoonInjectionByType.h */; settings = {ATTRIBUTES = (Public, ); }; }; 540484CA48AE6E538865EE0882E9B886 /* UITextView+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 02580255476480314DC90D373EF87D94 /* UITextView+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 543A777E19623D18C1389BC6189F21FC /* TyphoonAutoInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = D16AFD377C51E109866560DEE9CBC39E /* TyphoonAutoInjection.h */; settings = {ATTRIBUTES = (Public, ); }; }; 54C6DD71F9BA9CA676096215DDE74996 /* UIGestureRecognizer+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E295017E5EF894DD2C62B5422434280 /* UIGestureRecognizer+RACSignalSupport.m */; }; 550E329852F1C92D6E70854088B4D5D8 /* TyphoonInjectionContext.m in Sources */ = {isa = PBXBuildFile; fileRef = 227C536B9341DFDDAB8A93622D7BAB18 /* TyphoonInjectionContext.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 550F4E490798ADB3B4B2C706CEEA4A8C /* TyphoonConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 30935A228E64CE3387EE71949306F225 /* TyphoonConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5558D029B8BABF2823BAB0A4A689C100 /* TyphoonViewControllerFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 71C1C00FFE25AA899E9B77567B46E8CA /* TyphoonViewControllerFactory.h */; settings = {ATTRIBUTES = (Public, ); }; }; 55F7C7F055A18044497F8C88CAE34118 /* SDImageCachesManagerOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 273504B50C9E20A280FF38B369EECE87 /* SDImageCachesManagerOperation.m */; }; 561D1F03868EC8BA84A2B6A22031CB49 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; 56977A714C1767541B64A6D00C7BA62D /* UIResponder+TyphoonOutletTransfer.m in Sources */ = {isa = PBXBuildFile; fileRef = BE3BDF1044166B0EEE411717FD217E42 /* UIResponder+TyphoonOutletTransfer.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 56B73AB5484B65F2C078198B0775AB1D /* TyphoonTypeConversionUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 3EF8BF2E5B7814DF672FB319D2CB899A /* TyphoonTypeConversionUtils.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 56E800EB3B2BE8AE0BA45A30974D7920 /* Masonry-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 7BDD9A3C6FE6300A4953E8AC4C585381 /* Masonry-dummy.m */; }; 56FE435145D35628CB94B8DBD67E81F4 /* RACStream.h in Headers */ = {isa = PBXBuildFile; fileRef = 1DD1EDA8854FDDBB1314904C848EEADC /* RACStream.h */; settings = {ATTRIBUTES = (Public, ); }; }; 571C90CBC5777D2194962B65F994FE72 /* TyphoonPrimitiveTypeConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B69333B75E26F15AA2E42D06EF62B15 /* TyphoonPrimitiveTypeConverter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5722A8075D663DC1E070027A450C21AA /* TyphoonCollaboratingAssemblyProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 6027F1EC9BC5F3AB67DFFEC9E6DD6BEB /* TyphoonCollaboratingAssemblyProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5806683E752BD6A1B02EE96BE0741385 /* TyphoonAssemblySelectorAdviser.h in Headers */ = {isa = PBXBuildFile; fileRef = 772D4121E01F86089D23DBEBD83FC5AD /* TyphoonAssemblySelectorAdviser.h */; settings = {ATTRIBUTES = (Public, ); }; }; 585920A1A37B315FCA853A232773CC12 /* RACStringSequence.m in Sources */ = {isa = PBXBuildFile; fileRef = 4B2FCD3B157174CAD7F173B5F1FF0DA1 /* RACStringSequence.m */; }; 5884BE5A453A9E0417728E3FB8063EDD /* GCDAsyncSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 2735EF71996F30815132D35A2A30E845 /* GCDAsyncSocket.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5889FA7EB7333745C27C7FACAA9FBB87 /* UIControl+RACSignalSupportPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D76E7370C1E6F4EE5AE4D70E5C4D2C2 /* UIControl+RACSignalSupportPrivate.h */; settings = {ATTRIBUTES = (Private, ); }; }; 58E512BD916CDD9631428F30A623D370 /* RACTargetQueueScheduler.h in Headers */ = {isa = PBXBuildFile; fileRef = BDC02EE21DEF11D396F51D141C543D5C /* RACTargetQueueScheduler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 58F7CE37BB4CB3BE806B68A502E6E1A7 /* SDWeakProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 6176156DAB1442AF8B5FF1B18364793C /* SDWeakProxy.h */; settings = {ATTRIBUTES = (Private, ); }; }; 596180E0EC9F46D12BA840DC4AA62659 /* UIImage+MemoryCacheCost.m in Sources */ = {isa = PBXBuildFile; fileRef = 46B265A7E81C9ECE01048E340D45E9A3 /* UIImage+MemoryCacheCost.m */; }; 596AEAE7A03137CB24B6D05E3B4E47B7 /* RACIndexSetSequence.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D9038CEC928B9AF5CFA2FD0B75B9FEF /* RACIndexSetSequence.h */; settings = {ATTRIBUTES = (Public, ); }; }; 597E390C0BBB75B8045B651C487C2034 /* SDImageAWebPCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 15C365C1912A0FB488CECEF9950319ED /* SDImageAWebPCoder.m */; }; 59DD3A20CB015329E4F181DE7B1A7373 /* TyphoonSwizzlerDefaultImpl.h in Headers */ = {isa = PBXBuildFile; fileRef = CE6F1B69B9CDD8F4C2903BEDC2660DC3 /* TyphoonSwizzlerDefaultImpl.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5A592D4D0B89373E214B385BF7D4076D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; 5A6AFAF7515C84B07422A61E94280EB6 /* RACSignalSequence.h in Headers */ = {isa = PBXBuildFile; fileRef = 7D81778D9E6487677837D58B13894E94 /* RACSignalSequence.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5B08596E856E4CC2F34A8A2372F9F764 /* NSArray+MASAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = A4C87452D870EC6849B6A78C6E8FC492 /* NSArray+MASAdditions.m */; }; 5B95C0042EA30E36A37E4F686F05DE96 /* OCMVerifier.m in Sources */ = {isa = PBXBuildFile; fileRef = 83104DD312D9864E2BAA39D25537ADAA /* OCMVerifier.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 5C7199812C951AD5E976A6AA29A30BD5 /* RACReplaySubject.h in Headers */ = {isa = PBXBuildFile; fileRef = 141EB3C3A6A1510AA72DD2C9ACD6894C /* RACReplaySubject.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5C8279C226EB028B044C5A0F4AC5A91A /* SDAssociatedObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 19EA01E604C674E56BA259D0C3794651 /* SDAssociatedObject.h */; settings = {ATTRIBUTES = (Private, ); }; }; 5CFCD2D66F81B7F1AE4D8E82A51AFDCF /* NSMethodSignature+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = AACE72B147E103C5ABF8DD49C677152C /* NSMethodSignature+OCMAdditions.h */; settings = {ATTRIBUTES = (Project, ); }; }; 5D3BF2C909D103968F9FCBC99918728D /* TyphoonTypeDescriptor.m in Sources */ = {isa = PBXBuildFile; fileRef = C7F186EA53F19BDDCBCD2E2E2279D348 /* TyphoonTypeDescriptor.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0 -DOS_OBJECT_USE_OBJC=0"; }; }; 5DCBA14510E091D6A1CE499B08B794B5 /* UIImage+Metadata.h in Headers */ = {isa = PBXBuildFile; fileRef = DAF7C7F3C941B652A3015DFEA15DB259 /* UIImage+Metadata.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5E10328A83E05D0015D7459FAAEF121D /* SDGraphicsImageRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = C49042227AF27E657FDC7BA3CA8A537A /* SDGraphicsImageRenderer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5F45735DF355530CC955066D3C007E19 /* MASViewConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = D35CCCA0DFE0CE1EBDA590032372C0AB /* MASViewConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; 5F5426608460A8B17C1AE5C2351BAA19 /* UIScrollView+EmptyDataSet.m in Sources */ = {isa = PBXBuildFile; fileRef = 91DE4AD4922A1500C3D8808438EBC84A /* UIScrollView+EmptyDataSet.m */; }; 5F60001126438149B722B4CB58B32CC5 /* TyphoonInjectedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = F42860013A956ABF313EE9C9D084F191 /* TyphoonInjectedObject.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 5F8F021870002E9A2776909ACB965E3E /* NSString+RACSequenceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2D163BF4979C587AB2708ADA67ABB425 /* NSString+RACSequenceAdditions.m */; }; 61507E402F1F7C58BF119995A0479A22 /* NSArray+MASShorthandAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 041C0D0F13831619F5479D99056A302A /* NSArray+MASShorthandAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 616A8338C42FB01748DF1BDDA944858D /* UIView+WebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 50D1100A5FC66E513CDCEFED2FEC8138 /* UIView+WebCache.m */; }; 61B4CC930FD22979260025929C94D3F5 /* OCMock-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = A2ABDBBFB28FCCED57D0D680E16EBA76 /* OCMock-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6215CAAC4B72DECED49E706D88C3B66D /* RACDelegateProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 67EB4C9AA76FDBC2CD3FFBA7EE37A27C /* RACDelegateProxy.m */; }; 627357C7FD3404F2ABFA952B05FECE95 /* TyphoonAbstractInjection.m in Sources */ = {isa = PBXBuildFile; fileRef = 9EB5CC7B3930FE88F26ECB6C25761E21 /* TyphoonAbstractInjection.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 62C99100FB8B0AB574F6CA3B6EE47C61 /* TyphoonInjectionByFactoryReference.h in Headers */ = {isa = PBXBuildFile; fileRef = 891C96D521BE0268A6C2A44DF6CF804B /* TyphoonInjectionByFactoryReference.h */; settings = {ATTRIBUTES = (Public, ); }; }; 62FE895DF9D65A2955A275D909ECBE18 /* SDAnimatedImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 64094E39E3BA0988064CD05A86418E97 /* SDAnimatedImageView.m */; }; 633D48FC60D6C9C2BEEE6F9D7FB9FB31 /* NSInvocation+TCFCustomImplementation.h in Headers */ = {isa = PBXBuildFile; fileRef = 1BDDCD9A639B9E7EBCAF02C54E1DE1E3 /* NSInvocation+TCFCustomImplementation.h */; settings = {ATTRIBUTES = (Public, ); }; }; 63C0FC58D691FE32021C980B486A4FE7 /* TyphoonPlistStyleConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 87B55C9AC3241F2442119F64473D1F41 /* TyphoonPlistStyleConfiguration.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 6553A191C05881C622793B5DD6D312D0 /* TyphoonPathResource.m in Sources */ = {isa = PBXBuildFile; fileRef = CDA3D0B455A13ACC94AFADCBB2E311FE /* TyphoonPathResource.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 659C4CDCC04F165FA3A3C4FDF3DA6A2C /* UIViewController+TyphoonStoryboardIntegration.h in Headers */ = {isa = PBXBuildFile; fileRef = 5AD0D1CF37F873A7C98560DA90D7922B /* UIViewController+TyphoonStoryboardIntegration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 66187A77817A27C7EAA46CA9E1183C67 /* RACEmptySequence.m in Sources */ = {isa = PBXBuildFile; fileRef = DB2ED745168A43281FE03EC58108FBDE /* RACEmptySequence.m */; }; 67178A8153B1A2F1D0D544B8093E23C5 /* SDAnimatedImageView+WebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 826AE09BE9A75B5E4F07DCA8BD6E568A /* SDAnimatedImageView+WebCache.m */; }; 6720FB4D7E520BC198713915B63D0369 /* TyphoonAssemblyBuilder+PlistProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = C5F745BEA95E2A3A7A18764A8CE22A3F /* TyphoonAssemblyBuilder+PlistProcessor.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 67607C80710BD7E0B43D74104EE602AE /* OCMQuantifier.h in Headers */ = {isa = PBXBuildFile; fileRef = 5603722E8B41BC93F91BC672658EA10E /* OCMQuantifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; 676775CB29378BB6CA3CA5992E9C6A99 /* SDImageIOAnimatedCoderInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = C15E41DEC1751BDADA070E77A5D31D94 /* SDImageIOAnimatedCoderInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; 67A2E8F472D342E94FAA643110E9E184 /* TyphoonBlockDefinitionController.m in Sources */ = {isa = PBXBuildFile; fileRef = E842E755FB8A20406553415C9EACC8A1 /* TyphoonBlockDefinitionController.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 681A93DD87FCA7FCAFF578C095B6B9E7 /* TyphoonAssemblyDefinitionBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D3BAA9F0A9900B2F727C2916F30C12A /* TyphoonAssemblyDefinitionBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 694B8697854A776E32032999B2EF1FEA /* UIImage+Metadata.m in Sources */ = {isa = PBXBuildFile; fileRef = FF9570A386DE6A77332680BFBB13573F /* UIImage+Metadata.m */; }; 695E3BEA0C9766D4974998CF17C090BD /* NSDictionary+CustomInjection.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FB2623C122163DC9FB824E1A71D49FD /* NSDictionary+CustomInjection.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 69A06A02F52EB26259FAD1DF6B121BE1 /* SDCallbackQueue.m in Sources */ = {isa = PBXBuildFile; fileRef = 746C9C6D96CC90B1558DBCDA1EF575B4 /* SDCallbackQueue.m */; }; 69AB6A513D5F36D7360FEF4FDA1D60D0 /* UIView+WebCacheState.h in Headers */ = {isa = PBXBuildFile; fileRef = 4C20ADFFD788112D9472742A2E3ACB43 /* UIView+WebCacheState.h */; settings = {ATTRIBUTES = (Public, ); }; }; 69B92D1883A2D2069444FAE5AAE6272A /* CocoaAsyncSocket-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6D7D2C3B5323BC3D3465786EE817BE2D /* CocoaAsyncSocket-dummy.m */; }; 6A19379E3B0370EDA447743C9B1A1379 /* UIImageView+HighlightedWebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 907EE3AB436219C47FE62434C1DCFB33 /* UIImageView+HighlightedWebCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6A4C0518640F1FF5CB8024E732E9DF95 /* MKAnnotationView+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 2D39AF61764732228A5146A67595AEAE /* MKAnnotationView+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6AA16C78FAF9F27ADBA06E4AC494A490 /* NSArray+TyphoonManualEnumeration.h in Headers */ = {isa = PBXBuildFile; fileRef = D524BC409D8FA54C1A06A2E06C1FC36A /* NSArray+TyphoonManualEnumeration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6B0978C9398336656EE309E62060AEAB /* SDImageAssetManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 8233301E93D80EEC51CC7AFF84085AC7 /* SDImageAssetManager.m */; }; 6B5C3592B5E911E833D067D0BC785B1A /* SDImageFrame.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D4CB8BAE1E9433AA4D034F31AD60DE4 /* SDImageFrame.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6BF2FA3FD858A08305204D1A39AC6AB5 /* TyphoonDefinitionRegisterer.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AC6505E33748BB6A720F5B5C8C96382 /* TyphoonDefinitionRegisterer.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 6C2605B3C710A00D92D6D5059642EDDA /* TyphoonColorConversionUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 065E52ED2D30B3B331A00D8EB564E4D8 /* TyphoonColorConversionUtils.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 6CDB988A83D6057DEBDEE651EAB17FB9 /* TyphoonInjectionByCollection.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DA26B2B02963DB127DA3542FECF3472 /* TyphoonInjectionByCollection.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 6CEAFC9DBB829A1A09338FCC0E9072D2 /* OCMInvocationMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 14BA0C92570BFCBF28C82F32F37A96B6 /* OCMInvocationMatcher.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 6D03C8262D633E11DFF26765A28B3916 /* NSValue+TCFUnwrapValues.h in Headers */ = {isa = PBXBuildFile; fileRef = 10AF2F5387AC99FDF1EE38CC6AF81A5A /* NSValue+TCFUnwrapValues.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6D316F209F073EEEC9898B2DED020AF8 /* RACChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = ADA38AFC2E679FBA25375054E69DEFB2 /* RACChannel.m */; }; 6D423F8538D6266DB24DAFFED462D46C /* TyphoonComponentFactory+InstanceBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = FAEAEC0032244300776CF7AFB0401C03 /* TyphoonComponentFactory+InstanceBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6D7651C8B1EA18B2EB5278952B19FC15 /* TyphoonMethod+InstanceBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 6E9BA0379B500368359C8D6E3C90BE50 /* TyphoonMethod+InstanceBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6D9949D8FE86A2D522CB6C880B8E63CD /* NSString+RACKeyPathUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = 0EA9D6ACAA4596DDBF52BD0B1662EB44 /* NSString+RACKeyPathUtilities.m */; }; 6E261D72B6817096F7A6E136C3B32A9F /* Reachability.m in Sources */ = {isa = PBXBuildFile; fileRef = A420909D1D09A79AE6103170D58A4332 /* Reachability.m */; }; 6E66305665DBCFBCF5B2480BF705D500 /* SDWebImageTransition.h in Headers */ = {isa = PBXBuildFile; fileRef = A16A247B48BF9C8E1B54650152D9F47E /* SDWebImageTransition.h */; settings = {ATTRIBUTES = (Public, ); }; }; 6E95C1B16744B10481BFAAF67AA8D5F7 /* TyphoonStoryboardProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = A96DEC36622CF8577C0E003138EFD7EE /* TyphoonStoryboardProvider.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 6EFEEE3AE22E97DCEC4F5A3B88F56FC7 /* SDImageLoader.m in Sources */ = {isa = PBXBuildFile; fileRef = 3E8A97304D179B0ADAF704C7CEBEECD1 /* SDImageLoader.m */; }; 6F3637EE643EABB1DE9212EA68649A64 /* UIColor+SDHexString.m in Sources */ = {isa = PBXBuildFile; fileRef = A67FE7A5F980382C477CC0E5671FB1B3 /* UIColor+SDHexString.m */; }; 7008DDC9510D90489B283BEF7993789C /* TyphoonPropertyStyleConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = CA332156FBF114376E2FE5077E71C03E /* TyphoonPropertyStyleConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 700FCD9C14FA276BBED0A2A34D274808 /* OCMPassByRefSetter.h in Headers */ = {isa = PBXBuildFile; fileRef = 5881CE458D78CE4C7C6B914138FDE55B /* OCMPassByRefSetter.h */; settings = {ATTRIBUTES = (Project, ); }; }; 701B922F54EA9A9A9FDA2C17CE75CDE0 /* TyphoonReferenceDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = FD2C416CACE6B5586F404FC1E9F617FA /* TyphoonReferenceDefinition.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7074EA7FCC90B4967A437F5C43496828 /* SDDisplayLink.h in Headers */ = {isa = PBXBuildFile; fileRef = C40D9702A810253531E2904C61CE32F5 /* SDDisplayLink.h */; settings = {ATTRIBUTES = (Private, ); }; }; 711D32EF4A9901567A488291603BF906 /* SDWebImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 81E027BECECC686145B8874E339146B3 /* SDWebImage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 716F02FA5B659533261BD4BEE3695C1C /* TyphoonPassThroughTypeConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = 75013D4007A6A538ACAE4686AAC93DE4 /* TyphoonPassThroughTypeConverter.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 717F76926C7BCB5B10C3037AD9239084 /* SDImageIOCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 74A9B68BB1F2C7DC615156DBA4077781 /* SDImageIOCoder.m */; }; 71A9A14223B711ED398479921B0D099B /* RACEmptySequence.h in Headers */ = {isa = PBXBuildFile; fileRef = A9638C9372C5407A0209909EB7BAFDCA /* RACEmptySequence.h */; settings = {ATTRIBUTES = (Private, ); }; }; 71BEB1D9532900291A5A24B1C038516F /* UIColor+SDHexString.h in Headers */ = {isa = PBXBuildFile; fileRef = 56D32E1C48670C43C54173C7D2B28771 /* UIColor+SDHexString.h */; settings = {ATTRIBUTES = (Private, ); }; }; 71E3627F3118542E4CF67F2F67621BD7 /* TyphoonMethod+InstanceBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 34517F02303D979079D891F86B87BD5D /* TyphoonMethod+InstanceBuilder.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 71F2B8CBB99087F348C472230200586F /* SDGraphicsImageRenderer.m in Sources */ = {isa = PBXBuildFile; fileRef = 0C9B76DC6B005BE0C06B737507138A3E /* SDGraphicsImageRenderer.m */; }; 72493549A72116EBF5CAF735CD0AEA80 /* RACSequence.h in Headers */ = {isa = PBXBuildFile; fileRef = 9A2036089AC8297FFEE003F786496211 /* RACSequence.h */; settings = {ATTRIBUTES = (Public, ); }; }; 72AE6D5A79C2CC6D5E6578F9644C654B /* UIAlertView+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 7EB4C9DFCDF6EB86413C60976F25BE67 /* UIAlertView+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 73484D39A92E83DF9E2B56D524ECA9C5 /* NSObject+RACDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 72274AF631E665EB74378E781E9A7437 /* NSObject+RACDescription.h */; settings = {ATTRIBUTES = (Public, ); }; }; 73574D4AAB1219ED412B3D2A6D1E321F /* UIStepper+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = AD8E6EFAEA3F539F1D1F0B9752F7E7EF /* UIStepper+RACSignalSupport.m */; }; 73A70EE2C9C9FC76C6495945F3F0DC4D /* RACDisposable.m in Sources */ = {isa = PBXBuildFile; fileRef = 419B699C2112F512494E7DDB5FE14FCA /* RACDisposable.m */; }; 7434021F1AFA292EBE7A85C66AFD33AC /* TyphoonTestUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = E586B047153D54E5818AFAA7B25CC55A /* TyphoonTestUtils.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 74C474676C69A80BEC29B0F55FDF4D19 /* UIView+WebCacheState.m in Sources */ = {isa = PBXBuildFile; fileRef = 5517D5F4CC5D9F18F26A5ED4C9034C6D /* UIView+WebCacheState.m */; }; 74E069F8C9E22C0E37F261A5AB03A613 /* SDWebImageDownloaderConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = CDACF51466350F7A4B6A82E206A3F37A /* SDWebImageDownloaderConfig.m */; }; 752822FE3F5092322D18FEC4533B79A9 /* SDWebImageDownloader.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B0A54B1F889B9B87CDFE1DB41F6ABCC /* SDWebImageDownloader.m */; }; 756F7CB95E7C108324A2F7B8FBF6B17A /* TyphoonAssembly.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E4A10B9B5F3F352208D4FC87A2ECF /* TyphoonAssembly.h */; settings = {ATTRIBUTES = (Public, ); }; }; 75771A97B77FA30A0175A81B480F80EF /* UIImage+ForceDecode.h in Headers */ = {isa = PBXBuildFile; fileRef = FA43EAB3E19854BD7C0DF6BA3C16B9AF /* UIImage+ForceDecode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 76FE013CAA5D3972F17B96497E3DBC56 /* RACDynamicSequence.h in Headers */ = {isa = PBXBuildFile; fileRef = 0E69DD9AA22CCF06F1224BEB3E20EDD3 /* RACDynamicSequence.h */; settings = {ATTRIBUTES = (Public, ); }; }; 772CF8E9CD02ECA4275B6173E2110E80 /* View+MASShorthandAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 68E47146ED471AD73B71268A94D9F69D /* View+MASShorthandAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 777DF7EE911DE5E8A67933FF88906269 /* OCMNotificationPoster.m in Sources */ = {isa = PBXBuildFile; fileRef = FCD14B4785B937EC852B628A8B7F5227 /* OCMNotificationPoster.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 78DB67A6B39E0407C2987B9841C0B052 /* TyphoonFactoryPropertyInjectionPostProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = 458457A3A3CABCBDF2136A3F5E6E82BF /* TyphoonFactoryPropertyInjectionPostProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78EF98028F832D934652FDFF7527F595 /* TyphoonPlistStyleConfiguration.h in Headers */ = {isa = PBXBuildFile; fileRef = 4E5A663C1E60087F575DDB601D6017F3 /* TyphoonPlistStyleConfiguration.h */; settings = {ATTRIBUTES = (Public, ); }; }; 78FF43FC286BBB9D9B0D6AFADC6F0916 /* TyphoonDefinition+Option.m in Sources */ = {isa = PBXBuildFile; fileRef = 7B345A1E24575BA9D0BC33710FE943C0 /* TyphoonDefinition+Option.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 79CCECA8A1FF25E880AB21DA8BF337A6 /* TyphoonAssemblyBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 43C103B5C9A8BC55D3644883BC36B3A6 /* TyphoonAssemblyBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; 79D360313699E9086FEDED32E78C12B4 /* TyphoonOptionMatcher.h in Headers */ = {isa = PBXBuildFile; fileRef = CF5D6355EBCEC215D5F4264D1241E1B1 /* TyphoonOptionMatcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7A2A5EFE8571CB8A7E354E536997AD1B /* OCProtocolMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 78E1C3D8226EEA6918CB1B8A96FC843A /* OCProtocolMockObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 7A4EB9ED5D4E03170FFE61FCB299687B /* SDAnimatedImagePlayer.m in Sources */ = {isa = PBXBuildFile; fileRef = F77D3BA16F4A5B6BBE117AA9B4BC653E /* SDAnimatedImagePlayer.m */; }; 7A95ACE7157BEA7C34CE60ECCCD73F1C /* NSLayoutConstraint+TyphoonOutletTransfer.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DF90E5FD761A704C337FA27D910B4EC /* NSLayoutConstraint+TyphoonOutletTransfer.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 7AD50450C1AC9BC993C2911D83D14146 /* TyphoonStackElement.m in Sources */ = {isa = PBXBuildFile; fileRef = 1B68D1A525FF5B8E36976449BC5C2434 /* TyphoonStackElement.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 7C03AB94ABD7FA82C939386C8827E0B4 /* TyphoonNSNumberTypeConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = 13D762E3256505E91BAEA28CEAA5F68F /* TyphoonNSNumberTypeConverter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7C1725EF13F888580C9E2F3057B33FA4 /* Typhoon+Infrastructure.h in Headers */ = {isa = PBXBuildFile; fileRef = 76C8CC4DEAB56D01254A5E21663A9CB3 /* Typhoon+Infrastructure.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7C28695EB07687D1CC58A6B130EEBA54 /* TyphoonReferenceDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = C2D79FFE2E4790A5C03593BBCA678B0A /* TyphoonReferenceDefinition.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 7C45DBA62EE045C4922404182F6393B8 /* SDWebImageError.h in Headers */ = {isa = PBXBuildFile; fileRef = D6E9B831B9FB4D942099CD004CE0BAC3 /* SDWebImageError.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7C5505A2D3F2A697A5F324787061F4B7 /* MASConstraint+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = E5AE3EB7707FD5429801C6CB2A8A382E /* MASConstraint+Private.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7C6A24B103E14B07F00100A020D9BA7D /* YYModel-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B744979B63E3F8D6CD8B04CCE7E0E58 /* YYModel-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7C9155F50E0008C9C19C443D4257818A /* OCMock-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = B1233E43A91D5DA56B2AA3A634E7BF05 /* OCMock-dummy.m */; }; 7C92A7B83580ADE50BF1E77BE494445A /* TyphoonStoryboard.h in Headers */ = {isa = PBXBuildFile; fileRef = 384D578EC3A290F1ADF6C1728A6C1562 /* TyphoonStoryboard.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7CA28A9549AA6EC233E0F26268EE312E /* TyphoonObjectWithCustomInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = 661A8C92540E00912555E0561EB50777 /* TyphoonObjectWithCustomInjection.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7CBDB5FECCE29FAD4BB0D8E763FFDE83 /* TyphoonUIColorTypeConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = D421E7A95F7BBA522BE9AE982E7390CF /* TyphoonUIColorTypeConverter.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 7D2E8066457576BA5C42D43C38FDE09B /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 08703C437757C9668B19065994EA7EBD /* XCTest.framework */; }; 7D52D2C3BDE0C22A5305A2619A99E60C /* TyphoonLoadedView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3D455866AC1ECBAFA4D139C7DD0E3168 /* TyphoonLoadedView.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7E87D589F135093C179E571A12242622 /* RACSubscriptingAssignmentTrampoline.m in Sources */ = {isa = PBXBuildFile; fileRef = C218F557360FC510BBA92A7B3EEC1065 /* RACSubscriptingAssignmentTrampoline.m */; }; 7F455A25066EB5B687D341842A8825F8 /* TyphoonParentReferenceHydratingPostProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = C9703620E8931C6F0907DD19F302A7A9 /* TyphoonParentReferenceHydratingPostProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8042F6C17A7801DC10A84A74341EA593 /* TyphoonNSNumberTypeConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FEDDBAED301C3BCF65687E2F5B90003 /* TyphoonNSNumberTypeConverter.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 8085C3E407AA5EF1D9D7376F893F2B6A /* RACMulticastConnection+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = AFCF2DB5C3722AD61AAD5876C0E5E4CF /* RACMulticastConnection+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; 809CF3FAEE0A4B5157D245F04BFEF46A /* TyphoonAssemblyAdviser.m in Sources */ = {isa = PBXBuildFile; fileRef = B8D365F9E6ABE29BE745099B2A29F0B1 /* TyphoonAssemblyAdviser.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 80E157CBA7AE6BD79E34E063F46ED57C /* UIActionSheet+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 276FD7F39BAD92CD4DE60118D6BDC68B /* UIActionSheet+RACSignalSupport.m */; }; 80F35FE6EC89AAC77F968AC86618E92A /* UITableViewCell+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 9A1718181F5BEF030AA0CA446A6686F2 /* UITableViewCell+RACSignalSupport.m */; }; 80F8652EFE9F52A3E61806E5E01AE251 /* TyphoonNibLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = F180A82892F507476C7119020F8B59B0 /* TyphoonNibLoader.h */; settings = {ATTRIBUTES = (Public, ); }; }; 812856A2F30E87464C6509362FA23ECA /* TyphoonAssemblyAccessor.m in Sources */ = {isa = PBXBuildFile; fileRef = EFB2E394DB7A9A7EEE8003F7C8E6AD40 /* TyphoonAssemblyAccessor.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 813BE4C96A6D39C13EC50C6CD164F0AF /* MASConstraintMaker.h in Headers */ = {isa = PBXBuildFile; fileRef = E3F2828AF28F25FC9663EDF2BD110090 /* MASConstraintMaker.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8174C42751AFCD44787AF27A54C9CAB8 /* OCMFunctionsPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 73855182D583882A717B2006B73A0953 /* OCMFunctionsPrivate.h */; settings = {ATTRIBUTES = (Project, ); }; }; 81FEC159F4F96E6017E7B841CB99A5F0 /* TyphoonOrdered.h in Headers */ = {isa = PBXBuildFile; fileRef = 08856CD53ABCA9A62737A9BB60FD3496 /* TyphoonOrdered.h */; settings = {ATTRIBUTES = (Public, ); }; }; 822F14A51D26702C2C3CDE7FC6FD4559 /* NSObject+RACPropertySubscribing.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB3821735D0527AC675FE95C25658F8 /* NSObject+RACPropertySubscribing.m */; }; 8320C45699CAC5719F2A8520B6592F66 /* ReactiveObjC.h in Headers */ = {isa = PBXBuildFile; fileRef = 65BC03570C4B6FD508A02A616BE7DB93 /* ReactiveObjC.h */; settings = {ATTRIBUTES = (Public, ); }; }; 83530BF68848CD2C4A79A1FD69B304A5 /* SDImageGIFCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = D69D5EFB2189DEA994DD47111C55B929 /* SDImageGIFCoder.m */; }; 8383EB3CD47EA7CC0BB80D288FDA7483 /* NSInvocation+TCFWrapValues.h in Headers */ = {isa = PBXBuildFile; fileRef = F662CD4F60FC2C37B2BEB530D71C3725 /* NSInvocation+TCFWrapValues.h */; settings = {ATTRIBUTES = (Public, ); }; }; 83DD7E3A598132752EDA97173C87D115 /* TyphoonAssemblySelectorAdviser.m in Sources */ = {isa = PBXBuildFile; fileRef = 5E2C6F46F631D89037B7C021DF8B3373 /* TyphoonAssemblySelectorAdviser.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 8414CFEEB64ACA817EB88D2FEADDA3B3 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; 854807558DCB972EDDFC1D00032BA6E4 /* SDWebImageDownloaderConfig.h in Headers */ = {isa = PBXBuildFile; fileRef = 9B6EEC7E6EC11E55D1422113BAB387BF /* SDWebImageDownloaderConfig.h */; settings = {ATTRIBUTES = (Public, ); }; }; 85C08D3AEAC2C12FB06579C326550982 /* RACTestScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = 115C7117F68E39901C8DF436A5193C71 /* RACTestScheduler.m */; }; 85C0B4EE334B9972299E62DE61A4BB56 /* SDImageLoadersManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 72AB4D998C2E02839ACA5C88AC0929A9 /* SDImageLoadersManager.m */; }; 8632C5137523138C2CE74247D24B9B31 /* Reachability-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B40A0C264DE39B5EEE32E9062A0CCF2 /* Reachability-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 864972FB0DF4B464B1B505AA5F788E91 /* SDInternalMacros.m in Sources */ = {isa = PBXBuildFile; fileRef = 952891A7D50B078702F1A3896E8BA349 /* SDInternalMacros.m */; }; 8674138091C088977F9F531E5A61384F /* TyphoonComponentFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 8725F3C4CF3EA8CA846B7A932532168D /* TyphoonComponentFactory.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 8706F7110B1A424205AF1E4A50CDEA7B /* RACScheduler.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D249879A5BB3605D92C3BCE919251C0 /* RACScheduler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 871E0CD2D0694414A2C0C801E88C93BF /* TyphoonInjectionDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = 900C7737DD6EC812009057A2F330E295 /* TyphoonInjectionDefinition.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 87564E72B9489B5338ECCEDC18CC9705 /* NSLayoutConstraint+TyphoonOutletTransfer.h in Headers */ = {isa = PBXBuildFile; fileRef = 8EA5D8C4206359854EDBE37A94AF6B6A /* NSLayoutConstraint+TyphoonOutletTransfer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 88473AE7C22F952DACB39FA0758D1624 /* SDMemoryCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D6B350707C3EC3D1DAE907D140B8969 /* SDMemoryCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 884B156015530EE607F57D665B076E89 /* TyphoonMethod.h in Headers */ = {isa = PBXBuildFile; fileRef = 08B6FA0A27CA67CF82F94F768E83FBBD /* TyphoonMethod.h */; settings = {ATTRIBUTES = (Public, ); }; }; 888E7F1F8876AF0CF9EDA4A866E376AD /* UISegmentedControl+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B7C029402AB7307D26D094182B34A802 /* UISegmentedControl+RACSignalSupport.m */; }; 88A23DF6F5638AC66C28C4102824E8B5 /* NSImage+Compatibility.m in Sources */ = {isa = PBXBuildFile; fileRef = F21C58BD3E6383E8D7CDE5A22DFE4432 /* NSImage+Compatibility.m */; }; 89CAFC43896C8BF254CDEAEF1298D287 /* RACDynamicSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 27871BA973C3B687A1D538285665EBB6 /* RACDynamicSignal.m */; }; 89D6769F9AA17FE6F4742FB49196CE8B /* UIDatePicker+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 162947A4139E0A2C2197D6A479D07445 /* UIDatePicker+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8A0E94F87B74EB4E7C326ABB6FD7BF2C /* ReactiveObjC-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E270F13C426BEA6928171EC9DBCEE70 /* ReactiveObjC-dummy.m */; }; 8A5D97580D71011B24EB2F8394C021A3 /* OCPartialMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 1AE4A447234AC4D5BD55348C3E5BC6E8 /* OCPartialMockObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 8A66D5CED5EACD00CFB93A5A609B8EA2 /* TyphoonPreattachedComponentsRegisterer.h in Headers */ = {isa = PBXBuildFile; fileRef = 2BE98D49469466E2F555F3E1E65A4F4C /* TyphoonPreattachedComponentsRegisterer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8AF38EDB1E9BF0D334AEB23C488870B8 /* NSData+ImageContentType.h in Headers */ = {isa = PBXBuildFile; fileRef = 610276A9F0EF8CDEC1E67F7E18C1A180 /* NSData+ImageContentType.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8B8E0858C48167346060A72979D08AC1 /* RACQueueScheduler.h in Headers */ = {isa = PBXBuildFile; fileRef = A86A73484750ADD56A5905600D66D803 /* RACQueueScheduler.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BC4CE9F36D19A09404A29C66145AF28 /* NSObject+RACLifting.m in Sources */ = {isa = PBXBuildFile; fileRef = 032C0CB079DA640D62F451CF2BB09DBF /* NSObject+RACLifting.m */; }; 8C6C7E25C5A24C936F81823978190E96 /* ViewController+MASAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 0ED44D2F4F4F552521A65726C787244A /* ViewController+MASAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8D07F8CA69CF8A20CBC86823852AE944 /* RACUnit.m in Sources */ = {isa = PBXBuildFile; fileRef = B8FF03BB1DD7B75DF4D031C0DA692637 /* RACUnit.m */; }; 8D082ED05C36EDF278A1065FBE114F38 /* RACEXTRuntimeExtensions.m in Sources */ = {isa = PBXBuildFile; fileRef = AA87031490FD30914ED01BF6A257DB56 /* RACEXTRuntimeExtensions.m */; }; 8D8AD606ECD8E1F247965CD43956D412 /* UIImage+Transform.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A8368182067949B6C49AB727784993C /* UIImage+Transform.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8DBD580686F262FA6D74D45CEA708336 /* NSNullTypeConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = D655158D8BA8A371D68B4A11241E9616 /* NSNullTypeConverter.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8DE844D9A25F4BA974AEB6997F1848FE /* TyphoonAbstractDetachableComponentFactoryPostProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = FE4F16AD593A4CCB580F7CD5B86431EF /* TyphoonAbstractDetachableComponentFactoryPostProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8E0AEE69E2DFB3EC3358444351D332BD /* OCMBlockCaller.m in Sources */ = {isa = PBXBuildFile; fileRef = 437DE71945F67DF09BAC5E5BB65C5A20 /* OCMBlockCaller.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 8E596CBDA745237A010AE6C43429630D /* NSObject+RACKVOWrapper.m in Sources */ = {isa = PBXBuildFile; fileRef = 8536C813226AC2CB3B45AB654F52A79D /* NSObject+RACKVOWrapper.m */; }; 8FF7B6477BFA6E6ABA168E1417291D5F /* MASCompositeConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 59D821E26E6C5AFE07427222FB4AB639 /* MASCompositeConstraint.m */; }; 9027CF29E44F82D4CE33CB554C662F1B /* NSObject+RACDeallocating.m in Sources */ = {isa = PBXBuildFile; fileRef = B4250F1A74D43D858FA5327444397E4B /* NSObject+RACDeallocating.m */; }; 906DCE66CD5BD236081D468616199BB7 /* SDWebImageOptionsProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 72C6CF5849F4A490EF85880EED97E773 /* SDWebImageOptionsProcessor.m */; }; 913AD2CA2F03CE4F360AB192307AA9BB /* NSURLConnection+RACSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 3E461ADDAEAAFCFADE347E2C9267118A /* NSURLConnection+RACSupport.m */; }; 919576C1B49D8D1790FB53BBDC0B289E /* TyphoonRuntimeArguments.m in Sources */ = {isa = PBXBuildFile; fileRef = 3FE3393A74BB6D586E4B892A33322286 /* TyphoonRuntimeArguments.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 91AAF555B286FBF53E4F98D092B406BD /* SDWebImageTransitionInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BFF4254094763357CA3C84E0939A2C3 /* SDWebImageTransitionInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; 91E8B94F8E02ABF5197DF5AE7D0B3934 /* SDWebImageDownloaderDecryptor.h in Headers */ = {isa = PBXBuildFile; fileRef = E8412B691818531EBFB6124AD9FDA943 /* SDWebImageDownloaderDecryptor.h */; settings = {ATTRIBUTES = (Public, ); }; }; 928371B066E1211CE87089668D5BCB4C /* SDDiskCache.h in Headers */ = {isa = PBXBuildFile; fileRef = E0C543AB14CD3CB33002A31DFA8A3ED7 /* SDDiskCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; 92B43E6B9457B4CAC5D92E7576C6D137 /* TyphoonInjectionByDictionary.m in Sources */ = {isa = PBXBuildFile; fileRef = 70B4C8AEAC9F4A07E37287417595490E /* TyphoonInjectionByDictionary.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 9345137ED10358B60E37D05FB6165759 /* SDFileAttributeHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 7E12AEC65EEC09FD15C652679375036E /* SDFileAttributeHelper.m */; }; 9345A406A970313436762F4127B84249 /* TyphoonInjectionByReference.h in Headers */ = {isa = PBXBuildFile; fileRef = 218826C133BB15ACA97ED4D92CBF2243 /* TyphoonInjectionByReference.h */; settings = {ATTRIBUTES = (Public, ); }; }; 936352E7D044088C175A70DEB26D051A /* TyphoonTypeConverterRegistry.h in Headers */ = {isa = PBXBuildFile; fileRef = D3A21103401B7FEA00216399FCE680EE /* TyphoonTypeConverterRegistry.h */; settings = {ATTRIBUTES = (Public, ); }; }; 93E07C2516E34149D3E8946A095F41ED /* TyphoonCallStack.m in Sources */ = {isa = PBXBuildFile; fileRef = 3BC61D1DBB79E431F7B1D23F12DBA78A /* TyphoonCallStack.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 93FB8EE5015052DF64C1503CEB3B5233 /* TyphoonGlobalConfigCollector.m in Sources */ = {isa = PBXBuildFile; fileRef = CF7C5602AF31F940B921D40619F7D228 /* TyphoonGlobalConfigCollector.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 94633A6E95CF8D2DCB8D84EA25822024 /* TyphoonDefinitionRegisterer.h in Headers */ = {isa = PBXBuildFile; fileRef = F9523376659FAD01D3AAF8C11D821E14 /* TyphoonDefinitionRegisterer.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9470CC4CA104628FE6616BDC7AC04062 /* RACEagerSequence.h in Headers */ = {isa = PBXBuildFile; fileRef = 945289B9A84910C8F31646C46C4531EB /* RACEagerSequence.h */; settings = {ATTRIBUTES = (Public, ); }; }; 94825DA7AAEA8838DD00AE6C2FA11124 /* UIDatePicker+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 201545627F7DB82D38F76A51D3D4C33F /* UIDatePicker+RACSignalSupport.m */; }; 948E07C0F0FD61BD378ED66D5565D40F /* TyphoonDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = 35F7DE05C6E6BA99060F8F264A3CEBEA /* TyphoonDefinition.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 95621F7EE7F56B1ACF4141E6EFB175A8 /* GCDAsyncUdpSocket.h in Headers */ = {isa = PBXBuildFile; fileRef = 10396C86F2BA403403FECC4E3914CF31 /* GCDAsyncUdpSocket.h */; settings = {ATTRIBUTES = (Public, ); }; }; 95DF4BD28B81E167A3AC2B7629A561AB /* RACQueueScheduler+Subclass.h in Headers */ = {isa = PBXBuildFile; fileRef = 9AA886724CF6656F190D039297F38E78 /* RACQueueScheduler+Subclass.h */; settings = {ATTRIBUTES = (Public, ); }; }; 96E97174F4614FFA0649085022CB4AFE /* SDWebImage-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 576D3C34B2B7AC921593D4F072ECCBA3 /* SDWebImage-dummy.m */; }; 97235408E59E16C18B6BDA1D29E1CB26 /* SDWebImageManager.m in Sources */ = {isa = PBXBuildFile; fileRef = DF6104CF591EBBA4948511163C719CDB /* SDWebImageManager.m */; }; 97385A64CA020489951EF769392C6DCF /* UIView+WebCacheOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = A4F5A7AB017EF810CFD76454F977D8A0 /* UIView+WebCacheOperation.m */; }; 9744E7851D6BCCDD20453B7D9FC86A02 /* RACCommand.h in Headers */ = {isa = PBXBuildFile; fileRef = 74FEA9E372A4AFA285192EC20BF9AC10 /* RACCommand.h */; settings = {ATTRIBUTES = (Public, ); }; }; 97694B5A3E6FC632B648D23D314AF28C /* NSString+RACSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 8D7A7568A955352E3C8BEA8C6BE5EFA7 /* NSString+RACSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 97C5A66B195BD0F9E7468F6C561CB4C7 /* NSObject+YYModel.h in Headers */ = {isa = PBXBuildFile; fileRef = C3133631DC229F9194ABFF320BE45553 /* NSObject+YYModel.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9838965B6E76EAC0A8EC90802D1A064B /* RACIndexSetSequence.m in Sources */ = {isa = PBXBuildFile; fileRef = F069B23BF8F0BF5B39AAFB166C957FA3 /* RACIndexSetSequence.m */; }; 98BA98A7102962139D85FF53B6F66E66 /* UIViewController+TyphoonStoryboardIntegration.m in Sources */ = {isa = PBXBuildFile; fileRef = 5F6718C9B74C90CF46E2131FD2FB6517 /* UIViewController+TyphoonStoryboardIntegration.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 9993EEC4BDD2BB2437898147FCCE0B39 /* TyphoonViewHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 5A2D575E1ACB5A76D8F5214B6B4B171C /* TyphoonViewHelpers.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; 99FBE73B7B7E6DA785FAD8ED7A2EBB94 /* NSIndexSet+RACSequenceAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 252FA21B0D96A56E65E1481A1B27A6A3 /* NSIndexSet+RACSequenceAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9ADB69DEACB5B811F3CBF85ABF4C0F10 /* RACStream.m in Sources */ = {isa = PBXBuildFile; fileRef = 37CF1049FAEF879E02FD90463A2E3910 /* RACStream.m */; }; 9B2813CB4372A381C770D86A6B8FEA43 /* NSNotificationCenter+RACSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = F9B73C4A3EAA71EE3B8D1F790D768438 /* NSNotificationCenter+RACSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9B3420DEB8A0CCB9E1241A669AEFCA8E /* SDAnimatedImage.h in Headers */ = {isa = PBXBuildFile; fileRef = 0EF30728B0B55C95AD32A7BC318AC70A /* SDAnimatedImage.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9B9343E8599EE5196BA75E842DCB48B7 /* NSBezierPath+SDRoundedCorners.h in Headers */ = {isa = PBXBuildFile; fileRef = 9E18DA0F8466E227AA17A93CF40D4C08 /* NSBezierPath+SDRoundedCorners.h */; settings = {ATTRIBUTES = (Private, ); }; }; 9BF8077CFA9BBB10510FE915F2919A6E /* Typhoon.h in Headers */ = {isa = PBXBuildFile; fileRef = 203BC0FB7B1B96C82C4EAD4593D84A8D /* Typhoon.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C034894E873E67C35BAF6879BF6F05A /* ReactiveObjC-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 5EB6B9FD0EAFE674F066B55BF02CBAD8 /* ReactiveObjC-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C05A169911849DC9E4DAFA0290A9100 /* OCMArg.m in Sources */ = {isa = PBXBuildFile; fileRef = D21426F24A160A03F3A3B57A03B3671E /* OCMArg.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; 9CB0C910E0950794AF58F2F24D1D9D74 /* NSSet+RACSequenceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 11CA0DD35189B363533FA7DEBEAFC7E5 /* NSSet+RACSequenceAdditions.m */; }; 9CE425B89294BE2C13E70A86E75B15CF /* SDDiskCache.m in Sources */ = {isa = PBXBuildFile; fileRef = A652A78922C1F9E12F183395B8233FDA /* SDDiskCache.m */; }; 9DA8BF5E1F62665192ED18B27ADD0FBE /* UITableViewHeaderFooterView+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = CC5646C5FEED8AA9A5E6C5B7AA2A1AC0 /* UITableViewHeaderFooterView+RACSignalSupport.m */; }; 9DB8E1A6396E3AF5AF0855353063ED40 /* NSData+RACSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 5E4B3FE710833AB76C40A34DCAB075CA /* NSData+RACSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9DF446F8CA5BC4D4098766EC9063012C /* SDWebImageOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 1E5E520FF458D112BA579819CA973E7A /* SDWebImageOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; A046D9E46D0C8242D29D4365D8DBEC5A /* TyphoonViewControllerInjector.h in Headers */ = {isa = PBXBuildFile; fileRef = 7DB7EDBC439037E423096117CAA5C2D6 /* TyphoonViewControllerInjector.h */; settings = {ATTRIBUTES = (Public, ); }; }; A04BCE28D4B7FB41973BADA1BC666B7B /* OCMLocation.m in Sources */ = {isa = PBXBuildFile; fileRef = 0D5BC3205064C311F17DDCAF73FE5D91 /* OCMLocation.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; A07B14D885F9954EC871BBB950DD65F9 /* TyphoonInjectionByDictionary.h in Headers */ = {isa = PBXBuildFile; fileRef = 257F4F157F94CB40D1F5643758121C8F /* TyphoonInjectionByDictionary.h */; settings = {ATTRIBUTES = (Public, ); }; }; A096C06AA7D3C5C4E813C211767C3DDD /* TyphoonInjectionByObjectFromString.h in Headers */ = {isa = PBXBuildFile; fileRef = AD91A54AEE593AA59D16627F48452F75 /* TyphoonInjectionByObjectFromString.h */; settings = {ATTRIBUTES = (Public, ); }; }; A11E90132B32ABEA76703BAC68A86357 /* OCMStubRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = A139B74B3BD8E154D5BC6567D4D692D4 /* OCMStubRecorder.h */; settings = {ATTRIBUTES = (Public, ); }; }; A1560247914C760D9EE5F7A2392CC06C /* UIImage+GIF.h in Headers */ = {isa = PBXBuildFile; fileRef = 5E23710C44440556A6042E7F7D8E172B /* UIImage+GIF.h */; settings = {ATTRIBUTES = (Public, ); }; }; A1BD95BC04A46F6587569C6291351595 /* TyphoonViewControllerFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 330097FBD5A9529B89BA11A61293AD4C /* TyphoonViewControllerFactory.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; A2CC61E4BDAEBF249FCD573057423A7A /* TyphoonInjectionByCollection.h in Headers */ = {isa = PBXBuildFile; fileRef = 661239F2EA786F002F7624EEE9A6FB7D /* TyphoonInjectionByCollection.h */; settings = {ATTRIBUTES = (Public, ); }; }; A2CF6FBE8187A1AF97E9995C6F119354 /* TyphoonAssemblyAccessor.h in Headers */ = {isa = PBXBuildFile; fileRef = B0E7E234958AD2EE1C16F6F2EA4B24FB /* TyphoonAssemblyAccessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; A34F832763C924A0911D1E7797533EF8 /* NSDictionary+RACSequenceAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DC413ABFFB1444FE469D466ADE4A98C /* NSDictionary+RACSequenceAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; A3716B6B67727A527D76218D7A8C4C0B /* RACCompoundDisposableProvider.d in Sources */ = {isa = PBXBuildFile; fileRef = F13BB06DB4576664D2BD21C5F4490F59 /* RACCompoundDisposableProvider.d */; }; A3FD061F6162EC8CC95E8523C4F8E414 /* TyphoonDefinition+InstanceBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 429AF24E6181CE01E2CB7642A29FBB6C /* TyphoonDefinition+InstanceBuilder.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; A4334E9632A3BD0817D4F02E5D056D08 /* Collections+CustomInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = 818F3C6FD63BEF6B17D992E201321502 /* Collections+CustomInjection.h */; settings = {ATTRIBUTES = (Public, ); }; }; A4360119EC4A3CECAE7D5B4288F0FED4 /* TyphoonJsonStyleConfiguration.m in Sources */ = {isa = PBXBuildFile; fileRef = 12676821542EF4B94B6913AF583A7A69 /* TyphoonJsonStyleConfiguration.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; A451C30BD205FCAE86B24C0BF35ED465 /* TyphoonAssemblyDefinitionBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 4FA8209FE24832482CBDBD96568B60E3 /* TyphoonAssemblyDefinitionBuilder.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; A4CD275DADB3551201C2A05AD4BB269E /* NSEnumerator+RACSequenceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 59FB7D5724747D377FC8799D1133C4A5 /* NSEnumerator+RACSequenceAdditions.m */; }; A50833B08706004695F22D20FB258019 /* RACSubscriptionScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = AE9AC83D30952DBFFC1F9C714196F202 /* RACSubscriptionScheduler.m */; }; A543C4BC7F666F22D3912798D07C04D5 /* OCMConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = 5649D72ECEC9159D3DAA4B7051E947B9 /* OCMConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; A583A30CEA07A0748B6D95D5D12FF5F2 /* UICollectionReusableView+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 2E4DF20E80C1D11D64201766B309D73A /* UICollectionReusableView+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; A5C476EC387C41C840347804DF32CAF6 /* TyphoonInjectionDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = 9301B5A21675C37DED58F6472417FFF1 /* TyphoonInjectionDefinition.h */; settings = {ATTRIBUTES = (Public, ); }; }; A5F84EDD2763BADFD187D4E4C4043B95 /* NSObject+TyphoonIntrospectionUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = FA664986925B201863730F10AB76F9D5 /* NSObject+TyphoonIntrospectionUtils.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; A6B6F63BCC3A8BE45E9D30DF50935E0D /* OCMRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = C0225799E4F72C1CA22CAA520A08E589 /* OCMRecorder.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; A6FCF74348D1907891FBDE3F2E331136 /* DZNEmptyDataSet-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 3060A3BFE9758FA021148046B85E8CE5 /* DZNEmptyDataSet-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; A754D259316F9E237ABCA70AD7120758 /* TyphoonStartup.h in Headers */ = {isa = PBXBuildFile; fileRef = A13902D081C57317FEA1F09843D569F4 /* TyphoonStartup.h */; settings = {ATTRIBUTES = (Public, ); }; }; A79648E20089DBD6BF599C9F6B6570CE /* UIView+TyphoonOutletTransfer.h in Headers */ = {isa = PBXBuildFile; fileRef = 7CF6453EBAFEAB28E5DCF1E0FEA52A59 /* UIView+TyphoonOutletTransfer.h */; settings = {ATTRIBUTES = (Public, ); }; }; A826A5E0505880B690A0B1877D27CF17 /* NSArray+RACSequenceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = DF3C6B3C4596923C4E111113DF611DF6 /* NSArray+RACSequenceAdditions.m */; }; A839428F403C52D8AA3466B65E20C27A /* NSButton+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F69FEE7B67F930E4BAB806C41EDBDE5 /* NSButton+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; A8B06CAB8D3C0F81058B689563F2F6D2 /* OCPartialMockObject.h in Headers */ = {isa = PBXBuildFile; fileRef = C8A5867815E6437C1B59CDE852811716 /* OCPartialMockObject.h */; settings = {ATTRIBUTES = (Project, ); }; }; A8E58BF4665F9CC4142EA41D80CD6E17 /* Reachability-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = AFDA4AF26CABDC552E1905C902431F4F /* Reachability-dummy.m */; }; A9031D4C7A39A8F0CC9B4BB38D378A33 /* OCClassMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AC7882AF54A36E3A226CA3878D872F7 /* OCClassMockObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; A9071F54289564382DCD37D03E09EA0A /* RACSignalProvider.d in Sources */ = {isa = PBXBuildFile; fileRef = 89FE3BF0E622ACD1D48ABA7DCFF7855A /* RACSignalProvider.d */; }; A92AB5E65CA85947368E46E6627F1BFB /* UIButton+WebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = F817EA9532FEA2C318FD55D4CFE8D722 /* UIButton+WebCache.m */; }; A9A49E4A3BE8882F60DF32BAF39DE191 /* SDWebImageManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 4B18C13FE0EC5A5F756BBA99DC8559F1 /* SDWebImageManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; AA14FA452C6E6DCB9D55BEC86A91ECB2 /* SystemConfiguration.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6506E5BA0D201F5A9633E8253219386 /* SystemConfiguration.framework */; }; AA1EA8F0F0470F1596B1FFA58ABF3375 /* SDWebImageDownloaderOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 84E53FAE993E1AC61F28D1CDC8FF68BC /* SDWebImageDownloaderOperation.m */; }; AA53B6ED50FD6066CC3536EB4C571E0C /* TyphoonAssemblyPropertyInjectionPostProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = E59E27A8B1D09C1C222A67BFFD0845C6 /* TyphoonAssemblyPropertyInjectionPostProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; AAA6AF968C3E92D1BCCBAD27C696628A /* TyphoonOptionMatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = 3B33DB28F54E682CA83C889709720FB7 /* TyphoonOptionMatcher.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; AAFF52BFB654564326D45F7BC6C7C1A2 /* RACTestScheduler.h in Headers */ = {isa = PBXBuildFile; fileRef = D8C316292E777A36CEF459683AF45953 /* RACTestScheduler.h */; settings = {ATTRIBUTES = (Public, ); }; }; ABCB80C4813C849FC93D57676820C907 /* SDImageCacheDefine.h in Headers */ = {isa = PBXBuildFile; fileRef = E9BFDB08B8D6909114F2B3520B986044 /* SDImageCacheDefine.h */; settings = {ATTRIBUTES = (Public, ); }; }; ABDF2546C46AA0359E468651D3CBF26F /* RACTupleSequence.m in Sources */ = {isa = PBXBuildFile; fileRef = 48E883D9DB5DF435F64617A907DCD908 /* RACTupleSequence.m */; }; AC14E56ECA7A4980A8E1CA68E800B12C /* SDWebImagePrefetcher.h in Headers */ = {isa = PBXBuildFile; fileRef = 4D4A2340AC32529239D0841BD00726B1 /* SDWebImagePrefetcher.h */; settings = {ATTRIBUTES = (Public, ); }; }; AD0342BE54652F978FD313F99B10EB84 /* TyphoonMemoryManagementUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D14066F28EE5838EBA4D6142544116D /* TyphoonMemoryManagementUtils.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; AE7B02645B8F769CA5F215EE8F7CC5B0 /* View+MASAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 467F746C89C8EA1B73E4F4C1D741B528 /* View+MASAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; AEB8926360324E2A84020EAD714E33E4 /* RACSubject.m in Sources */ = {isa = PBXBuildFile; fileRef = 749EDDB4FA8F282C297D2ED71E5E2A95 /* RACSubject.m */; }; AED62511435CB679D8E8AC227B4AD8BD /* TyphoonFactoryDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = 28B9BD69A27186895E63DB6A19650F73 /* TyphoonFactoryDefinition.h */; settings = {ATTRIBUTES = (Public, ); }; }; AEEC1FF7C7D8C9B92AD8350441EED206 /* NSInvocation+TCFInstanceBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = 3DFCA73A433D742E4E61D2DF66334925 /* NSInvocation+TCFInstanceBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; AEF7F926931CDC5DF0FA40597840B792 /* TyphoonCollaboratingAssemblyProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 8866DCC2E6B6A7335F7564DFAAA6323F /* TyphoonCollaboratingAssemblyProxy.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; AFBA4DFDAFB6506D5DC862AFB1B4347B /* UISlider+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 4AA45AFD4053DDC8999AD121B914CE27 /* UISlider+RACSignalSupport.m */; }; AFD1D411A9387DCEC6F0034653E23787 /* DZNEmptyDataSet-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = D3B2AC38C369630473925094DAF536F5 /* DZNEmptyDataSet-dummy.m */; }; AFE16FE9315D936E91EECD0184A88FC0 /* TyphoonAssembly+TyphoonAssemblyFriend.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CC43EDBD34D1A25DB41669E7650524E /* TyphoonAssembly+TyphoonAssemblyFriend.h */; settings = {ATTRIBUTES = (Public, ); }; }; AFFC8A6E89ABBDA38786D88EFAFACC90 /* TyphoonComponentFactory+TyphoonDefinitionRegisterer.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F86F06F204B5BACDAEBC5742F93220C /* TyphoonComponentFactory+TyphoonDefinitionRegisterer.h */; settings = {ATTRIBUTES = (Public, ); }; }; AFFCF22AB10FDB1BA56E318CF0FEEACE /* RACKVOChannel.h in Headers */ = {isa = PBXBuildFile; fileRef = B61B3D89FE168025B8BCCE1DE7423DF5 /* RACKVOChannel.h */; settings = {ATTRIBUTES = (Public, ); }; }; B13C2503DD42AF17264AA8F1724C831F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; B1536B8A4A473A9779083C563063B9C8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; B17D5FAAC4B83F94F3841700C4FADE59 /* Pods-iOS-Network-Stack-Dive-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = B755FFA45FCE441B80E378A091AA25F4 /* Pods-iOS-Network-Stack-Dive-dummy.m */; }; B244D4AF0F8115F666321FE05AAFAE75 /* OCMObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 8C3A7610419191166B2C17925674DE17 /* OCMObjectReturnValueProvider.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; B24A1E4D80FC605011440F6676C52EF7 /* TyphoonInjectionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 9BFCB32C228CF4CC491157D1716E631D /* TyphoonInjectionContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; B2704AFFC5CC053154839DB44924D255 /* SDImageCoderHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 8CC52EA095AA94DD387D255F058AF87F /* SDImageCoderHelper.m */; }; B297981C7C5C77B17BE03972BB55D946 /* TyphoonDefinition+Storyboard.h in Headers */ = {isa = PBXBuildFile; fileRef = 30BC8EBE209C30443BACDAAA1B7A4CC1 /* TyphoonDefinition+Storyboard.h */; settings = {ATTRIBUTES = (Public, ); }; }; B312C7154719371C49BCF8055BCC1BFF /* NSOrderedSet+RACSequenceAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 2385FD3E31A6F83168DE9EFA6AC4A66F /* NSOrderedSet+RACSequenceAdditions.m */; }; B331CE2D3DEB461E738B886086A365F9 /* SDImageGraphics.h in Headers */ = {isa = PBXBuildFile; fileRef = FDF2495A0E39E684767BAF85F109189B /* SDImageGraphics.h */; settings = {ATTRIBUTES = (Public, ); }; }; B3BAE2216F20F91DBC4C3E3B105E077D /* RACCompoundDisposable.m in Sources */ = {isa = PBXBuildFile; fileRef = 06AEB99F8D2F4D764FE818A7D523C570 /* RACCompoundDisposable.m */; }; B3CF0FD8C5CAE5360ECF449DF19A4B73 /* NSObject+RACPropertySubscribing.h in Headers */ = {isa = PBXBuildFile; fileRef = 9998C36BB30E19196F7C6F7D1F3EF49B /* NSObject+RACPropertySubscribing.h */; settings = {ATTRIBUTES = (Public, ); }; }; B448686E41D49FAC044634DE4BACBF49 /* NSValue+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D4FB47B1FC93F629C32CDEE69E59AE4 /* NSValue+OCMAdditions.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; B47B9BB155E21E5FB4D01940758EB91D /* NSObject+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = E93FFD1537CD2D6332F9A080E750CAAE /* NSObject+OCMAdditions.h */; settings = {ATTRIBUTES = (Project, ); }; }; B49EB728B20A04676F4A4569A18C7515 /* OCMBlockCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 14EDB30D06B6BB66F89E7DC87C6F82CE /* OCMBlockCaller.h */; settings = {ATTRIBUTES = (Project, ); }; }; B4A055DE2DAA83775FEAD725E7E34F13 /* RACArraySequence.m in Sources */ = {isa = PBXBuildFile; fileRef = 28CC19C7A5B9A7C5439E55E906813635 /* RACArraySequence.m */; }; B4A3D389BDB1B0E149378182CC942228 /* YYClassInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 50A9EB17E5CD95FF47A7DC9D1F6FB3B4 /* YYClassInfo.h */; settings = {ATTRIBUTES = (Public, ); }; }; B4F231C5CBAB3D4A184699D0066E0E83 /* SDImageAWebPCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 31794A4FD83642DD83BC70B9F11CA23A /* SDImageAWebPCoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; B55D6315E855BFCE88C9B27B0C278C4F /* RACKVOTrampoline.h in Headers */ = {isa = PBXBuildFile; fileRef = E6FF1599AA4D761C6A0D334AC8A18BB7 /* RACKVOTrampoline.h */; settings = {ATTRIBUTES = (Public, ); }; }; B562AF7AAA4ACBCB972C87C4A5150D12 /* TyphoonBlockDefinition+InstanceBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = E9CD0BC00D291194A3441C33CBA01647 /* TyphoonBlockDefinition+InstanceBuilder.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; B59E60FBC9665FC1061B88B8E6FD9FAF /* Masonry-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 48A63D2EDED811435B879E1A51A2C5C3 /* Masonry-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; B5AF87C11A465F666473F6191D173905 /* UIView+WebCacheOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 41908C1E64673E808673215A63FCEC45 /* UIView+WebCacheOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; B5B4DEF5ED5485C7F0212CF50C06F191 /* OCMExpectationRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = E1D2B79FD0B5E7ECF99452AC0439FB6D /* OCMExpectationRecorder.h */; settings = {ATTRIBUTES = (Project, ); }; }; B6422D1C5378A1E8CF1DB4EA21D31BD3 /* RACErrorSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = F028FB003652E9875D30DDAF99C21162 /* RACErrorSignal.h */; settings = {ATTRIBUTES = (Public, ); }; }; B66356D4E7E43B3D15324569AA7EBB05 /* SDWebImageDownloaderOperation.h in Headers */ = {isa = PBXBuildFile; fileRef = 79AE668D1D91F91A64F0E74D1BA417EF /* SDWebImageDownloaderOperation.h */; settings = {ATTRIBUTES = (Public, ); }; }; B680C2604BD8BC9644AE7C67BC46B9BB /* MASLayoutConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = 6296945F878C9C5DD60D6B36B3DF4E9C /* MASLayoutConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; B741DBE2A466E6211F879EF997D9322D /* SDImageCodersManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 217EBAD9071176F8750E350BA9867152 /* SDImageCodersManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; B7E49AB762D1E2F7EA004FA2EEBD85A5 /* TyphoonDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = 4834BC1E84F882D9846062878F9F66C9 /* TyphoonDefinition.h */; settings = {ATTRIBUTES = (Public, ); }; }; B86B6B95CC8EE542367125BF22968812 /* TyphoonAssembly.m in Sources */ = {isa = PBXBuildFile; fileRef = 8D8C5527AA1D03083903623D593614BE /* TyphoonAssembly.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; B8E83660C660C63DD95B7C25B3C49DC5 /* NSOrderedSet+RACSequenceAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A69C05290E3D199301DB5557505AEEB /* NSOrderedSet+RACSequenceAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; B92BD83C69F10179ECCBF07FE6132768 /* RACScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = 027098AE8C5EB84C31AACA284246D555 /* RACScheduler.m */; }; B95C63A039D9D08896421291DEBD3AEB /* SDWebImageCacheKeyFilter.m in Sources */ = {isa = PBXBuildFile; fileRef = 146B7F55E58BD9E7C1CB0B3944357F9C /* SDWebImageCacheKeyFilter.m */; }; B995464A5578FA477280532E2C42C72E /* TyphoonInjectionsEnumeration.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A37BECA26F996876B784EFD357DC96A /* TyphoonInjectionsEnumeration.h */; settings = {ATTRIBUTES = (Public, ); }; }; B9B1C853AA90EF26C521CA880113A050 /* TyphoonPathResource.h in Headers */ = {isa = PBXBuildFile; fileRef = F0055EAC2B8C580C32290969DFF19220 /* TyphoonPathResource.h */; settings = {ATTRIBUTES = (Public, ); }; }; BA7F55364F2525C6B8AEA477AF3C8D67 /* TyphoonConfigPostProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = E1372334A95BBBD06EC3A3CDDE782AC3 /* TyphoonConfigPostProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; BA904ABA8ED36CC4E5EB2B2004CA1F18 /* MASCompositeConstraint.h in Headers */ = {isa = PBXBuildFile; fileRef = 903891AB3F9D2ABE990A52BF6D7CE724 /* MASCompositeConstraint.h */; settings = {ATTRIBUTES = (Public, ); }; }; BADA31750A2136D073EDA4461DBE1EEA /* UIButton+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = EF2113E6C3216757EFC73B0ACD0ABD90 /* UIButton+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; BBA7D6B1F5A85263992FEDA1FF97BA10 /* NSObject+RACSelectorSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = 45512E8214358C10C4B33C4B78C7026F /* NSObject+RACSelectorSignal.h */; settings = {ATTRIBUTES = (Public, ); }; }; BBF48D90A3AC8570E5336E819EFE2F0A /* TyphoonInjectionByConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = 71CC76D142ADC81938ACEAA26EA01492 /* TyphoonInjectionByConfig.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; BC60AF43B11C3B3BEA8E91CD67B3EB0C /* UISegmentedControl+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 646DB1C71B2294140046C6270BDC8C22 /* UISegmentedControl+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; BCDC1E1D46DD124B5726A064D2EE66A3 /* UIImage+MultiFormat.m in Sources */ = {isa = PBXBuildFile; fileRef = 1D09E947A17A03E7F1BE82DA4451726B /* UIImage+MultiFormat.m */; }; BCEFDE57BB0E0B36731C8D39FFA1BE2C /* SDWebImageDownloaderRequestModifier.h in Headers */ = {isa = PBXBuildFile; fileRef = AB64B8DD79975FE71CC74B5FBD9785F0 /* SDWebImageDownloaderRequestModifier.h */; settings = {ATTRIBUTES = (Public, ); }; }; BDBE494BAC544843982C3CA96A6C41DD /* SDAnimatedImagePlayer.h in Headers */ = {isa = PBXBuildFile; fileRef = A7FF20A852BCFA4897183BC64A6E1D00 /* SDAnimatedImagePlayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; BED810910D0734E1A3D77CC7A4B7B08F /* TyphoonStoryboardDefinitionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = A499F0CFC3E7B3B36E9786BCB6052F37 /* TyphoonStoryboardDefinitionContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; BF22D137EF6324675FA50080C5D93C00 /* NSArray+MASAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = B5261A9D35AA9978FA8E8427895CDA41 /* NSArray+MASAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; BF560791320154400F8183101750BAEF /* TyphoonSelector.h in Headers */ = {isa = PBXBuildFile; fileRef = 17B387B9415B8C98C3579629FB677E97 /* TyphoonSelector.h */; settings = {ATTRIBUTES = (Public, ); }; }; BF7D73BC31ED41C8C87A7AD8E615BA90 /* UITextView+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 312F34A0D29AA628F2B731F4DA50B1E7 /* UITextView+RACSignalSupport.m */; }; BF7E2E36C374B7B1CD0A5332F2A9B26E /* RACKVOChannel.m in Sources */ = {isa = PBXBuildFile; fileRef = E3CC05377476E8CA3BDD23296111A57A /* RACKVOChannel.m */; }; BFBC117B8B5FE76AECB2448980618552 /* TyphoonBlockDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = D5A65F1C73D00CF1864BB3078888AECD /* TyphoonBlockDefinition.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C029772A70ECFC78C192D0DED7EB10A5 /* TyphoonViewControllerInjector.m in Sources */ = {isa = PBXBuildFile; fileRef = BDD4238ED758F069DED59626A8859F28 /* TyphoonViewControllerInjector.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C051A259A6CC3FF96D942F78A70AD43D /* TyphoonDefinition+Config.m in Sources */ = {isa = PBXBuildFile; fileRef = 40D4E59B9A952E2A8649507C8627874B /* TyphoonDefinition+Config.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C08752A16103BCA4C7FD459F2D2CAF52 /* TyphoonUIColorTypeConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = 32939C9AD7B94007A62205D5A5CA486C /* TyphoonUIColorTypeConverter.h */; settings = {ATTRIBUTES = (Public, ); }; }; C08F788A609C4CB2D525FD21D4FAD291 /* NSObject+RACKVOWrapper.h in Headers */ = {isa = PBXBuildFile; fileRef = 633F2FE6EBD79D22A6B840F443F0A83B /* NSObject+RACKVOWrapper.h */; settings = {ATTRIBUTES = (Public, ); }; }; C1C54E804EC953B610851FBD38319CC6 /* TyphoonLoadedView.m in Sources */ = {isa = PBXBuildFile; fileRef = 1F38003E7B8D270C5FC263FAC5B717D5 /* TyphoonLoadedView.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C1CE7DDDEF51510CEDBE3B0C57A49FA3 /* NSString+RACSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 3290EEA634B358879EBD921ACF1B1422 /* NSString+RACSupport.m */; }; C1DD8C6A64F948E4C53560C76B995DA4 /* SDAnimatedImageView.h in Headers */ = {isa = PBXBuildFile; fileRef = 0845491EC6BC3A5BFF1808F961A5440F /* SDAnimatedImageView.h */; settings = {ATTRIBUTES = (Public, ); }; }; C1ED07F75CD8C2D19DA79D96EA77F802 /* RACUnarySequence.m in Sources */ = {isa = PBXBuildFile; fileRef = 44725BC3ADC3FA22EF1DD0F3164C12FF /* RACUnarySequence.m */; }; C2068AEACC2D9C7F1FFE41AA25B12A68 /* MASUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 6622E7C87393460832DC95149834E273 /* MASUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; C21BB4F85EC11E3A16DB3EFD10168CD9 /* RACCommand.m in Sources */ = {isa = PBXBuildFile; fileRef = 551D5C997519E15B230BE54573BA8B99 /* RACCommand.m */; }; C22FE4A2C5BCF1C1BE001EC38B21A720 /* TyphoonDefinition+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 424E4FDDC999763B5ECDE171AF2948A3 /* TyphoonDefinition+Internal.h */; settings = {ATTRIBUTES = (Public, ); }; }; C237DF03B34507F0E0E883D8F7D242BB /* TyphoonAssemblyPropertyInjectionPostProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = E4338428A593399BDE88E88C9CEF385C /* TyphoonAssemblyPropertyInjectionPostProcessor.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C2840BF1950FF7EE2DCD6D55F768A49C /* UIImage+GIF.m in Sources */ = {isa = PBXBuildFile; fileRef = 7A4EBA3FAF480391CADE1DF78A1EB2DA /* UIImage+GIF.m */; }; C2DE26C06C3E3FE7C2D67DFF8F1B4565 /* TyphoonMethod.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D46806A2CE11059C3A8BD7E50783943 /* TyphoonMethod.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C2FE60A10C792613E45031AE6E851ECB /* MASViewConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 4349B8FF1BD06C628405857A06C179DD /* MASViewConstraint.m */; }; C449FE8D987CA3477F9F9272BF091CCE /* TyphoonNSURLTypeConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = FFD16E6D61E5F1AA7ACD347FD6E0120D /* TyphoonNSURLTypeConverter.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C45BDBA1768C56781E48457C6543F6B2 /* RACBehaviorSubject.m in Sources */ = {isa = PBXBuildFile; fileRef = CBD6CB22BEFFD7B25F968C06E8168BE5 /* RACBehaviorSubject.m */; }; C5B5337075E0DB3B2369B4CF7ED68479 /* TyphoonWeakComponentsPool.m in Sources */ = {isa = PBXBuildFile; fileRef = B66C84A540CE194D4B1F225949E9EA7F /* TyphoonWeakComponentsPool.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C5F712FC268AE177C29C28244AADEF81 /* NSURLConnection+RACSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 98A3E71FFFC45DA5BB85A8525B39AC52 /* NSURLConnection+RACSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; C605731D421EA67B3D573E9C3E7C10CF /* TyphoonInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = DAF562B23684D41ED34CC07CD14C7875 /* TyphoonInjection.h */; settings = {ATTRIBUTES = (Public, ); }; }; C6A100159974349FEAAC99B82BE0F872 /* SDImageLoader.h in Headers */ = {isa = PBXBuildFile; fileRef = D029DA41DE158F6663F423C5BEC68F57 /* SDImageLoader.h */; settings = {ATTRIBUTES = (Public, ); }; }; C6E08EB55D40EC7D87E27012DA80C041 /* TyphoonViewControllerNibResolver.m in Sources */ = {isa = PBXBuildFile; fileRef = 645F110D573A95F0F5F61F5666D9AD90 /* TyphoonViewControllerNibResolver.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C814E15114F4A34E55A3C23173F05537 /* TyphoonInjections.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D4BFAFED921E3A451A3FCC58BD5A0A /* TyphoonInjections.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C857B8D2D0BAA5A8A764F9E1C4B85807 /* ViewController+MASAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 92B2A700D6653C0C3364645A2E691CC0 /* ViewController+MASAdditions.m */; }; C8B284A4946540AF76FF866F9F34CF67 /* RACValueTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = 89A79290D890BC1A7E558C5CC31E06E3 /* RACValueTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; }; C8EC35DFB0945DBE2F2FF9ECFE6D9711 /* NSLayoutConstraint+MASDebugAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 3DB7BACA057ADD843741CE2BDFDF62C8 /* NSLayoutConstraint+MASDebugAdditions.m */; }; C8FED80BA5A32BEA3BE00F5359153540 /* TyphoonStoryboardDefinition.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E2EED42BB0AD4DB8FDE62F4D4047113 /* TyphoonStoryboardDefinition.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; C93E972E75F84674690300123984EC43 /* SDAssociatedObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 252B49984FBEE0DBF2DD1314B8D9B974 /* SDAssociatedObject.m */; }; C96D04C53EB024F556E0F13422EED07A /* TyphoonTypeConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = 2E59442B228F5A36B8D5592A675C8879 /* TyphoonTypeConverter.h */; settings = {ATTRIBUTES = (Public, ); }; }; C9E19D164C26414115CC969ED9A303C1 /* MASLayoutConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = F867F81312FCE227A0925280E5C9263C /* MASLayoutConstraint.m */; }; CA1E0DCDF679EA2DE2ED0915426E1D04 /* SDWeakProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = D07B2E954DF932CCE682BD41F588F5BA /* SDWeakProxy.m */; }; CB6E35960C294DA751F679E953F4F14F /* RACEvent.h in Headers */ = {isa = PBXBuildFile; fileRef = CFCB3855AE45E56DBEA7833054EAD44F /* RACEvent.h */; settings = {ATTRIBUTES = (Public, ); }; }; CB7FD8FA81234BF9CC43527FE6260F62 /* RACReturnSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 229B6EF02232FF52DA22913B1FB01A1F /* RACReturnSignal.m */; }; CBAAE2A674699D3391C898EA00D0AA5C /* RACSubject.h in Headers */ = {isa = PBXBuildFile; fileRef = A28930506531365373875AD4CA53EDEC /* RACSubject.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC4630C63CAEBBA70AF10E01626A5D30 /* UIResponder+TyphoonOutletTransfer.h in Headers */ = {isa = PBXBuildFile; fileRef = D41D80BBE2F576947F11C3B0CDCDB170 /* UIResponder+TyphoonOutletTransfer.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD2D5C2B71BF29BC796199C6A4960927 /* NSObject+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 38F730D07E4C4D067A10C1C091A76D44 /* NSObject+OCMAdditions.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; CD7113EF56D2E1D58FC49C10330BD6BE /* OCLogTemplate.h in Headers */ = {isa = PBXBuildFile; fileRef = AAB807AFFC7C3B79795D0CA6C280CC13 /* OCLogTemplate.h */; settings = {ATTRIBUTES = (Public, ); }; }; CD923193903F98B2F7CD0354C2116417 /* TyphoonCircularDependencyTerminator.h in Headers */ = {isa = PBXBuildFile; fileRef = 0CB61254C5C3C9FFDE2C23A595DB093C /* TyphoonCircularDependencyTerminator.h */; settings = {ATTRIBUTES = (Public, ); }; }; CDB274EDA02524CBBF99EEE217AE4505 /* TyphoonOptionMatcher+Internal.h in Headers */ = {isa = PBXBuildFile; fileRef = 468798856EED0D94D6E982B4D271195A /* TyphoonOptionMatcher+Internal.h */; settings = {ATTRIBUTES = (Public, ); }; }; CE2CD6090ADB66ED21B0800C527D7B02 /* TyphoonInjectionByType.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CD424DC6ECC7B41C2BAB51078B5FB8E /* TyphoonInjectionByType.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; CF26C843968B8CBFF3931E82CA77FB81 /* RACKVOTrampoline.m in Sources */ = {isa = PBXBuildFile; fileRef = 871E83256238E28BEC109FBAA903A7BE /* RACKVOTrampoline.m */; }; CF2E818EBCD9D8AE88B891F33C042B46 /* TyphoonResource.h in Headers */ = {isa = PBXBuildFile; fileRef = ECBE53D68EFF65EB7F6020F1BE9C5B3B /* TyphoonResource.h */; settings = {ATTRIBUTES = (Public, ); }; }; CF96B3FEA3AEFCF6137DD7A22D92569E /* YYModel-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 6A1ED1A10CBE44A4F0190574BE33E602 /* YYModel-dummy.m */; }; CFF8D1A5E4C2097EF05E1021FE112886 /* SDWebImageIndicator.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D81FCE9D11F4054F824AE0AD9C905D9 /* SDWebImageIndicator.m */; }; D03A07BAF17F4E9CF1D2D3401AB1CF4B /* TyphoonStoryboard.m in Sources */ = {isa = PBXBuildFile; fileRef = A7EF786049567EB4166E1062885CCF21 /* TyphoonStoryboard.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; D06BB547D59D183FD1DDD84DEBAC9EE8 /* SDWebImageCacheSerializer.m in Sources */ = {isa = PBXBuildFile; fileRef = B96D3683B3E4B884909B07397CF43A5D /* SDWebImageCacheSerializer.m */; }; D0959AA30897EC5180B57C59FFA10849 /* OCMBoxedReturnValueProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = 1C87CAE8BB86399A29C7AAFACF0CD24D /* OCMBoxedReturnValueProvider.h */; settings = {ATTRIBUTES = (Project, ); }; }; D0A8F86C31CF8BE5C61785EF585EE6A2 /* RACDisposable.h in Headers */ = {isa = PBXBuildFile; fileRef = B9B7D7D1F5CDB012C8AEE5F8EF3F513D /* RACDisposable.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0DA377AA5FF23BEB89614E300E90D2D /* TyphoonMemoryManagementUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 9858D4D21F87468E116E7410167A453A /* TyphoonMemoryManagementUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; D0DD94C9BBAF8C653232930C3C3F2F73 /* NSObject+RACLifting.h in Headers */ = {isa = PBXBuildFile; fileRef = 5FAD9C753437FC1DCDDA7F7F6A8B9C29 /* NSObject+RACLifting.h */; settings = {ATTRIBUTES = (Public, ); }; }; D1D1BC57D17B127642A4A4DAFE50A033 /* NSInvocation+TCFUnwrapValues.m in Sources */ = {isa = PBXBuildFile; fileRef = C92A811BD719C9FA38BF1EB73409CA04 /* NSInvocation+TCFUnwrapValues.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; D1E674341E780EABFB25ECC1420B08A2 /* TyphoonBlockDefinition.h in Headers */ = {isa = PBXBuildFile; fileRef = 712ED30137ED21790E81D661BCDBE41A /* TyphoonBlockDefinition.h */; settings = {ATTRIBUTES = (Public, ); }; }; D287C76C50258D4C92FFDD2059168F45 /* RACSignal+Operations.h in Headers */ = {isa = PBXBuildFile; fileRef = 89570B4A4AE434196E38ABEADF0CB97E /* RACSignal+Operations.h */; settings = {ATTRIBUTES = (Public, ); }; }; D2AA8F17857AFD2BC5F384053D3FB619 /* NSObject+DeallocNotification.h in Headers */ = {isa = PBXBuildFile; fileRef = D179E0BB3A9462316E480E37EBCDDDF5 /* NSObject+DeallocNotification.h */; settings = {ATTRIBUTES = (Public, ); }; }; D2BB17CC67A51CF2396BEE012663441D /* TyphoonFactoryAutoInjectionPostProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = F06397AB33F7FA583810C154BB7C6BB2 /* TyphoonFactoryAutoInjectionPostProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; D2CD8848F856EC9942A76610AAE66F0A /* SDImageIOCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 996F286581DA660FC5CB9DCBB431AF08 /* SDImageIOCoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; D31E7D189E5A633875C9C723B5CCDECA /* TyphoonParameterInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = 13BDADC3DD9D34511EDA118822EA790E /* TyphoonParameterInjection.h */; settings = {ATTRIBUTES = (Public, ); }; }; D397F3D9E12E75959BB21782CBFD8755 /* UIAlertView+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B4C1BAAAE45FAB43E8ECC69C9DC35A7D /* UIAlertView+RACSignalSupport.m */; }; D3B29ED852413926E7B50A998517B1B5 /* TyphoonInjectionByObjectInstance.h in Headers */ = {isa = PBXBuildFile; fileRef = 83FC227B502F54235D6CCDB02449F168 /* TyphoonInjectionByObjectInstance.h */; settings = {ATTRIBUTES = (Public, ); }; }; D3BD1317C424DEBE3F14AA0F6B0F2D54 /* TyphoonInject.h in Headers */ = {isa = PBXBuildFile; fileRef = AA109A4B29ED94AC8CB0BB3D4626927E /* TyphoonInject.h */; settings = {ATTRIBUTES = (Public, ); }; }; D40F7E1DD6EA34E8F749A14B0CD9BE84 /* UIStepper+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 8B0A489E0C0397C62B216A44A21EDDFF /* UIStepper+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; D423726E8FA79619214170CEE328676A /* RACErrorSignal.m in Sources */ = {isa = PBXBuildFile; fileRef = 5ACE3A2A33422C2FD382351D2E96A005 /* RACErrorSignal.m */; }; D49896323350314AAEE3F22E1556C3D0 /* NSInvocation+TCFWrapValues.m in Sources */ = {isa = PBXBuildFile; fileRef = 69EDFB408AB8C8E85794472FD278D3F4 /* NSInvocation+TCFWrapValues.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; D51E06FD6284B91EAC91FD874C101ABF /* NSInvocation+TCFCustomImplementation.m in Sources */ = {isa = PBXBuildFile; fileRef = 032501D9A06A7D0265ED238BBC7D25E3 /* NSInvocation+TCFCustomImplementation.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; D55F6AFEF9DB4F474DACB788A627AAC9 /* UIView+TyphoonDefinitionKey.m in Sources */ = {isa = PBXBuildFile; fileRef = 1592F539A623B93799CF4145741CFC4E /* UIView+TyphoonDefinitionKey.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; D5A4DE2104B36BC4B8FD6E7E5FC30938 /* TyphoonWeakComponentsPool.h in Headers */ = {isa = PBXBuildFile; fileRef = 22EF63276DBF66ABD2D5FBF76746A708 /* TyphoonWeakComponentsPool.h */; settings = {ATTRIBUTES = (Public, ); }; }; D5F07F7462A9AD80E4A9DB21294D8FA2 /* UIGestureRecognizer+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 43D883AF4752701FF9AD2DF1CF845589 /* UIGestureRecognizer+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; D62A672EEB252581BD972DDA862BE1DD /* SDWebImage-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 0453653DE2FEC83ACFBA5BA70EBEF291 /* SDWebImage-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; D649EB2BE8D16B1BB89E41CAA09C1702 /* TyphoonBlockComponentFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 75D795D038F45CC15803AC36BBFEBF32 /* TyphoonBlockComponentFactory.h */; settings = {ATTRIBUTES = (Public, ); }; }; D662C83ECE8BEDA5FFB52F3575CA3E1A /* SDImageCache.h in Headers */ = {isa = PBXBuildFile; fileRef = B913D23BDDEDDE62A0E2C3493A0E97F5 /* SDImageCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; D663837F4347AF58660EE6F7FD426ECE /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; D788BA4B9E8186271BA75CA52B30502C /* View+MASAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = CDF6A89092579B8DC5007CD2E67B5C48 /* View+MASAdditions.m */; }; D7B3E8948DB04BD8FB6748419DA03EA9 /* SDAnimatedImageView+WebCache.h in Headers */ = {isa = PBXBuildFile; fileRef = 4F1747F10DA9479C6BDD54B652E1C545 /* SDAnimatedImageView+WebCache.h */; settings = {ATTRIBUTES = (Public, ); }; }; D7C50F17FD247E49530D1EF5D55EF4E5 /* OCMObserverRecorder.h in Headers */ = {isa = PBXBuildFile; fileRef = 0FC01820ACD24F5CF0969F06380ABFE9 /* OCMObserverRecorder.h */; settings = {ATTRIBUTES = (Project, ); }; }; D875F931575EA6C3308F17F716CC8748 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */; }; D8B4CB8F113156A6CD75C060D0113BE4 /* NSInvocation+RACTypeParsing.h in Headers */ = {isa = PBXBuildFile; fileRef = 9CB472E36DC6E2E3FEF5B6638CA34EC5 /* NSInvocation+RACTypeParsing.h */; settings = {ATTRIBUTES = (Public, ); }; }; D92622FB36407A6E91CB2509480A7F89 /* TyphoonPreattachedComponentsRegisterer.m in Sources */ = {isa = PBXBuildFile; fileRef = 7F2869DEA989F62C012853D92680ECEC /* TyphoonPreattachedComponentsRegisterer.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; D94824EC234BFBFF814D88F9DD042BEA /* OCMObserverRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = F4403468E084E9FF3F896A3734CD95FB /* OCMObserverRecorder.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; D968461E31E8FF3FF6BA1DC621B0433B /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C5460C93594FE49E0DF49CC9F802F1F3 /* UIKit.framework */; }; DB6EF4B473D70F75543FA047C10E1178 /* TyphoonFactoryPropertyInjectionPostProcessor.m in Sources */ = {isa = PBXBuildFile; fileRef = 90F23D124204BAE6E0746E042588193F /* TyphoonFactoryPropertyInjectionPostProcessor.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; DB7372889DFAB01A489C7A290AFFD9B2 /* TyphoonBundleResource.h in Headers */ = {isa = PBXBuildFile; fileRef = 94138F5AF19A8078AD96AA7CBA29AB4A /* TyphoonBundleResource.h */; settings = {ATTRIBUTES = (Public, ); }; }; DBA9500CBBA5FF6FCBBA115AE4D12152 /* NSLayoutConstraint+MASDebugAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 253F5F73A60CE9213B91A4027838C766 /* NSLayoutConstraint+MASDebugAdditions.h */; settings = {ATTRIBUTES = (Public, ); }; }; DBDC0A614556A03F8950731FCB0B2B92 /* TyphoonDefinition+Infrastructure.h in Headers */ = {isa = PBXBuildFile; fileRef = 23D6C0C2DA8003ECC644C3366B6E5247 /* TyphoonDefinition+Infrastructure.h */; settings = {ATTRIBUTES = (Public, ); }; }; DC2FDB5C74828AB18BA20E051DF0C37A /* TyphoonDefinitionNamespace.h in Headers */ = {isa = PBXBuildFile; fileRef = 1EE6F568285A1219683FB22F70790F2A /* TyphoonDefinitionNamespace.h */; settings = {ATTRIBUTES = (Public, ); }; }; DD256ACFDD49A35B9747419FFAB3A287 /* TyphoonAssemblyBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = 1466DD8719F4531C498D10CD285A8856 /* TyphoonAssemblyBuilder.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; DDA49C713DF9E0C1A9B1AD6A6F1DE610 /* TyphoonIntrospectionUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = 3D31CBCD57F3773D4DC8B94E19806A4E /* TyphoonIntrospectionUtils.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0 -DOS_OBJECT_USE_OBJC=0"; }; }; DE2553DFC19AAB617B63261D50C44091 /* UISlider+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 38A1E514232587E860EC29C335D4DC68 /* UISlider+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; DE5CB39DB19EE9A2F044B3AD41BCE22D /* Reachability.h in Headers */ = {isa = PBXBuildFile; fileRef = C51A574AFE5E5029BD52D9AC1C54C204 /* Reachability.h */; settings = {ATTRIBUTES = (Public, ); }; }; DEA09692CF813A23899CD4949A9B6801 /* SDAnimatedImageRep.h in Headers */ = {isa = PBXBuildFile; fileRef = BE52EEDB31E3B6435F83E7E317F3047D /* SDAnimatedImageRep.h */; settings = {ATTRIBUTES = (Public, ); }; }; DF1F2CC29D9871EF48FCD47055CB2A59 /* UIImagePickerController+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FFBE8455C54B78A685FE3CC5721FC439 /* UIImagePickerController+RACSignalSupport.m */; }; DF2B15402CE105F5A8CE48BBDCFFD5DD /* MASConstraint.m in Sources */ = {isa = PBXBuildFile; fileRef = 80E8E2470B296C3F1F4F779B79894FB3 /* MASConstraint.m */; }; DFC863A0F6A7902C35DC110A3CCBFE93 /* TyphoonCollaboratingAssemblyPropertyEnumerator.h in Headers */ = {isa = PBXBuildFile; fileRef = 572092422C1553EC0456C0BF53CDE8C2 /* TyphoonCollaboratingAssemblyPropertyEnumerator.h */; settings = {ATTRIBUTES = (Public, ); }; }; E075608ADB8A563BCA3D9F6C123220ED /* NSUserDefaults+RACSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = DAA9B1CEFE3B5F49253B19B6F13CBE00 /* NSUserDefaults+RACSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; E07A92468276197EE7EFACE1A4C7D456 /* OCMMacroState.m in Sources */ = {isa = PBXBuildFile; fileRef = 814AAFBE786E0EBCAFD7AC642DA8F8EF /* OCMMacroState.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; E0BCF21E9FA59F638C13ECCECC4D9690 /* SDMemoryCache.m in Sources */ = {isa = PBXBuildFile; fileRef = B534D8B84FDE7415642A1D01E3733910 /* SDMemoryCache.m */; }; E15319829D042A0E6BBE4005D44FACDA /* UIControl+RACSignalSupport.h in Headers */ = {isa = PBXBuildFile; fileRef = 707BB63597C3E112E47016299D9A2C40 /* UIControl+RACSignalSupport.h */; settings = {ATTRIBUTES = (Public, ); }; }; E1B4B95B63D486197DB87B5301B2AD68 /* TyphoonMethodSwizzler.h in Headers */ = {isa = PBXBuildFile; fileRef = 0D9725591DAD7A11FB7BCF2AA89B402B /* TyphoonMethodSwizzler.h */; settings = {ATTRIBUTES = (Public, ); }; }; E25B7F7ED64E459D6BA4E9FA1B812370 /* MKAnnotationView+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 53B86BED43D4B74CCE944876C11E57B1 /* MKAnnotationView+RACSignalSupport.m */; }; E3DB273F0E910956E9DE80D085901ECE /* TyphoonBundledImageTypeConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F05B9328BABBA34AFCE3172B61D1F83 /* TyphoonBundledImageTypeConverter.h */; settings = {ATTRIBUTES = (Public, ); }; }; E4E86D179162DB63010C0D8D68CF08FB /* TyphoonDefinition+Storyboard.m in Sources */ = {isa = PBXBuildFile; fileRef = 3E5BC7E6CAFF72A150840D4DC92D331D /* TyphoonDefinition+Storyboard.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; E4F1B478580D6D7328BC29607BDE46F6 /* UIImage+ExtendedCacheData.m in Sources */ = {isa = PBXBuildFile; fileRef = D1FE5339C49B5BC54F80CD72EDBB13EA /* UIImage+ExtendedCacheData.m */; }; E50613C67DD02AF6EA825DA0B31EFFAD /* SDImageGraphics.m in Sources */ = {isa = PBXBuildFile; fileRef = 7DF1AA147C3C7EA2E20CEC9444F9EDF2 /* SDImageGraphics.m */; }; E528D9C6017F3966EDE09AE63D49B22E /* TyphoonComponentFactory+Storyboard.h in Headers */ = {isa = PBXBuildFile; fileRef = 83F375B929B1442DA30D02F6A861124C /* TyphoonComponentFactory+Storyboard.h */; settings = {ATTRIBUTES = (Public, ); }; }; E58F464BF2649F7F2E4ED53CBFC5C466 /* OCMInvocationStub.h in Headers */ = {isa = PBXBuildFile; fileRef = B6C0AFCABBA43F288D26D6A9CA0DB2DA /* OCMInvocationStub.h */; settings = {ATTRIBUTES = (Project, ); }; }; E59439290BC7524578C0A0E9C06B7719 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = BCB7911698C2C8CF6B83F92DF10B11EF /* PrivacyInfo.xcprivacy */; }; E5AD74C206FCBD08FBBA2F359D787980 /* RACGroupedSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = 686988B863D5701A04411416EC7BF222 /* RACGroupedSignal.h */; settings = {ATTRIBUTES = (Public, ); }; }; E694501C0EFD2BDD91303F10CD877F31 /* TyphoonInjectionByComponentFactory.h in Headers */ = {isa = PBXBuildFile; fileRef = 0FFBFA01D44CE27E222D10DF44B1FBBB /* TyphoonInjectionByComponentFactory.h */; settings = {ATTRIBUTES = (Public, ); }; }; E76969F9B01139118427505B18F9CD21 /* SDImageHEICCoder.h in Headers */ = {isa = PBXBuildFile; fileRef = 5DD596766B816867DE3F36CD6A523540 /* SDImageHEICCoder.h */; settings = {ATTRIBUTES = (Public, ); }; }; E80BC1F3C11C00D429504583AEC7EB12 /* RACChannel.h in Headers */ = {isa = PBXBuildFile; fileRef = 246818BE8D07C34C5CC6113926619C5F /* RACChannel.h */; settings = {ATTRIBUTES = (Public, ); }; }; E81BE9935B2121C12CEC06480E77EA22 /* RACSubscriptingAssignmentTrampoline.h in Headers */ = {isa = PBXBuildFile; fileRef = 35811A877C1D60F1A3AB124ACBC29E9E /* RACSubscriptingAssignmentTrampoline.h */; settings = {ATTRIBUTES = (Public, ); }; }; E81F276ED056A35A0D7BB6CE9F9B26DC /* TyphoonAssemblyBuilder+PlistProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = AB7271D8A582657DF93DCFAD4F92FA7F /* TyphoonAssemblyBuilder+PlistProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; E87077FBDFB98FEFBCD2EE88AC4B62A4 /* OCMBlockArgCaller.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D066AE9F0D7A4328D8690DF15F20AA0 /* OCMBlockArgCaller.h */; settings = {ATTRIBUTES = (Project, ); }; }; E8AB529B9E0B4C23921344F6C4ABFEA4 /* SDImageCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = 82A6DCEB5EBCB25A5BF6375B4D2FE619 /* SDImageCoder.m */; }; E8F1A153D3063B75194913D03CA2D383 /* OCMockMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = EF447269480FB0580228D478883FFA69 /* OCMockMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; E930A5612DC6D120BE040AD17C6D1BCD /* MASViewAttribute.m in Sources */ = {isa = PBXBuildFile; fileRef = 2ADA8F4AE98D80692FA44970F3846684 /* MASViewAttribute.m */; }; E96F37E3D1E77EB2349FF06BD82E95E9 /* RACEagerSequence.m in Sources */ = {isa = PBXBuildFile; fileRef = DB54DF3DB0FDADB1A3893CFF8C1AF03A /* RACEagerSequence.m */; }; E99D0FDFED6B9B6CE4F1373A8D4CEE03 /* RACSubscriber+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 2AE9080EF975331395E265C09E03252C /* RACSubscriber+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; E9E16E749FD6121557C8D9E648A8C6AB /* RACDelegateProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = F8217C098AD84E361BF0AFBB60065247 /* RACDelegateProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; EA82B6D97C9C5D0558047AF552D63203 /* SDWebImageDefine.h in Headers */ = {isa = PBXBuildFile; fileRef = 702F65559FA8A65C2C9E8A7280D4E423 /* SDWebImageDefine.h */; settings = {ATTRIBUTES = (Public, ); }; }; EB27DEF6CC7E1606E358166B56931C05 /* NSObject+YYModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 7CC392EF120EE41995D6918D82EC742B /* NSObject+YYModel.m */; }; EB2B34DC4B6CA9FDEAF8EB5964D4EE3B /* OCMIndirectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 9752AAF7F49C7CFA8636AB119BF88C35 /* OCMIndirectReturnValueProvider.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; EC9B34262AED632D7EFB49804337648E /* Masonry.h in Headers */ = {isa = PBXBuildFile; fileRef = A20BC1B3BD8F0D819039127EE5346176 /* Masonry.h */; settings = {ATTRIBUTES = (Public, ); }; }; ECE64B732F9FA7C402DDEEC58DCB9D98 /* SDImageAPNGCoder.m in Sources */ = {isa = PBXBuildFile; fileRef = B67521AEB3057A6B1FDF7908EB18E894 /* SDImageAPNGCoder.m */; }; ED8F64FF98CFAE0B12CF60A1B0E6BAF8 /* SDCallbackQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = A66DC1E844041574BF00E1D8870DE753 /* SDCallbackQueue.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE19A56BB25AE31295A4D913C339FF02 /* TyphoonDefinition+Option.h in Headers */ = {isa = PBXBuildFile; fileRef = A146863CEB210DB61D7A021374982754 /* TyphoonDefinition+Option.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE77A716DB37472592376BD26C7DDD78 /* TyphoonGlobalConfigCollector.h in Headers */ = {isa = PBXBuildFile; fileRef = 875B63A814EBB6CC52061388F53BC97D /* TyphoonGlobalConfigCollector.h */; settings = {ATTRIBUTES = (Public, ); }; }; EE8AE70BAD73F5BE3689FA838B23AFB7 /* OCObserverMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 36EA9831D45C8C2DBBE9C8924552B0F3 /* OCObserverMockObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; EEA86BBF0385C53CE0567D92C87C5E8B /* TyphoonDefinition+InstanceBuilder.h in Headers */ = {isa = PBXBuildFile; fileRef = C4FBBF6C2C66E9E078C222133056807B /* TyphoonDefinition+InstanceBuilder.h */; settings = {ATTRIBUTES = (Public, ); }; }; EF6A6C725598F572A70C5FCEE328C184 /* SDImageTransformer.h in Headers */ = {isa = PBXBuildFile; fileRef = 83044A60173A4F57FC96702D95EF4DB8 /* SDImageTransformer.h */; settings = {ATTRIBUTES = (Public, ); }; }; F0A541AAA195CE1E0DB03048E621CC0D /* OCMNonRetainingObjectReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 357902381BACEC332590C847106F5C16 /* OCMNonRetainingObjectReturnValueProvider.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; F1454467D8C33378127057476591C1AF /* RACDynamicSequence.m in Sources */ = {isa = PBXBuildFile; fileRef = 15CD57574E2617CB7558B0BD733BAFC2 /* RACDynamicSequence.m */; }; F16BF2164AA7A01C3C46E80BBEBB14AE /* NSValue+OCMAdditions.h in Headers */ = {isa = PBXBuildFile; fileRef = 1D3495A662B6322B041706885A769B02 /* NSValue+OCMAdditions.h */; settings = {ATTRIBUTES = (Project, ); }; }; F18AB133D2D0F40618D0A4C87D3AF0BF /* RACBlockTrampoline.h in Headers */ = {isa = PBXBuildFile; fileRef = 25FCBE4E8D3998433D6BFA94FB2FAC69 /* RACBlockTrampoline.h */; settings = {ATTRIBUTES = (Public, ); }; }; F19558DCCE275BD72B143F8ACE45E7E0 /* OCMockObject.m in Sources */ = {isa = PBXBuildFile; fileRef = 8375DB2B7CC0F5AD235F69805AF4B1BA /* OCMockObject.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; F2DAC1DEF200EB8CBBF570272D86CA51 /* NSInvocation+RACTypeParsing.m in Sources */ = {isa = PBXBuildFile; fileRef = FB44908A42BD6D708EAD4C6F7B6F3B5A /* NSInvocation+RACTypeParsing.m */; }; F341B1F0FEDE67D2DB40B5C5D854DA8A /* RACEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 861A84C50B238264389CE8E34734EE4C /* RACEvent.m */; }; F3604694FDE5476E5AA37DFDB131C7C8 /* NSNotificationCenter+RACSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 0DBB238DBD9086C4A06C98A1C750EA43 /* NSNotificationCenter+RACSupport.m */; }; F3F7651923711E6A4A12FBE3B5DDCC4E /* NSString+RACKeyPathUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = FAD1079497F1714CF45F8A6BF851636D /* NSString+RACKeyPathUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; F40E7796D3A7339E16757A9B9A6A46E7 /* TyphoonTypeConverterRegistry.m in Sources */ = {isa = PBXBuildFile; fileRef = 906070901EFA9ECD175C85348FF57C1E /* TyphoonTypeConverterRegistry.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; F432C2EF2FDAE1EA39A56AA772932A38 /* NSInvocation+OCMAdditions.m in Sources */ = {isa = PBXBuildFile; fileRef = 47E16F28A08ED8AA345A07CC51EAF129 /* NSInvocation+OCMAdditions.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; F465B45E09EC9F7F08BAC6735CD7124D /* NSObject+PropertyInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = ADFAD562310FEB485867BE6D63C82804 /* NSObject+PropertyInjection.h */; settings = {ATTRIBUTES = (Public, ); }; }; F49CB22863CCFEC7817D259F27F91C57 /* SDWebImageIndicator.h in Headers */ = {isa = PBXBuildFile; fileRef = B478DF0CC09DF6605F4772947370CBE1 /* SDWebImageIndicator.h */; settings = {ATTRIBUTES = (Public, ); }; }; F5372060DD1B80D1DAB5ABA234F40C66 /* TyphoonStoryboardProvider.h in Headers */ = {isa = PBXBuildFile; fileRef = AC040A20E5DFBE7A6827B92595B02C58 /* TyphoonStoryboardProvider.h */; settings = {ATTRIBUTES = (Public, ); }; }; F53BE4449AE5896F76325E4DCB6D0B13 /* SDImageCachesManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 5F41C49350454308C011F60E41C91F54 /* SDImageCachesManager.h */; settings = {ATTRIBUTES = (Public, ); }; }; F59065DC32765AD32444CEAE1B1E8250 /* TyphoonUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 94E380F27B3CBBA506348C94A1707E2E /* TyphoonUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; F5A6806DA01AEDEA925EF7F6F42A1BA5 /* NSFileHandle+RACSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = B54343D5BDC4CA5AB85FD35468187848 /* NSFileHandle+RACSupport.m */; }; F5D147CA9C21670C9C509D0C40C55015 /* OCMArgAction.h in Headers */ = {isa = PBXBuildFile; fileRef = F5CC2EA5805079A0B25919E89EAB7C84 /* OCMArgAction.h */; settings = {ATTRIBUTES = (Project, ); }; }; F5E9B7AD93F258223E150D81EC68291A /* OCMExceptionReturnValueProvider.m in Sources */ = {isa = PBXBuildFile; fileRef = 1E25BC9F9B2C745B6471F74CCB47DC7F /* OCMExceptionReturnValueProvider.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; F68889CD481716EE5D6B75EBD8FD53A6 /* SDImageCoderHelper.h in Headers */ = {isa = PBXBuildFile; fileRef = F4CE8A12723ABBCC55A89EB52BEB7E84 /* SDImageCoderHelper.h */; settings = {ATTRIBUTES = (Public, ); }; }; F6D1C960368EB1E067ABD0BFF707FC56 /* MASConstraintMaker.m in Sources */ = {isa = PBXBuildFile; fileRef = 838C51303B89542DD2D308D0C408F616 /* MASConstraintMaker.m */; }; F775F679F321E234A469F54310116E6D /* RACValueTransformer.m in Sources */ = {isa = PBXBuildFile; fileRef = 28B829B34223ADB0F04E28F63DEA2868 /* RACValueTransformer.m */; }; F7CA711927E14F963F5E47EEBC79EE62 /* RACKVOProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = CA677B07FC524F62109AE180874AB60E /* RACKVOProxy.h */; settings = {ATTRIBUTES = (Public, ); }; }; F84821217598572DCD6377D7E5AF7D27 /* RACSignal.h in Headers */ = {isa = PBXBuildFile; fileRef = 2C69DF26B88BA2D74723697231D11733 /* RACSignal.h */; settings = {ATTRIBUTES = (Public, ); }; }; F8828C0B5037617E7C5EAB0F6499302E /* TyphoonInjectedObject.h in Headers */ = {isa = PBXBuildFile; fileRef = 35D424AE41D9D5DF8B4CD98EE40E20E2 /* TyphoonInjectedObject.h */; settings = {ATTRIBUTES = (Public, ); }; }; F99A8C6F169EBF7236DA9FD6A89A4CAD /* TyphoonPassThroughTypeConverter.h in Headers */ = {isa = PBXBuildFile; fileRef = AF03A2059F417E8AEC2BCF0FFB758649 /* TyphoonPassThroughTypeConverter.h */; settings = {ATTRIBUTES = (Public, ); }; }; F9FE542AFAA108FFD4F546ADBF6690F1 /* NSNullTypeConverter.m in Sources */ = {isa = PBXBuildFile; fileRef = BDDDFCB98FA96E2B25144D59DAD84EF4 /* NSNullTypeConverter.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; FA3021DED76B9B182CC9195A60EB1209 /* NSBezierPath+SDRoundedCorners.m in Sources */ = {isa = PBXBuildFile; fileRef = D68AF173A5494072218D36ADB175CC67 /* NSBezierPath+SDRoundedCorners.m */; }; FA4D59453FAEF4399232AB1CF9ABDDBD /* RACQueueScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = EC243AD4E47E5CAA863DF364A86CA635 /* RACQueueScheduler.m */; }; FA97AAE3347E765BA649A2587F6A7BBC /* TyphoonIntrospectionUtils.h in Headers */ = {isa = PBXBuildFile; fileRef = 0C08AB40F42970663D76869A9E5E9143 /* TyphoonIntrospectionUtils.h */; settings = {ATTRIBUTES = (Public, ); }; }; FB1D858F09AAF47F96795E1AEAC09A19 /* UIControl+RACSignalSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = 8F1A6A9C14EDD26D5A9D9CE5506A3971 /* UIControl+RACSignalSupport.m */; }; FBCE4760C09733E7FBE20D075564D30F /* NSInvocation+TCFInstanceBuilder.m in Sources */ = {isa = PBXBuildFile; fileRef = FD27FFD79501082E67620A5F5585D2D0 /* NSInvocation+TCFInstanceBuilder.m */; settings = {COMPILER_FLAGS = "-fno-objc-arc"; }; }; FC0726705C4A0C4073F97C70C7045539 /* TyphoonRuntimeArguments.h in Headers */ = {isa = PBXBuildFile; fileRef = 88024AA70385D01B3432E99546EFA4E1 /* TyphoonRuntimeArguments.h */; settings = {ATTRIBUTES = (Public, ); }; }; FC16BB1D601656C4C5F0B8B2D2D38A65 /* RACTuple.h in Headers */ = {isa = PBXBuildFile; fileRef = AB7F2ED32F5F65686BE8BE48409DB271 /* RACTuple.h */; settings = {ATTRIBUTES = (Public, ); }; }; FCDEC6A53CF5517E1AF5B331FD65F6D9 /* SDImageCacheConfig.m in Sources */ = {isa = PBXBuildFile; fileRef = B2E5AA16D441D1E41E9953B3FD0CE552 /* SDImageCacheConfig.m */; }; FCEE5BD645E95FF55468C4AB6D17CFDA /* UIImageView+HighlightedWebCache.m in Sources */ = {isa = PBXBuildFile; fileRef = 45AF458D3D9B4CF7A071A8AA27745113 /* UIImageView+HighlightedWebCache.m */; }; FD9BA74BACBA3F59304DDF3D5BCF6119 /* RACArraySequence.h in Headers */ = {isa = PBXBuildFile; fileRef = 9754B128A1DF8F57658751C1302FF46F /* RACArraySequence.h */; settings = {ATTRIBUTES = (Public, ); }; }; FE314B3711980149B7078A3606250317 /* TyphoonComponentFactory+Storyboard.m in Sources */ = {isa = PBXBuildFile; fileRef = 4065878CE75B090253FC030941E3DC2D /* TyphoonComponentFactory+Storyboard.m */; settings = {COMPILER_FLAGS = "-DOS_OBJECT_USE_OBJC=0"; }; }; FEA8BA4F82CCBD1D28DCC7EF39FB4096 /* SDImageCacheDefine.m in Sources */ = {isa = PBXBuildFile; fileRef = 4290D47244C9D83C19300F3FE4CA8170 /* SDImageCacheDefine.m */; }; FF266014693FE86BF83B9FD19AFCA071 /* TyphoonAbstractInjection.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C06ABD459BD9D2EC039A4278068AC2B /* TyphoonAbstractInjection.h */; settings = {ATTRIBUTES = (Public, ); }; }; FF298EDEF3741C40A1AB8FB9EE00CD2F /* RACMulticastConnection.h in Headers */ = {isa = PBXBuildFile; fileRef = 66D7BE61D87C0C7666DBE752021D186B /* RACMulticastConnection.h */; settings = {ATTRIBUTES = (Public, ); }; }; FF4E53497AD777F441D3CEF6355A4258 /* TyphoonDefinitionPostProcessor.h in Headers */ = {isa = PBXBuildFile; fileRef = C529B758067B01A288574A93D2868C42 /* TyphoonDefinitionPostProcessor.h */; settings = {ATTRIBUTES = (Public, ); }; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 5AE1D14F60B6E1194AEB2341B78F3262 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = C260B5A26D3CD54F215E5E39371483B6; remoteInfo = OCMock; }; 74C78EC43AE66E5D1A6C66170C1C1D61 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 94CFBA7D633ECA58DF85C327B035E6A3; remoteInfo = "SDWebImage-SDWebImage"; }; 76D528C13A764727FA8A429BB8ABC642 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 6D7BC7A068E56396BBE1DECFD19DDDEB; remoteInfo = Typhoon; }; 9B705E4CA2FCD950E81AB005C7565BE0 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 84B44807A12996D487A4A591A481D6A0; remoteInfo = YYModel; }; 9D64C0FC477F71B3B5C8D7DC509C043A /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 438B238ACC7DF1178D1BCE1A31983146; remoteInfo = ReactiveObjC; }; A26244063922B6BC1F46487F12D1016C /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 55AF53E6C77A10ED4985E04D74A8878E; remoteInfo = Masonry; }; B1BB2C6EA80EE90466DC3C902622C7B1 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = F1BCD9702276377FB5B3BDB6EAF709D7; remoteInfo = DZNEmptyDataSet; }; C2CBEAA478071DF837FB759F6C115BF3 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 3847153A6E5EEFB86565BA840768F429; remoteInfo = SDWebImage; }; C98ED4A91B240BEF49E26C808D0A8F3D /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 630AEC5FE2D10DE4AF88009DC6EC819A; remoteInfo = "Pods-iOS-Network-Stack-Dive"; }; E08BBA5114D60374EFEC60DCE34CDCFC /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = 6083682834ABE0AE7BD1CBF06CADD036; remoteInfo = CocoaAsyncSocket; }; F948F43B3C4BC87A483963360B1C0612 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; proxyType = 1; remoteGlobalIDString = CAA047C0F5E4106F3904E8497FA17F97; remoteInfo = Reachability; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 000F69D9699B233904DC78740C74688D /* SDInternalMacros.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDInternalMacros.h; path = SDWebImage/Private/SDInternalMacros.h; sourceTree = ""; }; 004DB416A9430FE442A136525FC3D93D /* TyphoonStoryboardResolver.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonStoryboardResolver.m; path = Source/ios/Storyboard/TyphoonStoryboardResolver.m; sourceTree = ""; }; 02580255476480314DC90D373EF87D94 /* UITextView+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UITextView+RACSignalSupport.h"; path = "ReactiveObjC/UITextView+RACSignalSupport.h"; sourceTree = ""; }; 026AD1F39E6F56A1374F84466603518D /* UIImage+Transform.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImage+Transform.m"; path = "SDWebImage/Core/UIImage+Transform.m"; sourceTree = ""; }; 027098AE8C5EB84C31AACA284246D555 /* RACScheduler.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACScheduler.m; path = ReactiveObjC/RACScheduler.m; sourceTree = ""; }; 02DBA7718F60FA5F5208D3C92DD2FAE1 /* TyphoonViewHelpers.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonViewHelpers.h; path = Source/ios/Storyboard/TyphoonViewHelpers.h; sourceTree = ""; }; 032501D9A06A7D0265ED238BBC7D25E3 /* NSInvocation+TCFCustomImplementation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSInvocation+TCFCustomImplementation.m"; path = "Source/Factory/Internal/NSInvocation+TCFCustomImplementation.m"; sourceTree = ""; }; 032C0CB079DA640D62F451CF2BB09DBF /* NSObject+RACLifting.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+RACLifting.m"; path = "ReactiveObjC/NSObject+RACLifting.m"; sourceTree = ""; }; 041C0D0F13831619F5479D99056A302A /* NSArray+MASShorthandAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSArray+MASShorthandAdditions.h"; path = "Masonry/NSArray+MASShorthandAdditions.h"; sourceTree = ""; }; 0453653DE2FEC83ACFBA5BA70EBEF291 /* SDWebImage-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SDWebImage-umbrella.h"; sourceTree = ""; }; 04CD8685467C1425C560A1E9C0AA43B7 /* TyphoonCircularDependencyTerminator.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonCircularDependencyTerminator.m; path = Source/Factory/Internal/TyphoonCircularDependencyTerminator.m; sourceTree = ""; }; 050CAE5EF6AFDE7FE32CEE7E030CDD0E /* TyphoonInjectionByComponentFactory.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByComponentFactory.m; path = Source/Definition/Injections/TyphoonInjectionByComponentFactory.m; sourceTree = ""; }; 0571A2C7BE7E0945F4212CD552F88BE1 /* SDAnimatedImageRep.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDAnimatedImageRep.m; path = SDWebImage/Core/SDAnimatedImageRep.m; sourceTree = ""; }; 05C311CA83E10EC09FC2A8CAAF090A30 /* TyphoonComponentFactory+InstanceBuilder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "TyphoonComponentFactory+InstanceBuilder.m"; path = "Source/Factory/Internal/TyphoonComponentFactory+InstanceBuilder.m"; sourceTree = ""; }; 065E52ED2D30B3B331A00D8EB564E4D8 /* TyphoonColorConversionUtils.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonColorConversionUtils.m; path = Source/TypeConversion/Helpers/TyphoonColorConversionUtils.m; sourceTree = ""; }; 06AEB99F8D2F4D764FE818A7D523C570 /* RACCompoundDisposable.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACCompoundDisposable.m; path = ReactiveObjC/RACCompoundDisposable.m; sourceTree = ""; }; 06B502FEF6AC1E2D1DECEF63D08CC04F /* TyphoonTypeConversionUtils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonTypeConversionUtils.h; path = Source/TypeConversion/TyphoonTypeConversionUtils.h; sourceTree = ""; }; 06DAA5ED399AAB042406902A676EED50 /* NSString+RACSequenceAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+RACSequenceAdditions.h"; path = "ReactiveObjC/NSString+RACSequenceAdditions.h"; sourceTree = ""; }; 06E2166895FA1466EACF8506DAA383DB /* NSNotificationCenter+OCMAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSNotificationCenter+OCMAdditions.m"; path = "Source/OCMock/NSNotificationCenter+OCMAdditions.m"; sourceTree = ""; }; 073742952E39DC51EF6052E0131E74C6 /* ReactiveObjC.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = ReactiveObjC.modulemap; sourceTree = ""; }; 073BF9A27C7F924FC6A5CF2E935DDDA8 /* SDImageTransformer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageTransformer.m; path = SDWebImage/Core/SDImageTransformer.m; sourceTree = ""; }; 0845491EC6BC3A5BFF1808F961A5440F /* SDAnimatedImageView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDAnimatedImageView.h; path = SDWebImage/Core/SDAnimatedImageView.h; sourceTree = ""; }; 0851ED90E0FAF2464FCE1AB36209BB60 /* SDImageLoadersManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageLoadersManager.h; path = SDWebImage/Core/SDImageLoadersManager.h; sourceTree = ""; }; 08703C437757C9668B19065994EA7EBD /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; 08856CD53ABCA9A62737A9BB60FD3496 /* TyphoonOrdered.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonOrdered.h; path = Source/Configuration/TyphoonOrdered.h; sourceTree = ""; }; 08B6FA0A27CA67CF82F94F768E83FBBD /* TyphoonMethod.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonMethod.h; path = Source/Definition/Method/TyphoonMethod.h; sourceTree = ""; }; 0A11CE004B91D55C90C201FD9902D9A8 /* Pods-iOS-Network-Stack-Dive.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iOS-Network-Stack-Dive.release.xcconfig"; sourceTree = ""; }; 0A69C05290E3D199301DB5557505AEEB /* NSOrderedSet+RACSequenceAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSOrderedSet+RACSequenceAdditions.h"; path = "ReactiveObjC/NSOrderedSet+RACSequenceAdditions.h"; sourceTree = ""; }; 0AE0533E46155704FC6E99D900ECFF97 /* SDImageGIFCoder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageGIFCoder.h; path = SDWebImage/Core/SDImageGIFCoder.h; sourceTree = ""; }; 0B107CBC10206BACB98A3A6524024891 /* Pods-iOS-Network-Stack-Dive-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-iOS-Network-Stack-Dive-frameworks.sh"; sourceTree = ""; }; 0B2C8AD692D98D47C5658A9FE6A6DE4F /* RACScheduler+Private.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "RACScheduler+Private.h"; path = "ReactiveObjC/RACScheduler+Private.h"; sourceTree = ""; }; 0B91A0326C0D75CA3ED540C456F99FA7 /* RACTargetQueueScheduler.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACTargetQueueScheduler.m; path = ReactiveObjC/RACTargetQueueScheduler.m; sourceTree = ""; }; 0BB3D4EE11AB02C35A56E26CD538EA8D /* RACSerialDisposable.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACSerialDisposable.m; path = ReactiveObjC/RACSerialDisposable.m; sourceTree = ""; }; 0C08AB40F42970663D76869A9E5E9143 /* TyphoonIntrospectionUtils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonIntrospectionUtils.h; path = Source/Utils/TyphoonIntrospectionUtils.h; sourceTree = ""; }; 0C9B76DC6B005BE0C06B737507138A3E /* SDGraphicsImageRenderer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDGraphicsImageRenderer.m; path = SDWebImage/Core/SDGraphicsImageRenderer.m; sourceTree = ""; }; 0CB61254C5C3C9FFDE2C23A595DB093C /* TyphoonCircularDependencyTerminator.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonCircularDependencyTerminator.h; path = Source/Factory/Internal/TyphoonCircularDependencyTerminator.h; sourceTree = ""; }; 0CEAF4D7D00B3A496611716400E24A3C /* NSData+RACSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSData+RACSupport.m"; path = "ReactiveObjC/NSData+RACSupport.m"; sourceTree = ""; }; 0D5BC3205064C311F17DDCAF73FE5D91 /* OCMLocation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMLocation.m; path = Source/OCMock/OCMLocation.m; sourceTree = ""; }; 0D9725591DAD7A11FB7BCF2AA89B402B /* TyphoonMethodSwizzler.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonMethodSwizzler.h; path = Source/Utils/Swizzle/TyphoonMethodSwizzler.h; sourceTree = ""; }; 0DBB238DBD9086C4A06C98A1C750EA43 /* NSNotificationCenter+RACSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSNotificationCenter+RACSupport.m"; path = "ReactiveObjC/NSNotificationCenter+RACSupport.m"; sourceTree = ""; }; 0E69DD9AA22CCF06F1224BEB3E20EDD3 /* RACDynamicSequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACDynamicSequence.h; path = ReactiveObjC/RACDynamicSequence.h; sourceTree = ""; }; 0EA9D6ACAA4596DDBF52BD0B1662EB44 /* NSString+RACKeyPathUtilities.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSString+RACKeyPathUtilities.m"; path = "ReactiveObjC/NSString+RACKeyPathUtilities.m"; sourceTree = ""; }; 0EB0DF190C36C4090D74F64BB54053A6 /* OCMFunctions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMFunctions.h; path = Source/OCMock/OCMFunctions.h; sourceTree = ""; }; 0ED44D2F4F4F552521A65726C787244A /* ViewController+MASAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "ViewController+MASAdditions.h"; path = "Masonry/ViewController+MASAdditions.h"; sourceTree = ""; }; 0EF30728B0B55C95AD32A7BC318AC70A /* SDAnimatedImage.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDAnimatedImage.h; path = SDWebImage/Core/SDAnimatedImage.h; sourceTree = ""; }; 0F07E5AA6AC318EED47D4865477C9372 /* NSButton+WebCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSButton+WebCache.m"; path = "SDWebImage/Core/NSButton+WebCache.m"; sourceTree = ""; }; 0F3AD4FDCF5DACA1727D2EDD1CDAA03F /* TyphoonInjections.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjections.h; path = Source/Definition/Injections/TyphoonInjections.h; sourceTree = ""; }; 0F47471F1705425CC58FB3536B619557 /* SDWebImageOperation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageOperation.m; path = SDWebImage/Core/SDWebImageOperation.m; sourceTree = ""; }; 0FC01820ACD24F5CF0969F06380ABFE9 /* OCMObserverRecorder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMObserverRecorder.h; path = Source/OCMock/OCMObserverRecorder.h; sourceTree = ""; }; 0FFBFA01D44CE27E222D10DF44B1FBBB /* TyphoonInjectionByComponentFactory.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByComponentFactory.h; path = Source/Definition/Injections/TyphoonInjectionByComponentFactory.h; sourceTree = ""; }; 10396C86F2BA403403FECC4E3914CF31 /* GCDAsyncUdpSocket.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GCDAsyncUdpSocket.h; path = Source/GCD/GCDAsyncUdpSocket.h; sourceTree = ""; }; 1051F60A0834845519249BBEEB7669EF /* Pods-iOS-Network-Stack-DiveTests-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-iOS-Network-Stack-DiveTests-umbrella.h"; sourceTree = ""; }; 1061DB1631D08DF649DC362D1104033F /* TyphoonInstancePostProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInstancePostProcessor.h; path = Source/Configuration/TyphoonInstancePostProcessor.h; sourceTree = ""; }; 1083669FB5354500A46F16186007AE49 /* UIImage+ForceDecode.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ForceDecode.m"; path = "SDWebImage/Core/UIImage+ForceDecode.m"; sourceTree = ""; }; 10AF2F5387AC99FDF1EE38CC6AF81A5A /* NSValue+TCFUnwrapValues.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSValue+TCFUnwrapValues.h"; path = "Source/Factory/Internal/NSValue+TCFUnwrapValues.h"; sourceTree = ""; }; 115C7117F68E39901C8DF436A5193C71 /* RACTestScheduler.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACTestScheduler.m; path = ReactiveObjC/RACTestScheduler.m; sourceTree = ""; }; 11CA0DD35189B363533FA7DEBEAFC7E5 /* NSSet+RACSequenceAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSSet+RACSequenceAdditions.m"; path = "ReactiveObjC/NSSet+RACSequenceAdditions.m"; sourceTree = ""; }; 12676821542EF4B94B6913AF583A7A69 /* TyphoonJsonStyleConfiguration.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonJsonStyleConfiguration.m; path = Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonJsonStyleConfiguration.m; sourceTree = ""; }; 1291349B3C35F79CB6F631FD1CDCC1B0 /* RACSubscriber.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACSubscriber.m; path = ReactiveObjC/RACSubscriber.m; sourceTree = ""; }; 12EC94936DE4F7A5029D489A3E4FB32E /* NSDictionary+CustomInjection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+CustomInjection.h"; path = "Source/Definition/Internal/NSDictionary+CustomInjection.h"; sourceTree = ""; }; 1343632E0E45C40E0DBA77566514A62A /* SDImageCachesManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageCachesManager.m; path = SDWebImage/Core/SDImageCachesManager.m; sourceTree = ""; }; 13B5BE064B370CA187BEE420A188E41F /* CocoaAsyncSocket.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = CocoaAsyncSocket.modulemap; sourceTree = ""; }; 13BDADC3DD9D34511EDA118822EA790E /* TyphoonParameterInjection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonParameterInjection.h; path = Source/Definition/Injections/TyphoonParameterInjection.h; sourceTree = ""; }; 13D762E3256505E91BAEA28CEAA5F68F /* TyphoonNSNumberTypeConverter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonNSNumberTypeConverter.h; path = Source/TypeConversion/Converters/TyphoonNSNumberTypeConverter.h; sourceTree = ""; }; 141EB3C3A6A1510AA72DD2C9ACD6894C /* RACReplaySubject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACReplaySubject.h; path = ReactiveObjC/RACReplaySubject.h; sourceTree = ""; }; 1428E07DEC90DAD4F228BB35096D20BF /* UITextField+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UITextField+RACSignalSupport.m"; path = "ReactiveObjC/UITextField+RACSignalSupport.m"; sourceTree = ""; }; 143606750438523F029FB6DA44890C07 /* RACSubscriber.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACSubscriber.h; path = ReactiveObjC/RACSubscriber.h; sourceTree = ""; }; 1443B87E860FFFC4733D9B16073E19F9 /* RACImmediateScheduler.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACImmediateScheduler.h; path = ReactiveObjC/RACImmediateScheduler.h; sourceTree = ""; }; 1466DD8719F4531C498D10CD285A8856 /* TyphoonAssemblyBuilder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonAssemblyBuilder.m; path = Source/Factory/Internal/TyphoonAssemblyBuilder.m; sourceTree = ""; }; 146B7F55E58BD9E7C1CB0B3944357F9C /* SDWebImageCacheKeyFilter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageCacheKeyFilter.m; path = SDWebImage/Core/SDWebImageCacheKeyFilter.m; sourceTree = ""; }; 14752B57BED25D3E2474FA3AFEB5ECD7 /* NSIndexSet+RACSequenceAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSIndexSet+RACSequenceAdditions.m"; path = "ReactiveObjC/NSIndexSet+RACSequenceAdditions.m"; sourceTree = ""; }; 1498864CCF5B387AE86A3D8C74065926 /* TyphoonBlockDefinition+Internal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonBlockDefinition+Internal.h"; path = "Source/Definition/Internal/TyphoonBlockDefinition+Internal.h"; sourceTree = ""; }; 14BA0C92570BFCBF28C82F32F37A96B6 /* OCMInvocationMatcher.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMInvocationMatcher.m; path = Source/OCMock/OCMInvocationMatcher.m; sourceTree = ""; }; 14EDB30D06B6BB66F89E7DC87C6F82CE /* OCMBlockCaller.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMBlockCaller.h; path = Source/OCMock/OCMBlockCaller.h; sourceTree = ""; }; 1592F539A623B93799CF4145741CFC4E /* UIView+TyphoonDefinitionKey.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIView+TyphoonDefinitionKey.m"; path = "Source/ios/Storyboard/Internal/UIView+TyphoonDefinitionKey.m"; sourceTree = ""; }; 15B1114AA6F4793852697F0F588E7204 /* TyphoonBundleResource.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonBundleResource.m; path = Source/Configuration/Resource/TyphoonBundleResource.m; sourceTree = ""; }; 15C365C1912A0FB488CECEF9950319ED /* SDImageAWebPCoder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageAWebPCoder.m; path = SDWebImage/Core/SDImageAWebPCoder.m; sourceTree = ""; }; 15CD57574E2617CB7558B0BD733BAFC2 /* RACDynamicSequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACDynamicSequence.m; path = ReactiveObjC/RACDynamicSequence.m; sourceTree = ""; }; 15DBB333129764191B36CFF037F62681 /* SDImageIOAnimatedCoder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageIOAnimatedCoder.m; path = SDWebImage/Core/SDImageIOAnimatedCoder.m; sourceTree = ""; }; 1610B6558D631E16BFCF0FD099283BDA /* OCMBlockArgCaller.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMBlockArgCaller.m; path = Source/OCMock/OCMBlockArgCaller.m; sourceTree = ""; }; 162947A4139E0A2C2197D6A479D07445 /* UIDatePicker+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIDatePicker+RACSignalSupport.h"; path = "ReactiveObjC/UIDatePicker+RACSignalSupport.h"; sourceTree = ""; }; 16BEA40224A6DCCBB7E983D78F3CE027 /* RACBehaviorSubject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACBehaviorSubject.h; path = ReactiveObjC/RACBehaviorSubject.h; sourceTree = ""; }; 17092355C96C3E633D28031E208BC50C /* UITextField+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UITextField+RACSignalSupport.h"; path = "ReactiveObjC/UITextField+RACSignalSupport.h"; sourceTree = ""; }; 17B387B9415B8C98C3579629FB677E97 /* TyphoonSelector.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonSelector.h; path = Source/Utils/TyphoonSelector.h; sourceTree = ""; }; 183FD7641AE552C24F25EC96C491FCBF /* RACScheduler+Subclass.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "RACScheduler+Subclass.h"; path = "ReactiveObjC/RACScheduler+Subclass.h"; sourceTree = ""; }; 18FB517C4077E6929DB734C793782E40 /* TyphoonParentReferenceHydratingPostProcessor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonParentReferenceHydratingPostProcessor.m; path = Source/Factory/Internal/TyphoonParentReferenceHydratingPostProcessor.m; sourceTree = ""; }; 198733CC5474A54E6311C06F7EF78E54 /* SDWebImageCompat.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageCompat.m; path = SDWebImage/Core/SDWebImageCompat.m; sourceTree = ""; }; 198BAFBAC2D38ECDB593D0EE2304B03C /* Pods-iOS-Network-Stack-DiveTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iOS-Network-Stack-DiveTests.release.xcconfig"; sourceTree = ""; }; 19EA01E604C674E56BA259D0C3794651 /* SDAssociatedObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDAssociatedObject.h; path = SDWebImage/Private/SDAssociatedObject.h; sourceTree = ""; }; 1AC6505E33748BB6A720F5B5C8C96382 /* TyphoonDefinitionRegisterer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonDefinitionRegisterer.m; path = Source/Factory/TyphoonDefinitionRegisterer.m; sourceTree = ""; }; 1AE4A447234AC4D5BD55348C3E5BC6E8 /* OCPartialMockObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCPartialMockObject.m; path = Source/OCMock/OCPartialMockObject.m; sourceTree = ""; }; 1B68D1A525FF5B8E36976449BC5C2434 /* TyphoonStackElement.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonStackElement.m; path = Source/Factory/Internal/TyphoonStackElement.m; sourceTree = ""; }; 1BDDCD9A639B9E7EBCAF02C54E1DE1E3 /* NSInvocation+TCFCustomImplementation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSInvocation+TCFCustomImplementation.h"; path = "Source/Factory/Internal/NSInvocation+TCFCustomImplementation.h"; sourceTree = ""; }; 1C87CAE8BB86399A29C7AAFACF0CD24D /* OCMBoxedReturnValueProvider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMBoxedReturnValueProvider.h; path = Source/OCMock/OCMBoxedReturnValueProvider.h; sourceTree = ""; }; 1C982E20E08BC60740455993A35E8A3F /* NSMethodSignature+TCFUnwrapValues.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSMethodSignature+TCFUnwrapValues.m"; path = "Source/Factory/Internal/NSMethodSignature+TCFUnwrapValues.m"; sourceTree = ""; }; 1CB4CE5D415687F6A3499F360794DA04 /* RACTuple.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACTuple.m; path = ReactiveObjC/RACTuple.m; sourceTree = ""; }; 1D09E947A17A03E7F1BE82DA4451726B /* UIImage+MultiFormat.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImage+MultiFormat.m"; path = "SDWebImage/Core/UIImage+MultiFormat.m"; sourceTree = ""; }; 1D3495A662B6322B041706885A769B02 /* NSValue+OCMAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSValue+OCMAdditions.h"; path = "Source/OCMock/NSValue+OCMAdditions.h"; sourceTree = ""; }; 1DD1EDA8854FDDBB1314904C848EEADC /* RACStream.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACStream.h; path = ReactiveObjC/RACStream.h; sourceTree = ""; }; 1E25BC9F9B2C745B6471F74CCB47DC7F /* OCMExceptionReturnValueProvider.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMExceptionReturnValueProvider.m; path = Source/OCMock/OCMExceptionReturnValueProvider.m; sourceTree = ""; }; 1E270F13C426BEA6928171EC9DBCEE70 /* ReactiveObjC-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "ReactiveObjC-dummy.m"; sourceTree = ""; }; 1E295017E5EF894DD2C62B5422434280 /* UIGestureRecognizer+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIGestureRecognizer+RACSignalSupport.m"; path = "ReactiveObjC/UIGestureRecognizer+RACSignalSupport.m"; sourceTree = ""; }; 1E2EED42BB0AD4DB8FDE62F4D4047113 /* TyphoonStoryboardDefinition.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonStoryboardDefinition.m; path = Source/ios/Definition/TyphoonStoryboardDefinition.m; sourceTree = ""; }; 1E5E520FF458D112BA579819CA973E7A /* SDWebImageOperation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageOperation.h; path = SDWebImage/Core/SDWebImageOperation.h; sourceTree = ""; }; 1EE6F568285A1219683FB22F70790F2A /* TyphoonDefinitionNamespace.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonDefinitionNamespace.h; path = Source/Definition/Namespacing/TyphoonDefinitionNamespace.h; sourceTree = ""; }; 1F18C60D29A81CE3566D4A38800979FA /* OCMRecorder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMRecorder.h; path = Source/OCMock/OCMRecorder.h; sourceTree = ""; }; 1F38003E7B8D270C5FC263FAC5B717D5 /* TyphoonLoadedView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonLoadedView.m; path = Source/ios/Storyboard/TyphoonLoadedView.m; sourceTree = ""; }; 1F40215065345DB127D2130E4F8AA842 /* UIScrollView+EmptyDataSet.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIScrollView+EmptyDataSet.h"; path = "Source/UIScrollView+EmptyDataSet.h"; sourceTree = ""; }; 1F5DE94BB623F6C7A33F0A746E8F58DB /* RACDynamicSignal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACDynamicSignal.h; path = ReactiveObjC/RACDynamicSignal.h; sourceTree = ""; }; 1FB6B3BEE43133D2BF7F12D5B6B8A89D /* Masonry-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Masonry-Info.plist"; sourceTree = ""; }; 1FFAB3AC801B7EA6B022EBB03D54462C /* CocoaAsyncSocket-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "CocoaAsyncSocket-umbrella.h"; sourceTree = ""; }; 1FFED36A657123030ABB700256D73F15 /* Masonry */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Masonry; path = Masonry.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 200AD54BC62CB8F66159C905EE9DED00 /* Pods-iOS-Network-Stack-DiveTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iOS-Network-Stack-DiveTests.debug.xcconfig"; sourceTree = ""; }; 201545627F7DB82D38F76A51D3D4C33F /* UIDatePicker+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIDatePicker+RACSignalSupport.m"; path = "ReactiveObjC/UIDatePicker+RACSignalSupport.m"; sourceTree = ""; }; 203BC0FB7B1B96C82C4EAD4593D84A8D /* Typhoon.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = Typhoon.h; path = Source/Typhoon.h; sourceTree = ""; }; 217EBAD9071176F8750E350BA9867152 /* SDImageCodersManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageCodersManager.h; path = SDWebImage/Core/SDImageCodersManager.h; sourceTree = ""; }; 218826C133BB15ACA97ED4D92CBF2243 /* TyphoonInjectionByReference.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByReference.h; path = Source/Definition/Injections/TyphoonInjectionByReference.h; sourceTree = ""; }; 21F7FF6AAD9960A7E34FA700AF4A2F1D /* RACTupleSequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACTupleSequence.h; path = ReactiveObjC/RACTupleSequence.h; sourceTree = ""; }; 227C536B9341DFDDAB8A93622D7BAB18 /* TyphoonInjectionContext.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionContext.m; path = Source/Definition/Injections/TyphoonInjectionContext.m; sourceTree = ""; }; 229B6EF02232FF52DA22913B1FB01A1F /* RACReturnSignal.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACReturnSignal.m; path = ReactiveObjC/RACReturnSignal.m; sourceTree = ""; }; 22EF63276DBF66ABD2D5FBF76746A708 /* TyphoonWeakComponentsPool.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonWeakComponentsPool.h; path = Source/Factory/Pool/TyphoonWeakComponentsPool.h; sourceTree = ""; }; 2385FD3E31A6F83168DE9EFA6AC4A66F /* NSOrderedSet+RACSequenceAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSOrderedSet+RACSequenceAdditions.m"; path = "ReactiveObjC/NSOrderedSet+RACSequenceAdditions.m"; sourceTree = ""; }; 23D6C0C2DA8003ECC644C3366B6E5247 /* TyphoonDefinition+Infrastructure.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonDefinition+Infrastructure.h"; path = "Source/Definition/Infrastructure/TyphoonDefinition+Infrastructure.h"; sourceTree = ""; }; 246818BE8D07C34C5CC6113926619C5F /* RACChannel.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACChannel.h; path = ReactiveObjC/RACChannel.h; sourceTree = ""; }; 24A117E496A0E15BE008AF6C24618D1F /* SDWebImageCompat.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageCompat.h; path = SDWebImage/Core/SDWebImageCompat.h; sourceTree = ""; }; 252B49984FBEE0DBF2DD1314B8D9B974 /* SDAssociatedObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDAssociatedObject.m; path = SDWebImage/Private/SDAssociatedObject.m; sourceTree = ""; }; 252FA21B0D96A56E65E1481A1B27A6A3 /* NSIndexSet+RACSequenceAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSIndexSet+RACSequenceAdditions.h"; path = "ReactiveObjC/NSIndexSet+RACSequenceAdditions.h"; sourceTree = ""; }; 253F5F73A60CE9213B91A4027838C766 /* NSLayoutConstraint+MASDebugAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSLayoutConstraint+MASDebugAdditions.h"; path = "Masonry/NSLayoutConstraint+MASDebugAdditions.h"; sourceTree = ""; }; 257F4F157F94CB40D1F5643758121C8F /* TyphoonInjectionByDictionary.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByDictionary.h; path = Source/Definition/Injections/TyphoonInjectionByDictionary.h; sourceTree = ""; }; 25FCBE4E8D3998433D6BFA94FB2FAC69 /* RACBlockTrampoline.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACBlockTrampoline.h; path = ReactiveObjC/RACBlockTrampoline.h; sourceTree = ""; }; 2604998C21DB77EB38BB124802109F52 /* RACEmptySignal.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACEmptySignal.m; path = ReactiveObjC/RACEmptySignal.m; sourceTree = ""; }; 26BFC051219DBE2ECE65E6DD31A177F9 /* RACSerialDisposable.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACSerialDisposable.h; path = ReactiveObjC/RACSerialDisposable.h; sourceTree = ""; }; 273504B50C9E20A280FF38B369EECE87 /* SDImageCachesManagerOperation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageCachesManagerOperation.m; path = SDWebImage/Private/SDImageCachesManagerOperation.m; sourceTree = ""; }; 2735EF71996F30815132D35A2A30E845 /* GCDAsyncSocket.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = GCDAsyncSocket.h; path = Source/GCD/GCDAsyncSocket.h; sourceTree = ""; }; 276FD7F39BAD92CD4DE60118D6BDC68B /* UIActionSheet+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIActionSheet+RACSignalSupport.m"; path = "ReactiveObjC/UIActionSheet+RACSignalSupport.m"; sourceTree = ""; }; 27871BA973C3B687A1D538285665EBB6 /* RACDynamicSignal.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACDynamicSignal.m; path = ReactiveObjC/RACDynamicSignal.m; sourceTree = ""; }; 28B829B34223ADB0F04E28F63DEA2868 /* RACValueTransformer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACValueTransformer.m; path = ReactiveObjC/RACValueTransformer.m; sourceTree = ""; }; 28B9BD69A27186895E63DB6A19650F73 /* TyphoonFactoryDefinition.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonFactoryDefinition.h; path = Source/Definition/Internal/TyphoonFactoryDefinition.h; sourceTree = ""; }; 28CC19C7A5B9A7C5439E55E906813635 /* RACArraySequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACArraySequence.m; path = ReactiveObjC/RACArraySequence.m; sourceTree = ""; }; 2ADA8F4AE98D80692FA44970F3846684 /* MASViewAttribute.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = MASViewAttribute.m; path = Masonry/MASViewAttribute.m; sourceTree = ""; }; 2AE9080EF975331395E265C09E03252C /* RACSubscriber+Private.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "RACSubscriber+Private.h"; path = "ReactiveObjC/RACSubscriber+Private.h"; sourceTree = ""; }; 2B7641571C129F0F034C78A348461477 /* YYModel-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "YYModel-prefix.pch"; sourceTree = ""; }; 2BE98D49469466E2F555F3E1E65A4F4C /* TyphoonPreattachedComponentsRegisterer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonPreattachedComponentsRegisterer.h; path = Source/Factory/TyphoonPreattachedComponentsRegisterer.h; sourceTree = ""; }; 2C379332A0D51473ADCF63C80BF3B4C8 /* TyphoonInjectionByType.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByType.h; path = Source/Definition/Injections/TyphoonInjectionByType.h; sourceTree = ""; }; 2C69DF26B88BA2D74723697231D11733 /* RACSignal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACSignal.h; path = ReactiveObjC/RACSignal.h; sourceTree = ""; }; 2CD41B1FA71FB0AE97EB624502ACF0A1 /* TyphoonPatcher.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonPatcher.h; path = Source/Test/Patcher/TyphoonPatcher.h; sourceTree = ""; }; 2CD5CB6BE4742004BAEA7654CE3C6AF7 /* UIImagePickerController+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImagePickerController+RACSignalSupport.h"; path = "ReactiveObjC/UIImagePickerController+RACSignalSupport.h"; sourceTree = ""; }; 2D163BF4979C587AB2708ADA67ABB425 /* NSString+RACSequenceAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSString+RACSequenceAdditions.m"; path = "ReactiveObjC/NSString+RACSequenceAdditions.m"; sourceTree = ""; }; 2D39AF61764732228A5146A67595AEAE /* MKAnnotationView+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "MKAnnotationView+RACSignalSupport.h"; path = "ReactiveObjC/MKAnnotationView+RACSignalSupport.h"; sourceTree = ""; }; 2D3D8BCFF92388848C3FD597F1895318 /* UIImage+MemoryCacheCost.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImage+MemoryCacheCost.h"; path = "SDWebImage/Core/UIImage+MemoryCacheCost.h"; sourceTree = ""; }; 2D5383F6C03BB8925217003BD9A3A82C /* TyphoonStoryboardDefinitionContext.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonStoryboardDefinitionContext.m; path = Source/ios/Definition/TyphoonStoryboardDefinitionContext.m; sourceTree = ""; }; 2DAE20DF4B8369B96AEB7A6A762F3DF5 /* YYModel.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = YYModel.release.xcconfig; sourceTree = ""; }; 2E023F08FB04D7249EE7E6B1E84A12DC /* RACMulticastConnection.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACMulticastConnection.m; path = ReactiveObjC/RACMulticastConnection.m; sourceTree = ""; }; 2E0DEBA6FAF64625746D89A660E075B9 /* RACSubscriptionScheduler.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACSubscriptionScheduler.h; path = ReactiveObjC/RACSubscriptionScheduler.h; sourceTree = ""; }; 2E4DF20E80C1D11D64201766B309D73A /* UICollectionReusableView+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UICollectionReusableView+RACSignalSupport.h"; path = "ReactiveObjC/UICollectionReusableView+RACSignalSupport.h"; sourceTree = ""; }; 2E59442B228F5A36B8D5592A675C8879 /* TyphoonTypeConverter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonTypeConverter.h; path = Source/TypeConversion/TyphoonTypeConverter.h; sourceTree = ""; }; 2EE6EC22E733D815C1F5084910F7491B /* TyphoonComponentFactory.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonComponentFactory.h; path = Source/Factory/TyphoonComponentFactory.h; sourceTree = ""; }; 2EEE06C505E1457D30A4ACDA3820D377 /* OCMIndirectReturnValueProvider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMIndirectReturnValueProvider.h; path = Source/OCMock/OCMIndirectReturnValueProvider.h; sourceTree = ""; }; 2F2015FF4C85AFF50A9D9CA166D8C5A4 /* ReactiveObjC-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ReactiveObjC-prefix.pch"; sourceTree = ""; }; 2F26AA73AA98E00F8C916633F02D50F8 /* RACEXTRuntimeExtensions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACEXTRuntimeExtensions.h; path = ReactiveObjC/extobjc/RACEXTRuntimeExtensions.h; sourceTree = ""; }; 2F69FEE7B67F930E4BAB806C41EDBDE5 /* NSButton+WebCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSButton+WebCache.h"; path = "SDWebImage/Core/NSButton+WebCache.h"; sourceTree = ""; }; 2FEB52E6488D9C7D2B20E87FA516436B /* Typhoon.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Typhoon.release.xcconfig; sourceTree = ""; }; 3060A3BFE9758FA021148046B85E8CE5 /* DZNEmptyDataSet-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "DZNEmptyDataSet-umbrella.h"; sourceTree = ""; }; 30935A228E64CE3387EE71949306F225 /* TyphoonConfiguration.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonConfiguration.h; path = Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonConfiguration.h; sourceTree = ""; }; 30BC8EBE209C30443BACDAAA1B7A4CC1 /* TyphoonDefinition+Storyboard.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonDefinition+Storyboard.h"; path = "Source/ios/Storyboard/TyphoonDefinition+Storyboard.h"; sourceTree = ""; }; 312F34A0D29AA628F2B731F4DA50B1E7 /* UITextView+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UITextView+RACSignalSupport.m"; path = "ReactiveObjC/UITextView+RACSignalSupport.m"; sourceTree = ""; }; 31794A4FD83642DD83BC70B9F11CA23A /* SDImageAWebPCoder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageAWebPCoder.h; path = SDWebImage/Core/SDImageAWebPCoder.h; sourceTree = ""; }; 325FC278EFE1EDE9A708FE21611C503E /* YYModel.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = YYModel.h; path = YYModel/YYModel.h; sourceTree = ""; }; 3290EEA634B358879EBD921ACF1B1422 /* NSString+RACSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSString+RACSupport.m"; path = "ReactiveObjC/NSString+RACSupport.m"; sourceTree = ""; }; 32939C9AD7B94007A62205D5A5CA486C /* TyphoonUIColorTypeConverter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonUIColorTypeConverter.h; path = Source/ios/TypeConversion/Converters/TyphoonUIColorTypeConverter.h; sourceTree = ""; }; 330097FBD5A9529B89BA11A61293AD4C /* TyphoonViewControllerFactory.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonViewControllerFactory.m; path = Source/ios/Storyboard/TyphoonViewControllerFactory.m; sourceTree = ""; }; 33C36535BCF88C0F8CE4BEE165483F16 /* YYClassInfo.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = YYClassInfo.m; path = YYModel/YYClassInfo.m; sourceTree = ""; }; 34517F02303D979079D891F86B87BD5D /* TyphoonMethod+InstanceBuilder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "TyphoonMethod+InstanceBuilder.m"; path = "Source/Definition/Method/Internal/TyphoonMethod+InstanceBuilder.m"; sourceTree = ""; }; 353C12944450C35AA7A2B53F9A9EB5B8 /* Pods-iOS-Network-Stack-Dive.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-iOS-Network-Stack-Dive.debug.xcconfig"; sourceTree = ""; }; 357902381BACEC332590C847106F5C16 /* OCMNonRetainingObjectReturnValueProvider.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMNonRetainingObjectReturnValueProvider.m; path = Source/OCMock/OCMNonRetainingObjectReturnValueProvider.m; sourceTree = ""; }; 35811A877C1D60F1A3AB124ACBC29E9E /* RACSubscriptingAssignmentTrampoline.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACSubscriptingAssignmentTrampoline.h; path = ReactiveObjC/RACSubscriptingAssignmentTrampoline.h; sourceTree = ""; }; 35D424AE41D9D5DF8B4CD98EE40E20E2 /* TyphoonInjectedObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectedObject.h; path = Source/Definition/AutoInjection/TyphoonInjectedObject.h; sourceTree = ""; }; 35F7DE05C6E6BA99060F8F264A3CEBEA /* TyphoonDefinition.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonDefinition.m; path = Source/Definition/TyphoonDefinition.m; sourceTree = ""; }; 36EA9831D45C8C2DBBE9C8924552B0F3 /* OCObserverMockObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCObserverMockObject.m; path = Source/OCMock/OCObserverMockObject.m; sourceTree = ""; }; 37CF1049FAEF879E02FD90463A2E3910 /* RACStream.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACStream.m; path = ReactiveObjC/RACStream.m; sourceTree = ""; }; 37E635EECAF9EE9AFD5F838C93C42334 /* TyphoonAssemblyAdviser.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAssemblyAdviser.h; path = Source/Factory/Internal/TyphoonAssemblyAdviser.h; sourceTree = ""; }; 384D578EC3A290F1ADF6C1728A6C1562 /* TyphoonStoryboard.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonStoryboard.h; path = Source/ios/Storyboard/TyphoonStoryboard.h; sourceTree = ""; }; 38A1E514232587E860EC29C335D4DC68 /* UISlider+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UISlider+RACSignalSupport.h"; path = "ReactiveObjC/UISlider+RACSignalSupport.h"; sourceTree = ""; }; 38B6C5CA78D03B16417EF54377D96D24 /* RACGroupedSignal.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACGroupedSignal.m; path = ReactiveObjC/RACGroupedSignal.m; sourceTree = ""; }; 38F730D07E4C4D067A10C1C091A76D44 /* NSObject+OCMAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+OCMAdditions.m"; path = "Source/OCMock/NSObject+OCMAdditions.m"; sourceTree = ""; }; 3A12D240C381406917C3EFB576E33710 /* OCMConstraint.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMConstraint.m; path = Source/OCMock/OCMConstraint.m; sourceTree = ""; }; 3B0A54B1F889B9B87CDFE1DB41F6ABCC /* SDWebImageDownloader.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageDownloader.m; path = SDWebImage/Core/SDWebImageDownloader.m; sourceTree = ""; }; 3B0FD681CCD09F3DE9E1B8663D3F1CC2 /* NSObject+FactoryHooks.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+FactoryHooks.h"; path = "Source/Factory/Hooks/NSObject+FactoryHooks.h"; sourceTree = ""; }; 3B33DB28F54E682CA83C889709720FB7 /* TyphoonOptionMatcher.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonOptionMatcher.m; path = Source/Configuration/DefinitionOptionConfiguration/Matcher/TyphoonOptionMatcher.m; sourceTree = ""; }; 3BC61D1DBB79E431F7B1D23F12DBA78A /* TyphoonCallStack.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonCallStack.m; path = Source/Factory/Internal/TyphoonCallStack.m; sourceTree = ""; }; 3BCF3C4AEB69B5B155617E83726EB0A0 /* RACSignal.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACSignal.m; path = ReactiveObjC/RACSignal.m; sourceTree = ""; }; 3C12837A69C57039BE360EAF15FE6C76 /* OCMVerifier.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMVerifier.h; path = Source/OCMock/OCMVerifier.h; sourceTree = ""; }; 3CB4F2D27F00A3C400E970AB3C7AB6BC /* NSArray+TyphoonManualEnumeration.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSArray+TyphoonManualEnumeration.m"; path = "Source/Utils/NSArray+TyphoonManualEnumeration.m"; sourceTree = ""; }; 3CD424DC6ECC7B41C2BAB51078B5FB8E /* TyphoonInjectionByType.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByType.m; path = Source/Definition/Injections/TyphoonInjectionByType.m; sourceTree = ""; }; 3D31CBCD57F3773D4DC8B94E19806A4E /* TyphoonIntrospectionUtils.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonIntrospectionUtils.m; path = Source/Utils/TyphoonIntrospectionUtils.m; sourceTree = ""; }; 3D455866AC1ECBAFA4D139C7DD0E3168 /* TyphoonLoadedView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonLoadedView.h; path = Source/ios/Storyboard/TyphoonLoadedView.h; sourceTree = ""; }; 3D4CB8BAE1E9433AA4D034F31AD60DE4 /* SDImageFrame.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageFrame.h; path = SDWebImage/Core/SDImageFrame.h; sourceTree = ""; }; 3D4FB47B1FC93F629C32CDEE69E59AE4 /* NSValue+OCMAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSValue+OCMAdditions.m"; path = "Source/OCMock/NSValue+OCMAdditions.m"; sourceTree = ""; }; 3D69D7A45A56F3986C52DF0088F96C35 /* TyphoonCollaboratingAssembliesCollector.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonCollaboratingAssembliesCollector.m; path = Source/Factory/Internal/TyphoonCollaboratingAssembliesCollector.m; sourceTree = ""; }; 3D81FCE9D11F4054F824AE0AD9C905D9 /* SDWebImageIndicator.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageIndicator.m; path = SDWebImage/Core/SDWebImageIndicator.m; sourceTree = ""; }; 3DA26B2B02963DB127DA3542FECF3472 /* TyphoonInjectionByCollection.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByCollection.m; path = Source/Definition/Injections/TyphoonInjectionByCollection.m; sourceTree = ""; }; 3DA3CE4677195B9C5B8F50C6E3B91D81 /* OCMArgAction.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMArgAction.m; path = Source/OCMock/OCMArgAction.m; sourceTree = ""; }; 3DB7BACA057ADD843741CE2BDFDF62C8 /* NSLayoutConstraint+MASDebugAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSLayoutConstraint+MASDebugAdditions.m"; path = "Masonry/NSLayoutConstraint+MASDebugAdditions.m"; sourceTree = ""; }; 3DC413ABFFB1444FE469D466ADE4A98C /* NSDictionary+RACSequenceAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSDictionary+RACSequenceAdditions.h"; path = "ReactiveObjC/NSDictionary+RACSequenceAdditions.h"; sourceTree = ""; }; 3DF90E5FD761A704C337FA27D910B4EC /* NSLayoutConstraint+TyphoonOutletTransfer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSLayoutConstraint+TyphoonOutletTransfer.m"; path = "Source/ios/Storyboard/NSLayoutConstraint+TyphoonOutletTransfer.m"; sourceTree = ""; }; 3DFCA73A433D742E4E61D2DF66334925 /* NSInvocation+TCFInstanceBuilder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSInvocation+TCFInstanceBuilder.h"; path = "Source/Factory/Internal/NSInvocation+TCFInstanceBuilder.h"; sourceTree = ""; }; 3E251AA3EC1653CC267FDFD659704A71 /* OCMock-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "OCMock-Info.plist"; sourceTree = ""; }; 3E461ADDAEAAFCFADE347E2C9267118A /* NSURLConnection+RACSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSURLConnection+RACSupport.m"; path = "ReactiveObjC/NSURLConnection+RACSupport.m"; sourceTree = ""; }; 3E5BC7E6CAFF72A150840D4DC92D331D /* TyphoonDefinition+Storyboard.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "TyphoonDefinition+Storyboard.m"; path = "Source/ios/Storyboard/TyphoonDefinition+Storyboard.m"; sourceTree = ""; }; 3E8A97304D179B0ADAF704C7CEBEECD1 /* SDImageLoader.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageLoader.m; path = SDWebImage/Core/SDImageLoader.m; sourceTree = ""; }; 3EF8BF2E5B7814DF672FB319D2CB899A /* TyphoonTypeConversionUtils.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonTypeConversionUtils.m; path = Source/TypeConversion/TyphoonTypeConversionUtils.m; sourceTree = ""; }; 3F05B9328BABBA34AFCE3172B61D1F83 /* TyphoonBundledImageTypeConverter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonBundledImageTypeConverter.h; path = Source/ios/TypeConversion/Converters/TyphoonBundledImageTypeConverter.h; sourceTree = ""; }; 3F86F06F204B5BACDAEBC5742F93220C /* TyphoonComponentFactory+TyphoonDefinitionRegisterer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h"; path = "Source/Factory/Internal/TyphoonComponentFactory+TyphoonDefinitionRegisterer.h"; sourceTree = ""; }; 3FE3393A74BB6D586E4B892A33322286 /* TyphoonRuntimeArguments.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonRuntimeArguments.m; path = Source/Factory/Internal/TyphoonRuntimeArguments.m; sourceTree = ""; }; 400FF55D0451E7A8F33A3D0D3E11C1B9 /* Reachability */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Reachability; path = Reachability.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4065878CE75B090253FC030941E3DC2D /* TyphoonComponentFactory+Storyboard.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "TyphoonComponentFactory+Storyboard.m"; path = "Source/ios/Storyboard/TyphoonComponentFactory+Storyboard.m"; sourceTree = ""; }; 40D4E59B9A952E2A8649507C8627874B /* TyphoonDefinition+Config.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "TyphoonDefinition+Config.m"; path = "Source/Configuration/ConfigPostProcessor/TyphoonDefinition+Config.m"; sourceTree = ""; }; 40F0A65B2C96B6E67B7417305F598D6D /* SDWebImageDownloaderResponseModifier.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageDownloaderResponseModifier.m; path = SDWebImage/Core/SDWebImageDownloaderResponseModifier.m; sourceTree = ""; }; 41908C1E64673E808673215A63FCEC45 /* UIView+WebCacheOperation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIView+WebCacheOperation.h"; path = "SDWebImage/Core/UIView+WebCacheOperation.h"; sourceTree = ""; }; 419694111A274B662810AE1FECA72F35 /* TyphoonInjectionByObjectFromString.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByObjectFromString.m; path = Source/Definition/Injections/TyphoonInjectionByObjectFromString.m; sourceTree = ""; }; 419B699C2112F512494E7DDB5FE14FCA /* RACDisposable.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACDisposable.m; path = ReactiveObjC/RACDisposable.m; sourceTree = ""; }; 41BDCD9D2400DA765DCB894C8509679B /* NSUserDefaults+RACSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSUserDefaults+RACSupport.m"; path = "ReactiveObjC/NSUserDefaults+RACSupport.m"; sourceTree = ""; }; 424E4FDDC999763B5ECDE171AF2948A3 /* TyphoonDefinition+Internal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonDefinition+Internal.h"; path = "Source/Definition/Internal/TyphoonDefinition+Internal.h"; sourceTree = ""; }; 4290D47244C9D83C19300F3FE4CA8170 /* SDImageCacheDefine.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageCacheDefine.m; path = SDWebImage/Core/SDImageCacheDefine.m; sourceTree = ""; }; 429AF24E6181CE01E2CB7642A29FBB6C /* TyphoonDefinition+InstanceBuilder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "TyphoonDefinition+InstanceBuilder.m"; path = "Source/Definition/Internal/TyphoonDefinition+InstanceBuilder.m"; sourceTree = ""; }; 42AC4C9E32B2DF0DF2366BB6D08859AE /* UIActionSheet+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIActionSheet+RACSignalSupport.h"; path = "ReactiveObjC/UIActionSheet+RACSignalSupport.h"; sourceTree = ""; }; 42BB4F99181287983D693A457D56DE96 /* TyphoonColorConversionUtils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonColorConversionUtils.h; path = Source/TypeConversion/Helpers/TyphoonColorConversionUtils.h; sourceTree = ""; }; 43154AF288B8222D79172AC422866E6C /* NSArray+RACSequenceAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSArray+RACSequenceAdditions.h"; path = "ReactiveObjC/NSArray+RACSequenceAdditions.h"; sourceTree = ""; }; 4349B8FF1BD06C628405857A06C179DD /* MASViewConstraint.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = MASViewConstraint.m; path = Masonry/MASViewConstraint.m; sourceTree = ""; }; 437B0AD6B69A2013C65A324A3CBBF3CA /* OCMExpectationRecorder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMExpectationRecorder.m; path = Source/OCMock/OCMExpectationRecorder.m; sourceTree = ""; }; 437DE71945F67DF09BAC5E5BB65C5A20 /* OCMBlockCaller.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMBlockCaller.m; path = Source/OCMock/OCMBlockCaller.m; sourceTree = ""; }; 43C103B5C9A8BC55D3644883BC36B3A6 /* TyphoonAssemblyBuilder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAssemblyBuilder.h; path = Source/Factory/Internal/TyphoonAssemblyBuilder.h; sourceTree = ""; }; 43C4233C6624441991C2F578A1ACAE04 /* OCMock.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = OCMock.modulemap; sourceTree = ""; }; 43D883AF4752701FF9AD2DF1CF845589 /* UIGestureRecognizer+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIGestureRecognizer+RACSignalSupport.h"; path = "ReactiveObjC/UIGestureRecognizer+RACSignalSupport.h"; sourceTree = ""; }; 44725BC3ADC3FA22EF1DD0F3164C12FF /* RACUnarySequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACUnarySequence.m; path = ReactiveObjC/RACUnarySequence.m; sourceTree = ""; }; 44A289ACE45B5D7FC30203A480801351 /* DZNEmptyDataSet.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = DZNEmptyDataSet.debug.xcconfig; sourceTree = ""; }; 45512E8214358C10C4B33C4B78C7026F /* NSObject+RACSelectorSignal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+RACSelectorSignal.h"; path = "ReactiveObjC/NSObject+RACSelectorSignal.h"; sourceTree = ""; }; 458457A3A3CABCBDF2136A3F5E6E82BF /* TyphoonFactoryPropertyInjectionPostProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonFactoryPropertyInjectionPostProcessor.h; path = Source/Factory/Internal/TyphoonFactoryPropertyInjectionPostProcessor.h; sourceTree = ""; }; 45AF458D3D9B4CF7A071A8AA27745113 /* UIImageView+HighlightedWebCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImageView+HighlightedWebCache.m"; path = "SDWebImage/Core/UIImageView+HighlightedWebCache.m"; sourceTree = ""; }; 45BD3FAB4630B2990E6B8F1BC78D9D8F /* OCMRealObjectForwarder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMRealObjectForwarder.h; path = Source/OCMock/OCMRealObjectForwarder.h; sourceTree = ""; }; 45FA653E23538B63DB444A6ECA2E3190 /* SDImageFramePool.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageFramePool.h; path = SDWebImage/Private/SDImageFramePool.h; sourceTree = ""; }; 467F746C89C8EA1B73E4F4C1D741B528 /* View+MASAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "View+MASAdditions.h"; path = "Masonry/View+MASAdditions.h"; sourceTree = ""; }; 468798856EED0D94D6E982B4D271195A /* TyphoonOptionMatcher+Internal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonOptionMatcher+Internal.h"; path = "Source/Configuration/DefinitionOptionConfiguration/Matcher/TyphoonOptionMatcher+Internal.h"; sourceTree = ""; }; 46B265A7E81C9ECE01048E340D45E9A3 /* UIImage+MemoryCacheCost.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImage+MemoryCacheCost.m"; path = "SDWebImage/Core/UIImage+MemoryCacheCost.m"; sourceTree = ""; }; 472F840FC38C62F9E93AD1BEE1A70F95 /* RACPassthroughSubscriber.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACPassthroughSubscriber.m; path = ReactiveObjC/RACPassthroughSubscriber.m; sourceTree = ""; }; 47E16F28A08ED8AA345A07CC51EAF129 /* NSInvocation+OCMAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSInvocation+OCMAdditions.m"; path = "Source/OCMock/NSInvocation+OCMAdditions.m"; sourceTree = ""; }; 4834BC1E84F882D9846062878F9F66C9 /* TyphoonDefinition.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonDefinition.h; path = Source/Definition/TyphoonDefinition.h; sourceTree = ""; }; 48A63D2EDED811435B879E1A51A2C5C3 /* Masonry-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Masonry-umbrella.h"; sourceTree = ""; }; 48D7189A91D3148BCB7AB21867EC74DA /* Reachability.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Reachability.release.xcconfig; sourceTree = ""; }; 48E883D9DB5DF435F64617A907DCD908 /* RACTupleSequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACTupleSequence.m; path = ReactiveObjC/RACTupleSequence.m; sourceTree = ""; }; 48F2519B307B02B43FC5B68399AF554F /* TyphoonDefinition+Config.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonDefinition+Config.h"; path = "Source/Configuration/ConfigPostProcessor/TyphoonDefinition+Config.h"; sourceTree = ""; }; 4987ACD1FED6CF90E9AE135E75E5AE57 /* OCMock.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMock.h; path = Source/OCMock/OCMock.h; sourceTree = ""; }; 4A8368182067949B6C49AB727784993C /* UIImage+Transform.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImage+Transform.h"; path = "SDWebImage/Core/UIImage+Transform.h"; sourceTree = ""; }; 4AA45AFD4053DDC8999AD121B914CE27 /* UISlider+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UISlider+RACSignalSupport.m"; path = "ReactiveObjC/UISlider+RACSignalSupport.m"; sourceTree = ""; }; 4AC7882AF54A36E3A226CA3878D872F7 /* OCClassMockObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCClassMockObject.m; path = Source/OCMock/OCClassMockObject.m; sourceTree = ""; }; 4B18C13FE0EC5A5F756BBA99DC8559F1 /* SDWebImageManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageManager.h; path = SDWebImage/Core/SDWebImageManager.h; sourceTree = ""; }; 4B2FCD3B157174CAD7F173B5F1FF0DA1 /* RACStringSequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACStringSequence.m; path = ReactiveObjC/RACStringSequence.m; sourceTree = ""; }; 4B7043029F68EFD2C4174A964B0551C6 /* TyphoonLinkerCategoryBugFix.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonLinkerCategoryBugFix.h; path = Source/Utils/TyphoonLinkerCategoryBugFix.h; sourceTree = ""; }; 4BE4EBEC7840EB2B41361CE32163EA16 /* SDWebImageDownloaderRequestModifier.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageDownloaderRequestModifier.m; path = SDWebImage/Core/SDWebImageDownloaderRequestModifier.m; sourceTree = ""; }; 4BFF4254094763357CA3C84E0939A2C3 /* SDWebImageTransitionInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageTransitionInternal.h; path = SDWebImage/Private/SDWebImageTransitionInternal.h; sourceTree = ""; }; 4C07DF0C9AD7EAEC76E6F3D297E58F66 /* NSInvocation+TCFUnwrapValues.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSInvocation+TCFUnwrapValues.h"; path = "Source/Factory/Internal/NSInvocation+TCFUnwrapValues.h"; sourceTree = ""; }; 4C20ADFFD788112D9472742A2E3ACB43 /* UIView+WebCacheState.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIView+WebCacheState.h"; path = "SDWebImage/Core/UIView+WebCacheState.h"; sourceTree = ""; }; 4D14066F28EE5838EBA4D6142544116D /* TyphoonMemoryManagementUtils.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonMemoryManagementUtils.m; path = Source/Factory/Internal/TyphoonMemoryManagementUtils.m; sourceTree = ""; }; 4D249879A5BB3605D92C3BCE919251C0 /* RACScheduler.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACScheduler.h; path = ReactiveObjC/RACScheduler.h; sourceTree = ""; }; 4D40DD7E6EAE2E8222553A0E28A69834 /* TyphoonStackElement.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonStackElement.h; path = Source/Factory/Internal/TyphoonStackElement.h; sourceTree = ""; }; 4D4A2340AC32529239D0841BD00726B1 /* SDWebImagePrefetcher.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImagePrefetcher.h; path = SDWebImage/Core/SDWebImagePrefetcher.h; sourceTree = ""; }; 4DB0A4F8CE90B57FB6935727ABCA7CE2 /* OCMInvocationMatcher.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMInvocationMatcher.h; path = Source/OCMock/OCMInvocationMatcher.h; sourceTree = ""; }; 4E1DD37617D3B108328AB936760E6BDD /* Pods-iOS-Network-Stack-Dive-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-iOS-Network-Stack-Dive-umbrella.h"; sourceTree = ""; }; 4E215B27470E9F7E028D285C07B15B9B /* ImageIO.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = ImageIO.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/ImageIO.framework; sourceTree = DEVELOPER_DIR; }; 4E5A663C1E60087F575DDB601D6017F3 /* TyphoonPlistStyleConfiguration.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonPlistStyleConfiguration.h; path = Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonPlistStyleConfiguration.h; sourceTree = ""; }; 4F1747F10DA9479C6BDD54B652E1C545 /* SDAnimatedImageView+WebCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "SDAnimatedImageView+WebCache.h"; path = "SDWebImage/Core/SDAnimatedImageView+WebCache.h"; sourceTree = ""; }; 4FA8209FE24832482CBDBD96568B60E3 /* TyphoonAssemblyDefinitionBuilder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonAssemblyDefinitionBuilder.m; path = Source/Factory/Internal/TyphoonAssemblyDefinitionBuilder.m; sourceTree = ""; }; 4FB2623C122163DC9FB824E1A71D49FD /* NSDictionary+CustomInjection.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+CustomInjection.m"; path = "Source/Definition/Internal/NSDictionary+CustomInjection.m"; sourceTree = ""; }; 4FEDDBAED301C3BCF65687E2F5B90003 /* TyphoonNSNumberTypeConverter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonNSNumberTypeConverter.m; path = Source/TypeConversion/Converters/TyphoonNSNumberTypeConverter.m; sourceTree = ""; }; 50A9EB17E5CD95FF47A7DC9D1F6FB3B4 /* YYClassInfo.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = YYClassInfo.h; path = YYModel/YYClassInfo.h; sourceTree = ""; }; 50B9C0A664812C3E14A3224A605C9402 /* SDAsyncBlockOperation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDAsyncBlockOperation.h; path = SDWebImage/Private/SDAsyncBlockOperation.h; sourceTree = ""; }; 50D1100A5FC66E513CDCEFED2FEC8138 /* UIView+WebCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIView+WebCache.m"; path = "SDWebImage/Core/UIView+WebCache.m"; sourceTree = ""; }; 533ECD8A38FC36466554EF43B205AD18 /* TyphoonBlockComponentFactory.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonBlockComponentFactory.m; path = Source/Factory/Internal/TyphoonBlockComponentFactory.m; sourceTree = ""; }; 5354038C92F8485F19D1D15D3BF5FFFC /* RACSignalSequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACSignalSequence.m; path = ReactiveObjC/RACSignalSequence.m; sourceTree = ""; }; 53B86BED43D4B74CCE944876C11E57B1 /* MKAnnotationView+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "MKAnnotationView+RACSignalSupport.m"; path = "ReactiveObjC/MKAnnotationView+RACSignalSupport.m"; sourceTree = ""; }; 54AA8A2F8E3322A66F3449823D3B76B5 /* SDmetamacros.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDmetamacros.h; path = SDWebImage/Private/SDmetamacros.h; sourceTree = ""; }; 5517D5F4CC5D9F18F26A5ED4C9034C6D /* UIView+WebCacheState.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIView+WebCacheState.m"; path = "SDWebImage/Core/UIView+WebCacheState.m"; sourceTree = ""; }; 551D5C997519E15B230BE54573BA8B99 /* RACCommand.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACCommand.m; path = ReactiveObjC/RACCommand.m; sourceTree = ""; }; 55EB526211489C964AE450F84E636291 /* RACSignal+Operations.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "RACSignal+Operations.m"; path = "ReactiveObjC/RACSignal+Operations.m"; sourceTree = ""; }; 5603722E8B41BC93F91BC672658EA10E /* OCMQuantifier.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMQuantifier.h; path = Source/OCMock/OCMQuantifier.h; sourceTree = ""; }; 5649D72ECEC9159D3DAA4B7051E947B9 /* OCMConstraint.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMConstraint.h; path = Source/OCMock/OCMConstraint.h; sourceTree = ""; }; 56D32E1C48670C43C54173C7D2B28771 /* UIColor+SDHexString.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIColor+SDHexString.h"; path = "SDWebImage/Private/UIColor+SDHexString.h"; sourceTree = ""; }; 572092422C1553EC0456C0BF53CDE8C2 /* TyphoonCollaboratingAssemblyPropertyEnumerator.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonCollaboratingAssemblyPropertyEnumerator.h; path = Source/Factory/Internal/TyphoonCollaboratingAssemblyPropertyEnumerator.h; sourceTree = ""; }; 576D3C34B2B7AC921593D4F072ECCBA3 /* SDWebImage-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SDWebImage-dummy.m"; sourceTree = ""; }; 5811A6AF5EFEA30ABF2E338DE7F3646D /* SDWebImage.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SDWebImage.release.xcconfig; sourceTree = ""; }; 5881CE458D78CE4C7C6B914138FDE55B /* OCMPassByRefSetter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMPassByRefSetter.h; path = Source/OCMock/OCMPassByRefSetter.h; sourceTree = ""; }; 588908A9A5F504D890248C42BDD5B35C /* SDImageHEICCoder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageHEICCoder.m; path = SDWebImage/Core/SDImageHEICCoder.m; sourceTree = ""; }; 5973E1C538DF3DFE2D2DCF5EF361B714 /* Masonry.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Masonry.release.xcconfig; sourceTree = ""; }; 59D821E26E6C5AFE07427222FB4AB639 /* MASCompositeConstraint.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = MASCompositeConstraint.m; path = Masonry/MASCompositeConstraint.m; sourceTree = ""; }; 59FB7D5724747D377FC8799D1133C4A5 /* NSEnumerator+RACSequenceAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSEnumerator+RACSequenceAdditions.m"; path = "ReactiveObjC/NSEnumerator+RACSequenceAdditions.m"; sourceTree = ""; }; 5A2D575E1ACB5A76D8F5214B6B4B171C /* TyphoonViewHelpers.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonViewHelpers.m; path = Source/ios/Storyboard/TyphoonViewHelpers.m; sourceTree = ""; }; 5A66CEA5D0C3EE038662C0D25CA04F50 /* RACScopedDisposable.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACScopedDisposable.h; path = ReactiveObjC/RACScopedDisposable.h; sourceTree = ""; }; 5ACE3A2A33422C2FD382351D2E96A005 /* RACErrorSignal.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACErrorSignal.m; path = ReactiveObjC/RACErrorSignal.m; sourceTree = ""; }; 5AD0D1CF37F873A7C98560DA90D7922B /* UIViewController+TyphoonStoryboardIntegration.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIViewController+TyphoonStoryboardIntegration.h"; path = "Source/ios/Storyboard/Internal/UIViewController+TyphoonStoryboardIntegration.h"; sourceTree = ""; }; 5C7C6F0CA41AC6FA5A0EAD49FADBE0DE /* NSObject+DeallocNotification.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+DeallocNotification.m"; path = "Source/Utils/NSObject+DeallocNotification.m"; sourceTree = ""; }; 5CC43EDBD34D1A25DB41669E7650524E /* TyphoonAssembly+TyphoonAssemblyFriend.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonAssembly+TyphoonAssemblyFriend.h"; path = "Source/Factory/Assembly/TyphoonAssembly+TyphoonAssemblyFriend.h"; sourceTree = ""; }; 5D1458925F5D672BF44A88E619165414 /* SDWebImageDownloader.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageDownloader.h; path = SDWebImage/Core/SDWebImageDownloader.h; sourceTree = ""; }; 5D46806A2CE11059C3A8BD7E50783943 /* TyphoonMethod.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonMethod.m; path = Source/Definition/Method/TyphoonMethod.m; sourceTree = ""; }; 5DA4577FE3BC4A03751108FFED07B385 /* DZNEmptyDataSet */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = DZNEmptyDataSet; path = DZNEmptyDataSet.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 5DD596766B816867DE3F36CD6A523540 /* SDImageHEICCoder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageHEICCoder.h; path = SDWebImage/Core/SDImageHEICCoder.h; sourceTree = ""; }; 5E23710C44440556A6042E7F7D8E172B /* UIImage+GIF.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImage+GIF.h"; path = "SDWebImage/Core/UIImage+GIF.h"; sourceTree = ""; }; 5E2C6F46F631D89037B7C021DF8B3373 /* TyphoonAssemblySelectorAdviser.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonAssemblySelectorAdviser.m; path = Source/Factory/Internal/TyphoonAssemblySelectorAdviser.m; sourceTree = ""; }; 5E440830ABB9D3025B26A80937A2EFE9 /* OCMRealObjectForwarder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMRealObjectForwarder.m; path = Source/OCMock/OCMRealObjectForwarder.m; sourceTree = ""; }; 5E4B3FE710833AB76C40A34DCAB075CA /* NSData+RACSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSData+RACSupport.h"; path = "ReactiveObjC/NSData+RACSupport.h"; sourceTree = ""; }; 5EB6B9FD0EAFE674F066B55BF02CBAD8 /* ReactiveObjC-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "ReactiveObjC-umbrella.h"; sourceTree = ""; }; 5EFE23AB30614DC2B36B61715A6990FD /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; }; 5F41C49350454308C011F60E41C91F54 /* SDImageCachesManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageCachesManager.h; path = SDWebImage/Core/SDImageCachesManager.h; sourceTree = ""; }; 5F6718C9B74C90CF46E2131FD2FB6517 /* UIViewController+TyphoonStoryboardIntegration.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIViewController+TyphoonStoryboardIntegration.m"; path = "Source/ios/Storyboard/Internal/UIViewController+TyphoonStoryboardIntegration.m"; sourceTree = ""; }; 5F8278A4894BA9FD972FEC5FCB716F39 /* TyphoonPatcher.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonPatcher.m; path = Source/Test/Patcher/TyphoonPatcher.m; sourceTree = ""; }; 5FAD9C753437FC1DCDDA7F7F6A8B9C29 /* NSObject+RACLifting.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+RACLifting.h"; path = "ReactiveObjC/NSObject+RACLifting.h"; sourceTree = ""; }; 6027F1EC9BC5F3AB67DFFEC9E6DD6BEB /* TyphoonCollaboratingAssemblyProxy.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonCollaboratingAssemblyProxy.h; path = Source/Factory/Internal/TyphoonCollaboratingAssemblyProxy.h; sourceTree = ""; }; 610276A9F0EF8CDEC1E67F7E18C1A180 /* NSData+ImageContentType.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSData+ImageContentType.h"; path = "SDWebImage/Core/NSData+ImageContentType.h"; sourceTree = ""; }; 6176156DAB1442AF8B5FF1B18364793C /* SDWeakProxy.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWeakProxy.h; path = SDWebImage/Private/SDWeakProxy.h; sourceTree = ""; }; 620115CAB08E9FF9898971DB60F30BA6 /* TyphoonPrimitiveTypeConverter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonPrimitiveTypeConverter.m; path = Source/TypeConversion/Converters/TyphoonPrimitiveTypeConverter.m; sourceTree = ""; }; 62678B3576827B7404F1FB231B054D0C /* ResourceBundle-SDWebImage-SDWebImage-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-SDWebImage-SDWebImage-Info.plist"; sourceTree = ""; }; 6296945F878C9C5DD60D6B36B3DF4E9C /* MASLayoutConstraint.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASLayoutConstraint.h; path = Masonry/MASLayoutConstraint.h; sourceTree = ""; }; 62B6BED9AA97ED0F595CFB0D93C898E2 /* TyphoonCollaboratingAssemblyPropertyEnumerator.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonCollaboratingAssemblyPropertyEnumerator.m; path = Source/Factory/Internal/TyphoonCollaboratingAssemblyPropertyEnumerator.m; sourceTree = ""; }; 63342C5E18062111D90810C1BF11878B /* UIBarButtonItem+RACCommandSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIBarButtonItem+RACCommandSupport.h"; path = "ReactiveObjC/UIBarButtonItem+RACCommandSupport.h"; sourceTree = ""; }; 633F2FE6EBD79D22A6B840F443F0A83B /* NSObject+RACKVOWrapper.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+RACKVOWrapper.h"; path = "ReactiveObjC/NSObject+RACKVOWrapper.h"; sourceTree = ""; }; 6390D1A8353BED0692FDB19AF76851C0 /* SDWebImageDownloaderDecryptor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageDownloaderDecryptor.m; path = SDWebImage/Core/SDWebImageDownloaderDecryptor.m; sourceTree = ""; }; 64094E39E3BA0988064CD05A86418E97 /* SDAnimatedImageView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDAnimatedImageView.m; path = SDWebImage/Core/SDAnimatedImageView.m; sourceTree = ""; }; 645F110D573A95F0F5F61F5666D9AD90 /* TyphoonViewControllerNibResolver.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonViewControllerNibResolver.m; path = Source/ios/Nib/TyphoonViewControllerNibResolver.m; sourceTree = ""; }; 646DB1C71B2294140046C6270BDC8C22 /* UISegmentedControl+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UISegmentedControl+RACSignalSupport.h"; path = "ReactiveObjC/UISegmentedControl+RACSignalSupport.h"; sourceTree = ""; }; 6499637B2B14B146BF05E82D9F524DB0 /* UIButton+RACCommandSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIButton+RACCommandSupport.m"; path = "ReactiveObjC/UIButton+RACCommandSupport.m"; sourceTree = ""; }; 64C5B954F96F32D4D9081CA4C2236229 /* Pods-iOS-Network-Stack-DiveTests-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iOS-Network-Stack-DiveTests-Info.plist"; sourceTree = ""; }; 65677A776ECF0D5458AAFDDA49ADDFC4 /* TyphoonComponentsPool.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonComponentsPool.h; path = Source/Factory/Pool/TyphoonComponentsPool.h; sourceTree = ""; }; 65BC03570C4B6FD508A02A616BE7DB93 /* ReactiveObjC.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = ReactiveObjC.h; path = ReactiveObjC/ReactiveObjC.h; sourceTree = ""; }; 661239F2EA786F002F7624EEE9A6FB7D /* TyphoonInjectionByCollection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByCollection.h; path = Source/Definition/Injections/TyphoonInjectionByCollection.h; sourceTree = ""; }; 661A8C92540E00912555E0561EB50777 /* TyphoonObjectWithCustomInjection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonObjectWithCustomInjection.h; path = Source/Definition/Internal/TyphoonObjectWithCustomInjection.h; sourceTree = ""; }; 6622E7C87393460832DC95149834E273 /* MASUtilities.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASUtilities.h; path = Masonry/MASUtilities.h; sourceTree = ""; }; 66D7BE61D87C0C7666DBE752021D186B /* RACMulticastConnection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACMulticastConnection.h; path = ReactiveObjC/RACMulticastConnection.h; sourceTree = ""; }; 67345DA89FFA1B9FC73EA47478CB886D /* TyphoonInjectionByObjectInstance.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByObjectInstance.m; path = Source/Definition/Injections/TyphoonInjectionByObjectInstance.m; sourceTree = ""; }; 67EB4C9AA76FDBC2CD3FFBA7EE37A27C /* RACDelegateProxy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACDelegateProxy.m; path = ReactiveObjC/RACDelegateProxy.m; sourceTree = ""; }; 686988B863D5701A04411416EC7BF222 /* RACGroupedSignal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACGroupedSignal.h; path = ReactiveObjC/RACGroupedSignal.h; sourceTree = ""; }; 68E47146ED471AD73B71268A94D9F69D /* View+MASShorthandAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "View+MASShorthandAdditions.h"; path = "Masonry/View+MASShorthandAdditions.h"; sourceTree = ""; }; 69139A572FFAF2E425701DCD352635E3 /* NSObject+PropertyInjection.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+PropertyInjection.m"; path = "Source/Utils/NSObject+PropertyInjection.m"; sourceTree = ""; }; 69EDFB408AB8C8E85794472FD278D3F4 /* NSInvocation+TCFWrapValues.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSInvocation+TCFWrapValues.m"; path = "Source/Factory/Internal/NSInvocation+TCFWrapValues.m"; sourceTree = ""; }; 6A1ED1A10CBE44A4F0190574BE33E602 /* YYModel-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "YYModel-dummy.m"; sourceTree = ""; }; 6A273691556F81E2F190244D0347ED31 /* TyphoonInjectionByCurrentRuntimeArguments.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByCurrentRuntimeArguments.m; path = Source/Definition/Injections/TyphoonInjectionByCurrentRuntimeArguments.m; sourceTree = ""; }; 6A6D607BCA593FEDA65F576E1A1F0882 /* DZNEmptyDataSet-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "DZNEmptyDataSet-prefix.pch"; sourceTree = ""; }; 6BAD6CF39127B4111B23EB0A1D7758E3 /* Pods-iOS-Network-Stack-Dive-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-iOS-Network-Stack-Dive-acknowledgements.markdown"; sourceTree = ""; }; 6C08E1C10481BD7FAC0F4A2019635945 /* Typhoon */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Typhoon; path = Typhoon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6C1ADE2AAE0ED64E1AC780F3A8C2B911 /* TyphoonInjectionByRuntimeArgument.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByRuntimeArgument.h; path = Source/Definition/Injections/TyphoonInjectionByRuntimeArgument.h; sourceTree = ""; }; 6CBEFE4F9E22AFDC6347A739BB35FF8C /* CocoaAsyncSocket */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = CocoaAsyncSocket; path = CocoaAsyncSocket.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6D7D2C3B5323BC3D3465786EE817BE2D /* CocoaAsyncSocket-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "CocoaAsyncSocket-dummy.m"; sourceTree = ""; }; 6E9BA0379B500368359C8D6E3C90BE50 /* TyphoonMethod+InstanceBuilder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonMethod+InstanceBuilder.h"; path = "Source/Definition/Method/Internal/TyphoonMethod+InstanceBuilder.h"; sourceTree = ""; }; 6EB25E54B53C87991D7A9945AD10B211 /* Pods-iOS-Network-Stack-DiveTests.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-iOS-Network-Stack-DiveTests.modulemap"; sourceTree = ""; }; 6ED217D4C98D62892AD92759A98D2928 /* SDImageAssetManager.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageAssetManager.h; path = SDWebImage/Private/SDImageAssetManager.h; sourceTree = ""; }; 702F65559FA8A65C2C9E8A7280D4E423 /* SDWebImageDefine.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageDefine.h; path = SDWebImage/Core/SDWebImageDefine.h; sourceTree = ""; }; 707BB63597C3E112E47016299D9A2C40 /* UIControl+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIControl+RACSignalSupport.h"; path = "ReactiveObjC/UIControl+RACSignalSupport.h"; sourceTree = ""; }; 70B4C8AEAC9F4A07E37287417595490E /* TyphoonInjectionByDictionary.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByDictionary.m; path = Source/Definition/Injections/TyphoonInjectionByDictionary.m; sourceTree = ""; }; 712ED30137ED21790E81D661BCDBE41A /* TyphoonBlockDefinition.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonBlockDefinition.h; path = Source/Definition/TyphoonBlockDefinition.h; sourceTree = ""; }; 71C1C00FFE25AA899E9B77567B46E8CA /* TyphoonViewControllerFactory.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonViewControllerFactory.h; path = Source/ios/Storyboard/TyphoonViewControllerFactory.h; sourceTree = ""; }; 71CC76D142ADC81938ACEAA26EA01492 /* TyphoonInjectionByConfig.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByConfig.m; path = Source/Definition/Injections/TyphoonInjectionByConfig.m; sourceTree = ""; }; 720CFEBFB23833C0091EFD8767731EA7 /* TyphoonPropertyStyleConfiguration.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonPropertyStyleConfiguration.m; path = Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonPropertyStyleConfiguration.m; sourceTree = ""; }; 72274AF631E665EB74378E781E9A7437 /* NSObject+RACDescription.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+RACDescription.h"; path = "ReactiveObjC/NSObject+RACDescription.h"; sourceTree = ""; }; 72AB4D998C2E02839ACA5C88AC0929A9 /* SDImageLoadersManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageLoadersManager.m; path = SDWebImage/Core/SDImageLoadersManager.m; sourceTree = ""; }; 72ADBA2DDCD657373F64C49B51DA5B7D /* Reachability.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Reachability.debug.xcconfig; sourceTree = ""; }; 72C6CF5849F4A490EF85880EED97E773 /* SDWebImageOptionsProcessor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageOptionsProcessor.m; path = SDWebImage/Core/SDWebImageOptionsProcessor.m; sourceTree = ""; }; 7333039A34CFB7AC3CD117DC76BF5999 /* SDWebImageTransition.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageTransition.m; path = SDWebImage/Core/SDWebImageTransition.m; sourceTree = ""; }; 73855182D583882A717B2006B73A0953 /* OCMFunctionsPrivate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMFunctionsPrivate.h; path = Source/OCMock/OCMFunctionsPrivate.h; sourceTree = ""; }; 744D46F0AC450056AAE46AA71A878857 /* UITableViewHeaderFooterView+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UITableViewHeaderFooterView+RACSignalSupport.h"; path = "ReactiveObjC/UITableViewHeaderFooterView+RACSignalSupport.h"; sourceTree = ""; }; 746C9C6D96CC90B1558DBCDA1EF575B4 /* SDCallbackQueue.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDCallbackQueue.m; path = SDWebImage/Core/SDCallbackQueue.m; sourceTree = ""; }; 749EDDB4FA8F282C297D2ED71E5E2A95 /* RACSubject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACSubject.m; path = ReactiveObjC/RACSubject.m; sourceTree = ""; }; 74A9B68BB1F2C7DC615156DBA4077781 /* SDImageIOCoder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageIOCoder.m; path = SDWebImage/Core/SDImageIOCoder.m; sourceTree = ""; }; 74E20E5BB34220E9282F27E20A9A567E /* NSEnumerator+RACSequenceAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSEnumerator+RACSequenceAdditions.h"; path = "ReactiveObjC/NSEnumerator+RACSequenceAdditions.h"; sourceTree = ""; }; 74FEA9E372A4AFA285192EC20BF9AC10 /* RACCommand.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACCommand.h; path = ReactiveObjC/RACCommand.h; sourceTree = ""; }; 75013D4007A6A538ACAE4686AAC93DE4 /* TyphoonPassThroughTypeConverter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonPassThroughTypeConverter.m; path = Source/TypeConversion/Converters/TyphoonPassThroughTypeConverter.m; sourceTree = ""; }; 75D795D038F45CC15803AC36BBFEBF32 /* TyphoonBlockComponentFactory.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonBlockComponentFactory.h; path = Source/Factory/Internal/TyphoonBlockComponentFactory.h; sourceTree = ""; }; 765309DEC91CA062A4172B75C508325C /* TyphoonInjectionByConfig.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByConfig.h; path = Source/Definition/Injections/TyphoonInjectionByConfig.h; sourceTree = ""; }; 76C8CC4DEAB56D01254A5E21663A9CB3 /* Typhoon+Infrastructure.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "Typhoon+Infrastructure.h"; path = "Source/Typhoon+Infrastructure.h"; sourceTree = ""; }; 772D4121E01F86089D23DBEBD83FC5AD /* TyphoonAssemblySelectorAdviser.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAssemblySelectorAdviser.h; path = Source/Factory/Internal/TyphoonAssemblySelectorAdviser.h; sourceTree = ""; }; 78AC2407E9C7F573D2B8F2765AD61982 /* UIRefreshControl+RACCommandSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIRefreshControl+RACCommandSupport.m"; path = "ReactiveObjC/UIRefreshControl+RACCommandSupport.m"; sourceTree = ""; }; 78E1C3D8226EEA6918CB1B8A96FC843A /* OCProtocolMockObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCProtocolMockObject.m; path = Source/OCMock/OCProtocolMockObject.m; sourceTree = ""; }; 793AD3A27E2A7A7D6570D1D0B8E748CF /* RACSequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACSequence.m; path = ReactiveObjC/RACSequence.m; sourceTree = ""; }; 795FD8B2FD7ED943A340C32633ED18F7 /* NSFileHandle+RACSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSFileHandle+RACSupport.h"; path = "ReactiveObjC/NSFileHandle+RACSupport.h"; sourceTree = ""; }; 79AE668D1D91F91A64F0E74D1BA417EF /* SDWebImageDownloaderOperation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageDownloaderOperation.h; path = SDWebImage/Core/SDWebImageDownloaderOperation.h; sourceTree = ""; }; 7A37BECA26F996876B784EFD357DC96A /* TyphoonInjectionsEnumeration.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionsEnumeration.h; path = Source/Definition/Infrastructure/TyphoonInjectionsEnumeration.h; sourceTree = ""; }; 7A4EBA3FAF480391CADE1DF78A1EB2DA /* UIImage+GIF.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImage+GIF.m"; path = "SDWebImage/Core/UIImage+GIF.m"; sourceTree = ""; }; 7A672A6EEB28F3A0ADB373CDCB3697AE /* TyphoonInject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInject.m; path = Source/Definition/Injections/TyphoonInject.m; sourceTree = ""; }; 7B345A1E24575BA9D0BC33710FE943C0 /* TyphoonDefinition+Option.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "TyphoonDefinition+Option.m"; path = "Source/Configuration/DefinitionOptionConfiguration/TyphoonDefinition+Option.m"; sourceTree = ""; }; 7BDD9A3C6FE6300A4953E8AC4C585381 /* Masonry-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Masonry-dummy.m"; sourceTree = ""; }; 7C4D52064EBE28BCC66250183EE0A11E /* GCDAsyncUdpSocket.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GCDAsyncUdpSocket.m; path = Source/GCD/GCDAsyncUdpSocket.m; sourceTree = ""; }; 7C9741B2966A4017CFF5F1396954F520 /* TyphoonBlockDefinitionController.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonBlockDefinitionController.h; path = Source/Definition/Internal/TyphoonBlockDefinitionController.h; sourceTree = ""; }; 7CC392EF120EE41995D6918D82EC742B /* NSObject+YYModel.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+YYModel.m"; path = "YYModel/NSObject+YYModel.m"; sourceTree = ""; }; 7CE2A8DCEE78DE1933B81C994103BE63 /* NSDictionary+RACSequenceAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSDictionary+RACSequenceAdditions.m"; path = "ReactiveObjC/NSDictionary+RACSequenceAdditions.m"; sourceTree = ""; }; 7CF6453EBAFEAB28E5DCF1E0FEA52A59 /* UIView+TyphoonOutletTransfer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIView+TyphoonOutletTransfer.h"; path = "Source/ios/Storyboard/UIView+TyphoonOutletTransfer.h"; sourceTree = ""; }; 7D1BD5E2CF0B45A47FC6BC88583E5B26 /* SDDeviceHelper.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDDeviceHelper.m; path = SDWebImage/Private/SDDeviceHelper.m; sourceTree = ""; }; 7D81778D9E6487677837D58B13894E94 /* RACSignalSequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACSignalSequence.h; path = ReactiveObjC/RACSignalSequence.h; sourceTree = ""; }; 7DB7EDBC439037E423096117CAA5C2D6 /* TyphoonViewControllerInjector.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonViewControllerInjector.h; path = Source/ios/Configuration/TyphoonViewControllerInjector.h; sourceTree = ""; }; 7DF1AA147C3C7EA2E20CEC9444F9EDF2 /* SDImageGraphics.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageGraphics.m; path = SDWebImage/Core/SDImageGraphics.m; sourceTree = ""; }; 7E12AEC65EEC09FD15C652679375036E /* SDFileAttributeHelper.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDFileAttributeHelper.m; path = SDWebImage/Private/SDFileAttributeHelper.m; sourceTree = ""; }; 7E53BFD7942CD993EE50F7FD12C87B2C /* OCMMacroState.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMMacroState.h; path = Source/OCMock/OCMMacroState.h; sourceTree = ""; }; 7E607FCD32089EA9D313F72664F0B17B /* TyphoonInjectionByRuntimeArgument.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByRuntimeArgument.m; path = Source/Definition/Injections/TyphoonInjectionByRuntimeArgument.m; sourceTree = ""; }; 7EB4C9DFCDF6EB86413C60976F25BE67 /* UIAlertView+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIAlertView+RACSignalSupport.h"; path = "ReactiveObjC/UIAlertView+RACSignalSupport.h"; sourceTree = ""; }; 7F2869DEA989F62C012853D92680ECEC /* TyphoonPreattachedComponentsRegisterer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonPreattachedComponentsRegisterer.m; path = Source/Factory/TyphoonPreattachedComponentsRegisterer.m; sourceTree = ""; }; 7F84B399A305F1F7EAFB02599E56A1B1 /* OCProtocolMockObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCProtocolMockObject.h; path = Source/OCMock/OCProtocolMockObject.h; sourceTree = ""; }; 80308619B6E726C5AFA5F9E661DA48A5 /* Pods-iOS-Network-Stack-DiveTests-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-iOS-Network-Stack-DiveTests-acknowledgements.markdown"; sourceTree = ""; }; 8065BF1B5F4AFDB278D6AB4C732153BE /* UIView+TyphoonOutletTransfer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIView+TyphoonOutletTransfer.m"; path = "Source/ios/Storyboard/UIView+TyphoonOutletTransfer.m"; sourceTree = ""; }; 80863B62A376A21783063A91F74E52C6 /* OCMInvocationExpectation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMInvocationExpectation.m; path = Source/OCMock/OCMInvocationExpectation.m; sourceTree = ""; }; 8090506D09D6280408FEFDC474C367CF /* RACStream+Private.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "RACStream+Private.h"; path = "ReactiveObjC/RACStream+Private.h"; sourceTree = ""; }; 80E8E2470B296C3F1F4F779B79894FB3 /* MASConstraint.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = MASConstraint.m; path = Masonry/MASConstraint.m; sourceTree = ""; }; 814AAFBE786E0EBCAFD7AC642DA8F8EF /* OCMMacroState.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMMacroState.m; path = Source/OCMock/OCMMacroState.m; sourceTree = ""; }; 818F3C6FD63BEF6B17D992E201321502 /* Collections+CustomInjection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "Collections+CustomInjection.h"; path = "Source/Definition/Internal/Collections+CustomInjection.h"; sourceTree = ""; }; 81E027BECECC686145B8874E339146B3 /* SDWebImage.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImage.h; path = WebImage/SDWebImage.h; sourceTree = ""; }; 821F3BAC1C291831ECD436462BB57063 /* TyphoonJsonStyleConfiguration.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonJsonStyleConfiguration.h; path = Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonJsonStyleConfiguration.h; sourceTree = ""; }; 8233301E93D80EEC51CC7AFF84085AC7 /* SDImageAssetManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageAssetManager.m; path = SDWebImage/Private/SDImageAssetManager.m; sourceTree = ""; }; 824B55D57BB9EC7E920131DD9197C61E /* Pods-iOS-Network-Stack-Dive-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iOS-Network-Stack-Dive-acknowledgements.plist"; sourceTree = ""; }; 826AE09BE9A75B5E4F07DCA8BD6E568A /* SDAnimatedImageView+WebCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "SDAnimatedImageView+WebCache.m"; path = "SDWebImage/Core/SDAnimatedImageView+WebCache.m"; sourceTree = ""; }; 82A6DCEB5EBCB25A5BF6375B4D2FE619 /* SDImageCoder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageCoder.m; path = SDWebImage/Core/SDImageCoder.m; sourceTree = ""; }; 83044A60173A4F57FC96702D95EF4DB8 /* SDImageTransformer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageTransformer.h; path = SDWebImage/Core/SDImageTransformer.h; sourceTree = ""; }; 83104DD312D9864E2BAA39D25537ADAA /* OCMVerifier.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMVerifier.m; path = Source/OCMock/OCMVerifier.m; sourceTree = ""; }; 8375DB2B7CC0F5AD235F69805AF4B1BA /* OCMockObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMockObject.m; path = Source/OCMock/OCMockObject.m; sourceTree = ""; }; 8386D6BB6CF3F2D21ED967CC288819B0 /* NSObject+RACSelectorSignal.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+RACSelectorSignal.m"; path = "ReactiveObjC/NSObject+RACSelectorSignal.m"; sourceTree = ""; }; 838C51303B89542DD2D308D0C408F616 /* MASConstraintMaker.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = MASConstraintMaker.m; path = Masonry/MASConstraintMaker.m; sourceTree = ""; }; 83901A74DF9DB0A0733D4591727B3770 /* UISwitch+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UISwitch+RACSignalSupport.m"; path = "ReactiveObjC/UISwitch+RACSignalSupport.m"; sourceTree = ""; }; 83F375B929B1442DA30D02F6A861124C /* TyphoonComponentFactory+Storyboard.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonComponentFactory+Storyboard.h"; path = "Source/ios/Storyboard/TyphoonComponentFactory+Storyboard.h"; sourceTree = ""; }; 83FC227B502F54235D6CCDB02449F168 /* TyphoonInjectionByObjectInstance.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByObjectInstance.h; path = Source/Definition/Injections/TyphoonInjectionByObjectInstance.h; sourceTree = ""; }; 84E53FAE993E1AC61F28D1CDC8FF68BC /* SDWebImageDownloaderOperation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageDownloaderOperation.m; path = SDWebImage/Core/SDWebImageDownloaderOperation.m; sourceTree = ""; }; 8536C813226AC2CB3B45AB654F52A79D /* NSObject+RACKVOWrapper.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+RACKVOWrapper.m"; path = "ReactiveObjC/NSObject+RACKVOWrapper.m"; sourceTree = ""; }; 856CE023A29559B385271D8014E85BC3 /* RACKVOProxy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACKVOProxy.m; path = ReactiveObjC/RACKVOProxy.m; sourceTree = ""; }; 8597C3C4E469AD35635B7B0381951012 /* YYModel-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "YYModel-Info.plist"; sourceTree = ""; }; 85BADAC4F3DCD2CDEA7EE692773C47A3 /* SDImageCodersManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageCodersManager.m; path = SDWebImage/Core/SDImageCodersManager.m; sourceTree = ""; }; 861A84C50B238264389CE8E34734EE4C /* RACEvent.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACEvent.m; path = ReactiveObjC/RACEvent.m; sourceTree = ""; }; 871E83256238E28BEC109FBAA903A7BE /* RACKVOTrampoline.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACKVOTrampoline.m; path = ReactiveObjC/RACKVOTrampoline.m; sourceTree = ""; }; 8725F3C4CF3EA8CA846B7A932532168D /* TyphoonComponentFactory.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonComponentFactory.m; path = Source/Factory/TyphoonComponentFactory.m; sourceTree = ""; }; 8732F0D1FA8AA62E703DE7973280BA55 /* TyphoonViewControllerNibResolver.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonViewControllerNibResolver.h; path = Source/ios/Nib/TyphoonViewControllerNibResolver.h; sourceTree = ""; }; 875B63A814EBB6CC52061388F53BC97D /* TyphoonGlobalConfigCollector.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonGlobalConfigCollector.h; path = Source/Configuration/GlobalConfigResolver/TyphoonGlobalConfigCollector.h; sourceTree = ""; }; 87B55C9AC3241F2442119F64473D1F41 /* TyphoonPlistStyleConfiguration.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonPlistStyleConfiguration.m; path = Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonPlistStyleConfiguration.m; sourceTree = ""; }; 87D6C46EF50641EB979304EC92ADD341 /* UICollectionReusableView+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UICollectionReusableView+RACSignalSupport.m"; path = "ReactiveObjC/UICollectionReusableView+RACSignalSupport.m"; sourceTree = ""; }; 88024AA70385D01B3432E99546EFA4E1 /* TyphoonRuntimeArguments.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonRuntimeArguments.h; path = Source/Factory/Internal/TyphoonRuntimeArguments.h; sourceTree = ""; }; 8866DCC2E6B6A7335F7564DFAAA6323F /* TyphoonCollaboratingAssemblyProxy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonCollaboratingAssemblyProxy.m; path = Source/Factory/Internal/TyphoonCollaboratingAssemblyProxy.m; sourceTree = ""; }; 88A89A507382B833489D6DB8D75E6DE2 /* GCDAsyncSocket.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = GCDAsyncSocket.m; path = Source/GCD/GCDAsyncSocket.m; sourceTree = ""; }; 891C96D521BE0268A6C2A44DF6CF804B /* TyphoonInjectionByFactoryReference.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByFactoryReference.h; path = Source/Definition/Injections/TyphoonInjectionByFactoryReference.h; sourceTree = ""; }; 89570B4A4AE434196E38ABEADF0CB97E /* RACSignal+Operations.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "RACSignal+Operations.h"; path = "ReactiveObjC/RACSignal+Operations.h"; sourceTree = ""; }; 89A79290D890BC1A7E558C5CC31E06E3 /* RACValueTransformer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACValueTransformer.h; path = ReactiveObjC/RACValueTransformer.h; sourceTree = ""; }; 89FE3BF0E622ACD1D48ABA7DCFF7855A /* RACSignalProvider.d */ = {isa = PBXFileReference; includeInIndex = 1; name = RACSignalProvider.d; path = ReactiveObjC/RACSignalProvider.d; sourceTree = ""; }; 8A14CECA21E108EE12451203537A6B99 /* RACStringSequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACStringSequence.h; path = ReactiveObjC/RACStringSequence.h; sourceTree = ""; }; 8A6B4184DF30072301A96D9DB46F53F7 /* SDWebImage-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SDWebImage-prefix.pch"; sourceTree = ""; }; 8AC5ECC34DB25CB5305086B539A17417 /* SDImageCacheConfig.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageCacheConfig.h; path = SDWebImage/Core/SDImageCacheConfig.h; sourceTree = ""; }; 8AEA2F92AE161A3CD9059649BB497327 /* Pods-iOS-Network-Stack-Dive */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-iOS-Network-Stack-Dive"; path = Pods_iOS_Network_Stack_Dive.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8B0A489E0C0397C62B216A44A21EDDFF /* UIStepper+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIStepper+RACSignalSupport.h"; path = "ReactiveObjC/UIStepper+RACSignalSupport.h"; sourceTree = ""; }; 8B16EB1B90C8121FC99C6D634CE916AA /* TyphoonConfigPostProcessor+Internal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonConfigPostProcessor+Internal.h"; path = "Source/Configuration/ConfigPostProcessor/Internal/TyphoonConfigPostProcessor+Internal.h"; sourceTree = ""; }; 8B2D3D98B9F08D80C8134D1C5346BA57 /* RACCompoundDisposable.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACCompoundDisposable.h; path = ReactiveObjC/RACCompoundDisposable.h; sourceTree = ""; }; 8B40A0C264DE39B5EEE32E9062A0CCF2 /* Reachability-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Reachability-umbrella.h"; sourceTree = ""; }; 8C279B3C5F55629685F87467C53EA540 /* Reachability-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Reachability-Info.plist"; sourceTree = ""; }; 8C35F5A9D87E1BF9E90AD737FA10D987 /* Typhoon-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Typhoon-umbrella.h"; sourceTree = ""; }; 8C3A7610419191166B2C17925674DE17 /* OCMObjectReturnValueProvider.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMObjectReturnValueProvider.m; path = Source/OCMock/OCMObjectReturnValueProvider.m; sourceTree = ""; }; 8C3F45C3BA62C5E06E78277D0D81E73D /* OCMStubRecorder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMStubRecorder.m; path = Source/OCMock/OCMStubRecorder.m; sourceTree = ""; }; 8CC52EA095AA94DD387D255F058AF87F /* SDImageCoderHelper.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageCoderHelper.m; path = SDWebImage/Core/SDImageCoderHelper.m; sourceTree = ""; }; 8CCB0069A6BAC8CD752488DF7B7D42B7 /* Collections+CustomInjection.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "Collections+CustomInjection.m"; path = "Source/Definition/Internal/Collections+CustomInjection.m"; sourceTree = ""; }; 8D3BAA9F0A9900B2F727C2916F30C12A /* TyphoonAssemblyDefinitionBuilder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAssemblyDefinitionBuilder.h; path = Source/Factory/Internal/TyphoonAssemblyDefinitionBuilder.h; sourceTree = ""; }; 8D633DA1F2ABA3E82E11435AE33357EC /* CocoaAsyncSocket-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "CocoaAsyncSocket-Info.plist"; sourceTree = ""; }; 8D713D070421A1E803471AA59CAE1B25 /* UIView+TyphoonDefinitionKey.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIView+TyphoonDefinitionKey.h"; path = "Source/ios/Storyboard/Internal/UIView+TyphoonDefinitionKey.h"; sourceTree = ""; }; 8D76E7370C1E6F4EE5AE4D70E5C4D2C2 /* UIControl+RACSignalSupportPrivate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIControl+RACSignalSupportPrivate.h"; path = "ReactiveObjC/UIControl+RACSignalSupportPrivate.h"; sourceTree = ""; }; 8D7A7568A955352E3C8BEA8C6BE5EFA7 /* NSString+RACSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+RACSupport.h"; path = "ReactiveObjC/NSString+RACSupport.h"; sourceTree = ""; }; 8D8C5527AA1D03083903623D593614BE /* TyphoonAssembly.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonAssembly.m; path = Source/Factory/Assembly/TyphoonAssembly.m; sourceTree = ""; }; 8D91285C522F06C05B537D395E259F59 /* TyphoonStoryboardDefinition.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonStoryboardDefinition.h; path = Source/ios/Definition/TyphoonStoryboardDefinition.h; sourceTree = ""; }; 8E22B83E5AD31A7FB5BFE354EF0D299C /* DZNEmptyDataSet-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "DZNEmptyDataSet-Info.plist"; sourceTree = ""; }; 8EA5D8C4206359854EDBE37A94AF6B6A /* NSLayoutConstraint+TyphoonOutletTransfer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSLayoutConstraint+TyphoonOutletTransfer.h"; path = "Source/ios/Storyboard/NSLayoutConstraint+TyphoonOutletTransfer.h"; sourceTree = ""; }; 8F0A29B88BD1DA370225345A593E408F /* TyphoonCallStack.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonCallStack.h; path = Source/Factory/Internal/TyphoonCallStack.h; sourceTree = ""; }; 8F1A6A9C14EDD26D5A9D9CE5506A3971 /* UIControl+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIControl+RACSignalSupport.m"; path = "ReactiveObjC/UIControl+RACSignalSupport.m"; sourceTree = ""; }; 8F54C727953E14DD511A46A9F90E8937 /* OCObserverMockObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCObserverMockObject.h; path = Source/OCMock/OCObserverMockObject.h; sourceTree = ""; }; 8F92503B695A9FE309F61E102877877B /* Masonry-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Masonry-prefix.pch"; sourceTree = ""; }; 900C7737DD6EC812009057A2F330E295 /* TyphoonInjectionDefinition.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionDefinition.m; path = Source/Definition/Internal/TyphoonInjectionDefinition.m; sourceTree = ""; }; 903891AB3F9D2ABE990A52BF6D7CE724 /* MASCompositeConstraint.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASCompositeConstraint.h; path = Masonry/MASCompositeConstraint.h; sourceTree = ""; }; 906070901EFA9ECD175C85348FF57C1E /* TyphoonTypeConverterRegistry.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonTypeConverterRegistry.m; path = Source/TypeConversion/TyphoonTypeConverterRegistry.m; sourceTree = ""; }; 907EE3AB436219C47FE62434C1DCFB33 /* UIImageView+HighlightedWebCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImageView+HighlightedWebCache.h"; path = "SDWebImage/Core/UIImageView+HighlightedWebCache.h"; sourceTree = ""; }; 909F75C790C08B1A24A71FF138EA5C7B /* UIImage+ExtendedCacheData.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImage+ExtendedCacheData.h"; path = "SDWebImage/Core/UIImage+ExtendedCacheData.h"; sourceTree = ""; }; 90F23D124204BAE6E0746E042588193F /* TyphoonFactoryPropertyInjectionPostProcessor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonFactoryPropertyInjectionPostProcessor.m; path = Source/Factory/Internal/TyphoonFactoryPropertyInjectionPostProcessor.m; sourceTree = ""; }; 918735CA6B8C36A5D220733BC8F45075 /* Masonry.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Masonry.debug.xcconfig; sourceTree = ""; }; 91A1F2245A7DAF44FC87AADAFA9E9BA9 /* SDImageCachesManagerOperation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageCachesManagerOperation.h; path = SDWebImage/Private/SDImageCachesManagerOperation.h; sourceTree = ""; }; 91DE4AD4922A1500C3D8808438EBC84A /* UIScrollView+EmptyDataSet.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIScrollView+EmptyDataSet.m"; path = "Source/UIScrollView+EmptyDataSet.m"; sourceTree = ""; }; 9240686C7AC135C5EA0942E5C3D224AC /* ReactiveObjC.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ReactiveObjC.debug.xcconfig; sourceTree = ""; }; 9267DAB90224A45DA4C991474C964341 /* UIRefreshControl+RACCommandSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIRefreshControl+RACCommandSupport.h"; path = "ReactiveObjC/UIRefreshControl+RACCommandSupport.h"; sourceTree = ""; }; 92A096C440891899FBCBB081ED6C8474 /* SDDeviceHelper.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDDeviceHelper.h; path = SDWebImage/Private/SDDeviceHelper.h; sourceTree = ""; }; 92B2A700D6653C0C3364645A2E691CC0 /* ViewController+MASAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "ViewController+MASAdditions.m"; path = "Masonry/ViewController+MASAdditions.m"; sourceTree = ""; }; 9301B5A21675C37DED58F6472417FFF1 /* TyphoonInjectionDefinition.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionDefinition.h; path = Source/Definition/Internal/TyphoonInjectionDefinition.h; sourceTree = ""; }; 933EF5CE0D7C9A91603A2F91BDFD0871 /* SDWebImageOptionsProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageOptionsProcessor.h; path = SDWebImage/Core/SDWebImageOptionsProcessor.h; sourceTree = ""; }; 93894FEF634076038AE57F0DA89A230A /* UIControl+RACSignalSupportPrivate.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIControl+RACSignalSupportPrivate.m"; path = "ReactiveObjC/UIControl+RACSignalSupportPrivate.m"; sourceTree = ""; }; 94138F5AF19A8078AD96AA7CBA29AB4A /* TyphoonBundleResource.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonBundleResource.h; path = Source/Configuration/Resource/TyphoonBundleResource.h; sourceTree = ""; }; 945289B9A84910C8F31646C46C4531EB /* RACEagerSequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACEagerSequence.h; path = ReactiveObjC/RACEagerSequence.h; sourceTree = ""; }; 94E380F27B3CBBA506348C94A1707E2E /* TyphoonUtils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonUtils.h; path = Source/Utils/TyphoonUtils.h; sourceTree = ""; }; 950CD6BE6707ED534FF448D9CC40FA3D /* RACmetamacros.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACmetamacros.h; path = ReactiveObjC/extobjc/RACmetamacros.h; sourceTree = ""; }; 952891A7D50B078702F1A3896E8BA349 /* SDInternalMacros.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDInternalMacros.m; path = SDWebImage/Private/SDInternalMacros.m; sourceTree = ""; }; 955DB0048C92F2F02195AF9A7C000F04 /* TyphoonBlockDefinition+InstanceBuilder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonBlockDefinition+InstanceBuilder.h"; path = "Source/Definition/Internal/TyphoonBlockDefinition+InstanceBuilder.h"; sourceTree = ""; }; 95DC39645A666C2B0AF83523FAD30758 /* TyphoonSwizzlerDefaultImpl.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonSwizzlerDefaultImpl.m; path = Source/Utils/Swizzle/TyphoonSwizzlerDefaultImpl.m; sourceTree = ""; }; 95E34BD0B0987191F26A0B35F90CF6E5 /* TyphoonSelector.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonSelector.m; path = Source/Utils/TyphoonSelector.m; sourceTree = ""; }; 9621C6383F5733C35183B2DE886C7EC6 /* ReactiveObjC */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = ReactiveObjC; path = ReactiveObjC.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9630797851476E7C030A663FB01C053D /* MASConstraint.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASConstraint.h; path = Masonry/MASConstraint.h; sourceTree = ""; }; 9752AAF7F49C7CFA8636AB119BF88C35 /* OCMIndirectReturnValueProvider.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMIndirectReturnValueProvider.m; path = Source/OCMock/OCMIndirectReturnValueProvider.m; sourceTree = ""; }; 9754B128A1DF8F57658751C1302FF46F /* RACArraySequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACArraySequence.h; path = ReactiveObjC/RACArraySequence.h; sourceTree = ""; }; 977CC2B85F6C66B4997C1A77F749621E /* SDAsyncBlockOperation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDAsyncBlockOperation.m; path = SDWebImage/Private/SDAsyncBlockOperation.m; sourceTree = ""; }; 984867BE9A2FC89AE6F4546840D5113D /* ReactiveObjC-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ReactiveObjC-Info.plist"; sourceTree = ""; }; 9858D4D21F87468E116E7410167A453A /* TyphoonMemoryManagementUtils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonMemoryManagementUtils.h; path = Source/Factory/Internal/TyphoonMemoryManagementUtils.h; sourceTree = ""; }; 98854B7F25ABB6BE45BB69C58166AA5E /* TyphoonNSURLTypeConverter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonNSURLTypeConverter.h; path = Source/TypeConversion/Converters/TyphoonNSURLTypeConverter.h; sourceTree = ""; }; 98A3E71FFFC45DA5BB85A8525B39AC52 /* NSURLConnection+RACSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSURLConnection+RACSupport.h"; path = "ReactiveObjC/NSURLConnection+RACSupport.h"; sourceTree = ""; }; 98D4BFAFED921E3A451A3FCC58BD5A0A /* TyphoonInjections.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjections.m; path = Source/Definition/Injections/TyphoonInjections.m; sourceTree = ""; }; 996F286581DA660FC5CB9DCBB431AF08 /* SDImageIOCoder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageIOCoder.h; path = SDWebImage/Core/SDImageIOCoder.h; sourceTree = ""; }; 9998C36BB30E19196F7C6F7D1F3EF49B /* NSObject+RACPropertySubscribing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+RACPropertySubscribing.h"; path = "ReactiveObjC/NSObject+RACPropertySubscribing.h"; sourceTree = ""; }; 9A1718181F5BEF030AA0CA446A6686F2 /* UITableViewCell+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UITableViewCell+RACSignalSupport.m"; path = "ReactiveObjC/UITableViewCell+RACSignalSupport.m"; sourceTree = ""; }; 9A2036089AC8297FFEE003F786496211 /* RACSequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACSequence.h; path = ReactiveObjC/RACSequence.h; sourceTree = ""; }; 9A3B0D97818AFB9EC630C3B522130631 /* TyphoonConfigPostProcessor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonConfigPostProcessor.m; path = Source/Configuration/ConfigPostProcessor/TyphoonConfigPostProcessor.m; sourceTree = ""; }; 9AA886724CF6656F190D039297F38E78 /* RACQueueScheduler+Subclass.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "RACQueueScheduler+Subclass.h"; path = "ReactiveObjC/RACQueueScheduler+Subclass.h"; sourceTree = ""; }; 9AE77ABB8D49E71EE7F0CFAFEC9710A4 /* UISwitch+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UISwitch+RACSignalSupport.h"; path = "ReactiveObjC/UISwitch+RACSignalSupport.h"; sourceTree = ""; }; 9B69333B75E26F15AA2E42D06EF62B15 /* TyphoonPrimitiveTypeConverter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonPrimitiveTypeConverter.h; path = Source/TypeConversion/Converters/TyphoonPrimitiveTypeConverter.h; sourceTree = ""; }; 9B6EEC7E6EC11E55D1422113BAB387BF /* SDWebImageDownloaderConfig.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageDownloaderConfig.h; path = SDWebImage/Core/SDWebImageDownloaderConfig.h; sourceTree = ""; }; 9B744979B63E3F8D6CD8B04CCE7E0E58 /* YYModel-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "YYModel-umbrella.h"; sourceTree = ""; }; 9B87846900F219AB28D136A1C7C67730 /* CocoaAsyncSocket.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = CocoaAsyncSocket.debug.xcconfig; sourceTree = ""; }; 9BE05A31C373E76C2393361CF1E8AF9C /* Typhoon-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Typhoon-dummy.m"; sourceTree = ""; }; 9BFCB32C228CF4CC491157D1716E631D /* TyphoonInjectionContext.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionContext.h; path = Source/Definition/Injections/TyphoonInjectionContext.h; sourceTree = ""; }; 9C06ABD459BD9D2EC039A4278068AC2B /* TyphoonAbstractInjection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAbstractInjection.h; path = Source/Definition/Injections/TyphoonAbstractInjection.h; sourceTree = ""; }; 9C26E7915BAEE3BDE02BCD9D2D389B9A /* OCMFunctions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMFunctions.m; path = Source/OCMock/OCMFunctions.m; sourceTree = ""; }; 9CB472E36DC6E2E3FEF5B6638CA34EC5 /* NSInvocation+RACTypeParsing.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSInvocation+RACTypeParsing.h"; path = "ReactiveObjC/NSInvocation+RACTypeParsing.h"; sourceTree = ""; }; 9CD76B64F3F84AD0EA28EDA5BE8A26BE /* SDImageFrame.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageFrame.m; path = SDWebImage/Core/SDImageFrame.m; sourceTree = ""; }; 9D066AE9F0D7A4328D8690DF15F20AA0 /* OCMBlockArgCaller.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMBlockArgCaller.h; path = Source/OCMock/OCMBlockArgCaller.h; sourceTree = ""; }; 9D6B350707C3EC3D1DAE907D140B8969 /* SDMemoryCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDMemoryCache.h; path = SDWebImage/Core/SDMemoryCache.h; sourceTree = ""; }; 9D9038CEC928B9AF5CFA2FD0B75B9FEF /* RACIndexSetSequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACIndexSetSequence.h; path = ReactiveObjC/RACIndexSetSequence.h; sourceTree = ""; }; 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; 9DA14F675C32DB069C21840DFF0D5B75 /* UIBarButtonItem+RACCommandSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIBarButtonItem+RACCommandSupport.m"; path = "ReactiveObjC/UIBarButtonItem+RACCommandSupport.m"; sourceTree = ""; }; 9E18DA0F8466E227AA17A93CF40D4C08 /* NSBezierPath+SDRoundedCorners.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSBezierPath+SDRoundedCorners.h"; path = "SDWebImage/Private/NSBezierPath+SDRoundedCorners.h"; sourceTree = ""; }; 9EB5CC7B3930FE88F26ECB6C25761E21 /* TyphoonAbstractInjection.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonAbstractInjection.m; path = Source/Definition/Injections/TyphoonAbstractInjection.m; sourceTree = ""; }; A09A92A8038E8620B3B2A61D8A929180 /* UIImageView+WebCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImageView+WebCache.m"; path = "SDWebImage/Core/UIImageView+WebCache.m"; sourceTree = ""; }; A0B26D8004A9D82FA6F99C42A9F8F1BD /* Typhoon-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Typhoon-prefix.pch"; sourceTree = ""; }; A13902D081C57317FEA1F09843D569F4 /* TyphoonStartup.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonStartup.h; path = Source/Configuration/Startup/TyphoonStartup.h; sourceTree = ""; }; A139B74B3BD8E154D5BC6567D4D692D4 /* OCMStubRecorder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMStubRecorder.h; path = Source/OCMock/OCMStubRecorder.h; sourceTree = ""; }; A146863CEB210DB61D7A021374982754 /* TyphoonDefinition+Option.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonDefinition+Option.h"; path = "Source/Configuration/DefinitionOptionConfiguration/TyphoonDefinition+Option.h"; sourceTree = ""; }; A16A247B48BF9C8E1B54650152D9F47E /* SDWebImageTransition.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageTransition.h; path = SDWebImage/Core/SDWebImageTransition.h; sourceTree = ""; }; A18C6BD836E25BB4DA9691CE3262B0B6 /* SDWebImage-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SDWebImage-Info.plist"; sourceTree = ""; }; A20BC1B3BD8F0D819039127EE5346176 /* Masonry.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = Masonry.h; path = Masonry/Masonry.h; sourceTree = ""; }; A28930506531365373875AD4CA53EDEC /* RACSubject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACSubject.h; path = ReactiveObjC/RACSubject.h; sourceTree = ""; }; A2ABDBBFB28FCCED57D0D680E16EBA76 /* OCMock-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "OCMock-umbrella.h"; sourceTree = ""; }; A420909D1D09A79AE6103170D58A4332 /* Reachability.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = Reachability.m; sourceTree = ""; }; A443C69F8A192846AC63B3202379F32A /* Pods-iOS-Network-Stack-DiveTests */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-iOS-Network-Stack-DiveTests"; path = Pods_iOS_Network_Stack_DiveTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; A499F0CFC3E7B3B36E9786BCB6052F37 /* TyphoonStoryboardDefinitionContext.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonStoryboardDefinitionContext.h; path = Source/ios/Definition/TyphoonStoryboardDefinitionContext.h; sourceTree = ""; }; A4C87452D870EC6849B6A78C6E8FC492 /* NSArray+MASAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSArray+MASAdditions.m"; path = "Masonry/NSArray+MASAdditions.m"; sourceTree = ""; }; A4F5A7AB017EF810CFD76454F977D8A0 /* UIView+WebCacheOperation.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIView+WebCacheOperation.m"; path = "SDWebImage/Core/UIView+WebCacheOperation.m"; sourceTree = ""; }; A5650C4A8862741DAF521E285ED2C03A /* Pods-iOS-Network-Stack-DiveTests-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iOS-Network-Stack-DiveTests-acknowledgements.plist"; sourceTree = ""; }; A5A8D22D51747CC57261A39513F63FBF /* TyphoonDefinitionNamespace.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonDefinitionNamespace.m; path = Source/Definition/Namespacing/TyphoonDefinitionNamespace.m; sourceTree = ""; }; A64869131A94BAF78EB608FAED187F90 /* RACScopedDisposable.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACScopedDisposable.m; path = ReactiveObjC/RACScopedDisposable.m; sourceTree = ""; }; A652A78922C1F9E12F183395B8233FDA /* SDDiskCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDDiskCache.m; path = SDWebImage/Core/SDDiskCache.m; sourceTree = ""; }; A66DC1E844041574BF00E1D8870DE753 /* SDCallbackQueue.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDCallbackQueue.h; path = SDWebImage/Core/SDCallbackQueue.h; sourceTree = ""; }; A67FE7A5F980382C477CC0E5671FB1B3 /* UIColor+SDHexString.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIColor+SDHexString.m"; path = "SDWebImage/Private/UIColor+SDHexString.m"; sourceTree = ""; }; A6E0FF3678C9C92FF565548A1B306517 /* Reachability-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Reachability-prefix.pch"; sourceTree = ""; }; A7EF786049567EB4166E1062885CCF21 /* TyphoonStoryboard.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonStoryboard.m; path = Source/ios/Storyboard/TyphoonStoryboard.m; sourceTree = ""; }; A7FF20A852BCFA4897183BC64A6E1D00 /* SDAnimatedImagePlayer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDAnimatedImagePlayer.h; path = SDWebImage/Core/SDAnimatedImagePlayer.h; sourceTree = ""; }; A840AE48AD863CF06B48FE55C745EA9D /* OCMObjectReturnValueProvider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMObjectReturnValueProvider.h; path = Source/OCMock/OCMObjectReturnValueProvider.h; sourceTree = ""; }; A86A73484750ADD56A5905600D66D803 /* RACQueueScheduler.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACQueueScheduler.h; path = ReactiveObjC/RACQueueScheduler.h; sourceTree = ""; }; A8F1809F162828E2A2E755E14EC48D54 /* DZNEmptyDataSet.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = DZNEmptyDataSet.modulemap; sourceTree = ""; }; A9267DE7A89C784B346A9CA842751576 /* OCMockObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMockObject.h; path = Source/OCMock/OCMockObject.h; sourceTree = ""; }; A9638C9372C5407A0209909EB7BAFDCA /* RACEmptySequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACEmptySequence.h; path = ReactiveObjC/RACEmptySequence.h; sourceTree = ""; }; A96DEC36622CF8577C0E003138EFD7EE /* TyphoonStoryboardProvider.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonStoryboardProvider.m; path = Source/ios/Storyboard/TyphoonStoryboardProvider.m; sourceTree = ""; }; A9F3496E6CC2C56684ACF7B2579BB765 /* ReactiveObjC.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = ReactiveObjC.release.xcconfig; sourceTree = ""; }; AA109A4B29ED94AC8CB0BB3D4626927E /* TyphoonInject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInject.h; path = Source/Definition/Injections/TyphoonInject.h; sourceTree = ""; }; AA87031490FD30914ED01BF6A257DB56 /* RACEXTRuntimeExtensions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACEXTRuntimeExtensions.m; path = ReactiveObjC/extobjc/RACEXTRuntimeExtensions.m; sourceTree = ""; }; AAB807AFFC7C3B79795D0CA6C280CC13 /* OCLogTemplate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCLogTemplate.h; path = Source/Vendor/OCLogTemplate/OCLogTemplate.h; sourceTree = ""; }; AACE72B147E103C5ABF8DD49C677152C /* NSMethodSignature+OCMAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSMethodSignature+OCMAdditions.h"; path = "Source/OCMock/NSMethodSignature+OCMAdditions.h"; sourceTree = ""; }; AB5B563B359E9B2E8813C9A2B5091E02 /* Pods-iOS-Network-Stack-Dive-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-iOS-Network-Stack-Dive-Info.plist"; sourceTree = ""; }; AB64B8DD79975FE71CC74B5FBD9785F0 /* SDWebImageDownloaderRequestModifier.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageDownloaderRequestModifier.h; path = SDWebImage/Core/SDWebImageDownloaderRequestModifier.h; sourceTree = ""; }; AB7271D8A582657DF93DCFAD4F92FA7F /* TyphoonAssemblyBuilder+PlistProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonAssemblyBuilder+PlistProcessor.h"; path = "Source/Factory/Internal/TyphoonAssemblyBuilder+PlistProcessor.h"; sourceTree = ""; }; AB7F2ED32F5F65686BE8BE48409DB271 /* RACTuple.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACTuple.h; path = ReactiveObjC/RACTuple.h; sourceTree = ""; }; ABBE463A361CCED6D1E02450518F512E /* SDWebImageError.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageError.m; path = SDWebImage/Core/SDWebImageError.m; sourceTree = ""; }; AC040A20E5DFBE7A6827B92595B02C58 /* TyphoonStoryboardProvider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonStoryboardProvider.h; path = Source/ios/Storyboard/TyphoonStoryboardProvider.h; sourceTree = ""; }; AD8E6EFAEA3F539F1D1F0B9752F7E7EF /* UIStepper+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIStepper+RACSignalSupport.m"; path = "ReactiveObjC/UIStepper+RACSignalSupport.m"; sourceTree = ""; }; AD91A54AEE593AA59D16627F48452F75 /* TyphoonInjectionByObjectFromString.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByObjectFromString.h; path = Source/Definition/Injections/TyphoonInjectionByObjectFromString.h; sourceTree = ""; }; ADA38AFC2E679FBA25375054E69DEFB2 /* RACChannel.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACChannel.m; path = ReactiveObjC/RACChannel.m; sourceTree = ""; }; ADFAD562310FEB485867BE6D63C82804 /* NSObject+PropertyInjection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+PropertyInjection.h"; path = "Source/Utils/NSObject+PropertyInjection.h"; sourceTree = ""; }; AE66E55A3439A3159AAD4EA49DF03076 /* Pods-iOS-Network-Stack-DiveTests-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-iOS-Network-Stack-DiveTests-frameworks.sh"; sourceTree = ""; }; AE9AC83D30952DBFFC1F9C714196F202 /* RACSubscriptionScheduler.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACSubscriptionScheduler.m; path = ReactiveObjC/RACSubscriptionScheduler.m; sourceTree = ""; }; AF03A2059F417E8AEC2BCF0FFB758649 /* TyphoonPassThroughTypeConverter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonPassThroughTypeConverter.h; path = Source/TypeConversion/Converters/TyphoonPassThroughTypeConverter.h; sourceTree = ""; }; AFCF2DB5C3722AD61AAD5876C0E5E4CF /* RACMulticastConnection+Private.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "RACMulticastConnection+Private.h"; path = "ReactiveObjC/RACMulticastConnection+Private.h"; sourceTree = ""; }; AFDA4AF26CABDC552E1905C902431F4F /* Reachability-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Reachability-dummy.m"; sourceTree = ""; }; B05C75D2995DD598CD05DC64D113A11E /* UIButton+RACCommandSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIButton+RACCommandSupport.h"; path = "ReactiveObjC/UIButton+RACCommandSupport.h"; sourceTree = ""; }; B0783913DE452E225655A280A31FEFA8 /* UIView+WebCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIView+WebCache.h"; path = "SDWebImage/Core/UIView+WebCache.h"; sourceTree = ""; }; B0B214D775196BA7CA8E17E53048A493 /* SDWebImage */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SDWebImage; path = SDWebImage.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B0E7E234958AD2EE1C16F6F2EA4B24FB /* TyphoonAssemblyAccessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAssemblyAccessor.h; path = Source/Factory/Assembly/TyphoonAssemblyAccessor.h; sourceTree = ""; }; B1233E43A91D5DA56B2AA3A634E7BF05 /* OCMock-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "OCMock-dummy.m"; sourceTree = ""; }; B2042CA0D12A3305ACABBBC22E60BE48 /* NSObject+RACDescription.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+RACDescription.m"; path = "ReactiveObjC/NSObject+RACDescription.m"; sourceTree = ""; }; B28FA990E304B0E57E1599F3DDAC5301 /* RACUnit.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACUnit.h; path = ReactiveObjC/RACUnit.h; sourceTree = ""; }; B2A8FF4E1BDBA3D59428BAC8FAA55FFD /* TyphoonAbstractDetachableComponentFactoryPostProcessor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonAbstractDetachableComponentFactoryPostProcessor.m; path = Source/Configuration/TyphoonAbstractDetachableComponentFactoryPostProcessor.m; sourceTree = ""; }; B2E5AA16D441D1E41E9953B3FD0CE552 /* SDImageCacheConfig.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageCacheConfig.m; path = SDWebImage/Core/SDImageCacheConfig.m; sourceTree = ""; }; B4250F1A74D43D858FA5327444397E4B /* NSObject+RACDeallocating.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+RACDeallocating.m"; path = "ReactiveObjC/NSObject+RACDeallocating.m"; sourceTree = ""; }; B478DF0CC09DF6605F4772947370CBE1 /* SDWebImageIndicator.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageIndicator.h; path = SDWebImage/Core/SDWebImageIndicator.h; sourceTree = ""; }; B49E72E762657E3284FA9D7A026A49E6 /* TyphoonNibLoader.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonNibLoader.m; path = Source/ios/Nib/TyphoonNibLoader.m; sourceTree = ""; }; B4C1BAAAE45FAB43E8ECC69C9DC35A7D /* UIAlertView+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIAlertView+RACSignalSupport.m"; path = "ReactiveObjC/UIAlertView+RACSignalSupport.m"; sourceTree = ""; }; B5261A9D35AA9978FA8E8427895CDA41 /* NSArray+MASAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSArray+MASAdditions.h"; path = "Masonry/NSArray+MASAdditions.h"; sourceTree = ""; }; B534D8B84FDE7415642A1D01E3733910 /* SDMemoryCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDMemoryCache.m; path = SDWebImage/Core/SDMemoryCache.m; sourceTree = ""; }; B54343D5BDC4CA5AB85FD35468187848 /* NSFileHandle+RACSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSFileHandle+RACSupport.m"; path = "ReactiveObjC/NSFileHandle+RACSupport.m"; sourceTree = ""; }; B5D03644E567590DEDC2D0AE3425D98D /* OCMNotificationPoster.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMNotificationPoster.h; path = Source/OCMock/OCMNotificationPoster.h; sourceTree = ""; }; B61B3D89FE168025B8BCCE1DE7423DF5 /* RACKVOChannel.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACKVOChannel.h; path = ReactiveObjC/RACKVOChannel.h; sourceTree = ""; }; B64CD45769D06F336C347EDF755FC7E0 /* CocoaAsyncSocket.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = CocoaAsyncSocket.release.xcconfig; sourceTree = ""; }; B663AA0974236ABB5B164B3F2FCA9874 /* OCMNonRetainingObjectReturnValueProvider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMNonRetainingObjectReturnValueProvider.h; path = Source/OCMock/OCMNonRetainingObjectReturnValueProvider.h; sourceTree = ""; }; B66C84A540CE194D4B1F225949E9EA7F /* TyphoonWeakComponentsPool.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonWeakComponentsPool.m; path = Source/Factory/Pool/TyphoonWeakComponentsPool.m; sourceTree = ""; }; B67521AEB3057A6B1FDF7908EB18E894 /* SDImageAPNGCoder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageAPNGCoder.m; path = SDWebImage/Core/SDImageAPNGCoder.m; sourceTree = ""; }; B6C0AFCABBA43F288D26D6A9CA0DB2DA /* OCMInvocationStub.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMInvocationStub.h; path = Source/OCMock/OCMInvocationStub.h; sourceTree = ""; }; B755FFA45FCE441B80E378A091AA25F4 /* Pods-iOS-Network-Stack-Dive-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-iOS-Network-Stack-Dive-dummy.m"; sourceTree = ""; }; B7B1F42980891C0D9253DA6E3EEDE467 /* TyphoonBundledImageTypeConverter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonBundledImageTypeConverter.m; path = Source/ios/TypeConversion/Converters/TyphoonBundledImageTypeConverter.m; sourceTree = ""; }; B7C029402AB7307D26D094182B34A802 /* UISegmentedControl+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UISegmentedControl+RACSignalSupport.m"; path = "ReactiveObjC/UISegmentedControl+RACSignalSupport.m"; sourceTree = ""; }; B7CC02F4E9B33CCE0CE749A8F5F1ACA7 /* OCClassMockObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCClassMockObject.h; path = Source/OCMock/OCClassMockObject.h; sourceTree = ""; }; B8D365F9E6ABE29BE745099B2A29F0B1 /* TyphoonAssemblyAdviser.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonAssemblyAdviser.m; path = Source/Factory/Internal/TyphoonAssemblyAdviser.m; sourceTree = ""; }; B8FF03BB1DD7B75DF4D031C0DA692637 /* RACUnit.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACUnit.m; path = ReactiveObjC/RACUnit.m; sourceTree = ""; }; B913D23BDDEDDE62A0E2C3493A0E97F5 /* SDImageCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageCache.h; path = SDWebImage/Core/SDImageCache.h; sourceTree = ""; }; B96D3683B3E4B884909B07397CF43A5D /* SDWebImageCacheSerializer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageCacheSerializer.m; path = SDWebImage/Core/SDWebImageCacheSerializer.m; sourceTree = ""; }; B9B7D7D1F5CDB012C8AEE5F8EF3F513D /* RACDisposable.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACDisposable.h; path = ReactiveObjC/RACDisposable.h; sourceTree = ""; }; BAF592AE664D056D895A926E0F7D0261 /* SDFileAttributeHelper.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDFileAttributeHelper.h; path = SDWebImage/Private/SDFileAttributeHelper.h; sourceTree = ""; }; BAFC1AB1A830CD29F44C74C0B71EF450 /* SDWebImageDownloaderResponseModifier.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageDownloaderResponseModifier.h; path = SDWebImage/Core/SDWebImageDownloaderResponseModifier.h; sourceTree = ""; }; BC0D43AD044D143B02D4E1D5EA86BE86 /* TyphoonStoryboardResolver.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonStoryboardResolver.h; path = Source/ios/Storyboard/TyphoonStoryboardResolver.h; sourceTree = ""; }; BCB7911698C2C8CF6B83F92DF10B11EF /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; includeInIndex = 1; name = PrivacyInfo.xcprivacy; path = WebImage/PrivacyInfo.xcprivacy; sourceTree = ""; }; BCD5C7A53B74E13B1712A968B6AD9302 /* RACEXTScope.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACEXTScope.h; path = ReactiveObjC/extobjc/RACEXTScope.h; sourceTree = ""; }; BCE7EC027D6727131CBC3BDA0CA0FA09 /* Typhoon.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Typhoon.debug.xcconfig; sourceTree = ""; }; BD5A68EEDD637AF3394D147538906789 /* SDImageAPNGCoder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageAPNGCoder.h; path = SDWebImage/Core/SDImageAPNGCoder.h; sourceTree = ""; }; BD5D5509B774B3EA20B4EBDBD246DD8C /* TyphooniOS.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphooniOS.h; path = Source/ios/TyphooniOS.h; sourceTree = ""; }; BDC02EE21DEF11D396F51D141C543D5C /* RACTargetQueueScheduler.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACTargetQueueScheduler.h; path = ReactiveObjC/RACTargetQueueScheduler.h; sourceTree = ""; }; BDD4238ED758F069DED59626A8859F28 /* TyphoonViewControllerInjector.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonViewControllerInjector.m; path = Source/ios/Configuration/TyphoonViewControllerInjector.m; sourceTree = ""; }; BDDDFCB98FA96E2B25144D59DAD84EF4 /* NSNullTypeConverter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = NSNullTypeConverter.m; path = Source/TypeConversion/Converters/NSNullTypeConverter.m; sourceTree = ""; }; BE3BDF1044166B0EEE411717FD217E42 /* UIResponder+TyphoonOutletTransfer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIResponder+TyphoonOutletTransfer.m"; path = "Source/ios/Storyboard/UIResponder+TyphoonOutletTransfer.m"; sourceTree = ""; }; BE52EEDB31E3B6435F83E7E317F3047D /* SDAnimatedImageRep.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDAnimatedImageRep.h; path = SDWebImage/Core/SDAnimatedImageRep.h; sourceTree = ""; }; BF03F228FDA5C0221C20EF2FDD92BDE7 /* SDWebImage.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SDWebImage.modulemap; sourceTree = ""; }; BFB3821735D0527AC675FE95C25658F8 /* NSObject+RACPropertySubscribing.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+RACPropertySubscribing.m"; path = "ReactiveObjC/NSObject+RACPropertySubscribing.m"; sourceTree = ""; }; BFD56F486BA750FFE7B13856AC85FEA2 /* RACReturnSignal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACReturnSignal.h; path = ReactiveObjC/RACReturnSignal.h; sourceTree = ""; }; C0225799E4F72C1CA22CAA520A08E589 /* OCMRecorder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMRecorder.m; path = Source/OCMock/OCMRecorder.m; sourceTree = ""; }; C0C099CEA20C5E235C69D071EC07EF0B /* Reachability.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Reachability.modulemap; sourceTree = ""; }; C0DFA61DA615C88D4F562DF9D047B134 /* TyphoonInjectionByCurrentRuntimeArguments.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjectionByCurrentRuntimeArguments.h; path = Source/Definition/Injections/TyphoonInjectionByCurrentRuntimeArguments.h; sourceTree = ""; }; C122C87FD2423368DF66F19780CE1593 /* DZNEmptyDataSet.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = DZNEmptyDataSet.release.xcconfig; sourceTree = ""; }; C1438D60DB93FD77AC7CEB329AA4D842 /* OCMock.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = OCMock.debug.xcconfig; sourceTree = ""; }; C14BDA045B4B1D88E3900EFD02FFA862 /* OCMPassByRefSetter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMPassByRefSetter.m; path = Source/OCMock/OCMPassByRefSetter.m; sourceTree = ""; }; C15E41DEC1751BDADA070E77A5D31D94 /* SDImageIOAnimatedCoderInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageIOAnimatedCoderInternal.h; path = SDWebImage/Private/SDImageIOAnimatedCoderInternal.h; sourceTree = ""; }; C218F557360FC510BBA92A7B3EEC1065 /* RACSubscriptingAssignmentTrampoline.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACSubscriptingAssignmentTrampoline.m; path = ReactiveObjC/RACSubscriptingAssignmentTrampoline.m; sourceTree = ""; }; C269BD3729C76AB77AB3EFFBD2F7192C /* Typhoon.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Typhoon.modulemap; sourceTree = ""; }; C2D79FFE2E4790A5C03593BBCA678B0A /* TyphoonReferenceDefinition.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonReferenceDefinition.m; path = Source/Definition/Internal/TyphoonReferenceDefinition.m; sourceTree = ""; }; C3133631DC229F9194ABFF320BE45553 /* NSObject+YYModel.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+YYModel.h"; path = "YYModel/NSObject+YYModel.h"; sourceTree = ""; }; C3E573EB2C906BB6045C58BC27C4CFB2 /* SDImageIOAnimatedCoder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageIOAnimatedCoder.h; path = SDWebImage/Core/SDImageIOAnimatedCoder.h; sourceTree = ""; }; C40D9702A810253531E2904C61CE32F5 /* SDDisplayLink.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDDisplayLink.h; path = SDWebImage/Private/SDDisplayLink.h; sourceTree = ""; }; C49042227AF27E657FDC7BA3CA8A537A /* SDGraphicsImageRenderer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDGraphicsImageRenderer.h; path = SDWebImage/Core/SDGraphicsImageRenderer.h; sourceTree = ""; }; C4FBBF6C2C66E9E078C222133056807B /* TyphoonDefinition+InstanceBuilder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonDefinition+InstanceBuilder.h"; path = "Source/Definition/Internal/TyphoonDefinition+InstanceBuilder.h"; sourceTree = ""; }; C51A574AFE5E5029BD52D9AC1C54C204 /* Reachability.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = Reachability.h; sourceTree = ""; }; C529B758067B01A288574A93D2868C42 /* TyphoonDefinitionPostProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonDefinitionPostProcessor.h; path = Source/Configuration/TyphoonDefinitionPostProcessor.h; sourceTree = ""; }; C5460C93594FE49E0DF49CC9F802F1F3 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; C5EE4FBC26F92ABB4464D8BED481D33A /* SDImageFramePool.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageFramePool.m; path = SDWebImage/Private/SDImageFramePool.m; sourceTree = ""; }; C5F745BEA95E2A3A7A18764A8CE22A3F /* TyphoonAssemblyBuilder+PlistProcessor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "TyphoonAssemblyBuilder+PlistProcessor.m"; path = "Source/Factory/Internal/TyphoonAssemblyBuilder+PlistProcessor.m"; sourceTree = ""; }; C6506E5BA0D201F5A9633E8253219386 /* SystemConfiguration.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SystemConfiguration.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/SystemConfiguration.framework; sourceTree = DEVELOPER_DIR; }; C6D6E31E7C7B4D803BA05555FE0FF607 /* CoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreFoundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/CoreFoundation.framework; sourceTree = DEVELOPER_DIR; }; C7F186EA53F19BDDCBCD2E2E2279D348 /* TyphoonTypeDescriptor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonTypeDescriptor.m; path = Source/TypeConversion/TyphoonTypeDescriptor.m; sourceTree = ""; }; C8A5867815E6437C1B59CDE852811716 /* OCPartialMockObject.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCPartialMockObject.h; path = Source/OCMock/OCPartialMockObject.h; sourceTree = ""; }; C92A811BD719C9FA38BF1EB73409CA04 /* NSInvocation+TCFUnwrapValues.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSInvocation+TCFUnwrapValues.m"; path = "Source/Factory/Internal/NSInvocation+TCFUnwrapValues.m"; sourceTree = ""; }; C9703620E8931C6F0907DD19F302A7A9 /* TyphoonParentReferenceHydratingPostProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonParentReferenceHydratingPostProcessor.h; path = Source/Factory/Internal/TyphoonParentReferenceHydratingPostProcessor.h; sourceTree = ""; }; CA332156FBF114376E2FE5077E71C03E /* TyphoonPropertyStyleConfiguration.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonPropertyStyleConfiguration.h; path = Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonPropertyStyleConfiguration.h; sourceTree = ""; }; CA677B07FC524F62109AE180874AB60E /* RACKVOProxy.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACKVOProxy.h; path = ReactiveObjC/RACKVOProxy.h; sourceTree = ""; }; CAE4756369959CF3EB303367CDDEBAFD /* NSMethodSignature+TCFUnwrapValues.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSMethodSignature+TCFUnwrapValues.h"; path = "Source/Factory/Internal/NSMethodSignature+TCFUnwrapValues.h"; sourceTree = ""; }; CB8729BC7993BA548B4D892BE63AEE83 /* RACPassthroughSubscriber.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACPassthroughSubscriber.h; path = ReactiveObjC/RACPassthroughSubscriber.h; sourceTree = ""; }; CBD6CB22BEFFD7B25F968C06E8168BE5 /* RACBehaviorSubject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACBehaviorSubject.m; path = ReactiveObjC/RACBehaviorSubject.m; sourceTree = ""; }; CC5646C5FEED8AA9A5E6C5B7AA2A1AC0 /* UITableViewHeaderFooterView+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UITableViewHeaderFooterView+RACSignalSupport.m"; path = "ReactiveObjC/UITableViewHeaderFooterView+RACSignalSupport.m"; sourceTree = ""; }; CDA3D0B455A13ACC94AFADCBB2E311FE /* TyphoonPathResource.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonPathResource.m; path = Source/Configuration/Resource/TyphoonPathResource.m; sourceTree = ""; }; CDACF51466350F7A4B6A82E206A3F37A /* SDWebImageDownloaderConfig.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageDownloaderConfig.m; path = SDWebImage/Core/SDWebImageDownloaderConfig.m; sourceTree = ""; }; CDF6A89092579B8DC5007CD2E67B5C48 /* View+MASAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "View+MASAdditions.m"; path = "Masonry/View+MASAdditions.m"; sourceTree = ""; }; CE6F1B69B9CDD8F4C2903BEDC2660DC3 /* TyphoonSwizzlerDefaultImpl.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonSwizzlerDefaultImpl.h; path = Source/Utils/Swizzle/TyphoonSwizzlerDefaultImpl.h; sourceTree = ""; }; CEB064A038D098A7FFF817FFA5B9F340 /* UIImage+MultiFormat.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImage+MultiFormat.h"; path = "SDWebImage/Core/UIImage+MultiFormat.h"; sourceTree = ""; }; CF1281E58AA1045D4B7F33FC56691C42 /* SDWebImage-SDWebImage */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = "SDWebImage-SDWebImage"; path = SDWebImage.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; CF5D6355EBCEC215D5F4264D1241E1B1 /* TyphoonOptionMatcher.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonOptionMatcher.h; path = Source/Configuration/DefinitionOptionConfiguration/Matcher/TyphoonOptionMatcher.h; sourceTree = ""; }; CF7C5602AF31F940B921D40619F7D228 /* TyphoonGlobalConfigCollector.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonGlobalConfigCollector.m; path = Source/Configuration/GlobalConfigResolver/TyphoonGlobalConfigCollector.m; sourceTree = ""; }; CFCB3855AE45E56DBEA7833054EAD44F /* RACEvent.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACEvent.h; path = ReactiveObjC/RACEvent.h; sourceTree = ""; }; D029DA41DE158F6663F423C5BEC68F57 /* SDImageLoader.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageLoader.h; path = SDWebImage/Core/SDImageLoader.h; sourceTree = ""; }; D07B2E954DF932CCE682BD41F588F5BA /* SDWeakProxy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWeakProxy.m; path = SDWebImage/Private/SDWeakProxy.m; sourceTree = ""; }; D16AFD377C51E109866560DEE9CBC39E /* TyphoonAutoInjection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAutoInjection.h; path = Source/Definition/AutoInjection/TyphoonAutoInjection.h; sourceTree = ""; }; D179E0BB3A9462316E480E37EBCDDDF5 /* NSObject+DeallocNotification.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+DeallocNotification.h"; path = "Source/Utils/NSObject+DeallocNotification.h"; sourceTree = ""; }; D1D5C8198B49A7A43168157C6AAACF8B /* YYModel.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = YYModel.debug.xcconfig; sourceTree = ""; }; D1FE5339C49B5BC54F80CD72EDBB13EA /* UIImage+ExtendedCacheData.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImage+ExtendedCacheData.m"; path = "SDWebImage/Core/UIImage+ExtendedCacheData.m"; sourceTree = ""; }; D21426F24A160A03F3A3B57A03B3671E /* OCMArg.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMArg.m; path = Source/OCMock/OCMArg.m; sourceTree = ""; }; D35CCCA0DFE0CE1EBDA590032372C0AB /* MASViewConstraint.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASViewConstraint.h; path = Masonry/MASViewConstraint.h; sourceTree = ""; }; D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; D3872D84DFE6C4C6FEBC86D6D30C705B /* SDWebImage.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SDWebImage.debug.xcconfig; sourceTree = ""; }; D3A21103401B7FEA00216399FCE680EE /* TyphoonTypeConverterRegistry.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonTypeConverterRegistry.h; path = Source/TypeConversion/TyphoonTypeConverterRegistry.h; sourceTree = ""; }; D3B2AC38C369630473925094DAF536F5 /* DZNEmptyDataSet-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "DZNEmptyDataSet-dummy.m"; sourceTree = ""; }; D400046FCD67C3ECE87A4FD7570DF9B7 /* OCMInvocationExpectation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMInvocationExpectation.h; path = Source/OCMock/OCMInvocationExpectation.h; sourceTree = ""; }; D4142DA4BEFA74E0A9968C239127FF24 /* SDWebImageCacheSerializer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageCacheSerializer.h; path = SDWebImage/Core/SDWebImageCacheSerializer.h; sourceTree = ""; }; D41D80BBE2F576947F11C3B0CDCDB170 /* UIResponder+TyphoonOutletTransfer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIResponder+TyphoonOutletTransfer.h"; path = "Source/ios/Storyboard/UIResponder+TyphoonOutletTransfer.h"; sourceTree = ""; }; D421E7A95F7BBA522BE9AE982E7390CF /* TyphoonUIColorTypeConverter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonUIColorTypeConverter.m; path = Source/ios/TypeConversion/Converters/TyphoonUIColorTypeConverter.m; sourceTree = ""; }; D45725C80C47BF66261F5057EA6C9389 /* Masonry.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Masonry.modulemap; sourceTree = ""; }; D524BC409D8FA54C1A06A2E06C1FC36A /* NSArray+TyphoonManualEnumeration.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSArray+TyphoonManualEnumeration.h"; path = "Source/Utils/NSArray+TyphoonManualEnumeration.h"; sourceTree = ""; }; D5A65F1C73D00CF1864BB3078888AECD /* TyphoonBlockDefinition.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonBlockDefinition.m; path = Source/Definition/TyphoonBlockDefinition.m; sourceTree = ""; }; D655158D8BA8A371D68B4A11241E9616 /* NSNullTypeConverter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = NSNullTypeConverter.h; path = Source/TypeConversion/Converters/NSNullTypeConverter.h; sourceTree = ""; }; D66DFDCCBA63EB04C097EA2E0C475EE5 /* NSInvocation+OCMAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSInvocation+OCMAdditions.h"; path = "Source/OCMock/NSInvocation+OCMAdditions.h"; sourceTree = ""; }; D68AF173A5494072218D36ADB175CC67 /* NSBezierPath+SDRoundedCorners.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSBezierPath+SDRoundedCorners.m"; path = "SDWebImage/Private/NSBezierPath+SDRoundedCorners.m"; sourceTree = ""; }; D69D5EFB2189DEA994DD47111C55B929 /* SDImageGIFCoder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageGIFCoder.m; path = SDWebImage/Core/SDImageGIFCoder.m; sourceTree = ""; }; D6E9B831B9FB4D942099CD004CE0BAC3 /* SDWebImageError.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageError.h; path = SDWebImage/Core/SDWebImageError.h; sourceTree = ""; }; D8706CB35453DAA97FE6E56A07BF5E79 /* RACEmptySignal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACEmptySignal.h; path = ReactiveObjC/RACEmptySignal.h; sourceTree = ""; }; D8C316292E777A36CEF459683AF45953 /* RACTestScheduler.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACTestScheduler.h; path = ReactiveObjC/RACTestScheduler.h; sourceTree = ""; }; D93216F983293A53686F68A6B9A39E70 /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Security.framework; sourceTree = DEVELOPER_DIR; }; DA6A12A58C1C3043FB0D92D39EF14EB7 /* TyphoonInjectionByFactoryReference.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByFactoryReference.m; path = Source/Definition/Injections/TyphoonInjectionByFactoryReference.m; sourceTree = ""; }; DAA9B1CEFE3B5F49253B19B6F13CBE00 /* NSUserDefaults+RACSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSUserDefaults+RACSupport.h"; path = "ReactiveObjC/NSUserDefaults+RACSupport.h"; sourceTree = ""; }; DAF562B23684D41ED34CC07CD14C7875 /* TyphoonInjection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonInjection.h; path = Source/Definition/Injections/TyphoonInjection.h; sourceTree = ""; }; DAF7C7F3C941B652A3015DFEA15DB259 /* UIImage+Metadata.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImage+Metadata.h"; path = "SDWebImage/Core/UIImage+Metadata.h"; sourceTree = ""; }; DB2ED745168A43281FE03EC58108FBDE /* RACEmptySequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACEmptySequence.m; path = ReactiveObjC/RACEmptySequence.m; sourceTree = ""; }; DB337B4965F7DD6CACD10EBA48209055 /* TyphoonStartup.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonStartup.m; path = Source/Configuration/Startup/TyphoonStartup.m; sourceTree = ""; }; DB54DF3DB0FDADB1A3893CFF8C1AF03A /* RACEagerSequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACEagerSequence.m; path = ReactiveObjC/RACEagerSequence.m; sourceTree = ""; }; DC81E927C43FEC142A408A3850366DA7 /* TyphoonInjectionByReference.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectionByReference.m; path = Source/Definition/Injections/TyphoonInjectionByReference.m; sourceTree = ""; }; DE4C0740ECC01D6ED27D9ABD7EA5172B /* NSValue+TCFUnwrapValues.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSValue+TCFUnwrapValues.m"; path = "Source/Factory/Internal/NSValue+TCFUnwrapValues.m"; sourceTree = ""; }; DEE925C4BC3366755D6B62640CCCFCAE /* NSMethodSignature+OCMAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSMethodSignature+OCMAdditions.m"; path = "Source/OCMock/NSMethodSignature+OCMAdditions.m"; sourceTree = ""; }; DF3C6B3C4596923C4E111113DF611DF6 /* NSArray+RACSequenceAdditions.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSArray+RACSequenceAdditions.m"; path = "ReactiveObjC/NSArray+RACSequenceAdditions.m"; sourceTree = ""; }; DF3E60E69433B185D69240ECF94CFCFB /* OCMInvocationStub.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMInvocationStub.m; path = Source/OCMock/OCMInvocationStub.m; sourceTree = ""; }; DF6104CF591EBBA4948511163C719CDB /* SDWebImageManager.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageManager.m; path = SDWebImage/Core/SDWebImageManager.m; sourceTree = ""; }; E0C543AB14CD3CB33002A31DFA8A3ED7 /* SDDiskCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDDiskCache.h; path = SDWebImage/Core/SDDiskCache.h; sourceTree = ""; }; E1372334A95BBBD06EC3A3CDDE782AC3 /* TyphoonConfigPostProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonConfigPostProcessor.h; path = Source/Configuration/ConfigPostProcessor/TyphoonConfigPostProcessor.h; sourceTree = ""; }; E14AF6E304C6098B7A146E1BAFC26C6C /* SDWebImagePrefetcher.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImagePrefetcher.m; path = SDWebImage/Core/SDWebImagePrefetcher.m; sourceTree = ""; }; E1B9673A5D86AD933259E18FA172B615 /* SDImageCoder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageCoder.h; path = SDWebImage/Core/SDImageCoder.h; sourceTree = ""; }; E1D2B79FD0B5E7ECF99452AC0439FB6D /* OCMExpectationRecorder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMExpectationRecorder.h; path = Source/OCMock/OCMExpectationRecorder.h; sourceTree = ""; }; E1DC9A972C497BFFC7B688A24BA28E98 /* UIImageView+WebCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImageView+WebCache.h"; path = "SDWebImage/Core/UIImageView+WebCache.h"; sourceTree = ""; }; E369629E03DEAACC1711DB17684549A9 /* NSSet+RACSequenceAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSSet+RACSequenceAdditions.h"; path = "ReactiveObjC/NSSet+RACSequenceAdditions.h"; sourceTree = ""; }; E37027B951A73179B2F4337FF41422D7 /* RACBlockTrampoline.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACBlockTrampoline.m; path = ReactiveObjC/RACBlockTrampoline.m; sourceTree = ""; }; E3CC05377476E8CA3BDD23296111A57A /* RACKVOChannel.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACKVOChannel.m; path = ReactiveObjC/RACKVOChannel.m; sourceTree = ""; }; E3F2828AF28F25FC9663EDF2BD110090 /* MASConstraintMaker.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASConstraintMaker.h; path = Masonry/MASConstraintMaker.h; sourceTree = ""; }; E4338428A593399BDE88E88C9CEF385C /* TyphoonAssemblyPropertyInjectionPostProcessor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonAssemblyPropertyInjectionPostProcessor.m; path = Source/Factory/Internal/TyphoonAssemblyPropertyInjectionPostProcessor.m; sourceTree = ""; }; E460D5B0416D36F66EE8EC89E5D2FA0A /* YYModel */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = YYModel; path = YYModel.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E49D958DD1C460DCA691F17881DE66D9 /* RACAnnotations.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACAnnotations.h; path = ReactiveObjC/RACAnnotations.h; sourceTree = ""; }; E526E2D4A9BDEA51AAF7B2A5AC6A46FA /* Pods-iOS-Network-Stack-DiveTests-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-iOS-Network-Stack-DiveTests-dummy.m"; sourceTree = ""; }; E54E4A10B9B5F3F352208D4FC87A2ECF /* TyphoonAssembly.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAssembly.h; path = Source/Factory/Assembly/TyphoonAssembly.h; sourceTree = ""; }; E586B047153D54E5818AFAA7B25CC55A /* TyphoonTestUtils.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonTestUtils.m; path = Source/Test/TestUtils/TyphoonTestUtils.m; sourceTree = ""; }; E597546B5AFBBDBBDE17E78E1E1EC111 /* SDImageCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDImageCache.m; path = SDWebImage/Core/SDImageCache.m; sourceTree = ""; }; E59E27A8B1D09C1C222A67BFFD0845C6 /* TyphoonAssemblyPropertyInjectionPostProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAssemblyPropertyInjectionPostProcessor.h; path = Source/Factory/Internal/TyphoonAssemblyPropertyInjectionPostProcessor.h; sourceTree = ""; }; E59E49F550CAE1C1C8665F6CCF893735 /* OCMQuantifier.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMQuantifier.m; path = Source/OCMock/OCMQuantifier.m; sourceTree = ""; }; E5AE3EB7707FD5429801C6CB2A8A382E /* MASConstraint+Private.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "MASConstraint+Private.h"; path = "Masonry/MASConstraint+Private.h"; sourceTree = ""; }; E5F9720FC7B55FFB33E76C326A7DDE5E /* SDAnimatedImage.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDAnimatedImage.m; path = SDWebImage/Core/SDAnimatedImage.m; sourceTree = ""; }; E6BFAC38D0C1749C0AF02A5825F2ECBB /* OCMock.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = OCMock.release.xcconfig; sourceTree = ""; }; E6FF1599AA4D761C6A0D334AC8A18BB7 /* RACKVOTrampoline.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACKVOTrampoline.h; path = ReactiveObjC/RACKVOTrampoline.h; sourceTree = ""; }; E8412B691818531EBFB6124AD9FDA943 /* SDWebImageDownloaderDecryptor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageDownloaderDecryptor.h; path = SDWebImage/Core/SDWebImageDownloaderDecryptor.h; sourceTree = ""; }; E842E755FB8A20406553415C9EACC8A1 /* TyphoonBlockDefinitionController.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonBlockDefinitionController.m; path = Source/Definition/Internal/TyphoonBlockDefinitionController.m; sourceTree = ""; }; E8561AC4257A4B8D0B0569EEEA2E4555 /* OCMExceptionReturnValueProvider.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMExceptionReturnValueProvider.h; path = Source/OCMock/OCMExceptionReturnValueProvider.h; sourceTree = ""; }; E87C4CECC849A38798AC17F0D2EDBDC7 /* TyphoonTypeDescriptor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonTypeDescriptor.h; path = Source/TypeConversion/TyphoonTypeDescriptor.h; sourceTree = ""; }; E928F67B2793E36DB0271AE60B7C1747 /* MASViewAttribute.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = MASViewAttribute.h; path = Masonry/MASViewAttribute.h; sourceTree = ""; }; E93FFD1537CD2D6332F9A080E750CAAE /* NSObject+OCMAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+OCMAdditions.h"; path = "Source/OCMock/NSObject+OCMAdditions.h"; sourceTree = ""; }; E9BFDB08B8D6909114F2B3520B986044 /* SDImageCacheDefine.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageCacheDefine.h; path = SDWebImage/Core/SDImageCacheDefine.h; sourceTree = ""; }; E9CD0BC00D291194A3441C33CBA01647 /* TyphoonBlockDefinition+InstanceBuilder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "TyphoonBlockDefinition+InstanceBuilder.m"; path = "Source/Definition/Internal/TyphoonBlockDefinition+InstanceBuilder.m"; sourceTree = ""; }; EA1F7903B377E9FFA5C2028618BA7439 /* TyphoonTestUtils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonTestUtils.h; path = Source/Test/TestUtils/TyphoonTestUtils.h; sourceTree = ""; }; EA318A2FA8725340866FC3083915FCFD /* RACReplaySubject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACReplaySubject.m; path = ReactiveObjC/RACReplaySubject.m; sourceTree = ""; }; EAFD5F2E394EE2C4144D87785849CD68 /* NSObject+TyphoonIntrospectionUtils.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+TyphoonIntrospectionUtils.h"; path = "Source/Utils/NSObject+TyphoonIntrospectionUtils.h"; sourceTree = ""; }; EB61987B9EEE414AA540987787047EEE /* CocoaAsyncSocket-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "CocoaAsyncSocket-prefix.pch"; sourceTree = ""; }; EC243AD4E47E5CAA863DF364A86CA635 /* RACQueueScheduler.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACQueueScheduler.m; path = ReactiveObjC/RACQueueScheduler.m; sourceTree = ""; }; ECA70F67E1A45D6DB171A53CF98C8090 /* NSData+ImageContentType.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSData+ImageContentType.m"; path = "SDWebImage/Core/NSData+ImageContentType.m"; sourceTree = ""; }; ECBE53D68EFF65EB7F6020F1BE9C5B3B /* TyphoonResource.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonResource.h; path = Source/Configuration/Resource/TyphoonResource.h; sourceTree = ""; }; ED4B3532992A08C1DE0094AFAED64A03 /* OCMBoxedReturnValueProvider.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMBoxedReturnValueProvider.m; path = Source/OCMock/OCMBoxedReturnValueProvider.m; sourceTree = ""; }; EDE075C32619BCEAF4A3F8B8AFFA26BD /* Pods-iOS-Network-Stack-Dive.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-iOS-Network-Stack-Dive.modulemap"; sourceTree = ""; }; EE05E240495D85D96BA4CB6CB3310F63 /* NSImage+Compatibility.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSImage+Compatibility.h"; path = "SDWebImage/Core/NSImage+Compatibility.h"; sourceTree = ""; }; EF2113E6C3216757EFC73B0ACD0ABD90 /* UIButton+WebCache.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIButton+WebCache.h"; path = "SDWebImage/Core/UIButton+WebCache.h"; sourceTree = ""; }; EF447269480FB0580228D478883FFA69 /* OCMockMacros.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMockMacros.h; path = Source/OCMock/OCMockMacros.h; sourceTree = ""; }; EFB2E394DB7A9A7EEE8003F7C8E6AD40 /* TyphoonAssemblyAccessor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonAssemblyAccessor.m; path = Source/Factory/Assembly/TyphoonAssemblyAccessor.m; sourceTree = ""; }; F0055EAC2B8C580C32290969DFF19220 /* TyphoonPathResource.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonPathResource.h; path = Source/Configuration/Resource/TyphoonPathResource.h; sourceTree = ""; }; F028FB003652E9875D30DDAF99C21162 /* RACErrorSignal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACErrorSignal.h; path = ReactiveObjC/RACErrorSignal.h; sourceTree = ""; }; F06397AB33F7FA583810C154BB7C6BB2 /* TyphoonFactoryAutoInjectionPostProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonFactoryAutoInjectionPostProcessor.h; path = Source/Definition/AutoInjection/TyphoonFactoryAutoInjectionPostProcessor.h; sourceTree = ""; }; F069B23BF8F0BF5B39AAFB166C957FA3 /* RACIndexSetSequence.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACIndexSetSequence.m; path = ReactiveObjC/RACIndexSetSequence.m; sourceTree = ""; }; F0C0E50FA94D388C9007312869F096D5 /* RACImmediateScheduler.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = RACImmediateScheduler.m; path = ReactiveObjC/RACImmediateScheduler.m; sourceTree = ""; }; F0E38DEA6A48AB6D0B5359119070CCD3 /* TyphoonFactoryAutoInjectionPostProcessor.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonFactoryAutoInjectionPostProcessor.m; path = Source/Definition/AutoInjection/TyphoonFactoryAutoInjectionPostProcessor.m; sourceTree = ""; }; F13BB06DB4576664D2BD21C5F4490F59 /* RACCompoundDisposableProvider.d */ = {isa = PBXFileReference; includeInIndex = 1; name = RACCompoundDisposableProvider.d; path = ReactiveObjC/RACCompoundDisposableProvider.d; sourceTree = ""; }; F180A82892F507476C7119020F8B59B0 /* TyphoonNibLoader.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonNibLoader.h; path = Source/ios/Nib/TyphoonNibLoader.h; sourceTree = ""; }; F21C58BD3E6383E8D7CDE5A22DFE4432 /* NSImage+Compatibility.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSImage+Compatibility.m"; path = "SDWebImage/Core/NSImage+Compatibility.m"; sourceTree = ""; }; F2C354A6FCF564B6B35C09D98D9B276E /* SDWebImageDefine.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDWebImageDefine.m; path = SDWebImage/Core/SDWebImageDefine.m; sourceTree = ""; }; F30C125DB91902F63DC73AEF0B0E6111 /* OCMock */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = OCMock; path = OCMock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; F383EDD80DA3184C9A032B89FD763BB7 /* NSObject+RACDeallocating.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSObject+RACDeallocating.h"; path = "ReactiveObjC/NSObject+RACDeallocating.h"; sourceTree = ""; }; F3F7B76572B953FCBB908A0E7605EB53 /* TyphoonPropertyInjection.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonPropertyInjection.h; path = Source/Definition/Injections/TyphoonPropertyInjection.h; sourceTree = ""; }; F42860013A956ABF313EE9C9D084F191 /* TyphoonInjectedObject.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonInjectedObject.m; path = Source/Definition/AutoInjection/TyphoonInjectedObject.m; sourceTree = ""; }; F4403468E084E9FF3F896A3734CD95FB /* OCMObserverRecorder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMObserverRecorder.m; path = Source/OCMock/OCMObserverRecorder.m; sourceTree = ""; }; F4690548BF3F6C5A71CDC7BF7187EC20 /* RACEXTKeyPathCoding.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACEXTKeyPathCoding.h; path = ReactiveObjC/extobjc/RACEXTKeyPathCoding.h; sourceTree = ""; }; F4CE8A12723ABBCC55A89EB52BEB7E84 /* SDImageCoderHelper.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageCoderHelper.h; path = SDWebImage/Core/SDImageCoderHelper.h; sourceTree = ""; }; F5AEF10212D323726736313347CAA30B /* SDWebImageCacheKeyFilter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDWebImageCacheKeyFilter.h; path = SDWebImage/Core/SDWebImageCacheKeyFilter.h; sourceTree = ""; }; F5CC2EA5805079A0B25919E89EAB7C84 /* OCMArgAction.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMArgAction.h; path = Source/OCMock/OCMArgAction.h; sourceTree = ""; }; F655091C80A455BB5F06459C5CC0EC71 /* TyphoonCollaboratingAssembliesCollector.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonCollaboratingAssembliesCollector.h; path = Source/Factory/Internal/TyphoonCollaboratingAssembliesCollector.h; sourceTree = ""; }; F662CD4F60FC2C37B2BEB530D71C3725 /* NSInvocation+TCFWrapValues.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSInvocation+TCFWrapValues.h"; path = "Source/Factory/Internal/NSInvocation+TCFWrapValues.h"; sourceTree = ""; }; F77D3BA16F4A5B6BBE117AA9B4BC653E /* SDAnimatedImagePlayer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDAnimatedImagePlayer.m; path = SDWebImage/Core/SDAnimatedImagePlayer.m; sourceTree = ""; }; F813C3F784E8340DED279F0AC918CD92 /* TyphoonFactoryDefinition.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonFactoryDefinition.m; path = Source/Definition/Internal/TyphoonFactoryDefinition.m; sourceTree = ""; }; F817EA9532FEA2C318FD55D4CFE8D722 /* UIButton+WebCache.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIButton+WebCache.m"; path = "SDWebImage/Core/UIButton+WebCache.m"; sourceTree = ""; }; F8217C098AD84E361BF0AFBB60065247 /* RACDelegateProxy.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACDelegateProxy.h; path = ReactiveObjC/RACDelegateProxy.h; sourceTree = ""; }; F867F81312FCE227A0925280E5C9263C /* MASLayoutConstraint.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = MASLayoutConstraint.m; path = Masonry/MASLayoutConstraint.m; sourceTree = ""; }; F89876C64AB0B6C50E8EE3A5E905CBB2 /* NSNotificationCenter+OCMAdditions.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSNotificationCenter+OCMAdditions.h"; path = "Source/OCMock/NSNotificationCenter+OCMAdditions.h"; sourceTree = ""; }; F9523376659FAD01D3AAF8C11D821E14 /* TyphoonDefinitionRegisterer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonDefinitionRegisterer.h; path = Source/Factory/TyphoonDefinitionRegisterer.h; sourceTree = ""; }; F9B6404AB89F0CB5797BFB129EE1B511 /* Typhoon-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Typhoon-Info.plist"; sourceTree = ""; }; F9B73C4A3EAA71EE3B8D1F790D768438 /* NSNotificationCenter+RACSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSNotificationCenter+RACSupport.h"; path = "ReactiveObjC/NSNotificationCenter+RACSupport.h"; sourceTree = ""; }; FA43EAB3E19854BD7C0DF6BA3C16B9AF /* UIImage+ForceDecode.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIImage+ForceDecode.h"; path = "SDWebImage/Core/UIImage+ForceDecode.h"; sourceTree = ""; }; FA664986925B201863730F10AB76F9D5 /* NSObject+TyphoonIntrospectionUtils.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSObject+TyphoonIntrospectionUtils.m"; path = "Source/Utils/NSObject+TyphoonIntrospectionUtils.m"; sourceTree = ""; }; FAAC0B25413E2A1F2085ADA648D51593 /* OCMock-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "OCMock-prefix.pch"; sourceTree = ""; }; FAD1079497F1714CF45F8A6BF851636D /* NSString+RACKeyPathUtilities.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+RACKeyPathUtilities.h"; path = "ReactiveObjC/NSString+RACKeyPathUtilities.h"; sourceTree = ""; }; FAEAEC0032244300776CF7AFB0401C03 /* TyphoonComponentFactory+InstanceBuilder.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "TyphoonComponentFactory+InstanceBuilder.h"; path = "Source/Factory/Internal/TyphoonComponentFactory+InstanceBuilder.h"; sourceTree = ""; }; FB0CBD82662E0CBDB55A48F245646888 /* SDDisplayLink.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SDDisplayLink.m; path = SDWebImage/Private/SDDisplayLink.m; sourceTree = ""; }; FB44908A42BD6D708EAD4C6F7B6F3B5A /* NSInvocation+RACTypeParsing.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSInvocation+RACTypeParsing.m"; path = "ReactiveObjC/NSInvocation+RACTypeParsing.m"; sourceTree = ""; }; FB49799876CD2FF91D515D753BBB6D1F /* RACUnarySequence.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = RACUnarySequence.h; path = ReactiveObjC/RACUnarySequence.h; sourceTree = ""; }; FB65E6E7D6717ADB26576EA05A351211 /* OCMArg.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMArg.h; path = Source/OCMock/OCMArg.h; sourceTree = ""; }; FCD14B4785B937EC852B628A8B7F5227 /* OCMNotificationPoster.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = OCMNotificationPoster.m; path = Source/OCMock/OCMNotificationPoster.m; sourceTree = ""; }; FCD6A66B2012CDBD25E31AE588E07483 /* OCMLocation.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = OCMLocation.h; path = Source/OCMock/OCMLocation.h; sourceTree = ""; }; FD27FFD79501082E67620A5F5585D2D0 /* NSInvocation+TCFInstanceBuilder.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSInvocation+TCFInstanceBuilder.m"; path = "Source/Factory/Internal/NSInvocation+TCFInstanceBuilder.m"; sourceTree = ""; }; FD2C416CACE6B5586F404FC1E9F617FA /* TyphoonReferenceDefinition.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonReferenceDefinition.h; path = Source/Definition/Internal/TyphoonReferenceDefinition.h; sourceTree = ""; }; FDECB72A0984F14F0CAF5B4A6D8E0849 /* YYModel.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = YYModel.modulemap; sourceTree = ""; }; FDF2495A0E39E684767BAF85F109189B /* SDImageGraphics.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SDImageGraphics.h; path = SDWebImage/Core/SDImageGraphics.h; sourceTree = ""; }; FE4F16AD593A4CCB580F7CD5B86431EF /* TyphoonAbstractDetachableComponentFactoryPostProcessor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = TyphoonAbstractDetachableComponentFactoryPostProcessor.h; path = Source/Configuration/TyphoonAbstractDetachableComponentFactoryPostProcessor.h; sourceTree = ""; }; FE568D9819827B0D7AC039F020E84BB1 /* UITableViewCell+RACSignalSupport.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UITableViewCell+RACSignalSupport.h"; path = "ReactiveObjC/UITableViewCell+RACSignalSupport.h"; sourceTree = ""; }; FF9570A386DE6A77332680BFBB13573F /* UIImage+Metadata.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImage+Metadata.m"; path = "SDWebImage/Core/UIImage+Metadata.m"; sourceTree = ""; }; FFBE8455C54B78A685FE3CC5721FC439 /* UIImagePickerController+RACSignalSupport.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIImagePickerController+RACSignalSupport.m"; path = "ReactiveObjC/UIImagePickerController+RACSignalSupport.m"; sourceTree = ""; }; FFD16E6D61E5F1AA7ACD347FD6E0120D /* TyphoonNSURLTypeConverter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = TyphoonNSURLTypeConverter.m; path = Source/TypeConversion/Converters/TyphoonNSURLTypeConverter.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ 0CB5494E5539E84494DEEC0B4BB3991D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 29939A199EE4BAE8976AEC88E59F2ABB /* CoreFoundation.framework in Frameworks */, 5005432EAECE0BBCAE0487FB541489F7 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 12A799DC8ABB2C283ADDDED4421A5EAB /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D663837F4347AF58660EE6F7FD426ECE /* Foundation.framework in Frameworks */, 4571A0EA37DC84F39E3830D38A1531AB /* UIKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 3A5330E1BD187252F408EBB46F1BDC42 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 8414CFEEB64ACA817EB88D2FEADDA3B3 /* Foundation.framework in Frameworks */, 3777CD89D444CBBB48AE323B303F3FC7 /* ImageIO.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 3C9BA069841AE1F4B830AFE040DBAF05 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( D875F931575EA6C3308F17F716CC8748 /* Foundation.framework in Frameworks */, AA14FA452C6E6DCB9D55BEC86A91ECB2 /* SystemConfiguration.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 412AD72FD28E53A869CC1DE993783892 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 40613D5600FE76DC9F8F377846577314 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 419B034446CD8E2FD17F87A0EAEC979A /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 423E6C84ACB40AD69A48E1B0525B5501 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( B1536B8A4A473A9779083C563063B9C8 /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; A8288B9C9F7E3201CF4C5CBD9735F905 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( B13C2503DD42AF17264AA8F1724C831F /* Foundation.framework in Frameworks */, 7D2E8066457576BA5C42D43C38FDE09B /* XCTest.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; AFA73E0444C77BC1DA31F0305CEBE0C7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 5A592D4D0B89373E214B385BF7D4076D /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; D18029DDA26EDC747F5E47916C3BB239 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 1046610A51620E727140217FEC6DD5BF /* Foundation.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; E672724427269716049B769344D7C3DC /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 15B27182B591769C57B55544260DC886 /* Foundation.framework in Frameworks */, D968461E31E8FF3FF6BA1DC621B0433B /* UIKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; FA3F4FA98DFFB612E3B16EA4A609DAB7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 147A5463FF6FFE40426B898B60344085 /* CFNetwork.framework in Frameworks */, 561D1F03868EC8BA84A2B6A22031CB49 /* Foundation.framework in Frameworks */, 0BF77394EAA86673E7948ECD9871D89F /* Security.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 06B5071AB91D45EB780EF7ECCD5BC590 /* Support Files */ = { isa = PBXGroup; children = ( C0C099CEA20C5E235C69D071EC07EF0B /* Reachability.modulemap */, AFDA4AF26CABDC552E1905C902431F4F /* Reachability-dummy.m */, 8C279B3C5F55629685F87467C53EA540 /* Reachability-Info.plist */, A6E0FF3678C9C92FF565548A1B306517 /* Reachability-prefix.pch */, 8B40A0C264DE39B5EEE32E9062A0CCF2 /* Reachability-umbrella.h */, 72ADBA2DDCD657373F64C49B51DA5B7D /* Reachability.debug.xcconfig */, 48D7189A91D3148BCB7AB21867EC74DA /* Reachability.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/Reachability"; sourceTree = ""; }; 07878FB8E434E88470AE99EF245DD87D /* DZNEmptyDataSet */ = { isa = PBXGroup; children = ( 1F40215065345DB127D2130E4F8AA842 /* UIScrollView+EmptyDataSet.h */, 91DE4AD4922A1500C3D8808438EBC84A /* UIScrollView+EmptyDataSet.m */, 8A43768854093C54A621A97195F27F64 /* Support Files */, ); name = DZNEmptyDataSet; path = DZNEmptyDataSet; sourceTree = ""; }; 08C5E0DB14551BD777073CA1CFDAC5E2 /* Support Files */ = { isa = PBXGroup; children = ( 43C4233C6624441991C2F578A1ACAE04 /* OCMock.modulemap */, B1233E43A91D5DA56B2AA3A634E7BF05 /* OCMock-dummy.m */, 3E251AA3EC1653CC267FDFD659704A71 /* OCMock-Info.plist */, FAAC0B25413E2A1F2085ADA648D51593 /* OCMock-prefix.pch */, A2ABDBBFB28FCCED57D0D680E16EBA76 /* OCMock-umbrella.h */, C1438D60DB93FD77AC7CEB329AA4D842 /* OCMock.debug.xcconfig */, E6BFAC38D0C1749C0AF02A5825F2ECBB /* OCMock.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/OCMock"; sourceTree = ""; }; 0C4BAF90A7DB1F47264F6D82B599DF24 /* Support Files */ = { isa = PBXGroup; children = ( 13B5BE064B370CA187BEE420A188E41F /* CocoaAsyncSocket.modulemap */, 6D7D2C3B5323BC3D3465786EE817BE2D /* CocoaAsyncSocket-dummy.m */, 8D633DA1F2ABA3E82E11435AE33357EC /* CocoaAsyncSocket-Info.plist */, EB61987B9EEE414AA540987787047EEE /* CocoaAsyncSocket-prefix.pch */, 1FFAB3AC801B7EA6B022EBB03D54462C /* CocoaAsyncSocket-umbrella.h */, 9B87846900F219AB28D136A1C7C67730 /* CocoaAsyncSocket.debug.xcconfig */, B64CD45769D06F336C347EDF755FC7E0 /* CocoaAsyncSocket.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/CocoaAsyncSocket"; sourceTree = ""; }; 0DCB8C6204AF09EB069414469054876F /* OCMock */ = { isa = PBXGroup; children = ( D66DFDCCBA63EB04C097EA2E0C475EE5 /* NSInvocation+OCMAdditions.h */, 47E16F28A08ED8AA345A07CC51EAF129 /* NSInvocation+OCMAdditions.m */, AACE72B147E103C5ABF8DD49C677152C /* NSMethodSignature+OCMAdditions.h */, DEE925C4BC3366755D6B62640CCCFCAE /* NSMethodSignature+OCMAdditions.m */, F89876C64AB0B6C50E8EE3A5E905CBB2 /* NSNotificationCenter+OCMAdditions.h */, 06E2166895FA1466EACF8506DAA383DB /* NSNotificationCenter+OCMAdditions.m */, E93FFD1537CD2D6332F9A080E750CAAE /* NSObject+OCMAdditions.h */, 38F730D07E4C4D067A10C1C091A76D44 /* NSObject+OCMAdditions.m */, 1D3495A662B6322B041706885A769B02 /* NSValue+OCMAdditions.h */, 3D4FB47B1FC93F629C32CDEE69E59AE4 /* NSValue+OCMAdditions.m */, B7CC02F4E9B33CCE0CE749A8F5F1ACA7 /* OCClassMockObject.h */, 4AC7882AF54A36E3A226CA3878D872F7 /* OCClassMockObject.m */, FB65E6E7D6717ADB26576EA05A351211 /* OCMArg.h */, D21426F24A160A03F3A3B57A03B3671E /* OCMArg.m */, F5CC2EA5805079A0B25919E89EAB7C84 /* OCMArgAction.h */, 3DA3CE4677195B9C5B8F50C6E3B91D81 /* OCMArgAction.m */, 9D066AE9F0D7A4328D8690DF15F20AA0 /* OCMBlockArgCaller.h */, 1610B6558D631E16BFCF0FD099283BDA /* OCMBlockArgCaller.m */, 14EDB30D06B6BB66F89E7DC87C6F82CE /* OCMBlockCaller.h */, 437DE71945F67DF09BAC5E5BB65C5A20 /* OCMBlockCaller.m */, 1C87CAE8BB86399A29C7AAFACF0CD24D /* OCMBoxedReturnValueProvider.h */, ED4B3532992A08C1DE0094AFAED64A03 /* OCMBoxedReturnValueProvider.m */, 5649D72ECEC9159D3DAA4B7051E947B9 /* OCMConstraint.h */, 3A12D240C381406917C3EFB576E33710 /* OCMConstraint.m */, E8561AC4257A4B8D0B0569EEEA2E4555 /* OCMExceptionReturnValueProvider.h */, 1E25BC9F9B2C745B6471F74CCB47DC7F /* OCMExceptionReturnValueProvider.m */, E1D2B79FD0B5E7ECF99452AC0439FB6D /* OCMExpectationRecorder.h */, 437B0AD6B69A2013C65A324A3CBBF3CA /* OCMExpectationRecorder.m */, 0EB0DF190C36C4090D74F64BB54053A6 /* OCMFunctions.h */, 9C26E7915BAEE3BDE02BCD9D2D389B9A /* OCMFunctions.m */, 73855182D583882A717B2006B73A0953 /* OCMFunctionsPrivate.h */, 2EEE06C505E1457D30A4ACDA3820D377 /* OCMIndirectReturnValueProvider.h */, 9752AAF7F49C7CFA8636AB119BF88C35 /* OCMIndirectReturnValueProvider.m */, D400046FCD67C3ECE87A4FD7570DF9B7 /* OCMInvocationExpectation.h */, 80863B62A376A21783063A91F74E52C6 /* OCMInvocationExpectation.m */, 4DB0A4F8CE90B57FB6935727ABCA7CE2 /* OCMInvocationMatcher.h */, 14BA0C92570BFCBF28C82F32F37A96B6 /* OCMInvocationMatcher.m */, B6C0AFCABBA43F288D26D6A9CA0DB2DA /* OCMInvocationStub.h */, DF3E60E69433B185D69240ECF94CFCFB /* OCMInvocationStub.m */, FCD6A66B2012CDBD25E31AE588E07483 /* OCMLocation.h */, 0D5BC3205064C311F17DDCAF73FE5D91 /* OCMLocation.m */, 7E53BFD7942CD993EE50F7FD12C87B2C /* OCMMacroState.h */, 814AAFBE786E0EBCAFD7AC642DA8F8EF /* OCMMacroState.m */, B663AA0974236ABB5B164B3F2FCA9874 /* OCMNonRetainingObjectReturnValueProvider.h */, 357902381BACEC332590C847106F5C16 /* OCMNonRetainingObjectReturnValueProvider.m */, B5D03644E567590DEDC2D0AE3425D98D /* OCMNotificationPoster.h */, FCD14B4785B937EC852B628A8B7F5227 /* OCMNotificationPoster.m */, A840AE48AD863CF06B48FE55C745EA9D /* OCMObjectReturnValueProvider.h */, 8C3A7610419191166B2C17925674DE17 /* OCMObjectReturnValueProvider.m */, 0FC01820ACD24F5CF0969F06380ABFE9 /* OCMObserverRecorder.h */, F4403468E084E9FF3F896A3734CD95FB /* OCMObserverRecorder.m */, 4987ACD1FED6CF90E9AE135E75E5AE57 /* OCMock.h */, EF447269480FB0580228D478883FFA69 /* OCMockMacros.h */, A9267DE7A89C784B346A9CA842751576 /* OCMockObject.h */, 8375DB2B7CC0F5AD235F69805AF4B1BA /* OCMockObject.m */, 5881CE458D78CE4C7C6B914138FDE55B /* OCMPassByRefSetter.h */, C14BDA045B4B1D88E3900EFD02FFA862 /* OCMPassByRefSetter.m */, 5603722E8B41BC93F91BC672658EA10E /* OCMQuantifier.h */, E59E49F550CAE1C1C8665F6CCF893735 /* OCMQuantifier.m */, 45BD3FAB4630B2990E6B8F1BC78D9D8F /* OCMRealObjectForwarder.h */, 5E440830ABB9D3025B26A80937A2EFE9 /* OCMRealObjectForwarder.m */, 1F18C60D29A81CE3566D4A38800979FA /* OCMRecorder.h */, C0225799E4F72C1CA22CAA520A08E589 /* OCMRecorder.m */, A139B74B3BD8E154D5BC6567D4D692D4 /* OCMStubRecorder.h */, 8C3F45C3BA62C5E06E78277D0D81E73D /* OCMStubRecorder.m */, 3C12837A69C57039BE360EAF15FE6C76 /* OCMVerifier.h */, 83104DD312D9864E2BAA39D25537ADAA /* OCMVerifier.m */, 8F54C727953E14DD511A46A9F90E8937 /* OCObserverMockObject.h */, 36EA9831D45C8C2DBBE9C8924552B0F3 /* OCObserverMockObject.m */, C8A5867815E6437C1B59CDE852811716 /* OCPartialMockObject.h */, 1AE4A447234AC4D5BD55348C3E5BC6E8 /* OCPartialMockObject.m */, 7F84B399A305F1F7EAFB02599E56A1B1 /* OCProtocolMockObject.h */, 78E1C3D8226EEA6918CB1B8A96FC843A /* OCProtocolMockObject.m */, 08C5E0DB14551BD777073CA1CFDAC5E2 /* Support Files */, ); name = OCMock; path = OCMock; sourceTree = ""; }; 149CFEB4FF5E4DB615D80FDAE578EB0C /* Targets Support Files */ = { isa = PBXGroup; children = ( 30ACDC9459A7CB1F4567A7F6B1EA8040 /* Pods-iOS-Network-Stack-Dive */, B04E6303DEBB965117D11508CF337DF4 /* Pods-iOS-Network-Stack-DiveTests */, ); name = "Targets Support Files"; sourceTree = ""; }; 188DAC4BBD3CE26D8F1253736B3F7CB5 /* YYModel */ = { isa = PBXGroup; children = ( C3133631DC229F9194ABFF320BE45553 /* NSObject+YYModel.h */, 7CC392EF120EE41995D6918D82EC742B /* NSObject+YYModel.m */, 50A9EB17E5CD95FF47A7DC9D1F6FB3B4 /* YYClassInfo.h */, 33C36535BCF88C0F8CE4BEE165483F16 /* YYClassInfo.m */, 325FC278EFE1EDE9A708FE21611C503E /* YYModel.h */, 3554C2D22855B675DDB67B64D1A502EA /* Support Files */, ); name = YYModel; path = YYModel; sourceTree = ""; }; 1FF60BCB01852F445494BA9733E861EB /* Products */ = { isa = PBXGroup; children = ( 6CBEFE4F9E22AFDC6347A739BB35FF8C /* CocoaAsyncSocket */, 5DA4577FE3BC4A03751108FFED07B385 /* DZNEmptyDataSet */, 1FFED36A657123030ABB700256D73F15 /* Masonry */, F30C125DB91902F63DC73AEF0B0E6111 /* OCMock */, 8AEA2F92AE161A3CD9059649BB497327 /* Pods-iOS-Network-Stack-Dive */, A443C69F8A192846AC63B3202379F32A /* Pods-iOS-Network-Stack-DiveTests */, 400FF55D0451E7A8F33A3D0D3E11C1B9 /* Reachability */, 9621C6383F5733C35183B2DE886C7EC6 /* ReactiveObjC */, B0B214D775196BA7CA8E17E53048A493 /* SDWebImage */, CF1281E58AA1045D4B7F33FC56691C42 /* SDWebImage-SDWebImage */, 6C08E1C10481BD7FAC0F4A2019635945 /* Typhoon */, E460D5B0416D36F66EE8EC89E5D2FA0A /* YYModel */, ); name = Products; sourceTree = ""; }; 2D1EE2372A7AC2DC12EE670EC92D3CCE /* Reachability */ = { isa = PBXGroup; children = ( C51A574AFE5E5029BD52D9AC1C54C204 /* Reachability.h */, A420909D1D09A79AE6103170D58A4332 /* Reachability.m */, 06B5071AB91D45EB780EF7ECCD5BC590 /* Support Files */, ); name = Reachability; path = Reachability; sourceTree = ""; }; 30A2528A35C4C6F5862F161A9271628A /* Support Files */ = { isa = PBXGroup; children = ( 073742952E39DC51EF6052E0131E74C6 /* ReactiveObjC.modulemap */, 1E270F13C426BEA6928171EC9DBCEE70 /* ReactiveObjC-dummy.m */, 984867BE9A2FC89AE6F4546840D5113D /* ReactiveObjC-Info.plist */, 2F2015FF4C85AFF50A9D9CA166D8C5A4 /* ReactiveObjC-prefix.pch */, 5EB6B9FD0EAFE674F066B55BF02CBAD8 /* ReactiveObjC-umbrella.h */, 9240686C7AC135C5EA0942E5C3D224AC /* ReactiveObjC.debug.xcconfig */, A9F3496E6CC2C56684ACF7B2579BB765 /* ReactiveObjC.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/ReactiveObjC"; sourceTree = ""; }; 30ACDC9459A7CB1F4567A7F6B1EA8040 /* Pods-iOS-Network-Stack-Dive */ = { isa = PBXGroup; children = ( EDE075C32619BCEAF4A3F8B8AFFA26BD /* Pods-iOS-Network-Stack-Dive.modulemap */, 6BAD6CF39127B4111B23EB0A1D7758E3 /* Pods-iOS-Network-Stack-Dive-acknowledgements.markdown */, 824B55D57BB9EC7E920131DD9197C61E /* Pods-iOS-Network-Stack-Dive-acknowledgements.plist */, B755FFA45FCE441B80E378A091AA25F4 /* Pods-iOS-Network-Stack-Dive-dummy.m */, 0B107CBC10206BACB98A3A6524024891 /* Pods-iOS-Network-Stack-Dive-frameworks.sh */, AB5B563B359E9B2E8813C9A2B5091E02 /* Pods-iOS-Network-Stack-Dive-Info.plist */, 4E1DD37617D3B108328AB936760E6BDD /* Pods-iOS-Network-Stack-Dive-umbrella.h */, 353C12944450C35AA7A2B53F9A9EB5B8 /* Pods-iOS-Network-Stack-Dive.debug.xcconfig */, 0A11CE004B91D55C90C201FD9902D9A8 /* Pods-iOS-Network-Stack-Dive.release.xcconfig */, ); name = "Pods-iOS-Network-Stack-Dive"; path = "Target Support Files/Pods-iOS-Network-Stack-Dive"; sourceTree = ""; }; 3554C2D22855B675DDB67B64D1A502EA /* Support Files */ = { isa = PBXGroup; children = ( FDECB72A0984F14F0CAF5B4A6D8E0849 /* YYModel.modulemap */, 6A1ED1A10CBE44A4F0190574BE33E602 /* YYModel-dummy.m */, 8597C3C4E469AD35635B7B0381951012 /* YYModel-Info.plist */, 2B7641571C129F0F034C78A348461477 /* YYModel-prefix.pch */, 9B744979B63E3F8D6CD8B04CCE7E0E58 /* YYModel-umbrella.h */, D1D5C8198B49A7A43168157C6AAACF8B /* YYModel.debug.xcconfig */, 2DAE20DF4B8369B96AEB7A6A762F3DF5 /* YYModel.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/YYModel"; sourceTree = ""; }; 39EDF0865A79D051B3793779D4DDF20F /* Support Files */ = { isa = PBXGroup; children = ( 62678B3576827B7404F1FB231B054D0C /* ResourceBundle-SDWebImage-SDWebImage-Info.plist */, BF03F228FDA5C0221C20EF2FDD92BDE7 /* SDWebImage.modulemap */, 576D3C34B2B7AC921593D4F072ECCBA3 /* SDWebImage-dummy.m */, A18C6BD836E25BB4DA9691CE3262B0B6 /* SDWebImage-Info.plist */, 8A6B4184DF30072301A96D9DB46F53F7 /* SDWebImage-prefix.pch */, 0453653DE2FEC83ACFBA5BA70EBEF291 /* SDWebImage-umbrella.h */, D3872D84DFE6C4C6FEBC86D6D30C705B /* SDWebImage.debug.xcconfig */, 5811A6AF5EFEA30ABF2E338DE7F3646D /* SDWebImage.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/SDWebImage"; sourceTree = ""; }; 3FA81B81222DED05CCEF2A9F5A5CD223 /* ReactiveObjC */ = { isa = PBXGroup; children = ( 2D39AF61764732228A5146A67595AEAE /* MKAnnotationView+RACSignalSupport.h */, 53B86BED43D4B74CCE944876C11E57B1 /* MKAnnotationView+RACSignalSupport.m */, 43154AF288B8222D79172AC422866E6C /* NSArray+RACSequenceAdditions.h */, DF3C6B3C4596923C4E111113DF611DF6 /* NSArray+RACSequenceAdditions.m */, 5E4B3FE710833AB76C40A34DCAB075CA /* NSData+RACSupport.h */, 0CEAF4D7D00B3A496611716400E24A3C /* NSData+RACSupport.m */, 3DC413ABFFB1444FE469D466ADE4A98C /* NSDictionary+RACSequenceAdditions.h */, 7CE2A8DCEE78DE1933B81C994103BE63 /* NSDictionary+RACSequenceAdditions.m */, 74E20E5BB34220E9282F27E20A9A567E /* NSEnumerator+RACSequenceAdditions.h */, 59FB7D5724747D377FC8799D1133C4A5 /* NSEnumerator+RACSequenceAdditions.m */, 795FD8B2FD7ED943A340C32633ED18F7 /* NSFileHandle+RACSupport.h */, B54343D5BDC4CA5AB85FD35468187848 /* NSFileHandle+RACSupport.m */, 252FA21B0D96A56E65E1481A1B27A6A3 /* NSIndexSet+RACSequenceAdditions.h */, 14752B57BED25D3E2474FA3AFEB5ECD7 /* NSIndexSet+RACSequenceAdditions.m */, 9CB472E36DC6E2E3FEF5B6638CA34EC5 /* NSInvocation+RACTypeParsing.h */, FB44908A42BD6D708EAD4C6F7B6F3B5A /* NSInvocation+RACTypeParsing.m */, F9B73C4A3EAA71EE3B8D1F790D768438 /* NSNotificationCenter+RACSupport.h */, 0DBB238DBD9086C4A06C98A1C750EA43 /* NSNotificationCenter+RACSupport.m */, F383EDD80DA3184C9A032B89FD763BB7 /* NSObject+RACDeallocating.h */, B4250F1A74D43D858FA5327444397E4B /* NSObject+RACDeallocating.m */, 72274AF631E665EB74378E781E9A7437 /* NSObject+RACDescription.h */, B2042CA0D12A3305ACABBBC22E60BE48 /* NSObject+RACDescription.m */, 633F2FE6EBD79D22A6B840F443F0A83B /* NSObject+RACKVOWrapper.h */, 8536C813226AC2CB3B45AB654F52A79D /* NSObject+RACKVOWrapper.m */, 5FAD9C753437FC1DCDDA7F7F6A8B9C29 /* NSObject+RACLifting.h */, 032C0CB079DA640D62F451CF2BB09DBF /* NSObject+RACLifting.m */, 9998C36BB30E19196F7C6F7D1F3EF49B /* NSObject+RACPropertySubscribing.h */, BFB3821735D0527AC675FE95C25658F8 /* NSObject+RACPropertySubscribing.m */, 45512E8214358C10C4B33C4B78C7026F /* NSObject+RACSelectorSignal.h */, 8386D6BB6CF3F2D21ED967CC288819B0 /* NSObject+RACSelectorSignal.m */, 0A69C05290E3D199301DB5557505AEEB /* NSOrderedSet+RACSequenceAdditions.h */, 2385FD3E31A6F83168DE9EFA6AC4A66F /* NSOrderedSet+RACSequenceAdditions.m */, E369629E03DEAACC1711DB17684549A9 /* NSSet+RACSequenceAdditions.h */, 11CA0DD35189B363533FA7DEBEAFC7E5 /* NSSet+RACSequenceAdditions.m */, FAD1079497F1714CF45F8A6BF851636D /* NSString+RACKeyPathUtilities.h */, 0EA9D6ACAA4596DDBF52BD0B1662EB44 /* NSString+RACKeyPathUtilities.m */, 06DAA5ED399AAB042406902A676EED50 /* NSString+RACSequenceAdditions.h */, 2D163BF4979C587AB2708ADA67ABB425 /* NSString+RACSequenceAdditions.m */, 8D7A7568A955352E3C8BEA8C6BE5EFA7 /* NSString+RACSupport.h */, 3290EEA634B358879EBD921ACF1B1422 /* NSString+RACSupport.m */, 98A3E71FFFC45DA5BB85A8525B39AC52 /* NSURLConnection+RACSupport.h */, 3E461ADDAEAAFCFADE347E2C9267118A /* NSURLConnection+RACSupport.m */, DAA9B1CEFE3B5F49253B19B6F13CBE00 /* NSUserDefaults+RACSupport.h */, 41BDCD9D2400DA765DCB894C8509679B /* NSUserDefaults+RACSupport.m */, E49D958DD1C460DCA691F17881DE66D9 /* RACAnnotations.h */, 9754B128A1DF8F57658751C1302FF46F /* RACArraySequence.h */, 28CC19C7A5B9A7C5439E55E906813635 /* RACArraySequence.m */, 16BEA40224A6DCCBB7E983D78F3CE027 /* RACBehaviorSubject.h */, CBD6CB22BEFFD7B25F968C06E8168BE5 /* RACBehaviorSubject.m */, 25FCBE4E8D3998433D6BFA94FB2FAC69 /* RACBlockTrampoline.h */, E37027B951A73179B2F4337FF41422D7 /* RACBlockTrampoline.m */, 246818BE8D07C34C5CC6113926619C5F /* RACChannel.h */, ADA38AFC2E679FBA25375054E69DEFB2 /* RACChannel.m */, 74FEA9E372A4AFA285192EC20BF9AC10 /* RACCommand.h */, 551D5C997519E15B230BE54573BA8B99 /* RACCommand.m */, 8B2D3D98B9F08D80C8134D1C5346BA57 /* RACCompoundDisposable.h */, 06AEB99F8D2F4D764FE818A7D523C570 /* RACCompoundDisposable.m */, F13BB06DB4576664D2BD21C5F4490F59 /* RACCompoundDisposableProvider.d */, F8217C098AD84E361BF0AFBB60065247 /* RACDelegateProxy.h */, 67EB4C9AA76FDBC2CD3FFBA7EE37A27C /* RACDelegateProxy.m */, B9B7D7D1F5CDB012C8AEE5F8EF3F513D /* RACDisposable.h */, 419B699C2112F512494E7DDB5FE14FCA /* RACDisposable.m */, 0E69DD9AA22CCF06F1224BEB3E20EDD3 /* RACDynamicSequence.h */, 15CD57574E2617CB7558B0BD733BAFC2 /* RACDynamicSequence.m */, 1F5DE94BB623F6C7A33F0A746E8F58DB /* RACDynamicSignal.h */, 27871BA973C3B687A1D538285665EBB6 /* RACDynamicSignal.m */, 945289B9A84910C8F31646C46C4531EB /* RACEagerSequence.h */, DB54DF3DB0FDADB1A3893CFF8C1AF03A /* RACEagerSequence.m */, A9638C9372C5407A0209909EB7BAFDCA /* RACEmptySequence.h */, DB2ED745168A43281FE03EC58108FBDE /* RACEmptySequence.m */, D8706CB35453DAA97FE6E56A07BF5E79 /* RACEmptySignal.h */, 2604998C21DB77EB38BB124802109F52 /* RACEmptySignal.m */, F028FB003652E9875D30DDAF99C21162 /* RACErrorSignal.h */, 5ACE3A2A33422C2FD382351D2E96A005 /* RACErrorSignal.m */, CFCB3855AE45E56DBEA7833054EAD44F /* RACEvent.h */, 861A84C50B238264389CE8E34734EE4C /* RACEvent.m */, F4690548BF3F6C5A71CDC7BF7187EC20 /* RACEXTKeyPathCoding.h */, 2F26AA73AA98E00F8C916633F02D50F8 /* RACEXTRuntimeExtensions.h */, AA87031490FD30914ED01BF6A257DB56 /* RACEXTRuntimeExtensions.m */, BCD5C7A53B74E13B1712A968B6AD9302 /* RACEXTScope.h */, 686988B863D5701A04411416EC7BF222 /* RACGroupedSignal.h */, 38B6C5CA78D03B16417EF54377D96D24 /* RACGroupedSignal.m */, 1443B87E860FFFC4733D9B16073E19F9 /* RACImmediateScheduler.h */, F0C0E50FA94D388C9007312869F096D5 /* RACImmediateScheduler.m */, 9D9038CEC928B9AF5CFA2FD0B75B9FEF /* RACIndexSetSequence.h */, F069B23BF8F0BF5B39AAFB166C957FA3 /* RACIndexSetSequence.m */, B61B3D89FE168025B8BCCE1DE7423DF5 /* RACKVOChannel.h */, E3CC05377476E8CA3BDD23296111A57A /* RACKVOChannel.m */, CA677B07FC524F62109AE180874AB60E /* RACKVOProxy.h */, 856CE023A29559B385271D8014E85BC3 /* RACKVOProxy.m */, E6FF1599AA4D761C6A0D334AC8A18BB7 /* RACKVOTrampoline.h */, 871E83256238E28BEC109FBAA903A7BE /* RACKVOTrampoline.m */, 950CD6BE6707ED534FF448D9CC40FA3D /* RACmetamacros.h */, 66D7BE61D87C0C7666DBE752021D186B /* RACMulticastConnection.h */, 2E023F08FB04D7249EE7E6B1E84A12DC /* RACMulticastConnection.m */, AFCF2DB5C3722AD61AAD5876C0E5E4CF /* RACMulticastConnection+Private.h */, CB8729BC7993BA548B4D892BE63AEE83 /* RACPassthroughSubscriber.h */, 472F840FC38C62F9E93AD1BEE1A70F95 /* RACPassthroughSubscriber.m */, A86A73484750ADD56A5905600D66D803 /* RACQueueScheduler.h */, EC243AD4E47E5CAA863DF364A86CA635 /* RACQueueScheduler.m */, 9AA886724CF6656F190D039297F38E78 /* RACQueueScheduler+Subclass.h */, 141EB3C3A6A1510AA72DD2C9ACD6894C /* RACReplaySubject.h */, EA318A2FA8725340866FC3083915FCFD /* RACReplaySubject.m */, BFD56F486BA750FFE7B13856AC85FEA2 /* RACReturnSignal.h */, 229B6EF02232FF52DA22913B1FB01A1F /* RACReturnSignal.m */, 4D249879A5BB3605D92C3BCE919251C0 /* RACScheduler.h */, 027098AE8C5EB84C31AACA284246D555 /* RACScheduler.m */, 0B2C8AD692D98D47C5658A9FE6A6DE4F /* RACScheduler+Private.h */, 183FD7641AE552C24F25EC96C491FCBF /* RACScheduler+Subclass.h */, 5A66CEA5D0C3EE038662C0D25CA04F50 /* RACScopedDisposable.h */, A64869131A94BAF78EB608FAED187F90 /* RACScopedDisposable.m */, 9A2036089AC8297FFEE003F786496211 /* RACSequence.h */, 793AD3A27E2A7A7D6570D1D0B8E748CF /* RACSequence.m */, 26BFC051219DBE2ECE65E6DD31A177F9 /* RACSerialDisposable.h */, 0BB3D4EE11AB02C35A56E26CD538EA8D /* RACSerialDisposable.m */, 2C69DF26B88BA2D74723697231D11733 /* RACSignal.h */, 3BCF3C4AEB69B5B155617E83726EB0A0 /* RACSignal.m */, 89570B4A4AE434196E38ABEADF0CB97E /* RACSignal+Operations.h */, 55EB526211489C964AE450F84E636291 /* RACSignal+Operations.m */, 89FE3BF0E622ACD1D48ABA7DCFF7855A /* RACSignalProvider.d */, 7D81778D9E6487677837D58B13894E94 /* RACSignalSequence.h */, 5354038C92F8485F19D1D15D3BF5FFFC /* RACSignalSequence.m */, 1DD1EDA8854FDDBB1314904C848EEADC /* RACStream.h */, 37CF1049FAEF879E02FD90463A2E3910 /* RACStream.m */, 8090506D09D6280408FEFDC474C367CF /* RACStream+Private.h */, 8A14CECA21E108EE12451203537A6B99 /* RACStringSequence.h */, 4B2FCD3B157174CAD7F173B5F1FF0DA1 /* RACStringSequence.m */, A28930506531365373875AD4CA53EDEC /* RACSubject.h */, 749EDDB4FA8F282C297D2ED71E5E2A95 /* RACSubject.m */, 143606750438523F029FB6DA44890C07 /* RACSubscriber.h */, 1291349B3C35F79CB6F631FD1CDCC1B0 /* RACSubscriber.m */, 2AE9080EF975331395E265C09E03252C /* RACSubscriber+Private.h */, 35811A877C1D60F1A3AB124ACBC29E9E /* RACSubscriptingAssignmentTrampoline.h */, C218F557360FC510BBA92A7B3EEC1065 /* RACSubscriptingAssignmentTrampoline.m */, 2E0DEBA6FAF64625746D89A660E075B9 /* RACSubscriptionScheduler.h */, AE9AC83D30952DBFFC1F9C714196F202 /* RACSubscriptionScheduler.m */, BDC02EE21DEF11D396F51D141C543D5C /* RACTargetQueueScheduler.h */, 0B91A0326C0D75CA3ED540C456F99FA7 /* RACTargetQueueScheduler.m */, D8C316292E777A36CEF459683AF45953 /* RACTestScheduler.h */, 115C7117F68E39901C8DF436A5193C71 /* RACTestScheduler.m */, AB7F2ED32F5F65686BE8BE48409DB271 /* RACTuple.h */, 1CB4CE5D415687F6A3499F360794DA04 /* RACTuple.m */, 21F7FF6AAD9960A7E34FA700AF4A2F1D /* RACTupleSequence.h */, 48E883D9DB5DF435F64617A907DCD908 /* RACTupleSequence.m */, FB49799876CD2FF91D515D753BBB6D1F /* RACUnarySequence.h */, 44725BC3ADC3FA22EF1DD0F3164C12FF /* RACUnarySequence.m */, B28FA990E304B0E57E1599F3DDAC5301 /* RACUnit.h */, B8FF03BB1DD7B75DF4D031C0DA692637 /* RACUnit.m */, 89A79290D890BC1A7E558C5CC31E06E3 /* RACValueTransformer.h */, 28B829B34223ADB0F04E28F63DEA2868 /* RACValueTransformer.m */, 65BC03570C4B6FD508A02A616BE7DB93 /* ReactiveObjC.h */, 42AC4C9E32B2DF0DF2366BB6D08859AE /* UIActionSheet+RACSignalSupport.h */, 276FD7F39BAD92CD4DE60118D6BDC68B /* UIActionSheet+RACSignalSupport.m */, 7EB4C9DFCDF6EB86413C60976F25BE67 /* UIAlertView+RACSignalSupport.h */, B4C1BAAAE45FAB43E8ECC69C9DC35A7D /* UIAlertView+RACSignalSupport.m */, 63342C5E18062111D90810C1BF11878B /* UIBarButtonItem+RACCommandSupport.h */, 9DA14F675C32DB069C21840DFF0D5B75 /* UIBarButtonItem+RACCommandSupport.m */, B05C75D2995DD598CD05DC64D113A11E /* UIButton+RACCommandSupport.h */, 6499637B2B14B146BF05E82D9F524DB0 /* UIButton+RACCommandSupport.m */, 2E4DF20E80C1D11D64201766B309D73A /* UICollectionReusableView+RACSignalSupport.h */, 87D6C46EF50641EB979304EC92ADD341 /* UICollectionReusableView+RACSignalSupport.m */, 707BB63597C3E112E47016299D9A2C40 /* UIControl+RACSignalSupport.h */, 8F1A6A9C14EDD26D5A9D9CE5506A3971 /* UIControl+RACSignalSupport.m */, 8D76E7370C1E6F4EE5AE4D70E5C4D2C2 /* UIControl+RACSignalSupportPrivate.h */, 93894FEF634076038AE57F0DA89A230A /* UIControl+RACSignalSupportPrivate.m */, 162947A4139E0A2C2197D6A479D07445 /* UIDatePicker+RACSignalSupport.h */, 201545627F7DB82D38F76A51D3D4C33F /* UIDatePicker+RACSignalSupport.m */, 43D883AF4752701FF9AD2DF1CF845589 /* UIGestureRecognizer+RACSignalSupport.h */, 1E295017E5EF894DD2C62B5422434280 /* UIGestureRecognizer+RACSignalSupport.m */, 2CD5CB6BE4742004BAEA7654CE3C6AF7 /* UIImagePickerController+RACSignalSupport.h */, FFBE8455C54B78A685FE3CC5721FC439 /* UIImagePickerController+RACSignalSupport.m */, 9267DAB90224A45DA4C991474C964341 /* UIRefreshControl+RACCommandSupport.h */, 78AC2407E9C7F573D2B8F2765AD61982 /* UIRefreshControl+RACCommandSupport.m */, 646DB1C71B2294140046C6270BDC8C22 /* UISegmentedControl+RACSignalSupport.h */, B7C029402AB7307D26D094182B34A802 /* UISegmentedControl+RACSignalSupport.m */, 38A1E514232587E860EC29C335D4DC68 /* UISlider+RACSignalSupport.h */, 4AA45AFD4053DDC8999AD121B914CE27 /* UISlider+RACSignalSupport.m */, 8B0A489E0C0397C62B216A44A21EDDFF /* UIStepper+RACSignalSupport.h */, AD8E6EFAEA3F539F1D1F0B9752F7E7EF /* UIStepper+RACSignalSupport.m */, 9AE77ABB8D49E71EE7F0CFAFEC9710A4 /* UISwitch+RACSignalSupport.h */, 83901A74DF9DB0A0733D4591727B3770 /* UISwitch+RACSignalSupport.m */, FE568D9819827B0D7AC039F020E84BB1 /* UITableViewCell+RACSignalSupport.h */, 9A1718181F5BEF030AA0CA446A6686F2 /* UITableViewCell+RACSignalSupport.m */, 744D46F0AC450056AAE46AA71A878857 /* UITableViewHeaderFooterView+RACSignalSupport.h */, CC5646C5FEED8AA9A5E6C5B7AA2A1AC0 /* UITableViewHeaderFooterView+RACSignalSupport.m */, 17092355C96C3E633D28031E208BC50C /* UITextField+RACSignalSupport.h */, 1428E07DEC90DAD4F228BB35096D20BF /* UITextField+RACSignalSupport.m */, 02580255476480314DC90D373EF87D94 /* UITextView+RACSignalSupport.h */, 312F34A0D29AA628F2B731F4DA50B1E7 /* UITextView+RACSignalSupport.m */, 30A2528A35C4C6F5862F161A9271628A /* Support Files */, ); name = ReactiveObjC; path = ReactiveObjC; sourceTree = ""; }; 41FF0639C883598194ABBE7B577870E0 /* Resources */ = { isa = PBXGroup; children = ( BCB7911698C2C8CF6B83F92DF10B11EF /* PrivacyInfo.xcprivacy */, ); name = Resources; sourceTree = ""; }; 462A9951C956D6A46FFC123F5A39DA68 /* Pods */ = { isa = PBXGroup; children = ( FCDC1342424DC4C037BAACACB3539981 /* CocoaAsyncSocket */, 07878FB8E434E88470AE99EF245DD87D /* DZNEmptyDataSet */, 5379629CF148872CA4F4B6B4646CC60F /* Masonry */, 0DCB8C6204AF09EB069414469054876F /* OCMock */, 2D1EE2372A7AC2DC12EE670EC92D3CCE /* Reachability */, 3FA81B81222DED05CCEF2A9F5A5CD223 /* ReactiveObjC */, 50998069B4CEEDDBBF4C876C8F965B45 /* SDWebImage */, F5561D14B2F6C241160261630C0336A4 /* Typhoon */, 188DAC4BBD3CE26D8F1253736B3F7CB5 /* YYModel */, ); name = Pods; sourceTree = ""; }; 4A295F24BC631F39960CA0D797B62846 /* Support Files */ = { isa = PBXGroup; children = ( D45725C80C47BF66261F5057EA6C9389 /* Masonry.modulemap */, 7BDD9A3C6FE6300A4953E8AC4C585381 /* Masonry-dummy.m */, 1FB6B3BEE43133D2BF7F12D5B6B8A89D /* Masonry-Info.plist */, 8F92503B695A9FE309F61E102877877B /* Masonry-prefix.pch */, 48A63D2EDED811435B879E1A51A2C5C3 /* Masonry-umbrella.h */, 918735CA6B8C36A5D220733BC8F45075 /* Masonry.debug.xcconfig */, 5973E1C538DF3DFE2D2DCF5EF361B714 /* Masonry.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/Masonry"; sourceTree = ""; }; 50998069B4CEEDDBBF4C876C8F965B45 /* SDWebImage */ = { isa = PBXGroup; children = ( 8F24DBFBEF57DA7F125488F43BE5ACA9 /* Core */, 39EDF0865A79D051B3793779D4DDF20F /* Support Files */, ); name = SDWebImage; path = SDWebImage; sourceTree = ""; }; 5379629CF148872CA4F4B6B4646CC60F /* Masonry */ = { isa = PBXGroup; children = ( 903891AB3F9D2ABE990A52BF6D7CE724 /* MASCompositeConstraint.h */, 59D821E26E6C5AFE07427222FB4AB639 /* MASCompositeConstraint.m */, 9630797851476E7C030A663FB01C053D /* MASConstraint.h */, 80E8E2470B296C3F1F4F779B79894FB3 /* MASConstraint.m */, E5AE3EB7707FD5429801C6CB2A8A382E /* MASConstraint+Private.h */, E3F2828AF28F25FC9663EDF2BD110090 /* MASConstraintMaker.h */, 838C51303B89542DD2D308D0C408F616 /* MASConstraintMaker.m */, 6296945F878C9C5DD60D6B36B3DF4E9C /* MASLayoutConstraint.h */, F867F81312FCE227A0925280E5C9263C /* MASLayoutConstraint.m */, A20BC1B3BD8F0D819039127EE5346176 /* Masonry.h */, 6622E7C87393460832DC95149834E273 /* MASUtilities.h */, E928F67B2793E36DB0271AE60B7C1747 /* MASViewAttribute.h */, 2ADA8F4AE98D80692FA44970F3846684 /* MASViewAttribute.m */, D35CCCA0DFE0CE1EBDA590032372C0AB /* MASViewConstraint.h */, 4349B8FF1BD06C628405857A06C179DD /* MASViewConstraint.m */, B5261A9D35AA9978FA8E8427895CDA41 /* NSArray+MASAdditions.h */, A4C87452D870EC6849B6A78C6E8FC492 /* NSArray+MASAdditions.m */, 041C0D0F13831619F5479D99056A302A /* NSArray+MASShorthandAdditions.h */, 253F5F73A60CE9213B91A4027838C766 /* NSLayoutConstraint+MASDebugAdditions.h */, 3DB7BACA057ADD843741CE2BDFDF62C8 /* NSLayoutConstraint+MASDebugAdditions.m */, 467F746C89C8EA1B73E4F4C1D741B528 /* View+MASAdditions.h */, CDF6A89092579B8DC5007CD2E67B5C48 /* View+MASAdditions.m */, 68E47146ED471AD73B71268A94D9F69D /* View+MASShorthandAdditions.h */, 0ED44D2F4F4F552521A65726C787244A /* ViewController+MASAdditions.h */, 92B2A700D6653C0C3364645A2E691CC0 /* ViewController+MASAdditions.m */, 4A295F24BC631F39960CA0D797B62846 /* Support Files */, ); name = Masonry; path = Masonry; sourceTree = ""; }; 6486BEC31F8F95E95248A4DEE0F28704 /* IntrospectionUtils */ = { isa = PBXGroup; children = ( ); name = IntrospectionUtils; sourceTree = ""; }; 8A43768854093C54A621A97195F27F64 /* Support Files */ = { isa = PBXGroup; children = ( A8F1809F162828E2A2E755E14EC48D54 /* DZNEmptyDataSet.modulemap */, D3B2AC38C369630473925094DAF536F5 /* DZNEmptyDataSet-dummy.m */, 8E22B83E5AD31A7FB5BFE354EF0D299C /* DZNEmptyDataSet-Info.plist */, 6A6D607BCA593FEDA65F576E1A1F0882 /* DZNEmptyDataSet-prefix.pch */, 3060A3BFE9758FA021148046B85E8CE5 /* DZNEmptyDataSet-umbrella.h */, 44A289ACE45B5D7FC30203A480801351 /* DZNEmptyDataSet.debug.xcconfig */, C122C87FD2423368DF66F19780CE1593 /* DZNEmptyDataSet.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/DZNEmptyDataSet"; sourceTree = ""; }; 8F24DBFBEF57DA7F125488F43BE5ACA9 /* Core */ = { isa = PBXGroup; children = ( 9E18DA0F8466E227AA17A93CF40D4C08 /* NSBezierPath+SDRoundedCorners.h */, D68AF173A5494072218D36ADB175CC67 /* NSBezierPath+SDRoundedCorners.m */, 2F69FEE7B67F930E4BAB806C41EDBDE5 /* NSButton+WebCache.h */, 0F07E5AA6AC318EED47D4865477C9372 /* NSButton+WebCache.m */, 610276A9F0EF8CDEC1E67F7E18C1A180 /* NSData+ImageContentType.h */, ECA70F67E1A45D6DB171A53CF98C8090 /* NSData+ImageContentType.m */, EE05E240495D85D96BA4CB6CB3310F63 /* NSImage+Compatibility.h */, F21C58BD3E6383E8D7CDE5A22DFE4432 /* NSImage+Compatibility.m */, 0EF30728B0B55C95AD32A7BC318AC70A /* SDAnimatedImage.h */, E5F9720FC7B55FFB33E76C326A7DDE5E /* SDAnimatedImage.m */, A7FF20A852BCFA4897183BC64A6E1D00 /* SDAnimatedImagePlayer.h */, F77D3BA16F4A5B6BBE117AA9B4BC653E /* SDAnimatedImagePlayer.m */, BE52EEDB31E3B6435F83E7E317F3047D /* SDAnimatedImageRep.h */, 0571A2C7BE7E0945F4212CD552F88BE1 /* SDAnimatedImageRep.m */, 0845491EC6BC3A5BFF1808F961A5440F /* SDAnimatedImageView.h */, 64094E39E3BA0988064CD05A86418E97 /* SDAnimatedImageView.m */, 4F1747F10DA9479C6BDD54B652E1C545 /* SDAnimatedImageView+WebCache.h */, 826AE09BE9A75B5E4F07DCA8BD6E568A /* SDAnimatedImageView+WebCache.m */, 19EA01E604C674E56BA259D0C3794651 /* SDAssociatedObject.h */, 252B49984FBEE0DBF2DD1314B8D9B974 /* SDAssociatedObject.m */, 50B9C0A664812C3E14A3224A605C9402 /* SDAsyncBlockOperation.h */, 977CC2B85F6C66B4997C1A77F749621E /* SDAsyncBlockOperation.m */, A66DC1E844041574BF00E1D8870DE753 /* SDCallbackQueue.h */, 746C9C6D96CC90B1558DBCDA1EF575B4 /* SDCallbackQueue.m */, 92A096C440891899FBCBB081ED6C8474 /* SDDeviceHelper.h */, 7D1BD5E2CF0B45A47FC6BC88583E5B26 /* SDDeviceHelper.m */, E0C543AB14CD3CB33002A31DFA8A3ED7 /* SDDiskCache.h */, A652A78922C1F9E12F183395B8233FDA /* SDDiskCache.m */, C40D9702A810253531E2904C61CE32F5 /* SDDisplayLink.h */, FB0CBD82662E0CBDB55A48F245646888 /* SDDisplayLink.m */, BAF592AE664D056D895A926E0F7D0261 /* SDFileAttributeHelper.h */, 7E12AEC65EEC09FD15C652679375036E /* SDFileAttributeHelper.m */, C49042227AF27E657FDC7BA3CA8A537A /* SDGraphicsImageRenderer.h */, 0C9B76DC6B005BE0C06B737507138A3E /* SDGraphicsImageRenderer.m */, BD5A68EEDD637AF3394D147538906789 /* SDImageAPNGCoder.h */, B67521AEB3057A6B1FDF7908EB18E894 /* SDImageAPNGCoder.m */, 6ED217D4C98D62892AD92759A98D2928 /* SDImageAssetManager.h */, 8233301E93D80EEC51CC7AFF84085AC7 /* SDImageAssetManager.m */, 31794A4FD83642DD83BC70B9F11CA23A /* SDImageAWebPCoder.h */, 15C365C1912A0FB488CECEF9950319ED /* SDImageAWebPCoder.m */, B913D23BDDEDDE62A0E2C3493A0E97F5 /* SDImageCache.h */, E597546B5AFBBDBBDE17E78E1E1EC111 /* SDImageCache.m */, 8AC5ECC34DB25CB5305086B539A17417 /* SDImageCacheConfig.h */, B2E5AA16D441D1E41E9953B3FD0CE552 /* SDImageCacheConfig.m */, E9BFDB08B8D6909114F2B3520B986044 /* SDImageCacheDefine.h */, 4290D47244C9D83C19300F3FE4CA8170 /* SDImageCacheDefine.m */, 5F41C49350454308C011F60E41C91F54 /* SDImageCachesManager.h */, 1343632E0E45C40E0DBA77566514A62A /* SDImageCachesManager.m */, 91A1F2245A7DAF44FC87AADAFA9E9BA9 /* SDImageCachesManagerOperation.h */, 273504B50C9E20A280FF38B369EECE87 /* SDImageCachesManagerOperation.m */, E1B9673A5D86AD933259E18FA172B615 /* SDImageCoder.h */, 82A6DCEB5EBCB25A5BF6375B4D2FE619 /* SDImageCoder.m */, F4CE8A12723ABBCC55A89EB52BEB7E84 /* SDImageCoderHelper.h */, 8CC52EA095AA94DD387D255F058AF87F /* SDImageCoderHelper.m */, 217EBAD9071176F8750E350BA9867152 /* SDImageCodersManager.h */, 85BADAC4F3DCD2CDEA7EE692773C47A3 /* SDImageCodersManager.m */, 3D4CB8BAE1E9433AA4D034F31AD60DE4 /* SDImageFrame.h */, 9CD76B64F3F84AD0EA28EDA5BE8A26BE /* SDImageFrame.m */, 45FA653E23538B63DB444A6ECA2E3190 /* SDImageFramePool.h */, C5EE4FBC26F92ABB4464D8BED481D33A /* SDImageFramePool.m */, 0AE0533E46155704FC6E99D900ECFF97 /* SDImageGIFCoder.h */, D69D5EFB2189DEA994DD47111C55B929 /* SDImageGIFCoder.m */, FDF2495A0E39E684767BAF85F109189B /* SDImageGraphics.h */, 7DF1AA147C3C7EA2E20CEC9444F9EDF2 /* SDImageGraphics.m */, 5DD596766B816867DE3F36CD6A523540 /* SDImageHEICCoder.h */, 588908A9A5F504D890248C42BDD5B35C /* SDImageHEICCoder.m */, C3E573EB2C906BB6045C58BC27C4CFB2 /* SDImageIOAnimatedCoder.h */, 15DBB333129764191B36CFF037F62681 /* SDImageIOAnimatedCoder.m */, C15E41DEC1751BDADA070E77A5D31D94 /* SDImageIOAnimatedCoderInternal.h */, 996F286581DA660FC5CB9DCBB431AF08 /* SDImageIOCoder.h */, 74A9B68BB1F2C7DC615156DBA4077781 /* SDImageIOCoder.m */, D029DA41DE158F6663F423C5BEC68F57 /* SDImageLoader.h */, 3E8A97304D179B0ADAF704C7CEBEECD1 /* SDImageLoader.m */, 0851ED90E0FAF2464FCE1AB36209BB60 /* SDImageLoadersManager.h */, 72AB4D998C2E02839ACA5C88AC0929A9 /* SDImageLoadersManager.m */, 83044A60173A4F57FC96702D95EF4DB8 /* SDImageTransformer.h */, 073BF9A27C7F924FC6A5CF2E935DDDA8 /* SDImageTransformer.m */, 000F69D9699B233904DC78740C74688D /* SDInternalMacros.h */, 952891A7D50B078702F1A3896E8BA349 /* SDInternalMacros.m */, 9D6B350707C3EC3D1DAE907D140B8969 /* SDMemoryCache.h */, B534D8B84FDE7415642A1D01E3733910 /* SDMemoryCache.m */, 54AA8A2F8E3322A66F3449823D3B76B5 /* SDmetamacros.h */, 6176156DAB1442AF8B5FF1B18364793C /* SDWeakProxy.h */, D07B2E954DF932CCE682BD41F588F5BA /* SDWeakProxy.m */, 81E027BECECC686145B8874E339146B3 /* SDWebImage.h */, F5AEF10212D323726736313347CAA30B /* SDWebImageCacheKeyFilter.h */, 146B7F55E58BD9E7C1CB0B3944357F9C /* SDWebImageCacheKeyFilter.m */, D4142DA4BEFA74E0A9968C239127FF24 /* SDWebImageCacheSerializer.h */, B96D3683B3E4B884909B07397CF43A5D /* SDWebImageCacheSerializer.m */, 24A117E496A0E15BE008AF6C24618D1F /* SDWebImageCompat.h */, 198733CC5474A54E6311C06F7EF78E54 /* SDWebImageCompat.m */, 702F65559FA8A65C2C9E8A7280D4E423 /* SDWebImageDefine.h */, F2C354A6FCF564B6B35C09D98D9B276E /* SDWebImageDefine.m */, 5D1458925F5D672BF44A88E619165414 /* SDWebImageDownloader.h */, 3B0A54B1F889B9B87CDFE1DB41F6ABCC /* SDWebImageDownloader.m */, 9B6EEC7E6EC11E55D1422113BAB387BF /* SDWebImageDownloaderConfig.h */, CDACF51466350F7A4B6A82E206A3F37A /* SDWebImageDownloaderConfig.m */, E8412B691818531EBFB6124AD9FDA943 /* SDWebImageDownloaderDecryptor.h */, 6390D1A8353BED0692FDB19AF76851C0 /* SDWebImageDownloaderDecryptor.m */, 79AE668D1D91F91A64F0E74D1BA417EF /* SDWebImageDownloaderOperation.h */, 84E53FAE993E1AC61F28D1CDC8FF68BC /* SDWebImageDownloaderOperation.m */, AB64B8DD79975FE71CC74B5FBD9785F0 /* SDWebImageDownloaderRequestModifier.h */, 4BE4EBEC7840EB2B41361CE32163EA16 /* SDWebImageDownloaderRequestModifier.m */, BAFC1AB1A830CD29F44C74C0B71EF450 /* SDWebImageDownloaderResponseModifier.h */, 40F0A65B2C96B6E67B7417305F598D6D /* SDWebImageDownloaderResponseModifier.m */, D6E9B831B9FB4D942099CD004CE0BAC3 /* SDWebImageError.h */, ABBE463A361CCED6D1E02450518F512E /* SDWebImageError.m */, B478DF0CC09DF6605F4772947370CBE1 /* SDWebImageIndicator.h */, 3D81FCE9D11F4054F824AE0AD9C905D9 /* SDWebImageIndicator.m */, 4B18C13FE0EC5A5F756BBA99DC8559F1 /* SDWebImageManager.h */, DF6104CF591EBBA4948511163C719CDB /* SDWebImageManager.m */, 1E5E520FF458D112BA579819CA973E7A /* SDWebImageOperation.h */, 0F47471F1705425CC58FB3536B619557 /* SDWebImageOperation.m */, 933EF5CE0D7C9A91603A2F91BDFD0871 /* SDWebImageOptionsProcessor.h */, 72C6CF5849F4A490EF85880EED97E773 /* SDWebImageOptionsProcessor.m */, 4D4A2340AC32529239D0841BD00726B1 /* SDWebImagePrefetcher.h */, E14AF6E304C6098B7A146E1BAFC26C6C /* SDWebImagePrefetcher.m */, A16A247B48BF9C8E1B54650152D9F47E /* SDWebImageTransition.h */, 7333039A34CFB7AC3CD117DC76BF5999 /* SDWebImageTransition.m */, 4BFF4254094763357CA3C84E0939A2C3 /* SDWebImageTransitionInternal.h */, EF2113E6C3216757EFC73B0ACD0ABD90 /* UIButton+WebCache.h */, F817EA9532FEA2C318FD55D4CFE8D722 /* UIButton+WebCache.m */, 56D32E1C48670C43C54173C7D2B28771 /* UIColor+SDHexString.h */, A67FE7A5F980382C477CC0E5671FB1B3 /* UIColor+SDHexString.m */, 909F75C790C08B1A24A71FF138EA5C7B /* UIImage+ExtendedCacheData.h */, D1FE5339C49B5BC54F80CD72EDBB13EA /* UIImage+ExtendedCacheData.m */, FA43EAB3E19854BD7C0DF6BA3C16B9AF /* UIImage+ForceDecode.h */, 1083669FB5354500A46F16186007AE49 /* UIImage+ForceDecode.m */, 5E23710C44440556A6042E7F7D8E172B /* UIImage+GIF.h */, 7A4EBA3FAF480391CADE1DF78A1EB2DA /* UIImage+GIF.m */, 2D3D8BCFF92388848C3FD597F1895318 /* UIImage+MemoryCacheCost.h */, 46B265A7E81C9ECE01048E340D45E9A3 /* UIImage+MemoryCacheCost.m */, DAF7C7F3C941B652A3015DFEA15DB259 /* UIImage+Metadata.h */, FF9570A386DE6A77332680BFBB13573F /* UIImage+Metadata.m */, CEB064A038D098A7FFF817FFA5B9F340 /* UIImage+MultiFormat.h */, 1D09E947A17A03E7F1BE82DA4451726B /* UIImage+MultiFormat.m */, 4A8368182067949B6C49AB727784993C /* UIImage+Transform.h */, 026AD1F39E6F56A1374F84466603518D /* UIImage+Transform.m */, 907EE3AB436219C47FE62434C1DCFB33 /* UIImageView+HighlightedWebCache.h */, 45AF458D3D9B4CF7A071A8AA27745113 /* UIImageView+HighlightedWebCache.m */, E1DC9A972C497BFFC7B688A24BA28E98 /* UIImageView+WebCache.h */, A09A92A8038E8620B3B2A61D8A929180 /* UIImageView+WebCache.m */, B0783913DE452E225655A280A31FEFA8 /* UIView+WebCache.h */, 50D1100A5FC66E513CDCEFED2FEC8138 /* UIView+WebCache.m */, 41908C1E64673E808673215A63FCEC45 /* UIView+WebCacheOperation.h */, A4F5A7AB017EF810CFD76454F977D8A0 /* UIView+WebCacheOperation.m */, 4C20ADFFD788112D9472742A2E3ACB43 /* UIView+WebCacheState.h */, 5517D5F4CC5D9F18F26A5ED4C9034C6D /* UIView+WebCacheState.m */, 41FF0639C883598194ABBE7B577870E0 /* Resources */, ); name = Core; sourceTree = ""; }; A42BEE77734080BB7A48812B478F1B93 /* Support Files */ = { isa = PBXGroup; children = ( C269BD3729C76AB77AB3EFFBD2F7192C /* Typhoon.modulemap */, 9BE05A31C373E76C2393361CF1E8AF9C /* Typhoon-dummy.m */, F9B6404AB89F0CB5797BFB129EE1B511 /* Typhoon-Info.plist */, A0B26D8004A9D82FA6F99C42A9F8F1BD /* Typhoon-prefix.pch */, 8C35F5A9D87E1BF9E90AD737FA10D987 /* Typhoon-umbrella.h */, BCE7EC027D6727131CBC3BDA0CA0FA09 /* Typhoon.debug.xcconfig */, 2FEB52E6488D9C7D2B20E87FA516436B /* Typhoon.release.xcconfig */, ); name = "Support Files"; path = "../Target Support Files/Typhoon"; sourceTree = ""; }; A6F49EB50338D615FB5F90B9EDE3415A /* iOS */ = { isa = PBXGroup; children = ( 5EFE23AB30614DC2B36B61715A6990FD /* CFNetwork.framework */, C6D6E31E7C7B4D803BA05555FE0FF607 /* CoreFoundation.framework */, D376F0D7BDE2FCC4DDC6743AFC8DDA9E /* Foundation.framework */, 4E215B27470E9F7E028D285C07B15B9B /* ImageIO.framework */, D93216F983293A53686F68A6B9A39E70 /* Security.framework */, C6506E5BA0D201F5A9633E8253219386 /* SystemConfiguration.framework */, C5460C93594FE49E0DF49CC9F802F1F3 /* UIKit.framework */, 08703C437757C9668B19065994EA7EBD /* XCTest.framework */, ); name = iOS; sourceTree = ""; }; B04E6303DEBB965117D11508CF337DF4 /* Pods-iOS-Network-Stack-DiveTests */ = { isa = PBXGroup; children = ( 6EB25E54B53C87991D7A9945AD10B211 /* Pods-iOS-Network-Stack-DiveTests.modulemap */, 80308619B6E726C5AFA5F9E661DA48A5 /* Pods-iOS-Network-Stack-DiveTests-acknowledgements.markdown */, A5650C4A8862741DAF521E285ED2C03A /* Pods-iOS-Network-Stack-DiveTests-acknowledgements.plist */, E526E2D4A9BDEA51AAF7B2A5AC6A46FA /* Pods-iOS-Network-Stack-DiveTests-dummy.m */, AE66E55A3439A3159AAD4EA49DF03076 /* Pods-iOS-Network-Stack-DiveTests-frameworks.sh */, 64C5B954F96F32D4D9081CA4C2236229 /* Pods-iOS-Network-Stack-DiveTests-Info.plist */, 1051F60A0834845519249BBEEB7669EF /* Pods-iOS-Network-Stack-DiveTests-umbrella.h */, 200AD54BC62CB8F66159C905EE9DED00 /* Pods-iOS-Network-Stack-DiveTests.debug.xcconfig */, 198BAFBAC2D38ECDB593D0EE2304B03C /* Pods-iOS-Network-Stack-DiveTests.release.xcconfig */, ); name = "Pods-iOS-Network-Stack-DiveTests"; path = "Target Support Files/Pods-iOS-Network-Stack-DiveTests"; sourceTree = ""; }; C8790AA3DB00592854D424AA33CB7359 /* DeallocNotifier */ = { isa = PBXGroup; children = ( ); name = DeallocNotifier; sourceTree = ""; }; CF1408CF629C7361332E53B88F7BD30C = { isa = PBXGroup; children = ( 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, F119B4A0ABE017AECEE69836596133B2 /* Frameworks */, 462A9951C956D6A46FFC123F5A39DA68 /* Pods */, 1FF60BCB01852F445494BA9733E861EB /* Products */, 149CFEB4FF5E4DB615D80FDAE578EB0C /* Targets Support Files */, ); sourceTree = ""; }; E087B57EFEEA1CCDFF450B3B12C6CC81 /* no-arc */ = { isa = PBXGroup; children = ( 3DFCA73A433D742E4E61D2DF66334925 /* NSInvocation+TCFInstanceBuilder.h */, FD27FFD79501082E67620A5F5585D2D0 /* NSInvocation+TCFInstanceBuilder.m */, ); name = "no-arc"; sourceTree = ""; }; F119B4A0ABE017AECEE69836596133B2 /* Frameworks */ = { isa = PBXGroup; children = ( A6F49EB50338D615FB5F90B9EDE3415A /* iOS */, ); name = Frameworks; sourceTree = ""; }; F5561D14B2F6C241160261630C0336A4 /* Typhoon */ = { isa = PBXGroup; children = ( 818F3C6FD63BEF6B17D992E201321502 /* Collections+CustomInjection.h */, 8CCB0069A6BAC8CD752488DF7B7D42B7 /* Collections+CustomInjection.m */, D524BC409D8FA54C1A06A2E06C1FC36A /* NSArray+TyphoonManualEnumeration.h */, 3CB4F2D27F00A3C400E970AB3C7AB6BC /* NSArray+TyphoonManualEnumeration.m */, 12EC94936DE4F7A5029D489A3E4FB32E /* NSDictionary+CustomInjection.h */, 4FB2623C122163DC9FB824E1A71D49FD /* NSDictionary+CustomInjection.m */, 1BDDCD9A639B9E7EBCAF02C54E1DE1E3 /* NSInvocation+TCFCustomImplementation.h */, 032501D9A06A7D0265ED238BBC7D25E3 /* NSInvocation+TCFCustomImplementation.m */, 4C07DF0C9AD7EAEC76E6F3D297E58F66 /* NSInvocation+TCFUnwrapValues.h */, C92A811BD719C9FA38BF1EB73409CA04 /* NSInvocation+TCFUnwrapValues.m */, F662CD4F60FC2C37B2BEB530D71C3725 /* NSInvocation+TCFWrapValues.h */, 69EDFB408AB8C8E85794472FD278D3F4 /* NSInvocation+TCFWrapValues.m */, 8EA5D8C4206359854EDBE37A94AF6B6A /* NSLayoutConstraint+TyphoonOutletTransfer.h */, 3DF90E5FD761A704C337FA27D910B4EC /* NSLayoutConstraint+TyphoonOutletTransfer.m */, CAE4756369959CF3EB303367CDDEBAFD /* NSMethodSignature+TCFUnwrapValues.h */, 1C982E20E08BC60740455993A35E8A3F /* NSMethodSignature+TCFUnwrapValues.m */, D655158D8BA8A371D68B4A11241E9616 /* NSNullTypeConverter.h */, BDDDFCB98FA96E2B25144D59DAD84EF4 /* NSNullTypeConverter.m */, D179E0BB3A9462316E480E37EBCDDDF5 /* NSObject+DeallocNotification.h */, 5C7C6F0CA41AC6FA5A0EAD49FADBE0DE /* NSObject+DeallocNotification.m */, 3B0FD681CCD09F3DE9E1B8663D3F1CC2 /* NSObject+FactoryHooks.h */, ADFAD562310FEB485867BE6D63C82804 /* NSObject+PropertyInjection.h */, 69139A572FFAF2E425701DCD352635E3 /* NSObject+PropertyInjection.m */, EAFD5F2E394EE2C4144D87785849CD68 /* NSObject+TyphoonIntrospectionUtils.h */, FA664986925B201863730F10AB76F9D5 /* NSObject+TyphoonIntrospectionUtils.m */, 10AF2F5387AC99FDF1EE38CC6AF81A5A /* NSValue+TCFUnwrapValues.h */, DE4C0740ECC01D6ED27D9ABD7EA5172B /* NSValue+TCFUnwrapValues.m */, AAB807AFFC7C3B79795D0CA6C280CC13 /* OCLogTemplate.h */, 203BC0FB7B1B96C82C4EAD4593D84A8D /* Typhoon.h */, 76C8CC4DEAB56D01254A5E21663A9CB3 /* Typhoon+Infrastructure.h */, FE4F16AD593A4CCB580F7CD5B86431EF /* TyphoonAbstractDetachableComponentFactoryPostProcessor.h */, B2A8FF4E1BDBA3D59428BAC8FAA55FFD /* TyphoonAbstractDetachableComponentFactoryPostProcessor.m */, 9C06ABD459BD9D2EC039A4278068AC2B /* TyphoonAbstractInjection.h */, 9EB5CC7B3930FE88F26ECB6C25761E21 /* TyphoonAbstractInjection.m */, E54E4A10B9B5F3F352208D4FC87A2ECF /* TyphoonAssembly.h */, 8D8C5527AA1D03083903623D593614BE /* TyphoonAssembly.m */, 5CC43EDBD34D1A25DB41669E7650524E /* TyphoonAssembly+TyphoonAssemblyFriend.h */, B0E7E234958AD2EE1C16F6F2EA4B24FB /* TyphoonAssemblyAccessor.h */, EFB2E394DB7A9A7EEE8003F7C8E6AD40 /* TyphoonAssemblyAccessor.m */, 37E635EECAF9EE9AFD5F838C93C42334 /* TyphoonAssemblyAdviser.h */, B8D365F9E6ABE29BE745099B2A29F0B1 /* TyphoonAssemblyAdviser.m */, 43C103B5C9A8BC55D3644883BC36B3A6 /* TyphoonAssemblyBuilder.h */, 1466DD8719F4531C498D10CD285A8856 /* TyphoonAssemblyBuilder.m */, AB7271D8A582657DF93DCFAD4F92FA7F /* TyphoonAssemblyBuilder+PlistProcessor.h */, C5F745BEA95E2A3A7A18764A8CE22A3F /* TyphoonAssemblyBuilder+PlistProcessor.m */, 8D3BAA9F0A9900B2F727C2916F30C12A /* TyphoonAssemblyDefinitionBuilder.h */, 4FA8209FE24832482CBDBD96568B60E3 /* TyphoonAssemblyDefinitionBuilder.m */, E59E27A8B1D09C1C222A67BFFD0845C6 /* TyphoonAssemblyPropertyInjectionPostProcessor.h */, E4338428A593399BDE88E88C9CEF385C /* TyphoonAssemblyPropertyInjectionPostProcessor.m */, 772D4121E01F86089D23DBEBD83FC5AD /* TyphoonAssemblySelectorAdviser.h */, 5E2C6F46F631D89037B7C021DF8B3373 /* TyphoonAssemblySelectorAdviser.m */, D16AFD377C51E109866560DEE9CBC39E /* TyphoonAutoInjection.h */, 75D795D038F45CC15803AC36BBFEBF32 /* TyphoonBlockComponentFactory.h */, 533ECD8A38FC36466554EF43B205AD18 /* TyphoonBlockComponentFactory.m */, 712ED30137ED21790E81D661BCDBE41A /* TyphoonBlockDefinition.h */, D5A65F1C73D00CF1864BB3078888AECD /* TyphoonBlockDefinition.m */, 955DB0048C92F2F02195AF9A7C000F04 /* TyphoonBlockDefinition+InstanceBuilder.h */, E9CD0BC00D291194A3441C33CBA01647 /* TyphoonBlockDefinition+InstanceBuilder.m */, 1498864CCF5B387AE86A3D8C74065926 /* TyphoonBlockDefinition+Internal.h */, 7C9741B2966A4017CFF5F1396954F520 /* TyphoonBlockDefinitionController.h */, E842E755FB8A20406553415C9EACC8A1 /* TyphoonBlockDefinitionController.m */, 3F05B9328BABBA34AFCE3172B61D1F83 /* TyphoonBundledImageTypeConverter.h */, B7B1F42980891C0D9253DA6E3EEDE467 /* TyphoonBundledImageTypeConverter.m */, 94138F5AF19A8078AD96AA7CBA29AB4A /* TyphoonBundleResource.h */, 15B1114AA6F4793852697F0F588E7204 /* TyphoonBundleResource.m */, 8F0A29B88BD1DA370225345A593E408F /* TyphoonCallStack.h */, 3BC61D1DBB79E431F7B1D23F12DBA78A /* TyphoonCallStack.m */, 0CB61254C5C3C9FFDE2C23A595DB093C /* TyphoonCircularDependencyTerminator.h */, 04CD8685467C1425C560A1E9C0AA43B7 /* TyphoonCircularDependencyTerminator.m */, F655091C80A455BB5F06459C5CC0EC71 /* TyphoonCollaboratingAssembliesCollector.h */, 3D69D7A45A56F3986C52DF0088F96C35 /* TyphoonCollaboratingAssembliesCollector.m */, 572092422C1553EC0456C0BF53CDE8C2 /* TyphoonCollaboratingAssemblyPropertyEnumerator.h */, 62B6BED9AA97ED0F595CFB0D93C898E2 /* TyphoonCollaboratingAssemblyPropertyEnumerator.m */, 6027F1EC9BC5F3AB67DFFEC9E6DD6BEB /* TyphoonCollaboratingAssemblyProxy.h */, 8866DCC2E6B6A7335F7564DFAAA6323F /* TyphoonCollaboratingAssemblyProxy.m */, 42BB4F99181287983D693A457D56DE96 /* TyphoonColorConversionUtils.h */, 065E52ED2D30B3B331A00D8EB564E4D8 /* TyphoonColorConversionUtils.m */, 2EE6EC22E733D815C1F5084910F7491B /* TyphoonComponentFactory.h */, 8725F3C4CF3EA8CA846B7A932532168D /* TyphoonComponentFactory.m */, FAEAEC0032244300776CF7AFB0401C03 /* TyphoonComponentFactory+InstanceBuilder.h */, 05C311CA83E10EC09FC2A8CAAF090A30 /* TyphoonComponentFactory+InstanceBuilder.m */, 83F375B929B1442DA30D02F6A861124C /* TyphoonComponentFactory+Storyboard.h */, 4065878CE75B090253FC030941E3DC2D /* TyphoonComponentFactory+Storyboard.m */, 3F86F06F204B5BACDAEBC5742F93220C /* TyphoonComponentFactory+TyphoonDefinitionRegisterer.h */, 65677A776ECF0D5458AAFDDA49ADDFC4 /* TyphoonComponentsPool.h */, E1372334A95BBBD06EC3A3CDDE782AC3 /* TyphoonConfigPostProcessor.h */, 9A3B0D97818AFB9EC630C3B522130631 /* TyphoonConfigPostProcessor.m */, 8B16EB1B90C8121FC99C6D634CE916AA /* TyphoonConfigPostProcessor+Internal.h */, 30935A228E64CE3387EE71949306F225 /* TyphoonConfiguration.h */, 4834BC1E84F882D9846062878F9F66C9 /* TyphoonDefinition.h */, 35F7DE05C6E6BA99060F8F264A3CEBEA /* TyphoonDefinition.m */, 48F2519B307B02B43FC5B68399AF554F /* TyphoonDefinition+Config.h */, 40D4E59B9A952E2A8649507C8627874B /* TyphoonDefinition+Config.m */, 23D6C0C2DA8003ECC644C3366B6E5247 /* TyphoonDefinition+Infrastructure.h */, C4FBBF6C2C66E9E078C222133056807B /* TyphoonDefinition+InstanceBuilder.h */, 429AF24E6181CE01E2CB7642A29FBB6C /* TyphoonDefinition+InstanceBuilder.m */, 424E4FDDC999763B5ECDE171AF2948A3 /* TyphoonDefinition+Internal.h */, A146863CEB210DB61D7A021374982754 /* TyphoonDefinition+Option.h */, 7B345A1E24575BA9D0BC33710FE943C0 /* TyphoonDefinition+Option.m */, 30BC8EBE209C30443BACDAAA1B7A4CC1 /* TyphoonDefinition+Storyboard.h */, 3E5BC7E6CAFF72A150840D4DC92D331D /* TyphoonDefinition+Storyboard.m */, 1EE6F568285A1219683FB22F70790F2A /* TyphoonDefinitionNamespace.h */, A5A8D22D51747CC57261A39513F63FBF /* TyphoonDefinitionNamespace.m */, C529B758067B01A288574A93D2868C42 /* TyphoonDefinitionPostProcessor.h */, F9523376659FAD01D3AAF8C11D821E14 /* TyphoonDefinitionRegisterer.h */, 1AC6505E33748BB6A720F5B5C8C96382 /* TyphoonDefinitionRegisterer.m */, F06397AB33F7FA583810C154BB7C6BB2 /* TyphoonFactoryAutoInjectionPostProcessor.h */, F0E38DEA6A48AB6D0B5359119070CCD3 /* TyphoonFactoryAutoInjectionPostProcessor.m */, 28B9BD69A27186895E63DB6A19650F73 /* TyphoonFactoryDefinition.h */, F813C3F784E8340DED279F0AC918CD92 /* TyphoonFactoryDefinition.m */, 458457A3A3CABCBDF2136A3F5E6E82BF /* TyphoonFactoryPropertyInjectionPostProcessor.h */, 90F23D124204BAE6E0746E042588193F /* TyphoonFactoryPropertyInjectionPostProcessor.m */, 875B63A814EBB6CC52061388F53BC97D /* TyphoonGlobalConfigCollector.h */, CF7C5602AF31F940B921D40619F7D228 /* TyphoonGlobalConfigCollector.m */, AA109A4B29ED94AC8CB0BB3D4626927E /* TyphoonInject.h */, 7A672A6EEB28F3A0ADB373CDCB3697AE /* TyphoonInject.m */, 35D424AE41D9D5DF8B4CD98EE40E20E2 /* TyphoonInjectedObject.h */, F42860013A956ABF313EE9C9D084F191 /* TyphoonInjectedObject.m */, DAF562B23684D41ED34CC07CD14C7875 /* TyphoonInjection.h */, 661239F2EA786F002F7624EEE9A6FB7D /* TyphoonInjectionByCollection.h */, 3DA26B2B02963DB127DA3542FECF3472 /* TyphoonInjectionByCollection.m */, 0FFBFA01D44CE27E222D10DF44B1FBBB /* TyphoonInjectionByComponentFactory.h */, 050CAE5EF6AFDE7FE32CEE7E030CDD0E /* TyphoonInjectionByComponentFactory.m */, 765309DEC91CA062A4172B75C508325C /* TyphoonInjectionByConfig.h */, 71CC76D142ADC81938ACEAA26EA01492 /* TyphoonInjectionByConfig.m */, C0DFA61DA615C88D4F562DF9D047B134 /* TyphoonInjectionByCurrentRuntimeArguments.h */, 6A273691556F81E2F190244D0347ED31 /* TyphoonInjectionByCurrentRuntimeArguments.m */, 257F4F157F94CB40D1F5643758121C8F /* TyphoonInjectionByDictionary.h */, 70B4C8AEAC9F4A07E37287417595490E /* TyphoonInjectionByDictionary.m */, 891C96D521BE0268A6C2A44DF6CF804B /* TyphoonInjectionByFactoryReference.h */, DA6A12A58C1C3043FB0D92D39EF14EB7 /* TyphoonInjectionByFactoryReference.m */, AD91A54AEE593AA59D16627F48452F75 /* TyphoonInjectionByObjectFromString.h */, 419694111A274B662810AE1FECA72F35 /* TyphoonInjectionByObjectFromString.m */, 83FC227B502F54235D6CCDB02449F168 /* TyphoonInjectionByObjectInstance.h */, 67345DA89FFA1B9FC73EA47478CB886D /* TyphoonInjectionByObjectInstance.m */, 218826C133BB15ACA97ED4D92CBF2243 /* TyphoonInjectionByReference.h */, DC81E927C43FEC142A408A3850366DA7 /* TyphoonInjectionByReference.m */, 6C1ADE2AAE0ED64E1AC780F3A8C2B911 /* TyphoonInjectionByRuntimeArgument.h */, 7E607FCD32089EA9D313F72664F0B17B /* TyphoonInjectionByRuntimeArgument.m */, 2C379332A0D51473ADCF63C80BF3B4C8 /* TyphoonInjectionByType.h */, 3CD424DC6ECC7B41C2BAB51078B5FB8E /* TyphoonInjectionByType.m */, 9BFCB32C228CF4CC491157D1716E631D /* TyphoonInjectionContext.h */, 227C536B9341DFDDAB8A93622D7BAB18 /* TyphoonInjectionContext.m */, 9301B5A21675C37DED58F6472417FFF1 /* TyphoonInjectionDefinition.h */, 900C7737DD6EC812009057A2F330E295 /* TyphoonInjectionDefinition.m */, 0F3AD4FDCF5DACA1727D2EDD1CDAA03F /* TyphoonInjections.h */, 98D4BFAFED921E3A451A3FCC58BD5A0A /* TyphoonInjections.m */, 7A37BECA26F996876B784EFD357DC96A /* TyphoonInjectionsEnumeration.h */, 1061DB1631D08DF649DC362D1104033F /* TyphoonInstancePostProcessor.h */, 0C08AB40F42970663D76869A9E5E9143 /* TyphoonIntrospectionUtils.h */, 3D31CBCD57F3773D4DC8B94E19806A4E /* TyphoonIntrospectionUtils.m */, BD5D5509B774B3EA20B4EBDBD246DD8C /* TyphooniOS.h */, 821F3BAC1C291831ECD436462BB57063 /* TyphoonJsonStyleConfiguration.h */, 12676821542EF4B94B6913AF583A7A69 /* TyphoonJsonStyleConfiguration.m */, 4B7043029F68EFD2C4174A964B0551C6 /* TyphoonLinkerCategoryBugFix.h */, 3D455866AC1ECBAFA4D139C7DD0E3168 /* TyphoonLoadedView.h */, 1F38003E7B8D270C5FC263FAC5B717D5 /* TyphoonLoadedView.m */, 9858D4D21F87468E116E7410167A453A /* TyphoonMemoryManagementUtils.h */, 4D14066F28EE5838EBA4D6142544116D /* TyphoonMemoryManagementUtils.m */, 08B6FA0A27CA67CF82F94F768E83FBBD /* TyphoonMethod.h */, 5D46806A2CE11059C3A8BD7E50783943 /* TyphoonMethod.m */, 6E9BA0379B500368359C8D6E3C90BE50 /* TyphoonMethod+InstanceBuilder.h */, 34517F02303D979079D891F86B87BD5D /* TyphoonMethod+InstanceBuilder.m */, 0D9725591DAD7A11FB7BCF2AA89B402B /* TyphoonMethodSwizzler.h */, F180A82892F507476C7119020F8B59B0 /* TyphoonNibLoader.h */, B49E72E762657E3284FA9D7A026A49E6 /* TyphoonNibLoader.m */, 13D762E3256505E91BAEA28CEAA5F68F /* TyphoonNSNumberTypeConverter.h */, 4FEDDBAED301C3BCF65687E2F5B90003 /* TyphoonNSNumberTypeConverter.m */, 98854B7F25ABB6BE45BB69C58166AA5E /* TyphoonNSURLTypeConverter.h */, FFD16E6D61E5F1AA7ACD347FD6E0120D /* TyphoonNSURLTypeConverter.m */, 661A8C92540E00912555E0561EB50777 /* TyphoonObjectWithCustomInjection.h */, CF5D6355EBCEC215D5F4264D1241E1B1 /* TyphoonOptionMatcher.h */, 3B33DB28F54E682CA83C889709720FB7 /* TyphoonOptionMatcher.m */, 468798856EED0D94D6E982B4D271195A /* TyphoonOptionMatcher+Internal.h */, 08856CD53ABCA9A62737A9BB60FD3496 /* TyphoonOrdered.h */, 13BDADC3DD9D34511EDA118822EA790E /* TyphoonParameterInjection.h */, C9703620E8931C6F0907DD19F302A7A9 /* TyphoonParentReferenceHydratingPostProcessor.h */, 18FB517C4077E6929DB734C793782E40 /* TyphoonParentReferenceHydratingPostProcessor.m */, AF03A2059F417E8AEC2BCF0FFB758649 /* TyphoonPassThroughTypeConverter.h */, 75013D4007A6A538ACAE4686AAC93DE4 /* TyphoonPassThroughTypeConverter.m */, 2CD41B1FA71FB0AE97EB624502ACF0A1 /* TyphoonPatcher.h */, 5F8278A4894BA9FD972FEC5FCB716F39 /* TyphoonPatcher.m */, F0055EAC2B8C580C32290969DFF19220 /* TyphoonPathResource.h */, CDA3D0B455A13ACC94AFADCBB2E311FE /* TyphoonPathResource.m */, 4E5A663C1E60087F575DDB601D6017F3 /* TyphoonPlistStyleConfiguration.h */, 87B55C9AC3241F2442119F64473D1F41 /* TyphoonPlistStyleConfiguration.m */, 2BE98D49469466E2F555F3E1E65A4F4C /* TyphoonPreattachedComponentsRegisterer.h */, 7F2869DEA989F62C012853D92680ECEC /* TyphoonPreattachedComponentsRegisterer.m */, 9B69333B75E26F15AA2E42D06EF62B15 /* TyphoonPrimitiveTypeConverter.h */, 620115CAB08E9FF9898971DB60F30BA6 /* TyphoonPrimitiveTypeConverter.m */, F3F7B76572B953FCBB908A0E7605EB53 /* TyphoonPropertyInjection.h */, CA332156FBF114376E2FE5077E71C03E /* TyphoonPropertyStyleConfiguration.h */, 720CFEBFB23833C0091EFD8767731EA7 /* TyphoonPropertyStyleConfiguration.m */, FD2C416CACE6B5586F404FC1E9F617FA /* TyphoonReferenceDefinition.h */, C2D79FFE2E4790A5C03593BBCA678B0A /* TyphoonReferenceDefinition.m */, ECBE53D68EFF65EB7F6020F1BE9C5B3B /* TyphoonResource.h */, 88024AA70385D01B3432E99546EFA4E1 /* TyphoonRuntimeArguments.h */, 3FE3393A74BB6D586E4B892A33322286 /* TyphoonRuntimeArguments.m */, 17B387B9415B8C98C3579629FB677E97 /* TyphoonSelector.h */, 95E34BD0B0987191F26A0B35F90CF6E5 /* TyphoonSelector.m */, 4D40DD7E6EAE2E8222553A0E28A69834 /* TyphoonStackElement.h */, 1B68D1A525FF5B8E36976449BC5C2434 /* TyphoonStackElement.m */, A13902D081C57317FEA1F09843D569F4 /* TyphoonStartup.h */, DB337B4965F7DD6CACD10EBA48209055 /* TyphoonStartup.m */, 384D578EC3A290F1ADF6C1728A6C1562 /* TyphoonStoryboard.h */, A7EF786049567EB4166E1062885CCF21 /* TyphoonStoryboard.m */, 8D91285C522F06C05B537D395E259F59 /* TyphoonStoryboardDefinition.h */, 1E2EED42BB0AD4DB8FDE62F4D4047113 /* TyphoonStoryboardDefinition.m */, A499F0CFC3E7B3B36E9786BCB6052F37 /* TyphoonStoryboardDefinitionContext.h */, 2D5383F6C03BB8925217003BD9A3A82C /* TyphoonStoryboardDefinitionContext.m */, AC040A20E5DFBE7A6827B92595B02C58 /* TyphoonStoryboardProvider.h */, A96DEC36622CF8577C0E003138EFD7EE /* TyphoonStoryboardProvider.m */, BC0D43AD044D143B02D4E1D5EA86BE86 /* TyphoonStoryboardResolver.h */, 004DB416A9430FE442A136525FC3D93D /* TyphoonStoryboardResolver.m */, CE6F1B69B9CDD8F4C2903BEDC2660DC3 /* TyphoonSwizzlerDefaultImpl.h */, 95DC39645A666C2B0AF83523FAD30758 /* TyphoonSwizzlerDefaultImpl.m */, EA1F7903B377E9FFA5C2028618BA7439 /* TyphoonTestUtils.h */, E586B047153D54E5818AFAA7B25CC55A /* TyphoonTestUtils.m */, 06B502FEF6AC1E2D1DECEF63D08CC04F /* TyphoonTypeConversionUtils.h */, 3EF8BF2E5B7814DF672FB319D2CB899A /* TyphoonTypeConversionUtils.m */, 2E59442B228F5A36B8D5592A675C8879 /* TyphoonTypeConverter.h */, D3A21103401B7FEA00216399FCE680EE /* TyphoonTypeConverterRegistry.h */, 906070901EFA9ECD175C85348FF57C1E /* TyphoonTypeConverterRegistry.m */, E87C4CECC849A38798AC17F0D2EDBDC7 /* TyphoonTypeDescriptor.h */, C7F186EA53F19BDDCBCD2E2E2279D348 /* TyphoonTypeDescriptor.m */, 32939C9AD7B94007A62205D5A5CA486C /* TyphoonUIColorTypeConverter.h */, D421E7A95F7BBA522BE9AE982E7390CF /* TyphoonUIColorTypeConverter.m */, 94E380F27B3CBBA506348C94A1707E2E /* TyphoonUtils.h */, 71C1C00FFE25AA899E9B77567B46E8CA /* TyphoonViewControllerFactory.h */, 330097FBD5A9529B89BA11A61293AD4C /* TyphoonViewControllerFactory.m */, 7DB7EDBC439037E423096117CAA5C2D6 /* TyphoonViewControllerInjector.h */, BDD4238ED758F069DED59626A8859F28 /* TyphoonViewControllerInjector.m */, 8732F0D1FA8AA62E703DE7973280BA55 /* TyphoonViewControllerNibResolver.h */, 645F110D573A95F0F5F61F5666D9AD90 /* TyphoonViewControllerNibResolver.m */, 02DBA7718F60FA5F5208D3C92DD2FAE1 /* TyphoonViewHelpers.h */, 5A2D575E1ACB5A76D8F5214B6B4B171C /* TyphoonViewHelpers.m */, 22EF63276DBF66ABD2D5FBF76746A708 /* TyphoonWeakComponentsPool.h */, B66C84A540CE194D4B1F225949E9EA7F /* TyphoonWeakComponentsPool.m */, D41D80BBE2F576947F11C3B0CDCDB170 /* UIResponder+TyphoonOutletTransfer.h */, BE3BDF1044166B0EEE411717FD217E42 /* UIResponder+TyphoonOutletTransfer.m */, 8D713D070421A1E803471AA59CAE1B25 /* UIView+TyphoonDefinitionKey.h */, 1592F539A623B93799CF4145741CFC4E /* UIView+TyphoonDefinitionKey.m */, 7CF6453EBAFEAB28E5DCF1E0FEA52A59 /* UIView+TyphoonOutletTransfer.h */, 8065BF1B5F4AFDB278D6AB4C732153BE /* UIView+TyphoonOutletTransfer.m */, 5AD0D1CF37F873A7C98560DA90D7922B /* UIViewController+TyphoonStoryboardIntegration.h */, 5F6718C9B74C90CF46E2131FD2FB6517 /* UIViewController+TyphoonStoryboardIntegration.m */, C8790AA3DB00592854D424AA33CB7359 /* DeallocNotifier */, 6486BEC31F8F95E95248A4DEE0F28704 /* IntrospectionUtils */, E087B57EFEEA1CCDFF450B3B12C6CC81 /* no-arc */, A42BEE77734080BB7A48812B478F1B93 /* Support Files */, ); name = Typhoon; path = Typhoon; sourceTree = ""; }; FCDC1342424DC4C037BAACACB3539981 /* CocoaAsyncSocket */ = { isa = PBXGroup; children = ( 2735EF71996F30815132D35A2A30E845 /* GCDAsyncSocket.h */, 88A89A507382B833489D6DB8D75E6DE2 /* GCDAsyncSocket.m */, 10396C86F2BA403403FECC4E3914CF31 /* GCDAsyncUdpSocket.h */, 7C4D52064EBE28BCC66250183EE0A11E /* GCDAsyncUdpSocket.m */, 0C4BAF90A7DB1F47264F6D82B599DF24 /* Support Files */, ); name = CocoaAsyncSocket; path = CocoaAsyncSocket; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ 09EE4349480B078AFE00E65B49E502BF /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( A6FCF74348D1907891FBDE3F2E331136 /* DZNEmptyDataSet-umbrella.h in Headers */, 2EFF1EFA2FF0D2F1AE44A9DDBB932425 /* UIScrollView+EmptyDataSet.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 0EF2526B5CC15C0E915F32190565F64C /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( BA904ABA8ED36CC4E5EB2B2004CA1F18 /* MASCompositeConstraint.h in Headers */, 37B890ABDC7DD441E6AA662325D412E6 /* MASConstraint.h in Headers */, 7C5505A2D3F2A697A5F324787061F4B7 /* MASConstraint+Private.h in Headers */, 813BE4C96A6D39C13EC50C6CD164F0AF /* MASConstraintMaker.h in Headers */, B680C2604BD8BC9644AE7C67BC46B9BB /* MASLayoutConstraint.h in Headers */, EC9B34262AED632D7EFB49804337648E /* Masonry.h in Headers */, B59E60FBC9665FC1061B88B8E6FD9FAF /* Masonry-umbrella.h in Headers */, C2068AEACC2D9C7F1FFE41AA25B12A68 /* MASUtilities.h in Headers */, 05E2B7C1DB7528A0BBEA1521BE0DBAF1 /* MASViewAttribute.h in Headers */, 5F45735DF355530CC955066D3C007E19 /* MASViewConstraint.h in Headers */, BF22D137EF6324675FA50080C5D93C00 /* NSArray+MASAdditions.h in Headers */, 61507E402F1F7C58BF119995A0479A22 /* NSArray+MASShorthandAdditions.h in Headers */, DBA9500CBBA5FF6FCBBA115AE4D12152 /* NSLayoutConstraint+MASDebugAdditions.h in Headers */, AE7B02645B8F769CA5F215EE8F7CC5B0 /* View+MASAdditions.h in Headers */, 772CF8E9CD02ECA4275B6173E2110E80 /* View+MASShorthandAdditions.h in Headers */, 8C6C7E25C5A24C936F81823978190E96 /* ViewController+MASAdditions.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 1632B972B125CE76633192F16FA2909B /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 10B1F7A3301C951B9F48B9B8F0CB1B31 /* Pods-iOS-Network-Stack-DiveTests-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 2317BC2378351692DC408D7D620ECDFD /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 0FFA63B0103710B3ACDAE72B33A73200 /* NSInvocation+OCMAdditions.h in Headers */, 5CFCD2D66F81B7F1AE4D8E82A51AFDCF /* NSMethodSignature+OCMAdditions.h in Headers */, 392AC16CCBF2A50096F0B3303E13ADA7 /* NSNotificationCenter+OCMAdditions.h in Headers */, B47B9BB155E21E5FB4D01940758EB91D /* NSObject+OCMAdditions.h in Headers */, F16BF2164AA7A01C3C46E80BBEBB14AE /* NSValue+OCMAdditions.h in Headers */, 0D45C478B6225E26D568D6CA97DA18FA /* OCClassMockObject.h in Headers */, 03815ACD5E64B92672AE8AA3A5A2EE27 /* OCMArg.h in Headers */, F5D147CA9C21670C9C509D0C40C55015 /* OCMArgAction.h in Headers */, E87077FBDFB98FEFBCD2EE88AC4B62A4 /* OCMBlockArgCaller.h in Headers */, B49EB728B20A04676F4A4569A18C7515 /* OCMBlockCaller.h in Headers */, D0959AA30897EC5180B57C59FFA10849 /* OCMBoxedReturnValueProvider.h in Headers */, A543C4BC7F666F22D3912798D07C04D5 /* OCMConstraint.h in Headers */, 436764D67026FF2C796BC826CEC803DD /* OCMExceptionReturnValueProvider.h in Headers */, B5B4DEF5ED5485C7F0212CF50C06F191 /* OCMExpectationRecorder.h in Headers */, 34519026CD19276CF7FA8A6921FBD3A7 /* OCMFunctions.h in Headers */, 8174C42751AFCD44787AF27A54C9CAB8 /* OCMFunctionsPrivate.h in Headers */, 3E983180E84D6AC282EF25CDC8368D41 /* OCMIndirectReturnValueProvider.h in Headers */, 31C05D64D42CA917CEF5585183DD47B3 /* OCMInvocationExpectation.h in Headers */, 228CDD92F991FA023D6F06B69327A028 /* OCMInvocationMatcher.h in Headers */, E58F464BF2649F7F2E4ED53CBFC5C466 /* OCMInvocationStub.h in Headers */, 2B7DFF21453E38A19B45F8E11EF278E3 /* OCMLocation.h in Headers */, 2AB2A962A952CBC6F38DE2938E055B0E /* OCMMacroState.h in Headers */, 0C994CE9E9CC8E7B31B2C862DB66E501 /* OCMNonRetainingObjectReturnValueProvider.h in Headers */, 0D535DEFAB38F82F523FA2970DDE9A88 /* OCMNotificationPoster.h in Headers */, 3D98D6C75AE8D46FEF6F5414F50815B2 /* OCMObjectReturnValueProvider.h in Headers */, D7C50F17FD247E49530D1EF5D55EF4E5 /* OCMObserverRecorder.h in Headers */, 3CB27E5F83DADA61D461099B00ED2895 /* OCMock.h in Headers */, 61B4CC930FD22979260025929C94D3F5 /* OCMock-umbrella.h in Headers */, E8F1A153D3063B75194913D03CA2D383 /* OCMockMacros.h in Headers */, 45D4E85C7031F33EA83C956C73136DBF /* OCMockObject.h in Headers */, 700FCD9C14FA276BBED0A2A34D274808 /* OCMPassByRefSetter.h in Headers */, 67607C80710BD7E0B43D74104EE602AE /* OCMQuantifier.h in Headers */, 037B74553C43333B8693ACA61DB05C78 /* OCMRealObjectForwarder.h in Headers */, 0E6C53D91EE72B414A29D19423842652 /* OCMRecorder.h in Headers */, A11E90132B32ABEA76703BAC68A86357 /* OCMStubRecorder.h in Headers */, 34C31624E93FB93E37E4C25D7E252A6A /* OCMVerifier.h in Headers */, 0EE4D51D6E6CB670272FED77C64D87EE /* OCObserverMockObject.h in Headers */, A8B06CAB8D3C0F81058B689563F2F6D2 /* OCPartialMockObject.h in Headers */, 5137D94F9CA1FAE507602788BC4988A2 /* OCProtocolMockObject.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 33783D69751B087D045FCF1FCA02E724 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 9B9343E8599EE5196BA75E842DCB48B7 /* NSBezierPath+SDRoundedCorners.h in Headers */, A839428F403C52D8AA3466B65E20C27A /* NSButton+WebCache.h in Headers */, 8AF38EDB1E9BF0D334AEB23C488870B8 /* NSData+ImageContentType.h in Headers */, 34B28D4F0168194B6EFAC0520EB7A7F4 /* NSImage+Compatibility.h in Headers */, 9B3420DEB8A0CCB9E1241A669AEFCA8E /* SDAnimatedImage.h in Headers */, BDBE494BAC544843982C3CA96A6C41DD /* SDAnimatedImagePlayer.h in Headers */, DEA09692CF813A23899CD4949A9B6801 /* SDAnimatedImageRep.h in Headers */, C1DD8C6A64F948E4C53560C76B995DA4 /* SDAnimatedImageView.h in Headers */, D7B3E8948DB04BD8FB6748419DA03EA9 /* SDAnimatedImageView+WebCache.h in Headers */, 5C8279C226EB028B044C5A0F4AC5A91A /* SDAssociatedObject.h in Headers */, 20D618EF3EA5E3BE96DA24D36E3CA9EF /* SDAsyncBlockOperation.h in Headers */, ED8F64FF98CFAE0B12CF60A1B0E6BAF8 /* SDCallbackQueue.h in Headers */, 042D40751BD2F51FBE9FECD4707CBBE9 /* SDDeviceHelper.h in Headers */, 928371B066E1211CE87089668D5BCB4C /* SDDiskCache.h in Headers */, 7074EA7FCC90B4967A437F5C43496828 /* SDDisplayLink.h in Headers */, 0453019EC6578A67B82CF569EC765546 /* SDFileAttributeHelper.h in Headers */, 5E10328A83E05D0015D7459FAAEF121D /* SDGraphicsImageRenderer.h in Headers */, 165F1C9CBD621828C788A3018D0426C5 /* SDImageAPNGCoder.h in Headers */, 4B2C2AE16AE3DDA7417AFCF7952588F1 /* SDImageAssetManager.h in Headers */, B4F231C5CBAB3D4A184699D0066E0E83 /* SDImageAWebPCoder.h in Headers */, D662C83ECE8BEDA5FFB52F3575CA3E1A /* SDImageCache.h in Headers */, 14CA284AC4FF1EED75E785641EE98034 /* SDImageCacheConfig.h in Headers */, ABCB80C4813C849FC93D57676820C907 /* SDImageCacheDefine.h in Headers */, F53BE4449AE5896F76325E4DCB6D0B13 /* SDImageCachesManager.h in Headers */, 1C8B70C74291A3076746C3B18781568E /* SDImageCachesManagerOperation.h in Headers */, 09A2ACBC8CE1761652EAA20886AEFE10 /* SDImageCoder.h in Headers */, F68889CD481716EE5D6B75EBD8FD53A6 /* SDImageCoderHelper.h in Headers */, B741DBE2A466E6211F879EF997D9322D /* SDImageCodersManager.h in Headers */, 6B5C3592B5E911E833D067D0BC785B1A /* SDImageFrame.h in Headers */, 44CD842019B1CEA681F820F37A30B7C4 /* SDImageFramePool.h in Headers */, 1B6CE67196EE181E6B56788EFC7E00D3 /* SDImageGIFCoder.h in Headers */, B331CE2D3DEB461E738B886086A365F9 /* SDImageGraphics.h in Headers */, E76969F9B01139118427505B18F9CD21 /* SDImageHEICCoder.h in Headers */, 089F3C4BAA46A37EC5763DD312771021 /* SDImageIOAnimatedCoder.h in Headers */, 676775CB29378BB6CA3CA5992E9C6A99 /* SDImageIOAnimatedCoderInternal.h in Headers */, D2CD8848F856EC9942A76610AAE66F0A /* SDImageIOCoder.h in Headers */, C6A100159974349FEAAC99B82BE0F872 /* SDImageLoader.h in Headers */, 10017B43AC38C3A89D7AC1376C6E7066 /* SDImageLoadersManager.h in Headers */, EF6A6C725598F572A70C5FCEE328C184 /* SDImageTransformer.h in Headers */, 2DDD48230ED9E8068C7E439D79B99A8E /* SDInternalMacros.h in Headers */, 88473AE7C22F952DACB39FA0758D1624 /* SDMemoryCache.h in Headers */, 3A1AD84C0DC3C256418CC46739024E96 /* SDmetamacros.h in Headers */, 58F7CE37BB4CB3BE806B68A502E6E1A7 /* SDWeakProxy.h in Headers */, 711D32EF4A9901567A488291603BF906 /* SDWebImage.h in Headers */, D62A672EEB252581BD972DDA862BE1DD /* SDWebImage-umbrella.h in Headers */, 53433003112C4FE271EC985803862B61 /* SDWebImageCacheKeyFilter.h in Headers */, 3C8F2F868D0C361CAF43E53CDB8EB631 /* SDWebImageCacheSerializer.h in Headers */, 4688743B7B845309486559EB7BD5D147 /* SDWebImageCompat.h in Headers */, EA82B6D97C9C5D0558047AF552D63203 /* SDWebImageDefine.h in Headers */, 29F7F0E98FD26A96364DBACD7D5F237A /* SDWebImageDownloader.h in Headers */, 854807558DCB972EDDFC1D00032BA6E4 /* SDWebImageDownloaderConfig.h in Headers */, 91E8B94F8E02ABF5197DF5AE7D0B3934 /* SDWebImageDownloaderDecryptor.h in Headers */, B66356D4E7E43B3D15324569AA7EBB05 /* SDWebImageDownloaderOperation.h in Headers */, BCEFDE57BB0E0B36731C8D39FFA1BE2C /* SDWebImageDownloaderRequestModifier.h in Headers */, 18AD90784D549657DF51BC8377DA3085 /* SDWebImageDownloaderResponseModifier.h in Headers */, 7C45DBA62EE045C4922404182F6393B8 /* SDWebImageError.h in Headers */, F49CB22863CCFEC7817D259F27F91C57 /* SDWebImageIndicator.h in Headers */, A9A49E4A3BE8882F60DF32BAF39DE191 /* SDWebImageManager.h in Headers */, 9DF446F8CA5BC4D4098766EC9063012C /* SDWebImageOperation.h in Headers */, 3C7815EEC599DD7D42FDEF19B2FF1563 /* SDWebImageOptionsProcessor.h in Headers */, AC14E56ECA7A4980A8E1CA68E800B12C /* SDWebImagePrefetcher.h in Headers */, 6E66305665DBCFBCF5B2480BF705D500 /* SDWebImageTransition.h in Headers */, 91AAF555B286FBF53E4F98D092B406BD /* SDWebImageTransitionInternal.h in Headers */, BADA31750A2136D073EDA4461DBE1EEA /* UIButton+WebCache.h in Headers */, 71BEB1D9532900291A5A24B1C038516F /* UIColor+SDHexString.h in Headers */, 1830558A4D2D63C8E76BC3136D8213F9 /* UIImage+ExtendedCacheData.h in Headers */, 75771A97B77FA30A0175A81B480F80EF /* UIImage+ForceDecode.h in Headers */, A1560247914C760D9EE5F7A2392CC06C /* UIImage+GIF.h in Headers */, 4ED05DB3E43FF6AE1FA22130B2B50F05 /* UIImage+MemoryCacheCost.h in Headers */, 5DCBA14510E091D6A1CE499B08B794B5 /* UIImage+Metadata.h in Headers */, 3C7EAECB8C573E714C818BA04EB33773 /* UIImage+MultiFormat.h in Headers */, 8D8AD606ECD8E1F247965CD43956D412 /* UIImage+Transform.h in Headers */, 6A19379E3B0370EDA447743C9B1A1379 /* UIImageView+HighlightedWebCache.h in Headers */, 32ACEDCEBE0507A82D6323114A1C74F1 /* UIImageView+WebCache.h in Headers */, 36F4B09E7C71DCC5CEC6057814033C37 /* UIView+WebCache.h in Headers */, B5AF87C11A465F666473F6191D173905 /* UIView+WebCacheOperation.h in Headers */, 69AB6A513D5F36D7360FEF4FDA1D60D0 /* UIView+WebCacheState.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 3AAB81F4DDEBB0F4D169298A90B35AA0 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( DE5CB39DB19EE9A2F044B3AD41BCE22D /* Reachability.h in Headers */, 8632C5137523138C2CE74247D24B9B31 /* Reachability-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 4B0EA8D1BC9C5902B3CAE467B93EA92B /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 97C5A66B195BD0F9E7468F6C561CB4C7 /* NSObject+YYModel.h in Headers */, B4A3D389BDB1B0E149378182CC942228 /* YYClassInfo.h in Headers */, 47E27473C6C3DAD243E76BBB9FAAC38A /* YYModel.h in Headers */, 7C6A24B103E14B07F00100A020D9BA7D /* YYModel-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 59CD76970965AEE096148B5F5C131EC0 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 6A4C0518640F1FF5CB8024E732E9DF95 /* MKAnnotationView+RACSignalSupport.h in Headers */, 22F72E872A783A8AB4670B5037787DD6 /* NSArray+RACSequenceAdditions.h in Headers */, 9DB8E1A6396E3AF5AF0855353063ED40 /* NSData+RACSupport.h in Headers */, A34F832763C924A0911D1E7797533EF8 /* NSDictionary+RACSequenceAdditions.h in Headers */, 3B959E6505C82CA0FF804E07CA51492A /* NSEnumerator+RACSequenceAdditions.h in Headers */, 392E6367FB1A10EEA969ED00E460E4DD /* NSFileHandle+RACSupport.h in Headers */, 99FBE73B7B7E6DA785FAD8ED7A2EBB94 /* NSIndexSet+RACSequenceAdditions.h in Headers */, D8B4CB8F113156A6CD75C060D0113BE4 /* NSInvocation+RACTypeParsing.h in Headers */, 9B2813CB4372A381C770D86A6B8FEA43 /* NSNotificationCenter+RACSupport.h in Headers */, 4851743937896F7DBEC0BCC7A878682C /* NSObject+RACDeallocating.h in Headers */, 73484D39A92E83DF9E2B56D524ECA9C5 /* NSObject+RACDescription.h in Headers */, C08F788A609C4CB2D525FD21D4FAD291 /* NSObject+RACKVOWrapper.h in Headers */, D0DD94C9BBAF8C653232930C3C3F2F73 /* NSObject+RACLifting.h in Headers */, B3CF0FD8C5CAE5360ECF449DF19A4B73 /* NSObject+RACPropertySubscribing.h in Headers */, BBA7D6B1F5A85263992FEDA1FF97BA10 /* NSObject+RACSelectorSignal.h in Headers */, B8E83660C660C63DD95B7C25B3C49DC5 /* NSOrderedSet+RACSequenceAdditions.h in Headers */, 34F0AC42EC0E17A78D8B472363FA732D /* NSSet+RACSequenceAdditions.h in Headers */, F3F7651923711E6A4A12FBE3B5DDCC4E /* NSString+RACKeyPathUtilities.h in Headers */, 4E64E3ACB0070325287252B917BA9EF3 /* NSString+RACSequenceAdditions.h in Headers */, 97694B5A3E6FC632B648D23D314AF28C /* NSString+RACSupport.h in Headers */, C5F712FC268AE177C29C28244AADEF81 /* NSURLConnection+RACSupport.h in Headers */, E075608ADB8A563BCA3D9F6C123220ED /* NSUserDefaults+RACSupport.h in Headers */, 2BEDD35426B348EC616C6A0939C6BD83 /* RACAnnotations.h in Headers */, FD9BA74BACBA3F59304DDF3D5BCF6119 /* RACArraySequence.h in Headers */, 0C6956D9B1BAF797C380C6AEA4C23A5D /* RACBehaviorSubject.h in Headers */, F18AB133D2D0F40618D0A4C87D3AF0BF /* RACBlockTrampoline.h in Headers */, E80BC1F3C11C00D429504583AEC7EB12 /* RACChannel.h in Headers */, 9744E7851D6BCCDD20453B7D9FC86A02 /* RACCommand.h in Headers */, 01337B28102993C3FDD41D9A2E0AFAB2 /* RACCompoundDisposable.h in Headers */, E9E16E749FD6121557C8D9E648A8C6AB /* RACDelegateProxy.h in Headers */, D0A8F86C31CF8BE5C61785EF585EE6A2 /* RACDisposable.h in Headers */, 76FE013CAA5D3972F17B96497E3DBC56 /* RACDynamicSequence.h in Headers */, 3E1D9FD4E15EAD00141A7A1342A0CC10 /* RACDynamicSignal.h in Headers */, 9470CC4CA104628FE6616BDC7AC04062 /* RACEagerSequence.h in Headers */, 71A9A14223B711ED398479921B0D099B /* RACEmptySequence.h in Headers */, 0F307CCAE2D45876CCD0983763D5D45B /* RACEmptySignal.h in Headers */, B6422D1C5378A1E8CF1DB4EA21D31BD3 /* RACErrorSignal.h in Headers */, CB6E35960C294DA751F679E953F4F14F /* RACEvent.h in Headers */, 52B6E27260DC936E3AAE8F2F46CFB416 /* RACEXTKeyPathCoding.h in Headers */, 3F9C3275F786E754C812482941E96F4A /* RACEXTRuntimeExtensions.h in Headers */, 08699A51E1CCDD2B8DD889A284ABE884 /* RACEXTScope.h in Headers */, E5AD74C206FCBD08FBBA2F359D787980 /* RACGroupedSignal.h in Headers */, 28011D6203A46798D789585BB2F5B7F9 /* RACImmediateScheduler.h in Headers */, 596AEAE7A03137CB24B6D05E3B4E47B7 /* RACIndexSetSequence.h in Headers */, AFFCF22AB10FDB1BA56E318CF0FEEACE /* RACKVOChannel.h in Headers */, F7CA711927E14F963F5E47EEBC79EE62 /* RACKVOProxy.h in Headers */, B55D6315E855BFCE88C9B27B0C278C4F /* RACKVOTrampoline.h in Headers */, 225F924255C41D53958FEB795EB51FDE /* RACmetamacros.h in Headers */, FF298EDEF3741C40A1AB8FB9EE00CD2F /* RACMulticastConnection.h in Headers */, 8085C3E407AA5EF1D9D7376F893F2B6A /* RACMulticastConnection+Private.h in Headers */, 45F861348548E0CD9D2C0781E04B3CEA /* RACPassthroughSubscriber.h in Headers */, 8B8E0858C48167346060A72979D08AC1 /* RACQueueScheduler.h in Headers */, 95DF4BD28B81E167A3AC2B7629A561AB /* RACQueueScheduler+Subclass.h in Headers */, 5C7199812C951AD5E976A6AA29A30BD5 /* RACReplaySubject.h in Headers */, 28C71D0DD2723BBE5D5C74A64FE185A0 /* RACReturnSignal.h in Headers */, 8706F7110B1A424205AF1E4A50CDEA7B /* RACScheduler.h in Headers */, 29489009B707CF4D904BEF00214A77C5 /* RACScheduler+Private.h in Headers */, 1F255700661347843AD0580B1AA7D6A5 /* RACScheduler+Subclass.h in Headers */, 1CDFABA9B2A06A4238E706017BE1F38A /* RACScopedDisposable.h in Headers */, 72493549A72116EBF5CAF735CD0AEA80 /* RACSequence.h in Headers */, 12DEDFBDD6A19E45734C9BF576DB4379 /* RACSerialDisposable.h in Headers */, F84821217598572DCD6377D7E5AF7D27 /* RACSignal.h in Headers */, D287C76C50258D4C92FFDD2059168F45 /* RACSignal+Operations.h in Headers */, 5A6AFAF7515C84B07422A61E94280EB6 /* RACSignalSequence.h in Headers */, 56FE435145D35628CB94B8DBD67E81F4 /* RACStream.h in Headers */, 34AAA32786D16B879ED317280AD7C550 /* RACStream+Private.h in Headers */, 14676CDFFDE06FF960179AA34C474EEE /* RACStringSequence.h in Headers */, CBAAE2A674699D3391C898EA00D0AA5C /* RACSubject.h in Headers */, 3C76848E9AA1EE9FB3CA1BAD6A217790 /* RACSubscriber.h in Headers */, E99D0FDFED6B9B6CE4F1373A8D4CEE03 /* RACSubscriber+Private.h in Headers */, E81BE9935B2121C12CEC06480E77EA22 /* RACSubscriptingAssignmentTrampoline.h in Headers */, 2609AEF9B7C24A507DAE3AA17E8ECA21 /* RACSubscriptionScheduler.h in Headers */, 58E512BD916CDD9631428F30A623D370 /* RACTargetQueueScheduler.h in Headers */, AAFF52BFB654564326D45F7BC6C7C1A2 /* RACTestScheduler.h in Headers */, FC16BB1D601656C4C5F0B8B2D2D38A65 /* RACTuple.h in Headers */, 17A4935F78766E41FC29D1D668146EF3 /* RACTupleSequence.h in Headers */, 46FE7652F3F63388BF97D9C8B7015A6B /* RACUnarySequence.h in Headers */, 17134C3258CDB162F0507654B177CAE3 /* RACUnit.h in Headers */, C8B284A4946540AF76FF866F9F34CF67 /* RACValueTransformer.h in Headers */, 8320C45699CAC5719F2A8520B6592F66 /* ReactiveObjC.h in Headers */, 9C034894E873E67C35BAF6879BF6F05A /* ReactiveObjC-umbrella.h in Headers */, 179413BD936A98D571223DDA39AB9786 /* UIActionSheet+RACSignalSupport.h in Headers */, 72AE6D5A79C2CC6D5E6578F9644C654B /* UIAlertView+RACSignalSupport.h in Headers */, 11422CB222C72A6EF77DA96D152FA6F8 /* UIBarButtonItem+RACCommandSupport.h in Headers */, 2B05197D9780DA9C2A6C74663FDC1BE8 /* UIButton+RACCommandSupport.h in Headers */, A583A30CEA07A0748B6D95D5D12FF5F2 /* UICollectionReusableView+RACSignalSupport.h in Headers */, E15319829D042A0E6BBE4005D44FACDA /* UIControl+RACSignalSupport.h in Headers */, 5889FA7EB7333745C27C7FACAA9FBB87 /* UIControl+RACSignalSupportPrivate.h in Headers */, 89D6769F9AA17FE6F4742FB49196CE8B /* UIDatePicker+RACSignalSupport.h in Headers */, D5F07F7462A9AD80E4A9DB21294D8FA2 /* UIGestureRecognizer+RACSignalSupport.h in Headers */, 3650C86E60D03E4678315B00B3910572 /* UIImagePickerController+RACSignalSupport.h in Headers */, 01B2E81FBD9A6D179150C9130B2C1807 /* UIRefreshControl+RACCommandSupport.h in Headers */, BC60AF43B11C3B3BEA8E91CD67B3EB0C /* UISegmentedControl+RACSignalSupport.h in Headers */, DE2553DFC19AAB617B63261D50C44091 /* UISlider+RACSignalSupport.h in Headers */, D40F7E1DD6EA34E8F749A14B0CD9BE84 /* UIStepper+RACSignalSupport.h in Headers */, 2D873076AFF986FB88A07B056E6E1B45 /* UISwitch+RACSignalSupport.h in Headers */, 30FE660831E11D2EA5791BB9D84A3978 /* UITableViewCell+RACSignalSupport.h in Headers */, 1A8F4D7FCCFAA0A692DB10B31D3C4A70 /* UITableViewHeaderFooterView+RACSignalSupport.h in Headers */, 4958D2D0D6DC37810FAB1105D10E90D3 /* UITextField+RACSignalSupport.h in Headers */, 540484CA48AE6E538865EE0882E9B886 /* UITextView+RACSignalSupport.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; 75390687FBF4745190E1761070980CBF /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( A4334E9632A3BD0817D4F02E5D056D08 /* Collections+CustomInjection.h in Headers */, 6AA16C78FAF9F27ADBA06E4AC494A490 /* NSArray+TyphoonManualEnumeration.h in Headers */, 1BE3F2879E492B7F9A5AEBA3E3766100 /* NSDictionary+CustomInjection.h in Headers */, 633D48FC60D6C9C2BEEE6F9D7FB9FB31 /* NSInvocation+TCFCustomImplementation.h in Headers */, AEEC1FF7C7D8C9B92AD8350441EED206 /* NSInvocation+TCFInstanceBuilder.h in Headers */, 2FA3D210CCCBA41BC36F0D716F173F19 /* NSInvocation+TCFUnwrapValues.h in Headers */, 8383EB3CD47EA7CC0BB80D288FDA7483 /* NSInvocation+TCFWrapValues.h in Headers */, 87564E72B9489B5338ECCEDC18CC9705 /* NSLayoutConstraint+TyphoonOutletTransfer.h in Headers */, 1725041850D30E89E549FD343C9B1073 /* NSMethodSignature+TCFUnwrapValues.h in Headers */, 8DBD580686F262FA6D74D45CEA708336 /* NSNullTypeConverter.h in Headers */, D2AA8F17857AFD2BC5F384053D3FB619 /* NSObject+DeallocNotification.h in Headers */, 055CCD518759FBBD7DFD0B7C079A428B /* NSObject+FactoryHooks.h in Headers */, F465B45E09EC9F7F08BAC6735CD7124D /* NSObject+PropertyInjection.h in Headers */, 317B814C899E76C4C794A92F42748AB5 /* NSObject+TyphoonIntrospectionUtils.h in Headers */, 6D03C8262D633E11DFF26765A28B3916 /* NSValue+TCFUnwrapValues.h in Headers */, CD7113EF56D2E1D58FC49C10330BD6BE /* OCLogTemplate.h in Headers */, 9BF8077CFA9BBB10510FE915F2919A6E /* Typhoon.h in Headers */, 7C1725EF13F888580C9E2F3057B33FA4 /* Typhoon+Infrastructure.h in Headers */, 086E51FFE1FBD5D2C964EC8F7C1C1F5A /* Typhoon-umbrella.h in Headers */, 8DE844D9A25F4BA974AEB6997F1848FE /* TyphoonAbstractDetachableComponentFactoryPostProcessor.h in Headers */, FF266014693FE86BF83B9FD19AFCA071 /* TyphoonAbstractInjection.h in Headers */, 756F7CB95E7C108324A2F7B8FBF6B17A /* TyphoonAssembly.h in Headers */, AFE16FE9315D936E91EECD0184A88FC0 /* TyphoonAssembly+TyphoonAssemblyFriend.h in Headers */, A2CF6FBE8187A1AF97E9995C6F119354 /* TyphoonAssemblyAccessor.h in Headers */, 1E23DAF8BFF220AA548435F8987B5EC8 /* TyphoonAssemblyAdviser.h in Headers */, 79CCECA8A1FF25E880AB21DA8BF337A6 /* TyphoonAssemblyBuilder.h in Headers */, E81F276ED056A35A0D7BB6CE9F9B26DC /* TyphoonAssemblyBuilder+PlistProcessor.h in Headers */, 681A93DD87FCA7FCAFF578C095B6B9E7 /* TyphoonAssemblyDefinitionBuilder.h in Headers */, AA53B6ED50FD6066CC3536EB4C571E0C /* TyphoonAssemblyPropertyInjectionPostProcessor.h in Headers */, 5806683E752BD6A1B02EE96BE0741385 /* TyphoonAssemblySelectorAdviser.h in Headers */, 543A777E19623D18C1389BC6189F21FC /* TyphoonAutoInjection.h in Headers */, D649EB2BE8D16B1BB89E41CAA09C1702 /* TyphoonBlockComponentFactory.h in Headers */, D1E674341E780EABFB25ECC1420B08A2 /* TyphoonBlockDefinition.h in Headers */, 299D0A26A874ADE845CBB1654CFB48EE /* TyphoonBlockDefinition+InstanceBuilder.h in Headers */, 4F650AA71665246CC359F05EA7B041D0 /* TyphoonBlockDefinition+Internal.h in Headers */, 47BB76E62236FE1305B71F3D4484E52B /* TyphoonBlockDefinitionController.h in Headers */, E3DB273F0E910956E9DE80D085901ECE /* TyphoonBundledImageTypeConverter.h in Headers */, DB7372889DFAB01A489C7A290AFFD9B2 /* TyphoonBundleResource.h in Headers */, 2A7799AF29FD31F1C0EAC4857E7511F2 /* TyphoonCallStack.h in Headers */, CD923193903F98B2F7CD0354C2116417 /* TyphoonCircularDependencyTerminator.h in Headers */, 3D81A472030665B9BA7F4BF2E11A4541 /* TyphoonCollaboratingAssembliesCollector.h in Headers */, DFC863A0F6A7902C35DC110A3CCBFE93 /* TyphoonCollaboratingAssemblyPropertyEnumerator.h in Headers */, 5722A8075D663DC1E070027A450C21AA /* TyphoonCollaboratingAssemblyProxy.h in Headers */, 0F0B53B1D332733C46244D44DAB58508 /* TyphoonColorConversionUtils.h in Headers */, 133E972891174F621EB1C605A111C61E /* TyphoonComponentFactory.h in Headers */, 6D423F8538D6266DB24DAFFED462D46C /* TyphoonComponentFactory+InstanceBuilder.h in Headers */, E528D9C6017F3966EDE09AE63D49B22E /* TyphoonComponentFactory+Storyboard.h in Headers */, AFFC8A6E89ABBDA38786D88EFAFACC90 /* TyphoonComponentFactory+TyphoonDefinitionRegisterer.h in Headers */, 385FEBB3BFA25B1AA8ECDFF703FD997A /* TyphoonComponentsPool.h in Headers */, BA7F55364F2525C6B8AEA477AF3C8D67 /* TyphoonConfigPostProcessor.h in Headers */, 215534F06D3902AE9E7D62DC1718C568 /* TyphoonConfigPostProcessor+Internal.h in Headers */, 550F4E490798ADB3B4B2C706CEEA4A8C /* TyphoonConfiguration.h in Headers */, B7E49AB762D1E2F7EA004FA2EEBD85A5 /* TyphoonDefinition.h in Headers */, 27DAE8D358BD60E2886684E5A056E752 /* TyphoonDefinition+Config.h in Headers */, DBDC0A614556A03F8950731FCB0B2B92 /* TyphoonDefinition+Infrastructure.h in Headers */, EEA86BBF0385C53CE0567D92C87C5E8B /* TyphoonDefinition+InstanceBuilder.h in Headers */, C22FE4A2C5BCF1C1BE001EC38B21A720 /* TyphoonDefinition+Internal.h in Headers */, EE19A56BB25AE31295A4D913C339FF02 /* TyphoonDefinition+Option.h in Headers */, B297981C7C5C77B17BE03972BB55D946 /* TyphoonDefinition+Storyboard.h in Headers */, DC2FDB5C74828AB18BA20E051DF0C37A /* TyphoonDefinitionNamespace.h in Headers */, FF4E53497AD777F441D3CEF6355A4258 /* TyphoonDefinitionPostProcessor.h in Headers */, 94633A6E95CF8D2DCB8D84EA25822024 /* TyphoonDefinitionRegisterer.h in Headers */, D2BB17CC67A51CF2396BEE012663441D /* TyphoonFactoryAutoInjectionPostProcessor.h in Headers */, AED62511435CB679D8E8AC227B4AD8BD /* TyphoonFactoryDefinition.h in Headers */, 78DB67A6B39E0407C2987B9841C0B052 /* TyphoonFactoryPropertyInjectionPostProcessor.h in Headers */, EE77A716DB37472592376BD26C7DDD78 /* TyphoonGlobalConfigCollector.h in Headers */, D3BD1317C424DEBE3F14AA0F6B0F2D54 /* TyphoonInject.h in Headers */, F8828C0B5037617E7C5EAB0F6499302E /* TyphoonInjectedObject.h in Headers */, C605731D421EA67B3D573E9C3E7C10CF /* TyphoonInjection.h in Headers */, A2CC61E4BDAEBF249FCD573057423A7A /* TyphoonInjectionByCollection.h in Headers */, E694501C0EFD2BDD91303F10CD877F31 /* TyphoonInjectionByComponentFactory.h in Headers */, 150373979C0DF8F34C788F000C2BE265 /* TyphoonInjectionByConfig.h in Headers */, 127034388C945656BD5A7C979DA4029E /* TyphoonInjectionByCurrentRuntimeArguments.h in Headers */, A07B14D885F9954EC871BBB950DD65F9 /* TyphoonInjectionByDictionary.h in Headers */, 62C99100FB8B0AB574F6CA3B6EE47C61 /* TyphoonInjectionByFactoryReference.h in Headers */, A096C06AA7D3C5C4E813C211767C3DDD /* TyphoonInjectionByObjectFromString.h in Headers */, D3B29ED852413926E7B50A998517B1B5 /* TyphoonInjectionByObjectInstance.h in Headers */, 9345A406A970313436762F4127B84249 /* TyphoonInjectionByReference.h in Headers */, 2483B32E63F6D95EFD66313968EDE3E7 /* TyphoonInjectionByRuntimeArgument.h in Headers */, 53EF718B00DF8A17F05A50B6E478E799 /* TyphoonInjectionByType.h in Headers */, B24A1E4D80FC605011440F6676C52EF7 /* TyphoonInjectionContext.h in Headers */, A5C476EC387C41C840347804DF32CAF6 /* TyphoonInjectionDefinition.h in Headers */, 1CCD65F93AB302520C82949625A48CE7 /* TyphoonInjections.h in Headers */, B995464A5578FA477280532E2C42C72E /* TyphoonInjectionsEnumeration.h in Headers */, 12A67ABB2A4FE1BCF978075EBC33EB90 /* TyphoonInstancePostProcessor.h in Headers */, FA97AAE3347E765BA649A2587F6A7BBC /* TyphoonIntrospectionUtils.h in Headers */, 422523173C30F2F9695A6635431DAF64 /* TyphooniOS.h in Headers */, 12A631453F473C22F8936BF263BAD23E /* TyphoonJsonStyleConfiguration.h in Headers */, 24D5DB6DC75B9301C8A56C067549ED18 /* TyphoonLinkerCategoryBugFix.h in Headers */, 7D52D2C3BDE0C22A5305A2619A99E60C /* TyphoonLoadedView.h in Headers */, D0DA377AA5FF23BEB89614E300E90D2D /* TyphoonMemoryManagementUtils.h in Headers */, 884B156015530EE607F57D665B076E89 /* TyphoonMethod.h in Headers */, 6D7651C8B1EA18B2EB5278952B19FC15 /* TyphoonMethod+InstanceBuilder.h in Headers */, E1B4B95B63D486197DB87B5301B2AD68 /* TyphoonMethodSwizzler.h in Headers */, 80F8652EFE9F52A3E61806E5E01AE251 /* TyphoonNibLoader.h in Headers */, 7C03AB94ABD7FA82C939386C8827E0B4 /* TyphoonNSNumberTypeConverter.h in Headers */, 03E6A871ACAE01F6AD2BFD0079FC0664 /* TyphoonNSURLTypeConverter.h in Headers */, 7CA28A9549AA6EC233E0F26268EE312E /* TyphoonObjectWithCustomInjection.h in Headers */, 79D360313699E9086FEDED32E78C12B4 /* TyphoonOptionMatcher.h in Headers */, CDB274EDA02524CBBF99EEE217AE4505 /* TyphoonOptionMatcher+Internal.h in Headers */, 81FEC159F4F96E6017E7B841CB99A5F0 /* TyphoonOrdered.h in Headers */, D31E7D189E5A633875C9C723B5CCDECA /* TyphoonParameterInjection.h in Headers */, 7F455A25066EB5B687D341842A8825F8 /* TyphoonParentReferenceHydratingPostProcessor.h in Headers */, F99A8C6F169EBF7236DA9FD6A89A4CAD /* TyphoonPassThroughTypeConverter.h in Headers */, 43BC3B34407AEA67381BAD180BCA1156 /* TyphoonPatcher.h in Headers */, B9B1C853AA90EF26C521CA880113A050 /* TyphoonPathResource.h in Headers */, 78EF98028F832D934652FDFF7527F595 /* TyphoonPlistStyleConfiguration.h in Headers */, 8A66D5CED5EACD00CFB93A5A609B8EA2 /* TyphoonPreattachedComponentsRegisterer.h in Headers */, 571C90CBC5777D2194962B65F994FE72 /* TyphoonPrimitiveTypeConverter.h in Headers */, 0F76408EFB678D11DB62752B64017A40 /* TyphoonPropertyInjection.h in Headers */, 7008DDC9510D90489B283BEF7993789C /* TyphoonPropertyStyleConfiguration.h in Headers */, 701B922F54EA9A9A9FDA2C17CE75CDE0 /* TyphoonReferenceDefinition.h in Headers */, CF2E818EBCD9D8AE88B891F33C042B46 /* TyphoonResource.h in Headers */, FC0726705C4A0C4073F97C70C7045539 /* TyphoonRuntimeArguments.h in Headers */, BF560791320154400F8183101750BAEF /* TyphoonSelector.h in Headers */, 3443028D46C9F3EF2BD8A7A22B4DC101 /* TyphoonStackElement.h in Headers */, A754D259316F9E237ABCA70AD7120758 /* TyphoonStartup.h in Headers */, 7C92A7B83580ADE50BF1E77BE494445A /* TyphoonStoryboard.h in Headers */, 44AEABDD5FE11DCE9EE1F00ECBC340F5 /* TyphoonStoryboardDefinition.h in Headers */, BED810910D0734E1A3D77CC7A4B7B08F /* TyphoonStoryboardDefinitionContext.h in Headers */, F5372060DD1B80D1DAB5ABA234F40C66 /* TyphoonStoryboardProvider.h in Headers */, 49C540A33BC7DB59595582A72D92E8FF /* TyphoonStoryboardResolver.h in Headers */, 59DD3A20CB015329E4F181DE7B1A7373 /* TyphoonSwizzlerDefaultImpl.h in Headers */, 2226813C530D9A755E29C9F35308E33C /* TyphoonTestUtils.h in Headers */, 110880E9AFA349C846476980F8AF8086 /* TyphoonTypeConversionUtils.h in Headers */, C96D04C53EB024F556E0F13422EED07A /* TyphoonTypeConverter.h in Headers */, 936352E7D044088C175A70DEB26D051A /* TyphoonTypeConverterRegistry.h in Headers */, 19B35F7B7E03A4884DF2861528AA40E1 /* TyphoonTypeDescriptor.h in Headers */, C08752A16103BCA4C7FD459F2D2CAF52 /* TyphoonUIColorTypeConverter.h in Headers */, F59065DC32765AD32444CEAE1B1E8250 /* TyphoonUtils.h in Headers */, 5558D029B8BABF2823BAB0A4A689C100 /* TyphoonViewControllerFactory.h in Headers */, A046D9E46D0C8242D29D4365D8DBEC5A /* TyphoonViewControllerInjector.h in Headers */, 1E21419F413F6D4CA470254D9A0349BA /* TyphoonViewControllerNibResolver.h in Headers */, 2A71217971D20B8AB2B3E50CC364417E /* TyphoonViewHelpers.h in Headers */, D5A4DE2104B36BC4B8FD6E7E5FC30938 /* TyphoonWeakComponentsPool.h in Headers */, CC4630C63CAEBBA70AF10E01626A5D30 /* UIResponder+TyphoonOutletTransfer.h in Headers */, 3B55DB273C7CD31AACE379298843C0A1 /* UIView+TyphoonDefinitionKey.h in Headers */, A79648E20089DBD6BF599C9F6B6570CE /* UIView+TyphoonOutletTransfer.h in Headers */, 659C4CDCC04F165FA3A3C4FDF3DA6A2C /* UIViewController+TyphoonStoryboardIntegration.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; A6A1125548A7071D292A6262ED6B138E /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 1F9CDA98F3994B1BE2B535BDF2D073E5 /* Pods-iOS-Network-Stack-Dive-umbrella.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; E00DD05508D994002EFC16C8D151D97D /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; files = ( 0CD72127DA641AD9C10CF2DB2D352C6D /* CocoaAsyncSocket-umbrella.h in Headers */, 5884BE5A453A9E0417728E3FB8063EDD /* GCDAsyncSocket.h in Headers */, 95621F7EE7F56B1ACF4141E6EFB175A8 /* GCDAsyncUdpSocket.h in Headers */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ 3847153A6E5EEFB86565BA840768F429 /* SDWebImage */ = { isa = PBXNativeTarget; buildConfigurationList = 2F9E94B5F79E365095CB33D3D3FCA6A2 /* Build configuration list for PBXNativeTarget "SDWebImage" */; buildPhases = ( 33783D69751B087D045FCF1FCA02E724 /* Headers */, 090ABC49CEE6AE75BCDDAAED6457F183 /* Sources */, 3A5330E1BD187252F408EBB46F1BDC42 /* Frameworks */, 44B3C0D7DDF289331B7732E9D87126DB /* Resources */, ); buildRules = ( ); dependencies = ( 5BD57B0A429FAA76BD164214280AF573 /* PBXTargetDependency */, ); name = SDWebImage; productName = SDWebImage; productReference = B0B214D775196BA7CA8E17E53048A493 /* SDWebImage */; productType = "com.apple.product-type.framework"; }; 438B238ACC7DF1178D1BCE1A31983146 /* ReactiveObjC */ = { isa = PBXNativeTarget; buildConfigurationList = F3525B72C16A0105CD51405DE75181D5 /* Build configuration list for PBXNativeTarget "ReactiveObjC" */; buildPhases = ( 59CD76970965AEE096148B5F5C131EC0 /* Headers */, 51734856874874E0CCB9EFBBA0B96F3F /* Sources */, D18029DDA26EDC747F5E47916C3BB239 /* Frameworks */, 8418C5E5E4B25B6D846B7AD586EBA5BF /* Resources */, ); buildRules = ( ); dependencies = ( ); name = ReactiveObjC; productName = ReactiveObjC; productReference = 9621C6383F5733C35183B2DE886C7EC6 /* ReactiveObjC */; productType = "com.apple.product-type.framework"; }; 498F16FB9B1AF5229DA97AFD7402676F /* Pods-iOS-Network-Stack-DiveTests */ = { isa = PBXNativeTarget; buildConfigurationList = 9D5D253855B8EC056D584BB71826AF43 /* Build configuration list for PBXNativeTarget "Pods-iOS-Network-Stack-DiveTests" */; buildPhases = ( 1632B972B125CE76633192F16FA2909B /* Headers */, 8839AD11D45758F09C71D584863AF55C /* Sources */, 423E6C84ACB40AD69A48E1B0525B5501 /* Frameworks */, 1A67D6A6A3F1EF31E02806A35E0A30FA /* Resources */, ); buildRules = ( ); dependencies = ( 281BE9CFB6988946C189C95F2AACB42C /* PBXTargetDependency */, FB2CC1F79B64220967D377FAA987EB70 /* PBXTargetDependency */, ); name = "Pods-iOS-Network-Stack-DiveTests"; productName = Pods_iOS_Network_Stack_DiveTests; productReference = A443C69F8A192846AC63B3202379F32A /* Pods-iOS-Network-Stack-DiveTests */; productType = "com.apple.product-type.framework"; }; 55AF53E6C77A10ED4985E04D74A8878E /* Masonry */ = { isa = PBXNativeTarget; buildConfigurationList = AAA1F8799DB68036C3BE983C05FAA2C7 /* Build configuration list for PBXNativeTarget "Masonry" */; buildPhases = ( 0EF2526B5CC15C0E915F32190565F64C /* Headers */, DA0B6A6F9B3EDF226BF081DAC7E777E7 /* Sources */, 12A799DC8ABB2C283ADDDED4421A5EAB /* Frameworks */, ECD6B9A8E754DF142B323DF2D7E0D112 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Masonry; productName = Masonry; productReference = 1FFED36A657123030ABB700256D73F15 /* Masonry */; productType = "com.apple.product-type.framework"; }; 6083682834ABE0AE7BD1CBF06CADD036 /* CocoaAsyncSocket */ = { isa = PBXNativeTarget; buildConfigurationList = 7C5D55F9FC3A8A30A56D403120D1C06B /* Build configuration list for PBXNativeTarget "CocoaAsyncSocket" */; buildPhases = ( E00DD05508D994002EFC16C8D151D97D /* Headers */, 2A77BB83B89C22992F39A72E779418C7 /* Sources */, FA3F4FA98DFFB612E3B16EA4A609DAB7 /* Frameworks */, 702F664334E9016A4CD711650A9506CA /* Resources */, ); buildRules = ( ); dependencies = ( ); name = CocoaAsyncSocket; productName = CocoaAsyncSocket; productReference = 6CBEFE4F9E22AFDC6347A739BB35FF8C /* CocoaAsyncSocket */; productType = "com.apple.product-type.framework"; }; 630AEC5FE2D10DE4AF88009DC6EC819A /* Pods-iOS-Network-Stack-Dive */ = { isa = PBXNativeTarget; buildConfigurationList = 455F1BBD53E2CF7CA53C22DCBD5B5BF0 /* Build configuration list for PBXNativeTarget "Pods-iOS-Network-Stack-Dive" */; buildPhases = ( A6A1125548A7071D292A6262ED6B138E /* Headers */, 820743F98B0FA9477A3A8C9E3E4E7DA3 /* Sources */, 412AD72FD28E53A869CC1DE993783892 /* Frameworks */, 9760D5C8D4601A2C6FDA73A08F9C54C5 /* Resources */, ); buildRules = ( ); dependencies = ( 0843473CB7CB6169AC7EF3C5B1319539 /* PBXTargetDependency */, 20D3ECBF6436731DFBDB894E0C8F9909 /* PBXTargetDependency */, BA75486E4414284637D76C083139C6E6 /* PBXTargetDependency */, 4604B5CB7C8FC5C7D8CE2CEC5AB1FCC8 /* PBXTargetDependency */, 3A810513404714D3C5FDEAFFDA8E9F60 /* PBXTargetDependency */, 729E1AD6D3FC220382AB7C459EDED432 /* PBXTargetDependency */, DE37340EF49D944251DFC55BA963D9A0 /* PBXTargetDependency */, 22A8C4276A99677AAA69DE811FB63204 /* PBXTargetDependency */, ); name = "Pods-iOS-Network-Stack-Dive"; productName = Pods_iOS_Network_Stack_Dive; productReference = 8AEA2F92AE161A3CD9059649BB497327 /* Pods-iOS-Network-Stack-Dive */; productType = "com.apple.product-type.framework"; }; 6D7BC7A068E56396BBE1DECFD19DDDEB /* Typhoon */ = { isa = PBXNativeTarget; buildConfigurationList = A89A1A0D451C2C02A9188F1F989D5D8F /* Build configuration list for PBXNativeTarget "Typhoon" */; buildPhases = ( 75390687FBF4745190E1761070980CBF /* Headers */, 3DC320636C5D7E3B8EBCD26375752456 /* Sources */, AFA73E0444C77BC1DA31F0305CEBE0C7 /* Frameworks */, 8A9204228D141A453374B618ED92CC57 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Typhoon; productName = Typhoon; productReference = 6C08E1C10481BD7FAC0F4A2019635945 /* Typhoon */; productType = "com.apple.product-type.framework"; }; 84B44807A12996D487A4A591A481D6A0 /* YYModel */ = { isa = PBXNativeTarget; buildConfigurationList = D0544D7DC4BCDADC7495D11D44228F4B /* Build configuration list for PBXNativeTarget "YYModel" */; buildPhases = ( 4B0EA8D1BC9C5902B3CAE467B93EA92B /* Headers */, CD5D9934A10A00C27F600B9FC3CA2A18 /* Sources */, 0CB5494E5539E84494DEEC0B4BB3991D /* Frameworks */, B5976ACC8512D9E7821BD1DBEB00A151 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = YYModel; productName = YYModel; productReference = E460D5B0416D36F66EE8EC89E5D2FA0A /* YYModel */; productType = "com.apple.product-type.framework"; }; 94CFBA7D633ECA58DF85C327B035E6A3 /* SDWebImage-SDWebImage */ = { isa = PBXNativeTarget; buildConfigurationList = 5192A2BBC2E6464EC36FA5D86309BB74 /* Build configuration list for PBXNativeTarget "SDWebImage-SDWebImage" */; buildPhases = ( 75A6069EC19BC1F48B6A153D3EA84633 /* Sources */, 419B034446CD8E2FD17F87A0EAEC979A /* Frameworks */, 7955425C97E8D3018B6D152B0BBB1213 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = "SDWebImage-SDWebImage"; productName = SDWebImage; productReference = CF1281E58AA1045D4B7F33FC56691C42 /* SDWebImage-SDWebImage */; productType = "com.apple.product-type.bundle"; }; C260B5A26D3CD54F215E5E39371483B6 /* OCMock */ = { isa = PBXNativeTarget; buildConfigurationList = DDFDACF484D4EC7740167FF70F676380 /* Build configuration list for PBXNativeTarget "OCMock" */; buildPhases = ( 2317BC2378351692DC408D7D620ECDFD /* Headers */, 69B48275140E3148722751E798D8DF26 /* Sources */, A8288B9C9F7E3201CF4C5CBD9735F905 /* Frameworks */, EC55AE5E644134F4E54E9D397597D823 /* Resources */, ); buildRules = ( ); dependencies = ( ); name = OCMock; productName = OCMock; productReference = F30C125DB91902F63DC73AEF0B0E6111 /* OCMock */; productType = "com.apple.product-type.framework"; }; CAA047C0F5E4106F3904E8497FA17F97 /* Reachability */ = { isa = PBXNativeTarget; buildConfigurationList = 35F578C15A7207B829452A13078C0F9C /* Build configuration list for PBXNativeTarget "Reachability" */; buildPhases = ( 3AAB81F4DDEBB0F4D169298A90B35AA0 /* Headers */, 8EC01F2F884A8FC99A6F71878A870A29 /* Sources */, 3C9BA069841AE1F4B830AFE040DBAF05 /* Frameworks */, AD591B1ADF4AA6BD222E6FD36DB9138E /* Resources */, ); buildRules = ( ); dependencies = ( ); name = Reachability; productName = Reachability; productReference = 400FF55D0451E7A8F33A3D0D3E11C1B9 /* Reachability */; productType = "com.apple.product-type.framework"; }; F1BCD9702276377FB5B3BDB6EAF709D7 /* DZNEmptyDataSet */ = { isa = PBXNativeTarget; buildConfigurationList = 1CF7CA11A791652D6975B2EDE2FC6719 /* Build configuration list for PBXNativeTarget "DZNEmptyDataSet" */; buildPhases = ( 09EE4349480B078AFE00E65B49E502BF /* Headers */, 06020C44A2C925372A030239192BBF6C /* Sources */, E672724427269716049B769344D7C3DC /* Frameworks */, FD378D1F5CAFCC3C5D44074788563A1C /* Resources */, ); buildRules = ( ); dependencies = ( ); name = DZNEmptyDataSet; productName = DZNEmptyDataSet; productReference = 5DA4577FE3BC4A03751108FFED07B385 /* DZNEmptyDataSet */; productType = "com.apple.product-type.framework"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ BFDFE7DC352907FC980B868725387E98 /* Project object */ = { isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1600; }; buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; compatibilityVersion = "Xcode 16.0"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( Base, en, ); mainGroup = CF1408CF629C7361332E53B88F7BD30C; minimizedProjectReferenceProxies = 0; preferredProjectObjectVersion = 77; productRefGroup = 1FF60BCB01852F445494BA9733E861EB /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 6083682834ABE0AE7BD1CBF06CADD036 /* CocoaAsyncSocket */, F1BCD9702276377FB5B3BDB6EAF709D7 /* DZNEmptyDataSet */, 55AF53E6C77A10ED4985E04D74A8878E /* Masonry */, C260B5A26D3CD54F215E5E39371483B6 /* OCMock */, 630AEC5FE2D10DE4AF88009DC6EC819A /* Pods-iOS-Network-Stack-Dive */, 498F16FB9B1AF5229DA97AFD7402676F /* Pods-iOS-Network-Stack-DiveTests */, CAA047C0F5E4106F3904E8497FA17F97 /* Reachability */, 438B238ACC7DF1178D1BCE1A31983146 /* ReactiveObjC */, 3847153A6E5EEFB86565BA840768F429 /* SDWebImage */, 94CFBA7D633ECA58DF85C327B035E6A3 /* SDWebImage-SDWebImage */, 6D7BC7A068E56396BBE1DECFD19DDDEB /* Typhoon */, 84B44807A12996D487A4A591A481D6A0 /* YYModel */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 1A67D6A6A3F1EF31E02806A35E0A30FA /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 44B3C0D7DDF289331B7732E9D87126DB /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( 16D7DCB7CC985C33EEC41B371C029C84 /* SDWebImage-SDWebImage in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 702F664334E9016A4CD711650A9506CA /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 7955425C97E8D3018B6D152B0BBB1213 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( E59439290BC7524578C0A0E9C06B7719 /* PrivacyInfo.xcprivacy in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; 8418C5E5E4B25B6D846B7AD586EBA5BF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 8A9204228D141A453374B618ED92CC57 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 9760D5C8D4601A2C6FDA73A08F9C54C5 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; AD591B1ADF4AA6BD222E6FD36DB9138E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; B5976ACC8512D9E7821BD1DBEB00A151 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; EC55AE5E644134F4E54E9D397597D823 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; ECD6B9A8E754DF142B323DF2D7E0D112 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; FD378D1F5CAFCC3C5D44074788563A1C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 06020C44A2C925372A030239192BBF6C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( AFD1D411A9387DCEC6F0034653E23787 /* DZNEmptyDataSet-dummy.m in Sources */, 5F5426608460A8B17C1AE5C2351BAA19 /* UIScrollView+EmptyDataSet.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 090ABC49CEE6AE75BCDDAAED6457F183 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( FA3021DED76B9B182CC9195A60EB1209 /* NSBezierPath+SDRoundedCorners.m in Sources */, 48916DE9521F627589300512ECC2D4A5 /* NSButton+WebCache.m in Sources */, 416DA8B2997381F954DBA6E6A53DA4A2 /* NSData+ImageContentType.m in Sources */, 88A23DF6F5638AC66C28C4102824E8B5 /* NSImage+Compatibility.m in Sources */, 1708C1D28B421C4AD310426D1695CE77 /* SDAnimatedImage.m in Sources */, 7A4EB9ED5D4E03170FFE61FCB299687B /* SDAnimatedImagePlayer.m in Sources */, 5111A0A0934551CD2B9DDB1A1CA79FA7 /* SDAnimatedImageRep.m in Sources */, 62FE895DF9D65A2955A275D909ECBE18 /* SDAnimatedImageView.m in Sources */, 67178A8153B1A2F1D0D544B8093E23C5 /* SDAnimatedImageView+WebCache.m in Sources */, C93E972E75F84674690300123984EC43 /* SDAssociatedObject.m in Sources */, 0B0E6CECDF516BC83756C1D5515A725B /* SDAsyncBlockOperation.m in Sources */, 69A06A02F52EB26259FAD1DF6B121BE1 /* SDCallbackQueue.m in Sources */, 526485EF6D2B62B24DB59122FB94BD42 /* SDDeviceHelper.m in Sources */, 9CE425B89294BE2C13E70A86E75B15CF /* SDDiskCache.m in Sources */, 5308E660E723C11E7691D311FD59C459 /* SDDisplayLink.m in Sources */, 9345137ED10358B60E37D05FB6165759 /* SDFileAttributeHelper.m in Sources */, 71F2B8CBB99087F348C472230200586F /* SDGraphicsImageRenderer.m in Sources */, ECE64B732F9FA7C402DDEEC58DCB9D98 /* SDImageAPNGCoder.m in Sources */, 6B0978C9398336656EE309E62060AEAB /* SDImageAssetManager.m in Sources */, 597E390C0BBB75B8045B651C487C2034 /* SDImageAWebPCoder.m in Sources */, 0FF9F459ED16719292443A4C99B52B20 /* SDImageCache.m in Sources */, FCDEC6A53CF5517E1AF5B331FD65F6D9 /* SDImageCacheConfig.m in Sources */, FEA8BA4F82CCBD1D28DCC7EF39FB4096 /* SDImageCacheDefine.m in Sources */, 33D3587AF629B2FA21554DA002D6ACB8 /* SDImageCachesManager.m in Sources */, 55F7C7F055A18044497F8C88CAE34118 /* SDImageCachesManagerOperation.m in Sources */, E8AB529B9E0B4C23921344F6C4ABFEA4 /* SDImageCoder.m in Sources */, B2704AFFC5CC053154839DB44924D255 /* SDImageCoderHelper.m in Sources */, 4D2C79AB2D24CFEC864F08D913CE7692 /* SDImageCodersManager.m in Sources */, 24E8E4ED0B5D988E3346E6638619F4E4 /* SDImageFrame.m in Sources */, 320DE42AF3CFE11FF785FEB1A7E6547B /* SDImageFramePool.m in Sources */, 83530BF68848CD2C4A79A1FD69B304A5 /* SDImageGIFCoder.m in Sources */, E50613C67DD02AF6EA825DA0B31EFFAD /* SDImageGraphics.m in Sources */, 18660FA595DBE133BB784E813A7122A8 /* SDImageHEICCoder.m in Sources */, 288D796F3F7B9F42690E24A3B1018B2C /* SDImageIOAnimatedCoder.m in Sources */, 717F76926C7BCB5B10C3037AD9239084 /* SDImageIOCoder.m in Sources */, 6EFEEE3AE22E97DCEC4F5A3B88F56FC7 /* SDImageLoader.m in Sources */, 85C0B4EE334B9972299E62DE61A4BB56 /* SDImageLoadersManager.m in Sources */, 3D0BBFEC1921CE71BC240DC18D8BE540 /* SDImageTransformer.m in Sources */, 864972FB0DF4B464B1B505AA5F788E91 /* SDInternalMacros.m in Sources */, E0BCF21E9FA59F638C13ECCECC4D9690 /* SDMemoryCache.m in Sources */, CA1E0DCDF679EA2DE2ED0915426E1D04 /* SDWeakProxy.m in Sources */, 96E97174F4614FFA0649085022CB4AFE /* SDWebImage-dummy.m in Sources */, B95C63A039D9D08896421291DEBD3AEB /* SDWebImageCacheKeyFilter.m in Sources */, D06BB547D59D183FD1DDD84DEBAC9EE8 /* SDWebImageCacheSerializer.m in Sources */, 1BC44E2FDD197D5210A23C9CCF1A906B /* SDWebImageCompat.m in Sources */, 31DC2EC78AD1F8241AE6051EF9E73B0A /* SDWebImageDefine.m in Sources */, 752822FE3F5092322D18FEC4533B79A9 /* SDWebImageDownloader.m in Sources */, 74E069F8C9E22C0E37F261A5AB03A613 /* SDWebImageDownloaderConfig.m in Sources */, 32F2B91621A2F8F9AD7C8E2B224D73F6 /* SDWebImageDownloaderDecryptor.m in Sources */, AA1EA8F0F0470F1596B1FFA58ABF3375 /* SDWebImageDownloaderOperation.m in Sources */, 00DAE48C9A4FBCD1FCAA922CA57B45F9 /* SDWebImageDownloaderRequestModifier.m in Sources */, 2F6D9BEA582A2DBB70A6C3B2FC2DB91E /* SDWebImageDownloaderResponseModifier.m in Sources */, 425C9EA28FBEB7F7FC09A3F4A88C5955 /* SDWebImageError.m in Sources */, CFF8D1A5E4C2097EF05E1021FE112886 /* SDWebImageIndicator.m in Sources */, 97235408E59E16C18B6BDA1D29E1CB26 /* SDWebImageManager.m in Sources */, 1754DD5511A7BF462B116F70B0D4006A /* SDWebImageOperation.m in Sources */, 906DCE66CD5BD236081D468616199BB7 /* SDWebImageOptionsProcessor.m in Sources */, 0F1D0F5DCC8C94A4C684DF846D14F436 /* SDWebImagePrefetcher.m in Sources */, 3187FF0C251D1B78BE87F64F6F6E944A /* SDWebImageTransition.m in Sources */, A92AB5E65CA85947368E46E6627F1BFB /* UIButton+WebCache.m in Sources */, 6F3637EE643EABB1DE9212EA68649A64 /* UIColor+SDHexString.m in Sources */, E4F1B478580D6D7328BC29607BDE46F6 /* UIImage+ExtendedCacheData.m in Sources */, 08D50C5AC969A3701B6F9137CF3A10F1 /* UIImage+ForceDecode.m in Sources */, C2840BF1950FF7EE2DCD6D55F768A49C /* UIImage+GIF.m in Sources */, 596180E0EC9F46D12BA840DC4AA62659 /* UIImage+MemoryCacheCost.m in Sources */, 694B8697854A776E32032999B2EF1FEA /* UIImage+Metadata.m in Sources */, BCDC1E1D46DD124B5726A064D2EE66A3 /* UIImage+MultiFormat.m in Sources */, 06C4E233E7977DB81A24482E69B2D7D7 /* UIImage+Transform.m in Sources */, FCEE5BD645E95FF55468C4AB6D17CFDA /* UIImageView+HighlightedWebCache.m in Sources */, 38938E604A7D708E6378A44063EF3512 /* UIImageView+WebCache.m in Sources */, 616A8338C42FB01748DF1BDDA944858D /* UIView+WebCache.m in Sources */, 97385A64CA020489951EF769392C6DCF /* UIView+WebCacheOperation.m in Sources */, 74C474676C69A80BEC29B0F55FDF4D19 /* UIView+WebCacheState.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 2A77BB83B89C22992F39A72E779418C7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 69B92D1883A2D2069444FAE5AAE6272A /* CocoaAsyncSocket-dummy.m in Sources */, 138463D749696B86F3324AC0882A64EB /* GCDAsyncSocket.m in Sources */, 08495F2DAFA36769FC16758CFDAC1F67 /* GCDAsyncUdpSocket.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 3DC320636C5D7E3B8EBCD26375752456 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 2FEAE6F6465F609BC48C5F020306A6C5 /* Collections+CustomInjection.m in Sources */, 2AD6EE04D5EC7568C100BEE596D4C545 /* NSArray+TyphoonManualEnumeration.m in Sources */, 695E3BEA0C9766D4974998CF17C090BD /* NSDictionary+CustomInjection.m in Sources */, D51E06FD6284B91EAC91FD874C101ABF /* NSInvocation+TCFCustomImplementation.m in Sources */, FBCE4760C09733E7FBE20D075564D30F /* NSInvocation+TCFInstanceBuilder.m in Sources */, D1D1BC57D17B127642A4A4DAFE50A033 /* NSInvocation+TCFUnwrapValues.m in Sources */, D49896323350314AAEE3F22E1556C3D0 /* NSInvocation+TCFWrapValues.m in Sources */, 7A95ACE7157BEA7C34CE60ECCCD73F1C /* NSLayoutConstraint+TyphoonOutletTransfer.m in Sources */, 33BE7CF4354A6753F1CAB84BEC9F0575 /* NSMethodSignature+TCFUnwrapValues.m in Sources */, F9FE542AFAA108FFD4F546ADBF6690F1 /* NSNullTypeConverter.m in Sources */, 1E4A2A24D1FA36114E4A37633FC546D3 /* NSObject+DeallocNotification.m in Sources */, 4BE76E020E0B05ADDB89CCA00CFB9933 /* NSObject+PropertyInjection.m in Sources */, A5F84EDD2763BADFD187D4E4C4043B95 /* NSObject+TyphoonIntrospectionUtils.m in Sources */, 2FDCF9468E03C9B56AB0F7740669B4F5 /* NSValue+TCFUnwrapValues.m in Sources */, 1A5A36F12519F4D3B9A2502D3A733381 /* Typhoon-dummy.m in Sources */, 07C6B68367DDC6BCD78E44FFBCB8F379 /* TyphoonAbstractDetachableComponentFactoryPostProcessor.m in Sources */, 627357C7FD3404F2ABFA952B05FECE95 /* TyphoonAbstractInjection.m in Sources */, B86B6B95CC8EE542367125BF22968812 /* TyphoonAssembly.m in Sources */, 812856A2F30E87464C6509362FA23ECA /* TyphoonAssemblyAccessor.m in Sources */, 809CF3FAEE0A4B5157D245F04BFEF46A /* TyphoonAssemblyAdviser.m in Sources */, DD256ACFDD49A35B9747419FFAB3A287 /* TyphoonAssemblyBuilder.m in Sources */, 6720FB4D7E520BC198713915B63D0369 /* TyphoonAssemblyBuilder+PlistProcessor.m in Sources */, A451C30BD205FCAE86B24C0BF35ED465 /* TyphoonAssemblyDefinitionBuilder.m in Sources */, C237DF03B34507F0E0E883D8F7D242BB /* TyphoonAssemblyPropertyInjectionPostProcessor.m in Sources */, 83DD7E3A598132752EDA97173C87D115 /* TyphoonAssemblySelectorAdviser.m in Sources */, 07BC2019BAA4D31E62D7624C8E4F22E8 /* TyphoonBlockComponentFactory.m in Sources */, BFBC117B8B5FE76AECB2448980618552 /* TyphoonBlockDefinition.m in Sources */, B562AF7AAA4ACBCB972C87C4A5150D12 /* TyphoonBlockDefinition+InstanceBuilder.m in Sources */, 67A2E8F472D342E94FAA643110E9E184 /* TyphoonBlockDefinitionController.m in Sources */, 0510DC02DA8CA59E8F2E373AF813BDB6 /* TyphoonBundledImageTypeConverter.m in Sources */, 0C2B2878E7CF92392FFBFBD12C748255 /* TyphoonBundleResource.m in Sources */, 93E07C2516E34149D3E8946A095F41ED /* TyphoonCallStack.m in Sources */, 259D1784222B56F16D06111003ABD61C /* TyphoonCircularDependencyTerminator.m in Sources */, 19AC17E22E91595F6E0D5FF22AF7E834 /* TyphoonCollaboratingAssembliesCollector.m in Sources */, 42DC6F6F01A9526A6A9687A6279E928B /* TyphoonCollaboratingAssemblyPropertyEnumerator.m in Sources */, AEF7F926931CDC5DF0FA40597840B792 /* TyphoonCollaboratingAssemblyProxy.m in Sources */, 6C2605B3C710A00D92D6D5059642EDDA /* TyphoonColorConversionUtils.m in Sources */, 8674138091C088977F9F531E5A61384F /* TyphoonComponentFactory.m in Sources */, 0B222396072B294B46CBB818B208CC48 /* TyphoonComponentFactory+InstanceBuilder.m in Sources */, FE314B3711980149B7078A3606250317 /* TyphoonComponentFactory+Storyboard.m in Sources */, 09E7EED01CBCF945678E3D23B23EC0DD /* TyphoonConfigPostProcessor.m in Sources */, 948E07C0F0FD61BD378ED66D5565D40F /* TyphoonDefinition.m in Sources */, C051A259A6CC3FF96D942F78A70AD43D /* TyphoonDefinition+Config.m in Sources */, A3FD061F6162EC8CC95E8523C4F8E414 /* TyphoonDefinition+InstanceBuilder.m in Sources */, 78FF43FC286BBB9D9B0D6AFADC6F0916 /* TyphoonDefinition+Option.m in Sources */, E4E86D179162DB63010C0D8D68CF08FB /* TyphoonDefinition+Storyboard.m in Sources */, 2B0D8ABFCA7A2A99F4E3A2F35820DAAA /* TyphoonDefinitionNamespace.m in Sources */, 6BF2FA3FD858A08305204D1A39AC6AB5 /* TyphoonDefinitionRegisterer.m in Sources */, 04743C039FBAF096D2EFAF6CBBA24BE0 /* TyphoonFactoryAutoInjectionPostProcessor.m in Sources */, 000A3A1711489B7F1981551FB9D5FF42 /* TyphoonFactoryDefinition.m in Sources */, DB6EF4B473D70F75543FA047C10E1178 /* TyphoonFactoryPropertyInjectionPostProcessor.m in Sources */, 93FB8EE5015052DF64C1503CEB3B5233 /* TyphoonGlobalConfigCollector.m in Sources */, 09C8A4EE1CC2BC62C3BC5EAC4F9E3BFD /* TyphoonInject.m in Sources */, 5F60001126438149B722B4CB58B32CC5 /* TyphoonInjectedObject.m in Sources */, 6CDB988A83D6057DEBDEE651EAB17FB9 /* TyphoonInjectionByCollection.m in Sources */, 4F35842E1298A391EC638FE90727CC31 /* TyphoonInjectionByComponentFactory.m in Sources */, BBF48D90A3AC8570E5336E819EFE2F0A /* TyphoonInjectionByConfig.m in Sources */, 1CB52E450D48ACF5A2F1232D03755A1D /* TyphoonInjectionByCurrentRuntimeArguments.m in Sources */, 92B43E6B9457B4CAC5D92E7576C6D137 /* TyphoonInjectionByDictionary.m in Sources */, 47987D5D284981A7B1FDA8EDAD4EA80E /* TyphoonInjectionByFactoryReference.m in Sources */, 0F5ACCF4A4F36FAC65E045A9F87E3003 /* TyphoonInjectionByObjectFromString.m in Sources */, 0E391D08B6652D83393D3E86C70B9BEB /* TyphoonInjectionByObjectInstance.m in Sources */, 4C4C5CBDDABB5F5850DFE69DB34F6AD7 /* TyphoonInjectionByReference.m in Sources */, 10F5D29724DC5FC71BFB33A0B6EB3D6B /* TyphoonInjectionByRuntimeArgument.m in Sources */, CE2CD6090ADB66ED21B0800C527D7B02 /* TyphoonInjectionByType.m in Sources */, 550E329852F1C92D6E70854088B4D5D8 /* TyphoonInjectionContext.m in Sources */, 871E0CD2D0694414A2C0C801E88C93BF /* TyphoonInjectionDefinition.m in Sources */, C814E15114F4A34E55A3C23173F05537 /* TyphoonInjections.m in Sources */, DDA49C713DF9E0C1A9B1AD6A6F1DE610 /* TyphoonIntrospectionUtils.m in Sources */, A4360119EC4A3CECAE7D5B4288F0FED4 /* TyphoonJsonStyleConfiguration.m in Sources */, C1C54E804EC953B610851FBD38319CC6 /* TyphoonLoadedView.m in Sources */, AD0342BE54652F978FD313F99B10EB84 /* TyphoonMemoryManagementUtils.m in Sources */, C2DE26C06C3E3FE7C2D67DFF8F1B4565 /* TyphoonMethod.m in Sources */, 71E3627F3118542E4CF67F2F67621BD7 /* TyphoonMethod+InstanceBuilder.m in Sources */, 196AAABF9ECA51DE0971598C883B4834 /* TyphoonNibLoader.m in Sources */, 8042F6C17A7801DC10A84A74341EA593 /* TyphoonNSNumberTypeConverter.m in Sources */, C449FE8D987CA3477F9F9272BF091CCE /* TyphoonNSURLTypeConverter.m in Sources */, AAA6AF968C3E92D1BCCBAD27C696628A /* TyphoonOptionMatcher.m in Sources */, 5146309CBB46C9B12CC1648AF02C3A20 /* TyphoonParentReferenceHydratingPostProcessor.m in Sources */, 716F02FA5B659533261BD4BEE3695C1C /* TyphoonPassThroughTypeConverter.m in Sources */, 02BDC4A577B91415561618A54DCB721E /* TyphoonPatcher.m in Sources */, 6553A191C05881C622793B5DD6D312D0 /* TyphoonPathResource.m in Sources */, 63C0FC58D691FE32021C980B486A4FE7 /* TyphoonPlistStyleConfiguration.m in Sources */, D92622FB36407A6E91CB2509480A7F89 /* TyphoonPreattachedComponentsRegisterer.m in Sources */, 22C00E6FDBCBBEB33AE6CDC8DFA60571 /* TyphoonPrimitiveTypeConverter.m in Sources */, 521D285E66EFF69011341B410DE59767 /* TyphoonPropertyStyleConfiguration.m in Sources */, 7C28695EB07687D1CC58A6B130EEBA54 /* TyphoonReferenceDefinition.m in Sources */, 919576C1B49D8D1790FB53BBDC0B289E /* TyphoonRuntimeArguments.m in Sources */, 0DF1C43C7B27B093F8BDF7B951233D19 /* TyphoonSelector.m in Sources */, 7AD50450C1AC9BC993C2911D83D14146 /* TyphoonStackElement.m in Sources */, 2CFFDBFE764BA3B8B0B878BC3A0E7083 /* TyphoonStartup.m in Sources */, D03A07BAF17F4E9CF1D2D3401AB1CF4B /* TyphoonStoryboard.m in Sources */, C8FED80BA5A32BEA3BE00F5359153540 /* TyphoonStoryboardDefinition.m in Sources */, 3ACE99697BE48783D60907A57A5F2FF1 /* TyphoonStoryboardDefinitionContext.m in Sources */, 6E95C1B16744B10481BFAAF67AA8D5F7 /* TyphoonStoryboardProvider.m in Sources */, 0298EAC75B6300AD806B86273DE13278 /* TyphoonStoryboardResolver.m in Sources */, 4F599FFE9C09DD9532BD46E758387116 /* TyphoonSwizzlerDefaultImpl.m in Sources */, 7434021F1AFA292EBE7A85C66AFD33AC /* TyphoonTestUtils.m in Sources */, 56B73AB5484B65F2C078198B0775AB1D /* TyphoonTypeConversionUtils.m in Sources */, F40E7796D3A7339E16757A9B9A6A46E7 /* TyphoonTypeConverterRegistry.m in Sources */, 5D3BF2C909D103968F9FCBC99918728D /* TyphoonTypeDescriptor.m in Sources */, 7CBDB5FECCE29FAD4BB0D8E763FFDE83 /* TyphoonUIColorTypeConverter.m in Sources */, A1BD95BC04A46F6587569C6291351595 /* TyphoonViewControllerFactory.m in Sources */, C029772A70ECFC78C192D0DED7EB10A5 /* TyphoonViewControllerInjector.m in Sources */, C6E08EB55D40EC7D87E27012DA80C041 /* TyphoonViewControllerNibResolver.m in Sources */, 9993EEC4BDD2BB2437898147FCCE0B39 /* TyphoonViewHelpers.m in Sources */, C5B5337075E0DB3B2369B4CF7ED68479 /* TyphoonWeakComponentsPool.m in Sources */, 56977A714C1767541B64A6D00C7BA62D /* UIResponder+TyphoonOutletTransfer.m in Sources */, D55F6AFEF9DB4F474DACB788A627AAC9 /* UIView+TyphoonDefinitionKey.m in Sources */, 12BE89AC12766C6CE239FD4EA3543599 /* UIView+TyphoonOutletTransfer.m in Sources */, 98BA98A7102962139D85FF53B6F66E66 /* UIViewController+TyphoonStoryboardIntegration.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 51734856874874E0CCB9EFBBA0B96F3F /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( E25B7F7ED64E459D6BA4E9FA1B812370 /* MKAnnotationView+RACSignalSupport.m in Sources */, A826A5E0505880B690A0B1877D27CF17 /* NSArray+RACSequenceAdditions.m in Sources */, 3FF9873BBEDD1C8AA6EC43EBCB23D060 /* NSData+RACSupport.m in Sources */, 1444DFD1492DECA5C8C0760EF0FB6CA6 /* NSDictionary+RACSequenceAdditions.m in Sources */, A4CD275DADB3551201C2A05AD4BB269E /* NSEnumerator+RACSequenceAdditions.m in Sources */, F5A6806DA01AEDEA925EF7F6F42A1BA5 /* NSFileHandle+RACSupport.m in Sources */, 1839437DDEA6EB16A5239F19484FDC2F /* NSIndexSet+RACSequenceAdditions.m in Sources */, F2DAC1DEF200EB8CBBF570272D86CA51 /* NSInvocation+RACTypeParsing.m in Sources */, F3604694FDE5476E5AA37DFDB131C7C8 /* NSNotificationCenter+RACSupport.m in Sources */, 9027CF29E44F82D4CE33CB554C662F1B /* NSObject+RACDeallocating.m in Sources */, 01AD9C4D5168F89B9844CB3E46072DE7 /* NSObject+RACDescription.m in Sources */, 8E596CBDA745237A010AE6C43429630D /* NSObject+RACKVOWrapper.m in Sources */, 8BC4CE9F36D19A09404A29C66145AF28 /* NSObject+RACLifting.m in Sources */, 822F14A51D26702C2C3CDE7FC6FD4559 /* NSObject+RACPropertySubscribing.m in Sources */, 1C6A9BE75AF46B08D04DEBB8DA4D687A /* NSObject+RACSelectorSignal.m in Sources */, B312C7154719371C49BCF8055BCC1BFF /* NSOrderedSet+RACSequenceAdditions.m in Sources */, 9CB0C910E0950794AF58F2F24D1D9D74 /* NSSet+RACSequenceAdditions.m in Sources */, 6D9949D8FE86A2D522CB6C880B8E63CD /* NSString+RACKeyPathUtilities.m in Sources */, 5F8F021870002E9A2776909ACB965E3E /* NSString+RACSequenceAdditions.m in Sources */, C1CE7DDDEF51510CEDBE3B0C57A49FA3 /* NSString+RACSupport.m in Sources */, 913AD2CA2F03CE4F360AB192307AA9BB /* NSURLConnection+RACSupport.m in Sources */, 31E03594824B9C1868700CB719A49612 /* NSUserDefaults+RACSupport.m in Sources */, B4A055DE2DAA83775FEAD725E7E34F13 /* RACArraySequence.m in Sources */, C45BDBA1768C56781E48457C6543F6B2 /* RACBehaviorSubject.m in Sources */, 255702D1853E23E30B6847F6A26402F9 /* RACBlockTrampoline.m in Sources */, 6D316F209F073EEEC9898B2DED020AF8 /* RACChannel.m in Sources */, C21BB4F85EC11E3A16DB3EFD10168CD9 /* RACCommand.m in Sources */, B3BAE2216F20F91DBC4C3E3B105E077D /* RACCompoundDisposable.m in Sources */, A3716B6B67727A527D76218D7A8C4C0B /* RACCompoundDisposableProvider.d in Sources */, 6215CAAC4B72DECED49E706D88C3B66D /* RACDelegateProxy.m in Sources */, 73A70EE2C9C9FC76C6495945F3F0DC4D /* RACDisposable.m in Sources */, F1454467D8C33378127057476591C1AF /* RACDynamicSequence.m in Sources */, 89CAFC43896C8BF254CDEAEF1298D287 /* RACDynamicSignal.m in Sources */, E96F37E3D1E77EB2349FF06BD82E95E9 /* RACEagerSequence.m in Sources */, 66187A77817A27C7EAA46CA9E1183C67 /* RACEmptySequence.m in Sources */, 2DD65DE2BC1D3B6536ECC06F2BB1CB26 /* RACEmptySignal.m in Sources */, D423726E8FA79619214170CEE328676A /* RACErrorSignal.m in Sources */, F341B1F0FEDE67D2DB40B5C5D854DA8A /* RACEvent.m in Sources */, 8D082ED05C36EDF278A1065FBE114F38 /* RACEXTRuntimeExtensions.m in Sources */, 50559EFD7F12C78B1448D757DCC81218 /* RACGroupedSignal.m in Sources */, 4FE0E785F97144E411FCE8F30C51F7EA /* RACImmediateScheduler.m in Sources */, 9838965B6E76EAC0A8EC90802D1A064B /* RACIndexSetSequence.m in Sources */, BF7E2E36C374B7B1CD0A5332F2A9B26E /* RACKVOChannel.m in Sources */, 244415898475C361F53B52BC1A34407E /* RACKVOProxy.m in Sources */, CF26C843968B8CBFF3931E82CA77FB81 /* RACKVOTrampoline.m in Sources */, 054C3B2C429E6F3BDCDA2A480D917F51 /* RACMulticastConnection.m in Sources */, 4ED8D4C89D65CD9BAFF98BA1EA8543BE /* RACPassthroughSubscriber.m in Sources */, FA4D59453FAEF4399232AB1CF9ABDDBD /* RACQueueScheduler.m in Sources */, 4F2506F20F6E778B71CC643C0C76F805 /* RACReplaySubject.m in Sources */, CB7FD8FA81234BF9CC43527FE6260F62 /* RACReturnSignal.m in Sources */, B92BD83C69F10179ECCBF07FE6132768 /* RACScheduler.m in Sources */, 38007D8B2D7B055D75C486183F0EADF3 /* RACScopedDisposable.m in Sources */, 3BFA92648AC1313CFD040750ACE5D0C1 /* RACSequence.m in Sources */, 1A51CF9668347C9AB7F0DA8A9D149399 /* RACSerialDisposable.m in Sources */, 0E8A6DC318277807A45CB21C40672C3F /* RACSignal.m in Sources */, 3C2E1F3AF1E008F5610EDBB2258CA617 /* RACSignal+Operations.m in Sources */, A9071F54289564382DCD37D03E09EA0A /* RACSignalProvider.d in Sources */, 207791430D1E92DF86EA8E7CE685BE30 /* RACSignalSequence.m in Sources */, 9ADB69DEACB5B811F3CBF85ABF4C0F10 /* RACStream.m in Sources */, 585920A1A37B315FCA853A232773CC12 /* RACStringSequence.m in Sources */, AEB8926360324E2A84020EAD714E33E4 /* RACSubject.m in Sources */, 0DB61F661491104AF5F8778F810C37D4 /* RACSubscriber.m in Sources */, 7E87D589F135093C179E571A12242622 /* RACSubscriptingAssignmentTrampoline.m in Sources */, A50833B08706004695F22D20FB258019 /* RACSubscriptionScheduler.m in Sources */, 4BCA5F8C6F0C73C76E656BA0E3EA858F /* RACTargetQueueScheduler.m in Sources */, 85C08D3AEAC2C12FB06579C326550982 /* RACTestScheduler.m in Sources */, 45A2252FE9A80B5B59B021395E19EE0C /* RACTuple.m in Sources */, ABDF2546C46AA0359E468651D3CBF26F /* RACTupleSequence.m in Sources */, C1ED07F75CD8C2D19DA79D96EA77F802 /* RACUnarySequence.m in Sources */, 8D07F8CA69CF8A20CBC86823852AE944 /* RACUnit.m in Sources */, F775F679F321E234A469F54310116E6D /* RACValueTransformer.m in Sources */, 8A0E94F87B74EB4E7C326ABB6FD7BF2C /* ReactiveObjC-dummy.m in Sources */, 80E157CBA7AE6BD79E34E063F46ED57C /* UIActionSheet+RACSignalSupport.m in Sources */, D397F3D9E12E75959BB21782CBFD8755 /* UIAlertView+RACSignalSupport.m in Sources */, 445C3272BC1602F3CF1B140BC2A0B39C /* UIBarButtonItem+RACCommandSupport.m in Sources */, 25C3A2BA3CEF4D94C5C34CD8FE3FFAE2 /* UIButton+RACCommandSupport.m in Sources */, 009A101C6D6749EEE990C935A028904B /* UICollectionReusableView+RACSignalSupport.m in Sources */, FB1D858F09AAF47F96795E1AEAC09A19 /* UIControl+RACSignalSupport.m in Sources */, 42735B679585CBE859585C298BB91FC6 /* UIControl+RACSignalSupportPrivate.m in Sources */, 94825DA7AAEA8838DD00AE6C2FA11124 /* UIDatePicker+RACSignalSupport.m in Sources */, 54C6DD71F9BA9CA676096215DDE74996 /* UIGestureRecognizer+RACSignalSupport.m in Sources */, DF1F2CC29D9871EF48FCD47055CB2A59 /* UIImagePickerController+RACSignalSupport.m in Sources */, 08E8B7ECC821E18E4B5B4F746E7337CD /* UIRefreshControl+RACCommandSupport.m in Sources */, 888E7F1F8876AF0CF9EDA4A866E376AD /* UISegmentedControl+RACSignalSupport.m in Sources */, AFBA4DFDAFB6506D5DC862AFB1B4347B /* UISlider+RACSignalSupport.m in Sources */, 73574D4AAB1219ED412B3D2A6D1E321F /* UIStepper+RACSignalSupport.m in Sources */, 35F8F16617EA4D58688C0633DF1ED887 /* UISwitch+RACSignalSupport.m in Sources */, 80F35FE6EC89AAC77F968AC86618E92A /* UITableViewCell+RACSignalSupport.m in Sources */, 9DA8BF5E1F62665192ED18B27ADD0FBE /* UITableViewHeaderFooterView+RACSignalSupport.m in Sources */, 05F22F8B6A39ADEE5F919070E6B2B869 /* UITextField+RACSignalSupport.m in Sources */, BF7D73BC31ED41C8C87A7AD8E615BA90 /* UITextView+RACSignalSupport.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 69B48275140E3148722751E798D8DF26 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( F432C2EF2FDAE1EA39A56AA772932A38 /* NSInvocation+OCMAdditions.m in Sources */, 3DEB0926F884FA92604800641EF8BAC6 /* NSMethodSignature+OCMAdditions.m in Sources */, 17D77D58509A819B927EFD3454361B4D /* NSNotificationCenter+OCMAdditions.m in Sources */, CD2D5C2B71BF29BC796199C6A4960927 /* NSObject+OCMAdditions.m in Sources */, B448686E41D49FAC044634DE4BACBF49 /* NSValue+OCMAdditions.m in Sources */, A9031D4C7A39A8F0CC9B4BB38D378A33 /* OCClassMockObject.m in Sources */, 9C05A169911849DC9E4DAFA0290A9100 /* OCMArg.m in Sources */, 3B46F3921BE0E2CCB23E716DD6D41D6D /* OCMArgAction.m in Sources */, 026C6439062428052B58FA8D41877DB1 /* OCMBlockArgCaller.m in Sources */, 8E0AEE69E2DFB3EC3358444351D332BD /* OCMBlockCaller.m in Sources */, 4775DBD4961BF6BA60BA6ADECCCF03DA /* OCMBoxedReturnValueProvider.m in Sources */, 1FD7A52353875571B01C3E7FD9845744 /* OCMConstraint.m in Sources */, F5E9B7AD93F258223E150D81EC68291A /* OCMExceptionReturnValueProvider.m in Sources */, 3AECFF92CB2DDFE798511330B5E8D704 /* OCMExpectationRecorder.m in Sources */, 2D12D093F3425B423613BB39DC40A72A /* OCMFunctions.m in Sources */, EB2B34DC4B6CA9FDEAF8EB5964D4EE3B /* OCMIndirectReturnValueProvider.m in Sources */, 03A1468408B50A609EF3669C178B99D4 /* OCMInvocationExpectation.m in Sources */, 6CEAFC9DBB829A1A09338FCC0E9072D2 /* OCMInvocationMatcher.m in Sources */, 2EC9A7EDF03D4C66CB4DA9F65E1AC35B /* OCMInvocationStub.m in Sources */, A04BCE28D4B7FB41973BADA1BC666B7B /* OCMLocation.m in Sources */, E07A92468276197EE7EFACE1A4C7D456 /* OCMMacroState.m in Sources */, F0A541AAA195CE1E0DB03048E621CC0D /* OCMNonRetainingObjectReturnValueProvider.m in Sources */, 777DF7EE911DE5E8A67933FF88906269 /* OCMNotificationPoster.m in Sources */, B244D4AF0F8115F666321FE05AAFAE75 /* OCMObjectReturnValueProvider.m in Sources */, D94824EC234BFBFF814D88F9DD042BEA /* OCMObserverRecorder.m in Sources */, 7C9155F50E0008C9C19C443D4257818A /* OCMock-dummy.m in Sources */, F19558DCCE275BD72B143F8ACE45E7E0 /* OCMockObject.m in Sources */, 52D27AEA262A912CA0A4F72D1BD32586 /* OCMPassByRefSetter.m in Sources */, 3C0A1F9E0F5FDD72150F8A21A4672935 /* OCMQuantifier.m in Sources */, 4E104BAD7705D8D5D9F45D41535914EE /* OCMRealObjectForwarder.m in Sources */, A6B6F63BCC3A8BE45E9D30DF50935E0D /* OCMRecorder.m in Sources */, 3025FD4C72722EDAA7B5778B22735B62 /* OCMStubRecorder.m in Sources */, 5B95C0042EA30E36A37E4F686F05DE96 /* OCMVerifier.m in Sources */, EE8AE70BAD73F5BE3689FA838B23AFB7 /* OCObserverMockObject.m in Sources */, 8A5D97580D71011B24EB2F8394C021A3 /* OCPartialMockObject.m in Sources */, 7A2A5EFE8571CB8A7E354E536997AD1B /* OCProtocolMockObject.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 75A6069EC19BC1F48B6A153D3EA84633 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 820743F98B0FA9477A3A8C9E3E4E7DA3 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( B17D5FAAC4B83F94F3841700C4FADE59 /* Pods-iOS-Network-Stack-Dive-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 8839AD11D45758F09C71D584863AF55C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 20199A7A56B3DF111724477B175701D0 /* Pods-iOS-Network-Stack-DiveTests-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; 8EC01F2F884A8FC99A6F71878A870A29 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 6E261D72B6817096F7A6E136C3B32A9F /* Reachability.m in Sources */, A8E58BF4665F9CC4142EA41D80CD6E17 /* Reachability-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; CD5D9934A10A00C27F600B9FC3CA2A18 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( EB27DEF6CC7E1606E358166B56931C05 /* NSObject+YYModel.m in Sources */, 2DB0021F9583F06D433E0EA01CC4BD65 /* YYClassInfo.m in Sources */, CF96B3FEA3AEFCF6137DD7A22D92569E /* YYModel-dummy.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; DA0B6A6F9B3EDF226BF081DAC7E777E7 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 8FF7B6477BFA6E6ABA168E1417291D5F /* MASCompositeConstraint.m in Sources */, DF2B15402CE105F5A8CE48BBDCFFD5DD /* MASConstraint.m in Sources */, F6D1C960368EB1E067ABD0BFF707FC56 /* MASConstraintMaker.m in Sources */, C9E19D164C26414115CC969ED9A303C1 /* MASLayoutConstraint.m in Sources */, 56E800EB3B2BE8AE0BA45A30974D7920 /* Masonry-dummy.m in Sources */, E930A5612DC6D120BE040AD17C6D1BCD /* MASViewAttribute.m in Sources */, C2FE60A10C792613E45031AE6E851ECB /* MASViewConstraint.m in Sources */, 5B08596E856E4CC2F34A8A2372F9F764 /* NSArray+MASAdditions.m in Sources */, C8EC35DFB0945DBE2F2FF9ECFE6D9711 /* NSLayoutConstraint+MASDebugAdditions.m in Sources */, D788BA4B9E8186271BA75CA52B30502C /* View+MASAdditions.m in Sources */, C857B8D2D0BAA5A8A764F9E1C4B85807 /* ViewController+MASAdditions.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 0843473CB7CB6169AC7EF3C5B1319539 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = CocoaAsyncSocket; target = 6083682834ABE0AE7BD1CBF06CADD036 /* CocoaAsyncSocket */; targetProxy = E08BBA5114D60374EFEC60DCE34CDCFC /* PBXContainerItemProxy */; }; 20D3ECBF6436731DFBDB894E0C8F9909 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = DZNEmptyDataSet; target = F1BCD9702276377FB5B3BDB6EAF709D7 /* DZNEmptyDataSet */; targetProxy = B1BB2C6EA80EE90466DC3C902622C7B1 /* PBXContainerItemProxy */; }; 22A8C4276A99677AAA69DE811FB63204 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = YYModel; target = 84B44807A12996D487A4A591A481D6A0 /* YYModel */; targetProxy = 9B705E4CA2FCD950E81AB005C7565BE0 /* PBXContainerItemProxy */; }; 281BE9CFB6988946C189C95F2AACB42C /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = OCMock; target = C260B5A26D3CD54F215E5E39371483B6 /* OCMock */; targetProxy = 5AE1D14F60B6E1194AEB2341B78F3262 /* PBXContainerItemProxy */; }; 3A810513404714D3C5FDEAFFDA8E9F60 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = ReactiveObjC; target = 438B238ACC7DF1178D1BCE1A31983146 /* ReactiveObjC */; targetProxy = 9D64C0FC477F71B3B5C8D7DC509C043A /* PBXContainerItemProxy */; }; 4604B5CB7C8FC5C7D8CE2CEC5AB1FCC8 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Reachability; target = CAA047C0F5E4106F3904E8497FA17F97 /* Reachability */; targetProxy = F948F43B3C4BC87A483963360B1C0612 /* PBXContainerItemProxy */; }; 5BD57B0A429FAA76BD164214280AF573 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = "SDWebImage-SDWebImage"; target = 94CFBA7D633ECA58DF85C327B035E6A3 /* SDWebImage-SDWebImage */; targetProxy = 74C78EC43AE66E5D1A6C66170C1C1D61 /* PBXContainerItemProxy */; }; 729E1AD6D3FC220382AB7C459EDED432 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = SDWebImage; target = 3847153A6E5EEFB86565BA840768F429 /* SDWebImage */; targetProxy = C2CBEAA478071DF837FB759F6C115BF3 /* PBXContainerItemProxy */; }; BA75486E4414284637D76C083139C6E6 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Masonry; target = 55AF53E6C77A10ED4985E04D74A8878E /* Masonry */; targetProxy = A26244063922B6BC1F46487F12D1016C /* PBXContainerItemProxy */; }; DE37340EF49D944251DFC55BA963D9A0 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = Typhoon; target = 6D7BC7A068E56396BBE1DECFD19DDDEB /* Typhoon */; targetProxy = 76D528C13A764727FA8A429BB8ABC642 /* PBXContainerItemProxy */; }; FB2CC1F79B64220967D377FAA987EB70 /* PBXTargetDependency */ = { isa = PBXTargetDependency; name = "Pods-iOS-Network-Stack-Dive"; target = 630AEC5FE2D10DE4AF88009DC6EC819A /* Pods-iOS-Network-Stack-Dive */; targetProxy = C98ED4A91B240BEF49E26C808D0A8F3D /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 003E0FD3ED4718C6130DC62D87688BC0 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D3872D84DFE6C4C6FEBC86D6D30C705B /* SDWebImage.debug.xcconfig */; buildSettings = { CODE_SIGNING_ALLOWED = NO; CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SDWebImage"; IBSC_MODULE = SDWebImage; INFOPLIST_FILE = "Target Support Files/SDWebImage/ResourceBundle-SDWebImage-SDWebImage-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; PRODUCT_NAME = SDWebImage; SDKROOT = iphoneos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; WRAPPER_EXTENSION = bundle; }; name = Debug; }; 16DE79D2C81EA845B72356D2425E6A6B /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = BCE7EC027D6727131CBC3BDA0CA0FA09 /* Typhoon.debug.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/Typhoon/Typhoon-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/Typhoon/Typhoon-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/Typhoon/Typhoon.modulemap"; PRODUCT_MODULE_NAME = Typhoon; PRODUCT_NAME = Typhoon; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 19BAD7A965A06061D8B5500BBA89869B /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 48D7189A91D3148BCB7AB21867EC74DA /* Reachability.release.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/Reachability/Reachability-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/Reachability/Reachability-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/Reachability/Reachability.modulemap"; PRODUCT_MODULE_NAME = Reachability; PRODUCT_NAME = Reachability; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_VERSION = 4.1; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 1F067A52DFCEB046226F5653A048C198 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = C1438D60DB93FD77AC7CEB329AA4D842 /* OCMock.debug.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/OCMock/OCMock-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/OCMock/OCMock-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/OCMock/OCMock.modulemap"; PRODUCT_MODULE_NAME = OCMock; PRODUCT_NAME = OCMock; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 2842FABC035A7A9141B79181D49260FA /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 198BAFBAC2D38ECDB593D0EE2304B03C /* Pods-iOS-Network-Stack-DiveTests.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = "Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACH_O_TYPE = staticlib; MODULEMAP_FILE = "Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests.modulemap"; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PODS_ROOT = "$(SRCROOT)"; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 2D1085CA7BD144CABF012FC10C6C9120 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 918735CA6B8C36A5D220733BC8F45075 /* Masonry.debug.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/Masonry/Masonry-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/Masonry/Masonry-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/Masonry/Masonry.modulemap"; PRODUCT_MODULE_NAME = Masonry; PRODUCT_NAME = Masonry; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 35044A57510DB3F4E442A9C16E980E4A /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 44A289ACE45B5D7FC30203A480801351 /* DZNEmptyDataSet.debug.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet.modulemap"; PRODUCT_MODULE_NAME = DZNEmptyDataSet; PRODUCT_NAME = DZNEmptyDataSet; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 351BFC2CBD4FD5DA556F5FC49A87626F /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D1D5C8198B49A7A43168157C6AAACF8B /* YYModel.debug.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/YYModel/YYModel-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/YYModel/YYModel-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/YYModel/YYModel.modulemap"; PRODUCT_MODULE_NAME = YYModel; PRODUCT_NAME = YYModel; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 44A4B2FB6211B90C1949A44A381F6D1F /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_PREPROCESSOR_DEFINITIONS = ( "POD_CONFIGURATION_RELEASE=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.6; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; STRIP_INSTALLED_PRODUCT = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OPTIMIZATION_LEVEL = "-O"; SWIFT_VERSION = 5.0; SYMROOT = "${SRCROOT}/../build"; }; name = Release; }; 49CEA33268972C89BF290FCBF8F9DFC0 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = C122C87FD2423368DF66F19780CE1593 /* DZNEmptyDataSet.release.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet.modulemap"; PRODUCT_MODULE_NAME = DZNEmptyDataSet; PRODUCT_NAME = DZNEmptyDataSet; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 5FB83460DEB5FBCB496308D2E33BEF59 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = B64CD45769D06F336C347EDF755FC7E0 /* CocoaAsyncSocket.release.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket.modulemap"; PRODUCT_MODULE_NAME = CocoaAsyncSocket; PRODUCT_NAME = CocoaAsyncSocket; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 786EDE8C8AED46F5F3B72B209F6347A6 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 72ADBA2DDCD657373F64C49B51DA5B7D /* Reachability.debug.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/Reachability/Reachability-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/Reachability/Reachability-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/Reachability/Reachability.modulemap"; PRODUCT_MODULE_NAME = Reachability; PRODUCT_NAME = Reachability; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; SWIFT_VERSION = 4.1; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 7942076CD4A8D49815633D58D737B8C3 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 200AD54BC62CB8F66159C905EE9DED00 /* Pods-iOS-Network-Stack-DiveTests.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = "Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACH_O_TYPE = staticlib; MODULEMAP_FILE = "Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests.modulemap"; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PODS_ROOT = "$(SRCROOT)"; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; 9AA28F52D802C6DE6DE3B16C3DEF54FC /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = E6BFAC38D0C1749C0AF02A5825F2ECBB /* OCMock.release.xcconfig */; buildSettings = { CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/OCMock/OCMock-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/OCMock/OCMock-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/OCMock/OCMock.modulemap"; PRODUCT_MODULE_NAME = OCMock; PRODUCT_NAME = OCMock; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; 9F519E5162C0E51D10B7E999E2FD0125 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5811A6AF5EFEA30ABF2E338DE7F3646D /* SDWebImage.release.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/SDWebImage/SDWebImage-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/SDWebImage/SDWebImage-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/SDWebImage/SDWebImage.modulemap"; PRODUCT_MODULE_NAME = SDWebImage; PRODUCT_NAME = SDWebImage; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; A0DC1C2C814704734F83EAB35BB20B21 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "POD_CONFIGURATION_DEBUG=1", "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.6; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; PRODUCT_NAME = "$(TARGET_NAME)"; STRIP_INSTALLED_PRODUCT = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; SYMROOT = "${SRCROOT}/../build"; }; name = Debug; }; B04295D726C1883ADA40A304483D7E33 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = D3872D84DFE6C4C6FEBC86D6D30C705B /* SDWebImage.debug.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/SDWebImage/SDWebImage-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/SDWebImage/SDWebImage-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/SDWebImage/SDWebImage.modulemap"; PRODUCT_MODULE_NAME = SDWebImage; PRODUCT_NAME = SDWebImage; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; B058972BE440356574E6B4081428F17E /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 353C12944450C35AA7A2B53F9A9EB5B8 /* Pods-iOS-Network-Stack-Dive.debug.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = "Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACH_O_TYPE = staticlib; MODULEMAP_FILE = "Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive.modulemap"; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PODS_ROOT = "$(SRCROOT)"; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; C107A6F614E3876FF6318CF358AA4CC9 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 2FEB52E6488D9C7D2B20E87FA516436B /* Typhoon.release.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/Typhoon/Typhoon-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/Typhoon/Typhoon-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/Typhoon/Typhoon.modulemap"; PRODUCT_MODULE_NAME = Typhoon; PRODUCT_NAME = Typhoon; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; D0AB0AEF4014B926FCD853D3AE0A370A /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5973E1C538DF3DFE2D2DCF5EF361B714 /* Masonry.release.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/Masonry/Masonry-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/Masonry/Masonry-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/Masonry/Masonry.modulemap"; PRODUCT_MODULE_NAME = Masonry; PRODUCT_NAME = Masonry; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; D1DAC748EDC478498BB6818CE5B38988 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 5811A6AF5EFEA30ABF2E338DE7F3646D /* SDWebImage.release.xcconfig */; buildSettings = { CODE_SIGNING_ALLOWED = NO; CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SDWebImage"; IBSC_MODULE = SDWebImage; INFOPLIST_FILE = "Target Support Files/SDWebImage/ResourceBundle-SDWebImage-SDWebImage-Info.plist"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; PRODUCT_NAME = SDWebImage; SDKROOT = iphoneos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; WRAPPER_EXTENSION = bundle; }; name = Release; }; D3EC38E5DCF7076CCA37A7888D4A1952 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9240686C7AC135C5EA0942E5C3D224AC /* ReactiveObjC.debug.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/ReactiveObjC/ReactiveObjC-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/ReactiveObjC/ReactiveObjC-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/ReactiveObjC/ReactiveObjC.modulemap"; PRODUCT_MODULE_NAME = ReactiveObjC; PRODUCT_NAME = ReactiveObjC; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; DEF564A26D77FC31338DB6E7544DCD6D /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 2DAE20DF4B8369B96AEB7A6A762F3DF5 /* YYModel.release.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/YYModel/YYModel-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/YYModel/YYModel-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/YYModel/YYModel.modulemap"; PRODUCT_MODULE_NAME = YYModel; PRODUCT_NAME = YYModel; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; E25DDDC7D8E7F5AC4EA1EC435318C1A6 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 0A11CE004B91D55C90C201FD9902D9A8 /* Pods-iOS-Network-Stack-Dive.release.xcconfig */; buildSettings = { ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; CLANG_ENABLE_OBJC_WEAK = NO; "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; INFOPLIST_FILE = "Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MACH_O_TYPE = staticlib; MODULEMAP_FILE = "Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive.modulemap"; OTHER_LDFLAGS = ""; OTHER_LIBTOOLFLAGS = ""; PODS_ROOT = "$(SRCROOT)"; PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; E36E9C44A8F1DEC2F22A4E27D336A543 /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = A9F3496E6CC2C56684ACF7B2579BB765 /* ReactiveObjC.release.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/ReactiveObjC/ReactiveObjC-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/ReactiveObjC/ReactiveObjC-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 8.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/ReactiveObjC/ReactiveObjC.modulemap"; PRODUCT_MODULE_NAME = ReactiveObjC; PRODUCT_NAME = ReactiveObjC; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Release; }; FF7C21C27BB218981AFEAAF82355E7D0 /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9B87846900F219AB28D136A1C7C67730 /* CocoaAsyncSocket.debug.xcconfig */; buildSettings = { "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; CURRENT_PROJECT_VERSION = 1; DEFINES_MODULE = YES; DYLIB_COMPATIBILITY_VERSION = 1; DYLIB_CURRENT_VERSION = 1; DYLIB_INSTALL_NAME_BASE = "@rpath"; ENABLE_MODULE_VERIFIER = NO; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = "Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-prefix.pch"; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = "Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 9.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", "@loader_path/Frameworks", ); MODULEMAP_FILE = "Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket.modulemap"; PRODUCT_MODULE_NAME = CocoaAsyncSocket; PRODUCT_NAME = CocoaAsyncSocket; SDKROOT = iphoneos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; SWIFT_INSTALL_OBJC_HEADER = YES; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; VERSION_INFO_PREFIX = ""; }; name = Debug; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 1CF7CA11A791652D6975B2EDE2FC6719 /* Build configuration list for PBXNativeTarget "DZNEmptyDataSet" */ = { isa = XCConfigurationList; buildConfigurations = ( 35044A57510DB3F4E442A9C16E980E4A /* Debug */, 49CEA33268972C89BF290FCBF8F9DFC0 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 2F9E94B5F79E365095CB33D3D3FCA6A2 /* Build configuration list for PBXNativeTarget "SDWebImage" */ = { isa = XCConfigurationList; buildConfigurations = ( B04295D726C1883ADA40A304483D7E33 /* Debug */, 9F519E5162C0E51D10B7E999E2FD0125 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 35F578C15A7207B829452A13078C0F9C /* Build configuration list for PBXNativeTarget "Reachability" */ = { isa = XCConfigurationList; buildConfigurations = ( 786EDE8C8AED46F5F3B72B209F6347A6 /* Debug */, 19BAD7A965A06061D8B5500BBA89869B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 455F1BBD53E2CF7CA53C22DCBD5B5BF0 /* Build configuration list for PBXNativeTarget "Pods-iOS-Network-Stack-Dive" */ = { isa = XCConfigurationList; buildConfigurations = ( B058972BE440356574E6B4081428F17E /* Debug */, E25DDDC7D8E7F5AC4EA1EC435318C1A6 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { isa = XCConfigurationList; buildConfigurations = ( A0DC1C2C814704734F83EAB35BB20B21 /* Debug */, 44A4B2FB6211B90C1949A44A381F6D1F /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 5192A2BBC2E6464EC36FA5D86309BB74 /* Build configuration list for PBXNativeTarget "SDWebImage-SDWebImage" */ = { isa = XCConfigurationList; buildConfigurations = ( 003E0FD3ED4718C6130DC62D87688BC0 /* Debug */, D1DAC748EDC478498BB6818CE5B38988 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 7C5D55F9FC3A8A30A56D403120D1C06B /* Build configuration list for PBXNativeTarget "CocoaAsyncSocket" */ = { isa = XCConfigurationList; buildConfigurations = ( FF7C21C27BB218981AFEAAF82355E7D0 /* Debug */, 5FB83460DEB5FBCB496308D2E33BEF59 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 9D5D253855B8EC056D584BB71826AF43 /* Build configuration list for PBXNativeTarget "Pods-iOS-Network-Stack-DiveTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 7942076CD4A8D49815633D58D737B8C3 /* Debug */, 2842FABC035A7A9141B79181D49260FA /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; A89A1A0D451C2C02A9188F1F989D5D8F /* Build configuration list for PBXNativeTarget "Typhoon" */ = { isa = XCConfigurationList; buildConfigurations = ( 16DE79D2C81EA845B72356D2425E6A6B /* Debug */, C107A6F614E3876FF6318CF358AA4CC9 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; AAA1F8799DB68036C3BE983C05FAA2C7 /* Build configuration list for PBXNativeTarget "Masonry" */ = { isa = XCConfigurationList; buildConfigurations = ( 2D1085CA7BD144CABF012FC10C6C9120 /* Debug */, D0AB0AEF4014B926FCD853D3AE0A370A /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; D0544D7DC4BCDADC7495D11D44228F4B /* Build configuration list for PBXNativeTarget "YYModel" */ = { isa = XCConfigurationList; buildConfigurations = ( 351BFC2CBD4FD5DA556F5FC49A87626F /* Debug */, DEF564A26D77FC31338DB6E7544DCD6D /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; DDFDACF484D4EC7740167FF70F676380 /* Build configuration list for PBXNativeTarget "OCMock" */ = { isa = XCConfigurationList; buildConfigurations = ( 1F067A52DFCEB046226F5653A048C198 /* Debug */, 9AA28F52D802C6DE6DE3B16C3DEF54FC /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; F3525B72C16A0105CD51405DE75181D5 /* Build configuration list for PBXNativeTarget "ReactiveObjC" */ = { isa = XCConfigurationList; buildConfigurations = ( D3EC38E5DCF7076CCA37A7888D4A1952 /* Debug */, E36E9C44A8F1DEC2F22A4E27D336A543 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; } ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/CocoaAsyncSocket.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/DZNEmptyDataSet.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/Masonry.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/OCMock.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/Pods-iOS-Network-Stack-Dive.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/Pods-iOS-Network-Stack-DiveTests.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/Reachability.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/ReactiveObjC.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/SDWebImage-SDWebImage.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/SDWebImage.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/Typhoon.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/YYModel.xcscheme ================================================ ================================================ FILE: Pods/Pods.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/xcschememanagement.plist ================================================ SchemeUserState CocoaAsyncSocket.xcscheme isShown orderHint 0 DZNEmptyDataSet.xcscheme isShown orderHint 1 Masonry.xcscheme isShown orderHint 2 OCMock.xcscheme isShown orderHint 3 Pods-iOS-Network-Stack-Dive.xcscheme isShown orderHint 4 Pods-iOS-Network-Stack-DiveTests.xcscheme isShown orderHint 5 Reachability.xcscheme isShown orderHint 6 ReactiveObjC.xcscheme isShown orderHint 7 SDWebImage-SDWebImage.xcscheme isShown orderHint 9 SDWebImage.xcscheme isShown orderHint 8 Typhoon.xcscheme isShown orderHint 10 YYModel.xcscheme isShown orderHint 11 SuppressBuildableAutocreation ================================================ FILE: Pods/Reachability/LICENCE.txt ================================================ Copyright (c) 2011-2013, Tony Million. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: Pods/Reachability/README.md ================================================ [![Reference Status](https://www.versioneye.com/objective-c/reachability/reference_badge.svg?style=flat)](https://www.versioneye.com/objective-c/reachability/references) [![build-status](https://github.com/tonymillion/Reachability/actions/workflows/CI.yml/badge.svg)](https://github.com/tonymillion/Reachability/actions) # **WARNING** there have been reports of apps being rejected when Reachability is used in a framework. The only solution to this so far is to rename the class. # Reachability This is a drop-in replacement for Apple's `Reachability` class. It is ARC-compatible, and it uses the new GCD methods to notify of network interface changes. In addition to the standard `NSNotification`, it supports the use of blocks for when the network becomes reachable and unreachable. Finally, you can specify whether a WWAN connection is considered "reachable". *DO NOT OPEN BUGS UNTIL YOU HAVE TESTED ON DEVICE* **BEFORE YOU OPEN A BUG ABOUT iOS6/iOS5 build errors, use Tag 3.2 or 3.1 as they support assign types** ## Requirements Once you have added the `.h/m` files to your project, simply: * Go to the `Project->TARGETS->Build Phases->Link Binary With Libraries`. * Press the plus in the lower left of the list. * Add `SystemConfiguration.framework`. Boom, you're done. ## Examples ### Block Example This sample uses blocks to notify when the interface state has changed. The blocks will be called on a **BACKGROUND THREAD**, so you need to dispatch UI updates onto the main thread. #### In Objective-C ```objc // Allocate a reachability object Reachability* reach = [Reachability reachabilityWithHostname:@"www.google.com"]; // Set the blocks reach.reachableBlock = ^(Reachability*reach) { // keep in mind this is called on a background thread // and if you are updating the UI it needs to happen // on the main thread, like this: dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"REACHABLE!"); }); }; reach.unreachableBlock = ^(Reachability*reach) { NSLog(@"UNREACHABLE!"); }; // Start the notifier, which will cause the reachability object to retain itself! [reach startNotifier]; ``` ### In Swift 3 ```swift import Reachability var reach: Reachability? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Allocate a reachability object self.reach = Reachability.forInternetConnection() // Set the blocks self.reach!.reachableBlock = { (reach: Reachability?) -> Void in // keep in mind this is called on a background thread // and if you are updating the UI it needs to happen // on the main thread, like this: DispatchQueue.main.async { print("REACHABLE!") } } self.reach!.unreachableBlock = { (reach: Reachability?) -> Void in print("UNREACHABLE!") } self.reach!.startNotifier() return true } ``` ### `NSNotification` Example This sample will use `NSNotification`s to notify when the interface has changed. They will be delivered on the **MAIN THREAD**, so you *can* do UI updates from within the function. In addition, it asks the `Reachability` object to consider the WWAN (3G/EDGE/CDMA) as a non-reachable connection (you might use this if you are writing a video streaming app, for example, to save the user's data plan). #### In Objective-C ```objc // Allocate a reachability object Reachability* reach = [Reachability reachabilityWithHostname:@"www.google.com"]; // Tell the reachability that we DON'T want to be reachable on 3G/EDGE/CDMA reach.reachableOnWWAN = NO; // Here we set up a NSNotification observer. The Reachability that caused the notification // is passed in the object parameter [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reachabilityChanged:) name:kReachabilityChangedNotification object:nil]; [reach startNotifier]; ``` #### In Swift 3 ```swift import Reachability var reach: Reachability? func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool { // Allocate a reachability object self.reach = Reachability.forInternetConnection() // Tell the reachability that we DON'T want to be reachable on 3G/EDGE/CDMA self.reach!.reachableOnWWAN = false // Here we set up a NSNotification observer. The Reachability that caused the notification // is passed in the object parameter NotificationCenter.default.addObserver( self, selector: #selector(reachabilityChanged), name: NSNotification.Name.reachabilityChanged, object: nil ) self.reach!.startNotifier() return true } func reachabilityChanged(notification: NSNotification) { if self.reach!.isReachableViaWiFi() || self.reach!.isReachableViaWWAN() { print("Service available!!!") } else { print("No service available!!!") } } ``` ## Tell the world Head over to [Projects using Reachability](https://github.com/tonymillion/Reachability/wiki/Projects-using-Reachability) and add your project for "Maximum Wins!". ================================================ FILE: Pods/Reachability/Reachability.h ================================================ /* Copyright (c) 2011, Tony Million. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import #import //! Project version number for MacOSReachability. FOUNDATION_EXPORT double ReachabilityVersionNumber; //! Project version string for MacOSReachability. FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[]; /** * Create NS_ENUM macro if it does not exist on the targeted version of iOS or OS X. * * @see http://nshipster.com/ns_enum-ns_options/ **/ #ifndef NS_ENUM #define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type #endif extern NSString *const kReachabilityChangedNotification; typedef NS_ENUM(NSInteger, NetworkStatus) { // Apple NetworkStatus Compatible Names. NotReachable = 0, ReachableViaWiFi = 2, ReachableViaWWAN = 1 }; @class Reachability; typedef void (^NetworkReachable)(Reachability * reachability); typedef void (^NetworkUnreachable)(Reachability * reachability); typedef void (^NetworkReachability)(Reachability * reachability, SCNetworkConnectionFlags flags); @interface Reachability : NSObject @property (nonatomic, copy) NetworkReachable reachableBlock; @property (nonatomic, copy) NetworkUnreachable unreachableBlock; @property (nonatomic, copy) NetworkReachability reachabilityBlock; @property (nonatomic, assign) BOOL reachableOnWWAN; +(instancetype)reachabilityWithHostname:(NSString*)hostname; // This is identical to the function above, but is here to maintain //compatibility with Apples original code. (see .m) +(instancetype)reachabilityWithHostName:(NSString*)hostname; +(instancetype)reachabilityForInternetConnection; +(instancetype)reachabilityWithAddress:(void *)hostAddress; +(instancetype)reachabilityForLocalWiFi; +(instancetype)reachabilityWithURL:(NSURL*)url; -(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref; -(BOOL)startNotifier; -(void)stopNotifier; -(BOOL)isReachable; -(BOOL)isReachableViaWWAN; -(BOOL)isReachableViaWiFi; // WWAN may be available, but not active until a connection has been established. // WiFi may require a connection for VPN on Demand. -(BOOL)isConnectionRequired; // Identical DDG variant. -(BOOL)connectionRequired; // Apple's routine. // Dynamic, on demand connection? -(BOOL)isConnectionOnDemand; // Is user intervention required? -(BOOL)isInterventionRequired; -(NetworkStatus)currentReachabilityStatus; -(SCNetworkReachabilityFlags)reachabilityFlags; -(NSString*)currentReachabilityString; -(NSString*)currentReachabilityFlags; @end ================================================ FILE: Pods/Reachability/Reachability.m ================================================ /* Copyright (c) 2011, Tony Million. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #import "Reachability.h" #import #import #import #import #import #import NSString *const kReachabilityChangedNotification = @"kReachabilityChangedNotification"; @interface Reachability () @property (nonatomic, assign) SCNetworkReachabilityRef reachabilityRef; @property (nonatomic, strong) dispatch_queue_t reachabilitySerialQueue; @property (nonatomic, strong) id reachabilityObject; -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags; -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags; @end static NSString *reachabilityFlags(SCNetworkReachabilityFlags flags) { return [NSString stringWithFormat:@"%c%c %c%c%c%c%c%c%c", #if TARGET_OS_IPHONE (flags & kSCNetworkReachabilityFlagsIsWWAN) ? 'W' : '-', #else 'X', #endif (flags & kSCNetworkReachabilityFlagsReachable) ? 'R' : '-', (flags & kSCNetworkReachabilityFlagsConnectionRequired) ? 'c' : '-', (flags & kSCNetworkReachabilityFlagsTransientConnection) ? 't' : '-', (flags & kSCNetworkReachabilityFlagsInterventionRequired) ? 'i' : '-', (flags & kSCNetworkReachabilityFlagsConnectionOnTraffic) ? 'C' : '-', (flags & kSCNetworkReachabilityFlagsConnectionOnDemand) ? 'D' : '-', (flags & kSCNetworkReachabilityFlagsIsLocalAddress) ? 'l' : '-', (flags & kSCNetworkReachabilityFlagsIsDirect) ? 'd' : '-']; } // Start listening for reachability notifications on the current run loop static void TMReachabilityCallback(SCNetworkReachabilityRef target, SCNetworkReachabilityFlags flags, void* info) { #pragma unused (target) Reachability *reachability = ((__bridge Reachability*)info); // We probably don't need an autoreleasepool here, as GCD docs state each queue has its own autorelease pool, // but what the heck eh? @autoreleasepool { [reachability reachabilityChanged:flags]; } } @implementation Reachability #pragma mark - Class Constructor Methods +(instancetype)reachabilityWithHostName:(NSString*)hostname { return [Reachability reachabilityWithHostname:hostname]; } +(instancetype)reachabilityWithHostname:(NSString*)hostname { SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]); if (ref) { id reachability = [[self alloc] initWithReachabilityRef:ref]; return reachability; } return nil; } +(instancetype)reachabilityWithAddress:(void *)hostAddress { SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithAddress(kCFAllocatorDefault, (const struct sockaddr*)hostAddress); if (ref) { id reachability = [[self alloc] initWithReachabilityRef:ref]; return reachability; } return nil; } +(instancetype)reachabilityForInternetConnection { struct sockaddr_in zeroAddress; bzero(&zeroAddress, sizeof(zeroAddress)); zeroAddress.sin_len = sizeof(zeroAddress); zeroAddress.sin_family = AF_INET; return [self reachabilityWithAddress:&zeroAddress]; } +(instancetype)reachabilityForLocalWiFi { struct sockaddr_in localWifiAddress; bzero(&localWifiAddress, sizeof(localWifiAddress)); localWifiAddress.sin_len = sizeof(localWifiAddress); localWifiAddress.sin_family = AF_INET; // IN_LINKLOCALNETNUM is defined in as 169.254.0.0 localWifiAddress.sin_addr.s_addr = htonl(IN_LINKLOCALNETNUM); return [self reachabilityWithAddress:&localWifiAddress]; } +(instancetype)reachabilityWithURL:(NSURL*)url { id reachability; NSString *host = url.host; BOOL isIpAddress = [self isIpAddress:host]; if (isIpAddress) { NSNumber *port = url.port ?: [url.scheme isEqualToString:@"https"] ? @(443) : @(80); struct sockaddr_in address; address.sin_len = sizeof(address); address.sin_family = AF_INET; address.sin_port = htons([port intValue]); address.sin_addr.s_addr = inet_addr([host UTF8String]); reachability = [self reachabilityWithAddress:&address]; } else { reachability = [self reachabilityWithHostname:host]; } return reachability; } +(BOOL)isIpAddress:(NSString*)host { struct in_addr pin; return 1 == inet_aton([host UTF8String], &pin); } // Initialization methods -(instancetype)initWithReachabilityRef:(SCNetworkReachabilityRef)ref { self = [super init]; if (self != nil) { self.reachableOnWWAN = YES; self.reachabilityRef = ref; // We need to create a serial queue. // We allocate this once for the lifetime of the notifier. self.reachabilitySerialQueue = dispatch_queue_create("com.tonymillion.reachability", NULL); } return self; } -(void)dealloc { [self stopNotifier]; if(self.reachabilityRef) { CFRelease(self.reachabilityRef); self.reachabilityRef = nil; } self.reachableBlock = nil; self.unreachableBlock = nil; self.reachabilityBlock = nil; self.reachabilitySerialQueue = nil; } #pragma mark - Notifier Methods // Notifier // NOTE: This uses GCD to trigger the blocks - they *WILL NOT* be called on THE MAIN THREAD // - In other words DO NOT DO ANY UI UPDATES IN THE BLOCKS. // INSTEAD USE dispatch_async(dispatch_get_main_queue(), ^{UISTUFF}) (or dispatch_sync if you want) -(BOOL)startNotifier { // allow start notifier to be called multiple times if(self.reachabilityObject && (self.reachabilityObject == self)) { return YES; } SCNetworkReachabilityContext context = { 0, NULL, NULL, NULL, NULL }; context.info = (__bridge void *)self; if(SCNetworkReachabilitySetCallback(self.reachabilityRef, TMReachabilityCallback, &context)) { // Set it as our reachability queue, which will retain the queue if(SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, self.reachabilitySerialQueue)) { // this should do a retain on ourself, so as long as we're in notifier mode we shouldn't disappear out from under ourselves // woah self.reachabilityObject = self; return YES; } else { #ifdef DEBUG NSLog(@"SCNetworkReachabilitySetDispatchQueue() failed: %s", SCErrorString(SCError())); #endif // UH OH - FAILURE - stop any callbacks! SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL); } } else { #ifdef DEBUG NSLog(@"SCNetworkReachabilitySetCallback() failed: %s", SCErrorString(SCError())); #endif } // if we get here we fail at the internet self.reachabilityObject = nil; return NO; } -(void)stopNotifier { // First stop, any callbacks! SCNetworkReachabilitySetCallback(self.reachabilityRef, NULL, NULL); // Unregister target from the GCD serial dispatch queue. SCNetworkReachabilitySetDispatchQueue(self.reachabilityRef, NULL); self.reachabilityObject = nil; } #pragma mark - reachability tests // This is for the case where you flick the airplane mode; // you end up getting something like this: //Reachability: WR ct----- //Reachability: -- ------- //Reachability: WR ct----- //Reachability: -- ------- // We treat this as 4 UNREACHABLE triggers - really apple should do better than this #define testcase (kSCNetworkReachabilityFlagsConnectionRequired | kSCNetworkReachabilityFlagsTransientConnection) -(BOOL)isReachableWithFlags:(SCNetworkReachabilityFlags)flags { BOOL connectionUP = YES; if(!(flags & kSCNetworkReachabilityFlagsReachable)) connectionUP = NO; if( (flags & testcase) == testcase ) connectionUP = NO; #if TARGET_OS_IPHONE if(flags & kSCNetworkReachabilityFlagsIsWWAN) { // We're on 3G. if(!self.reachableOnWWAN) { // We don't want to connect when on 3G. connectionUP = NO; } } #endif return connectionUP; } -(BOOL)isReachable { SCNetworkReachabilityFlags flags; if(!SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) return NO; return [self isReachableWithFlags:flags]; } -(BOOL)isReachableViaWWAN { #if TARGET_OS_IPHONE SCNetworkReachabilityFlags flags = 0; if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) { // Check we're REACHABLE if(flags & kSCNetworkReachabilityFlagsReachable) { // Now, check we're on WWAN if(flags & kSCNetworkReachabilityFlagsIsWWAN) { return YES; } } } #endif return NO; } -(BOOL)isReachableViaWiFi { SCNetworkReachabilityFlags flags = 0; if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) { // Check we're reachable if((flags & kSCNetworkReachabilityFlagsReachable)) { #if TARGET_OS_IPHONE // Check we're NOT on WWAN if((flags & kSCNetworkReachabilityFlagsIsWWAN)) { return NO; } #endif return YES; } } return NO; } // WWAN may be available, but not active until a connection has been established. // WiFi may require a connection for VPN on Demand. -(BOOL)isConnectionRequired { return [self connectionRequired]; } -(BOOL)connectionRequired { SCNetworkReachabilityFlags flags; if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) { return (flags & kSCNetworkReachabilityFlagsConnectionRequired); } return NO; } // Dynamic, on demand connection? -(BOOL)isConnectionOnDemand { SCNetworkReachabilityFlags flags; if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) { return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && (flags & (kSCNetworkReachabilityFlagsConnectionOnTraffic | kSCNetworkReachabilityFlagsConnectionOnDemand))); } return NO; } // Is user intervention required? -(BOOL)isInterventionRequired { SCNetworkReachabilityFlags flags; if (SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) { return ((flags & kSCNetworkReachabilityFlagsConnectionRequired) && (flags & kSCNetworkReachabilityFlagsInterventionRequired)); } return NO; } #pragma mark - reachability status stuff -(NetworkStatus)currentReachabilityStatus { if([self isReachable]) { if([self isReachableViaWiFi]) return ReachableViaWiFi; #if TARGET_OS_IPHONE return ReachableViaWWAN; #endif } return NotReachable; } -(SCNetworkReachabilityFlags)reachabilityFlags { SCNetworkReachabilityFlags flags = 0; if(SCNetworkReachabilityGetFlags(self.reachabilityRef, &flags)) { return flags; } return 0; } -(NSString*)currentReachabilityString { NetworkStatus temp = [self currentReachabilityStatus]; if(temp == ReachableViaWWAN) { // Updated for the fact that we have CDMA phones now! return NSLocalizedString(@"Cellular", @""); } if (temp == ReachableViaWiFi) { return NSLocalizedString(@"WiFi", @""); } return NSLocalizedString(@"No Connection", @""); } -(NSString*)currentReachabilityFlags { return reachabilityFlags([self reachabilityFlags]); } #pragma mark - Callback function calls this method -(void)reachabilityChanged:(SCNetworkReachabilityFlags)flags { if([self isReachableWithFlags:flags]) { if(self.reachableBlock) { self.reachableBlock(self); } } else { if(self.unreachableBlock) { self.unreachableBlock(self); } } if(self.reachabilityBlock) { self.reachabilityBlock(self, flags); } // this makes sure the change notification happens on the MAIN THREAD dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:kReachabilityChangedNotification object:self]; }); } #pragma mark - Debug Description - (NSString *) description { NSString *description = [NSString stringWithFormat:@"<%@: %p (%@)>", NSStringFromClass([self class]), self, [self currentReachabilityFlags]]; return description; } @end ================================================ FILE: Pods/ReactiveObjC/LICENSE.md ================================================ **Copyright (c) 2012 - 2016, GitHub, Inc.** **All rights reserved.** Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Pods/ReactiveObjC/README.md ================================================ # ReactiveObjC _NOTE: This is legacy introduction to the Objective-C ReactiveCocoa, which is now known as ReactiveObjC. For the updated version that uses Swift, please see [ReactiveCocoa][] or [ReactiveSwift][]_ ReactiveObjC (formally ReactiveCocoa or RAC) is an Objective-C framework inspired by [Functional Reactive Programming][]. It provides APIs for **composing and transforming streams of values**. If you're already familiar with functional reactive programming or know the basic premise of ReactiveObjC, check out the other documentation in this folder for a framework overview and more in-depth information about how it all works in practice. ## New to ReactiveObjC? ReactiveObjC is documented like crazy, and there's a wealth of introductory material available to explain what RAC is and how you can use it. If you want to learn more, we recommend these resources, roughly in order: 1. [Introduction](#introduction) 1. [When to use ReactiveObjC](#when-to-use-reactiveobjc) 1. [Framework Overview][] 1. [Basic Operators][] 1. [Header documentation](ReactiveObjC/) 1. Previously answered [Stack Overflow](https://github.com/ReactiveCocoa/ReactiveCocoa/wiki) questions and [GitHub issues](https://github.com/ReactiveCocoa/ReactiveCocoa/issues?labels=question&state=closed) 1. The rest of this folder 1. [Functional Reactive Programming on iOS](https://leanpub.com/iosfrp/) (eBook) If you have any further questions, please feel free to [file an issue](https://github.com/ReactiveCocoa/ReactiveObjC/issues/new). ## Introduction ReactiveObjC is inspired by [functional reactive programming](http://blog.maybeapps.com/post/42894317939/input-and-output). Rather than using mutable variables which are replaced and modified in-place, RAC provides signals (represented by `RACSignal`) that capture present and future values. By chaining, combining, and reacting to signals, software can be written declaratively, without the need for code that continually observes and updates values. For example, a text field can be bound to the latest time, even as it changes, instead of using additional code that watches the clock and updates the text field every second. It works much like KVO, but with blocks instead of overriding `-observeValueForKeyPath:ofObject:change:context:`. Signals can also represent asynchronous operations, much like [futures and promises][]. This greatly simplifies asynchronous software, including networking code. One of the major advantages of RAC is that it provides a single, unified approach to dealing with asynchronous behaviors, including delegate methods, callback blocks, target-action mechanisms, notifications, and KVO. Here's a simple example: ```objc // When self.username changes, logs the new name to the console. // // RACObserve(self, username) creates a new RACSignal that sends the current // value of self.username, then the new value whenever it changes. // -subscribeNext: will execute the block whenever the signal sends a value. [RACObserve(self, username) subscribeNext:^(NSString *newName) { NSLog(@"%@", newName); }]; ``` But unlike KVO notifications, signals can be chained together and operated on: ```objc // Only logs names that starts with "j". // // -filter returns a new RACSignal that only sends a new value when its block // returns YES. [[RACObserve(self, username) filter:^(NSString *newName) { return [newName hasPrefix:@"j"]; }] subscribeNext:^(NSString *newName) { NSLog(@"%@", newName); }]; ``` Signals can also be used to derive state. Instead of observing properties and setting other properties in response to the new values, RAC makes it possible to express properties in terms of signals and operations: ```objc // Creates a one-way binding so that self.createEnabled will be // true whenever self.password and self.passwordConfirmation // are equal. // // RAC() is a macro that makes the binding look nicer. // // +combineLatest:reduce: takes an array of signals, executes the block with the // latest value from each signal whenever any of them changes, and returns a new // RACSignal that sends the return value of that block as values. RAC(self, createEnabled) = [RACSignal combineLatest:@[ RACObserve(self, password), RACObserve(self, passwordConfirmation) ] reduce:^(NSString *password, NSString *passwordConfirm) { return @([passwordConfirm isEqualToString:password]); }]; ``` Signals can be built on any stream of values over time, not just KVO. For example, they can also represent button presses: ```objc // Logs a message whenever the button is pressed. // // RACCommand creates signals to represent UI actions. Each signal can // represent a button press, for example, and have additional work associated // with it. // // -rac_command is an addition to NSButton. The button will send itself on that // command whenever it's pressed. self.button.rac_command = [[RACCommand alloc] initWithSignalBlock:^(id _) { NSLog(@"button was pressed!"); return [RACSignal empty]; }]; ``` Or asynchronous network operations: ```objc // Hooks up a "Log in" button to log in over the network. // // This block will be run whenever the login command is executed, starting // the login process. self.loginCommand = [[RACCommand alloc] initWithSignalBlock:^(id sender) { // The hypothetical -logIn method returns a signal that sends a value when // the network request finishes. return [client logIn]; }]; // -executionSignals returns a signal that includes the signals returned from // the above block, one for each time the command is executed. [self.loginCommand.executionSignals subscribeNext:^(RACSignal *loginSignal) { // Log a message whenever we log in successfully. [loginSignal subscribeCompleted:^{ NSLog(@"Logged in successfully!"); }]; }]; // Executes the login command when the button is pressed. self.loginButton.rac_command = self.loginCommand; ``` Signals can also represent timers, other UI events, or anything else that changes over time. Using signals for asynchronous operations makes it possible to build up more complex behavior by chaining and transforming those signals. Work can easily be triggered after a group of operations completes: ```objc // Performs 2 network operations and logs a message to the console when they are // both completed. // // +merge: takes an array of signals and returns a new RACSignal that passes // through the values of all of the signals and completes when all of the // signals complete. // // -subscribeCompleted: will execute the block when the signal completes. [[RACSignal merge:@[ [client fetchUserRepos], [client fetchOrgRepos] ]] subscribeCompleted:^{ NSLog(@"They're both done!"); }]; ``` Signals can be chained to sequentially execute asynchronous operations, instead of nesting callbacks with blocks. This is similar to how [futures and promises][] are usually used: ```objc // Logs in the user, then loads any cached messages, then fetches the remaining // messages from the server. After that's all done, logs a message to the // console. // // The hypothetical -logInUser methods returns a signal that completes after // logging in. // // -flattenMap: will execute its block whenever the signal sends a value, and // returns a new RACSignal that merges all of the signals returned from the block // into a single signal. [[[[client logInUser] flattenMap:^(User *user) { // Return a signal that loads cached messages for the user. return [client loadCachedMessagesForUser:user]; }] flattenMap:^(NSArray *messages) { // Return a signal that fetches any remaining messages. return [client fetchMessagesAfterMessage:messages.lastObject]; }] subscribeNext:^(NSArray *newMessages) { NSLog(@"New messages: %@", newMessages); } completed:^{ NSLog(@"Fetched all messages."); }]; ``` RAC even makes it easy to bind to the result of an asynchronous operation: ```objc // Creates a one-way binding so that self.imageView.image will be set as the user's // avatar as soon as it's downloaded. // // The hypothetical -fetchUserWithUsername: method returns a signal which sends // the user. // // -deliverOn: creates new signals that will do their work on other queues. In // this example, it's used to move work to a background queue and then back to the main thread. // // -map: calls its block with each user that's fetched and returns a new // RACSignal that sends values returned from the block. RAC(self.imageView, image) = [[[[client fetchUserWithUsername:@"joshaber"] deliverOn:[RACScheduler scheduler]] map:^(User *user) { // Download the avatar (this is done on a background queue). return [[NSImage alloc] initWithContentsOfURL:user.avatarURL]; }] // Now the assignment will be done on the main thread. deliverOn:RACScheduler.mainThreadScheduler]; ``` That demonstrates some of what RAC can do, but it doesn't demonstrate why RAC is so powerful. It's hard to appreciate RAC from README-sized examples, but it makes it possible to write code with less state, less boilerplate, better code locality, and better expression of intent. For more sample code, check out [C-41][] or [GroceryList][], which are real iOS apps written using ReactiveObjC. Additional information about RAC can be found in this folder. ## When to use ReactiveObjC Upon first glance, ReactiveObjC is very abstract, and it can be difficult to understand how to apply it to concrete problems. Here are some of the use cases that RAC excels at. ### Handling asynchronous or event-driven data sources Much of Cocoa programming is focused on reacting to user events or changes in application state. Code that deals with such events can quickly become very complex and spaghetti-like, with lots of callbacks and state variables to handle ordering issues. Patterns that seem superficially different, like UI callbacks, network responses, and KVO notifications, actually have a lot in common. [RACSignal][] unifies all these different APIs so that they can be composed together and manipulated in the same way. For example, the following code: ```objc static void *ObservationContext = &ObservationContext; - (void)viewDidLoad { [super viewDidLoad]; [LoginManager.sharedManager addObserver:self forKeyPath:@"loggingIn" options:NSKeyValueObservingOptionInitial context:&ObservationContext]; [NSNotificationCenter.defaultCenter addObserver:self selector:@selector(loggedOut:) name:UserDidLogOutNotification object:LoginManager.sharedManager]; [self.usernameTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged]; [self.passwordTextField addTarget:self action:@selector(updateLogInButton) forControlEvents:UIControlEventEditingChanged]; [self.logInButton addTarget:self action:@selector(logInPressed:) forControlEvents:UIControlEventTouchUpInside]; } - (void)dealloc { [LoginManager.sharedManager removeObserver:self forKeyPath:@"loggingIn" context:ObservationContext]; [NSNotificationCenter.defaultCenter removeObserver:self]; } - (void)updateLogInButton { BOOL textFieldsNonEmpty = self.usernameTextField.text.length > 0 && self.passwordTextField.text.length > 0; BOOL readyToLogIn = !LoginManager.sharedManager.isLoggingIn && !self.loggedIn; self.logInButton.enabled = textFieldsNonEmpty && readyToLogIn; } - (IBAction)logInPressed:(UIButton *)sender { [[LoginManager sharedManager] logInWithUsername:self.usernameTextField.text password:self.passwordTextField.text success:^{ self.loggedIn = YES; } failure:^(NSError *error) { [self presentError:error]; }]; } - (void)loggedOut:(NSNotification *)notification { self.loggedIn = NO; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == ObservationContext) { [self updateLogInButton]; } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } ``` … could be expressed in RAC like so: ```objc - (void)viewDidLoad { [super viewDidLoad]; @weakify(self); RAC(self.logInButton, enabled) = [RACSignal combineLatest:@[ self.usernameTextField.rac_textSignal, self.passwordTextField.rac_textSignal, RACObserve(LoginManager.sharedManager, loggingIn), RACObserve(self, loggedIn) ] reduce:^(NSString *username, NSString *password, NSNumber *loggingIn, NSNumber *loggedIn) { return @(username.length > 0 && password.length > 0 && !loggingIn.boolValue && !loggedIn.boolValue); }]; [[self.logInButton rac_signalForControlEvents:UIControlEventTouchUpInside] subscribeNext:^(UIButton *sender) { @strongify(self); RACSignal *loginSignal = [LoginManager.sharedManager logInWithUsername:self.usernameTextField.text password:self.passwordTextField.text]; [loginSignal subscribeError:^(NSError *error) { @strongify(self); [self presentError:error]; } completed:^{ @strongify(self); self.loggedIn = YES; }]; }]; RAC(self, loggedIn) = [[NSNotificationCenter.defaultCenter rac_addObserverForName:UserDidLogOutNotification object:nil] mapReplace:@NO]; } ``` ### Chaining dependent operations Dependencies are most often found in network requests, where a previous request to the server needs to complete before the next one can be constructed, and so on: ```objc [client logInWithSuccess:^{ [client loadCachedMessagesWithSuccess:^(NSArray *messages) { [client fetchMessagesAfterMessage:messages.lastObject success:^(NSArray *nextMessages) { NSLog(@"Fetched all messages."); } failure:^(NSError *error) { [self presentError:error]; }]; } failure:^(NSError *error) { [self presentError:error]; }]; } failure:^(NSError *error) { [self presentError:error]; }]; ``` ReactiveObjC makes this pattern particularly easy: ```objc [[[[client logIn] then:^{ return [client loadCachedMessages]; }] flattenMap:^(NSArray *messages) { return [client fetchMessagesAfterMessage:messages.lastObject]; }] subscribeError:^(NSError *error) { [self presentError:error]; } completed:^{ NSLog(@"Fetched all messages."); }]; ``` ### Parallelizing independent work Working with independent data sets in parallel and then combining them into a final result is non-trivial in Cocoa, and often involves a lot of synchronization: ```objc __block NSArray *databaseObjects; __block NSArray *fileContents; NSOperationQueue *backgroundQueue = [[NSOperationQueue alloc] init]; NSBlockOperation *databaseOperation = [NSBlockOperation blockOperationWithBlock:^{ databaseObjects = [databaseClient fetchObjectsMatchingPredicate:predicate]; }]; NSBlockOperation *filesOperation = [NSBlockOperation blockOperationWithBlock:^{ NSMutableArray *filesInProgress = [NSMutableArray array]; for (NSString *path in files) { [filesInProgress addObject:[NSData dataWithContentsOfFile:path]]; } fileContents = [filesInProgress copy]; }]; NSBlockOperation *finishOperation = [NSBlockOperation blockOperationWithBlock:^{ [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents]; NSLog(@"Done processing"); }]; [finishOperation addDependency:databaseOperation]; [finishOperation addDependency:filesOperation]; [backgroundQueue addOperation:databaseOperation]; [backgroundQueue addOperation:filesOperation]; [backgroundQueue addOperation:finishOperation]; ``` The above code can be cleaned up and optimized by simply composing signals: ```objc RACSignal *databaseSignal = [[databaseClient fetchObjectsMatchingPredicate:predicate] subscribeOn:[RACScheduler scheduler]]; RACSignal *fileSignal = [RACSignal startEagerlyWithScheduler:[RACScheduler scheduler] block:^(id subscriber) { NSMutableArray *filesInProgress = [NSMutableArray array]; for (NSString *path in files) { [filesInProgress addObject:[NSData dataWithContentsOfFile:path]]; } [subscriber sendNext:[filesInProgress copy]]; [subscriber sendCompleted]; }]; [[RACSignal combineLatest:@[ databaseSignal, fileSignal ] reduce:^ id (NSArray *databaseObjects, NSArray *fileContents) { [self finishProcessingDatabaseObjects:databaseObjects fileContents:fileContents]; return nil; }] subscribeCompleted:^{ NSLog(@"Done processing"); }]; ``` ### Simplifying collection transformations Higher-order functions like `map`, `filter`, `fold`/`reduce` are sorely missing from Foundation, leading to loop-focused code like this: ```objc NSMutableArray *results = [NSMutableArray array]; for (NSString *str in strings) { if (str.length < 2) { continue; } NSString *newString = [str stringByAppendingString:@"foobar"]; [results addObject:newString]; } ``` [RACSequence][] allows any Cocoa collection to be manipulated in a uniform and declarative way: ```objc RACSequence *results = [[strings.rac_sequence filter:^ BOOL (NSString *str) { return str.length >= 2; }] map:^(NSString *str) { return [str stringByAppendingString:@"foobar"]; }]; ``` ## System Requirements ReactiveObjC supports OS X 10.8+ and iOS 8.0+. ## Importing ReactiveObjC To add RAC to your application: 1. Add the ReactiveObjC repository as a submodule of your application's repository. 1. Run `git submodule update --init --recursive` from within the ReactiveObjC folder. 1. Drag and drop `ReactiveObjC.xcodeproj` into your application's Xcode project or workspace. 1. On the "Build Phases" tab of your application target, add RAC to the "Link Binary With Libraries" phase. 1. Add `ReactiveObjC.framework`. RAC must also be added to any "Copy Frameworks" build phase. If you don't already have one, simply add a "Copy Files" build phase and target the "Frameworks" destination. 1. Add `"$(BUILD_ROOT)/../IntermediateBuildFilesPath/UninstalledProducts/include" $(inherited)` to the "Header Search Paths" build setting (this is only necessary for archive builds, but it has no negative effect otherwise). 1. **For iOS targets**, add `-ObjC` to the "Other Linker Flags" build setting. 1. **If you added RAC to a project (not a workspace)**, you will also need to add the appropriate RAC target to the "Target Dependencies" of your application. To see a project already set up with RAC, check out [C-41][] or [GroceryList][], which are real iOS apps written using ReactiveObjC. ## More Info ReactiveObjC is inspired by .NET's [Reactive Extensions](http://msdn.microsoft.com/en-us/data/gg577609) (Rx). Most of the principles of Rx apply to RAC as well. There are some really good Rx resources out there: * [Reactive Extensions MSDN entry](http://msdn.microsoft.com/en-us/library/hh242985.aspx) * [Reactive Extensions for .NET Introduction](http://leecampbell.blogspot.com/2010/08/reactive-extensions-for-net.html) * [Rx - Channel 9 videos](http://channel9.msdn.com/tags/Rx/) * [Reactive Extensions wiki](http://rxwiki.wikidot.com/) * [101 Rx Samples](http://rxwiki.wikidot.com/101samples) * [Programming Reactive Extensions and LINQ](http://www.amazon.com/Programming-Reactive-Extensions-Jesse-Liberty/dp/1430237473) RAC and Rx are both frameworks inspired by functional reactive programming. Here are some resources related to FRP: * [What is FRP? - Elm Language](http://elm-lang.org/learn/What-is-FRP.elm) * [What is Functional Reactive Programming - Stack Overflow](http://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming/1030631#1030631) * [Specification for a Functional Reactive Language - Stack Overflow](http://stackoverflow.com/questions/5875929/specification-for-a-functional-reactive-programming-language#5878525) * [Principles of Reactive Programming on Coursera](https://www.coursera.org/course/reactive) [ReactiveCocoa]: https://github.com/ReactiveCocoa/ReactiveCocoa [ReactiveSwift]: https://github.com/ReactiveCocoa/ReactiveSwift [Basic Operators]: Documentation/BasicOperators.md [Framework Overview]: Documentation/FrameworkOverview.md [Functional Reactive Programming]: http://en.wikipedia.org/wiki/Functional_reactive_programming [GroceryList]: https://github.com/jspahrsummers/GroceryList [RACSequence]: ReactiveObjC/RACSequence.h [RACSignal]: ReactiveOjC/RACSignal.h [futures and promises]: http://en.wikipedia.org/wiki/Futures_and_promises [C-41]: https://github.com/AshFurrow/C-41 ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/MKAnnotationView+RACSignalSupport.h ================================================ // // MKAnnotationView+RACSignalSupport.h // ReactiveObjC // // Created by Zak Remer on 3/31/15. // Copyright (c) 2015 GitHub. All rights reserved. // #import #import @class RACSignal<__covariant ValueType>; @class RACUnit; NS_ASSUME_NONNULL_BEGIN @interface MKAnnotationView (RACSignalSupport) /// A signal which will send a RACUnit whenever -prepareForReuse is invoked upon /// the receiver. /// /// Examples /// /// [[[self.cancelButton /// rac_signalForControlEvents:UIControlEventTouchUpInside] /// takeUntil:self.rac_prepareForReuseSignal] /// subscribeNext:^(UIButton *x) { /// // do other things /// }]; @property (nonatomic, strong, readonly) RACSignal *rac_prepareForReuseSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/MKAnnotationView+RACSignalSupport.m ================================================ // // MKAnnotationView+RACSignalSupport.m // ReactiveObjC // // Created by Zak Remer on 3/31/15. // Copyright (c) 2015 GitHub. All rights reserved. // #import "MKAnnotationView+RACSignalSupport.h" #import "NSObject+RACDescription.h" #import "NSObject+RACSelectorSignal.h" #import "RACSignal+Operations.h" #import "RACUnit.h" #import @implementation MKAnnotationView (RACSignalSupport) - (RACSignal *)rac_prepareForReuseSignal { RACSignal *signal = objc_getAssociatedObject(self, _cmd); if (signal != nil) return signal; signal = [[[self rac_signalForSelector:@selector(prepareForReuse)] mapReplace:RACUnit.defaultUnit] setNameWithFormat:@"%@ -rac_prepareForReuseSignal", RACDescription(self)]; objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return signal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSArray+RACSequenceAdditions.h ================================================ // // NSArray+RACSequenceAdditions.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import @class RACSequence<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSArray<__covariant ObjectType> (RACSequenceAdditions) /// Creates and returns a sequence corresponding to the receiver. /// /// Mutating the receiver will not affect the sequence after it's been created. @property (nonatomic, copy, readonly) RACSequence *rac_sequence; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSArray+RACSequenceAdditions.m ================================================ // // NSArray+RACSequenceAdditions.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "NSArray+RACSequenceAdditions.h" #import "RACArraySequence.h" @implementation NSArray (RACSequenceAdditions) - (RACSequence *)rac_sequence { return [RACArraySequence sequenceWithArray:self offset:0]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSData+RACSupport.h ================================================ // // NSData+RACSupport.h // ReactiveObjC // // Created by Josh Abernathy on 5/11/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACScheduler; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSData (RACSupport) // Read the data at the URL using -[NSData initWithContentsOfURL:options:error:]. // Sends the data or the error. // // scheduler - cannot be nil. + (RACSignal *)rac_readContentsOfURL:(nullable NSURL *)URL options:(NSDataReadingOptions)options scheduler:(RACScheduler *)scheduler; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSData+RACSupport.m ================================================ // // NSData+RACSupport.m // ReactiveObjC // // Created by Josh Abernathy on 5/11/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "NSData+RACSupport.h" #import "RACReplaySubject.h" #import "RACScheduler.h" @implementation NSData (RACSupport) + (RACSignal *)rac_readContentsOfURL:(NSURL *)URL options:(NSDataReadingOptions)options scheduler:(RACScheduler *)scheduler { NSCParameterAssert(scheduler != nil); RACReplaySubject *subject = [RACReplaySubject subject]; [subject setNameWithFormat:@"+rac_readContentsOfURL: %@ options: %lu scheduler: %@", URL, (unsigned long)options, scheduler]; [scheduler schedule:^{ NSError *error = nil; NSData *data = [[NSData alloc] initWithContentsOfURL:URL options:options error:&error]; if (data == nil) { [subject sendError:error]; } else { [subject sendNext:data]; [subject sendCompleted]; } }]; return subject; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSDictionary+RACSequenceAdditions.h ================================================ // // NSDictionary+RACSequenceAdditions.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import @class RACSequence<__covariant ValueType>; @class RACTwoTuple<__covariant First, __covariant Second>; NS_ASSUME_NONNULL_BEGIN @interface NSDictionary<__covariant KeyType, __covariant ObjectType> (RACSequenceAdditions) /// Creates and returns a sequence of key/value tuples. /// /// Mutating the receiver will not affect the sequence after it's been created. @property (nonatomic, copy, readonly) RACSequence *> *rac_sequence; /// Creates and returns a sequence corresponding to the keys in the receiver. /// /// Mutating the receiver will not affect the sequence after it's been created. @property (nonatomic, copy, readonly) RACSequence *rac_keySequence; /// Creates and returns a sequence corresponding to the values in the receiver. /// /// Mutating the receiver will not affect the sequence after it's been created. @property (nonatomic, copy, readonly) RACSequence *rac_valueSequence; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSDictionary+RACSequenceAdditions.m ================================================ // // NSDictionary+RACSequenceAdditions.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "NSDictionary+RACSequenceAdditions.h" #import "NSArray+RACSequenceAdditions.h" #import "RACSequence.h" #import "RACTuple.h" @implementation NSDictionary (RACSequenceAdditions) - (RACSequence *)rac_sequence { NSDictionary *immutableDict = [self copy]; // TODO: First class support for dictionary sequences. return [immutableDict.allKeys.rac_sequence map:^(id key) { id value = immutableDict[key]; return RACTuplePack(key, value); }]; } - (RACSequence *)rac_keySequence { return self.allKeys.rac_sequence; } - (RACSequence *)rac_valueSequence { return self.allValues.rac_sequence; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSEnumerator+RACSequenceAdditions.h ================================================ // // NSEnumerator+RACSequenceAdditions.h // ReactiveObjC // // Created by Uri Baghin on 07/01/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACSequence<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSEnumerator (RACSequenceAdditions) /// Creates and returns a sequence corresponding to the receiver. /// /// The receiver is exhausted lazily as the sequence is enumerated. @property (nonatomic, copy, readonly) RACSequence *rac_sequence; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSEnumerator+RACSequenceAdditions.m ================================================ // // NSEnumerator+RACSequenceAdditions.m // ReactiveObjC // // Created by Uri Baghin on 07/01/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "NSEnumerator+RACSequenceAdditions.h" #import "RACSequence.h" @implementation NSEnumerator (RACSequenceAdditions) - (RACSequence *)rac_sequence { return [RACSequence sequenceWithHeadBlock:^{ return [self nextObject]; } tailBlock:^{ return self.rac_sequence; }]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSFileHandle+RACSupport.h ================================================ // // NSFileHandle+RACSupport.h // ReactiveObjC // // Created by Josh Abernathy on 5/10/12. // Copyright (c) 2012 GitHub. All rights reserved. // #import @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSFileHandle (RACSupport) // Read any available data in the background and send it. Completes when data // length is <= 0. - (RACSignal *)rac_readInBackground; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSFileHandle+RACSupport.m ================================================ // // NSFileHandle+RACSupport.m // ReactiveObjC // // Created by Josh Abernathy on 5/10/12. // Copyright (c) 2012 GitHub. All rights reserved. // #import "NSFileHandle+RACSupport.h" #import "NSNotificationCenter+RACSupport.h" #import "NSObject+RACDescription.h" #import "RACReplaySubject.h" #import "RACDisposable.h" @implementation NSFileHandle (RACSupport) - (RACSignal *)rac_readInBackground { RACReplaySubject *subject = [RACReplaySubject subject]; [subject setNameWithFormat:@"%@ -rac_readInBackground", RACDescription(self)]; RACSignal *dataNotification = [[[NSNotificationCenter defaultCenter] rac_addObserverForName:NSFileHandleReadCompletionNotification object:self] map:^(NSNotification *note) { return note.userInfo[NSFileHandleNotificationDataItem]; }]; __block RACDisposable *subscription = [dataNotification subscribeNext:^(NSData *data) { if (data.length > 0) { [subject sendNext:data]; [self readInBackgroundAndNotify]; } else { [subject sendCompleted]; [subscription dispose]; } }]; [self readInBackgroundAndNotify]; return subject; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSIndexSet+RACSequenceAdditions.h ================================================ // // NSIndexSet+RACSequenceAdditions.h // ReactiveObjC // // Created by Sergey Gavrilyuk on 12/17/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACSequence<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSIndexSet (RACSequenceAdditions) /// Creates and returns a sequence of indexes (as `NSNumber`s) corresponding to /// the receiver. /// /// Mutating the receiver will not affect the sequence after it's been created. @property (nonatomic, copy, readonly) RACSequence *rac_sequence; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSIndexSet+RACSequenceAdditions.m ================================================ // // NSIndexSet+RACSequenceAdditions.m // ReactiveObjC // // Created by Sergey Gavrilyuk on 12/17/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "NSIndexSet+RACSequenceAdditions.h" #import "RACIndexSetSequence.h" @implementation NSIndexSet (RACSequenceAdditions) - (RACSequence *)rac_sequence { return [RACIndexSetSequence sequenceWithIndexSet:self]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSInvocation+RACTypeParsing.h ================================================ // // NSInvocation+RACTypeParsing.h // ReactiveObjC // // Created by Josh Abernathy on 11/17/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACTuple; // A private category of methods to handle wrapping and unwrapping of values. @interface NSInvocation (RACTypeParsing) // Sets the argument for the invocation at the given index by unboxing the given // object based on the type signature of the argument. // // This does not support C arrays or unions. // // Note that calling this on a char * or const char * argument can cause all // arguments to be retained. // // object - The object to unbox and set as the argument. // index - The index of the argument to set. - (void)rac_setArgument:(id)object atIndex:(NSUInteger)index; // Gets the argument for the invocation at the given index based on the // invocation's method signature. The value is then wrapped in the appropriate // object type. // // This does not support C arrays or unions. // // index - The index of the argument to get. // // Returns the argument of the invocation, wrapped in an object. - (id)rac_argumentAtIndex:(NSUInteger)index; // Arguments tuple for the invocation. // // The arguments tuple excludes implicit variables `self` and `_cmd`. // // See -rac_argumentAtIndex: and -rac_setArgumentAtIndex: for further // description of the underlying behavior. @property (nonatomic, copy) RACTuple *rac_argumentsTuple; // Gets the return value from the invocation based on the invocation's method // signature. The value is then wrapped in the appropriate object type. // // This does not support C arrays or unions. // // Returns the return value of the invocation, wrapped in an object. Voids are // returned as `RACUnit.defaultUnit`. - (id)rac_returnValue; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSInvocation+RACTypeParsing.m ================================================ // // NSInvocation+RACTypeParsing.m // ReactiveObjC // // Created by Josh Abernathy on 11/17/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "NSInvocation+RACTypeParsing.h" #import "RACTuple.h" #import "RACUnit.h" #import @implementation NSInvocation (RACTypeParsing) - (void)rac_setArgument:(id)object atIndex:(NSUInteger)index { #define PULL_AND_SET(type, selector) \ do { \ type val = [object selector]; \ [self setArgument:&val atIndex:(NSInteger)index]; \ } while (0) const char *argType = [self.methodSignature getArgumentTypeAtIndex:index]; // Skip const type qualifier. if (argType[0] == 'r') { argType++; } if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) { [self setArgument:&object atIndex:(NSInteger)index]; } else if (strcmp(argType, @encode(char)) == 0) { PULL_AND_SET(char, charValue); } else if (strcmp(argType, @encode(int)) == 0) { PULL_AND_SET(int, intValue); } else if (strcmp(argType, @encode(short)) == 0) { PULL_AND_SET(short, shortValue); } else if (strcmp(argType, @encode(long)) == 0) { PULL_AND_SET(long, longValue); } else if (strcmp(argType, @encode(long long)) == 0) { PULL_AND_SET(long long, longLongValue); } else if (strcmp(argType, @encode(unsigned char)) == 0) { PULL_AND_SET(unsigned char, unsignedCharValue); } else if (strcmp(argType, @encode(unsigned int)) == 0) { PULL_AND_SET(unsigned int, unsignedIntValue); } else if (strcmp(argType, @encode(unsigned short)) == 0) { PULL_AND_SET(unsigned short, unsignedShortValue); } else if (strcmp(argType, @encode(unsigned long)) == 0) { PULL_AND_SET(unsigned long, unsignedLongValue); } else if (strcmp(argType, @encode(unsigned long long)) == 0) { PULL_AND_SET(unsigned long long, unsignedLongLongValue); } else if (strcmp(argType, @encode(float)) == 0) { PULL_AND_SET(float, floatValue); } else if (strcmp(argType, @encode(double)) == 0) { PULL_AND_SET(double, doubleValue); } else if (strcmp(argType, @encode(BOOL)) == 0) { PULL_AND_SET(BOOL, boolValue); } else if (strcmp(argType, @encode(char *)) == 0) { const char *cString = [object UTF8String]; [self setArgument:&cString atIndex:(NSInteger)index]; [self retainArguments]; } else if (strcmp(argType, @encode(void (^)(void))) == 0) { [self setArgument:&object atIndex:(NSInteger)index]; } else { NSCParameterAssert([object isKindOfClass:NSValue.class]); NSUInteger valueSize = 0; NSGetSizeAndAlignment([object objCType], &valueSize, NULL); #if DEBUG NSUInteger argSize = 0; NSGetSizeAndAlignment(argType, &argSize, NULL); NSCAssert(valueSize == argSize, @"Value size does not match argument size in -rac_setArgument: %@ atIndex: %lu", object, (unsigned long)index); #endif unsigned char valueBytes[valueSize]; [object getValue:valueBytes]; [self setArgument:valueBytes atIndex:(NSInteger)index]; } #undef PULL_AND_SET } - (id)rac_argumentAtIndex:(NSUInteger)index { #define WRAP_AND_RETURN(type) \ do { \ type val = 0; \ [self getArgument:&val atIndex:(NSInteger)index]; \ return @(val); \ } while (0) const char *argType = [self.methodSignature getArgumentTypeAtIndex:index]; // Skip const type qualifier. if (argType[0] == 'r') { argType++; } if (strcmp(argType, @encode(id)) == 0 || strcmp(argType, @encode(Class)) == 0) { __autoreleasing id returnObj; [self getArgument:&returnObj atIndex:(NSInteger)index]; return returnObj; } else if (strcmp(argType, @encode(char)) == 0) { WRAP_AND_RETURN(char); } else if (strcmp(argType, @encode(int)) == 0) { WRAP_AND_RETURN(int); } else if (strcmp(argType, @encode(short)) == 0) { WRAP_AND_RETURN(short); } else if (strcmp(argType, @encode(long)) == 0) { WRAP_AND_RETURN(long); } else if (strcmp(argType, @encode(long long)) == 0) { WRAP_AND_RETURN(long long); } else if (strcmp(argType, @encode(unsigned char)) == 0) { WRAP_AND_RETURN(unsigned char); } else if (strcmp(argType, @encode(unsigned int)) == 0) { WRAP_AND_RETURN(unsigned int); } else if (strcmp(argType, @encode(unsigned short)) == 0) { WRAP_AND_RETURN(unsigned short); } else if (strcmp(argType, @encode(unsigned long)) == 0) { WRAP_AND_RETURN(unsigned long); } else if (strcmp(argType, @encode(unsigned long long)) == 0) { WRAP_AND_RETURN(unsigned long long); } else if (strcmp(argType, @encode(float)) == 0) { WRAP_AND_RETURN(float); } else if (strcmp(argType, @encode(double)) == 0) { WRAP_AND_RETURN(double); } else if (strcmp(argType, @encode(BOOL)) == 0) { WRAP_AND_RETURN(BOOL); } else if (strcmp(argType, @encode(char *)) == 0) { WRAP_AND_RETURN(const char *); } else if (strcmp(argType, @encode(void (^)(void))) == 0) { __unsafe_unretained id block = nil; [self getArgument:&block atIndex:(NSInteger)index]; return [block copy]; } else { NSUInteger valueSize = 0; NSGetSizeAndAlignment(argType, &valueSize, NULL); unsigned char valueBytes[valueSize]; [self getArgument:valueBytes atIndex:(NSInteger)index]; return [NSValue valueWithBytes:valueBytes objCType:argType]; } return nil; #undef WRAP_AND_RETURN } - (RACTuple *)rac_argumentsTuple { NSUInteger numberOfArguments = self.methodSignature.numberOfArguments; NSMutableArray *argumentsArray = [NSMutableArray arrayWithCapacity:numberOfArguments - 2]; for (NSUInteger index = 2; index < numberOfArguments; index++) { [argumentsArray addObject:[self rac_argumentAtIndex:index] ?: RACTupleNil.tupleNil]; } return [RACTuple tupleWithObjectsFromArray:argumentsArray]; } - (void)setRac_argumentsTuple:(RACTuple *)arguments { NSCAssert(arguments.count == self.methodSignature.numberOfArguments - 2, @"Number of supplied arguments (%lu), does not match the number expected by the signature (%lu)", (unsigned long)arguments.count, (unsigned long)self.methodSignature.numberOfArguments - 2); NSUInteger index = 2; for (id arg in arguments) { [self rac_setArgument:(arg == RACTupleNil.tupleNil ? nil : arg) atIndex:index]; index++; } } - (id)rac_returnValue { #define WRAP_AND_RETURN(type) \ do { \ type val = 0; \ [self getReturnValue:&val]; \ return @(val); \ } while (0) const char *returnType = self.methodSignature.methodReturnType; // Skip const type qualifier. if (returnType[0] == 'r') { returnType++; } if (strcmp(returnType, @encode(id)) == 0 || strcmp(returnType, @encode(Class)) == 0 || strcmp(returnType, @encode(void (^)(void))) == 0) { __autoreleasing id returnObj; [self getReturnValue:&returnObj]; return returnObj; } else if (strcmp(returnType, @encode(char)) == 0) { WRAP_AND_RETURN(char); } else if (strcmp(returnType, @encode(int)) == 0) { WRAP_AND_RETURN(int); } else if (strcmp(returnType, @encode(short)) == 0) { WRAP_AND_RETURN(short); } else if (strcmp(returnType, @encode(long)) == 0) { WRAP_AND_RETURN(long); } else if (strcmp(returnType, @encode(long long)) == 0) { WRAP_AND_RETURN(long long); } else if (strcmp(returnType, @encode(unsigned char)) == 0) { WRAP_AND_RETURN(unsigned char); } else if (strcmp(returnType, @encode(unsigned int)) == 0) { WRAP_AND_RETURN(unsigned int); } else if (strcmp(returnType, @encode(unsigned short)) == 0) { WRAP_AND_RETURN(unsigned short); } else if (strcmp(returnType, @encode(unsigned long)) == 0) { WRAP_AND_RETURN(unsigned long); } else if (strcmp(returnType, @encode(unsigned long long)) == 0) { WRAP_AND_RETURN(unsigned long long); } else if (strcmp(returnType, @encode(float)) == 0) { WRAP_AND_RETURN(float); } else if (strcmp(returnType, @encode(double)) == 0) { WRAP_AND_RETURN(double); } else if (strcmp(returnType, @encode(BOOL)) == 0) { WRAP_AND_RETURN(BOOL); } else if (strcmp(returnType, @encode(char *)) == 0) { WRAP_AND_RETURN(const char *); } else if (strcmp(returnType, @encode(void)) == 0) { return RACUnit.defaultUnit; } else { NSUInteger valueSize = 0; NSGetSizeAndAlignment(returnType, &valueSize, NULL); unsigned char valueBytes[valueSize]; [self getReturnValue:valueBytes]; return [NSValue valueWithBytes:valueBytes objCType:returnType]; } return nil; #undef WRAP_AND_RETURN } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSNotificationCenter+RACSupport.h ================================================ // // NSNotificationCenter+RACSupport.h // ReactiveObjC // // Created by Josh Abernathy on 5/10/12. // Copyright (c) 2012 GitHub. All rights reserved. // #import @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSNotificationCenter (RACSupport) // Sends the NSNotification every time the notification is posted. - (RACSignal *)rac_addObserverForName:(nullable NSString *)notificationName object:(nullable id)object; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSNotificationCenter+RACSupport.m ================================================ // // NSNotificationCenter+RACSupport.m // ReactiveObjC // // Created by Josh Abernathy on 5/10/12. // Copyright (c) 2012 GitHub. All rights reserved. // #import "NSNotificationCenter+RACSupport.h" #import #import "RACSignal.h" #import "RACSubscriber.h" #import "RACDisposable.h" @implementation NSNotificationCenter (RACSupport) - (RACSignal *)rac_addObserverForName:(NSString *)notificationName object:(id)object { @unsafeify(object); return [[RACSignal createSignal:^(id subscriber) { @strongify(object); id observer = [self addObserverForName:notificationName object:object queue:nil usingBlock:^(NSNotification *note) { [subscriber sendNext:note]; }]; return [RACDisposable disposableWithBlock:^{ [self removeObserver:observer]; }]; }] setNameWithFormat:@"-rac_addObserverForName: %@ object: <%@: %p>", notificationName, [object class], object]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACDeallocating.h ================================================ // // NSObject+RACDeallocating.h // ReactiveObjC // // Created by Kazuo Koga on 2013/03/15. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACCompoundDisposable; @class RACDisposable; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSObject (RACDeallocating) /// The compound disposable which will be disposed of when the receiver is /// deallocated. @property (atomic, readonly, strong) RACCompoundDisposable *rac_deallocDisposable; /// Returns a signal that will complete immediately before the receiver is fully /// deallocated. If already deallocated when the signal is subscribed to, /// a `completed` event will be sent immediately. - (RACSignal *)rac_willDeallocSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACDeallocating.m ================================================ // // NSObject+RACDeallocating.m // ReactiveObjC // // Created by Kazuo Koga on 2013/03/15. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "NSObject+RACDeallocating.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACReplaySubject.h" #import #import static const void *RACObjectCompoundDisposable = &RACObjectCompoundDisposable; static NSMutableSet *swizzledClasses() { static dispatch_once_t onceToken; static NSMutableSet *swizzledClasses = nil; dispatch_once(&onceToken, ^{ swizzledClasses = [[NSMutableSet alloc] init]; }); return swizzledClasses; } static void swizzleDeallocIfNeeded(Class classToSwizzle) { @synchronized (swizzledClasses()) { NSString *className = NSStringFromClass(classToSwizzle); if ([swizzledClasses() containsObject:className]) return; SEL deallocSelector = sel_registerName("dealloc"); __block void (*originalDealloc)(__unsafe_unretained id, SEL) = NULL; id newDealloc = ^(__unsafe_unretained id self) { RACCompoundDisposable *compoundDisposable = objc_getAssociatedObject(self, RACObjectCompoundDisposable); [compoundDisposable dispose]; if (originalDealloc == NULL) { struct objc_super superInfo = { .receiver = self, .super_class = class_getSuperclass(classToSwizzle) }; void (*msgSend)(struct objc_super *, SEL) = (__typeof__(msgSend))objc_msgSendSuper; msgSend(&superInfo, deallocSelector); } else { originalDealloc(self, deallocSelector); } }; IMP newDeallocIMP = imp_implementationWithBlock(newDealloc); if (!class_addMethod(classToSwizzle, deallocSelector, newDeallocIMP, "v@:")) { // The class already contains a method implementation. Method deallocMethod = class_getInstanceMethod(classToSwizzle, deallocSelector); // We need to store original implementation before setting new implementation // in case method is called at the time of setting. originalDealloc = (__typeof__(originalDealloc))method_getImplementation(deallocMethod); // We need to store original implementation again, in case it just changed. originalDealloc = (__typeof__(originalDealloc))method_setImplementation(deallocMethod, newDeallocIMP); } [swizzledClasses() addObject:className]; } } @implementation NSObject (RACDeallocating) - (RACSignal *)rac_willDeallocSignal { RACSignal *signal = objc_getAssociatedObject(self, _cmd); if (signal != nil) return signal; RACReplaySubject *subject = [RACReplaySubject subject]; [self.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ [subject sendCompleted]; }]]; objc_setAssociatedObject(self, _cmd, subject, OBJC_ASSOCIATION_RETAIN); return subject; } - (RACCompoundDisposable *)rac_deallocDisposable { @synchronized (self) { RACCompoundDisposable *compoundDisposable = objc_getAssociatedObject(self, RACObjectCompoundDisposable); if (compoundDisposable != nil) return compoundDisposable; swizzleDeallocIfNeeded(self.class); compoundDisposable = [RACCompoundDisposable compoundDisposable]; objc_setAssociatedObject(self, RACObjectCompoundDisposable, compoundDisposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return compoundDisposable; } } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACDescription.h ================================================ // // NSObject+RACDescription.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-05-13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import // A simplified description of the object, which does not invoke -description // (and thus should be much faster in many cases). // // This is for debugging purposes only, and will return a constant string // unless the RAC_DEBUG_SIGNAL_NAMES environment variable is set. NSString *RACDescription(id object); ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACDescription.m ================================================ // // NSObject+RACDescription.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-05-13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "NSObject+RACDescription.h" #import "RACTuple.h" @implementation NSValue (RACDescription) - (NSString *)rac_description { return self.description; } @end @implementation NSString (RACDescription) - (NSString *)rac_description { return self.description; } @end @implementation RACTuple (RACDescription) - (NSString *)rac_description { if (getenv("RAC_DEBUG_SIGNAL_NAMES") != NULL) { return self.allObjects.description; } else { return @"(description skipped)"; } } @end NSString *RACDescription(id object) { if (getenv("RAC_DEBUG_SIGNAL_NAMES") != NULL) { if ([object respondsToSelector:@selector(rac_description)]) { return [object rac_description]; } else { return [[NSString alloc] initWithFormat:@"<%@: %p>", [object class], object]; } } else { return @"(description skipped)"; } } ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACKVOWrapper.h ================================================ // // NSObject+RACKVOWrapper.h // ReactiveObjC // // Created by Josh Abernathy on 10/11/11. // Copyright (c) 2011 GitHub. All rights reserved. // #import @class RACDisposable; @class RACKVOTrampoline; // A private category providing a block based interface to KVO. @interface NSObject (RACKVOWrapper) // Adds the given block as the callbacks for when the key path changes. // // Unlike direct KVO observation, this handles deallocation of `weak` properties // by generating an appropriate notification. This will only occur if there is // an `@property` declaration visible in the observed class, with the `weak` // memory management attribute. // // The observation does not need to be explicitly removed. It will be removed // when the observer or the receiver deallocate. // // keyPath - The key path to observe. Must not be nil. // options - The KVO observation options. // observer - The object that requested the observation. May be nil. // block - The block called when the value at the key path changes. It is // passed the current value of the key path and the extended KVO // change dictionary including RAC-specific keys and values. Must not // be nil. // // Returns a disposable that can be used to stop the observation. - (RACDisposable *)rac_observeKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)observer block:(void (^)(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent))block; @end typedef void (^RACKVOBlock)(id target, id observer, NSDictionary *change); @interface NSObject (RACUnavailableKVOWrapper) - (RACKVOTrampoline *)rac_addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(RACKVOBlock)block __attribute((unavailable("Use rac_observeKeyPath:options:observer:block: instead."))); @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACKVOWrapper.m ================================================ // // NSObject+RACKVOWrapper.m // ReactiveObjC // // Created by Josh Abernathy on 10/11/11. // Copyright (c) 2011 GitHub. All rights reserved. // #import "NSObject+RACKVOWrapper.h" #import #import #import "NSObject+RACDeallocating.h" #import "NSString+RACKeyPathUtilities.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACKVOTrampoline.h" #import "RACSerialDisposable.h" @implementation NSObject (RACKVOWrapper) - (RACDisposable *)rac_observeKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)weakObserver block:(void (^)(id, NSDictionary *, BOOL, BOOL))block { NSCParameterAssert(block != nil); NSCParameterAssert(keyPath.rac_keyPathComponents.count > 0); keyPath = [keyPath copy]; NSObject *strongObserver = weakObserver; NSArray *keyPathComponents = keyPath.rac_keyPathComponents; BOOL keyPathHasOneComponent = (keyPathComponents.count == 1); NSString *keyPathHead = keyPathComponents[0]; NSString *keyPathTail = keyPath.rac_keyPathByDeletingFirstKeyPathComponent; RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; // The disposable that groups all disposal necessary to clean up the callbacks // added to the value of the first key path component. RACSerialDisposable *firstComponentSerialDisposable = [RACSerialDisposable serialDisposableWithDisposable:[RACCompoundDisposable compoundDisposable]]; RACCompoundDisposable * (^firstComponentDisposable)(void) = ^{ return (RACCompoundDisposable *)firstComponentSerialDisposable.disposable; }; [disposable addDisposable:firstComponentSerialDisposable]; BOOL shouldAddDeallocObserver = NO; objc_property_t property = class_getProperty(object_getClass(self), keyPathHead.UTF8String); if (property != NULL) { rac_propertyAttributes *attributes = rac_copyPropertyAttributes(property); if (attributes != NULL) { @onExit { free(attributes); }; BOOL isObject = attributes->objectClass != nil || strstr(attributes->type, @encode(id)) == attributes->type; BOOL isProtocol = attributes->objectClass == NSClassFromString(@"Protocol"); BOOL isBlock = strcmp(attributes->type, @encode(void(^)(void))) == 0; BOOL isWeak = attributes->weak; // If this property isn't actually an object (or is a Class object), // no point in observing the deallocation of the wrapper returned by // KVC. // // If this property is an object, but not declared `weak`, we // don't need to watch for it spontaneously being set to nil. // // Attempting to observe non-weak properties will result in // broken behavior for dynamic getters, so don't even try. shouldAddDeallocObserver = isObject && isWeak && !isBlock && !isProtocol; } } // Adds the callback block to the value's deallocation. Also adds the logic to // clean up the callback to the firstComponentDisposable. void (^addDeallocObserverToPropertyValue)(NSObject *) = ^(NSObject *value) { if (!shouldAddDeallocObserver) return; // If a key path value is the observer, commonly when a key path begins // with "self", we prevent deallocation triggered callbacks for any such key // path components. Thus, the observer's deallocation is not considered a // change to the key path. if (value == weakObserver) return; NSDictionary *change = @{ NSKeyValueChangeKindKey: @(NSKeyValueChangeSetting), NSKeyValueChangeNewKey: NSNull.null, }; RACCompoundDisposable *valueDisposable = value.rac_deallocDisposable; RACDisposable *deallocDisposable = [RACDisposable disposableWithBlock:^{ block(nil, change, YES, keyPathHasOneComponent); }]; [valueDisposable addDisposable:deallocDisposable]; [firstComponentDisposable() addDisposable:[RACDisposable disposableWithBlock:^{ [valueDisposable removeDisposable:deallocDisposable]; }]]; }; // Adds the callback block to the remaining path components on the value. Also // adds the logic to clean up the callbacks to the firstComponentDisposable. void (^addObserverToValue)(NSObject *) = ^(NSObject *value) { RACDisposable *observerDisposable = [value rac_observeKeyPath:keyPathTail options:(options & ~NSKeyValueObservingOptionInitial) observer:weakObserver block:block]; [firstComponentDisposable() addDisposable:observerDisposable]; }; // Observe only the first key path component, when the value changes clean up // the callbacks on the old value, add callbacks to the new value and call the // callback block as needed. // // Note this does not use NSKeyValueObservingOptionInitial so this only // handles changes to the value, callbacks to the initial value must be added // separately. NSKeyValueObservingOptions trampolineOptions = (options | NSKeyValueObservingOptionPrior) & ~NSKeyValueObservingOptionInitial; RACKVOTrampoline *trampoline = [[RACKVOTrampoline alloc] initWithTarget:self observer:strongObserver keyPath:keyPathHead options:trampolineOptions block:^(id trampolineTarget, id trampolineObserver, NSDictionary *change) { // If this is a prior notification, clean up all the callbacks added to the // previous value and call the callback block. Everything else is deferred // until after we get the notification after the change. if ([change[NSKeyValueChangeNotificationIsPriorKey] boolValue]) { [firstComponentDisposable() dispose]; if ((options & NSKeyValueObservingOptionPrior) != 0) { block([trampolineTarget valueForKeyPath:keyPath], change, NO, keyPathHasOneComponent); } return; } // From here the notification is not prior. NSObject *value = [trampolineTarget valueForKey:keyPathHead]; // If the value has changed but is nil, there is no need to add callbacks to // it, just call the callback block. if (value == nil) { block(nil, change, NO, keyPathHasOneComponent); return; } // From here the notification is not prior and the value is not nil. // Create a new firstComponentDisposable while getting rid of the old one at // the same time, in case this is being called concurrently. RACDisposable *oldFirstComponentDisposable = [firstComponentSerialDisposable swapInDisposable:[RACCompoundDisposable compoundDisposable]]; [oldFirstComponentDisposable dispose]; addDeallocObserverToPropertyValue(value); // If there are no further key path components, there is no need to add the // other callbacks, just call the callback block with the value itself. if (keyPathHasOneComponent) { block(value, change, NO, keyPathHasOneComponent); return; } // The value has changed, is not nil, and there are more key path components // to consider. Add the callbacks to the value for the remaining key path // components and call the callback block with the current value of the full // key path. addObserverToValue(value); block([value valueForKeyPath:keyPathTail], change, NO, keyPathHasOneComponent); }]; // Stop the KVO observation when this one is disposed of. [disposable addDisposable:trampoline]; // Add the callbacks to the initial value if needed. NSObject *value = [self valueForKey:keyPathHead]; if (value != nil) { addDeallocObserverToPropertyValue(value); if (!keyPathHasOneComponent) { addObserverToValue(value); } } // Call the block with the initial value if needed. if ((options & NSKeyValueObservingOptionInitial) != 0) { id initialValue = [self valueForKeyPath:keyPath]; NSDictionary *initialChange = @{ NSKeyValueChangeKindKey: @(NSKeyValueChangeSetting), NSKeyValueChangeNewKey: initialValue ?: NSNull.null, }; block(initialValue, initialChange, NO, keyPathHasOneComponent); } RACCompoundDisposable *observerDisposable = strongObserver.rac_deallocDisposable; RACCompoundDisposable *selfDisposable = self.rac_deallocDisposable; // Dispose of this observation if the receiver or the observer deallocate. [observerDisposable addDisposable:disposable]; [selfDisposable addDisposable:disposable]; return [RACDisposable disposableWithBlock:^{ [disposable dispose]; [observerDisposable removeDisposable:disposable]; [selfDisposable removeDisposable:disposable]; }]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACLifting.h ================================================ // // NSObject+RACLifting.h // ReactiveObjC // // Created by Josh Abernathy on 10/13/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACSignal<__covariant ValueType>; @class RACTuple; NS_ASSUME_NONNULL_BEGIN @interface NSObject (RACLifting) /// Lifts the selector on the receiver into the reactive world. The selector will /// be invoked whenever any signal argument sends a value, but only after each /// signal has sent an initial value. /// /// It will replay the most recently sent value to new subscribers. /// /// This does not support C arrays or unions. /// /// selector - The selector on self to invoke. /// firstSignal - The signal corresponding to the first method argument. This /// must not be nil. /// ... - A list of RACSignals corresponding to the remaining arguments. /// There must be a non-nil signal for each method argument. /// /// Examples /// /// [button rac_liftSelector:@selector(setTitleColor:forState:) withSignals:textColorSignal, [RACSignal return:@(UIControlStateNormal)], nil]; /// /// Returns a signal which sends the return value from each invocation of the /// selector. If the selector returns void, it instead sends RACUnit.defaultUnit. /// It completes only after all the signal arguments complete. - (RACSignal *)rac_liftSelector:(SEL)selector withSignals:(RACSignal *)firstSignal, ... NS_REQUIRES_NIL_TERMINATION; /// Like -rac_liftSelector:withSignals:, but accepts an array instead of /// a variadic list of arguments. - (RACSignal *)rac_liftSelector:(SEL)selector withSignalsFromArray:(NSArray *)signals; /// Like -rac_liftSelector:withSignals:, but accepts a signal sending tuples of /// arguments instead of a variadic list of arguments. - (RACSignal *)rac_liftSelector:(SEL)selector withSignalOfArguments:(RACSignal *)arguments; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACLifting.m ================================================ // // NSObject+RACLifting.m // ReactiveObjC // // Created by Josh Abernathy on 10/13/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "NSObject+RACLifting.h" #import #import "NSInvocation+RACTypeParsing.h" #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import "RACSignal+Operations.h" #import "RACTuple.h" @implementation NSObject (RACLifting) - (RACSignal *)rac_liftSelector:(SEL)selector withSignalOfArguments:(RACSignal *)arguments { NSCParameterAssert(selector != NULL); NSCParameterAssert(arguments != nil); @unsafeify(self); NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; NSCAssert(methodSignature != nil, @"%@ does not respond to %@", self, NSStringFromSelector(selector)); return [[[[arguments takeUntil:self.rac_willDeallocSignal] map:^(RACTuple *arguments) { @strongify(self); NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature]; invocation.selector = selector; invocation.rac_argumentsTuple = arguments; [invocation invokeWithTarget:self]; return invocation.rac_returnValue; }] replayLast] setNameWithFormat:@"%@ -rac_liftSelector: %s withSignalsOfArguments: %@", RACDescription(self), sel_getName(selector), arguments]; } - (RACSignal *)rac_liftSelector:(SEL)selector withSignalsFromArray:(NSArray *)signals { NSCParameterAssert(signals != nil); NSCParameterAssert(signals.count > 0); NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector]; NSCAssert(methodSignature != nil, @"%@ does not respond to %@", self, NSStringFromSelector(selector)); NSUInteger numberOfArguments __attribute__((unused)) = methodSignature.numberOfArguments - 2; NSCAssert(numberOfArguments == signals.count, @"Wrong number of signals for %@ (expected %lu, got %lu)", NSStringFromSelector(selector), (unsigned long)numberOfArguments, (unsigned long)signals.count); return [[self rac_liftSelector:selector withSignalOfArguments:[RACSignal combineLatest:signals]] setNameWithFormat:@"%@ -rac_liftSelector: %s withSignalsFromArray: %@", RACDescription(self), sel_getName(selector), signals]; } - (RACSignal *)rac_liftSelector:(SEL)selector withSignals:(RACSignal *)firstSignal, ... { NSCParameterAssert(firstSignal != nil); NSMutableArray *signals = [NSMutableArray array]; va_list args; va_start(args, firstSignal); for (id currentSignal = firstSignal; currentSignal != nil; currentSignal = va_arg(args, id)) { NSCAssert([currentSignal isKindOfClass:RACSignal.class], @"Argument %@ is not a RACSignal", currentSignal); [signals addObject:currentSignal]; } va_end(args); return [[self rac_liftSelector:selector withSignalsFromArray:signals] setNameWithFormat:@"%@ -rac_liftSelector: %s withSignals: %@", RACDescription(self), sel_getName(selector), signals]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACPropertySubscribing.h ================================================ // // NSObject+RACPropertySubscribing.h // ReactiveObjC // // Created by Josh Abernathy on 3/2/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import #import #import "RACmetamacros.h" /// Creates a signal which observes `KEYPATH` on `TARGET` for changes. /// /// In either case, the observation continues until `TARGET` _or self_ is /// deallocated. If any intermediate object is deallocated instead, it will be /// assumed to have been set to nil. /// /// Make sure to `@strongify(self)` when using this macro within a block! The /// macro will _always_ reference `self`, which can silently introduce a retain /// cycle within a block. As a result, you should make sure that `self` is a weak /// reference (e.g., created by `@weakify` and `@strongify`) before the /// expression that uses `RACObserve`. /// /// Examples /// /// // Observes self, and doesn't stop until self is deallocated. /// RACSignal *selfSignal = RACObserve(self, arrayController.items); /// /// // Observes the array controller, and stops when self _or_ the array /// // controller is deallocated. /// RACSignal *arrayControllerSignal = RACObserve(self.arrayController, items); /// /// // Observes obj.arrayController, and stops when self _or_ the array /// // controller is deallocated. /// RACSignal *signal2 = RACObserve(obj.arrayController, items); /// /// @weakify(self); /// RACSignal *signal3 = [anotherSignal flattenMap:^(NSArrayController *arrayController) { /// // Avoids a retain cycle because of RACObserve implicitly referencing /// // self. /// @strongify(self); /// return RACObserve(arrayController, items); /// }]; /// /// Returns a signal which sends the current value of the key path on /// subscription, then sends the new value every time it changes, and sends /// completed if self or observer is deallocated. #define _RACObserve(TARGET, KEYPATH) \ ({ \ __weak id target_ = (TARGET); \ [target_ rac_valuesForKeyPath:@keypath(TARGET, KEYPATH) observer:self]; \ }) #if __clang__ && (__clang_major__ >= 8) #define RACObserve(TARGET, KEYPATH) _RACObserve(TARGET, KEYPATH) #else #define RACObserve(TARGET, KEYPATH) \ ({ \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wreceiver-is-weak\"") \ _RACObserve(TARGET, KEYPATH) \ _Pragma("clang diagnostic pop") \ }) #endif @class RACDisposable; @class RACTwoTuple<__covariant First, __covariant Second>; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSObject (RACPropertySubscribing) /// Creates a signal to observe the value at the given key path. /// /// The initial value is sent on subscription, the subsequent values are sent /// from whichever thread the change occured on, even if it doesn't have a valid /// scheduler. /// /// Returns a signal that immediately sends the receiver's current value at the /// given keypath, then any changes thereafter. #if OS_OBJECT_HAVE_OBJC_SUPPORT - (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath observer:(__weak NSObject *)observer; #else // Swift builds with OS_OBJECT_HAVE_OBJC_SUPPORT=0 for Playgrounds and LLDB :( - (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath observer:(NSObject *)observer; #endif /// Creates a signal to observe the changes of the given key path. /// /// The initial value is sent on subscription if `NSKeyValueObservingOptionInitial` is set. /// The subsequent values are sent from whichever thread the change occured on, /// even if it doesn't have a valid scheduler. /// /// Returns a signal that sends tuples containing the current value at the key /// path and the change dictionary for each KVO callback. #if OS_OBJECT_HAVE_OBJC_SUPPORT - (RACSignal *> *)rac_valuesAndChangesForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)observer; #else - (RACSignal *> *)rac_valuesAndChangesForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(NSObject *)observer; #endif @end NS_ASSUME_NONNULL_END #define RACAble(...) \ metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__)) \ (_RACAbleObject(self, __VA_ARGS__)) \ (_RACAbleObject(__VA_ARGS__)) #define _RACAbleObject(object, property) [object rac_signalForKeyPath:@keypath(object, property) observer:self] #define RACAbleWithStart(...) \ metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__)) \ (_RACAbleWithStartObject(self, __VA_ARGS__)) \ (_RACAbleWithStartObject(__VA_ARGS__)) #define _RACAbleWithStartObject(object, property) [object rac_signalWithStartingValueForKeyPath:@keypath(object, property) observer:self] ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACPropertySubscribing.m ================================================ // // NSObject+RACPropertySubscribing.m // ReactiveObjC // // Created by Josh Abernathy on 3/2/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "NSObject+RACPropertySubscribing.h" #import #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import "NSObject+RACKVOWrapper.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACKVOTrampoline.h" #import "RACSubscriber.h" #import "RACSignal+Operations.h" #import "RACTuple.h" #import @implementation NSObject (RACPropertySubscribing) - (RACSignal *)rac_valuesForKeyPath:(NSString *)keyPath observer:(__weak NSObject *)observer { return [[[self rac_valuesAndChangesForKeyPath:keyPath options:NSKeyValueObservingOptionInitial observer:observer] map:^(RACTuple *value) { // -map: because it doesn't require the block trampoline that -reduceEach: uses return value[0]; }] setNameWithFormat:@"RACObserve(%@, %@)", RACDescription(self), keyPath]; } - (RACSignal *)rac_valuesAndChangesForKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options observer:(__weak NSObject *)weakObserver { NSObject *strongObserver = weakObserver; keyPath = [keyPath copy]; NSRecursiveLock *objectLock = [[NSRecursiveLock alloc] init]; objectLock.name = @"org.reactivecocoa.ReactiveObjC.NSObjectRACPropertySubscribing"; __weak NSObject *weakSelf = self; RACSignal *deallocSignal = [[RACSignal zip:@[ self.rac_willDeallocSignal, strongObserver.rac_willDeallocSignal ?: [RACSignal never] ]] doCompleted:^{ // Forces deallocation to wait if the object variables are currently // being read on another thread. [objectLock lock]; @onExit { [objectLock unlock]; }; }]; return [[[RACSignal createSignal:^ RACDisposable * (id subscriber) { // Hold onto the lock the whole time we're setting up the KVO // observation, because any resurrection that might be caused by our // retaining below must be balanced out by the time -dealloc returns // (if another thread is waiting on the lock above). [objectLock lock]; @onExit { [objectLock unlock]; }; __strong NSObject *observer __attribute__((objc_precise_lifetime)) = weakObserver; __strong NSObject *self __attribute__((objc_precise_lifetime)) = weakSelf; if (self == nil) { [subscriber sendCompleted]; return nil; } return [self rac_observeKeyPath:keyPath options:options observer:observer block:^(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent) { [subscriber sendNext:RACTuplePack(value, change)]; }]; }] takeUntil:deallocSignal] setNameWithFormat:@"%@ -rac_valueAndChangesForKeyPath: %@ options: %lu observer: %@", RACDescription(self), keyPath, (unsigned long)options, RACDescription(strongObserver)]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACSelectorSignal.h ================================================ // // NSObject+RACSelectorSignal.h // ReactiveObjC // // Created by Josh Abernathy on 3/18/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACTuple; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN /// The domain for any errors originating from -rac_signalForSelector:. extern NSErrorDomain const RACSelectorSignalErrorDomain; typedef NS_ERROR_ENUM(RACSelectorSignalErrorDomain, RACSelectorSignalError) { /// -rac_signalForSelector: was going to add a new method implementation for /// `selector`, but another thread added an implementation before it was able to. /// /// This will _not_ occur for cases where a method implementation exists before /// -rac_signalForSelector: is invoked. RACSelectorSignalErrorMethodSwizzlingRace = 1, }; @interface NSObject (RACSelectorSignal) /// Creates a signal associated with the receiver, which will send a tuple of the /// method's arguments each time the given selector is invoked. /// /// If the selector is already implemented on the receiver, the existing /// implementation will be invoked _before_ the signal fires. /// /// If the selector is not yet implemented on the receiver, the injected /// implementation will have a `void` return type and accept only object /// arguments. Invoking the added implementation with non-object values, or /// expecting a return value, will result in undefined behavior. /// /// This is useful for changing an event or delegate callback into a signal. For /// example, on an NSView: /// /// [[view rac_signalForSelector:@selector(mouseDown:)] subscribeNext:^(RACTuple *args) { /// NSEvent *event = args.first; /// NSLog(@"mouse button pressed: %@", event); /// }]; /// /// selector - The selector for whose invocations are to be observed. If it /// doesn't exist, it will be implemented to accept object arguments /// and return void. This cannot have C arrays or unions as arguments /// or C arrays, unions, structs, complex or vector types as return /// type. /// /// Returns a signal which will send a tuple of arguments upon each invocation of /// the selector, then completes when the receiver is deallocated. `next` events /// will be sent synchronously from the thread that invoked the method. If /// a runtime call fails, the signal will send an error in the /// RACSelectorSignalErrorDomain. - (RACSignal *)rac_signalForSelector:(SEL)selector; /// Behaves like -rac_signalForSelector:, but if the selector is not yet /// implemented on the receiver, its method signature is looked up within /// `protocol`, and may accept non-object arguments. /// /// If the selector is not yet implemented and has a return value, the injected /// method will return all zero bits (equal to `nil`, `NULL`, 0, 0.0f, etc.). /// /// selector - The selector for whose invocations are to be observed. If it /// doesn't exist, it will be implemented using information from /// `protocol`, and may accept non-object arguments and return /// a value. This cannot have C arrays or unions as arguments or /// return type. /// protocol - The protocol in which `selector` is declared. This will be used /// for type information if the selector is not already implemented on /// the receiver. This must not be `NULL`, and `selector` must exist /// in this protocol. /// /// Returns a signal which will send a tuple of arguments on each invocation of /// the selector, or an error in RACSelectorSignalErrorDomain if a runtime /// call fails. - (RACSignal *)rac_signalForSelector:(SEL)selector fromProtocol:(Protocol *)protocol; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSObject+RACSelectorSignal.m ================================================ // // NSObject+RACSelectorSignal.m // ReactiveObjC // // Created by Josh Abernathy on 3/18/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "NSObject+RACSelectorSignal.h" #import #import "NSInvocation+RACTypeParsing.h" #import "NSObject+RACDeallocating.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACSubject.h" #import "RACTuple.h" #import "NSObject+RACDescription.h" #import #import NSErrorDomain const RACSelectorSignalErrorDomain = @"RACSelectorSignalErrorDomain"; static NSString * const RACSignalForSelectorAliasPrefix = @"rac_alias_"; static NSString * const RACSubclassSuffix = @"_RACSelectorSignal"; static void *RACSubclassAssociationKey = &RACSubclassAssociationKey; static NSMutableSet *swizzledClasses() { static NSMutableSet *set; static dispatch_once_t pred; dispatch_once(&pred, ^{ set = [[NSMutableSet alloc] init]; }); return set; } @implementation NSObject (RACSelectorSignal) static BOOL RACForwardInvocation(id self, NSInvocation *invocation) { SEL aliasSelector = RACAliasForSelector(invocation.selector); RACSubject *subject = objc_getAssociatedObject(self, aliasSelector); Class class = object_getClass(invocation.target); BOOL respondsToAlias = [class instancesRespondToSelector:aliasSelector]; if (respondsToAlias) { invocation.selector = aliasSelector; [invocation invoke]; } if (subject == nil) return respondsToAlias; [subject sendNext:invocation.rac_argumentsTuple]; return YES; } static void RACSwizzleForwardInvocation(Class class) { SEL forwardInvocationSEL = @selector(forwardInvocation:); Method forwardInvocationMethod = class_getInstanceMethod(class, forwardInvocationSEL); // Preserve any existing implementation of -forwardInvocation:. void (*originalForwardInvocation)(id, SEL, NSInvocation *) = NULL; if (forwardInvocationMethod != NULL) { originalForwardInvocation = (__typeof__(originalForwardInvocation))method_getImplementation(forwardInvocationMethod); } // Set up a new version of -forwardInvocation:. // // If the selector has been passed to -rac_signalForSelector:, invoke // the aliased method, and forward the arguments to any attached signals. // // If the selector has not been passed to -rac_signalForSelector:, // invoke any existing implementation of -forwardInvocation:. If there // was no existing implementation, throw an unrecognized selector // exception. id newForwardInvocation = ^(id self, NSInvocation *invocation) { BOOL matched = RACForwardInvocation(self, invocation); if (matched) return; if (originalForwardInvocation == NULL) { [self doesNotRecognizeSelector:invocation.selector]; } else { originalForwardInvocation(self, forwardInvocationSEL, invocation); } }; class_replaceMethod(class, forwardInvocationSEL, imp_implementationWithBlock(newForwardInvocation), "v@:@"); } static void RACSwizzleRespondsToSelector(Class class) { SEL respondsToSelectorSEL = @selector(respondsToSelector:); // Preserve existing implementation of -respondsToSelector:. Method respondsToSelectorMethod = class_getInstanceMethod(class, respondsToSelectorSEL); BOOL (*originalRespondsToSelector)(id, SEL, SEL) = (__typeof__(originalRespondsToSelector))method_getImplementation(respondsToSelectorMethod); // Set up a new version of -respondsToSelector: that returns YES for methods // added by -rac_signalForSelector:. // // If the selector has a method defined on the receiver's actual class, and // if that method's implementation is _objc_msgForward, then returns whether // the instance has a signal for the selector. // Otherwise, call the original -respondsToSelector:. id newRespondsToSelector = ^ BOOL (id self, SEL selector) { Method method = rac_getImmediateInstanceMethod(class, selector); if (method != NULL && method_getImplementation(method) == _objc_msgForward) { SEL aliasSelector = RACAliasForSelector(selector); if (objc_getAssociatedObject(self, aliasSelector) != nil) return YES; } return originalRespondsToSelector(self, respondsToSelectorSEL, selector); }; class_replaceMethod(class, respondsToSelectorSEL, imp_implementationWithBlock(newRespondsToSelector), method_getTypeEncoding(respondsToSelectorMethod)); } static void RACSwizzleGetClass(Class class, Class statedClass) { SEL selector = @selector(class); Method method = class_getInstanceMethod(class, selector); IMP newIMP = imp_implementationWithBlock(^(id self) { return statedClass; }); class_replaceMethod(class, selector, newIMP, method_getTypeEncoding(method)); } static void RACSwizzleMethodSignatureForSelector(Class class) { IMP newIMP = imp_implementationWithBlock(^(id self, SEL selector) { // Don't send the -class message to the receiver because we've changed // that to return the original class. Class actualClass = object_getClass(self); Method method = class_getInstanceMethod(actualClass, selector); if (method == NULL) { // Messages that the original class dynamically implements fall // here. // // Call the original class' -methodSignatureForSelector:. struct objc_super target = { .super_class = class_getSuperclass(class), .receiver = self, }; NSMethodSignature * (*messageSend)(struct objc_super *, SEL, SEL) = (__typeof__(messageSend))objc_msgSendSuper; return messageSend(&target, @selector(methodSignatureForSelector:), selector); } char const *encoding = method_getTypeEncoding(method); return [NSMethodSignature signatureWithObjCTypes:encoding]; }); SEL selector = @selector(methodSignatureForSelector:); Method methodSignatureForSelectorMethod = class_getInstanceMethod(class, selector); class_replaceMethod(class, selector, newIMP, method_getTypeEncoding(methodSignatureForSelectorMethod)); } // It's hard to tell which struct return types use _objc_msgForward, and // which use _objc_msgForward_stret instead, so just exclude all struct, array, // union, complex and vector return types. static void RACCheckTypeEncoding(const char *typeEncoding) { #if !NS_BLOCK_ASSERTIONS // Some types, including vector types, are not encoded. In these cases the // signature starts with the size of the argument frame. NSCAssert(*typeEncoding < '1' || *typeEncoding > '9', @"unknown method return type not supported in type encoding: %s", typeEncoding); NSCAssert(strstr(typeEncoding, "(") != typeEncoding, @"union method return type not supported"); NSCAssert(strstr(typeEncoding, "{") != typeEncoding, @"struct method return type not supported"); NSCAssert(strstr(typeEncoding, "[") != typeEncoding, @"array method return type not supported"); NSCAssert(strstr(typeEncoding, @encode(_Complex float)) != typeEncoding, @"complex float method return type not supported"); NSCAssert(strstr(typeEncoding, @encode(_Complex double)) != typeEncoding, @"complex double method return type not supported"); NSCAssert(strstr(typeEncoding, @encode(_Complex long double)) != typeEncoding, @"complex long double method return type not supported"); #endif // !NS_BLOCK_ASSERTIONS } static RACSignal *NSObjectRACSignalForSelector(NSObject *self, SEL selector, Protocol *protocol) { SEL aliasSelector = RACAliasForSelector(selector); @synchronized (self) { RACSubject *subject = objc_getAssociatedObject(self, aliasSelector); if (subject != nil) return subject; Class class = RACSwizzleClass(self); NSCAssert(class != nil, @"Could not swizzle class of %@", self); subject = [[RACSubject subject] setNameWithFormat:@"%@ -rac_signalForSelector: %s", RACDescription(self), sel_getName(selector)]; objc_setAssociatedObject(self, aliasSelector, subject, OBJC_ASSOCIATION_RETAIN); [self.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ [subject sendCompleted]; }]]; Method targetMethod = class_getInstanceMethod(class, selector); if (targetMethod == NULL) { const char *typeEncoding; if (protocol == NULL) { typeEncoding = RACSignatureForUndefinedSelector(selector); } else { // Look for the selector as an optional instance method. struct objc_method_description methodDescription = protocol_getMethodDescription(protocol, selector, NO, YES); if (methodDescription.name == NULL) { // Then fall back to looking for a required instance // method. methodDescription = protocol_getMethodDescription(protocol, selector, YES, YES); NSCAssert(methodDescription.name != NULL, @"Selector %@ does not exist in <%s>", NSStringFromSelector(selector), protocol_getName(protocol)); } typeEncoding = methodDescription.types; } RACCheckTypeEncoding(typeEncoding); // Define the selector to call -forwardInvocation:. if (!class_addMethod(class, selector, _objc_msgForward, typeEncoding)) { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: [NSString stringWithFormat:NSLocalizedString(@"A race condition occurred implementing %@ on class %@", nil), NSStringFromSelector(selector), class], NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Invoke -rac_signalForSelector: again to override the implementation.", nil) }; return [RACSignal error:[NSError errorWithDomain:RACSelectorSignalErrorDomain code:RACSelectorSignalErrorMethodSwizzlingRace userInfo:userInfo]]; } } else if (method_getImplementation(targetMethod) != _objc_msgForward) { // Make a method alias for the existing method implementation. const char *typeEncoding = method_getTypeEncoding(targetMethod); RACCheckTypeEncoding(typeEncoding); BOOL addedAlias __attribute__((unused)) = class_addMethod(class, aliasSelector, method_getImplementation(targetMethod), typeEncoding); NSCAssert(addedAlias, @"Original implementation for %@ is already copied to %@ on %@", NSStringFromSelector(selector), NSStringFromSelector(aliasSelector), class); // Redefine the selector to call -forwardInvocation:. class_replaceMethod(class, selector, _objc_msgForward, method_getTypeEncoding(targetMethod)); } return subject; } } static SEL RACAliasForSelector(SEL originalSelector) { NSString *selectorName = NSStringFromSelector(originalSelector); return NSSelectorFromString([RACSignalForSelectorAliasPrefix stringByAppendingString:selectorName]); } static const char *RACSignatureForUndefinedSelector(SEL selector) { const char *name = sel_getName(selector); NSMutableString *signature = [NSMutableString stringWithString:@"v@:"]; while ((name = strchr(name, ':')) != NULL) { [signature appendString:@"@"]; name++; } return signature.UTF8String; } static Class RACSwizzleClass(NSObject *self) { Class statedClass = self.class; Class baseClass = object_getClass(self); // The "known dynamic subclass" is the subclass generated by RAC. // It's stored as an associated object on every instance that's already // been swizzled, so that even if something else swizzles the class of // this instance, we can still access the RAC generated subclass. Class knownDynamicSubclass = objc_getAssociatedObject(self, RACSubclassAssociationKey); if (knownDynamicSubclass != Nil) return knownDynamicSubclass; NSString *className = NSStringFromClass(baseClass); if (statedClass != baseClass) { // If the class is already lying about what it is, it's probably a KVO // dynamic subclass or something else that we shouldn't subclass // ourselves. // // Just swizzle -forwardInvocation: in-place. Since the object's class // was almost certainly dynamically changed, we shouldn't see another of // these classes in the hierarchy. // // Additionally, swizzle -respondsToSelector: because the default // implementation may be ignorant of methods added to this class. @synchronized (swizzledClasses()) { if (![swizzledClasses() containsObject:className]) { RACSwizzleForwardInvocation(baseClass); RACSwizzleRespondsToSelector(baseClass); RACSwizzleGetClass(baseClass, statedClass); RACSwizzleGetClass(object_getClass(baseClass), statedClass); RACSwizzleMethodSignatureForSelector(baseClass); [swizzledClasses() addObject:className]; } } return baseClass; } const char *subclassName = [className stringByAppendingString:RACSubclassSuffix].UTF8String; Class subclass = objc_getClass(subclassName); if (subclass == nil) { subclass = objc_allocateClassPair(baseClass, subclassName, 0); if (subclass == nil) return nil; RACSwizzleForwardInvocation(subclass); RACSwizzleRespondsToSelector(subclass); RACSwizzleGetClass(subclass, statedClass); RACSwizzleGetClass(object_getClass(subclass), statedClass); RACSwizzleMethodSignatureForSelector(subclass); objc_registerClassPair(subclass); } object_setClass(self, subclass); objc_setAssociatedObject(self, RACSubclassAssociationKey, subclass, OBJC_ASSOCIATION_ASSIGN); return subclass; } - (RACSignal *)rac_signalForSelector:(SEL)selector { NSCParameterAssert(selector != NULL); return NSObjectRACSignalForSelector(self, selector, NULL); } - (RACSignal *)rac_signalForSelector:(SEL)selector fromProtocol:(Protocol *)protocol { NSCParameterAssert(selector != NULL); NSCParameterAssert(protocol != NULL); return NSObjectRACSignalForSelector(self, selector, protocol); } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSOrderedSet+RACSequenceAdditions.h ================================================ // // NSOrderedSet+RACSequenceAdditions.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import @class RACSequence<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSOrderedSet<__covariant ObjectType> (RACSequenceAdditions) /// Creates and returns a sequence corresponding to the receiver. /// /// Mutating the receiver will not affect the sequence after it's been created. @property (nonatomic, copy, readonly) RACSequence *rac_sequence; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSOrderedSet+RACSequenceAdditions.m ================================================ // // NSOrderedSet+RACSequenceAdditions.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "NSOrderedSet+RACSequenceAdditions.h" #import "NSArray+RACSequenceAdditions.h" @implementation NSOrderedSet (RACSequenceAdditions) - (RACSequence *)rac_sequence { // TODO: First class support for ordered set sequences. return self.array.rac_sequence; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSSet+RACSequenceAdditions.h ================================================ // // NSSet+RACSequenceAdditions.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import @class RACSequence<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSSet<__covariant ObjectType> (RACSequenceAdditions) /// Creates and returns a sequence corresponding to the receiver. /// /// Mutating the receiver will not affect the sequence after it's been created. @property (nonatomic, copy, readonly) RACSequence *rac_sequence; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSSet+RACSequenceAdditions.m ================================================ // // NSSet+RACSequenceAdditions.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "NSSet+RACSequenceAdditions.h" #import "NSArray+RACSequenceAdditions.h" @implementation NSSet (RACSequenceAdditions) - (RACSequence *)rac_sequence { // TODO: First class support for set sequences. return self.allObjects.rac_sequence; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSString+RACKeyPathUtilities.h ================================================ // // NSString+RACKeyPathUtilities.h // ReactiveObjC // // Created by Uri Baghin on 05/05/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import // A private category of methods to extract parts of a key path. @interface NSString (RACKeyPathUtilities) // Returns an array of the components of the receiver. // // Calling this method on a string that isn't a key path is considered undefined // behavior. - (NSArray *)rac_keyPathComponents; // Returns a key path with all the components of the receiver except for the // last one. // // Calling this method on a string that isn't a key path is considered undefined // behavior. - (NSString *)rac_keyPathByDeletingLastKeyPathComponent; // Returns a key path with all the components of the receiver expect for the // first one. // // Calling this method on a string that isn't a key path is considered undefined // behavior. - (NSString *)rac_keyPathByDeletingFirstKeyPathComponent; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSString+RACKeyPathUtilities.m ================================================ // // NSString+RACKeyPathUtilities.m // ReactiveObjC // // Created by Uri Baghin on 05/05/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "NSString+RACKeyPathUtilities.h" @implementation NSString (RACKeyPathUtilities) - (NSArray *)rac_keyPathComponents { if (self.length == 0) { return nil; } return [self componentsSeparatedByString:@"."]; } - (NSString *)rac_keyPathByDeletingLastKeyPathComponent { NSUInteger lastDotIndex = [self rangeOfString:@"." options:NSBackwardsSearch].location; if (lastDotIndex == NSNotFound) { return nil; } return [self substringToIndex:lastDotIndex]; } - (NSString *)rac_keyPathByDeletingFirstKeyPathComponent { NSUInteger firstDotIndex = [self rangeOfString:@"."].location; if (firstDotIndex == NSNotFound) { return nil; } return [self substringFromIndex:firstDotIndex + 1]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSString+RACSequenceAdditions.h ================================================ // // NSString+RACSequenceAdditions.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import @class RACSequence<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSString (RACSequenceAdditions) /// Creates and returns a sequence containing strings corresponding to each /// composed character sequence in the receiver. /// /// Mutating the receiver will not affect the sequence after it's been created. @property (nonatomic, copy, readonly) RACSequence *rac_sequence; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSString+RACSequenceAdditions.m ================================================ // // NSString+RACSequenceAdditions.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "NSString+RACSequenceAdditions.h" #import "RACStringSequence.h" @implementation NSString (RACSequenceAdditions) - (RACSequence *)rac_sequence { return [RACStringSequence sequenceWithString:self offset:0]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSString+RACSupport.h ================================================ // // NSString+RACSupport.h // ReactiveObjC // // Created by Josh Abernathy on 5/11/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACScheduler; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSString (RACSupport) // Reads in the contents of the file using +[NSString stringWithContentsOfURL:usedEncoding:error:]. // Note that encoding won't be valid until the signal completes successfully. // // scheduler - cannot be nil. + (RACSignal *)rac_readContentsOfURL:(NSURL *)URL usedEncoding:(NSStringEncoding *)encoding scheduler:(RACScheduler *)scheduler; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSString+RACSupport.m ================================================ // // NSString+RACSupport.m // ReactiveObjC // // Created by Josh Abernathy on 5/11/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "NSString+RACSupport.h" #import "RACReplaySubject.h" #import "RACScheduler.h" @implementation NSString (RACSupport) + (RACSignal *)rac_readContentsOfURL:(NSURL *)URL usedEncoding:(NSStringEncoding *)encoding scheduler:(RACScheduler *)scheduler { NSCParameterAssert(URL != nil); NSCParameterAssert(encoding != nil); NSCParameterAssert(scheduler != nil); RACReplaySubject *subject = [RACReplaySubject subject]; [subject setNameWithFormat:@"+rac_readContentsOfURL: %@ usedEncoding:scheduler: %@", URL, scheduler]; [scheduler schedule:^{ NSError *error = nil; NSString *string = [NSString stringWithContentsOfURL:URL usedEncoding:encoding error:&error]; if (string == nil) { [subject sendError:error]; } else { [subject sendNext:string]; [subject sendCompleted]; } }]; return subject; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSURLConnection+RACSupport.h ================================================ // // NSURLConnection+RACSupport.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-01. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACTwoTuple<__covariant First, __covariant Second>; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface NSURLConnection (RACSupport) /// Lazily loads data for the given request in the background. /// /// request - The URL request to load. This must not be nil. /// /// Returns a signal which will begin loading the request upon each subscription, /// then send a tuple of the received response and downloaded data, and complete /// on a background thread. If any errors occur, the returned signal will error /// out. + (RACSignal *> *)rac_sendAsynchronousRequest:(NSURLRequest *)request; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSURLConnection+RACSupport.m ================================================ // // NSURLConnection+RACSupport.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-01. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "NSURLConnection+RACSupport.h" #import "RACDisposable.h" #import "RACSignal.h" #import "RACSubscriber.h" #import "RACTuple.h" @implementation NSURLConnection (RACSupport) + (RACSignal *)rac_sendAsynchronousRequest:(NSURLRequest *)request { NSCParameterAssert(request != nil); return [[RACSignal createSignal:^(id subscriber) { NSOperationQueue *queue = [[NSOperationQueue alloc] init]; queue.name = @"org.reactivecocoa.ReactiveObjC.NSURLConnectionRACSupport"; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" [NSURLConnection sendAsynchronousRequest:request queue:queue completionHandler:^(NSURLResponse *response, NSData *data, NSError *error) { // The docs say that `nil` data means an error occurred, but // `nil` responses can also occur in practice (circumstances // unknown). Consider either to be an error. // // Note that _empty_ data is not necessarily erroneous, as there // may be headers but no HTTP body. if (response == nil || data == nil) { [subscriber sendError:error]; } else { [subscriber sendNext:RACTuplePack(response, data)]; [subscriber sendCompleted]; } }]; #pragma clang diagnostic pop return [RACDisposable disposableWithBlock:^{ // It's not clear if this will actually cancel the connection, // but we can at least prevent _some_ unnecessary work -- // without writing all the code for a proper delegate, which // doesn't really belong in RAC. queue.suspended = YES; [queue cancelAllOperations]; }]; }] setNameWithFormat:@"+rac_sendAsynchronousRequest: %@", request]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSUserDefaults+RACSupport.h ================================================ // // NSUserDefaults+RACSupport.h // ReactiveObjC // // Created by Matt Diephouse on 12/19/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACChannelTerminal; NS_ASSUME_NONNULL_BEGIN @interface NSUserDefaults (RACSupport) /// Creates and returns a terminal for binding the user defaults key. /// /// **Note:** The value in the user defaults is *asynchronously* updated with /// values sent to the channel. /// /// key - The user defaults key to create the channel terminal for. /// /// Returns a channel terminal that sends the value of the user defaults key /// upon subscription, sends an updated value whenever the default changes, and /// updates the default asynchronously with values it receives. - (RACChannelTerminal *)rac_channelTerminalForKey:(NSString *)key; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/NSUserDefaults+RACSupport.m ================================================ // // NSUserDefaults+RACSupport.m // ReactiveObjC // // Created by Matt Diephouse on 12/19/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "NSUserDefaults+RACSupport.h" #import #import "NSNotificationCenter+RACSupport.h" #import "NSObject+RACDeallocating.h" #import "RACChannel.h" #import "RACScheduler.h" #import "RACSignal+Operations.h" @implementation NSUserDefaults (RACSupport) - (RACChannelTerminal *)rac_channelTerminalForKey:(NSString *)key { NSParameterAssert(key != nil); RACChannel *channel = [RACChannel new]; RACScheduler *scheduler = [RACScheduler scheduler]; __block BOOL ignoreNextValue = NO; @weakify(self); [[[[[[[NSNotificationCenter.defaultCenter rac_addObserverForName:NSUserDefaultsDidChangeNotification object:self] map:^(id _) { @strongify(self); return [self objectForKey:key]; }] startWith:[self objectForKey:key]] // Don't send values that were set on the other side of the terminal. filter:^ BOOL (id _) { if (RACScheduler.currentScheduler == scheduler && ignoreNextValue) { ignoreNextValue = NO; return NO; } return YES; }] distinctUntilChanged] takeUntil:self.rac_willDeallocSignal] subscribe:channel.leadingTerminal]; [[channel.leadingTerminal deliverOn:scheduler] subscribeNext:^(id value) { @strongify(self); ignoreNextValue = YES; [self setObject:value forKey:key]; }]; return channel.followingTerminal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACAnnotations.h ================================================ // // RACAnnotations.h // ReactiveObjC // // Created by Eric Horacek on 3/31/17. // Copyright © 2017 GitHub. All rights reserved. // #ifndef RAC_WARN_UNUSED_RESULT #define RAC_WARN_UNUSED_RESULT __attribute__((warn_unused_result)) #endif ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACArraySequence.h ================================================ // // RACArraySequence.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "RACSequence.h" // Private class that adapts an array to the RACSequence interface. @interface RACArraySequence : RACSequence // Returns a sequence for enumerating over the given array, starting from the // given offset. The array will be copied to prevent mutation. + (RACSequence *)sequenceWithArray:(NSArray *)array offset:(NSUInteger)offset; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACArraySequence.m ================================================ // // RACArraySequence.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "RACArraySequence.h" @interface RACArraySequence () // Redeclared from the superclass and marked deprecated to prevent using `array` // where `backingArray` is intended. @property (nonatomic, copy, readonly) NSArray *array __attribute__((deprecated)); // The array being sequenced. @property (nonatomic, copy, readonly) NSArray *backingArray; // The index in the array from which the sequence starts. @property (nonatomic, assign, readonly) NSUInteger offset; @end @implementation RACArraySequence #pragma mark Lifecycle + (RACSequence *)sequenceWithArray:(NSArray *)array offset:(NSUInteger)offset { NSCParameterAssert(offset <= array.count); if (offset == array.count) return self.empty; RACArraySequence *seq = [[self alloc] init]; seq->_backingArray = [array copy]; seq->_offset = offset; return seq; } #pragma mark RACSequence - (id)head { return self.backingArray[self.offset]; } - (RACSequence *)tail { RACSequence *sequence = [self.class sequenceWithArray:self.backingArray offset:self.offset + 1]; sequence.name = self.name; return sequence; } #pragma mark NSFastEnumeration - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id[])stackbuf count:(NSUInteger)len { NSCParameterAssert(len > 0); if (state->state >= self.backingArray.count) { // Enumeration has completed. return 0; } if (state->state == 0) { state->state = self.offset; // Since a sequence doesn't mutate, this just needs to be set to // something non-NULL. state->mutationsPtr = state->extra; } state->itemsPtr = stackbuf; NSUInteger startIndex = state->state; NSUInteger index = 0; for (id value in self.backingArray) { // Constructing an index set for -enumerateObjectsAtIndexes: can actually be // slower than just skipping the items we don't care about. if (index < startIndex) { ++index; continue; } stackbuf[index - startIndex] = value; ++index; if (index - startIndex >= len) break; } NSCAssert(index > startIndex, @"Final index (%lu) should be greater than start index (%lu)", (unsigned long)index, (unsigned long)startIndex); state->state = index; return index - startIndex; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (NSArray *)array { return [self.backingArray subarrayWithRange:NSMakeRange(self.offset, self.backingArray.count - self.offset)]; } #pragma clang diagnostic pop #pragma mark NSCoding - (instancetype)initWithCoder:(NSCoder *)coder { self = [super initWithCoder:coder]; if (self == nil) return nil; _backingArray = [coder decodeObjectForKey:@"array"]; _offset = 0; return self; } - (void)encodeWithCoder:(NSCoder *)coder { // Encoding is handled in RACSequence. [super encodeWithCoder:coder]; } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p>{ name = %@, array = %@ }", self.class, self, self.name, self.backingArray]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACBehaviorSubject.h ================================================ // // RACBehaviorSubject.h // ReactiveObjC // // Created by Josh Abernathy on 3/16/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSubject.h" NS_ASSUME_NONNULL_BEGIN /// A behavior subject sends the last value it received when it is subscribed to. @interface RACBehaviorSubject : RACSubject /// Creates a new behavior subject with a default value. If it hasn't received /// any values when it gets subscribed to, it sends the default value. + (instancetype)behaviorSubjectWithDefaultValue:(nullable ValueType)value; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACBehaviorSubject.m ================================================ // // RACBehaviorSubject.m // ReactiveObjC // // Created by Josh Abernathy on 3/16/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACBehaviorSubject.h" #import "RACDisposable.h" #import "RACScheduler+Private.h" @interface RACBehaviorSubject () // This property should only be used while synchronized on self. @property (nonatomic, strong) ValueType currentValue; @end @implementation RACBehaviorSubject #pragma mark Lifecycle + (instancetype)behaviorSubjectWithDefaultValue:(id)value { RACBehaviorSubject *subject = [self subject]; subject.currentValue = value; return subject; } #pragma mark RACSignal - (RACDisposable *)subscribe:(id)subscriber { RACDisposable *subscriptionDisposable = [super subscribe:subscriber]; RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{ @synchronized (self) { [subscriber sendNext:self.currentValue]; } }]; return [RACDisposable disposableWithBlock:^{ [subscriptionDisposable dispose]; [schedulingDisposable dispose]; }]; } #pragma mark RACSubscriber - (void)sendNext:(id)value { @synchronized (self) { self.currentValue = value; [super sendNext:value]; } } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACBlockTrampoline.h ================================================ // // RACBlockTrampoline.h // ReactiveObjC // // Created by Josh Abernathy on 10/21/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACTuple; // A private class that allows a limited type of dynamic block invocation. @interface RACBlockTrampoline : NSObject // Invokes the given block with the given arguments. All of the block's // argument types must be objects and it must be typed to return an object. // // At this time, it only supports blocks that take up to 15 arguments. Any more // is just cray. // // block - The block to invoke. Must accept as many arguments as are given in // the arguments array. Cannot be nil. // arguments - The arguments with which to invoke the block. `RACTupleNil`s will // be passed as nils. // // Returns the return value of invoking the block. + (id)invokeBlock:(id)block withArguments:(RACTuple *)arguments; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACBlockTrampoline.m ================================================ // // RACBlockTrampoline.m // ReactiveObjC // // Created by Josh Abernathy on 10/21/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACBlockTrampoline.h" #import "RACTuple.h" @interface RACBlockTrampoline () @property (nonatomic, readonly, copy) id block; @end @implementation RACBlockTrampoline #pragma mark API - (instancetype)initWithBlock:(id)block { self = [super init]; _block = [block copy]; return self; } + (id)invokeBlock:(id)block withArguments:(RACTuple *)arguments { NSCParameterAssert(block != NULL); RACBlockTrampoline *trampoline = [(RACBlockTrampoline *)[self alloc] initWithBlock:block]; return [trampoline invokeWithArguments:arguments]; } - (id)invokeWithArguments:(RACTuple *)arguments { SEL selector = [self selectorForArgumentCount:arguments.count]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:selector]]; invocation.selector = selector; invocation.target = self; for (NSUInteger i = 0; i < arguments.count; i++) { id arg = arguments[i]; NSInteger argIndex = (NSInteger)(i + 2); [invocation setArgument:&arg atIndex:argIndex]; } [invocation invoke]; __unsafe_unretained id returnVal; [invocation getReturnValue:&returnVal]; return returnVal; } - (SEL)selectorForArgumentCount:(NSUInteger)count { NSCParameterAssert(count > 0); switch (count) { case 0: return NULL; case 1: return @selector(performWith:); case 2: return @selector(performWith::); case 3: return @selector(performWith:::); case 4: return @selector(performWith::::); case 5: return @selector(performWith:::::); case 6: return @selector(performWith::::::); case 7: return @selector(performWith:::::::); case 8: return @selector(performWith::::::::); case 9: return @selector(performWith:::::::::); case 10: return @selector(performWith::::::::::); case 11: return @selector(performWith:::::::::::); case 12: return @selector(performWith::::::::::::); case 13: return @selector(performWith:::::::::::::); case 14: return @selector(performWith::::::::::::::); case 15: return @selector(performWith:::::::::::::::); } NSCAssert(NO, @"The argument count is too damn high! Only blocks of up to 15 arguments are currently supported."); return NULL; } - (id)performWith:(id)obj1 { id (^block)(id) = self.block; return block(obj1); } - (id)performWith:(id)obj1 :(id)obj2 { id (^block)(id, id) = self.block; return block(obj1, obj2); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 { id (^block)(id, id, id) = self.block; return block(obj1, obj2, obj3); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 { id (^block)(id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 { id (^block)(id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 { id (^block)(id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 { id (^block)(id, id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6, obj7); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 { id (^block)(id, id, id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 :(id)obj9 { id (^block)(id, id, id, id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 :(id)obj9 :(id)obj10 { id (^block)(id, id, id, id, id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 :(id)obj9 :(id)obj10 :(id)obj11 { id (^block)(id, id, id, id, id, id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 :(id)obj9 :(id)obj10 :(id)obj11 :(id)obj12 { id (^block)(id, id, id, id, id, id, id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11, obj12); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 :(id)obj9 :(id)obj10 :(id)obj11 :(id)obj12 :(id)obj13 { id (^block)(id, id, id, id, id, id, id, id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11, obj12, obj13); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 :(id)obj9 :(id)obj10 :(id)obj11 :(id)obj12 :(id)obj13 :(id)obj14 { id (^block)(id, id, id, id, id, id, id, id, id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11, obj12, obj13, obj14); } - (id)performWith:(id)obj1 :(id)obj2 :(id)obj3 :(id)obj4 :(id)obj5 :(id)obj6 :(id)obj7 :(id)obj8 :(id)obj9 :(id)obj10 :(id)obj11 :(id)obj12 :(id)obj13 :(id)obj14 :(id)obj15 { id (^block)(id, id, id, id, id, id, id, id, id, id, id, id, id, id, id) = self.block; return block(obj1, obj2, obj3, obj4, obj5, obj6, obj7, obj8, obj9, obj10, obj11, obj12, obj13, obj14, obj15); } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACChannel.h ================================================ // // RACChannel.h // ReactiveObjC // // Created by Uri Baghin on 01/01/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSignal.h" #import "RACSubscriber.h" @class RACChannelTerminal; NS_ASSUME_NONNULL_BEGIN /// A two-way channel. /// /// Conceptually, RACChannel can be thought of as a bidirectional connection, /// composed of two controllable signals that work in parallel. /// /// For example, when connecting between a view and a model: /// /// Model View /// `leadingTerminal` ------> `followingTerminal` /// `leadingTerminal` <------ `followingTerminal` /// /// The initial value of the model and all future changes to it are _sent on_ the /// `leadingTerminal`, and _received by_ subscribers of the `followingTerminal`. /// /// Likewise, whenever the user changes the value of the view, that value is sent /// on the `followingTerminal`, and received in the model from the /// `leadingTerminal`. However, the initial value of the view is not received /// from the `leadingTerminal` (only future changes). @interface RACChannel : NSObject /// The terminal which "leads" the channel, by sending its latest value /// immediately to new subscribers of the `followingTerminal`. /// /// New subscribers to this terminal will not receive a starting value, but will /// receive all future values that are sent to the `followingTerminal`. @property (nonatomic, strong, readonly) RACChannelTerminal *leadingTerminal; /// The terminal which "follows" the lead of the other terminal, only sending /// _future_ values to the subscribers of the `leadingTerminal`. /// /// The latest value sent to the `leadingTerminal` (if any) will be sent /// immediately to new subscribers of this terminal, and then all future values /// as well. @property (nonatomic, strong, readonly) RACChannelTerminal *followingTerminal; @end /// Represents one end of a RACChannel. /// /// An terminal is similar to a socket or pipe -- it represents one end of /// a connection (the RACChannel, in this case). Values sent to this terminal /// will _not_ be received by its subscribers. Instead, the values will be sent /// to the subscribers of the RACChannel's _other_ terminal. /// /// For example, when using the `followingTerminal`, _sent_ values can only be /// _received_ from the `leadingTerminal`, and vice versa. /// /// To make it easy to terminate a RACChannel, `error` and `completed` events /// sent to either terminal will be received by the subscribers of _both_ /// terminals. /// /// Do not instantiate this class directly. Create a RACChannel instead. @interface RACChannelTerminal : RACSignal - (instancetype)init __attribute__((unavailable("Instantiate a RACChannel instead"))); // Redeclaration of the RACSubscriber method. Made in order to specify a generic type. - (void)sendNext:(nullable ValueType)value; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACChannel.m ================================================ // // RACChannel.m // ReactiveObjC // // Created by Uri Baghin on 01/01/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACChannel.h" #import "RACDisposable.h" #import "RACReplaySubject.h" #import "RACSignal+Operations.h" @interface RACChannelTerminal () /// The values for this terminal. @property (nonatomic, strong, readonly) RACSignal *values; /// A subscriber will will send values to the other terminal. @property (nonatomic, strong, readonly) id otherTerminal; - (instancetype)initWithValues:(RACSignal *)values otherTerminal:(id)otherTerminal; @end @implementation RACChannel - (instancetype)init { self = [super init]; // We don't want any starting value from the leadingSubject, but we do want // error and completion to be replayed. RACReplaySubject *leadingSubject = [[RACReplaySubject replaySubjectWithCapacity:0] setNameWithFormat:@"leadingSubject"]; RACReplaySubject *followingSubject = [[RACReplaySubject replaySubjectWithCapacity:1] setNameWithFormat:@"followingSubject"]; // Propagate errors and completion to everything. [[leadingSubject ignoreValues] subscribe:followingSubject]; [[followingSubject ignoreValues] subscribe:leadingSubject]; _leadingTerminal = [[[RACChannelTerminal alloc] initWithValues:leadingSubject otherTerminal:followingSubject] setNameWithFormat:@"leadingTerminal"]; _followingTerminal = [[[RACChannelTerminal alloc] initWithValues:followingSubject otherTerminal:leadingSubject] setNameWithFormat:@"followingTerminal"]; return self; } @end @implementation RACChannelTerminal #pragma mark Lifecycle - (instancetype)initWithValues:(RACSignal *)values otherTerminal:(id)otherTerminal { NSCParameterAssert(values != nil); NSCParameterAssert(otherTerminal != nil); self = [super init]; _values = values; _otherTerminal = otherTerminal; return self; } #pragma mark RACSignal - (RACDisposable *)subscribe:(id)subscriber { return [self.values subscribe:subscriber]; } #pragma mark - (void)sendNext:(id)value { [self.otherTerminal sendNext:value]; } - (void)sendError:(NSError *)error { [self.otherTerminal sendError:error]; } - (void)sendCompleted { [self.otherTerminal sendCompleted]; } - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable { [self.otherTerminal didSubscribeWithDisposable:disposable]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACCommand.h ================================================ // // RACCommand.h // ReactiveObjC // // Created by Josh Abernathy on 3/3/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN /// The domain for errors originating within `RACCommand`. extern NSErrorDomain const RACCommandErrorDomain; typedef NS_ERROR_ENUM(RACCommandErrorDomain, RACCommandError) { /// -execute: was invoked while the command was disabled. RACCommandErrorNotEnabled = 1, }; /// A `userInfo` key for an error, associated with the `RACCommand` that the /// error originated from. /// /// This is included only when the error code is `RACCommandErrorNotEnabled`. extern NSString * const RACUnderlyingCommandErrorKey; /// A command is a signal triggered in response to some action, typically /// UI-related. @interface RACCommand<__contravariant InputType, __covariant ValueType> : NSObject /// A signal of the signals returned by successful invocations of -execute: /// (i.e., while the receiver is `enabled`). /// /// Errors will be automatically caught upon the inner signals, and sent upon /// `errors` instead. If you _want_ to receive inner errors, use -execute: or /// -[RACSignal materialize]. /// /// Only executions that begin _after_ subscription will be sent upon this /// signal. All inner signals will arrive upon the main thread. @property (nonatomic, strong, readonly) RACSignal *> *executionSignals; /// A signal of whether this command is currently executing. /// /// This will send YES whenever -execute: is invoked and the created signal has /// not yet terminated. Once all executions have terminated, `executing` will /// send NO. /// /// This signal will send its current value upon subscription, and then all /// future values on the main thread. @property (nonatomic, strong, readonly) RACSignal *executing; /// A signal of whether this command is able to execute. /// /// This will send NO if: /// /// - The command was created with an `enabledSignal`, and NO is sent upon that /// signal, or /// - `allowsConcurrentExecution` is NO and the command has started executing. /// /// Once the above conditions are no longer met, the signal will send YES. /// /// This signal will send its current value upon subscription, and then all /// future values on the main thread. @property (nonatomic, strong, readonly) RACSignal *enabled; /// Forwards any errors that occur within signals returned by -execute:. /// /// When an error occurs on a signal returned from -execute:, this signal will /// send the associated NSError value as a `next` event (since an `error` event /// would terminate the stream). /// /// After subscription, this signal will send all future errors on the main /// thread. @property (nonatomic, strong, readonly) RACSignal *errors; /// Whether the command allows multiple executions to proceed concurrently. /// /// The default value for this property is NO. @property (atomic, assign) BOOL allowsConcurrentExecution; /// Invokes -initWithEnabled:signalBlock: with a nil `enabledSignal`. - (instancetype)initWithSignalBlock:(RACSignal * (^)(InputType _Nullable input))signalBlock; /// Initializes a command that is conditionally enabled. /// /// This is the designated initializer for this class. /// /// enabledSignal - A signal of BOOLs which indicate whether the command should /// be enabled. `enabled` will be based on the latest value sent /// from this signal. Before any values are sent, `enabled` will /// default to YES. This argument may be nil. /// signalBlock - A block which will map each input value (passed to -execute:) /// to a signal of work. The returned signal will be multicasted /// to a replay subject, sent on `executionSignals`, then /// subscribed to synchronously. Neither the block nor the /// returned signal may be nil. - (instancetype)initWithEnabled:(nullable RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(InputType _Nullable input))signalBlock; /// If the receiver is enabled, this method will: /// /// 1. Invoke the `signalBlock` given at the time of initialization. /// 2. Multicast the returned signal to a RACReplaySubject. /// 3. Send the multicasted signal on `executionSignals`. /// 4. Subscribe (connect) to the original signal on the main thread. /// /// input - The input value to pass to the receiver's `signalBlock`. This may be /// nil. /// /// Returns the multicasted signal, after subscription. If the receiver is not /// enabled, returns a signal that will send an error with code /// RACCommandErrorNotEnabled. - (RACSignal *)execute:(nullable InputType)input; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACCommand.m ================================================ // // RACCommand.m // ReactiveObjC // // Created by Josh Abernathy on 3/3/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACCommand.h" #import #import "NSArray+RACSequenceAdditions.h" #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import "NSObject+RACPropertySubscribing.h" #import "RACMulticastConnection.h" #import "RACReplaySubject.h" #import "RACScheduler.h" #import "RACSequence.h" #import "RACSignal+Operations.h" #import NSErrorDomain const RACCommandErrorDomain = @"RACCommandErrorDomain"; NSString * const RACUnderlyingCommandErrorKey = @"RACUnderlyingCommandErrorKey"; @interface RACCommand () { // Atomic backing variable for `allowsConcurrentExecution`. volatile uint32_t _allowsConcurrentExecution; } /// A subject that sends added execution signals. @property (nonatomic, strong, readonly) RACSubject *addedExecutionSignalsSubject; /// A subject that sends the new value of `allowsConcurrentExecution` whenever it changes. @property (nonatomic, strong, readonly) RACSubject *allowsConcurrentExecutionSubject; // `enabled`, but without a hop to the main thread. // // Values from this signal may arrive on any thread. @property (nonatomic, strong, readonly) RACSignal *immediateEnabled; // The signal block that the receiver was initialized with. @property (nonatomic, copy, readonly) RACSignal * (^signalBlock)(id input); @end @implementation RACCommand #pragma mark Properties - (BOOL)allowsConcurrentExecution { return _allowsConcurrentExecution != 0; } - (void)setAllowsConcurrentExecution:(BOOL)allowed { if (allowed) { OSAtomicOr32Barrier(1, &_allowsConcurrentExecution); } else { OSAtomicAnd32Barrier(0, &_allowsConcurrentExecution); } [self.allowsConcurrentExecutionSubject sendNext:@(_allowsConcurrentExecution)]; } #pragma mark Lifecycle - (instancetype)init { NSCAssert(NO, @"Use -initWithSignalBlock: instead"); return nil; } - (instancetype)initWithSignalBlock:(RACSignal * (^)(id input))signalBlock { return [self initWithEnabled:nil signalBlock:signalBlock]; } - (void)dealloc { [_addedExecutionSignalsSubject sendCompleted]; [_allowsConcurrentExecutionSubject sendCompleted]; } - (instancetype)initWithEnabled:(RACSignal *)enabledSignal signalBlock:(RACSignal * (^)(id input))signalBlock { NSCParameterAssert(signalBlock != nil); self = [super init]; _addedExecutionSignalsSubject = [RACSubject new]; _allowsConcurrentExecutionSubject = [RACSubject new]; _signalBlock = [signalBlock copy]; _executionSignals = [[[self.addedExecutionSignalsSubject map:^(RACSignal *signal) { return [signal catchTo:[RACSignal empty]]; }] deliverOn:RACScheduler.mainThreadScheduler] setNameWithFormat:@"%@ -executionSignals", self]; // `errors` needs to be multicasted so that it picks up all // `activeExecutionSignals` that are added. // // In other words, if someone subscribes to `errors` _after_ an execution // has started, it should still receive any error from that execution. RACMulticastConnection *errorsConnection = [[[self.addedExecutionSignalsSubject flattenMap:^(RACSignal *signal) { return [[signal ignoreValues] catch:^(NSError *error) { return [RACSignal return:error]; }]; }] deliverOn:RACScheduler.mainThreadScheduler] publish]; _errors = [errorsConnection.signal setNameWithFormat:@"%@ -errors", self]; [errorsConnection connect]; RACSignal *immediateExecuting = [[[[self.addedExecutionSignalsSubject flattenMap:^(RACSignal *signal) { return [[[signal catchTo:[RACSignal empty]] then:^{ return [RACSignal return:@-1]; }] startWith:@1]; }] scanWithStart:@0 reduce:^(NSNumber *running, NSNumber *next) { return @(running.integerValue + next.integerValue); }] map:^(NSNumber *count) { return @(count.integerValue > 0); }] startWith:@NO]; _executing = [[[[[immediateExecuting deliverOn:RACScheduler.mainThreadScheduler] // This is useful before the first value arrives on the main thread. startWith:@NO] distinctUntilChanged] replayLast] setNameWithFormat:@"%@ -executing", self]; RACSignal *moreExecutionsAllowed = [RACSignal if:[self.allowsConcurrentExecutionSubject startWith:@NO] then:[RACSignal return:@YES] else:[immediateExecuting not]]; if (enabledSignal == nil) { enabledSignal = [RACSignal return:@YES]; } else { enabledSignal = [enabledSignal startWith:@YES]; } _immediateEnabled = [[[[RACSignal combineLatest:@[ enabledSignal, moreExecutionsAllowed ]] and] takeUntil:self.rac_willDeallocSignal] replayLast]; _enabled = [[[[[self.immediateEnabled take:1] concat:[[self.immediateEnabled skip:1] deliverOn:RACScheduler.mainThreadScheduler]] distinctUntilChanged] replayLast] setNameWithFormat:@"%@ -enabled", self]; return self; } #pragma mark Execution - (RACSignal *)execute:(id)input { // `immediateEnabled` is guaranteed to send a value upon subscription, so // -first is acceptable here. BOOL enabled = [[self.immediateEnabled first] boolValue]; if (!enabled) { NSError *error = [NSError errorWithDomain:RACCommandErrorDomain code:RACCommandErrorNotEnabled userInfo:@{ NSLocalizedDescriptionKey: NSLocalizedString(@"The command is disabled and cannot be executed", nil), RACUnderlyingCommandErrorKey: self }]; return [RACSignal error:error]; } RACSignal *signal = self.signalBlock(input); NSCAssert(signal != nil, @"nil signal returned from signal block for value: %@", input); // We subscribe to the signal on the main thread so that it occurs _after_ // -addActiveExecutionSignal: completes below. // // This means that `executing` and `enabled` will send updated values before // the signal actually starts performing work. RACMulticastConnection *connection = [[signal subscribeOn:RACScheduler.mainThreadScheduler] multicast:[RACReplaySubject subject]]; [self.addedExecutionSignalsSubject sendNext:connection.signal]; [connection connect]; return [connection.signal setNameWithFormat:@"%@ -execute: %@", self, RACDescription(input)]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACCompoundDisposable.h ================================================ // // RACCompoundDisposable.h // ReactiveObjC // // Created by Josh Abernathy on 11/30/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACDisposable.h" NS_ASSUME_NONNULL_BEGIN /// A disposable of disposables. When it is disposed, it disposes of all its /// contained disposables. /// /// If -addDisposable: is called after the compound disposable has been disposed /// of, the given disposable is immediately disposed. This allows a compound /// disposable to act as a stand-in for a disposable that will be delivered /// asynchronously. @interface RACCompoundDisposable : RACDisposable /// Creates and returns a new compound disposable. + (instancetype)compoundDisposable; /// Creates and returns a new compound disposable containing the given /// disposables. + (instancetype)compoundDisposableWithDisposables:(nullable NSArray *)disposables; /// Adds the given disposable. If the receiving disposable has already been /// disposed of, the given disposable is disposed immediately. /// /// This method is thread-safe. /// /// disposable - The disposable to add. This may be nil, in which case nothing /// happens. - (void)addDisposable:(nullable RACDisposable *)disposable; /// Removes the specified disposable from the compound disposable (regardless of /// its disposed status), or does nothing if it's not in the compound disposable. /// /// This is mainly useful for limiting the memory usage of the compound /// disposable for long-running operations. /// /// This method is thread-safe. /// /// disposable - The disposable to remove. This argument may be nil (to make the /// use of weak references easier). - (void)removeDisposable:(nullable RACDisposable *)disposable; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACCompoundDisposable.m ================================================ // // RACCompoundDisposable.m // ReactiveObjC // // Created by Josh Abernathy on 11/30/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACCompoundDisposable.h" #import "RACCompoundDisposableProvider.h" #import // The number of child disposables for which space will be reserved directly in // `RACCompoundDisposable`. // // This number has been empirically determined to provide a good tradeoff // between performance, memory usage, and `RACCompoundDisposable` instance size // in a moderately complex GUI application. // // Profile any change! #define RACCompoundDisposableInlineCount 2 static CFMutableArrayRef RACCreateDisposablesArray(void) { // Compare values using only pointer equality. CFArrayCallBacks callbacks = kCFTypeArrayCallBacks; callbacks.equal = NULL; return CFArrayCreateMutable(NULL, 0, &callbacks); } @interface RACCompoundDisposable () { // Used for synchronization. pthread_mutex_t _mutex; #if RACCompoundDisposableInlineCount // A fast array to the first N of the receiver's disposables. // // Once this is full, `_disposables` will be created and used for additional // disposables. // // This array should only be manipulated while _mutex is held. RACDisposable *_inlineDisposables[RACCompoundDisposableInlineCount]; #endif // Contains the receiver's disposables. // // This array should only be manipulated while _mutex is held. If // `_disposed` is YES, this may be NULL. CFMutableArrayRef _disposables; // Whether the receiver has already been disposed. // // This ivar should only be accessed while _mutex is held. BOOL _disposed; } @end @implementation RACCompoundDisposable #pragma mark Properties - (BOOL)isDisposed { pthread_mutex_lock(&_mutex); BOOL disposed = _disposed; pthread_mutex_unlock(&_mutex); return disposed; } #pragma mark Lifecycle + (instancetype)compoundDisposable { return [[self alloc] initWithDisposables:nil]; } + (instancetype)compoundDisposableWithDisposables:(NSArray *)disposables { return [[self alloc] initWithDisposables:disposables]; } - (instancetype)init { self = [super init]; const int result __attribute__((unused)) = pthread_mutex_init(&_mutex, NULL); NSCAssert(0 == result, @"Failed to initialize mutex with error %d.", result); return self; } - (instancetype)initWithDisposables:(NSArray *)otherDisposables { self = [self init]; #if RACCompoundDisposableInlineCount [otherDisposables enumerateObjectsUsingBlock:^(RACDisposable *disposable, NSUInteger index, BOOL *stop) { self->_inlineDisposables[index] = disposable; // Stop after this iteration if we've reached the end of the inlined // array. if (index == RACCompoundDisposableInlineCount - 1) *stop = YES; }]; #endif if (otherDisposables.count > RACCompoundDisposableInlineCount) { _disposables = RACCreateDisposablesArray(); CFRange range = CFRangeMake(RACCompoundDisposableInlineCount, (CFIndex)otherDisposables.count - RACCompoundDisposableInlineCount); CFArrayAppendArray(_disposables, (__bridge CFArrayRef)otherDisposables, range); } return self; } - (instancetype)initWithBlock:(void (^)(void))block { RACDisposable *disposable = [RACDisposable disposableWithBlock:block]; return [self initWithDisposables:@[ disposable ]]; } - (void)dealloc { #if RACCompoundDisposableInlineCount for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) { _inlineDisposables[i] = nil; } #endif if (_disposables != NULL) { CFRelease(_disposables); _disposables = NULL; } const int result __attribute__((unused)) = pthread_mutex_destroy(&_mutex); NSCAssert(0 == result, @"Failed to destroy mutex with error %d.", result); } #pragma mark Addition and Removal - (void)addDisposable:(RACDisposable *)disposable { NSCParameterAssert(disposable != self); if (disposable == nil || disposable.disposed) return; BOOL shouldDispose = NO; pthread_mutex_lock(&_mutex); { if (_disposed) { shouldDispose = YES; } else { #if RACCompoundDisposableInlineCount for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) { if (_inlineDisposables[i] == nil) { _inlineDisposables[i] = disposable; goto foundSlot; } } #endif if (_disposables == NULL) _disposables = RACCreateDisposablesArray(); CFArrayAppendValue(_disposables, (__bridge void *)disposable); if (RACCOMPOUNDDISPOSABLE_ADDED_ENABLED()) { RACCOMPOUNDDISPOSABLE_ADDED(self.description.UTF8String, disposable.description.UTF8String, CFArrayGetCount(_disposables) + RACCompoundDisposableInlineCount); } #if RACCompoundDisposableInlineCount foundSlot:; #endif } } pthread_mutex_unlock(&_mutex); // Performed outside of the lock in case the compound disposable is used // recursively. if (shouldDispose) [disposable dispose]; } - (void)removeDisposable:(RACDisposable *)disposable { if (disposable == nil) return; pthread_mutex_lock(&_mutex); { if (!_disposed) { #if RACCompoundDisposableInlineCount for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) { if (_inlineDisposables[i] == disposable) _inlineDisposables[i] = nil; } #endif if (_disposables != NULL) { CFIndex count = CFArrayGetCount(_disposables); for (CFIndex i = count - 1; i >= 0; i--) { const void *item = CFArrayGetValueAtIndex(_disposables, i); if (item == (__bridge void *)disposable) { CFArrayRemoveValueAtIndex(_disposables, i); } } if (RACCOMPOUNDDISPOSABLE_REMOVED_ENABLED()) { RACCOMPOUNDDISPOSABLE_REMOVED(self.description.UTF8String, disposable.description.UTF8String, CFArrayGetCount(_disposables) + RACCompoundDisposableInlineCount); } } } } pthread_mutex_unlock(&_mutex); } #pragma mark RACDisposable static void disposeEach(const void *value, void *context) { RACDisposable *disposable = (__bridge id)value; [disposable dispose]; } - (void)dispose { #if RACCompoundDisposableInlineCount RACDisposable *inlineCopy[RACCompoundDisposableInlineCount]; #endif CFArrayRef remainingDisposables = NULL; pthread_mutex_lock(&_mutex); { _disposed = YES; #if RACCompoundDisposableInlineCount for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) { inlineCopy[i] = _inlineDisposables[i]; _inlineDisposables[i] = nil; } #endif remainingDisposables = _disposables; _disposables = NULL; } pthread_mutex_unlock(&_mutex); #if RACCompoundDisposableInlineCount // Dispose outside of the lock in case the compound disposable is used // recursively. for (unsigned i = 0; i < RACCompoundDisposableInlineCount; i++) { [inlineCopy[i] dispose]; } #endif if (remainingDisposables == NULL) return; CFIndex count = CFArrayGetCount(remainingDisposables); CFArrayApplyFunction(remainingDisposables, CFRangeMake(0, count), &disposeEach, NULL); CFRelease(remainingDisposables); } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACCompoundDisposableProvider.d ================================================ provider RACCompoundDisposable { probe added(char *compoundDisposable, char *disposable, long newTotal); probe removed(char *compoundDisposable, char *disposable, long newTotal); }; ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACDelegateProxy.h ================================================ // // RACDelegateProxy.h // ReactiveObjC // // Created by Cody Krieger on 5/19/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN // A private delegate object suitable for using // -rac_signalForSelector:fromProtocol: upon. @interface RACDelegateProxy : NSObject // The delegate to which messages should be forwarded if not handled by // any -signalForSelector: applications. @property (nonatomic, unsafe_unretained) id rac_proxiedDelegate; // Creates a delegate proxy capable of responding to selectors from `protocol`. - (instancetype)initWithProtocol:(Protocol *)protocol; // Calls -rac_signalForSelector:fromProtocol: using the `protocol` specified // during initialization. - (RACSignal *)signalForSelector:(SEL)selector; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACDelegateProxy.m ================================================ // // RACDelegateProxy.m // ReactiveObjC // // Created by Cody Krieger on 5/19/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACDelegateProxy.h" #import "NSObject+RACSelectorSignal.h" #import @interface RACDelegateProxy () { // Declared as an ivar to avoid method naming conflicts. Protocol *_protocol; } @end @implementation RACDelegateProxy #pragma mark Lifecycle - (instancetype)initWithProtocol:(Protocol *)protocol { NSCParameterAssert(protocol != NULL); self = [super init]; class_addProtocol(self.class, protocol); _protocol = protocol; return self; } #pragma mark API - (RACSignal *)signalForSelector:(SEL)selector { return [self rac_signalForSelector:selector fromProtocol:_protocol]; } #pragma mark NSObject - (BOOL)isProxy { return YES; } - (void)forwardInvocation:(NSInvocation *)invocation { [invocation invokeWithTarget:self.rac_proxiedDelegate]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { // Look for the selector as an optional instance method. struct objc_method_description methodDescription = protocol_getMethodDescription(_protocol, selector, NO, YES); if (methodDescription.name == NULL) { // Then fall back to looking for a required instance // method. methodDescription = protocol_getMethodDescription(_protocol, selector, YES, YES); if (methodDescription.name == NULL) return [super methodSignatureForSelector:selector]; } return [NSMethodSignature signatureWithObjCTypes:methodDescription.types]; } - (BOOL)respondsToSelector:(SEL)selector { // Add the delegate to the autorelease pool, so it doesn't get deallocated // between this method call and -forwardInvocation:. __autoreleasing id delegate = self.rac_proxiedDelegate; if ([delegate respondsToSelector:selector]) return YES; return [super respondsToSelector:selector]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACDisposable.h ================================================ // // RACDisposable.h // ReactiveObjC // // Created by Josh Abernathy on 3/16/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACScopedDisposable; NS_ASSUME_NONNULL_BEGIN /// A disposable encapsulates the work necessary to tear down and cleanup a /// subscription. @interface RACDisposable : NSObject /// Whether the receiver has been disposed. /// /// Use of this property is discouraged, since it may be set to `YES` /// concurrently at any time. /// /// This property is not KVO-compliant. @property (atomic, assign, getter = isDisposed, readonly) BOOL disposed; + (instancetype)disposableWithBlock:(void (^)(void))block; /// Performs the disposal work. Can be called multiple times, though subsequent /// calls won't do anything. - (void)dispose; /// Returns a new disposable which will dispose of this disposable when it gets /// dealloc'd. - (RACScopedDisposable *)asScopedDisposable; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACDisposable.m ================================================ // // RACDisposable.m // ReactiveObjC // // Created by Josh Abernathy on 3/16/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACDisposable.h" #import "RACScopedDisposable.h" #import @interface RACDisposable () { // A copied block of type void (^)(void) containing the logic for disposal, // a pointer to `self` if no logic should be performed upon disposal, or // NULL if the receiver is already disposed. // // This should only be used atomically. void * volatile _disposeBlock; } @end @implementation RACDisposable #pragma mark Properties - (BOOL)isDisposed { return _disposeBlock == NULL; } #pragma mark Lifecycle - (instancetype)init { self = [super init]; _disposeBlock = (__bridge void *)self; OSMemoryBarrier(); return self; } - (instancetype)initWithBlock:(void (^)(void))block { NSCParameterAssert(block != nil); self = [super init]; _disposeBlock = (void *)CFBridgingRetain([block copy]); OSMemoryBarrier(); return self; } + (instancetype)disposableWithBlock:(void (^)(void))block { return [(RACDisposable *)[self alloc] initWithBlock:block]; } - (void)dealloc { if (_disposeBlock == NULL || _disposeBlock == (__bridge void *)self) return; CFRelease(_disposeBlock); _disposeBlock = NULL; } #pragma mark Disposal - (void)dispose { void (^disposeBlock)(void) = NULL; while (YES) { void *blockPtr = _disposeBlock; if (OSAtomicCompareAndSwapPtrBarrier(blockPtr, NULL, &_disposeBlock)) { if (blockPtr != (__bridge void *)self) { disposeBlock = CFBridgingRelease(blockPtr); } break; } } if (disposeBlock != nil) disposeBlock(); } #pragma mark Scoped Disposables - (RACScopedDisposable *)asScopedDisposable { return [RACScopedDisposable scopedDisposableWithDisposable:self]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACDynamicSequence.h ================================================ // // RACDynamicSequence.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "RACSequence.h" // Private class that implements a sequence dynamically using blocks. @interface RACDynamicSequence : RACSequence // Returns a sequence which evaluates `dependencyBlock` only once, the first // time either `headBlock` or `tailBlock` is evaluated. The result of // `dependencyBlock` will be passed into `headBlock` and `tailBlock` when // invoked. + (RACSequence *)sequenceWithLazyDependency:(id (^)(void))dependencyBlock headBlock:(id (^)(id dependency))headBlock tailBlock:(RACSequence *(^)(id dependency))tailBlock; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACDynamicSequence.m ================================================ // // RACDynamicSequence.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "RACDynamicSequence.h" #import // Determines how RACDynamicSequences will be deallocated before the next one is // shifted onto the autorelease pool. // // This avoids stack overflows when deallocating long chains of dynamic // sequences. #define DEALLOC_OVERFLOW_GUARD 100 @interface RACDynamicSequence () { // The value for the "head" property, if it's been evaluated already. // // Because it's legal for head to be nil, this ivar is valid any time // headBlock is nil. // // This ivar should only be accessed while synchronized on self. id _head; // The value for the "tail" property, if it's been evaluated already. // // Because it's legal for tail to be nil, this ivar is valid any time // tailBlock is nil. // // This ivar should only be accessed while synchronized on self. RACSequence *_tail; // The result of an evaluated `dependencyBlock`. // // This ivar is valid any time `hasDependency` is YES and `dependencyBlock` // is nil. // // This ivar should only be accessed while synchronized on self. id _dependency; } // A block used to evaluate head. This should be set to nil after `_head` has been // initialized. // // This is marked `strong` instead of `copy` because of some bizarre block // copying bug. See https://github.com/ReactiveCocoa/ReactiveCocoa/pull/506. // // The signature of this block varies based on the value of `hasDependency`: // // - If YES, this block is of type `id (^)(id)`. // - If NO, this block is of type `id (^)(void)`. // // This property should only be accessed while synchronized on self. @property (nonatomic, strong) id headBlock; // A block used to evaluate tail. This should be set to nil after `_tail` has been // initialized. // // This is marked `strong` instead of `copy` because of some bizarre block // copying bug. See https://github.com/ReactiveCocoa/ReactiveCocoa/pull/506. // // The signature of this block varies based on the value of `hasDependency`: // // - If YES, this block is of type `RACSequence * (^)(id)`. // - If NO, this block is of type `RACSequence * (^)(void)`. // // This property should only be accessed while synchronized on self. @property (nonatomic, strong) id tailBlock; // Whether the receiver was initialized with a `dependencyBlock`. // // This property should only be accessed while synchronized on self. @property (nonatomic, assign) BOOL hasDependency; // A dependency which must be evaluated before `headBlock` and `tailBlock`. This // should be set to nil after `_dependency` and `dependencyBlockExecuted` have // been set. // // This is marked `strong` instead of `copy` because of some bizarre block // copying bug. See https://github.com/ReactiveCocoa/ReactiveCocoa/pull/506. // // This property should only be accessed while synchronized on self. @property (nonatomic, strong) id (^dependencyBlock)(void); @end @implementation RACDynamicSequence #pragma mark Lifecycle + (RACSequence *)sequenceWithHeadBlock:(id (^)(void))headBlock tailBlock:(RACSequence *(^)(void))tailBlock { NSCParameterAssert(headBlock != nil); RACDynamicSequence *seq = [[RACDynamicSequence alloc] init]; seq.headBlock = [headBlock copy]; seq.tailBlock = [tailBlock copy]; seq.hasDependency = NO; return seq; } + (RACSequence *)sequenceWithLazyDependency:(id (^)(void))dependencyBlock headBlock:(id (^)(id dependency))headBlock tailBlock:(RACSequence *(^)(id dependency))tailBlock { NSCParameterAssert(dependencyBlock != nil); NSCParameterAssert(headBlock != nil); RACDynamicSequence *seq = [[RACDynamicSequence alloc] init]; seq.headBlock = [headBlock copy]; seq.tailBlock = [tailBlock copy]; seq.dependencyBlock = [dependencyBlock copy]; seq.hasDependency = YES; return seq; } - (void)dealloc { static volatile int32_t directDeallocCount = 0; if (OSAtomicIncrement32(&directDeallocCount) >= DEALLOC_OVERFLOW_GUARD) { OSAtomicAdd32(-DEALLOC_OVERFLOW_GUARD, &directDeallocCount); // Put this sequence's tail onto the autorelease pool so we stop // recursing. __autoreleasing RACSequence *tail __attribute__((unused)) = _tail; } _tail = nil; } #pragma mark RACSequence - (id)head { @synchronized (self) { id untypedHeadBlock = self.headBlock; if (untypedHeadBlock == nil) return _head; if (self.hasDependency) { if (self.dependencyBlock != nil) { _dependency = self.dependencyBlock(); self.dependencyBlock = nil; } id (^headBlock)(id) = untypedHeadBlock; _head = headBlock(_dependency); } else { id (^headBlock)(void) = untypedHeadBlock; _head = headBlock(); } self.headBlock = nil; return _head; } } - (RACSequence *)tail { @synchronized (self) { id untypedTailBlock = self.tailBlock; if (untypedTailBlock == nil) return _tail; if (self.hasDependency) { if (self.dependencyBlock != nil) { _dependency = self.dependencyBlock(); self.dependencyBlock = nil; } RACSequence * (^tailBlock)(id) = untypedTailBlock; _tail = tailBlock(_dependency); } else { RACSequence * (^tailBlock)(void) = untypedTailBlock; _tail = tailBlock(); } if (_tail.name == nil) _tail.name = self.name; self.tailBlock = nil; return _tail; } } #pragma mark NSObject - (NSString *)description { id head = @"(unresolved)"; id tail = @"(unresolved)"; @synchronized (self) { if (self.headBlock == nil) head = _head; if (self.tailBlock == nil) { tail = _tail; if (tail == self) tail = @"(self)"; } } return [NSString stringWithFormat:@"<%@: %p>{ name = %@, head = %@, tail = %@ }", self.class, self, self.name, head, tail]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACDynamicSignal.h ================================================ // // RACDynamicSignal.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-10. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSignal.h" // A private `RACSignal` subclasses that implements its subscription behavior // using a block. @interface RACDynamicSignal : RACSignal + (RACSignal *)createSignal:(RACDisposable * (^)(id subscriber))didSubscribe; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACDynamicSignal.m ================================================ // // RACDynamicSignal.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-10. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACDynamicSignal.h" #import #import "RACCompoundDisposable.h" #import "RACPassthroughSubscriber.h" #import "RACScheduler+Private.h" #import "RACSubscriber.h" #import @interface RACDynamicSignal () // The block to invoke for each subscriber. @property (nonatomic, copy, readonly) RACDisposable * (^didSubscribe)(id subscriber); @end @implementation RACDynamicSignal #pragma mark Lifecycle + (RACSignal *)createSignal:(RACDisposable * (^)(id subscriber))didSubscribe { RACDynamicSignal *signal = [[self alloc] init]; signal->_didSubscribe = [didSubscribe copy]; return [signal setNameWithFormat:@"+createSignal:"]; } #pragma mark Managing Subscribers - (RACDisposable *)subscribe:(id)subscriber { NSCParameterAssert(subscriber != nil); RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable]; if (self.didSubscribe != NULL) { RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{ RACDisposable *innerDisposable = self.didSubscribe(subscriber); [disposable addDisposable:innerDisposable]; }]; [disposable addDisposable:schedulingDisposable]; } return disposable; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACEagerSequence.h ================================================ // // RACEagerSequence.h // ReactiveObjC // // Created by Uri Baghin on 02/01/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACArraySequence.h" // Private class that implements an eager sequence. @interface RACEagerSequence : RACArraySequence @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACEagerSequence.m ================================================ // // RACEagerSequence.m // ReactiveObjC // // Created by Uri Baghin on 02/01/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACEagerSequence.h" #import "NSObject+RACDescription.h" #import "RACArraySequence.h" @implementation RACEagerSequence #pragma mark RACStream + (RACSequence *)return:(id)value { return [[self sequenceWithArray:@[ value ] offset:0] setNameWithFormat:@"+return: %@", RACDescription(value)]; } - (RACSequence *)bind:(RACSequenceBindBlock (^)(void))block { NSCParameterAssert(block != nil); RACStreamBindBlock bindBlock = block(); NSArray *currentArray = self.array; NSMutableArray *resultArray = [NSMutableArray arrayWithCapacity:currentArray.count]; for (id value in currentArray) { BOOL stop = NO; RACSequence *boundValue = (id)bindBlock(value, &stop); if (boundValue == nil) break; for (id x in boundValue) { [resultArray addObject:x]; } if (stop) break; } return [[self.class sequenceWithArray:resultArray offset:0] setNameWithFormat:@"[%@] -bind:", self.name]; } - (RACSequence *)concat:(RACSequence *)sequence { NSCParameterAssert(sequence != nil); NSCParameterAssert([sequence isKindOfClass:RACSequence.class]); NSArray *array = [self.array arrayByAddingObjectsFromArray:sequence.array]; return [[self.class sequenceWithArray:array offset:0] setNameWithFormat:@"[%@] -concat: %@", self.name, sequence]; } #pragma mark Extended methods - (RACSequence *)eagerSequence { return self; } - (RACSequence *)lazySequence { return [RACArraySequence sequenceWithArray:self.array offset:0]; } - (id)foldRightWithStart:(id)start reduce:(id (^)(id, RACSequence *rest))reduce { return [super foldRightWithStart:start reduce:^(id first, RACSequence *rest) { return reduce(first, rest.eagerSequence); }]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACEmptySequence.h ================================================ // // RACEmptySequence.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "RACSequence.h" // Private class representing an empty sequence. @interface RACEmptySequence : RACSequence + (RACEmptySequence *)empty; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACEmptySequence.m ================================================ // // RACEmptySequence.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "RACEmptySequence.h" @implementation RACEmptySequence #pragma mark Lifecycle + (instancetype)empty { static id singleton; static dispatch_once_t pred; dispatch_once(&pred, ^{ singleton = [[self alloc] init]; }); return singleton; } #pragma mark RACSequence - (id)head { return nil; } - (RACSequence *)tail { return nil; } - (RACSequence *)bind:(RACStreamBindBlock)bindBlock passingThroughValuesFromSequence:(RACSequence *)passthroughSequence { return passthroughSequence ?: self; } #pragma mark NSCoding - (Class)classForCoder { // Empty sequences should be encoded as themselves, not array sequences. return self.class; } - (instancetype)initWithCoder:(NSCoder *)coder { // Return the singleton. return self.class.empty; } - (void)encodeWithCoder:(NSCoder *)coder { } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p>{ name = %@ }", self.class, self, self.name]; } - (NSUInteger)hash { // This hash isn't ideal, but it's better than -[RACSequence hash], which // would just be zero because we have no head. return (NSUInteger)(__bridge void *)self; } - (BOOL)isEqual:(RACSequence *)seq { return (self == seq); } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACEmptySignal.h ================================================ // // RACEmptySignal.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-10. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSignal.h" // A private `RACSignal` subclasses that synchronously sends completed to any // subscribers. @interface RACEmptySignal<__covariant ValueType> : RACSignal + (RACSignal *)empty; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACEmptySignal.m ================================================ // // RACEmptySignal.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-10. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACEmptySignal.h" #import "RACScheduler+Private.h" #import "RACSubscriber.h" @implementation RACEmptySignal #pragma mark Properties // Only allow this signal's name to be customized in DEBUG, since it's // a singleton in release builds (see +empty). - (void)setName:(NSString *)name { #ifdef DEBUG [super setName:name]; #endif } - (NSString *)name { #ifdef DEBUG return super.name; #else return @"+empty"; #endif } #pragma mark Lifecycle + (RACSignal *)empty { #ifdef DEBUG // Create multiple instances of this class in DEBUG so users can set custom // names on each. return [[[self alloc] init] setNameWithFormat:@"+empty"]; #else static id singleton; static dispatch_once_t pred; dispatch_once(&pred, ^{ singleton = [[self alloc] init]; }); return singleton; #endif } #pragma mark Subscription - (RACDisposable *)subscribe:(id)subscriber { NSCParameterAssert(subscriber != nil); return [RACScheduler.subscriptionScheduler schedule:^{ [subscriber sendCompleted]; }]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACErrorSignal.h ================================================ // // RACErrorSignal.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-10. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSignal.h" // A private `RACSignal` subclass that synchronously sends an error to any // subscriber. @interface RACErrorSignal : RACSignal + (RACSignal *)error:(NSError *)error; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACErrorSignal.m ================================================ // // RACErrorSignal.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-10. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACErrorSignal.h" #import "RACScheduler+Private.h" #import "RACSubscriber.h" @interface RACErrorSignal () // The error to send upon subscription. @property (nonatomic, strong, readonly) NSError *error; @end @implementation RACErrorSignal #pragma mark Lifecycle + (RACSignal *)error:(NSError *)error { RACErrorSignal *signal = [[self alloc] init]; signal->_error = error; #ifdef DEBUG [signal setNameWithFormat:@"+error: %@", error]; #else signal.name = @"+error:"; #endif return signal; } #pragma mark Subscription - (RACDisposable *)subscribe:(id)subscriber { NSCParameterAssert(subscriber != nil); return [RACScheduler.subscriptionScheduler schedule:^{ [subscriber sendError:self.error]; }]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACEvent.h ================================================ // // RACEvent.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-01-07. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN /// Describes the type of a RACEvent. /// /// RACEventTypeCompleted - A `completed` event. /// RACEventTypeError - An `error` event. /// RACEventTypeNext - A `next` event. typedef NS_ENUM(NSUInteger, RACEventType) { RACEventTypeCompleted, RACEventTypeError, RACEventTypeNext }; /// Represents an event sent by a RACSignal. /// /// This corresponds to the `Notification` class in Rx. @interface RACEvent<__covariant ValueType> : NSObject /// Returns a singleton RACEvent representing the `completed` event. + (RACEvent *)completedEvent; /// Returns a new event of type RACEventTypeError, containing the given error. + (RACEvent *)eventWithError:(nullable NSError *)error; /// Returns a new event of type RACEventTypeNext, containing the given value. + (RACEvent *)eventWithValue:(nullable ValueType)value; /// The type of event represented by the receiver. @property (nonatomic, assign, readonly) RACEventType eventType; /// Returns whether the receiver is of type RACEventTypeCompleted or /// RACEventTypeError. @property (nonatomic, getter = isFinished, assign, readonly) BOOL finished; /// The error associated with an event of type RACEventTypeError. This will be /// nil for all other event types. @property (nonatomic, strong, readonly, nullable) NSError *error; /// The value associated with an event of type RACEventTypeNext. This will be /// nil for all other event types. @property (nonatomic, strong, readonly, nullable) ValueType value; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACEvent.m ================================================ // // RACEvent.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-01-07. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACEvent.h" @interface RACEvent () // An object associated with this event. This will be used for the error and // value properties. @property (nonatomic, strong, readonly) id object; // Initializes the receiver with the given type and object. - (instancetype)initWithEventType:(RACEventType)type object:(id)object; @end @implementation RACEvent #pragma mark Properties - (BOOL)isFinished { return self.eventType == RACEventTypeCompleted || self.eventType == RACEventTypeError; } - (NSError *)error { return (self.eventType == RACEventTypeError ? self.object : nil); } - (id)value { return (self.eventType == RACEventTypeNext ? self.object : nil); } #pragma mark Lifecycle + (instancetype)completedEvent { static dispatch_once_t pred; static id singleton; dispatch_once(&pred, ^{ singleton = [[self alloc] initWithEventType:RACEventTypeCompleted object:nil]; }); return singleton; } + (instancetype)eventWithError:(NSError *)error { return [[self alloc] initWithEventType:RACEventTypeError object:error]; } + (instancetype)eventWithValue:(id)value { return [[self alloc] initWithEventType:RACEventTypeNext object:value]; } - (instancetype)initWithEventType:(RACEventType)type object:(id)object { self = [super init]; _eventType = type; _object = object; return self; } #pragma mark NSCopying - (id)copyWithZone:(NSZone *)zone { return self; } #pragma mark NSObject - (NSString *)description { NSString *eventDescription = nil; switch (self.eventType) { case RACEventTypeCompleted: eventDescription = @"completed"; break; case RACEventTypeError: eventDescription = [NSString stringWithFormat:@"error = %@", self.object]; break; case RACEventTypeNext: eventDescription = [NSString stringWithFormat:@"next = %@", self.object]; break; default: NSCAssert(NO, @"Unrecognized event type: %i", (int)self.eventType); } return [NSString stringWithFormat:@"<%@: %p>{ %@ }", self.class, self, eventDescription]; } - (NSUInteger)hash { return self.eventType ^ [self.object hash]; } - (BOOL)isEqual:(id)event { if (event == self) return YES; if (![event isKindOfClass:RACEvent.class]) return NO; if (self.eventType != [event eventType]) return NO; // Catches the nil case too. return self.object == [event object] || [self.object isEqual:[event object]]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACGroupedSignal.h ================================================ // // RACGroupedSignal.h // ReactiveObjC // // Created by Josh Abernathy on 5/2/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSubject.h" NS_ASSUME_NONNULL_BEGIN /// A grouped signal is used by -[RACSignal groupBy:transform:]. @interface RACGroupedSignal : RACSubject /// The key shared by the group. @property (nonatomic, readonly, copy) id key; + (instancetype)signalWithKey:(id)key; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACGroupedSignal.m ================================================ // // RACGroupedSignal.m // ReactiveObjC // // Created by Josh Abernathy on 5/2/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACGroupedSignal.h" @interface RACGroupedSignal () @property (nonatomic, copy) id key; @end @implementation RACGroupedSignal #pragma mark API + (instancetype)signalWithKey:(id)key { RACGroupedSignal *subject = [self subject]; subject.key = key; return subject; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACImmediateScheduler.h ================================================ // // RACImmediateScheduler.h // ReactiveObjC // // Created by Josh Abernathy on 11/30/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACScheduler.h" // A private scheduler which immediately executes its scheduled blocks. @interface RACImmediateScheduler : RACScheduler @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACImmediateScheduler.m ================================================ // // RACImmediateScheduler.m // ReactiveObjC // // Created by Josh Abernathy on 11/30/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACImmediateScheduler.h" #import "RACScheduler+Private.h" @implementation RACImmediateScheduler #pragma mark Lifecycle - (instancetype)init { return [super initWithName:@"org.reactivecocoa.ReactiveObjC.RACScheduler.immediateScheduler"]; } #pragma mark RACScheduler - (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != NULL); block(); return nil; } - (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(block != NULL); [NSThread sleepUntilDate:date]; block(); return nil; } - (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { NSCAssert(NO, @"+[RACScheduler immediateScheduler] does not support %@.", NSStringFromSelector(_cmd)); return nil; } - (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock { for (__block NSUInteger remaining = 1; remaining > 0; remaining--) { recursiveBlock(^{ remaining++; }); } return nil; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACIndexSetSequence.h ================================================ // // RACIndexSetSequence.h // ReactiveObjC // // Created by Sergey Gavrilyuk on 12/18/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSequence.h" // Private class that adapts an array to the RACSequence interface. @interface RACIndexSetSequence : RACSequence + (RACSequence *)sequenceWithIndexSet:(NSIndexSet *)indexSet; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACIndexSetSequence.m ================================================ // // RACIndexSetSequence.m // ReactiveObjC // // Created by Sergey Gavrilyuk on 12/18/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACIndexSetSequence.h" @interface RACIndexSetSequence () // A buffer holding the `NSUInteger` values to enumerate over. // // This is mostly used for memory management. Most access should go through // `indexes` instead. @property (nonatomic, strong, readonly) NSData *data; // The indexes that this sequence should enumerate. @property (nonatomic, readonly) const NSUInteger *indexes; // The number of indexes to enumerate. @property (nonatomic, readonly) NSUInteger count; @end @implementation RACIndexSetSequence #pragma mark Lifecycle + (RACSequence *)sequenceWithIndexSet:(NSIndexSet *)indexSet { NSUInteger count = indexSet.count; if (count == 0) return self.empty; NSUInteger sizeInBytes = sizeof(NSUInteger) * count; NSMutableData *data = [[NSMutableData alloc] initWithCapacity:sizeInBytes]; [indexSet getIndexes:data.mutableBytes maxCount:count inIndexRange:NULL]; RACIndexSetSequence *seq = [[self alloc] init]; seq->_data = data; seq->_indexes = data.bytes; seq->_count = count; return seq; } + (instancetype)sequenceWithIndexSetSequence:(RACIndexSetSequence *)indexSetSequence offset:(NSUInteger)offset { NSCParameterAssert(offset < indexSetSequence.count); RACIndexSetSequence *seq = [[self alloc] init]; seq->_data = indexSetSequence.data; seq->_indexes = indexSetSequence.indexes + offset; seq->_count = indexSetSequence.count - offset; return seq; } #pragma mark RACSequence - (id)head { return @(self.indexes[0]); } - (RACSequence *)tail { if (self.count <= 1) return [RACSequence empty]; return [self.class sequenceWithIndexSetSequence:self offset:1]; } #pragma mark NSFastEnumeration - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id[])stackbuf count:(NSUInteger)len { NSCParameterAssert(len > 0); if (state->state >= self.count) { // Enumeration has completed. return 0; } if (state->state == 0) { // Enumeration begun, mark the mutation flag. state->mutationsPtr = state->extra; } state->itemsPtr = stackbuf; unsigned long index = 0; while (index < MIN(self.count - state->state, len)) { stackbuf[index] = @(self.indexes[index + state->state]); ++index; } state->state += index; return index; } #pragma mark NSObject - (NSString *)description { NSMutableString *indexesStr = [NSMutableString string]; for (unsigned int i = 0; i < self.count; ++i) { if (i > 0) [indexesStr appendString:@", "]; [indexesStr appendFormat:@"%lu", (unsigned long)self.indexes[i]]; } return [NSString stringWithFormat:@"<%@: %p>{ name = %@, indexes = %@ }", self.class, self, self.name, indexesStr]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACKVOChannel.h ================================================ // // RACKVOChannel.h // ReactiveObjC // // Created by Uri Baghin on 27/12/2012. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACChannel.h" #import #import "RACmetamacros.h" /// Creates a RACKVOChannel to the given key path. When the targeted object /// deallocates, the channel will complete. /// /// If RACChannelTo() is used as an expression, it returns a RACChannelTerminal that /// can be used to watch the specified property for changes, and set new values /// for it. The terminal will start with the property's current value upon /// subscription. /// /// If RACChannelTo() is used on the left-hand side of an assignment, there must a /// RACChannelTerminal on the right-hand side of the assignment. The two will be /// subscribed to one another: the property's value is immediately set to the /// value of the channel terminal on the right-hand side, and subsequent changes /// to either terminal will be reflected on the other. /// /// There are two different versions of this macro: /// /// - RACChannelTo(TARGET, KEYPATH, NILVALUE) will create a channel to the `KEYPATH` /// of `TARGET`. If the terminal is ever sent a `nil` value, the property will /// be set to `NILVALUE` instead. `NILVALUE` may itself be `nil` for object /// properties, but an NSValue should be used for primitive properties, to /// avoid an exception if `nil` is sent (which might occur if an intermediate /// object is set to `nil`). /// - RACChannelTo(TARGET, KEYPATH) is the same as the above, but `NILVALUE` defaults to /// `nil`. /// /// Examples /// /// RACChannelTerminal *integerChannel = RACChannelTo(self, integerProperty, @42); /// /// // Sets self.integerProperty to 5. /// [integerChannel sendNext:@5]; /// /// // Logs the current value of self.integerProperty, and all future changes. /// [integerChannel subscribeNext:^(id value) { /// NSLog(@"value: %@", value); /// }]; /// /// // Binds properties to each other, taking the initial value from the right /// side. /// RACChannelTo(view, objectProperty) = RACChannelTo(model, objectProperty); /// RACChannelTo(view, integerProperty, @2) = RACChannelTo(model, integerProperty, @10); #define RACChannelTo(TARGET, ...) \ metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__)) \ (RACChannelTo_(TARGET, __VA_ARGS__, nil)) \ (RACChannelTo_(TARGET, __VA_ARGS__)) /// Do not use this directly. Use the RACChannelTo macro above. #define RACChannelTo_(TARGET, KEYPATH, NILVALUE) \ [[RACKVOChannel alloc] initWithTarget:(TARGET) keyPath:@keypath(TARGET, KEYPATH) nilValue:(NILVALUE)][@keypath(RACKVOChannel.new, followingTerminal)] NS_ASSUME_NONNULL_BEGIN /// A RACChannel that observes a KVO-compliant key path for changes. @interface RACKVOChannel : RACChannel /// Initializes a channel that will observe the given object and key path. /// /// The current value of the key path, and future KVO notifications for the given /// key path, will be sent to subscribers of the channel's `followingTerminal`. /// Values sent to the `followingTerminal` will be set at the given key path using /// key-value coding. /// /// When the target object deallocates, the channel will complete. Signal errors /// are considered undefined behavior. /// /// This is the designated initializer for this class. /// /// target - The object to bind to. /// keyPath - The key path to observe and set the value of. /// nilValue - The value to set at the key path whenever a `nil` value is /// received. This may be nil when connecting to object properties, but /// an NSValue should be used for primitive properties, to avoid an /// exception if `nil` is received (which might occur if an intermediate /// object is set to `nil`). #if OS_OBJECT_HAVE_OBJC_SUPPORT - (instancetype)initWithTarget:(__weak NSObject *)target keyPath:(NSString *)keyPath nilValue:(nullable ValueType)nilValue; #else // Swift builds with OS_OBJECT_HAVE_OBJC_SUPPORT=0 for Playgrounds and LLDB :( - (instancetype)initWithTarget:(NSObject *)target keyPath:(NSString *)keyPath nilValue:(nullable ValueType)nilValue; #endif - (instancetype)init __attribute__((unavailable("Use -initWithTarget:keyPath:nilValue: instead"))); @end /// Methods needed for the convenience macro. Do not call explicitly. @interface RACKVOChannel (RACChannelTo) - (RACChannelTerminal *)objectForKeyedSubscript:(NSString *)key; - (void)setObject:(RACChannelTerminal *)otherTerminal forKeyedSubscript:(NSString *)key; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACKVOChannel.m ================================================ // // RACKVOChannel.m // ReactiveObjC // // Created by Uri Baghin on 27/12/2012. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACKVOChannel.h" #import #import "NSObject+RACDeallocating.h" #import "NSObject+RACKVOWrapper.h" #import "NSString+RACKeyPathUtilities.h" #import "RACChannel.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACSignal+Operations.h" // Key for the array of RACKVOChannel's additional thread local // data in the thread dictionary. static NSString * const RACKVOChannelDataDictionaryKey = @"RACKVOChannelKey"; // Wrapper class for additional thread local data. @interface RACKVOChannelData : NSObject // The flag used to ignore updates the channel itself has triggered. @property (nonatomic, assign) BOOL ignoreNextUpdate; // A pointer to the owner of the data. Only use this for pointer comparison, // never as an object reference. @property (nonatomic, assign) void *owner; + (instancetype)dataForChannel:(RACKVOChannel *)channel; @end @interface RACKVOChannel () // The object whose key path the channel is wrapping. @property (atomic, weak) NSObject *target; // The key path the channel is wrapping. @property (nonatomic, copy, readonly) NSString *keyPath; // Returns the existing thread local data container or nil if none exists. @property (nonatomic, strong, readonly) RACKVOChannelData *currentThreadData; // Creates the thread local data container for the channel. - (void)createCurrentThreadData; // Destroy the thread local data container for the channel. - (void)destroyCurrentThreadData; @end @implementation RACKVOChannel #pragma mark Properties - (RACKVOChannelData *)currentThreadData { NSMutableArray *dataArray = NSThread.currentThread.threadDictionary[RACKVOChannelDataDictionaryKey]; for (RACKVOChannelData *data in dataArray) { if (data.owner == (__bridge void *)self) return data; } return nil; } #pragma mark Lifecycle - (instancetype)initWithTarget:(__weak NSObject *)target keyPath:(NSString *)keyPath nilValue:(id)nilValue { NSCParameterAssert(keyPath.rac_keyPathComponents.count > 0); NSObject *strongTarget = target; self = [super init]; _target = target; _keyPath = [keyPath copy]; [self.leadingTerminal setNameWithFormat:@"[-initWithTarget: %@ keyPath: %@ nilValue: %@] -leadingTerminal", target, keyPath, nilValue]; [self.followingTerminal setNameWithFormat:@"[-initWithTarget: %@ keyPath: %@ nilValue: %@] -followingTerminal", target, keyPath, nilValue]; if (strongTarget == nil) { [self.leadingTerminal sendCompleted]; return self; } // Observe the key path on target for changes and forward the changes to the // terminal. // // Intentionally capturing `self` strongly in the blocks below, so the // channel object stays alive while observing. RACDisposable *observationDisposable = [strongTarget rac_observeKeyPath:keyPath options:NSKeyValueObservingOptionInitial observer:nil block:^(id value, NSDictionary *change, BOOL causedByDealloc, BOOL affectedOnlyLastComponent) { // If the change wasn't triggered by deallocation, only affects the last // path component, and ignoreNextUpdate is set, then it was triggered by // this channel and should not be forwarded. if (!causedByDealloc && affectedOnlyLastComponent && self.currentThreadData.ignoreNextUpdate) { [self destroyCurrentThreadData]; return; } [self.leadingTerminal sendNext:value]; }]; NSString *keyPathByDeletingLastKeyPathComponent = keyPath.rac_keyPathByDeletingLastKeyPathComponent; NSArray *keyPathComponents = keyPath.rac_keyPathComponents; NSUInteger keyPathComponentsCount = keyPathComponents.count; NSString *lastKeyPathComponent = keyPathComponents.lastObject; // Update the value of the property with the values received. [[self.leadingTerminal finally:^{ [observationDisposable dispose]; }] subscribeNext:^(id x) { // Check the value of the second to last key path component. Since the // channel can only update the value of a property on an object, and not // update intermediate objects, it can only update the value of the whole // key path if this object is not nil. NSObject *object = (keyPathComponentsCount > 1 ? [self.target valueForKeyPath:keyPathByDeletingLastKeyPathComponent] : self.target); if (object == nil) return; // Set the ignoreNextUpdate flag before setting the value so this channel // ignores the value in the subsequent -didChangeValueForKey: callback. [self createCurrentThreadData]; self.currentThreadData.ignoreNextUpdate = YES; [object setValue:x ?: nilValue forKey:lastKeyPathComponent]; } error:^(NSError *error) { NSCAssert(NO, @"Received error in %@: %@", self, error); // Log the error if we're running with assertions disabled. NSLog(@"Received error in %@: %@", self, error); }]; // Capture `self` weakly for the target's deallocation disposable, so we can // freely deallocate if we complete before then. @weakify(self); [strongTarget.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ @strongify(self); [self.leadingTerminal sendCompleted]; self.target = nil; }]]; return self; } - (void)createCurrentThreadData { NSMutableArray *dataArray = NSThread.currentThread.threadDictionary[RACKVOChannelDataDictionaryKey]; if (dataArray == nil) { dataArray = [NSMutableArray array]; NSThread.currentThread.threadDictionary[RACKVOChannelDataDictionaryKey] = dataArray; [dataArray addObject:[RACKVOChannelData dataForChannel:self]]; return; } for (RACKVOChannelData *data in dataArray) { if (data.owner == (__bridge void *)self) return; } [dataArray addObject:[RACKVOChannelData dataForChannel:self]]; } - (void)destroyCurrentThreadData { NSMutableArray *dataArray = NSThread.currentThread.threadDictionary[RACKVOChannelDataDictionaryKey]; NSUInteger index = [dataArray indexOfObjectPassingTest:^ BOOL (RACKVOChannelData *data, NSUInteger idx, BOOL *stop) { return data.owner == (__bridge void *)self; }]; if (index != NSNotFound) [dataArray removeObjectAtIndex:index]; } @end @implementation RACKVOChannel (RACChannelTo) - (RACChannelTerminal *)objectForKeyedSubscript:(NSString *)key { NSCParameterAssert(key != nil); RACChannelTerminal *terminal = [self valueForKey:key]; NSCAssert([terminal isKindOfClass:RACChannelTerminal.class], @"Key \"%@\" does not identify a channel terminal", key); return terminal; } - (void)setObject:(RACChannelTerminal *)otherTerminal forKeyedSubscript:(NSString *)key { NSCParameterAssert(otherTerminal != nil); RACChannelTerminal *selfTerminal = [self objectForKeyedSubscript:key]; [otherTerminal subscribe:selfTerminal]; [[selfTerminal skip:1] subscribe:otherTerminal]; } @end @implementation RACKVOChannelData + (instancetype)dataForChannel:(RACKVOChannel *)channel { RACKVOChannelData *data = [[self alloc] init]; data->_owner = (__bridge void *)channel; return data; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACKVOProxy.h ================================================ // // RACKVOProxy.h // ReactiveObjC // // Created by Richard Speyer on 4/10/14. // Copyright (c) 2014 GitHub, Inc. All rights reserved. // #import /// A singleton that can act as a proxy between a KVO observation and a RAC /// subscriber, in order to protect against KVO lifetime issues. @interface RACKVOProxy : NSObject /// Returns the singleton KVO proxy object. + (instancetype)sharedProxy; /// Registers an observer with the proxy, such that when the proxy receives a /// KVO change with the given context, it forwards it to the observer. /// /// observer - True observer of the KVO change. Must not be nil. /// context - Arbitrary context object used to differentiate multiple /// observations of the same keypath. Must be unique, cannot be nil. - (void)addObserver:(__weak NSObject *)observer forContext:(void *)context; /// Removes an observer from the proxy. Parameters must match those passed to /// addObserver:forContext:. /// /// observer - True observer of the KVO change. Must not be nil. /// context - Arbitrary context object used to differentiate multiple /// observations of the same keypath. Must be unique, cannot be nil. - (void)removeObserver:(NSObject *)observer forContext:(void *)context; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACKVOProxy.m ================================================ // // RACKVOProxy.m // ReactiveObjC // // Created by Richard Speyer on 4/10/14. // Copyright (c) 2014 GitHub, Inc. All rights reserved. // #import "RACKVOProxy.h" @interface RACKVOProxy() @property (strong, nonatomic, readonly) NSMapTable *trampolines; @property (strong, nonatomic, readonly) dispatch_queue_t queue; @end @implementation RACKVOProxy + (instancetype)sharedProxy { static RACKVOProxy *proxy; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ proxy = [[self alloc] init]; }); return proxy; } - (instancetype)init { self = [super init]; _queue = dispatch_queue_create("org.reactivecocoa.ReactiveObjC.RACKVOProxy", DISPATCH_QUEUE_SERIAL); _trampolines = [NSMapTable strongToWeakObjectsMapTable]; return self; } - (void)addObserver:(__weak NSObject *)observer forContext:(void *)context { NSValue *valueContext = [NSValue valueWithPointer:context]; dispatch_sync(self.queue, ^{ [self.trampolines setObject:observer forKey:valueContext]; }); } - (void)removeObserver:(NSObject *)observer forContext:(void *)context { NSValue *valueContext = [NSValue valueWithPointer:context]; dispatch_sync(self.queue, ^{ [self.trampolines removeObjectForKey:valueContext]; }); } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { NSValue *valueContext = [NSValue valueWithPointer:context]; __block NSObject *trueObserver; dispatch_sync(self.queue, ^{ trueObserver = [self.trampolines objectForKey:valueContext]; }); if (trueObserver != nil) { [trueObserver observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACKVOTrampoline.h ================================================ // // RACKVOTrampoline.h // ReactiveObjC // // Created by Josh Abernathy on 1/15/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import #import "NSObject+RACKVOWrapper.h" #import "RACDisposable.h" // A private trampoline object that represents a KVO observation. // // Disposing of the trampoline will stop observation. @interface RACKVOTrampoline : RACDisposable // Initializes the receiver with the given parameters. // // target - The object whose key path should be observed. Cannot be nil. // observer - The object that gets notified when the value at the key path // changes. Can be nil. // keyPath - The key path on `target` to observe. Cannot be nil. // options - Any key value observing options to use in the observation. // block - The block to call when the value at the observed key path changes. // Cannot be nil. // // Returns the initialized object. - (instancetype)initWithTarget:(__weak NSObject *)target observer:(__weak NSObject *)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(RACKVOBlock)block; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACKVOTrampoline.m ================================================ // // RACKVOTrampoline.m // ReactiveObjC // // Created by Josh Abernathy on 1/15/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACKVOTrampoline.h" #import "NSObject+RACDeallocating.h" #import "RACCompoundDisposable.h" #import "RACKVOProxy.h" @interface RACKVOTrampoline () // The keypath which the trampoline is observing. @property (nonatomic, readonly, copy) NSString *keyPath; // These properties should only be manipulated while synchronized on the // receiver. @property (nonatomic, readonly, copy) RACKVOBlock block; @property (nonatomic, readonly, unsafe_unretained) NSObject *unsafeTarget; @property (nonatomic, readonly, weak) NSObject *weakTarget; @property (nonatomic, readonly, weak) NSObject *observer; @end @implementation RACKVOTrampoline #pragma mark Lifecycle - (instancetype)initWithTarget:(__weak NSObject *)target observer:(__weak NSObject *)observer keyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options block:(RACKVOBlock)block { NSCParameterAssert(keyPath != nil); NSCParameterAssert(block != nil); NSObject *strongTarget = target; if (strongTarget == nil) return nil; self = [super init]; _keyPath = [keyPath copy]; _block = [block copy]; _weakTarget = target; _unsafeTarget = strongTarget; _observer = observer; [RACKVOProxy.sharedProxy addObserver:self forContext:(__bridge void *)self]; [strongTarget addObserver:RACKVOProxy.sharedProxy forKeyPath:self.keyPath options:options context:(__bridge void *)self]; [strongTarget.rac_deallocDisposable addDisposable:self]; [self.observer.rac_deallocDisposable addDisposable:self]; return self; } - (void)dealloc { [self dispose]; } #pragma mark Observation - (void)dispose { NSObject *target; NSObject *observer; @synchronized (self) { _block = nil; // The target should still exist at this point, because we still need to // tear down its KVO observation. Therefore, we can use the unsafe // reference (and need to, because the weak one will have been zeroed by // now). target = self.unsafeTarget; observer = self.observer; _unsafeTarget = nil; _observer = nil; } [target.rac_deallocDisposable removeDisposable:self]; [observer.rac_deallocDisposable removeDisposable:self]; [target removeObserver:RACKVOProxy.sharedProxy forKeyPath:self.keyPath context:(__bridge void *)self]; [RACKVOProxy.sharedProxy removeObserver:self forContext:(__bridge void *)self]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context != (__bridge void *)self) { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; return; } RACKVOBlock block; id observer; id target; @synchronized (self) { block = self.block; observer = self.observer; target = self.weakTarget; } if (block == nil || target == nil) return; block(target, observer, change); } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACMulticastConnection+Private.h ================================================ // // RACMulticastConnection+Private.h // ReactiveObjC // // Created by Josh Abernathy on 4/11/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACMulticastConnection.h" @class RACSubject; @interface RACMulticastConnection<__covariant ValueType> () - (instancetype)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACMulticastConnection.h ================================================ // // RACMulticastConnection.h // ReactiveObjC // // Created by Josh Abernathy on 4/11/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import #import "RACAnnotations.h" @class RACDisposable; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN /// A multicast connection encapsulates the idea of sharing one subscription to a /// signal to many subscribers. This is most often needed if the subscription to /// the underlying signal involves side-effects or shouldn't be called more than /// once. /// /// The multicasted signal is only subscribed to when /// -[RACMulticastConnection connect] is called. Until that happens, no values /// will be sent on `signal`. See -[RACMulticastConnection autoconnect] for how /// -[RACMulticastConnection connect] can be called automatically. /// /// Note that you shouldn't create RACMulticastConnection manually. Instead use /// -[RACSignal publish] or -[RACSignal multicast:]. @interface RACMulticastConnection<__covariant ValueType> : NSObject /// The multicasted signal. @property (nonatomic, strong, readonly) RACSignal *signal; /// Connect to the underlying signal by subscribing to it. Calling this multiple /// times does nothing but return the existing connection's disposable. /// /// Returns the disposable for the subscription to the multicasted signal. - (RACDisposable *)connect; /// Connects to the underlying signal when the returned signal is first /// subscribed to, and disposes of the subscription to the multicasted signal /// when the returned signal has no subscribers. /// /// If new subscribers show up after being disposed, they'll subscribe and then /// be immediately disposed of. The returned signal will never re-connect to the /// multicasted signal. /// /// Returns the autoconnecting signal. - (RACSignal *)autoconnect RAC_WARN_UNUSED_RESULT; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACMulticastConnection.m ================================================ // // RACMulticastConnection.m // ReactiveObjC // // Created by Josh Abernathy on 4/11/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACMulticastConnection.h" #import "RACMulticastConnection+Private.h" #import "RACDisposable.h" #import "RACSerialDisposable.h" #import "RACSubject.h" #import @interface RACMulticastConnection () { RACSubject *_signal; // When connecting, a caller should attempt to atomically swap the value of this // from `0` to `1`. // // If the swap is successful the caller is resposible for subscribing `_signal` // to `sourceSignal` and storing the returned disposable in `serialDisposable`. // // If the swap is unsuccessful it means that `_sourceSignal` has already been // connected and the caller has no action to take. int32_t volatile _hasConnected; } @property (nonatomic, readonly, strong) RACSignal *sourceSignal; @property (strong) RACSerialDisposable *serialDisposable; @end @implementation RACMulticastConnection #pragma mark Lifecycle - (instancetype)initWithSourceSignal:(RACSignal *)source subject:(RACSubject *)subject { NSCParameterAssert(source != nil); NSCParameterAssert(subject != nil); self = [super init]; _sourceSignal = source; _serialDisposable = [[RACSerialDisposable alloc] init]; _signal = subject; return self; } #pragma mark Connecting - (RACDisposable *)connect { BOOL shouldConnect = OSAtomicCompareAndSwap32Barrier(0, 1, &_hasConnected); if (shouldConnect) { self.serialDisposable.disposable = [self.sourceSignal subscribe:_signal]; } return self.serialDisposable; } - (RACSignal *)autoconnect { __block volatile int32_t subscriberCount = 0; return [[RACSignal createSignal:^(id subscriber) { OSAtomicIncrement32Barrier(&subscriberCount); RACDisposable *subscriptionDisposable = [self.signal subscribe:subscriber]; RACDisposable *connectionDisposable = [self connect]; return [RACDisposable disposableWithBlock:^{ [subscriptionDisposable dispose]; if (OSAtomicDecrement32Barrier(&subscriberCount) == 0) { [connectionDisposable dispose]; } }]; }] setNameWithFormat:@"[%@] -autoconnect", self.signal.name]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACPassthroughSubscriber.h ================================================ // // RACPassthroughSubscriber.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-06-13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import #import "RACSubscriber.h" @class RACCompoundDisposable; @class RACSignal<__covariant ValueType>; // A private subscriber that passes through all events to another subscriber // while not disposed. @interface RACPassthroughSubscriber : NSObject // Initializes the receiver to pass through events until disposed. // // subscriber - The subscriber to forward events to. This must not be nil. // signal - The signal that will be sending events to the receiver. // disposable - When this disposable is disposed, no more events will be // forwarded. This must not be nil. // // Returns an initialized passthrough subscriber. - (instancetype)initWithSubscriber:(id)subscriber signal:(RACSignal *)signal disposable:(RACCompoundDisposable *)disposable; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACPassthroughSubscriber.m ================================================ // // RACPassthroughSubscriber.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-06-13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACPassthroughSubscriber.h" #import "RACCompoundDisposable.h" #import "RACSignal.h" #import "RACSignalProvider.h" #if !defined(DTRACE_PROBES_DISABLED) || !DTRACE_PROBES_DISABLED static const char *cleanedDTraceString(NSString *original) { return [original stringByReplacingOccurrencesOfString:@"\\s+" withString:@" " options:NSRegularExpressionSearch range:NSMakeRange(0, original.length)].UTF8String; } static const char *cleanedSignalDescription(RACSignal *signal) { NSString *desc = signal.description; NSRange range = [desc rangeOfString:@" name:"]; if (range.location != NSNotFound) { desc = [desc stringByReplacingCharactersInRange:range withString:@""]; } return cleanedDTraceString(desc); } #endif @interface RACPassthroughSubscriber () // The subscriber to which events should be forwarded. @property (nonatomic, strong, readonly) id innerSubscriber; // The signal sending events to this subscriber. // // This property isn't `weak` because it's only used for DTrace probes, so // a zeroing weak reference would incur an unnecessary performance penalty in // normal usage. @property (nonatomic, unsafe_unretained, readonly) RACSignal *signal; // A disposable representing the subscription. When disposed, no further events // should be sent to the `innerSubscriber`. @property (nonatomic, strong, readonly) RACCompoundDisposable *disposable; @end @implementation RACPassthroughSubscriber #pragma mark Lifecycle - (instancetype)initWithSubscriber:(id)subscriber signal:(RACSignal *)signal disposable:(RACCompoundDisposable *)disposable { NSCParameterAssert(subscriber != nil); self = [super init]; _innerSubscriber = subscriber; _signal = signal; _disposable = disposable; [self.innerSubscriber didSubscribeWithDisposable:self.disposable]; return self; } #pragma mark RACSubscriber - (void)sendNext:(id)value { if (self.disposable.disposed) return; if (RACSIGNAL_NEXT_ENABLED()) { RACSIGNAL_NEXT(cleanedSignalDescription(self.signal), cleanedDTraceString(self.innerSubscriber.description), cleanedDTraceString([value description])); } [self.innerSubscriber sendNext:value]; } - (void)sendError:(NSError *)error { if (self.disposable.disposed) return; if (RACSIGNAL_ERROR_ENABLED()) { RACSIGNAL_ERROR(cleanedSignalDescription(self.signal), cleanedDTraceString(self.innerSubscriber.description), cleanedDTraceString(error.description)); } [self.innerSubscriber sendError:error]; } - (void)sendCompleted { if (self.disposable.disposed) return; if (RACSIGNAL_COMPLETED_ENABLED()) { RACSIGNAL_COMPLETED(cleanedSignalDescription(self.signal), cleanedDTraceString(self.innerSubscriber.description)); } [self.innerSubscriber sendCompleted]; } - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable { if (disposable != self.disposable) { [self.disposable addDisposable:disposable]; } } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACQueueScheduler+Subclass.h ================================================ // // RACQueueScheduler+Subclass.h // ReactiveObjC // // Created by Josh Abernathy on 6/6/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACQueueScheduler.h" #import "RACScheduler+Subclass.h" NS_ASSUME_NONNULL_BEGIN /// An interface for use by GCD queue-based subclasses. /// /// See RACScheduler+Subclass.h for subclassing notes. @interface RACQueueScheduler () /// The queue on which blocks are enqueued. #if OS_OBJECT_USE_OBJC @property (nonatomic, strong, readonly) dispatch_queue_t queue; #else // Swift builds with OS_OBJECT_HAVE_OBJC_SUPPORT=0 for Playgrounds and LLDB :( @property (nonatomic, assign, readonly) dispatch_queue_t queue; #endif /// Initializes the receiver with the name of the scheduler and the queue which /// the scheduler should use. /// /// name - The name of the scheduler. If nil, a default name will be used. /// queue - The queue upon which the receiver should enqueue scheduled blocks. /// This argument must not be NULL. /// /// Returns the initialized object. - (instancetype)initWithName:(nullable NSString *)name queue:(dispatch_queue_t)queue; /// Converts a date into a GCD time using dispatch_walltime(). /// /// date - The date to convert. This must not be nil. + (dispatch_time_t)wallTimeWithDate:(NSDate *)date; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACQueueScheduler.h ================================================ // // RACQueueScheduler.h // ReactiveObjC // // Created by Josh Abernathy on 11/30/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACScheduler.h" NS_ASSUME_NONNULL_BEGIN /// An abstract scheduler which asynchronously enqueues all its work to a Grand /// Central Dispatch queue. /// /// Because RACQueueScheduler is abstract, it should not be instantiated /// directly. Create a subclass using the `RACQueueScheduler+Subclass.h` /// interface and use that instead. @interface RACQueueScheduler : RACScheduler @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACQueueScheduler.m ================================================ // // RACQueueScheduler.m // ReactiveObjC // // Created by Josh Abernathy on 11/30/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACQueueScheduler.h" #import "RACDisposable.h" #import "RACQueueScheduler+Subclass.h" #import "RACScheduler+Private.h" @implementation RACQueueScheduler #pragma mark Lifecycle - (instancetype)initWithName:(NSString *)name queue:(dispatch_queue_t)queue { NSCParameterAssert(queue != NULL); self = [super initWithName:name]; _queue = queue; #if !OS_OBJECT_USE_OBJC dispatch_retain(_queue); #endif return self; } #if !OS_OBJECT_USE_OBJC - (void)dealloc { if (_queue != NULL) { dispatch_release(_queue); _queue = NULL; } } #endif #pragma mark Date Conversions + (dispatch_time_t)wallTimeWithDate:(NSDate *)date { NSCParameterAssert(date != nil); double seconds = 0; double frac = modf(date.timeIntervalSince1970, &seconds); struct timespec walltime = { .tv_sec = (time_t)fmin(fmax(seconds, LONG_MIN), LONG_MAX), .tv_nsec = (long)fmin(fmax(frac * NSEC_PER_SEC, LONG_MIN), LONG_MAX) }; return dispatch_walltime(&walltime, 0); } #pragma mark RACScheduler - (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != NULL); RACDisposable *disposable = [[RACDisposable alloc] init]; dispatch_async(self.queue, ^{ if (disposable.disposed) return; [self performAsCurrentScheduler:block]; }); return disposable; } - (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(block != NULL); RACDisposable *disposable = [[RACDisposable alloc] init]; dispatch_after([self.class wallTimeWithDate:date], self.queue, ^{ if (disposable.disposed) return; [self performAsCurrentScheduler:block]; }); return disposable; } - (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(interval > 0.0 && interval < INT64_MAX / NSEC_PER_SEC); NSCParameterAssert(leeway >= 0.0 && leeway < INT64_MAX / NSEC_PER_SEC); NSCParameterAssert(block != NULL); uint64_t intervalInNanoSecs = (uint64_t)(interval * NSEC_PER_SEC); uint64_t leewayInNanoSecs = (uint64_t)(leeway * NSEC_PER_SEC); dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.queue); dispatch_source_set_timer(timer, [self.class wallTimeWithDate:date], intervalInNanoSecs, leewayInNanoSecs); dispatch_source_set_event_handler(timer, block); dispatch_resume(timer); return [RACDisposable disposableWithBlock:^{ dispatch_source_cancel(timer); }]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACReplaySubject.h ================================================ // // RACReplaySubject.h // ReactiveObjC // // Created by Josh Abernathy on 3/14/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSubject.h" NS_ASSUME_NONNULL_BEGIN extern const NSUInteger RACReplaySubjectUnlimitedCapacity; /// A replay subject saves the values it is sent (up to its defined capacity) /// and resends those to new subscribers. It will also replay an error or /// completion. @interface RACReplaySubject : RACSubject /// Creates a new replay subject with the given capacity. A capacity of /// RACReplaySubjectUnlimitedCapacity means values are never trimmed. + (instancetype)replaySubjectWithCapacity:(NSUInteger)capacity; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACReplaySubject.m ================================================ // // RACReplaySubject.m // ReactiveObjC // // Created by Josh Abernathy on 3/14/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACReplaySubject.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACScheduler+Private.h" #import "RACSubscriber.h" #import "RACTuple.h" const NSUInteger RACReplaySubjectUnlimitedCapacity = NSUIntegerMax; @interface RACReplaySubject () @property (nonatomic, assign, readonly) NSUInteger capacity; // These properties should only be modified while synchronized on self. @property (nonatomic, strong, readonly) NSMutableArray *valuesReceived; @property (nonatomic, assign) BOOL hasCompleted; @property (nonatomic, assign) BOOL hasError; @property (nonatomic, strong) NSError *error; @end @implementation RACReplaySubject #pragma mark Lifecycle + (instancetype)replaySubjectWithCapacity:(NSUInteger)capacity { return [(RACReplaySubject *)[self alloc] initWithCapacity:capacity]; } - (instancetype)init { return [self initWithCapacity:RACReplaySubjectUnlimitedCapacity]; } - (instancetype)initWithCapacity:(NSUInteger)capacity { self = [super init]; _capacity = capacity; _valuesReceived = (capacity == RACReplaySubjectUnlimitedCapacity ? [NSMutableArray array] : [NSMutableArray arrayWithCapacity:capacity]); return self; } #pragma mark RACSignal - (RACDisposable *)subscribe:(id)subscriber { RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable]; RACDisposable *schedulingDisposable = [RACScheduler.subscriptionScheduler schedule:^{ @synchronized (self) { for (id value in self.valuesReceived) { if (compoundDisposable.disposed) return; [subscriber sendNext:(value == RACTupleNil.tupleNil ? nil : value)]; } if (compoundDisposable.disposed) return; if (self.hasCompleted) { [subscriber sendCompleted]; } else if (self.hasError) { [subscriber sendError:self.error]; } else { RACDisposable *subscriptionDisposable = [super subscribe:subscriber]; [compoundDisposable addDisposable:subscriptionDisposable]; } } }]; [compoundDisposable addDisposable:schedulingDisposable]; return compoundDisposable; } #pragma mark RACSubscriber - (void)sendNext:(id)value { @synchronized (self) { [self.valuesReceived addObject:value ?: RACTupleNil.tupleNil]; if (self.capacity != RACReplaySubjectUnlimitedCapacity && self.valuesReceived.count > self.capacity) { [self.valuesReceived removeObjectsInRange:NSMakeRange(0, self.valuesReceived.count - self.capacity)]; } [super sendNext:value]; } } - (void)sendCompleted { @synchronized (self) { self.hasCompleted = YES; [super sendCompleted]; } } - (void)sendError:(NSError *)e { @synchronized (self) { self.hasError = YES; self.error = e; [super sendError:e]; } } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACReturnSignal.h ================================================ // // RACReturnSignal.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-10. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSignal.h" // A private `RACSignal` subclasses that synchronously sends a value to any // subscribers, then completes. @interface RACReturnSignal<__covariant ValueType> : RACSignal + (RACSignal *)return:(ValueType)value; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACReturnSignal.m ================================================ // // RACReturnSignal.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-10-10. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACReturnSignal.h" #import "RACScheduler+Private.h" #import "RACSubscriber.h" #import "RACUnit.h" @interface RACReturnSignal () // The value to send upon subscription. @property (nonatomic, strong, readonly) id value; @end @implementation RACReturnSignal #pragma mark Properties // Only allow this signal's name to be customized in DEBUG, since it's // potentially a singleton in release builds (see +return:). - (void)setName:(NSString *)name { #ifdef DEBUG [super setName:name]; #endif } - (NSString *)name { #ifdef DEBUG return super.name; #else return @"+return:"; #endif } #pragma mark Lifecycle + (RACSignal *)return:(id)value { #ifndef DEBUG // In release builds, use singletons for two very common cases. if (value == RACUnit.defaultUnit) { static RACReturnSignal *unitSingleton; static dispatch_once_t unitPred; dispatch_once(&unitPred, ^{ unitSingleton = [[self alloc] init]; unitSingleton->_value = RACUnit.defaultUnit; }); return unitSingleton; } else if (value == nil) { static RACReturnSignal *nilSingleton; static dispatch_once_t nilPred; dispatch_once(&nilPred, ^{ nilSingleton = [[self alloc] init]; nilSingleton->_value = nil; }); return nilSingleton; } #endif RACReturnSignal *signal = [[self alloc] init]; signal->_value = value; #ifdef DEBUG [signal setNameWithFormat:@"+return: %@", value]; #endif return signal; } #pragma mark Subscription - (RACDisposable *)subscribe:(id)subscriber { NSCParameterAssert(subscriber != nil); return [RACScheduler.subscriptionScheduler schedule:^{ [subscriber sendNext:self.value]; [subscriber sendCompleted]; }]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACScheduler+Private.h ================================================ // // RACScheduler+Private.h // ReactiveObjC // // Created by Josh Abernathy on 11/29/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACScheduler.h" NS_ASSUME_NONNULL_BEGIN // The thread-specific current scheduler key. extern NSString * const RACSchedulerCurrentSchedulerKey; // A private interface for internal RAC use only. @interface RACScheduler () // A dedicated scheduler that fills two requirements: // // 1. By the time subscription happens, we need a valid +currentScheduler. // 2. Subscription should happen as soon as possible. // // To fulfill those two, if we already have a valid +currentScheduler, it // immediately executes scheduled blocks. If we don't, it will execute scheduled // blocks with a private background scheduler. + (instancetype)subscriptionScheduler; // Initializes the receiver with the given name. // // name - The name of the scheduler. If nil, a default name will be used. // // Returns the initialized object. - (instancetype)initWithName:(nullable NSString *)name; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACScheduler+Subclass.h ================================================ // // RACScheduler.m // ReactiveObjC // // Created by Miķelis Vindavs on 5/27/14. // Copyright (c) 2014 GitHub, Inc. All rights reserved. // #import #import "RACScheduler.h" NS_ASSUME_NONNULL_BEGIN /// An interface for use by subclasses. /// /// Subclasses should use `-performAsCurrentScheduler:` to do the actual block /// invocation so that +[RACScheduler currentScheduler] behaves as expected. /// /// **Note that RACSchedulers are expected to be serial**. Subclasses must honor /// that contract. See `RACTargetQueueScheduler` for a queue-based scheduler /// which will enforce the serialization guarantee. @interface RACScheduler () /// Performs the given block with the receiver as the current scheduler for /// its thread. This should only be called by subclasses to perform their /// scheduled blocks. /// /// block - The block to execute. Cannot be NULL. - (void)performAsCurrentScheduler:(void (^)(void))block; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACScheduler.h ================================================ // // RACScheduler.h // ReactiveObjC // // Created by Josh Abernathy on 4/16/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN /// The priority for the scheduler. /// /// RACSchedulerPriorityHigh - High priority. /// RACSchedulerPriorityDefault - Default priority. /// RACSchedulerPriorityLow - Low priority. /// RACSchedulerPriorityBackground - Background priority. typedef enum : long { RACSchedulerPriorityHigh = DISPATCH_QUEUE_PRIORITY_HIGH, RACSchedulerPriorityDefault = DISPATCH_QUEUE_PRIORITY_DEFAULT, RACSchedulerPriorityLow = DISPATCH_QUEUE_PRIORITY_LOW, RACSchedulerPriorityBackground = DISPATCH_QUEUE_PRIORITY_BACKGROUND, } RACSchedulerPriority; /// Scheduled with -scheduleRecursiveBlock:, this type of block is passed a block /// with which it can call itself recursively. typedef void (^RACSchedulerRecursiveBlock)(void (^reschedule)(void)); @class RACDisposable; /// Schedulers are used to control when and where work is performed. @interface RACScheduler : NSObject /// A singleton scheduler that immediately executes the blocks it is given. /// /// **Note:** Unlike most other schedulers, this does not set the current /// scheduler. There may still be a valid +currentScheduler if this is used /// within a block scheduled on a different scheduler. + (RACScheduler *)immediateScheduler; /// A singleton scheduler that executes blocks in the main thread. + (RACScheduler *)mainThreadScheduler; /// Creates and returns a new background scheduler with the given priority and /// name. The name is for debug and instrumentation purposes only. /// /// Scheduler creation is cheap. It's unnecessary to save the result of this /// method call unless you want to serialize some actions on the same background /// scheduler. + (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority name:(nullable NSString *)name; /// Invokes +schedulerWithPriority:name: with a default name. + (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority; /// Invokes +schedulerWithPriority: with RACSchedulerPriorityDefault. + (RACScheduler *)scheduler; /// The current scheduler. This will only be valid when used from within a /// -[RACScheduler schedule:] block or when on the main thread. + (nullable RACScheduler *)currentScheduler; /// Schedule the given block for execution on the scheduler. /// /// Scheduled blocks will be executed in the order in which they were scheduled. /// /// block - The block to schedule for execution. Cannot be nil. /// /// Returns a disposable which can be used to cancel the scheduled block before /// it begins executing, or nil if cancellation is not supported. - (nullable RACDisposable *)schedule:(void (^)(void))block; /// Schedule the given block for execution on the scheduler at or after /// a specific time. /// /// Note that blocks scheduled for a certain time will not preempt any other /// scheduled work that is executing at the time. /// /// When invoked on the +immediateScheduler, the calling thread **will block** /// until the specified time. /// /// date - The earliest time at which `block` should begin executing. The block /// may not execute immediately at this time, whether due to system load /// or another block on the scheduler currently being run. Cannot be nil. /// block - The block to schedule for execution. Cannot be nil. /// /// Returns a disposable which can be used to cancel the scheduled block before /// it begins executing, or nil if cancellation is not supported. - (nullable RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block; /// Schedule the given block for execution on the scheduler after the delay. /// /// Converts the delay into an NSDate, then invokes `-after:schedule:`. - (nullable RACDisposable *)afterDelay:(NSTimeInterval)delay schedule:(void (^)(void))block; /// Reschedule the given block at a particular interval, starting at a specific /// time, and with a given leeway for deferral. /// /// Note that blocks scheduled for a certain time will not preempt any other /// scheduled work that is executing at the time. /// /// Regardless of the value of `leeway`, the given block may not execute exactly /// at `when` or exactly on successive intervals, whether due to system load or /// because another block is currently being run on the scheduler. /// /// It is considered undefined behavior to invoke this method on the /// +immediateScheduler. /// /// date - The earliest time at which `block` should begin executing. The /// block may not execute immediately at this time, whether due to /// system load or another block on the scheduler currently being /// run. Cannot be nil. /// interval - The interval at which the block should be rescheduled, starting /// from `date`. This will use the system wall clock, to avoid /// skew when the computer goes to sleep. /// leeway - A hint to the system indicating the number of seconds that each /// scheduling can be deferred. Note that this is just a hint, and /// there may be some additional latency no matter what. /// block - The block to repeatedly schedule for execution. Cannot be nil. /// /// Returns a disposable which can be used to cancel the automatic scheduling and /// rescheduling, or nil if cancellation is not supported. - (nullable RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block; /// Schedule the given recursive block for execution on the scheduler. The /// scheduler will automatically flatten any recursive scheduling into iteration /// instead, so this can be used without issue for blocks that may keep invoking /// themselves forever. /// /// Scheduled blocks will be executed in the order in which they were scheduled. /// /// recursiveBlock - The block to schedule for execution. When invoked, the /// recursive block will be passed a `void (^)(void)` block /// which will reschedule the recursive block at the end of the /// receiver's queue. This passed-in block will automatically /// skip scheduling if the scheduling of the `recursiveBlock` /// was disposed in the meantime. /// /// Returns a disposable which can be used to cancel the scheduled block before /// it begins executing, or to stop it from rescheduling if it's already begun /// execution. - (nullable RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACScheduler.m ================================================ // // RACScheduler.m // ReactiveObjC // // Created by Josh Abernathy on 4/16/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACScheduler.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACImmediateScheduler.h" #import "RACScheduler+Private.h" #import "RACSubscriptionScheduler.h" #import "RACTargetQueueScheduler.h" // The key for the thread-specific current scheduler. NSString * const RACSchedulerCurrentSchedulerKey = @"RACSchedulerCurrentSchedulerKey"; @interface RACScheduler () @property (nonatomic, readonly, copy) NSString *name; @end @implementation RACScheduler #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, self.name]; } #pragma mark Initializers - (instancetype)initWithName:(NSString *)name { self = [super init]; if (name == nil) { _name = [NSString stringWithFormat:@"org.reactivecocoa.ReactiveObjC.%@.anonymousScheduler", self.class]; } else { _name = [name copy]; } return self; } #pragma mark Schedulers + (RACScheduler *)immediateScheduler { static dispatch_once_t onceToken; static RACScheduler *immediateScheduler; dispatch_once(&onceToken, ^{ immediateScheduler = [[RACImmediateScheduler alloc] init]; }); return immediateScheduler; } + (RACScheduler *)mainThreadScheduler { static dispatch_once_t onceToken; static RACScheduler *mainThreadScheduler; dispatch_once(&onceToken, ^{ mainThreadScheduler = [[RACTargetQueueScheduler alloc] initWithName:@"org.reactivecocoa.ReactiveObjC.RACScheduler.mainThreadScheduler" targetQueue:dispatch_get_main_queue()]; }); return mainThreadScheduler; } + (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority name:(NSString *)name { return [[RACTargetQueueScheduler alloc] initWithName:name targetQueue:dispatch_get_global_queue(priority, 0)]; } + (RACScheduler *)schedulerWithPriority:(RACSchedulerPriority)priority { return [self schedulerWithPriority:priority name:@"org.reactivecocoa.ReactiveObjC.RACScheduler.backgroundScheduler"]; } + (RACScheduler *)scheduler { return [self schedulerWithPriority:RACSchedulerPriorityDefault]; } + (RACScheduler *)subscriptionScheduler { static dispatch_once_t onceToken; static RACScheduler *subscriptionScheduler; dispatch_once(&onceToken, ^{ subscriptionScheduler = [[RACSubscriptionScheduler alloc] init]; }); return subscriptionScheduler; } + (BOOL)isOnMainThread { return [NSOperationQueue.currentQueue isEqual:NSOperationQueue.mainQueue] || [NSThread isMainThread]; } + (RACScheduler *)currentScheduler { RACScheduler *scheduler = NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey]; if (scheduler != nil) return scheduler; if ([self.class isOnMainThread]) return RACScheduler.mainThreadScheduler; return nil; } #pragma mark Scheduling - (RACDisposable *)schedule:(void (^)(void))block { NSCAssert(NO, @"%@ must be implemented by subclasses.", NSStringFromSelector(_cmd)); return nil; } - (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { NSCAssert(NO, @"%@ must be implemented by subclasses.", NSStringFromSelector(_cmd)); return nil; } - (RACDisposable *)afterDelay:(NSTimeInterval)delay schedule:(void (^)(void))block { return [self after:[NSDate dateWithTimeIntervalSinceNow:delay] schedule:block]; } - (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { NSCAssert(NO, @"%@ must be implemented by subclasses.", NSStringFromSelector(_cmd)); return nil; } - (RACDisposable *)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock { RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; [self scheduleRecursiveBlock:[recursiveBlock copy] addingToDisposable:disposable]; return disposable; } - (void)scheduleRecursiveBlock:(RACSchedulerRecursiveBlock)recursiveBlock addingToDisposable:(RACCompoundDisposable *)disposable { @autoreleasepool { RACCompoundDisposable *selfDisposable = [RACCompoundDisposable compoundDisposable]; [disposable addDisposable:selfDisposable]; __weak RACDisposable *weakSelfDisposable = selfDisposable; RACDisposable *schedulingDisposable = [self schedule:^{ @autoreleasepool { // At this point, we've been invoked, so our disposable is now useless. [disposable removeDisposable:weakSelfDisposable]; } if (disposable.disposed) return; void (^reallyReschedule)(void) = ^{ if (disposable.disposed) return; [self scheduleRecursiveBlock:recursiveBlock addingToDisposable:disposable]; }; // Protects the variables below. // // This doesn't actually need to be __block qualified, but Clang // complains otherwise. :C __block NSLock *lock = [[NSLock alloc] init]; lock.name = [NSString stringWithFormat:@"%@ %s", self, sel_getName(_cmd)]; __block NSUInteger rescheduleCount = 0; // Set to YES once synchronous execution has finished. Further // rescheduling should occur immediately (rather than being // flattened). __block BOOL rescheduleImmediately = NO; @autoreleasepool { recursiveBlock(^{ [lock lock]; BOOL immediate = rescheduleImmediately; if (!immediate) ++rescheduleCount; [lock unlock]; if (immediate) reallyReschedule(); }); } [lock lock]; NSUInteger synchronousCount = rescheduleCount; rescheduleImmediately = YES; [lock unlock]; for (NSUInteger i = 0; i < synchronousCount; i++) { reallyReschedule(); } }]; [selfDisposable addDisposable:schedulingDisposable]; } } - (void)performAsCurrentScheduler:(void (^)(void))block { NSCParameterAssert(block != NULL); // If we're using a concurrent queue, we could end up in here concurrently, // in which case we *don't* want to clear the current scheduler immediately // after our block is done executing, but only *after* all our concurrent // invocations are done. RACScheduler *previousScheduler = RACScheduler.currentScheduler; NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = self; @autoreleasepool { block(); } if (previousScheduler != nil) { NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = previousScheduler; } else { [NSThread.currentThread.threadDictionary removeObjectForKey:RACSchedulerCurrentSchedulerKey]; } } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACScopedDisposable.h ================================================ // // RACScopedDisposable.h // ReactiveObjC // // Created by Josh Abernathy on 3/28/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACDisposable.h" NS_ASSUME_NONNULL_BEGIN /// A disposable that calls its own -dispose when it is dealloc'd. @interface RACScopedDisposable : RACDisposable /// Creates a new scoped disposable that will also dispose of the given /// disposable when it is dealloc'd. + (instancetype)scopedDisposableWithDisposable:(RACDisposable *)disposable; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACScopedDisposable.m ================================================ // // RACScopedDisposable.m // ReactiveObjC // // Created by Josh Abernathy on 3/28/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACScopedDisposable.h" @implementation RACScopedDisposable #pragma mark Lifecycle + (instancetype)scopedDisposableWithDisposable:(RACDisposable *)disposable { return [self disposableWithBlock:^{ [disposable dispose]; }]; } - (void)dealloc { [self dispose]; } #pragma mark RACDisposable - (RACScopedDisposable *)asScopedDisposable { // totally already are return self; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSequence.h ================================================ // // RACSequence.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import #import "RACStream.h" @class RACTuple; @class RACScheduler; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN /// Represents an immutable sequence of values. Unless otherwise specified, the /// sequences' values are evaluated lazily on demand. Like Cocoa collections, /// sequences cannot contain nil. /// /// Most inherited RACStream methods that accept a block will execute the block /// _at most_ once for each value that is evaluated in the returned sequence. /// Side effects are subject to the behavior described in /// +sequenceWithHeadBlock:tailBlock:. /// /// Implemented as a class cluster. A minimal implementation for a subclass /// consists simply of -head and -tail. @interface RACSequence<__covariant ValueType> : RACStream /// The first object in the sequence, or nil if the sequence is empty. /// /// Subclasses must provide an implementation of this method. @property (nonatomic, strong, readonly, nullable) ValueType head; /// All but the first object in the sequence, or nil if there are no other /// objects. /// /// Subclasses must provide an implementation of this method. @property (nonatomic, strong, readonly, nullable) RACSequence *tail; /// Evaluates the full sequence to produce an equivalently-sized array. @property (nonatomic, copy, readonly) NSArray *array; /// Returns an enumerator of all objects in the sequence. @property (nonatomic, copy, readonly) NSEnumerator *objectEnumerator; /// Converts a sequence into an eager sequence. /// /// An eager sequence fully evaluates all of its values immediately. Sequences /// derived from an eager sequence will also be eager. /// /// Returns a new eager sequence, or the receiver if the sequence is already /// eager. @property (nonatomic, copy, readonly) RACSequence *eagerSequence; /// Converts a sequence into a lazy sequence. /// /// A lazy sequence evaluates its values on demand, as they are accessed. /// Sequences derived from a lazy sequence will also be lazy. /// /// Returns a new lazy sequence, or the receiver if the sequence is already lazy. @property (nonatomic, copy, readonly) RACSequence *lazySequence; /// Invokes -signalWithScheduler: with a new RACScheduler. - (RACSignal *)signal; /// Evaluates the full sequence on the given scheduler. /// /// Each item is evaluated in its own scheduled block, such that control of the /// scheduler is yielded between each value. /// /// Returns a signal which sends the receiver's values on the given scheduler as /// they're evaluated. - (RACSignal *)signalWithScheduler:(RACScheduler *)scheduler; /// Applies a left fold to the sequence. /// /// This is the same as iterating the sequence along with a provided start value. /// This uses a constant amount of memory. A left fold is left-associative so in /// the sequence [1,2,3] the block would applied in the following order: /// reduce(reduce(reduce(start, 1), 2), 3) /// /// start - The starting value for the fold. Used as `accumulator` for the /// first fold. /// reduce - The block used to combine the accumulated value and the next value. /// Cannot be nil. /// /// Returns a reduced value. - (id)foldLeftWithStart:(nullable id)start reduce:(id _Nullable (^)(id _Nullable accumulator, ValueType _Nullable value))reduce; /// Applies a right fold to the sequence. /// /// A right fold is equivalent to recursion on the list. The block is evaluated /// from the right to the left in list. It is right associative so it's applied /// to the rightmost elements first. For example, in the sequence [1,2,3] the /// block is applied in the order: /// reduce(1, reduce(2, reduce(3, start))) /// /// start - The starting value for the fold. /// reduce - The block used to combine the accumulated value and the next head. /// The block is given the accumulated value and the value of the rest /// of the computation (result of the recursion). This is computed when /// you retrieve its value using `rest.head`. This allows you to /// prevent unnecessary computation by not accessing `rest.head` if you /// don't need to. /// /// Returns a reduced value. - (id)foldRightWithStart:(nullable id)start reduce:(id _Nullable (^)(id _Nullable first, RACSequence *rest))reduce; /// Check if any value in sequence passes the block. /// /// block - The block predicate used to check each item. Cannot be nil. /// /// Returns a boolean indiciating if any value in the sequence passed. - (BOOL)any:(BOOL (^)(ValueType _Nullable value))block; /// Check if all values in the sequence pass the block. /// /// block - The block predicate used to check each item. Cannot be nil. /// /// Returns a boolean indicating if all values in the sequence passed. - (BOOL)all:(BOOL (^)(ValueType _Nullable value))block; /// Returns the first object that passes the block. /// /// block - The block predicate used to check each item. Cannot be nil. /// /// Returns an object that passes the block or nil if no objects passed. - (nullable ValueType)objectPassingTest:(BOOL (^)(ValueType _Nullable value))block; /// Creates a sequence that dynamically generates its values. /// /// headBlock - Invoked the first time -head is accessed. /// tailBlock - Invoked the first time -tail is accessed. /// /// The results from each block are memoized, so each block will be invoked at /// most once, no matter how many times the head and tail properties of the /// sequence are accessed. /// /// Any side effects in `headBlock` or `tailBlock` should be thread-safe, since /// the sequence may be evaluated at any time from any thread. Not only that, but /// -tail may be accessed before -head, or both may be accessed simultaneously. /// As noted above, side effects will only be triggered the _first_ time -head or /// -tail is invoked. /// /// Returns a sequence that lazily invokes the given blocks to provide head and /// tail. `headBlock` must not be nil. + (RACSequence *)sequenceWithHeadBlock:(ValueType _Nullable (^)(void))headBlock tailBlock:(nullable RACSequence *(^)(void))tailBlock; @end @interface RACSequence<__covariant ValueType> (RACStream) /// Returns a sequence that immediately sends the given value and then completes. + (RACSequence *)return:(nullable ValueType)value; /// Returns a sequence that immediately completes. + (RACSequence *)empty; /// A block which accepts a value from a RACSequence and returns a new sequence. /// /// Setting `stop` to `YES` will cause the bind to terminate after the returned /// value. Returning `nil` will result in immediate termination. typedef RACSequence * _Nullable (^RACSequenceBindBlock)(ValueType _Nullable value, BOOL *stop); /// Lazily binds a block to the values in the receiver. /// /// This should only be used if you need to terminate the bind early, or close /// over some state. -flattenMap: is more appropriate for all other cases. /// /// block - A block returning a RACSequenceBindBlock. This block will be invoked /// each time the bound sequence is re-evaluated. This block must not be /// nil or return nil. /// /// Returns a new sequence which represents the combined result of all lazy /// applications of `block`. - (RACSequence *)bind:(RACSequenceBindBlock (^)(void))block; /// Subscribes to `sequence` when the source sequence completes. - (RACSequence *)concat:(RACSequence *)sequence; /// Zips the values in the receiver with those of the given sequence to create /// RACTuples. /// /// The first `next` of each sequence will be combined, then the second `next`, /// and so forth, until either sequence completes or errors. /// /// sequence - The sequence to zip with. This must not be `nil`. /// /// Returns a new sequence of RACTuples, representing the combined values of the /// two sequences. Any error from one of the original sequence will be forwarded /// on the returned sequence. - (RACSequence *)zipWith:(RACSequence *)sequence; @end /// Redeclarations of operations built on the RACStream primitives with more /// precise type information. @interface RACSequence<__covariant ValueType> (RACStreamOperations) /// Maps `block` across the values in the receiver and flattens the result. /// /// Note that operators applied _after_ -flattenMap: behave differently from /// operators _within_ -flattenMap:. See the Examples section below. /// /// This corresponds to the `SelectMany` method in Rx. /// /// block - A block which accepts the values in the receiver and returns a new /// instance of the receiver's class. Returning `nil` from this block is /// equivalent to returning an empty sequence. /// /// Returns a new sequence which represents the combined sequences resulting /// from mapping `block`. - (RACSequence *)flattenMap:(__kindof RACSequence * _Nullable (^)(ValueType _Nullable value))block; /// Flattens a sequence of sequences. /// /// This corresponds to the `Merge` method in Rx. /// /// Returns a sequence consisting of the combined sequences obtained from the /// receiver. - (RACSequence *)flatten; /// Maps `block` across the values in the receiver. /// /// This corresponds to the `Select` method in Rx. /// /// Returns a new sequence with the mapped values. - (RACSequence *)map:(id _Nullable (^)(ValueType _Nullable value))block; /// Replaces each value in the receiver with the given object. /// /// Returns a new sequence which includes the given object once for each value in /// the receiver. - (RACSequence *)mapReplace:(nullable id)object; /// Filters out values in the receiver that don't pass the given test. /// /// This corresponds to the `Where` method in Rx. /// /// Returns a new sequence with only those values that passed. - (RACSequence *)filter:(BOOL (^)(id _Nullable value))block; /// Filters out values in the receiver that equal (via -isEqual:) the provided /// value. /// /// value - The value can be `nil`, in which case it ignores `nil` values. /// /// Returns a new sequence containing only the values which did not compare /// equal to `value`. - (RACSequence *)ignore:(nullable ValueType)value; /// Unpacks each RACTuple in the receiver and maps the values to a new value. /// /// reduceBlock - The block which reduces each RACTuple's values into one value. /// It must take as many arguments as the number of tuple elements /// to process. Each argument will be an object argument. The /// return value must be an object. This argument cannot be nil. /// /// Returns a new sequence of reduced tuple values. - (RACSequence *)reduceEach:(RACReduceBlock)reduceBlock; /// Returns a sequence consisting of `value`, followed by the values in the /// receiver. - (RACSequence *)startWith:(nullable ValueType)value; /// Skips the first `skipCount` values in the receiver. /// /// Returns the receiver after skipping the first `skipCount` values. If /// `skipCount` is greater than the number of values in the sequence, an empty /// sequence is returned. - (RACSequence *)skip:(NSUInteger)skipCount; /// Returns a sequence of the first `count` values in the receiver. If `count` is /// greater than or equal to the number of values in the sequence, a sequence /// equivalent to the receiver is returned. - (RACSequence *)take:(NSUInteger)count; /// Zips the values in the given sequences to create RACTuples. /// /// The first value of each sequence will be combined, then the second value, /// and so forth, until at least one of the sequences is exhausted. /// /// sequences - The sequence to combine. If this collection is empty, the /// returned sequence will be empty. /// /// Returns a new sequence containing RACTuples of the zipped values from the /// sequences. + (RACSequence *)zip:(id)sequence; /// Zips sequences using +zip:, then reduces the resulting tuples into a single /// value using -reduceEach: /// /// sequences - The sequences to combine. If this collection is empty, the /// returned sequence will be empty. /// reduceBlock - The block which reduces the values from all the sequences /// into one value. It must take as many arguments as the /// number of sequences given. Each argument will be an object /// argument. The return value must be an object. This argument /// must not be nil. /// /// Example: /// /// [RACSequence zip:@[ stringSequence, intSequence ] /// reduce:^(NSString *string, NSNumber *number) { /// return [NSString stringWithFormat:@"%@: %@", string, number]; /// }]; /// /// Returns a new sequence containing the results from each invocation of /// `reduceBlock`. + (RACSequence *)zip:(id)sequences reduce:(RACReduceBlock)reduceBlock; /// Returns a sequence obtained by concatenating `sequences` in order. + (RACSequence *)concat:(id)sequences; /// Combines values in the receiver from left to right using the given block. /// /// The algorithm proceeds as follows: /// /// 1. `startingValue` is passed into the block as the `running` value, and the /// first element of the receiver is passed into the block as the `next` value. /// 2. The result of the invocation is added to the returned sequence. /// 3. The result of the invocation (`running`) and the next element of the /// receiver (`next`) is passed into `block`. /// 4. Steps 2 and 3 are repeated until all values have been processed. /// /// startingValue - The value to be combined with the first element of the /// receiver. This value may be `nil`. /// reduceBlock - The block that describes how to combine values of the /// receiver. If the receiver is empty, this block will never be /// invoked. Cannot be nil. /// /// Examples /// /// RACSequence *numbers = @[ @1, @2, @3, @4 ].rac_sequence; /// /// // Contains 1, 3, 6, 10 /// RACSequence *sums = [numbers scanWithStart:@0 reduce:^(NSNumber *sum, NSNumber *next) { /// return @(sum.integerValue + next.integerValue); /// }]; /// /// Returns a new sequence that consists of each application of `reduceBlock`. If /// the receiver is empty, an empty sequence is returned. - (RACSequence *)scanWithStart:(nullable id)startingValue reduce:(id _Nullable (^)(id _Nullable running, ValueType _Nullable next))reduceBlock; /// Combines values in the receiver from left to right using the given block /// which also takes zero-based index of the values. /// /// startingValue - The value to be combined with the first element of the /// receiver. This value may be `nil`. /// reduceBlock - The block that describes how to combine values of the /// receiver. This block takes zero-based index value as the last /// parameter. If the receiver is empty, this block will never /// be invoked. Cannot be nil. /// /// Returns a new sequence that consists of each application of `reduceBlock`. /// If the receiver is empty, an empty sequence is returned. - (RACSequence *)scanWithStart:(nullable id)startingValue reduceWithIndex:(id _Nullable (^)(id _Nullable running, ValueType _Nullable next, NSUInteger index))reduceBlock; /// Combines each previous and current value into one object. /// /// This method is similar to -scanWithStart:reduce:, but only ever operates on /// the previous and current values (instead of the whole sequence), and does /// not pass the return value of `reduceBlock` into the next invocation of it. /// /// start - The value passed into `reduceBlock` as `previous` for the /// first value. /// reduceBlock - The block that combines the previous value and the current /// value to create the reduced value. Cannot be nil. /// /// Examples /// /// RACSequence *numbers = [@[ @1, @2, @3, @4 ].rac_sequence; /// /// // Contains 1, 3, 5, 7 /// RACSequence *sums = [numbers combinePreviousWithStart:@0 reduce:^(NSNumber *previous, NSNumber *next) { /// return @(previous.integerValue + next.integerValue); /// }]; /// /// Returns a new sequence consisting of the return values from each application of /// `reduceBlock`. - (RACSequence *)combinePreviousWithStart:(nullable ValueType)start reduce:(id _Nullable (^)(ValueType _Nullable previous, ValueType _Nullable current))reduceBlock; /// Takes values until the given block returns `YES`. /// /// Returns a RACSequence of the initial values in the receiver that fail /// `predicate`. If `predicate` never returns `YES`, a sequence equivalent to /// the receiver is returned. - (RACSequence *)takeUntilBlock:(BOOL (^)(ValueType _Nullable x))predicate; /// Takes values until the given block returns `NO`. /// /// Returns a sequence of the initial values in the receiver that pass /// `predicate`. If `predicate` never returns `NO`, a sequence equivalent to the /// receiver is returned. - (RACSequence *)takeWhileBlock:(BOOL (^)(ValueType _Nullable x))predicate; /// Skips values until the given block returns `YES`. /// /// Returns a sequence containing the values of the receiver that follow any /// initial values failing `predicate`. If `predicate` never returns `YES`, /// an empty sequence is returned. - (RACSequence *)skipUntilBlock:(BOOL (^)(ValueType _Nullable x))predicate; /// Skips values until the given block returns `NO`. /// /// Returns a sequence containing the values of the receiver that follow any /// initial values passing `predicate`. If `predicate` never returns `NO`, an /// empty sequence is returned. - (RACSequence *)skipWhileBlock:(BOOL (^)(ValueType _Nullable x))predicate; /// Returns a sequence of values for which -isEqual: returns NO when compared to /// the previous value. - (RACSequence *)distinctUntilChanged; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSequence.m ================================================ // // RACSequence.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "RACSequence.h" #import "RACArraySequence.h" #import "RACDynamicSequence.h" #import "RACEagerSequence.h" #import "RACEmptySequence.h" #import "RACScheduler.h" #import "RACSignal.h" #import "RACSubscriber.h" #import "RACTuple.h" #import "RACUnarySequence.h" // An enumerator over sequences. @interface RACSequenceEnumerator : NSEnumerator // The sequence the enumerator is enumerating. // // This will change as the enumerator is exhausted. This property should only be // accessed while synchronized on self. @property (nonatomic, strong) RACSequence *sequence; @end @interface RACSequence () // Performs one iteration of lazy binding, passing through values from `current` // until the sequence is exhausted, then recursively binding the remaining // values in the receiver. // // Returns a new sequence which contains `current`, followed by the combined // result of all applications of `block` to the remaining values in the receiver. - (RACSequence *)bind:(RACSequenceBindBlock)block passingThroughValuesFromSequence:(RACSequence *)current; @end @implementation RACSequenceEnumerator - (id)nextObject { id object = nil; @synchronized (self) { object = self.sequence.head; self.sequence = self.sequence.tail; } return object; } @end @implementation RACSequence #pragma mark Lifecycle + (RACSequence *)sequenceWithHeadBlock:(id (^)(void))headBlock tailBlock:(RACSequence *(^)(void))tailBlock { return [[RACDynamicSequence sequenceWithHeadBlock:headBlock tailBlock:tailBlock] setNameWithFormat:@"+sequenceWithHeadBlock:tailBlock:"]; } #pragma mark Class cluster primitives - (id)head { NSCAssert(NO, @"%s must be overridden by subclasses", __func__); return nil; } - (RACSequence *)tail { NSCAssert(NO, @"%s must be overridden by subclasses", __func__); return nil; } #pragma mark RACStream + (RACSequence *)empty { return RACEmptySequence.empty; } + (RACSequence *)return:(id)value { return [RACUnarySequence return:value]; } - (RACSequence *)bind:(RACSequenceBindBlock (^)(void))block { RACSequenceBindBlock bindBlock = block(); return [[self bind:bindBlock passingThroughValuesFromSequence:nil] setNameWithFormat:@"[%@] -bind:", self.name]; } - (RACSequence *)bind:(RACSequenceBindBlock)bindBlock passingThroughValuesFromSequence:(RACSequence *)passthroughSequence { // Store values calculated in the dependency here instead, avoiding any kind // of temporary collection and boxing. // // This relies on the implementation of RACDynamicSequence synchronizing // access to its head, tail, and dependency, and we're only doing it because // we really need the performance. __block RACSequence *valuesSeq = self; __block RACSequence *current = passthroughSequence; __block BOOL stop = NO; RACSequence *sequence = [RACDynamicSequence sequenceWithLazyDependency:^ id { while (current.head == nil) { if (stop) return nil; // We've exhausted the current sequence, create a sequence from the // next value. id value = valuesSeq.head; if (value == nil) { // We've exhausted all the sequences. stop = YES; return nil; } current = (id)bindBlock(value, &stop); if (current == nil) { stop = YES; return nil; } valuesSeq = valuesSeq.tail; } NSCAssert([current isKindOfClass:RACSequence.class], @"-bind: block returned an object that is not a sequence: %@", current); return nil; } headBlock:^(id _) { return current.head; } tailBlock:^ id (id _) { if (stop) return nil; return [valuesSeq bind:bindBlock passingThroughValuesFromSequence:current.tail]; }]; sequence.name = self.name; return sequence; } - (RACSequence *)concat:(RACSequence *)sequence { NSCParameterAssert(sequence != nil); return [[[RACArraySequence sequenceWithArray:@[ self, sequence ] offset:0] flatten] setNameWithFormat:@"[%@] -concat: %@", self.name, sequence]; } - (RACSequence *)zipWith:(RACSequence *)sequence { NSCParameterAssert(sequence != nil); return [[RACSequence sequenceWithHeadBlock:^ id { if (self.head == nil || sequence.head == nil) return nil; return RACTuplePack(self.head, sequence.head); } tailBlock:^ id { if (self.tail == nil || [[RACSequence empty] isEqual:self.tail]) return nil; if (sequence.tail == nil || [[RACSequence empty] isEqual:sequence.tail]) return nil; return [self.tail zipWith:sequence.tail]; }] setNameWithFormat:@"[%@] -zipWith: %@", self.name, sequence]; } #pragma mark Extended methods - (NSArray *)array { NSMutableArray *array = [NSMutableArray array]; for (id obj in self) { [array addObject:obj]; } return [array copy]; } - (NSEnumerator *)objectEnumerator { RACSequenceEnumerator *enumerator = [[RACSequenceEnumerator alloc] init]; enumerator.sequence = self; return enumerator; } - (RACSignal *)signal { return [[self signalWithScheduler:[RACScheduler scheduler]] setNameWithFormat:@"[%@] -signal", self.name]; } - (RACSignal *)signalWithScheduler:(RACScheduler *)scheduler { return [[RACSignal createSignal:^(id subscriber) { __block RACSequence *sequence = self; return [scheduler scheduleRecursiveBlock:^(void (^reschedule)(void)) { if (sequence.head == nil) { [subscriber sendCompleted]; return; } [subscriber sendNext:sequence.head]; sequence = sequence.tail; reschedule(); }]; }] setNameWithFormat:@"[%@] -signalWithScheduler: %@", self.name, scheduler]; } - (id)foldLeftWithStart:(id)start reduce:(id (^)(id, id))reduce { NSCParameterAssert(reduce != NULL); if (self.head == nil) return start; for (id value in self) { start = reduce(start, value); } return start; } - (id)foldRightWithStart:(id)start reduce:(id (^)(id, RACSequence *))reduce { NSCParameterAssert(reduce != NULL); if (self.head == nil) return start; RACSequence *rest = [RACSequence sequenceWithHeadBlock:^{ if (self.tail) { return [self.tail foldRightWithStart:start reduce:reduce]; } else { return start; } } tailBlock:nil]; return reduce(self.head, rest); } - (BOOL)any:(BOOL (^)(id))block { NSCParameterAssert(block != NULL); return [self objectPassingTest:block] != nil; } - (BOOL)all:(BOOL (^)(id))block { NSCParameterAssert(block != NULL); NSNumber *result = [self foldLeftWithStart:@YES reduce:^(NSNumber *accumulator, id value) { return @(accumulator.boolValue && block(value)); }]; return result.boolValue; } - (id)objectPassingTest:(BOOL (^)(id))block { NSCParameterAssert(block != NULL); return [self filter:block].head; } - (RACSequence *)eagerSequence { return [RACEagerSequence sequenceWithArray:self.array offset:0]; } - (RACSequence *)lazySequence { return self; } #pragma mark NSCopying - (id)copyWithZone:(NSZone *)zone { return self; } #pragma mark NSCoding - (Class)classForCoder { // Most sequences should be archived as RACArraySequences. return RACArraySequence.class; } - (id)initWithCoder:(NSCoder *)coder { if (![self isKindOfClass:RACArraySequence.class]) return [[RACArraySequence alloc] initWithCoder:coder]; // Decoding is handled in RACArraySequence. return [super init]; } - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:self.array forKey:@"array"]; } #pragma mark NSFastEnumeration - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id *)stackbuf count:(NSUInteger)len { if (state->state == ULONG_MAX) { // Enumeration has completed. return 0; } // We need to traverse the sequence itself on repeated calls to this // method, so use the 'state' field to track the current head. RACSequence *(^getSequence)(void) = ^{ return (__bridge RACSequence *)(void *)state->state; }; void (^setSequence)(RACSequence *) = ^(RACSequence *sequence) { // Release the old sequence and retain the new one. CFBridgingRelease((void *)state->state); state->state = (unsigned long)CFBridgingRetain(sequence); }; void (^complete)(void) = ^{ // Release any stored sequence. setSequence(nil); state->state = ULONG_MAX; }; if (state->state == 0) { // Since a sequence doesn't mutate, this just needs to be set to // something non-NULL. state->mutationsPtr = state->extra; setSequence(self); } state->itemsPtr = stackbuf; NSUInteger enumeratedCount = 0; while (enumeratedCount < len) { RACSequence *seq = getSequence(); // Because the objects in a sequence may be generated lazily, we want to // prevent them from being released until the enumerator's used them. __autoreleasing id obj = seq.head; if (obj == nil) { complete(); break; } stackbuf[enumeratedCount++] = obj; if (seq.tail == nil) { complete(); break; } setSequence(seq.tail); } return enumeratedCount; } #pragma mark NSObject - (NSUInteger)hash { return [self.head hash]; } - (BOOL)isEqual:(RACSequence *)seq { if (self == seq) return YES; if (![seq isKindOfClass:RACSequence.class]) return NO; for (id selfObj in self) { id seqObj = seq.head; // Handles the nil case too. if (![seqObj isEqual:selfObj]) return NO; seq = seq.tail; } // self is now depleted -- the argument should be too. return (seq.head == nil); } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSerialDisposable.h ================================================ // // RACSerialDisposable.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-07-22. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACDisposable.h" NS_ASSUME_NONNULL_BEGIN /// A disposable that contains exactly one other disposable and allows it to be /// swapped out atomically. @interface RACSerialDisposable : RACDisposable /// The inner disposable managed by the serial disposable. /// /// This property is thread-safe for reading and writing. However, if you want to /// read the current value _and_ write a new one atomically, use /// -swapInDisposable: instead. /// /// Disposing of the receiver will also dispose of the current disposable set for /// this property, then set the property to nil. If any new disposable is set /// after the receiver is disposed, it will be disposed immediately and this /// property will remain set to nil. @property (atomic, strong, nullable) RACDisposable *disposable; /// Creates a serial disposable which will wrap the given disposable. /// /// disposable - The value to set for `disposable`. This may be nil. /// /// Returns a RACSerialDisposable. + (instancetype)serialDisposableWithDisposable:(nullable RACDisposable *)disposable; /// Atomically swaps the receiver's `disposable` for `newDisposable`. /// /// newDisposable - The new value for `disposable`. If the receiver has already /// been disposed, this disposable will be too, and `disposable` /// will remain set to nil. This argument may be nil. /// /// Returns the previous value for the `disposable` property. - (nullable RACDisposable *)swapInDisposable:(nullable RACDisposable *)newDisposable; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSerialDisposable.m ================================================ // // RACSerialDisposable.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-07-22. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSerialDisposable.h" #import @interface RACSerialDisposable () { // The receiver's `disposable`. This variable must only be referenced while // _mutex is held. RACDisposable * _disposable; // YES if the receiver has been disposed. This variable must only be accessed // while _mutex is held. BOOL _disposed; // A mutex to protect access to _disposable and _disposed. pthread_mutex_t _mutex; } @end @implementation RACSerialDisposable #pragma mark Properties - (BOOL)isDisposed { pthread_mutex_lock(&_mutex); const BOOL disposed = _disposed; pthread_mutex_unlock(&_mutex); return disposed; } - (RACDisposable *)disposable { pthread_mutex_lock(&_mutex); RACDisposable * const result = _disposable; pthread_mutex_unlock(&_mutex); return result; } - (void)setDisposable:(RACDisposable *)disposable { [self swapInDisposable:disposable]; } #pragma mark Lifecycle + (instancetype)serialDisposableWithDisposable:(RACDisposable *)disposable { RACSerialDisposable *serialDisposable = [[self alloc] init]; serialDisposable.disposable = disposable; return serialDisposable; } - (instancetype)init { self = [super init]; if (self == nil) return nil; const int result __attribute__((unused)) = pthread_mutex_init(&_mutex, NULL); NSCAssert(0 == result, @"Failed to initialize mutex with error %d", result); return self; } - (instancetype)initWithBlock:(void (^)(void))block { self = [self init]; if (self == nil) return nil; self.disposable = [RACDisposable disposableWithBlock:block]; return self; } - (void)dealloc { const int result __attribute__((unused)) = pthread_mutex_destroy(&_mutex); NSCAssert(0 == result, @"Failed to destroy mutex with error %d", result); } #pragma mark Inner Disposable - (RACDisposable *)swapInDisposable:(RACDisposable *)newDisposable { RACDisposable *existingDisposable; BOOL alreadyDisposed; pthread_mutex_lock(&_mutex); alreadyDisposed = _disposed; if (!alreadyDisposed) { existingDisposable = _disposable; _disposable = newDisposable; } pthread_mutex_unlock(&_mutex); if (alreadyDisposed) { [newDisposable dispose]; return nil; } return existingDisposable; } #pragma mark Disposal - (void)dispose { RACDisposable *existingDisposable; pthread_mutex_lock(&_mutex); if (!_disposed) { existingDisposable = _disposable; _disposed = YES; _disposable = nil; } pthread_mutex_unlock(&_mutex); [existingDisposable dispose]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSignal+Operations.h ================================================ // // RACSignal+Operations.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-09-06. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import #import "RACSignal.h" @class RACCommand; @class RACDisposable; @class RACMulticastConnection<__covariant ValueType>; @class RACScheduler; @class RACSequence<__covariant ValueType>; @class RACSubject; @class RACTuple; @class RACTwoTuple<__covariant First, __covariant Second>; @class RACEvent<__covariant ValueType>; @class RACGroupedSignal; @protocol RACSubscriber; NS_ASSUME_NONNULL_BEGIN /// The domain for errors originating in RACSignal operations. extern NSErrorDomain const RACSignalErrorDomain; typedef NS_ERROR_ENUM(RACSignalErrorDomain, RACSignalError) { /// The error code used with -timeout:. RACSignalErrorTimedOut = 1, /// The error code used when a value passed into +switch:cases:default: does not /// match any of the cases, and no default was given. RACSignalErrorNoMatchingCase = 2, }; @interface RACSignal<__covariant ValueType> (Operations) /// Do the given block on `next`. This should be used to inject side effects into /// the signal. - (RACSignal *)doNext:(void (^)(ValueType _Nullable x))block RAC_WARN_UNUSED_RESULT; /// Do the given block on `error`. This should be used to inject side effects /// into the signal. - (RACSignal *)doError:(void (^)(NSError * _Nonnull error))block RAC_WARN_UNUSED_RESULT; /// Do the given block on `completed`. This should be used to inject side effects /// into the signal. - (RACSignal *)doCompleted:(void (^)(void))block RAC_WARN_UNUSED_RESULT; /// Sends `next`s only if we don't receive another `next` in `interval` seconds. /// /// If a `next` is received, and then another `next` is received before /// `interval` seconds have passed, the first value is discarded. /// /// After `interval` seconds have passed since the most recent `next` was sent, /// the most recent `next` is forwarded on the scheduler that the value was /// originally received on. If +[RACScheduler currentScheduler] was nil at the /// time, a private background scheduler is used. /// /// Returns a signal which sends throttled and delayed `next` events. Completion /// and errors are always forwarded immediately. - (RACSignal *)throttle:(NSTimeInterval)interval RAC_WARN_UNUSED_RESULT; /// Throttles `next`s for which `predicate` returns YES. /// /// When `predicate` returns YES for a `next`: /// /// 1. If another `next` is received before `interval` seconds have passed, the /// prior value is discarded. This happens regardless of whether the new /// value will be throttled. /// 2. After `interval` seconds have passed since the value was originally /// received, it will be forwarded on the scheduler that it was received /// upon. If +[RACScheduler currentScheduler] was nil at the time, a private /// background scheduler is used. /// /// When `predicate` returns NO for a `next`, it is forwarded immediately, /// without any throttling. /// /// interval - The number of seconds for which to buffer the latest value that /// passes `predicate`. /// predicate - Passed each `next` from the receiver, this block returns /// whether the given value should be throttled. This argument must /// not be nil. /// /// Returns a signal which sends `next` events, throttled when `predicate` /// returns YES. Completion and errors are always forwarded immediately. - (RACSignal *)throttle:(NSTimeInterval)interval valuesPassingTest:(BOOL (^)(id _Nullable next))predicate RAC_WARN_UNUSED_RESULT; /// Forwards `next` and `completed` events after delaying for `interval` seconds /// on the current scheduler (on which the events were delivered). /// /// If +[RACScheduler currentScheduler] is nil when `next` or `completed` is /// received, a private background scheduler is used. /// /// Returns a signal which sends delayed `next` and `completed` events. Errors /// are always forwarded immediately. - (RACSignal *)delay:(NSTimeInterval)interval RAC_WARN_UNUSED_RESULT; /// Resubscribes when the signal completes. - (RACSignal *)repeat RAC_WARN_UNUSED_RESULT; /// Executes the given block each time a subscription is created. /// /// block - A block which defines the subscription side effects. Cannot be `nil`. /// /// Example: /// /// // Write new file, with backup. /// [[[[fileManager /// rac_createFileAtPath:path contents:data] /// initially:^{ /// // 2. Second, backup current file /// [fileManager moveItemAtPath:path toPath:backupPath error:nil]; /// }] /// initially:^{ /// // 1. First, acquire write lock. /// [writeLock lock]; /// }] /// finally:^{ /// [writeLock unlock]; /// }]; /// /// Returns a signal that passes through all events of the receiver, plus /// introduces side effects which occur prior to any subscription side effects /// of the receiver. - (RACSignal *)initially:(void (^)(void))block RAC_WARN_UNUSED_RESULT; /// Executes the given block when the signal completes or errors. - (RACSignal *)finally:(void (^)(void))block RAC_WARN_UNUSED_RESULT; /// Divides the receiver's `next`s into buffers which deliver every `interval` /// seconds. /// /// interval - The interval in which values are grouped into one buffer. /// scheduler - The scheduler upon which the returned signal will deliver its /// values. This must not be nil or +[RACScheduler /// immediateScheduler]. /// /// Returns a signal which sends RACTuples of the buffered values at each /// interval on `scheduler`. When the receiver completes, any currently-buffered /// values will be sent immediately. - (RACSignal *)bufferWithTime:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler; /// Collects all receiver's `next`s into a NSArray. Nil values will be converted /// to NSNull. /// /// This corresponds to the `ToArray` method in Rx. /// /// Returns a signal which sends a single NSArray when the receiver completes /// successfully. - (RACSignal *> *)collect RAC_WARN_UNUSED_RESULT; /// Takes the last `count` `next`s after the receiving signal completes. - (RACSignal *)takeLast:(NSUInteger)count RAC_WARN_UNUSED_RESULT; /// Combines the latest values from the receiver and the given signal into /// 2-tuples, once both have sent at least one `next`. /// /// Any additional `next`s will result in a new RACTuple with the latest values /// from both signals. /// /// signal - The signal to combine with. This argument must not be nil. /// /// Returns a signal which sends RACTuples of the combined values, forwards any /// `error` events, and completes when both input signals complete. - (RACSignal *> *)combineLatestWith:(RACSignal *)signal RAC_WARN_UNUSED_RESULT; /// Combines the latest values from the given signals into RACTuples, once all /// the signals have sent at least one `next`. /// /// Any additional `next`s will result in a new RACTuple with the latest values /// from all signals. /// /// signals - The signals to combine. If this collection is empty, the returned /// signal will immediately complete upon subscription. /// /// Returns a signal which sends RACTuples of the combined values, forwards any /// `error` events, and completes when all input signals complete. + (RACSignal *)combineLatest:(id)signals RAC_WARN_UNUSED_RESULT; /// Combines signals using +combineLatest:, then reduces the resulting tuples /// into a single value using -reduceEach:. /// /// signals - The signals to combine. If this collection is empty, the /// returned signal will immediately complete upon subscription. /// reduceBlock - The block which reduces the latest values from all the /// signals into one value. It must take as many arguments as the /// number of signals given. Each argument will be an object /// argument. The return value must be an object. This argument /// must not be nil. /// /// Example: /// /// [RACSignal combineLatest:@[ stringSignal, intSignal ] reduce:^(NSString *string, NSNumber *number) { /// return [NSString stringWithFormat:@"%@: %@", string, number]; /// }]; /// /// Returns a signal which sends the results from each invocation of /// `reduceBlock`. + (RACSignal *)combineLatest:(id)signals reduce:(RACGenericReduceBlock)reduceBlock RAC_WARN_UNUSED_RESULT; /// Merges the receiver and the given signal with `+merge:` and returns the /// resulting signal. - (RACSignal *)merge:(RACSignal *)signal RAC_WARN_UNUSED_RESULT; /// Sends the latest `next` from any of the signals. /// /// Returns a signal that passes through values from each of the given signals, /// and sends `completed` when all of them complete. If any signal sends an error, /// the returned signal sends `error` immediately. + (RACSignal *)merge:(id)signals RAC_WARN_UNUSED_RESULT; /// Merges the signals sent by the receiver into a flattened signal, but only /// subscribes to `maxConcurrent` number of signals at a time. New signals are /// queued and subscribed to as other signals complete. /// /// If an error occurs on any of the signals, it is sent on the returned signal. /// It completes only after the receiver and all sent signals have completed. /// /// This corresponds to `Merge(IObservable>, Int32)` /// in Rx. /// /// maxConcurrent - the maximum number of signals to subscribe to at a /// time. If 0, it subscribes to an unlimited number of /// signals. - (RACSignal *)flatten:(NSUInteger)maxConcurrent RAC_WARN_UNUSED_RESULT; /// Ignores all `next`s from the receiver, waits for the receiver to complete, /// then subscribes to a new signal. /// /// block - A block which will create or obtain a new signal to subscribe to, /// executed only after the receiver completes. This block must not be /// nil, and it must not return a nil signal. /// /// Returns a signal which will pass through the events of the signal created in /// `block`. If the receiver errors out, the returned signal will error as well. - (RACSignal *)then:(RACSignal * (^)(void))block RAC_WARN_UNUSED_RESULT; /// Concats the inner signals of a signal of signals. - (RACSignal *)concat RAC_WARN_UNUSED_RESULT; /// Aggregates the `next` values of the receiver into a single combined value. /// /// The algorithm proceeds as follows: /// /// 1. `start` is passed into the block as the `running` value, and the first /// element of the receiver is passed into the block as the `next` value. /// 2. The result of the invocation (`running`) and the next element of the /// receiver (`next`) is passed into `reduceBlock`. /// 3. Steps 2 and 3 are repeated until all values have been processed. /// 4. The last result of `reduceBlock` is sent on the returned signal. /// /// This method is similar to -scanWithStart:reduce:, except that only the /// final result is sent on the returned signal. /// /// start - The value to be combined with the first element of the /// receiver. This value may be `nil`. /// reduceBlock - The block that describes how to combine values of the /// receiver. If the receiver is empty, this block will never be /// invoked. Cannot be nil. /// /// Returns a signal that will send the aggregated value when the receiver /// completes, then itself complete. If the receiver never sends any values, /// `start` will be sent instead. - (RACSignal *)aggregateWithStart:(id)start reduce:(id (^)(id running, id next))reduceBlock RAC_WARN_UNUSED_RESULT; /// Aggregates the `next` values of the receiver into a single combined value. /// This is indexed version of -aggregateWithStart:reduce:. /// /// start - The value to be combined with the first element of the /// receiver. This value may be `nil`. /// reduceBlock - The block that describes how to combine values of the /// receiver. This block takes zero-based index value as the last /// parameter. If the receiver is empty, this block will never be /// invoked. Cannot be nil. /// /// Returns a signal that will send the aggregated value when the receiver /// completes, then itself complete. If the receiver never sends any values, /// `start` will be sent instead. - (RACSignal *)aggregateWithStart:(id)start reduceWithIndex:(id (^)(id running, id next, NSUInteger index))reduceBlock RAC_WARN_UNUSED_RESULT; /// Aggregates the `next` values of the receiver into a single combined value. /// /// This invokes `startFactory` block on each subscription, then calls /// -aggregateWithStart:reduce: with the return value of the block as start value. /// /// startFactory - The block that returns start value which will be combined /// with the first element of the receiver. Cannot be nil. /// reduceBlock - The block that describes how to combine values of the /// receiver. If the receiver is empty, this block will never be /// invoked. Cannot be nil. /// /// Returns a signal that will send the aggregated value when the receiver /// completes, then itself complete. If the receiver never sends any values, /// the return value of `startFactory` will be sent instead. - (RACSignal *)aggregateWithStartFactory:(id (^)(void))startFactory reduce:(id (^)(id running, id next))reduceBlock RAC_WARN_UNUSED_RESULT; /// Invokes -setKeyPath:onObject:nilValue: with `nil` for the nil value. /// /// WARNING: Under certain conditions, this method is known to be thread-unsafe. /// See the description in -setKeyPath:onObject:nilValue:. - (RACDisposable *)setKeyPath:(NSString *)keyPath onObject:(NSObject *)object; /// Binds the receiver to an object, automatically setting the given key path on /// every `next`. When the signal completes, the binding is automatically /// disposed of. /// /// WARNING: Under certain conditions, this method is known to be thread-unsafe. /// A crash can result if `object` is deallocated concurrently on /// another thread within a window of time between a value being sent /// on this signal and immediately prior to the invocation of /// -setValue:forKeyPath:, which sets the property. To prevent this, /// ensure `object` is deallocated on the same thread the receiver /// sends on, or ensure that the returned disposable is disposed of /// before `object` deallocates. /// See https://github.com/ReactiveCocoa/ReactiveCocoa/pull/1184 /// /// Sending an error on the signal is considered undefined behavior, and will /// generate an assertion failure in Debug builds. /// /// A given key on an object should only have one active signal bound to it at any /// given time. Binding more than one signal to the same property is considered /// undefined behavior. /// /// keyPath - The key path to update with `next`s from the receiver. /// object - The object that `keyPath` is relative to. /// nilValue - The value to set at the key path whenever `nil` is sent by the /// receiver. This may be nil when binding to object properties, but /// an NSValue should be used for primitive properties, to avoid an /// exception if `nil` is sent (which might occur if an intermediate /// object is set to `nil`). /// /// Returns a disposable which can be used to terminate the binding. - (RACDisposable *)setKeyPath:(NSString *)keyPath onObject:(NSObject *)object nilValue:(nullable id)nilValue; /// Sends NSDate.date every `interval` seconds. /// /// interval - The time interval in seconds at which the current time is sent. /// scheduler - The scheduler upon which the current NSDate should be sent. This /// must not be nil or +[RACScheduler immediateScheduler]. /// /// Returns a signal that sends the current date/time every `interval` on /// `scheduler`. + (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler RAC_WARN_UNUSED_RESULT; /// Sends NSDate.date at intervals of at least `interval` seconds, up to /// approximately `interval` + `leeway` seconds. /// /// The created signal will defer sending each `next` for at least `interval` /// seconds, and for an additional amount of time up to `leeway` seconds in the /// interest of performance or power consumption. Note that some additional /// latency is to be expected, even when specifying a `leeway` of 0. /// /// interval - The base interval between `next`s. /// scheduler - The scheduler upon which the current NSDate should be sent. This /// must not be nil or +[RACScheduler immediateScheduler]. /// leeway - The maximum amount of additional time the `next` can be deferred. /// /// Returns a signal that sends the current date/time at intervals of at least /// `interval seconds` up to approximately `interval` + `leeway` seconds on /// `scheduler`. + (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler withLeeway:(NSTimeInterval)leeway RAC_WARN_UNUSED_RESULT; /// Takes `next`s until the `signalTrigger` sends `next` or `completed`. /// /// Returns a signal which passes through all events from the receiver until /// `signalTrigger` sends `next` or `completed`, at which point the returned signal /// will send `completed`. - (RACSignal *)takeUntil:(RACSignal *)signalTrigger RAC_WARN_UNUSED_RESULT; /// Takes `next`s until the `replacement` sends an event. /// /// replacement - The signal which replaces the receiver as soon as it sends an /// event. /// /// Returns a signal which passes through `next`s and `error` from the receiver /// until `replacement` sends an event, at which point the returned signal will /// send that event and switch to passing through events from `replacement` /// instead, regardless of whether the receiver has sent events already. - (RACSignal *)takeUntilReplacement:(RACSignal *)replacement RAC_WARN_UNUSED_RESULT; /// Subscribes to the returned signal when an error occurs. - (RACSignal *)catch:(RACSignal * (^)(NSError * _Nonnull error))catchBlock RAC_WARN_UNUSED_RESULT; /// Subscribes to the given signal when an error occurs. - (RACSignal *)catchTo:(RACSignal *)signal RAC_WARN_UNUSED_RESULT; /// Returns a signal that will either immediately send the return value of /// `tryBlock` and complete, or error using the `NSError` passed out from the /// block. /// /// tryBlock - An action that performs some computation that could fail. If the /// block returns nil, the block must return an error via the /// `errorPtr` parameter. /// /// Example: /// /// [RACSignal try:^(NSError **error) { /// return [NSJSONSerialization JSONObjectWithData:someJSONData options:0 error:error]; /// }]; + (RACSignal *)try:(nullable ValueType (^)(NSError **errorPtr))tryBlock RAC_WARN_UNUSED_RESULT; /// Runs `tryBlock` against each of the receiver's values, passing values /// until `tryBlock` returns NO, or the receiver completes. /// /// tryBlock - An action to run against each of the receiver's values. /// The block should return YES to indicate that the action was /// successful. This block must not be nil. /// /// Example: /// /// // The returned signal will send an error if data values cannot be /// // written to `someFileURL`. /// [signal try:^(NSData *data, NSError **errorPtr) { /// return [data writeToURL:someFileURL options:NSDataWritingAtomic error:errorPtr]; /// }]; /// /// Returns a signal which passes through all the values of the receiver. If /// `tryBlock` fails for any value, the returned signal will error using the /// `NSError` passed out from the block. - (RACSignal *)try:(BOOL (^)(id _Nullable value, NSError **errorPtr))tryBlock RAC_WARN_UNUSED_RESULT; /// Runs `mapBlock` against each of the receiver's values, mapping values until /// `mapBlock` returns nil, or the receiver completes. /// /// mapBlock - An action to map each of the receiver's values. The block should /// return a non-nil value to indicate that the action was successful. /// This block must not be nil. /// /// Example: /// /// // The returned signal will send an error if data cannot be read from /// // `fileURL`. /// [signal tryMap:^(NSURL *fileURL, NSError **errorPtr) { /// return [NSData dataWithContentsOfURL:fileURL options:0 error:errorPtr]; /// }]; /// /// Returns a signal which transforms all the values of the receiver. If /// `mapBlock` returns nil for any value, the returned signal will error using /// the `NSError` passed out from the block. - (RACSignal *)tryMap:(id (^)(id _Nullable value, NSError **errorPtr))mapBlock RAC_WARN_UNUSED_RESULT; /// Returns the first `next`. Note that this is a blocking call. - (nullable ValueType)first; /// Returns the first `next` or `defaultValue` if the signal completes or errors /// without sending a `next`. Note that this is a blocking call. - (nullable ValueType)firstOrDefault:(nullable ValueType)defaultValue; /// Returns the first `next` or `defaultValue` if the signal completes or errors /// without sending a `next`. If an error occurs success will be NO and error /// will be populated. Note that this is a blocking call. /// /// Both success and error may be NULL. - (nullable ValueType)firstOrDefault:(nullable ValueType)defaultValue success:(nullable BOOL *)success error:(NSError * _Nullable * _Nullable)error; /// Blocks the caller and waits for the signal to complete. /// /// error - If not NULL, set to any error that occurs. /// /// Returns whether the signal completed successfully. If NO, `error` will be set /// to the error that occurred. - (BOOL)waitUntilCompleted:(NSError * _Nullable * _Nullable)error; /// Defers creation of a signal until the signal's actually subscribed to. /// /// This can be used to effectively turn a hot signal into a cold signal. + (RACSignal *)defer:(RACSignal * (^)(void))block RAC_WARN_UNUSED_RESULT; /// Every time the receiver sends a new RACSignal, subscribes and sends `next`s and /// `error`s only for that signal. /// /// The receiver must be a signal of signals. /// /// Returns a signal which passes through `next`s and `error`s from the latest /// signal sent by the receiver, and sends `completed` when both the receiver and /// the last sent signal complete. - (RACSignal *)switchToLatest RAC_WARN_UNUSED_RESULT; /// Switches between the signals in `cases` as well as `defaultSignal` based on /// the latest value sent by `signal`. /// /// signal - A signal of objects used as keys in the `cases` dictionary. /// This argument must not be nil. /// cases - A dictionary that has signals as values. This argument must /// not be nil. A RACTupleNil key in this dictionary will match /// nil `next` events that are received on `signal`. /// defaultSignal - The signal to pass through after `signal` sends a value for /// which `cases` does not contain a signal. If nil, any /// unmatched values will result in /// a RACSignalErrorNoMatchingCase error. /// /// Returns a signal which passes through `next`s and `error`s from one of the /// the signals in `cases` or `defaultSignal`, and sends `completed` when both /// `signal` and the last used signal complete. If no `defaultSignal` is given, /// an unmatched `next` will result in an error on the returned signal. + (RACSignal *)switch:(RACSignal *)signal cases:(NSDictionary *)cases default:(nullable RACSignal *)defaultSignal RAC_WARN_UNUSED_RESULT; /// Switches between `trueSignal` and `falseSignal` based on the latest value /// sent by `boolSignal`. /// /// boolSignal - A signal of BOOLs determining whether `trueSignal` or /// `falseSignal` should be active. This argument must not be nil. /// trueSignal - The signal to pass through after `boolSignal` has sent YES. /// This argument must not be nil. /// falseSignal - The signal to pass through after `boolSignal` has sent NO. This /// argument must not be nil. /// /// Returns a signal which passes through `next`s and `error`s from `trueSignal` /// and/or `falseSignal`, and sends `completed` when both `boolSignal` and the /// last switched signal complete. + (RACSignal *)if:(RACSignal *)boolSignal then:(RACSignal *)trueSignal else:(RACSignal *)falseSignal RAC_WARN_UNUSED_RESULT; /// Adds every `next` to an array. Nils are represented by NSNulls. Note that /// this is a blocking call. /// /// **This is not the same as the `ToArray` method in Rx.** See -collect for /// that behavior instead. /// /// Returns the array of `next` values, or nil if an error occurs. - (nullable NSArray *)toArray; /// Adds every `next` to a sequence. Nils are represented by NSNulls. /// /// This corresponds to the `ToEnumerable` method in Rx. /// /// Returns a sequence which provides values from the signal as they're sent. /// Trying to retrieve a value from the sequence which has not yet been sent will /// block. @property (nonatomic, strong, readonly) RACSequence *sequence; /// Creates and returns a multicast connection. This allows you to share a single /// subscription to the underlying signal. - (RACMulticastConnection *)publish RAC_WARN_UNUSED_RESULT; /// Creates and returns a multicast connection that pushes values into the given /// subject. This allows you to share a single subscription to the underlying /// signal. - (RACMulticastConnection *)multicast:(RACSubject *)subject RAC_WARN_UNUSED_RESULT; /// Multicasts the signal to a RACReplaySubject of unlimited capacity, and /// immediately connects to the resulting RACMulticastConnection. /// /// Returns the connected, multicasted signal. - (RACSignal *)replay; /// Multicasts the signal to a RACReplaySubject of capacity 1, and immediately /// connects to the resulting RACMulticastConnection. /// /// Returns the connected, multicasted signal. - (RACSignal *)replayLast; /// Multicasts the signal to a RACReplaySubject of unlimited capacity, and /// lazily connects to the resulting RACMulticastConnection. /// /// This means the returned signal will subscribe to the multicasted signal only /// when the former receives its first subscription. /// /// Returns the lazily connected, multicasted signal. - (RACSignal *)replayLazily; /// Sends an error after `interval` seconds if the source doesn't complete /// before then. /// /// The error will be in the RACSignalErrorDomain and have a code of /// RACSignalErrorTimedOut. /// /// interval - The number of seconds after which the signal should error out. /// scheduler - The scheduler upon which any timeout error should be sent. This /// must not be nil or +[RACScheduler immediateScheduler]. /// /// Returns a signal that passes through the receiver's events, until the stream /// finishes or times out, at which point an error will be sent on `scheduler`. - (RACSignal *)timeout:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler RAC_WARN_UNUSED_RESULT; /// Creates and returns a signal that delivers its events on the given scheduler. /// Any side effects of the receiver will still be performed on the original /// thread. /// /// This is ideal when the signal already performs its work on the desired /// thread, but you want to handle its events elsewhere. /// /// This corresponds to the `ObserveOn` method in Rx. - (RACSignal *)deliverOn:(RACScheduler *)scheduler RAC_WARN_UNUSED_RESULT; /// Creates and returns a signal that executes its side effects and delivers its /// events on the given scheduler. /// /// Use of this operator should be avoided whenever possible, because the /// receiver's side effects may not be safe to run on another thread. If you just /// want to receive the signal's events on `scheduler`, use -deliverOn: instead. - (RACSignal *)subscribeOn:(RACScheduler *)scheduler RAC_WARN_UNUSED_RESULT; /// Creates and returns a signal that delivers its events on the main thread. /// If events are already being sent on the main thread, they may be passed on /// without delay. An event will instead be queued for later delivery on the main /// thread if sent on another thread, or if a previous event is already being /// processed, or has been queued. /// /// Any side effects of the receiver will still be performed on the original /// thread. /// /// This can be used when a signal will cause UI updates, to avoid potential /// flicker caused by delayed delivery of events, such as the first event from /// a RACObserve at view instantiation. - (RACSignal *)deliverOnMainThread RAC_WARN_UNUSED_RESULT; /// Groups each received object into a group, as determined by calling `keyBlock` /// with that object. The object sent is transformed by calling `transformBlock` /// with the object. If `transformBlock` is nil, it sends the original object. /// /// The returned signal is a signal of RACGroupedSignal. - (RACSignal *)groupBy:(id _Nullable (^)(id _Nullable object))keyBlock transform:(nullable id _Nullable (^)(id _Nullable object))transformBlock RAC_WARN_UNUSED_RESULT; /// Calls -[RACSignal groupBy:keyBlock transform:nil]. - (RACSignal *)groupBy:(id _Nullable (^)(id _Nullable object))keyBlock RAC_WARN_UNUSED_RESULT; /// Sends an [NSNumber numberWithBool:YES] if the receiving signal sends any /// objects. - (RACSignal *)any RAC_WARN_UNUSED_RESULT; /// Sends an [NSNumber numberWithBool:YES] if the receiving signal sends any /// objects that pass `predicateBlock`. /// /// predicateBlock - cannot be nil. - (RACSignal *)any:(BOOL (^)(id _Nullable object))predicateBlock RAC_WARN_UNUSED_RESULT; /// Sends an [NSNumber numberWithBool:YES] if all the objects the receiving /// signal sends pass `predicateBlock`. /// /// predicateBlock - cannot be nil. - (RACSignal *)all:(BOOL (^)(id _Nullable object))predicateBlock RAC_WARN_UNUSED_RESULT; /// Resubscribes to the receiving signal if an error occurs, up until it has /// retried the given number of times. /// /// retryCount - if 0, it keeps retrying until it completes. - (RACSignal *)retry:(NSInteger)retryCount RAC_WARN_UNUSED_RESULT; /// Resubscribes to the receiving signal if an error occurs. - (RACSignal *)retry RAC_WARN_UNUSED_RESULT; /// Sends the latest value from the receiver only when `sampler` sends a value. /// The returned signal could repeat values if `sampler` fires more often than /// the receiver. Values from `sampler` are ignored before the receiver sends /// its first value. /// /// sampler - The signal that controls when the latest value from the receiver /// is sent. Cannot be nil. - (RACSignal *)sample:(RACSignal *)sampler RAC_WARN_UNUSED_RESULT; /// Ignores all `next`s from the receiver. /// /// Returns a signal which only passes through `error` or `completed` events from /// the receiver. - (RACSignal *)ignoreValues RAC_WARN_UNUSED_RESULT; /// Converts each of the receiver's events into a RACEvent object. /// /// Returns a signal which sends the receiver's events as RACEvents, and /// completes after the receiver sends `completed` or `error`. - (RACSignal *> *)materialize RAC_WARN_UNUSED_RESULT; /// Converts each RACEvent in the receiver back into "real" RACSignal events. /// /// Returns a signal which sends `next` for each value RACEvent, `error` for each /// error RACEvent, and `completed` for each completed RACEvent. - (RACSignal *)dematerialize RAC_WARN_UNUSED_RESULT; /// Inverts each NSNumber-wrapped BOOL sent by the receiver. It will assert if /// the receiver sends anything other than NSNumbers. /// /// Returns a signal of inverted NSNumber-wrapped BOOLs. - (RACSignal *)not RAC_WARN_UNUSED_RESULT; /// Performs a boolean AND on all of the RACTuple of NSNumbers in sent by the receiver. /// /// Asserts if the receiver sends anything other than a RACTuple of one or more NSNumbers. /// /// Returns a signal that applies AND to each NSNumber in the tuple. - (RACSignal *)and RAC_WARN_UNUSED_RESULT; /// Performs a boolean OR on all of the RACTuple of NSNumbers in sent by the receiver. /// /// Asserts if the receiver sends anything other than a RACTuple of one or more NSNumbers. /// /// Returns a signal that applies OR to each NSNumber in the tuple. - (RACSignal *)or RAC_WARN_UNUSED_RESULT; /// Sends the result of calling the block with arguments as packed in each RACTuple /// sent by the receiver. /// /// The receiver must send tuple values, where the first element of the tuple is /// a block, taking a number of parameters equal to the count of the remaining /// elements of the tuple, and returning an object. Each block must take at least /// one argument, so each tuple must contain at least 2 elements. /// /// Example: /// /// RACSignal *adder = [RACSignal return:^(NSNumber *a, NSNumber *b) { /// return @(a.intValue + b.intValue); /// }]; /// RACSignal *sums = [[RACSignal /// combineLatest:@[ adder, as, bs ]] /// reduceApply]; /// /// Returns a signal of the result of applying the first element of each tuple /// to the remaining elements. - (RACSignal *)reduceApply RAC_WARN_UNUSED_RESULT; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSignal+Operations.m ================================================ // // RACSignal+Operations.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-09-06. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSignal+Operations.h" #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import "RACBlockTrampoline.h" #import "RACCommand.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACEvent.h" #import "RACGroupedSignal.h" #import "RACMulticastConnection+Private.h" #import "RACReplaySubject.h" #import "RACScheduler.h" #import "RACSerialDisposable.h" #import "RACSignalSequence.h" #import "RACStream+Private.h" #import "RACSubject.h" #import "RACSubscriber+Private.h" #import "RACSubscriber.h" #import "RACTuple.h" #import "RACUnit.h" #import #import NSErrorDomain const RACSignalErrorDomain = @"RACSignalErrorDomain"; // Subscribes to the given signal with the given blocks. // // If the signal errors or completes, the corresponding block is invoked. If the // disposable passed to the block is _not_ disposed, then the signal is // subscribed to again. static RACDisposable *subscribeForever (RACSignal *signal, void (^next)(id), void (^error)(NSError *, RACDisposable *), void (^completed)(RACDisposable *)) { next = [next copy]; error = [error copy]; completed = [completed copy]; RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable]; RACSchedulerRecursiveBlock recursiveBlock = ^(void (^recurse)(void)) { RACCompoundDisposable *selfDisposable = [RACCompoundDisposable compoundDisposable]; [compoundDisposable addDisposable:selfDisposable]; __weak RACDisposable *weakSelfDisposable = selfDisposable; RACDisposable *subscriptionDisposable = [signal subscribeNext:next error:^(NSError *e) { @autoreleasepool { error(e, compoundDisposable); [compoundDisposable removeDisposable:weakSelfDisposable]; } recurse(); } completed:^{ @autoreleasepool { completed(compoundDisposable); [compoundDisposable removeDisposable:weakSelfDisposable]; } recurse(); }]; [selfDisposable addDisposable:subscriptionDisposable]; }; // Subscribe once immediately, and then use recursive scheduling for any // further resubscriptions. recursiveBlock(^{ RACScheduler *recursiveScheduler = RACScheduler.currentScheduler ?: [RACScheduler scheduler]; RACDisposable *schedulingDisposable = [recursiveScheduler scheduleRecursiveBlock:recursiveBlock]; [compoundDisposable addDisposable:schedulingDisposable]; }); return compoundDisposable; } @implementation RACSignal (Operations) - (RACSignal *)doNext:(void (^)(id x))block { NSCParameterAssert(block != NULL); return [[RACSignal createSignal:^(id subscriber) { return [self subscribeNext:^(id x) { block(x); [subscriber sendNext:x]; } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ [subscriber sendCompleted]; }]; }] setNameWithFormat:@"[%@] -doNext:", self.name]; } - (RACSignal *)doError:(void (^)(NSError *error))block { NSCParameterAssert(block != NULL); return [[RACSignal createSignal:^(id subscriber) { return [self subscribeNext:^(id x) { [subscriber sendNext:x]; } error:^(NSError *error) { block(error); [subscriber sendError:error]; } completed:^{ [subscriber sendCompleted]; }]; }] setNameWithFormat:@"[%@] -doError:", self.name]; } - (RACSignal *)doCompleted:(void (^)(void))block { NSCParameterAssert(block != NULL); return [[RACSignal createSignal:^(id subscriber) { return [self subscribeNext:^(id x) { [subscriber sendNext:x]; } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ block(); [subscriber sendCompleted]; }]; }] setNameWithFormat:@"[%@] -doCompleted:", self.name]; } - (RACSignal *)throttle:(NSTimeInterval)interval { return [[self throttle:interval valuesPassingTest:^(id _) { return YES; }] setNameWithFormat:@"[%@] -throttle: %f", self.name, (double)interval]; } - (RACSignal *)throttle:(NSTimeInterval)interval valuesPassingTest:(BOOL (^)(id next))predicate { NSCParameterAssert(interval >= 0); NSCParameterAssert(predicate != nil); return [[RACSignal createSignal:^(id subscriber) { RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable]; // We may never use this scheduler, but we need to set it up ahead of // time so that our scheduled blocks are run serially if we do. RACScheduler *scheduler = [RACScheduler scheduler]; // Information about any currently-buffered `next` event. __block id nextValue = nil; __block BOOL hasNextValue = NO; RACSerialDisposable *nextDisposable = [[RACSerialDisposable alloc] init]; void (^flushNext)(BOOL send) = ^(BOOL send) { @synchronized (compoundDisposable) { [nextDisposable.disposable dispose]; if (!hasNextValue) return; if (send) [subscriber sendNext:nextValue]; nextValue = nil; hasNextValue = NO; } }; RACDisposable *subscriptionDisposable = [self subscribeNext:^(id x) { RACScheduler *delayScheduler = RACScheduler.currentScheduler ?: scheduler; BOOL shouldThrottle = predicate(x); @synchronized (compoundDisposable) { flushNext(NO); if (!shouldThrottle) { [subscriber sendNext:x]; return; } nextValue = x; hasNextValue = YES; nextDisposable.disposable = [delayScheduler afterDelay:interval schedule:^{ flushNext(YES); }]; } } error:^(NSError *error) { [compoundDisposable dispose]; [subscriber sendError:error]; } completed:^{ flushNext(YES); [subscriber sendCompleted]; }]; [compoundDisposable addDisposable:subscriptionDisposable]; return compoundDisposable; }] setNameWithFormat:@"[%@] -throttle: %f valuesPassingTest:", self.name, (double)interval]; } - (RACSignal *)delay:(NSTimeInterval)interval { return [[RACSignal createSignal:^(id subscriber) { RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; // We may never use this scheduler, but we need to set it up ahead of // time so that our scheduled blocks are run serially if we do. RACScheduler *scheduler = [RACScheduler scheduler]; void (^schedule)(dispatch_block_t) = ^(dispatch_block_t block) { RACScheduler *delayScheduler = RACScheduler.currentScheduler ?: scheduler; RACDisposable *schedulerDisposable = [delayScheduler afterDelay:interval schedule:block]; [disposable addDisposable:schedulerDisposable]; }; RACDisposable *subscriptionDisposable = [self subscribeNext:^(id x) { schedule(^{ [subscriber sendNext:x]; }); } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ schedule(^{ [subscriber sendCompleted]; }); }]; [disposable addDisposable:subscriptionDisposable]; return disposable; }] setNameWithFormat:@"[%@] -delay: %f", self.name, (double)interval]; } - (RACSignal *)repeat { return [[RACSignal createSignal:^(id subscriber) { return subscribeForever(self, ^(id x) { [subscriber sendNext:x]; }, ^(NSError *error, RACDisposable *disposable) { [disposable dispose]; [subscriber sendError:error]; }, ^(RACDisposable *disposable) { // Resubscribe. }); }] setNameWithFormat:@"[%@] -repeat", self.name]; } - (RACSignal *)catch:(RACSignal * (^)(NSError *error))catchBlock { NSCParameterAssert(catchBlock != NULL); return [[RACSignal createSignal:^(id subscriber) { RACSerialDisposable *catchDisposable = [[RACSerialDisposable alloc] init]; RACDisposable *subscriptionDisposable = [self subscribeNext:^(id x) { [subscriber sendNext:x]; } error:^(NSError *error) { RACSignal *signal = catchBlock(error); NSCAssert(signal != nil, @"Expected non-nil signal from catch block on %@", self); catchDisposable.disposable = [signal subscribe:subscriber]; } completed:^{ [subscriber sendCompleted]; }]; return [RACDisposable disposableWithBlock:^{ [catchDisposable dispose]; [subscriptionDisposable dispose]; }]; }] setNameWithFormat:@"[%@] -catch:", self.name]; } - (RACSignal *)catchTo:(RACSignal *)signal { return [[self catch:^(NSError *error) { return signal; }] setNameWithFormat:@"[%@] -catchTo: %@", self.name, signal]; } + (RACSignal *)try:(id (^)(NSError **errorPtr))tryBlock { NSCParameterAssert(tryBlock != NULL); return [[RACSignal createSignal:^(id subscriber) { NSError *error; id value = tryBlock(&error); RACSignal *signal = (value == nil ? [RACSignal error:error] : [RACSignal return:value]); return [signal subscribe:subscriber]; }] setNameWithFormat:@"+try:"]; } - (RACSignal *)try:(BOOL (^)(id value, NSError **errorPtr))tryBlock { NSCParameterAssert(tryBlock != NULL); return [[self flattenMap:^(id value) { NSError *error = nil; BOOL passed = tryBlock(value, &error); return (passed ? [RACSignal return:value] : [RACSignal error:error]); }] setNameWithFormat:@"[%@] -try:", self.name]; } - (RACSignal *)tryMap:(id (^)(id value, NSError **errorPtr))mapBlock { NSCParameterAssert(mapBlock != NULL); return [[self flattenMap:^(id value) { NSError *error = nil; id mappedValue = mapBlock(value, &error); return (mappedValue == nil ? [RACSignal error:error] : [RACSignal return:mappedValue]); }] setNameWithFormat:@"[%@] -tryMap:", self.name]; } - (RACSignal *)initially:(void (^)(void))block { NSCParameterAssert(block != NULL); return [[RACSignal defer:^{ block(); return self; }] setNameWithFormat:@"[%@] -initially:", self.name]; } - (RACSignal *)finally:(void (^)(void))block { NSCParameterAssert(block != NULL); return [[[self doError:^(NSError *error) { block(); }] doCompleted:^{ block(); }] setNameWithFormat:@"[%@] -finally:", self.name]; } - (RACSignal *)bufferWithTime:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler { NSCParameterAssert(scheduler != nil); NSCParameterAssert(scheduler != RACScheduler.immediateScheduler); return [[RACSignal createSignal:^(id subscriber) { RACSerialDisposable *timerDisposable = [[RACSerialDisposable alloc] init]; NSMutableArray *values = [NSMutableArray array]; void (^flushValues)(void) = ^{ @synchronized (values) { [timerDisposable.disposable dispose]; if (values.count == 0) return; RACTuple *tuple = [RACTuple tupleWithObjectsFromArray:values]; [values removeAllObjects]; [subscriber sendNext:tuple]; } }; RACDisposable *selfDisposable = [self subscribeNext:^(id x) { @synchronized (values) { if (values.count == 0) { timerDisposable.disposable = [scheduler afterDelay:interval schedule:flushValues]; } [values addObject:x ?: RACTupleNil.tupleNil]; } } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ flushValues(); [subscriber sendCompleted]; }]; return [RACDisposable disposableWithBlock:^{ [selfDisposable dispose]; [timerDisposable dispose]; }]; }] setNameWithFormat:@"[%@] -bufferWithTime: %f onScheduler: %@", self.name, (double)interval, scheduler]; } - (RACSignal *)collect { return [[self aggregateWithStartFactory:^{ return [[NSMutableArray alloc] init]; } reduce:^(NSMutableArray *collectedValues, id x) { [collectedValues addObject:(x ?: NSNull.null)]; return collectedValues; }] setNameWithFormat:@"[%@] -collect", self.name]; } - (RACSignal *)takeLast:(NSUInteger)count { return [[RACSignal createSignal:^(id subscriber) { NSMutableArray *valuesTaken = [NSMutableArray arrayWithCapacity:count]; return [self subscribeNext:^(id x) { [valuesTaken addObject:x ? : RACTupleNil.tupleNil]; while (valuesTaken.count > count) { [valuesTaken removeObjectAtIndex:0]; } } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ for (id value in valuesTaken) { [subscriber sendNext:value == RACTupleNil.tupleNil ? nil : value]; } [subscriber sendCompleted]; }]; }] setNameWithFormat:@"[%@] -takeLast: %lu", self.name, (unsigned long)count]; } - (RACSignal *)combineLatestWith:(RACSignal *)signal { NSCParameterAssert(signal != nil); return [[RACSignal createSignal:^(id subscriber) { RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; __block id lastSelfValue = nil; __block BOOL selfCompleted = NO; __block id lastOtherValue = nil; __block BOOL otherCompleted = NO; void (^sendNext)(void) = ^{ @synchronized (disposable) { if (lastSelfValue == nil || lastOtherValue == nil) return; [subscriber sendNext:RACTuplePack(lastSelfValue, lastOtherValue)]; } }; RACDisposable *selfDisposable = [self subscribeNext:^(id x) { @synchronized (disposable) { lastSelfValue = x ?: RACTupleNil.tupleNil; sendNext(); } } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ @synchronized (disposable) { selfCompleted = YES; if (otherCompleted) [subscriber sendCompleted]; } }]; [disposable addDisposable:selfDisposable]; RACDisposable *otherDisposable = [signal subscribeNext:^(id x) { @synchronized (disposable) { lastOtherValue = x ?: RACTupleNil.tupleNil; sendNext(); } } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ @synchronized (disposable) { otherCompleted = YES; if (selfCompleted) [subscriber sendCompleted]; } }]; [disposable addDisposable:otherDisposable]; return disposable; }] setNameWithFormat:@"[%@] -combineLatestWith: %@", self.name, signal]; } + (RACSignal *)combineLatest:(id)signals { return [[self join:signals block:^(RACSignal *left, RACSignal *right) { return [left combineLatestWith:right]; }] setNameWithFormat:@"+combineLatest: %@", signals]; } + (RACSignal *)combineLatest:(id)signals reduce:(RACGenericReduceBlock)reduceBlock { NSCParameterAssert(reduceBlock != nil); RACSignal *result = [self combineLatest:signals]; // Although we assert this condition above, older versions of this method // supported this argument being nil. Avoid crashing Release builds of // apps that depended on that. if (reduceBlock != nil) result = [result reduceEach:reduceBlock]; return [result setNameWithFormat:@"+combineLatest: %@ reduce:", signals]; } - (RACSignal *)merge:(RACSignal *)signal { return [[RACSignal merge:@[ self, signal ]] setNameWithFormat:@"[%@] -merge: %@", self.name, signal]; } + (RACSignal *)merge:(id)signals { NSMutableArray *copiedSignals = [[NSMutableArray alloc] init]; for (RACSignal *signal in signals) { [copiedSignals addObject:signal]; } return [[[RACSignal createSignal:^ RACDisposable * (id subscriber) { for (RACSignal *signal in copiedSignals) { [subscriber sendNext:signal]; } [subscriber sendCompleted]; return nil; }] flatten] setNameWithFormat:@"+merge: %@", copiedSignals]; } - (RACSignal *)flatten:(NSUInteger)maxConcurrent { return [[RACSignal createSignal:^(id subscriber) { RACCompoundDisposable *compoundDisposable = [[RACCompoundDisposable alloc] init]; // Contains disposables for the currently active subscriptions. // // This should only be used while synchronized on `subscriber`. NSMutableArray *activeDisposables = [[NSMutableArray alloc] initWithCapacity:maxConcurrent]; // Whether the signal-of-signals has completed yet. // // This should only be used while synchronized on `subscriber`. __block BOOL selfCompleted = NO; // Subscribes to the given signal. __block void (^subscribeToSignal)(RACSignal *); // Weak reference to the above, to avoid a leak. __weak __block void (^recur)(RACSignal *); // Sends completed to the subscriber if all signals are finished. // // This should only be used while synchronized on `subscriber`. void (^completeIfAllowed)(void) = ^{ if (selfCompleted && activeDisposables.count == 0) { [subscriber sendCompleted]; } }; // The signals waiting to be started. // // This array should only be used while synchronized on `subscriber`. NSMutableArray *queuedSignals = [NSMutableArray array]; recur = subscribeToSignal = ^(RACSignal *signal) { RACSerialDisposable *serialDisposable = [[RACSerialDisposable alloc] init]; @synchronized (subscriber) { [compoundDisposable addDisposable:serialDisposable]; [activeDisposables addObject:serialDisposable]; } serialDisposable.disposable = [signal subscribeNext:^(id x) { [subscriber sendNext:x]; } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ __strong void (^subscribeToSignal)(RACSignal *) = recur; RACSignal *nextSignal; @synchronized (subscriber) { [compoundDisposable removeDisposable:serialDisposable]; [activeDisposables removeObjectIdenticalTo:serialDisposable]; if (queuedSignals.count == 0) { completeIfAllowed(); return; } nextSignal = queuedSignals[0]; [queuedSignals removeObjectAtIndex:0]; } subscribeToSignal(nextSignal); }]; }; [compoundDisposable addDisposable:[self subscribeNext:^(RACSignal *signal) { if (signal == nil) return; NSCAssert([signal isKindOfClass:RACSignal.class], @"Expected a RACSignal, got %@", signal); @synchronized (subscriber) { if (maxConcurrent > 0 && activeDisposables.count >= maxConcurrent) { [queuedSignals addObject:signal]; // If we need to wait, skip subscribing to this // signal. return; } } subscribeToSignal(signal); } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ @synchronized (subscriber) { selfCompleted = YES; completeIfAllowed(); } }]]; [compoundDisposable addDisposable:[RACDisposable disposableWithBlock:^{ // A strong reference is held to `subscribeToSignal` until we're // done, preventing it from deallocating early. subscribeToSignal = nil; }]]; return compoundDisposable; }] setNameWithFormat:@"[%@] -flatten: %lu", self.name, (unsigned long)maxConcurrent]; } - (RACSignal *)then:(RACSignal * (^)(void))block { NSCParameterAssert(block != nil); return [[[self ignoreValues] concat:[RACSignal defer:block]] setNameWithFormat:@"[%@] -then:", self.name]; } - (RACSignal *)concat { return [[self flatten:1] setNameWithFormat:@"[%@] -concat", self.name]; } - (RACSignal *)aggregateWithStartFactory:(id (^)(void))startFactory reduce:(id (^)(id running, id next))reduceBlock { NSCParameterAssert(startFactory != NULL); NSCParameterAssert(reduceBlock != NULL); return [[RACSignal defer:^{ return [self aggregateWithStart:startFactory() reduce:reduceBlock]; }] setNameWithFormat:@"[%@] -aggregateWithStartFactory:reduce:", self.name]; } - (RACSignal *)aggregateWithStart:(id)start reduce:(id (^)(id running, id next))reduceBlock { return [[self aggregateWithStart:start reduceWithIndex:^(id running, id next, NSUInteger index) { return reduceBlock(running, next); }] setNameWithFormat:@"[%@] -aggregateWithStart: %@ reduce:", self.name, RACDescription(start)]; } - (RACSignal *)aggregateWithStart:(id)start reduceWithIndex:(id (^)(id, id, NSUInteger))reduceBlock { return [[[[self scanWithStart:start reduceWithIndex:reduceBlock] startWith:start] takeLast:1] setNameWithFormat:@"[%@] -aggregateWithStart: %@ reduceWithIndex:", self.name, RACDescription(start)]; } - (RACDisposable *)setKeyPath:(NSString *)keyPath onObject:(NSObject *)object { return [self setKeyPath:keyPath onObject:object nilValue:nil]; } - (RACDisposable *)setKeyPath:(NSString *)keyPath onObject:(NSObject *)object nilValue:(id)nilValue { NSCParameterAssert(keyPath != nil); NSCParameterAssert(object != nil); keyPath = [keyPath copy]; RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; // Purposely not retaining 'object', since we want to tear down the binding // when it deallocates normally. __block void * volatile objectPtr = (__bridge void *)object; RACDisposable *subscriptionDisposable = [self subscribeNext:^(id x) { // Possibly spec, possibly compiler bug, but this __bridge cast does not // result in a retain here, effectively an invisible __unsafe_unretained // qualifier. Using objc_precise_lifetime gives the __strong reference // desired. The explicit use of __strong is strictly defensive. __strong NSObject *object __attribute__((objc_precise_lifetime)) = (__bridge __strong id)objectPtr; [object setValue:x ?: nilValue forKeyPath:keyPath]; } error:^(NSError *error) { __strong NSObject *object __attribute__((objc_precise_lifetime)) = (__bridge __strong id)objectPtr; NSCAssert(NO, @"Received error from %@ in binding for key path \"%@\" on %@: %@", self, keyPath, object, error); // Log the error if we're running with assertions disabled. NSLog(@"Received error from %@ in binding for key path \"%@\" on %@: %@", self, keyPath, object, error); [disposable dispose]; } completed:^{ [disposable dispose]; }]; [disposable addDisposable:subscriptionDisposable]; #if DEBUG static void *bindingsKey = &bindingsKey; NSMutableDictionary *bindings; @synchronized (object) { bindings = objc_getAssociatedObject(object, bindingsKey); if (bindings == nil) { bindings = [NSMutableDictionary dictionary]; objc_setAssociatedObject(object, bindingsKey, bindings, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } @synchronized (bindings) { NSCAssert(bindings[keyPath] == nil, @"Signal %@ is already bound to key path \"%@\" on object %@, adding signal %@ is undefined behavior", [bindings[keyPath] nonretainedObjectValue], keyPath, object, self); bindings[keyPath] = [NSValue valueWithNonretainedObject:self]; } #endif RACDisposable *clearPointerDisposable = [RACDisposable disposableWithBlock:^{ #if DEBUG @synchronized (bindings) { [bindings removeObjectForKey:keyPath]; } #endif while (YES) { void *ptr = objectPtr; if (OSAtomicCompareAndSwapPtrBarrier(ptr, NULL, &objectPtr)) { break; } } }]; [disposable addDisposable:clearPointerDisposable]; [object.rac_deallocDisposable addDisposable:disposable]; RACCompoundDisposable *objectDisposable = object.rac_deallocDisposable; return [RACDisposable disposableWithBlock:^{ [objectDisposable removeDisposable:disposable]; [disposable dispose]; }]; } + (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler { return [[RACSignal interval:interval onScheduler:scheduler withLeeway:0.0] setNameWithFormat:@"+interval: %f onScheduler: %@", (double)interval, scheduler]; } + (RACSignal *)interval:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler withLeeway:(NSTimeInterval)leeway { NSCParameterAssert(scheduler != nil); NSCParameterAssert(scheduler != RACScheduler.immediateScheduler); return [[RACSignal createSignal:^(id subscriber) { return [scheduler after:[NSDate dateWithTimeIntervalSinceNow:interval] repeatingEvery:interval withLeeway:leeway schedule:^{ [subscriber sendNext:[NSDate date]]; }]; }] setNameWithFormat:@"+interval: %f onScheduler: %@ withLeeway: %f", (double)interval, scheduler, (double)leeway]; } - (RACSignal *)takeUntil:(RACSignal *)signalTrigger { return [[RACSignal createSignal:^(id subscriber) { RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; void (^triggerCompletion)(void) = ^{ [disposable dispose]; [subscriber sendCompleted]; }; RACDisposable *triggerDisposable = [signalTrigger subscribeNext:^(id _) { triggerCompletion(); } completed:^{ triggerCompletion(); }]; [disposable addDisposable:triggerDisposable]; if (!disposable.disposed) { RACDisposable *selfDisposable = [self subscribeNext:^(id x) { [subscriber sendNext:x]; } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ [disposable dispose]; [subscriber sendCompleted]; }]; [disposable addDisposable:selfDisposable]; } return disposable; }] setNameWithFormat:@"[%@] -takeUntil: %@", self.name, signalTrigger]; } - (RACSignal *)takeUntilReplacement:(RACSignal *)replacement { return [RACSignal createSignal:^(id subscriber) { RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init]; RACDisposable *replacementDisposable = [replacement subscribeNext:^(id x) { [selfDisposable dispose]; [subscriber sendNext:x]; } error:^(NSError *error) { [selfDisposable dispose]; [subscriber sendError:error]; } completed:^{ [selfDisposable dispose]; [subscriber sendCompleted]; }]; if (!selfDisposable.disposed) { selfDisposable.disposable = [[self concat:[RACSignal never]] subscribe:subscriber]; } return [RACDisposable disposableWithBlock:^{ [selfDisposable dispose]; [replacementDisposable dispose]; }]; }]; } - (RACSignal *)switchToLatest { return [[RACSignal createSignal:^(id subscriber) { RACMulticastConnection *connection = [self publish]; RACDisposable *subscriptionDisposable = [[connection.signal flattenMap:^(RACSignal *x) { NSCAssert(x == nil || [x isKindOfClass:RACSignal.class], @"-switchToLatest requires that the source signal (%@) send signals. Instead we got: %@", self, x); // -concat:[RACSignal never] prevents completion of the receiver from // prematurely terminating the inner signal. return [x takeUntil:[connection.signal concat:[RACSignal never]]]; }] subscribe:subscriber]; RACDisposable *connectionDisposable = [connection connect]; return [RACDisposable disposableWithBlock:^{ [subscriptionDisposable dispose]; [connectionDisposable dispose]; }]; }] setNameWithFormat:@"[%@] -switchToLatest", self.name]; } + (RACSignal *)switch:(RACSignal *)signal cases:(NSDictionary *)cases default:(RACSignal *)defaultSignal { NSCParameterAssert(signal != nil); NSCParameterAssert(cases != nil); for (id key in cases) { id value __attribute__((unused)) = cases[key]; NSCAssert([value isKindOfClass:RACSignal.class], @"Expected all cases to be RACSignals, %@ isn't", value); } NSDictionary *copy = [cases copy]; return [[[signal map:^(id key) { if (key == nil) key = RACTupleNil.tupleNil; RACSignal *signal = copy[key] ?: defaultSignal; if (signal == nil) { NSString *description = [NSString stringWithFormat:NSLocalizedString(@"No matching signal found for value %@", @""), key]; return [RACSignal error:[NSError errorWithDomain:RACSignalErrorDomain code:RACSignalErrorNoMatchingCase userInfo:@{ NSLocalizedDescriptionKey: description }]]; } return signal; }] switchToLatest] setNameWithFormat:@"+switch: %@ cases: %@ default: %@", signal, cases, defaultSignal]; } + (RACSignal *)if:(RACSignal *)boolSignal then:(RACSignal *)trueSignal else:(RACSignal *)falseSignal { NSCParameterAssert(boolSignal != nil); NSCParameterAssert(trueSignal != nil); NSCParameterAssert(falseSignal != nil); return [[[boolSignal map:^(NSNumber *value) { NSCAssert([value isKindOfClass:NSNumber.class], @"Expected %@ to send BOOLs, not %@", boolSignal, value); return (value.boolValue ? trueSignal : falseSignal); }] switchToLatest] setNameWithFormat:@"+if: %@ then: %@ else: %@", boolSignal, trueSignal, falseSignal]; } - (id)first { return [self firstOrDefault:nil]; } - (id)firstOrDefault:(id)defaultValue { return [self firstOrDefault:defaultValue success:NULL error:NULL]; } - (id)firstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error { NSCondition *condition = [[NSCondition alloc] init]; condition.name = [NSString stringWithFormat:@"[%@] -firstOrDefault: %@ success:error:", self.name, defaultValue]; __block id value = defaultValue; __block BOOL done = NO; // Ensures that we don't pass values across thread boundaries by reference. __block NSError *localError; __block BOOL localSuccess; [[self take:1] subscribeNext:^(id x) { [condition lock]; value = x; localSuccess = YES; done = YES; [condition broadcast]; [condition unlock]; } error:^(NSError *e) { [condition lock]; if (!done) { localSuccess = NO; localError = e; done = YES; [condition broadcast]; } [condition unlock]; } completed:^{ [condition lock]; localSuccess = YES; done = YES; [condition broadcast]; [condition unlock]; }]; [condition lock]; while (!done) { [condition wait]; } if (success != NULL) *success = localSuccess; if (error != NULL) *error = localError; [condition unlock]; return value; } - (BOOL)waitUntilCompleted:(NSError **)error { BOOL success = NO; [[[self ignoreValues] setNameWithFormat:@"[%@] -waitUntilCompleted:", self.name] firstOrDefault:nil success:&success error:error]; return success; } + (RACSignal *)defer:(RACSignal * (^)(void))block { NSCParameterAssert(block != NULL); return [[RACSignal createSignal:^(id subscriber) { return [block() subscribe:subscriber]; }] setNameWithFormat:@"+defer:"]; } - (NSArray *)toArray { return [[[self collect] first] copy]; } - (RACSequence *)sequence { return [[RACSignalSequence sequenceWithSignal:self] setNameWithFormat:@"[%@] -sequence", self.name]; } - (RACMulticastConnection *)publish { RACSubject *subject = [[RACSubject subject] setNameWithFormat:@"[%@] -publish", self.name]; RACMulticastConnection *connection = [self multicast:subject]; return connection; } - (RACMulticastConnection *)multicast:(RACSubject *)subject { [subject setNameWithFormat:@"[%@] -multicast: %@", self.name, subject.name]; RACMulticastConnection *connection = [[RACMulticastConnection alloc] initWithSourceSignal:self subject:subject]; return connection; } - (RACSignal *)replay { RACReplaySubject *subject = [[RACReplaySubject subject] setNameWithFormat:@"[%@] -replay", self.name]; RACMulticastConnection *connection = [self multicast:subject]; [connection connect]; return connection.signal; } - (RACSignal *)replayLast { RACReplaySubject *subject = [[RACReplaySubject replaySubjectWithCapacity:1] setNameWithFormat:@"[%@] -replayLast", self.name]; RACMulticastConnection *connection = [self multicast:subject]; [connection connect]; return connection.signal; } - (RACSignal *)replayLazily { RACMulticastConnection *connection = [self multicast:[RACReplaySubject subject]]; return [[RACSignal defer:^{ [connection connect]; return connection.signal; }] setNameWithFormat:@"[%@] -replayLazily", self.name]; } - (RACSignal *)timeout:(NSTimeInterval)interval onScheduler:(RACScheduler *)scheduler { NSCParameterAssert(scheduler != nil); NSCParameterAssert(scheduler != RACScheduler.immediateScheduler); return [[RACSignal createSignal:^(id subscriber) { RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; RACDisposable *timeoutDisposable = [scheduler afterDelay:interval schedule:^{ [disposable dispose]; [subscriber sendError:[NSError errorWithDomain:RACSignalErrorDomain code:RACSignalErrorTimedOut userInfo:nil]]; }]; [disposable addDisposable:timeoutDisposable]; RACDisposable *subscriptionDisposable = [self subscribeNext:^(id x) { [subscriber sendNext:x]; } error:^(NSError *error) { [disposable dispose]; [subscriber sendError:error]; } completed:^{ [disposable dispose]; [subscriber sendCompleted]; }]; [disposable addDisposable:subscriptionDisposable]; return disposable; }] setNameWithFormat:@"[%@] -timeout: %f onScheduler: %@", self.name, (double)interval, scheduler]; } - (RACSignal *)deliverOn:(RACScheduler *)scheduler { return [[RACSignal createSignal:^(id subscriber) { return [self subscribeNext:^(id x) { [scheduler schedule:^{ [subscriber sendNext:x]; }]; } error:^(NSError *error) { [scheduler schedule:^{ [subscriber sendError:error]; }]; } completed:^{ [scheduler schedule:^{ [subscriber sendCompleted]; }]; }]; }] setNameWithFormat:@"[%@] -deliverOn: %@", self.name, scheduler]; } - (RACSignal *)subscribeOn:(RACScheduler *)scheduler { return [[RACSignal createSignal:^(id subscriber) { RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; RACDisposable *schedulingDisposable = [scheduler schedule:^{ RACDisposable *subscriptionDisposable = [self subscribe:subscriber]; [disposable addDisposable:subscriptionDisposable]; }]; [disposable addDisposable:schedulingDisposable]; return disposable; }] setNameWithFormat:@"[%@] -subscribeOn: %@", self.name, scheduler]; } - (RACSignal *)deliverOnMainThread { return [[RACSignal createSignal:^(id subscriber) { __block volatile int32_t queueLength = 0; void (^performOnMainThread)(dispatch_block_t) = ^(dispatch_block_t block) { int32_t queued = OSAtomicIncrement32(&queueLength); if (NSThread.isMainThread && queued == 1) { block(); OSAtomicDecrement32(&queueLength); } else { dispatch_async(dispatch_get_main_queue(), ^{ block(); OSAtomicDecrement32(&queueLength); }); } }; return [self subscribeNext:^(id x) { performOnMainThread(^{ [subscriber sendNext:x]; }); } error:^(NSError *error) { performOnMainThread(^{ [subscriber sendError:error]; }); } completed:^{ performOnMainThread(^{ [subscriber sendCompleted]; }); }]; }] setNameWithFormat:@"[%@] -deliverOnMainThread", self.name]; } - (RACSignal *)groupBy:(id (^)(id object))keyBlock transform:(id (^)(id object))transformBlock { NSCParameterAssert(keyBlock != NULL); return [[RACSignal createSignal:^(id subscriber) { NSMutableDictionary *groups = [NSMutableDictionary dictionary]; NSMutableArray *orderedGroups = [NSMutableArray array]; return [self subscribeNext:^(id x) { id key = keyBlock(x); RACGroupedSignal *groupSubject = nil; @synchronized(groups) { groupSubject = groups[key]; if (groupSubject == nil) { groupSubject = [RACGroupedSignal signalWithKey:key]; groups[key] = groupSubject; [orderedGroups addObject:groupSubject]; [subscriber sendNext:groupSubject]; } } [groupSubject sendNext:transformBlock != NULL ? transformBlock(x) : x]; } error:^(NSError *error) { [subscriber sendError:error]; [orderedGroups makeObjectsPerformSelector:@selector(sendError:) withObject:error]; } completed:^{ [subscriber sendCompleted]; [orderedGroups makeObjectsPerformSelector:@selector(sendCompleted)]; }]; }] setNameWithFormat:@"[%@] -groupBy:transform:", self.name]; } - (RACSignal *)groupBy:(id (^)(id object))keyBlock { return [[self groupBy:keyBlock transform:nil] setNameWithFormat:@"[%@] -groupBy:", self.name]; } - (RACSignal *)any { return [[self any:^(id x) { return YES; }] setNameWithFormat:@"[%@] -any", self.name]; } - (RACSignal *)any:(BOOL (^)(id object))predicateBlock { NSCParameterAssert(predicateBlock != NULL); return [[[self materialize] bind:^{ return ^(RACEvent *event, BOOL *stop) { if (event.finished) { *stop = YES; return [RACSignal return:@NO]; } if (predicateBlock(event.value)) { *stop = YES; return [RACSignal return:@YES]; } return [RACSignal empty]; }; }] setNameWithFormat:@"[%@] -any:", self.name]; } - (RACSignal *)all:(BOOL (^)(id object))predicateBlock { NSCParameterAssert(predicateBlock != NULL); return [[[self materialize] bind:^{ return ^(RACEvent *event, BOOL *stop) { if (event.eventType == RACEventTypeCompleted) { *stop = YES; return [RACSignal return:@YES]; } if (event.eventType == RACEventTypeError || !predicateBlock(event.value)) { *stop = YES; return [RACSignal return:@NO]; } return [RACSignal empty]; }; }] setNameWithFormat:@"[%@] -all:", self.name]; } - (RACSignal *)retry:(NSInteger)retryCount { return [[RACSignal createSignal:^(id subscriber) { __block NSInteger currentRetryCount = 0; return subscribeForever(self, ^(id x) { [subscriber sendNext:x]; }, ^(NSError *error, RACDisposable *disposable) { if (retryCount == 0 || currentRetryCount < retryCount) { // Resubscribe. currentRetryCount++; return; } [disposable dispose]; [subscriber sendError:error]; }, ^(RACDisposable *disposable) { [disposable dispose]; [subscriber sendCompleted]; }); }] setNameWithFormat:@"[%@] -retry: %lu", self.name, (unsigned long)retryCount]; } - (RACSignal *)retry { return [[self retry:0] setNameWithFormat:@"[%@] -retry", self.name]; } - (RACSignal *)sample:(RACSignal *)sampler { NSCParameterAssert(sampler != nil); return [[RACSignal createSignal:^(id subscriber) { NSLock *lock = [[NSLock alloc] init]; __block id lastValue; __block BOOL hasValue = NO; RACSerialDisposable *samplerDisposable = [[RACSerialDisposable alloc] init]; RACDisposable *sourceDisposable = [self subscribeNext:^(id x) { [lock lock]; hasValue = YES; lastValue = x; [lock unlock]; } error:^(NSError *error) { [samplerDisposable dispose]; [subscriber sendError:error]; } completed:^{ [samplerDisposable dispose]; [subscriber sendCompleted]; }]; samplerDisposable.disposable = [sampler subscribeNext:^(id _) { BOOL shouldSend = NO; id value; [lock lock]; shouldSend = hasValue; value = lastValue; [lock unlock]; if (shouldSend) { [subscriber sendNext:value]; } } error:^(NSError *error) { [sourceDisposable dispose]; [subscriber sendError:error]; } completed:^{ [sourceDisposable dispose]; [subscriber sendCompleted]; }]; return [RACDisposable disposableWithBlock:^{ [samplerDisposable dispose]; [sourceDisposable dispose]; }]; }] setNameWithFormat:@"[%@] -sample: %@", self.name, sampler]; } - (RACSignal *)ignoreValues { return [[self filter:^(id _) { return NO; }] setNameWithFormat:@"[%@] -ignoreValues", self.name]; } - (RACSignal *)materialize { return [[RACSignal createSignal:^(id subscriber) { return [self subscribeNext:^(id x) { [subscriber sendNext:[RACEvent eventWithValue:x]]; } error:^(NSError *error) { [subscriber sendNext:[RACEvent eventWithError:error]]; [subscriber sendCompleted]; } completed:^{ [subscriber sendNext:RACEvent.completedEvent]; [subscriber sendCompleted]; }]; }] setNameWithFormat:@"[%@] -materialize", self.name]; } - (RACSignal *)dematerialize { return [[self bind:^{ return ^(RACEvent *event, BOOL *stop) { switch (event.eventType) { case RACEventTypeCompleted: *stop = YES; return [RACSignal empty]; case RACEventTypeError: *stop = YES; return [RACSignal error:event.error]; case RACEventTypeNext: return [RACSignal return:event.value]; } }; }] setNameWithFormat:@"[%@] -dematerialize", self.name]; } - (RACSignal *)not { return [[self map:^(NSNumber *value) { NSCAssert([value isKindOfClass:NSNumber.class], @"-not must only be used on a signal of NSNumbers. Instead, got: %@", value); return @(!value.boolValue); }] setNameWithFormat:@"[%@] -not", self.name]; } - (RACSignal *)and { return [[self map:^(RACTuple *tuple) { NSCAssert([tuple isKindOfClass:RACTuple.class], @"-and must only be used on a signal of RACTuples of NSNumbers. Instead, received: %@", tuple); NSCAssert(tuple.count > 0, @"-and must only be used on a signal of RACTuples of NSNumbers, with at least 1 value in the tuple"); return @([tuple.rac_sequence all:^(NSNumber *number) { NSCAssert([number isKindOfClass:NSNumber.class], @"-and must only be used on a signal of RACTuples of NSNumbers. Instead, tuple contains a non-NSNumber value: %@", tuple); return number.boolValue; }]); }] setNameWithFormat:@"[%@] -and", self.name]; } - (RACSignal *)or { return [[self map:^(RACTuple *tuple) { NSCAssert([tuple isKindOfClass:RACTuple.class], @"-or must only be used on a signal of RACTuples of NSNumbers. Instead, received: %@", tuple); NSCAssert(tuple.count > 0, @"-or must only be used on a signal of RACTuples of NSNumbers, with at least 1 value in the tuple"); return @([tuple.rac_sequence any:^(NSNumber *number) { NSCAssert([number isKindOfClass:NSNumber.class], @"-or must only be used on a signal of RACTuples of NSNumbers. Instead, tuple contains a non-NSNumber value: %@", tuple); return number.boolValue; }]); }] setNameWithFormat:@"[%@] -or", self.name]; } - (RACSignal *)reduceApply { return [[self map:^(RACTuple *tuple) { NSCAssert([tuple isKindOfClass:RACTuple.class], @"-reduceApply must only be used on a signal of RACTuples. Instead, received: %@", tuple); NSCAssert(tuple.count > 1, @"-reduceApply must only be used on a signal of RACTuples, with at least a block in tuple[0] and its first argument in tuple[1]"); // We can't use -array, because we need to preserve RACTupleNil NSMutableArray *tupleArray = [NSMutableArray arrayWithCapacity:tuple.count]; for (id val in tuple) { [tupleArray addObject:val]; } RACTuple *arguments = [RACTuple tupleWithObjectsFromArray:[tupleArray subarrayWithRange:NSMakeRange(1, tupleArray.count - 1)]]; return [RACBlockTrampoline invokeBlock:tuple[0] withArguments:arguments]; }] setNameWithFormat:@"[%@] -reduceApply", self.name]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSignal.h ================================================ // // RACSignal.h // ReactiveObjC // // Created by Josh Abernathy on 3/1/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import #import "RACAnnotations.h" #import "RACStream.h" @class RACDisposable; @class RACScheduler; @class RACSubject; @class RACTuple; @class RACTwoTuple<__covariant First, __covariant Second>; @protocol RACSubscriber; NS_ASSUME_NONNULL_BEGIN @interface RACSignal<__covariant ValueType> : RACStream /// Creates a new signal. This is the preferred way to create a new signal /// operation or behavior. /// /// Events can be sent to new subscribers immediately in the `didSubscribe` /// block, but the subscriber will not be able to dispose of the signal until /// a RACDisposable is returned from `didSubscribe`. In the case of infinite /// signals, this won't _ever_ happen if events are sent immediately. /// /// To ensure that the signal is disposable, events can be scheduled on the /// +[RACScheduler currentScheduler] (so that they're deferred, not sent /// immediately), or they can be sent in the background. The RACDisposable /// returned by the `didSubscribe` block should cancel any such scheduling or /// asynchronous work. /// /// didSubscribe - Called when the signal is subscribed to. The new subscriber is /// passed in. You can then manually control the by /// sending it -sendNext:, -sendError:, and -sendCompleted, /// as defined by the operation you're implementing. This block /// should return a RACDisposable which cancels any ongoing work /// triggered by the subscription, and cleans up any resources or /// disposables created as part of it. When the disposable is /// disposed of, the signal must not send any more events to the /// `subscriber`. If no cleanup is necessary, return nil. /// /// **Note:** The `didSubscribe` block is called every time a new subscriber /// subscribes. Any side effects within the block will thus execute once for each /// subscription, not necessarily on one thread, and possibly even /// simultaneously! + (RACSignal *)createSignal:(RACDisposable * _Nullable (^)(id subscriber))didSubscribe RAC_WARN_UNUSED_RESULT; /// Returns a signal that immediately sends the given error. + (RACSignal *)error:(nullable NSError *)error RAC_WARN_UNUSED_RESULT; /// Returns a signal that never completes. + (RACSignal *)never RAC_WARN_UNUSED_RESULT; /// Immediately schedules the given block on the given scheduler. The block is /// given a subscriber to which it can send events. /// /// scheduler - The scheduler on which `block` will be scheduled and results /// delivered. Cannot be nil. /// block - The block to invoke. Cannot be NULL. /// /// Returns a signal which will send all events sent on the subscriber given to /// `block`. All events will be sent on `scheduler` and it will replay any missed /// events to new subscribers. + (RACSignal *)startEagerlyWithScheduler:(RACScheduler *)scheduler block:(void (^)(id subscriber))block; /// Invokes the given block only on the first subscription. The block is given a /// subscriber to which it can send events. /// /// Note that disposing of the subscription to the returned signal will *not* /// dispose of the underlying subscription. If you need that behavior, see /// -[RACMulticastConnection autoconnect]. The underlying subscription will never /// be disposed of. Because of this, `block` should never return an infinite /// signal since there would be no way of ending it. /// /// scheduler - The scheduler on which the block should be scheduled. Note that /// if given +[RACScheduler immediateScheduler], the block will be /// invoked synchronously on the first subscription. Cannot be nil. /// block - The block to invoke on the first subscription. Cannot be NULL. /// /// Returns a signal which will pass through the events sent to the subscriber /// given to `block` and replay any missed events to new subscribers. + (RACSignal *)startLazilyWithScheduler:(RACScheduler *)scheduler block:(void (^)(id subscriber))block RAC_WARN_UNUSED_RESULT; @end @interface RACSignal<__covariant ValueType> (RACStream) /// Returns a signal that immediately sends the given value and then completes. + (RACSignal *)return:(nullable ValueType)value RAC_WARN_UNUSED_RESULT; /// Returns a signal that immediately completes. + (RACSignal *)empty RAC_WARN_UNUSED_RESULT; /// A block which accepts a value from a RACSignal and returns a new signal. /// /// Setting `stop` to `YES` will cause the bind to terminate after the returned /// value. Returning `nil` will result in immediate termination. typedef RACSignal * _Nullable (^RACSignalBindBlock)(ValueType _Nullable value, BOOL *stop); /// Lazily binds a block to the values in the receiver. /// /// This should only be used if you need to terminate the bind early, or close /// over some state. -flattenMap: is more appropriate for all other cases. /// /// block - A block returning a RACSignalBindBlock. This block will be invoked /// each time the bound signal is re-evaluated. This block must not be /// nil or return nil. /// /// Returns a new signal which represents the combined result of all lazy /// applications of `block`. - (RACSignal *)bind:(RACSignalBindBlock (^)(void))block RAC_WARN_UNUSED_RESULT; /// Subscribes to `signal` when the source signal completes. - (RACSignal *)concat:(RACSignal *)signal RAC_WARN_UNUSED_RESULT; /// Zips the values in the receiver with those of the given signal to create /// RACTuples. /// /// The first `next` of each signal will be combined, then the second `next`, /// and so forth, until either signal completes or errors. /// /// signal - The signal to zip with. This must not be `nil`. /// /// Returns a new signal of RACTuples, representing the combined values of the /// two signals. Any error from one of the original signals will be forwarded on /// the returned signal. - (RACSignal *> *)zipWith:(RACSignal *)signal RAC_WARN_UNUSED_RESULT; @end /// Redeclarations of operations built on the RACStream primitives with more /// precise ValueType information. /// /// In cases where the ValueType of the result of the operation is not able to /// be inferred, the ValueType is erased in the result. /// /// In cases where instancetype is a valid return type, the operation is not /// redeclared here. @interface RACSignal<__covariant ValueType> (RACStreamOperations) /// Maps `block` across the values in the receiver and flattens the result. /// /// Note that operators applied _after_ -flattenMap: behave differently from /// operators _within_ -flattenMap:. See the Examples section below. /// /// This corresponds to the `SelectMany` method in Rx. /// /// block - A block which accepts the values in the receiver and returns a new /// instance of the receiver's class. Returning `nil` from this block is /// equivalent to returning an empty signal. /// /// Examples /// /// [signal flattenMap:^(id x) { /// // Logs each time a returned signal completes. /// return [[RACSignal return:x] logCompleted]; /// }]; /// /// [[signal /// flattenMap:^(id x) { /// return [RACSignal return:x]; /// }] /// // Logs only once, when all of the signals complete. /// logCompleted]; /// /// Returns a new signal which represents the combined signals resulting from /// mapping `block`. - (RACSignal *)flattenMap:(__kindof RACSignal * _Nullable (^)(ValueType _Nullable value))block RAC_WARN_UNUSED_RESULT; /// Flattens a signal of signals. /// /// This corresponds to the `Merge` method in Rx. /// /// Returns a signal consisting of the combined signals obtained from the /// receiver. - (RACSignal *)flatten RAC_WARN_UNUSED_RESULT; /// Maps `block` across the values in the receiver. /// /// This corresponds to the `Select` method in Rx. /// /// Returns a new signal with the mapped values. - (RACSignal *)map:(id _Nullable (^)(ValueType _Nullable value))block RAC_WARN_UNUSED_RESULT; /// Replaces each value in the receiver with the given object. /// /// Returns a new signal which includes the given object once for each value in /// the receiver. - (RACSignal *)mapReplace:(nullable id)object RAC_WARN_UNUSED_RESULT; /// Filters out values in the receiver that don't pass the given test. /// /// This corresponds to the `Where` method in Rx. /// /// Returns a new signal with only those values that passed. - (RACSignal *)filter:(BOOL (^)(ValueType _Nullable value))block RAC_WARN_UNUSED_RESULT; /// Filters out values in the receiver that equal (via -isEqual:) the provided /// value. /// /// value - The value can be `nil`, in which case it ignores `nil` values. /// /// Returns a new signal containing only the values which did not compare equal /// to `value`. - (RACSignal *)ignore:(nullable ValueType)value RAC_WARN_UNUSED_RESULT; /// Unpacks each RACTuple in the receiver and maps the values to a new value. /// /// reduceBlock - The block which reduces each RACTuple's values into one value. /// It must take as many arguments as the number of tuple elements /// to process. Each argument will be an object argument. The /// return value must be an object. This argument cannot be nil. /// /// Returns a new signal of reduced tuple values. - (RACSignal *)reduceEach:(RACReduceBlock)reduceBlock RAC_WARN_UNUSED_RESULT; /// Returns a signal consisting of `value`, followed by the values in the /// receiver. - (RACSignal *)startWith:(nullable ValueType)value RAC_WARN_UNUSED_RESULT; /// Skips the first `skipCount` values in the receiver. /// /// Returns the receiver after skipping the first `skipCount` values. If /// `skipCount` is greater than the number of values in the signal, an empty /// signal is returned. - (RACSignal *)skip:(NSUInteger)skipCount RAC_WARN_UNUSED_RESULT; /// Returns a signal of the first `count` values in the receiver. If `count` is /// greater than or equal to the number of values in the signal, a signal /// equivalent to the receiver is returned. - (RACSignal *)take:(NSUInteger)count RAC_WARN_UNUSED_RESULT; /// Zips the values in the given signals to create RACTuples. /// /// The first value of each signals will be combined, then the second value, and /// so forth, until at least one of the signals is exhausted. /// /// signals - The signals to combine. If this collection is empty, the returned /// signal will be empty. /// /// Returns a new signal containing RACTuples of the zipped values from the /// signals. + (RACSignal *)zip:(id)signals RAC_WARN_UNUSED_RESULT; /// Zips signals using +zip:, then reduces the resulting tuples into a single /// value using -reduceEach: /// /// signals - The signals to combine. If this collection is empty, the /// returned signal will be empty. /// reduceBlock - The block which reduces the values from all the signals /// into one value. It must take as many arguments as the /// number of signals given. Each argument will be an object /// argument. The return value must be an object. This argument /// must not be nil. /// /// Example: /// /// [RACSignal zip:@[ stringSignal, intSignal ] /// reduce:^(NSString *string, NSNumber *number) { /// return [NSString stringWithFormat:@"%@: %@", string, number]; /// }]; /// /// Returns a new signal containing the results from each invocation of /// `reduceBlock`. + (RACSignal *)zip:(id)signals reduce:(RACGenericReduceBlock)reduceBlock RAC_WARN_UNUSED_RESULT; /// Returns a signal obtained by concatenating `signals` in order. + (RACSignal *)concat:(id)signals RAC_WARN_UNUSED_RESULT; /// Combines values in the receiver from left to right using the given block. /// /// The algorithm proceeds as follows: /// /// 1. `startingValue` is passed into the block as the `running` value, and the /// first element of the receiver is passed into the block as the `next` value. /// 2. The result of the invocation is added to the returned signal. /// 3. The result of the invocation (`running`) and the next element of the /// receiver (`next`) is passed into `block`. /// 4. Steps 2 and 3 are repeated until all values have been processed. /// /// startingValue - The value to be combined with the first element of the /// receiver. This value may be `nil`. /// reduceBlock - The block that describes how to combine values of the /// receiver. If the receiver is empty, this block will never be /// invoked. Cannot be nil. /// /// Examples /// /// RACSequence *numbers = @[ @1, @2, @3, @4 ].rac_sequence; /// /// // Contains 1, 3, 6, 10 /// RACSequence *sums = [numbers scanWithStart:@0 reduce:^(NSNumber *sum, NSNumber *next) { /// return @(sum.integerValue + next.integerValue); /// }]; /// /// Returns a new signal that consists of each application of `reduceBlock`. If /// the receiver is empty, an empty signal is returned. - (RACSignal *)scanWithStart:(nullable id)startingValue reduce:(id _Nullable (^)(id _Nullable running, ValueType _Nullable next))reduceBlock RAC_WARN_UNUSED_RESULT; /// Combines values in the receiver from left to right using the given block /// which also takes zero-based index of the values. /// /// startingValue - The value to be combined with the first element of the /// receiver. This value may be `nil`. /// reduceBlock - The block that describes how to combine values of the /// receiver. This block takes zero-based index value as the last /// parameter. If the receiver is empty, this block will never /// be invoked. Cannot be nil. /// /// Returns a new signal that consists of each application of `reduceBlock`. If /// the receiver is empty, an empty signal is returned. - (RACSignal *)scanWithStart:(nullable id)startingValue reduceWithIndex:(id _Nullable (^)(id _Nullable running, ValueType _Nullable next, NSUInteger index))reduceBlock RAC_WARN_UNUSED_RESULT; /// Combines each previous and current value into one object. /// /// This method is similar to -scanWithStart:reduce:, but only ever operates on /// the previous and current values (instead of the whole signal), and does not /// pass the return value of `reduceBlock` into the next invocation of it. /// /// start - The value passed into `reduceBlock` as `previous` for the /// first value. /// reduceBlock - The block that combines the previous value and the current /// value to create the reduced value. Cannot be nil. /// /// Examples /// /// RACSignal *numbers = [@[ @1, @2, @3, @4 ].rac_sequence /// signalWithScheduler:RACScheduler.immediateScheduler]; /// /// // Contains 1, 3, 5, 7 /// RACSignal *sums = [numbers combinePreviousWithStart:@0 reduce:^(NSNumber *previous, NSNumber *next) { /// return @(previous.integerValue + next.integerValue); /// }]; /// /// Returns a new signal consisting of the return values from each application of /// `reduceBlock`. - (RACSignal *)combinePreviousWithStart:(nullable ValueType)start reduce:(id _Nullable (^)(ValueType _Nullable previous, ValueType _Nullable current))reduceBlock RAC_WARN_UNUSED_RESULT; /// Takes values until the given block returns `YES`. /// /// Returns a signal of the initial values in the receiver that fail `predicate`. /// If `predicate` never returns `YES`, a signal equivalent to the receiver is /// returned. - (RACSignal *)takeUntilBlock:(BOOL (^)(ValueType _Nullable x))predicate RAC_WARN_UNUSED_RESULT; /// Takes values until the given block returns `NO`. /// /// Returns a signal of the initial values in the receiver that pass `predicate`. /// If `predicate` never returns `NO`, a signal equivalent to the receiver is /// returned. - (RACSignal *)takeWhileBlock:(BOOL (^)(ValueType _Nullable x))predicate RAC_WARN_UNUSED_RESULT; /// Skips values until the given block returns `YES`. /// /// Returns a signal containing the values of the receiver that follow any /// initial values failing `predicate`. If `predicate` never returns `YES`, /// an empty signal is returned. - (RACSignal *)skipUntilBlock:(BOOL (^)(ValueType _Nullable x))predicate RAC_WARN_UNUSED_RESULT; /// Skips values until the given block returns `NO`. /// /// Returns a signal containing the values of the receiver that follow any /// initial values passing `predicate`. If `predicate` never returns `NO`, an /// empty signal is returned. - (RACSignal *)skipWhileBlock:(BOOL (^)(ValueType _Nullable x))predicate RAC_WARN_UNUSED_RESULT; /// Returns a signal of values for which -isEqual: returns NO when compared to the /// previous value. - (RACSignal *)distinctUntilChanged RAC_WARN_UNUSED_RESULT; @end @interface RACSignal<__covariant ValueType> (Subscription) /// Subscribes `subscriber` to changes on the receiver. The receiver defines which /// events it actually sends and in what situations the events are sent. /// /// Subscription will always happen on a valid RACScheduler. If the /// +[RACScheduler currentScheduler] cannot be determined at the time of /// subscription (e.g., because the calling code is running on a GCD queue or /// NSOperationQueue), subscription will occur on a private background scheduler. /// On the main thread, subscriptions will always occur immediately, with a /// +[RACScheduler currentScheduler] of +[RACScheduler mainThreadScheduler]. /// /// This method must be overridden by any subclasses. /// /// Returns nil or a disposable. You can call -[RACDisposable dispose] if you /// need to end your subscription before it would "naturally" end, either by /// completing or erroring. Once the disposable has been disposed, the subscriber /// won't receive any more events from the subscription. - (RACDisposable *)subscribe:(id)subscriber; /// Convenience method to subscribe to the `next` event. /// /// This corresponds to `IObserver.OnNext` in Rx. - (RACDisposable *)subscribeNext:(void (^)(ValueType _Nullable x))nextBlock; /// Convenience method to subscribe to the `next` and `completed` events. - (RACDisposable *)subscribeNext:(void (^)(ValueType _Nullable x))nextBlock completed:(void (^)(void))completedBlock; /// Convenience method to subscribe to the `next`, `completed`, and `error` events. - (RACDisposable *)subscribeNext:(void (^)(ValueType _Nullable x))nextBlock error:(void (^)(NSError * _Nullable error))errorBlock completed:(void (^)(void))completedBlock; /// Convenience method to subscribe to `error` events. /// /// This corresponds to the `IObserver.OnError` in Rx. - (RACDisposable *)subscribeError:(void (^)(NSError * _Nullable error))errorBlock; /// Convenience method to subscribe to `completed` events. /// /// This corresponds to the `IObserver.OnCompleted` in Rx. - (RACDisposable *)subscribeCompleted:(void (^)(void))completedBlock; /// Convenience method to subscribe to `next` and `error` events. - (RACDisposable *)subscribeNext:(void (^)(ValueType _Nullable x))nextBlock error:(void (^)(NSError * _Nullable error))errorBlock; /// Convenience method to subscribe to `error` and `completed` events. - (RACDisposable *)subscribeError:(void (^)(NSError * _Nullable error))errorBlock completed:(void (^)(void))completedBlock; @end /// Additional methods to assist with debugging. @interface RACSignal<__covariant ValueType> (Debugging) /// Logs all events that the receiver sends. - (RACSignal *)logAll RAC_WARN_UNUSED_RESULT; /// Logs each `next` that the receiver sends. - (RACSignal *)logNext RAC_WARN_UNUSED_RESULT; /// Logs any error that the receiver sends. - (RACSignal *)logError RAC_WARN_UNUSED_RESULT; /// Logs any `completed` event that the receiver sends. - (RACSignal *)logCompleted RAC_WARN_UNUSED_RESULT; @end /// Additional methods to assist with unit testing. /// /// **These methods should never ship in production code.** @interface RACSignal<__covariant ValueType> (Testing) /// Spins the main run loop for a short while, waiting for the receiver to send a `next` /// or the provided timeout to elapse. /// /// **Because this method executes the run loop recursively, it should only be used /// on the main thread, and only from a unit test.** /// /// defaultValue - Returned if the receiver completes or errors before sending /// a `next`, or if the method times out. This argument may be /// nil. /// success - If not NULL, set to whether the receiver completed /// successfully. /// error - If not NULL, set to any error that occurred. /// /// Returns the first value received, or `defaultValue` if no value is received /// before the signal finishes or the method times out. - (nullable ValueType)asynchronousFirstOrDefault:(nullable ValueType)defaultValue success:(nullable BOOL *)success error:(NSError * _Nullable * _Nullable)error timeout:(NSTimeInterval)timeout; /// Spins the main run loop for a short while, waiting for the receiver to send a `next`. /// /// **Because this method executes the run loop recursively, it should only be used /// on the main thread, and only from a unit test.** /// /// defaultValue - Returned if the receiver completes or errors before sending /// a `next`, or if the method times out. This argument may be /// nil. /// success - If not NULL, set to whether the receiver completed /// successfully. /// error - If not NULL, set to any error that occurred. /// /// Returns the first value received, or `defaultValue` if no value is received /// before the signal finishes or the method times out. - (nullable ValueType)asynchronousFirstOrDefault:(nullable ValueType)defaultValue success:(nullable BOOL *)success error:(NSError * _Nullable * _Nullable)error; /// Spins the main run loop for a short while, waiting for the receiver to complete. /// or the provided timeout to elapse. /// /// **Because this method executes the run loop recursively, it should only be used /// on the main thread, and only from a unit test.** /// /// error - If not NULL, set to any error that occurs. /// /// Returns whether the signal completed successfully before timing out. If NO, /// `error` will be set to any error that occurred. - (BOOL)asynchronouslyWaitUntilCompleted:(NSError * _Nullable * _Nullable)error timeout:(NSTimeInterval)timeout; /// Spins the main run loop for a short while, waiting for the receiver to complete /// /// **Because this method executes the run loop recursively, it should only be used /// on the main thread, and only from a unit test.** /// /// error - If not NULL, set to any error that occurs. /// /// Returns whether the signal completed successfully before timing out. If NO, /// `error` will be set to any error that occurred. - (BOOL)asynchronouslyWaitUntilCompleted:(NSError * _Nullable * _Nullable)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSignal.m ================================================ // // RACSignal.m // ReactiveObjC // // Created by Josh Abernathy on 3/15/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSignal.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACDynamicSignal.h" #import "RACEmptySignal.h" #import "RACErrorSignal.h" #import "RACMulticastConnection.h" #import "RACReplaySubject.h" #import "RACReturnSignal.h" #import "RACScheduler.h" #import "RACSerialDisposable.h" #import "RACSignal+Operations.h" #import "RACSubject.h" #import "RACSubscriber+Private.h" #import "RACTuple.h" #import @implementation RACSignal #pragma mark Lifecycle + (RACSignal *)createSignal:(RACDisposable * (^)(id subscriber))didSubscribe { return [RACDynamicSignal createSignal:didSubscribe]; } + (RACSignal *)error:(NSError *)error { return [RACErrorSignal error:error]; } + (RACSignal *)never { return [[self createSignal:^ RACDisposable * (id subscriber) { return nil; }] setNameWithFormat:@"+never"]; } + (RACSignal *)startEagerlyWithScheduler:(RACScheduler *)scheduler block:(void (^)(id subscriber))block { NSCParameterAssert(scheduler != nil); NSCParameterAssert(block != NULL); RACSignal *signal = [self startLazilyWithScheduler:scheduler block:block]; // Subscribe to force the lazy signal to call its block. [[signal publish] connect]; return [signal setNameWithFormat:@"+startEagerlyWithScheduler: %@ block:", scheduler]; } + (RACSignal *)startLazilyWithScheduler:(RACScheduler *)scheduler block:(void (^)(id subscriber))block { NSCParameterAssert(scheduler != nil); NSCParameterAssert(block != NULL); RACMulticastConnection *connection = [[RACSignal createSignal:^ id (id subscriber) { block(subscriber); return nil; }] multicast:[RACReplaySubject subject]]; return [[[RACSignal createSignal:^ id (id subscriber) { [connection.signal subscribe:subscriber]; [connection connect]; return nil; }] subscribeOn:scheduler] setNameWithFormat:@"+startLazilyWithScheduler: %@ block:", scheduler]; } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p> name: %@", self.class, self, self.name]; } @end @implementation RACSignal (RACStream) + (RACSignal *)empty { return [RACEmptySignal empty]; } + (RACSignal *)return:(id)value { return [RACReturnSignal return:value]; } - (RACSignal *)bind:(RACSignalBindBlock (^)(void))block { NSCParameterAssert(block != NULL); /* * -bind: should: * * 1. Subscribe to the original signal of values. * 2. Any time the original signal sends a value, transform it using the binding block. * 3. If the binding block returns a signal, subscribe to it, and pass all of its values through to the subscriber as they're received. * 4. If the binding block asks the bind to terminate, complete the _original_ signal. * 5. When _all_ signals complete, send completed to the subscriber. * * If any signal sends an error at any point, send that to the subscriber. */ return [[RACSignal createSignal:^(id subscriber) { RACSignalBindBlock bindingBlock = block(); __block volatile int32_t signalCount = 1; // indicates self RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable]; void (^completeSignal)(RACDisposable *) = ^(RACDisposable *finishedDisposable) { if (OSAtomicDecrement32Barrier(&signalCount) == 0) { [subscriber sendCompleted]; [compoundDisposable dispose]; } else { [compoundDisposable removeDisposable:finishedDisposable]; } }; void (^addSignal)(RACSignal *) = ^(RACSignal *signal) { OSAtomicIncrement32Barrier(&signalCount); RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init]; [compoundDisposable addDisposable:selfDisposable]; RACDisposable *disposable = [signal subscribeNext:^(id x) { [subscriber sendNext:x]; } error:^(NSError *error) { [compoundDisposable dispose]; [subscriber sendError:error]; } completed:^{ @autoreleasepool { completeSignal(selfDisposable); } }]; selfDisposable.disposable = disposable; }; @autoreleasepool { RACSerialDisposable *selfDisposable = [[RACSerialDisposable alloc] init]; [compoundDisposable addDisposable:selfDisposable]; RACDisposable *bindingDisposable = [self subscribeNext:^(id x) { // Manually check disposal to handle synchronous errors. if (compoundDisposable.disposed) return; BOOL stop = NO; id signal = bindingBlock(x, &stop); @autoreleasepool { if (signal != nil) addSignal(signal); if (signal == nil || stop) { [selfDisposable dispose]; completeSignal(selfDisposable); } } } error:^(NSError *error) { [compoundDisposable dispose]; [subscriber sendError:error]; } completed:^{ @autoreleasepool { completeSignal(selfDisposable); } }]; selfDisposable.disposable = bindingDisposable; } return compoundDisposable; }] setNameWithFormat:@"[%@] -bind:", self.name]; } - (RACSignal *)concat:(RACSignal *)signal { return [[RACSignal createSignal:^(id subscriber) { RACCompoundDisposable *compoundDisposable = [[RACCompoundDisposable alloc] init]; RACDisposable *sourceDisposable = [self subscribeNext:^(id x) { [subscriber sendNext:x]; } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ RACDisposable *concattedDisposable = [signal subscribe:subscriber]; [compoundDisposable addDisposable:concattedDisposable]; }]; [compoundDisposable addDisposable:sourceDisposable]; return compoundDisposable; }] setNameWithFormat:@"[%@] -concat: %@", self.name, signal]; } - (RACSignal *)zipWith:(RACSignal *)signal { NSCParameterAssert(signal != nil); return [[RACSignal createSignal:^(id subscriber) { __block BOOL selfCompleted = NO; NSMutableArray *selfValues = [NSMutableArray array]; __block BOOL otherCompleted = NO; NSMutableArray *otherValues = [NSMutableArray array]; void (^sendCompletedIfNecessary)(void) = ^{ @synchronized (selfValues) { BOOL selfEmpty = (selfCompleted && selfValues.count == 0); BOOL otherEmpty = (otherCompleted && otherValues.count == 0); if (selfEmpty || otherEmpty) [subscriber sendCompleted]; } }; void (^sendNext)(void) = ^{ @synchronized (selfValues) { if (selfValues.count == 0) return; if (otherValues.count == 0) return; RACTuple *tuple = RACTuplePack(selfValues[0], otherValues[0]); [selfValues removeObjectAtIndex:0]; [otherValues removeObjectAtIndex:0]; [subscriber sendNext:tuple]; sendCompletedIfNecessary(); } }; RACDisposable *selfDisposable = [self subscribeNext:^(id x) { @synchronized (selfValues) { [selfValues addObject:x ?: RACTupleNil.tupleNil]; sendNext(); } } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ @synchronized (selfValues) { selfCompleted = YES; sendCompletedIfNecessary(); } }]; RACDisposable *otherDisposable = [signal subscribeNext:^(id x) { @synchronized (selfValues) { [otherValues addObject:x ?: RACTupleNil.tupleNil]; sendNext(); } } error:^(NSError *error) { [subscriber sendError:error]; } completed:^{ @synchronized (selfValues) { otherCompleted = YES; sendCompletedIfNecessary(); } }]; return [RACDisposable disposableWithBlock:^{ [selfDisposable dispose]; [otherDisposable dispose]; }]; }] setNameWithFormat:@"[%@] -zipWith: %@", self.name, signal]; } @end @implementation RACSignal (Subscription) - (RACDisposable *)subscribe:(id)subscriber { NSCAssert(NO, @"This method must be overridden by subclasses"); return nil; } - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock { NSCParameterAssert(nextBlock != NULL); RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:NULL]; return [self subscribe:o]; } - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock completed:(void (^)(void))completedBlock { NSCParameterAssert(nextBlock != NULL); NSCParameterAssert(completedBlock != NULL); RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:NULL completed:completedBlock]; return [self subscribe:o]; } - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock completed:(void (^)(void))completedBlock { NSCParameterAssert(nextBlock != NULL); NSCParameterAssert(errorBlock != NULL); NSCParameterAssert(completedBlock != NULL); RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:errorBlock completed:completedBlock]; return [self subscribe:o]; } - (RACDisposable *)subscribeError:(void (^)(NSError *error))errorBlock { NSCParameterAssert(errorBlock != NULL); RACSubscriber *o = [RACSubscriber subscriberWithNext:NULL error:errorBlock completed:NULL]; return [self subscribe:o]; } - (RACDisposable *)subscribeCompleted:(void (^)(void))completedBlock { NSCParameterAssert(completedBlock != NULL); RACSubscriber *o = [RACSubscriber subscriberWithNext:NULL error:NULL completed:completedBlock]; return [self subscribe:o]; } - (RACDisposable *)subscribeNext:(void (^)(id x))nextBlock error:(void (^)(NSError *error))errorBlock { NSCParameterAssert(nextBlock != NULL); NSCParameterAssert(errorBlock != NULL); RACSubscriber *o = [RACSubscriber subscriberWithNext:nextBlock error:errorBlock completed:NULL]; return [self subscribe:o]; } - (RACDisposable *)subscribeError:(void (^)(NSError *))errorBlock completed:(void (^)(void))completedBlock { NSCParameterAssert(completedBlock != NULL); NSCParameterAssert(errorBlock != NULL); RACSubscriber *o = [RACSubscriber subscriberWithNext:NULL error:errorBlock completed:completedBlock]; return [self subscribe:o]; } @end @implementation RACSignal (Debugging) - (RACSignal *)logAll { return [[[self logNext] logError] logCompleted]; } - (RACSignal *)logNext { return [[self doNext:^(id x) { NSLog(@"%@ next: %@", self, x); }] setNameWithFormat:@"%@", self.name]; } - (RACSignal *)logError { return [[self doError:^(NSError *error) { NSLog(@"%@ error: %@", self, error); }] setNameWithFormat:@"%@", self.name]; } - (RACSignal *)logCompleted { return [[self doCompleted:^{ NSLog(@"%@ completed", self); }] setNameWithFormat:@"%@", self.name]; } @end @implementation RACSignal (Testing) static const NSTimeInterval RACSignalAsynchronousWaitTimeout = 10; - (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error { return [self asynchronousFirstOrDefault:defaultValue success:success error:error timeout:RACSignalAsynchronousWaitTimeout]; } - (id)asynchronousFirstOrDefault:(id)defaultValue success:(BOOL *)success error:(NSError **)error timeout:(NSTimeInterval)timeout { NSCAssert([NSThread isMainThread], @"%s should only be used from the main thread", __func__); __block id result = defaultValue; __block BOOL done = NO; // Ensures that we don't pass values across thread boundaries by reference. __block NSError *localError; __block BOOL localSuccess = YES; [[[[self take:1] timeout:timeout onScheduler:[RACScheduler scheduler]] deliverOn:RACScheduler.mainThreadScheduler] subscribeNext:^(id x) { result = x; done = YES; } error:^(NSError *e) { if (!done) { localSuccess = NO; localError = e; done = YES; } } completed:^{ done = YES; }]; do { [NSRunLoop.mainRunLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; } while (!done); if (success != NULL) *success = localSuccess; if (error != NULL) *error = localError; return result; } - (BOOL)asynchronouslyWaitUntilCompleted:(NSError **)error timeout:(NSTimeInterval)timeout { BOOL success = NO; [[self ignoreValues] asynchronousFirstOrDefault:nil success:&success error:error timeout:timeout]; return success; } - (BOOL)asynchronouslyWaitUntilCompleted:(NSError **)error { return [self asynchronouslyWaitUntilCompleted:error timeout:RACSignalAsynchronousWaitTimeout]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSignalProvider.d ================================================ provider RACSignal { probe next(char *signal, char *subscriber, char *valueDescription); probe completed(char *signal, char *subscriber); probe error(char *signal, char *subscriber, char *errorDescription); }; ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSignalSequence.h ================================================ // // RACSignalSequence.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-11-09. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSequence.h" @class RACSignal<__covariant ValueType>; // Private class that adapts a RACSignal to the RACSequence interface. @interface RACSignalSequence : RACSequence // Returns a sequence for enumerating over the given signal. + (RACSequence *)sequenceWithSignal:(RACSignal *)signal; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSignalSequence.m ================================================ // // RACSignalSequence.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-11-09. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSignalSequence.h" #import "RACDisposable.h" #import "RACReplaySubject.h" #import "RACSignal+Operations.h" @interface RACSignalSequence () // Replays the signal given on initialization. @property (nonatomic, strong, readonly) RACReplaySubject *subject; @end @implementation RACSignalSequence #pragma mark Lifecycle + (RACSequence *)sequenceWithSignal:(RACSignal *)signal { RACSignalSequence *seq = [[self alloc] init]; RACReplaySubject *subject = [RACReplaySubject subject]; [signal subscribeNext:^(id value) { [subject sendNext:value]; } error:^(NSError *error) { [subject sendError:error]; } completed:^{ [subject sendCompleted]; }]; seq->_subject = subject; return seq; } #pragma mark RACSequence - (id)head { id value = [self.subject firstOrDefault:self]; if (value == self) { return nil; } else { return value ?: NSNull.null; } } - (RACSequence *)tail { RACSequence *sequence = [self.class sequenceWithSignal:[self.subject skip:1]]; sequence.name = self.name; return sequence; } - (NSArray *)array { return self.subject.toArray; } #pragma mark NSObject - (NSString *)description { // Synchronously accumulate the values that have been sent so far. NSMutableArray *values = [NSMutableArray array]; RACDisposable *disposable = [self.subject subscribeNext:^(id value) { @synchronized (values) { [values addObject:value ?: NSNull.null]; } }]; [disposable dispose]; return [NSString stringWithFormat:@"<%@: %p>{ name = %@, values = %@ … }", self.class, self, self.name, values]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACStream+Private.h ================================================ // // RACStream+Private.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-07-22. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACStream.h" @interface RACStream () // Combines a list of streams using the logic of the given block. // // streams - The streams to combine. // block - An operator that combines two streams and returns a new one. The // returned stream should contain 2-tuples of the streams' combined // values. // // Returns a combined stream. + (__kindof RACStream *)join:(id)streams block:(RACStream * (^)(id, id))block; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACStream.h ================================================ // // RACStream.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-31. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACStream; NS_ASSUME_NONNULL_BEGIN /// An abstract class representing any stream of values. /// /// This class represents a monad, upon which many stream-based operations can /// be built. /// /// When subclassing RACStream, only the methods in the main @interface body need /// to be overridden. @interface RACStream<__covariant ValueType> : NSObject _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wstrict-prototypes\"") \ typedef id _Nonnull (^RACReduceBlock)(); typedef ValueType _Nonnull (^RACGenericReduceBlock)(); _Pragma("clang diagnostic pop") /// Returns an empty stream. + (__kindof RACStream *)empty; /// Lifts `value` into the stream monad. /// /// Returns a stream containing only the given value. + (__kindof RACStream *)return:(nullable ValueType)value; /// A block which accepts a value from a RACStream and returns a new instance /// of the same stream class. /// /// Setting `stop` to `YES` will cause the bind to terminate after the returned /// value. Returning `nil` will result in immediate termination. typedef RACStream * _Nullable (^RACStreamBindBlock)(ValueType _Nullable value, BOOL *stop); /// Lazily binds a block to the values in the receiver. /// /// This should only be used if you need to terminate the bind early, or close /// over some state. -flattenMap: is more appropriate for all other cases. /// /// block - A block returning a RACStreamBindBlock. This block will be invoked /// each time the bound stream is re-evaluated. This block must not be /// nil or return nil. /// /// Returns a new stream which represents the combined result of all lazy /// applications of `block`. - (__kindof RACStream *)bind:(RACStreamBindBlock (^)(void))block; /// Appends the values of `stream` to the values in the receiver. /// /// stream - A stream to concatenate. This must be an instance of the same /// concrete class as the receiver, and should not be `nil`. /// /// Returns a new stream representing the receiver followed by `stream`. - (__kindof RACStream *)concat:(RACStream *)stream; /// Zips the values in the receiver with those of the given stream to create /// RACTuples. /// /// The first value of each stream will be combined, then the second value, and /// so forth, until at least one of the streams is exhausted. /// /// stream - The stream to zip with. This must be an instance of the same /// concrete class as the receiver, and should not be `nil`. /// /// Returns a new stream of RACTuples, representing the zipped values of the /// two streams. - (__kindof RACStream *)zipWith:(RACStream *)stream; @end /// This extension contains functionality to support naming streams for /// debugging. /// /// Subclasses do not need to override the methods here. @interface RACStream () /// The name of the stream. This is for debugging/human purposes only. @property (copy) NSString *name; /// Sets the name of the receiver to the given format string. /// /// This is for debugging purposes only, and won't do anything unless the /// RAC_DEBUG_SIGNAL_NAMES environment variable is set. /// /// Returns the receiver, for easy method chaining. - (instancetype)setNameWithFormat:(NSString *)format, ... NS_FORMAT_FUNCTION(1, 2); @end /// Operations built on the RACStream primitives. /// /// These methods do not need to be overridden, although subclasses may /// occasionally gain better performance from doing so. @interface RACStream<__covariant ValueType> (Operations) /// Maps `block` across the values in the receiver and flattens the result. /// /// Note that operators applied _after_ -flattenMap: behave differently from /// operators _within_ -flattenMap:. See the Examples section below. /// /// This corresponds to the `SelectMany` method in Rx. /// /// block - A block which accepts the values in the receiver and returns a new /// instance of the receiver's class. Returning `nil` from this block is /// equivalent to returning an empty signal. /// /// Examples /// /// [signal flattenMap:^(id x) { /// // Logs each time a returned signal completes. /// return [[RACSignal return:x] logCompleted]; /// }]; /// /// [[signal /// flattenMap:^(id x) { /// return [RACSignal return:x]; /// }] /// // Logs only once, when all of the signals complete. /// logCompleted]; /// /// Returns a new stream which represents the combined streams resulting from /// mapping `block`. - (__kindof RACStream *)flattenMap:(__kindof RACStream * _Nullable (^)(ValueType _Nullable value))block; /// Flattens a stream of streams. /// /// This corresponds to the `Merge` method in Rx. /// /// Returns a stream consisting of the combined streams obtained from the /// receiver. - (__kindof RACStream *)flatten; /// Maps `block` across the values in the receiver. /// /// This corresponds to the `Select` method in Rx. /// /// Returns a new stream with the mapped values. - (__kindof RACStream *)map:(id _Nullable (^)(ValueType _Nullable value))block; /// Replaces each value in the receiver with the given object. /// /// Returns a new stream which includes the given object once for each value in /// the receiver. - (__kindof RACStream *)mapReplace:(nullable id)object; /// Filters out values in the receiver that don't pass the given test. /// /// This corresponds to the `Where` method in Rx. /// /// Returns a new stream with only those values that passed. - (__kindof RACStream *)filter:(BOOL (^)(ValueType _Nullable value))block; /// Filters out values in the receiver that equal (via -isEqual:) the provided value. /// /// value - The value can be `nil`, in which case it ignores `nil` values. /// /// Returns a new stream containing only the values which did not compare equal /// to `value`. - (__kindof RACStream *)ignore:(nullable ValueType)value; /// Unpacks each RACTuple in the receiver and maps the values to a new value. /// /// reduceBlock - The block which reduces each RACTuple's values into one value. /// It must take as many arguments as the number of tuple elements /// to process. Each argument will be an object argument. The /// return value must be an object. This argument cannot be nil. /// /// Returns a new stream of reduced tuple values. - (__kindof RACStream *)reduceEach:(RACReduceBlock)reduceBlock; /// Returns a stream consisting of `value`, followed by the values in the /// receiver. - (__kindof RACStream *)startWith:(nullable ValueType)value; /// Skips the first `skipCount` values in the receiver. /// /// Returns the receiver after skipping the first `skipCount` values. If /// `skipCount` is greater than the number of values in the stream, an empty /// stream is returned. - (__kindof RACStream *)skip:(NSUInteger)skipCount; /// Returns a stream of the first `count` values in the receiver. If `count` is /// greater than or equal to the number of values in the stream, a stream /// equivalent to the receiver is returned. - (__kindof RACStream *)take:(NSUInteger)count; /// Zips the values in the given streams to create RACTuples. /// /// The first value of each stream will be combined, then the second value, and /// so forth, until at least one of the streams is exhausted. /// /// streams - The streams to combine. These must all be instances of the same /// concrete class implementing the protocol. If this collection is /// empty, the returned stream will be empty. /// /// Returns a new stream containing RACTuples of the zipped values from the /// streams. + (__kindof RACStream *)zip:(id)streams; /// Zips streams using +zip:, then reduces the resulting tuples into a single /// value using -reduceEach: /// /// streams - The streams to combine. These must all be instances of the /// same concrete class implementing the protocol. If this /// collection is empty, the returned stream will be empty. /// reduceBlock - The block which reduces the values from all the streams /// into one value. It must take as many arguments as the /// number of streams given. Each argument will be an object /// argument. The return value must be an object. This argument /// must not be nil. /// /// Example: /// /// [RACStream zip:@[ stringSignal, intSignal ] reduce:^(NSString *string, NSNumber *number) { /// return [NSString stringWithFormat:@"%@: %@", string, number]; /// }]; /// /// Returns a new stream containing the results from each invocation of /// `reduceBlock`. + (__kindof RACStream *)zip:(id)streams reduce:(RACGenericReduceBlock)reduceBlock; /// Returns a stream obtained by concatenating `streams` in order. + (__kindof RACStream *)concat:(id)streams; /// Combines values in the receiver from left to right using the given block. /// /// The algorithm proceeds as follows: /// /// 1. `startingValue` is passed into the block as the `running` value, and the /// first element of the receiver is passed into the block as the `next` value. /// 2. The result of the invocation is added to the returned stream. /// 3. The result of the invocation (`running`) and the next element of the /// receiver (`next`) is passed into `block`. /// 4. Steps 2 and 3 are repeated until all values have been processed. /// /// startingValue - The value to be combined with the first element of the /// receiver. This value may be `nil`. /// reduceBlock - The block that describes how to combine values of the /// receiver. If the receiver is empty, this block will never be /// invoked. Cannot be nil. /// /// Examples /// /// RACSequence *numbers = @[ @1, @2, @3, @4 ].rac_sequence; /// /// // Contains 1, 3, 6, 10 /// RACSequence *sums = [numbers scanWithStart:@0 reduce:^(NSNumber *sum, NSNumber *next) { /// return @(sum.integerValue + next.integerValue); /// }]; /// /// Returns a new stream that consists of each application of `reduceBlock`. If the /// receiver is empty, an empty stream is returned. - (__kindof RACStream *)scanWithStart:(nullable id)startingValue reduce:(id _Nullable (^)(id _Nullable running, ValueType _Nullable next))reduceBlock; /// Combines values in the receiver from left to right using the given block /// which also takes zero-based index of the values. /// /// startingValue - The value to be combined with the first element of the /// receiver. This value may be `nil`. /// reduceBlock - The block that describes how to combine values of the /// receiver. This block takes zero-based index value as the last /// parameter. If the receiver is empty, this block will never /// be invoked. Cannot be nil. /// /// Returns a new stream that consists of each application of `reduceBlock`. If the /// receiver is empty, an empty stream is returned. - (__kindof RACStream *)scanWithStart:(nullable id)startingValue reduceWithIndex:(id _Nullable (^)(id _Nullable running, ValueType _Nullable next, NSUInteger index))reduceBlock; /// Combines each previous and current value into one object. /// /// This method is similar to -scanWithStart:reduce:, but only ever operates on /// the previous and current values (instead of the whole stream), and does not /// pass the return value of `reduceBlock` into the next invocation of it. /// /// start - The value passed into `reduceBlock` as `previous` for the /// first value. /// reduceBlock - The block that combines the previous value and the current /// value to create the reduced value. Cannot be nil. /// /// Examples /// /// RACSequence *numbers = @[ @1, @2, @3, @4 ].rac_sequence; /// /// // Contains 1, 3, 5, 7 /// RACSequence *sums = [numbers combinePreviousWithStart:@0 reduce:^(NSNumber *previous, NSNumber *next) { /// return @(previous.integerValue + next.integerValue); /// }]; /// /// Returns a new stream consisting of the return values from each application of /// `reduceBlock`. - (__kindof RACStream *)combinePreviousWithStart:(nullable ValueType)start reduce:(id _Nullable (^)(ValueType _Nullable previous, ValueType _Nullable current))reduceBlock; /// Takes values until the given block returns `YES`. /// /// Returns a stream of the initial values in the receiver that fail `predicate`. /// If `predicate` never returns `YES`, a stream equivalent to the receiver is /// returned. - (__kindof RACStream *)takeUntilBlock:(BOOL (^)(ValueType _Nullable x))predicate; /// Takes values until the given block returns `NO`. /// /// Returns a stream of the initial values in the receiver that pass `predicate`. /// If `predicate` never returns `NO`, a stream equivalent to the receiver is /// returned. - (__kindof RACStream *)takeWhileBlock:(BOOL (^)(ValueType _Nullable x))predicate; /// Skips values until the given block returns `YES`. /// /// Returns a stream containing the values of the receiver that follow any /// initial values failing `predicate`. If `predicate` never returns `YES`, /// an empty stream is returned. - (__kindof RACStream *)skipUntilBlock:(BOOL (^)(ValueType _Nullable x))predicate; /// Skips values until the given block returns `NO`. /// /// Returns a stream containing the values of the receiver that follow any /// initial values passing `predicate`. If `predicate` never returns `NO`, an /// empty stream is returned. - (__kindof RACStream *)skipWhileBlock:(BOOL (^)(ValueType _Nullable x))predicate; /// Returns a stream of values for which -isEqual: returns NO when compared to the /// previous value. - (__kindof RACStream *)distinctUntilChanged; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACStream.m ================================================ // // RACStream.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-31. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACStream.h" #import "NSObject+RACDescription.h" #import "RACBlockTrampoline.h" #import "RACTuple.h" @implementation RACStream #pragma mark Lifecycle - (instancetype)init { self = [super init]; self.name = @""; return self; } #pragma mark Abstract methods + (__kindof RACStream *)empty { NSString *reason = [NSString stringWithFormat:@"%@ must be overridden by subclasses", NSStringFromSelector(_cmd)]; @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil]; } - (__kindof RACStream *)bind:(RACStreamBindBlock (^)(void))block { NSString *reason = [NSString stringWithFormat:@"%@ must be overridden by subclasses", NSStringFromSelector(_cmd)]; @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil]; } + (__kindof RACStream *)return:(id)value { NSString *reason = [NSString stringWithFormat:@"%@ must be overridden by subclasses", NSStringFromSelector(_cmd)]; @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil]; } - (__kindof RACStream *)concat:(RACStream *)stream { NSString *reason = [NSString stringWithFormat:@"%@ must be overridden by subclasses", NSStringFromSelector(_cmd)]; @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil]; } - (__kindof RACStream *)zipWith:(RACStream *)stream { NSString *reason = [NSString stringWithFormat:@"%@ must be overridden by subclasses", NSStringFromSelector(_cmd)]; @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:reason userInfo:nil]; } #pragma mark Naming - (instancetype)setNameWithFormat:(NSString *)format, ... { if (getenv("RAC_DEBUG_SIGNAL_NAMES") == NULL) return self; NSCParameterAssert(format != nil); va_list args; va_start(args, format); NSString *str = [[NSString alloc] initWithFormat:format arguments:args]; va_end(args); self.name = str; return self; } @end @implementation RACStream (Operations) - (__kindof RACStream *)flattenMap:(__kindof RACStream * (^)(id value))block { Class class = self.class; return [[self bind:^{ return ^(id value, BOOL *stop) { id stream = block(value) ?: [class empty]; NSCAssert([stream isKindOfClass:RACStream.class], @"Value returned from -flattenMap: is not a stream: %@", stream); return stream; }; }] setNameWithFormat:@"[%@] -flattenMap:", self.name]; } - (__kindof RACStream *)flatten { return [[self flattenMap:^(id value) { return value; }] setNameWithFormat:@"[%@] -flatten", self.name]; } - (__kindof RACStream *)map:(id (^)(id value))block { NSCParameterAssert(block != nil); Class class = self.class; return [[self flattenMap:^(id value) { return [class return:block(value)]; }] setNameWithFormat:@"[%@] -map:", self.name]; } - (__kindof RACStream *)mapReplace:(id)object { return [[self map:^(id _) { return object; }] setNameWithFormat:@"[%@] -mapReplace: %@", self.name, RACDescription(object)]; } - (__kindof RACStream *)combinePreviousWithStart:(id)start reduce:(id (^)(id previous, id next))reduceBlock { NSCParameterAssert(reduceBlock != NULL); return [[[self scanWithStart:RACTuplePack(start) reduce:^(RACTuple *previousTuple, id next) { id value = reduceBlock(previousTuple[0], next); return RACTuplePack(next, value); }] map:^(RACTuple *tuple) { return tuple[1]; }] setNameWithFormat:@"[%@] -combinePreviousWithStart: %@ reduce:", self.name, RACDescription(start)]; } - (__kindof RACStream *)filter:(BOOL (^)(id value))block { NSCParameterAssert(block != nil); Class class = self.class; return [[self flattenMap:^ id (id value) { if (block(value)) { return [class return:value]; } else { return class.empty; } }] setNameWithFormat:@"[%@] -filter:", self.name]; } - (__kindof RACStream *)ignore:(id)value { return [[self filter:^ BOOL (id innerValue) { return innerValue != value && ![innerValue isEqual:value]; }] setNameWithFormat:@"[%@] -ignore: %@", self.name, RACDescription(value)]; } - (__kindof RACStream *)reduceEach:(RACReduceBlock)reduceBlock { NSCParameterAssert(reduceBlock != nil); __weak RACStream *stream __attribute__((unused)) = self; return [[self map:^(RACTuple *t) { NSCAssert([t isKindOfClass:RACTuple.class], @"Value from stream %@ is not a tuple: %@", stream, t); return [RACBlockTrampoline invokeBlock:reduceBlock withArguments:t]; }] setNameWithFormat:@"[%@] -reduceEach:", self.name]; } - (__kindof RACStream *)startWith:(id)value { return [[[self.class return:value] concat:self] setNameWithFormat:@"[%@] -startWith: %@", self.name, RACDescription(value)]; } - (__kindof RACStream *)skip:(NSUInteger)skipCount { Class class = self.class; return [[self bind:^{ __block NSUInteger skipped = 0; return ^(id value, BOOL *stop) { if (skipped >= skipCount) return [class return:value]; skipped++; return class.empty; }; }] setNameWithFormat:@"[%@] -skip: %lu", self.name, (unsigned long)skipCount]; } - (__kindof RACStream *)take:(NSUInteger)count { Class class = self.class; if (count == 0) return class.empty; return [[self bind:^{ __block NSUInteger taken = 0; return ^ id (id value, BOOL *stop) { if (taken < count) { ++taken; if (taken == count) *stop = YES; return [class return:value]; } else { return nil; } }; }] setNameWithFormat:@"[%@] -take: %lu", self.name, (unsigned long)count]; } + (__kindof RACStream *)join:(id)streams block:(RACStream * (^)(id, id))block { RACStream *current = nil; // Creates streams of successively larger tuples by combining the input // streams one-by-one. for (RACStream *stream in streams) { // For the first stream, just wrap its values in a RACTuple. That way, // if only one stream is given, the result is still a stream of tuples. if (current == nil) { current = [stream map:^(id x) { return RACTuplePack(x); }]; continue; } current = block(current, stream); } if (current == nil) return [self empty]; return [current map:^(RACTuple *xs) { // Right now, each value is contained in its own tuple, sorta like: // // (((1), 2), 3) // // We need to unwrap all the layers and create a tuple out of the result. NSMutableArray *values = [[NSMutableArray alloc] init]; while (xs != nil) { [values insertObject:xs.last ?: RACTupleNil.tupleNil atIndex:0]; xs = (xs.count > 1 ? xs.first : nil); } return [RACTuple tupleWithObjectsFromArray:values]; }]; } + (__kindof RACStream *)zip:(id)streams { return [[self join:streams block:^(RACStream *left, RACStream *right) { return [left zipWith:right]; }] setNameWithFormat:@"+zip: %@", streams]; } + (__kindof RACStream *)zip:(id)streams reduce:(RACGenericReduceBlock)reduceBlock { NSCParameterAssert(reduceBlock != nil); RACStream *result = [self zip:streams]; // Although we assert this condition above, older versions of this method // supported this argument being nil. Avoid crashing Release builds of // apps that depended on that. if (reduceBlock != nil) result = [result reduceEach:reduceBlock]; return [result setNameWithFormat:@"+zip: %@ reduce:", streams]; } + (__kindof RACStream *)concat:(id)streams { RACStream *result = self.empty; for (RACStream *stream in streams) { result = [result concat:stream]; } return [result setNameWithFormat:@"+concat: %@", streams]; } - (__kindof RACStream *)scanWithStart:(id)startingValue reduce:(id (^)(id running, id next))reduceBlock { NSCParameterAssert(reduceBlock != nil); return [[self scanWithStart:startingValue reduceWithIndex:^(id running, id next, NSUInteger index) { return reduceBlock(running, next); }] setNameWithFormat:@"[%@] -scanWithStart: %@ reduce:", self.name, RACDescription(startingValue)]; } - (__kindof RACStream *)scanWithStart:(id)startingValue reduceWithIndex:(id (^)(id, id, NSUInteger))reduceBlock { NSCParameterAssert(reduceBlock != nil); Class class = self.class; return [[self bind:^{ __block id running = startingValue; __block NSUInteger index = 0; return ^(id value, BOOL *stop) { running = reduceBlock(running, value, index++); return [class return:running]; }; }] setNameWithFormat:@"[%@] -scanWithStart: %@ reduceWithIndex:", self.name, RACDescription(startingValue)]; } - (__kindof RACStream *)takeUntilBlock:(BOOL (^)(id x))predicate { NSCParameterAssert(predicate != nil); Class class = self.class; return [[self bind:^{ return ^ id (id value, BOOL *stop) { if (predicate(value)) return nil; return [class return:value]; }; }] setNameWithFormat:@"[%@] -takeUntilBlock:", self.name]; } - (__kindof RACStream *)takeWhileBlock:(BOOL (^)(id x))predicate { NSCParameterAssert(predicate != nil); return [[self takeUntilBlock:^ BOOL (id x) { return !predicate(x); }] setNameWithFormat:@"[%@] -takeWhileBlock:", self.name]; } - (__kindof RACStream *)skipUntilBlock:(BOOL (^)(id x))predicate { NSCParameterAssert(predicate != nil); Class class = self.class; return [[self bind:^{ __block BOOL skipping = YES; return ^ id (id value, BOOL *stop) { if (skipping) { if (predicate(value)) { skipping = NO; } else { return class.empty; } } return [class return:value]; }; }] setNameWithFormat:@"[%@] -skipUntilBlock:", self.name]; } - (__kindof RACStream *)skipWhileBlock:(BOOL (^)(id x))predicate { NSCParameterAssert(predicate != nil); return [[self skipUntilBlock:^ BOOL (id x) { return !predicate(x); }] setNameWithFormat:@"[%@] -skipWhileBlock:", self.name]; } - (__kindof RACStream *)distinctUntilChanged { Class class = self.class; return [[self bind:^{ __block id lastValue = nil; __block BOOL initial = YES; return ^(id x, BOOL *stop) { if (!initial && (lastValue == x || [x isEqual:lastValue])) return [class empty]; initial = NO; lastValue = x; return [class return:x]; }; }] setNameWithFormat:@"[%@] -distinctUntilChanged", self.name]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACStringSequence.h ================================================ // // RACStringSequence.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "RACSequence.h" // Private class that adapts a string to the RACSequence interface. @interface RACStringSequence : RACSequence // Returns a sequence for enumerating over the given string, starting from the // given character offset. The string will be copied to prevent mutation. + (RACSequence *)sequenceWithString:(NSString *)string offset:(NSUInteger)offset; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACStringSequence.m ================================================ // // RACStringSequence.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2012-10-29. // Copyright (c) 2012 GitHub. All rights reserved. // #import "RACStringSequence.h" @interface RACStringSequence () // The string being sequenced. @property (nonatomic, copy, readonly) NSString *string; // The index in the string from which the sequence starts. @property (nonatomic, assign, readonly) NSUInteger offset; @end @implementation RACStringSequence #pragma mark Lifecycle + (RACSequence *)sequenceWithString:(NSString *)string offset:(NSUInteger)offset { NSCParameterAssert(offset <= string.length); if (offset == string.length) return self.empty; RACStringSequence *seq = [[self alloc] init]; seq->_string = [string copy]; seq->_offset = offset; return seq; } #pragma mark RACSequence - (id)head { return [self.string substringWithRange:NSMakeRange(self.offset, 1)]; } - (RACSequence *)tail { RACSequence *sequence = [self.class sequenceWithString:self.string offset:self.offset + 1]; sequence.name = self.name; return sequence; } - (NSArray *)array { NSUInteger substringLength = self.string.length - self.offset; NSMutableArray *array = [NSMutableArray arrayWithCapacity:substringLength]; [self.string enumerateSubstringsInRange:NSMakeRange(self.offset, substringLength) options:NSStringEnumerationByComposedCharacterSequences usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) { [array addObject:substring]; }]; return [array copy]; } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p>{ name = %@, string = %@ }", self.class, self, self.name, [self.string substringFromIndex:self.offset]]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSubject.h ================================================ // // RACSubject.h // ReactiveObjC // // Created by Josh Abernathy on 3/9/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSignal.h" #import "RACSubscriber.h" NS_ASSUME_NONNULL_BEGIN /// A subject can be thought of as a signal that you can manually control by /// sending next, completed, and error. /// /// They're most helpful in bridging the non-RAC world to RAC, since they let you /// manually control the sending of events. @interface RACSubject : RACSignal /// Returns a new subject. + (instancetype)subject; // Redeclaration of the RACSubscriber method. Made in order to specify a generic type. - (void)sendNext:(nullable ValueType)value; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSubject.m ================================================ // // RACSubject.m // ReactiveObjC // // Created by Josh Abernathy on 3/9/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSubject.h" #import #import "RACCompoundDisposable.h" #import "RACPassthroughSubscriber.h" @interface RACSubject () // Contains all current subscribers to the receiver. // // This should only be used while synchronized on `self`. @property (nonatomic, strong, readonly) NSMutableArray *subscribers; // Contains all of the receiver's subscriptions to other signals. @property (nonatomic, strong, readonly) RACCompoundDisposable *disposable; // Enumerates over each of the receiver's `subscribers` and invokes `block` for // each. - (void)enumerateSubscribersUsingBlock:(void (^)(id subscriber))block; @end @implementation RACSubject #pragma mark Lifecycle + (instancetype)subject { return [[self alloc] init]; } - (instancetype)init { self = [super init]; if (self == nil) return nil; _disposable = [RACCompoundDisposable compoundDisposable]; _subscribers = [[NSMutableArray alloc] initWithCapacity:1]; return self; } - (void)dealloc { [self.disposable dispose]; } #pragma mark Subscription - (RACDisposable *)subscribe:(id)subscriber { NSCParameterAssert(subscriber != nil); RACCompoundDisposable *disposable = [RACCompoundDisposable compoundDisposable]; subscriber = [[RACPassthroughSubscriber alloc] initWithSubscriber:subscriber signal:self disposable:disposable]; NSMutableArray *subscribers = self.subscribers; @synchronized (subscribers) { [subscribers addObject:subscriber]; } [disposable addDisposable:[RACDisposable disposableWithBlock:^{ @synchronized (subscribers) { // Since newer subscribers are generally shorter-lived, search // starting from the end of the list. NSUInteger index = [subscribers indexOfObjectWithOptions:NSEnumerationReverse passingTest:^ BOOL (id obj, NSUInteger index, BOOL *stop) { return obj == subscriber; }]; if (index != NSNotFound) [subscribers removeObjectAtIndex:index]; } }]]; return disposable; } - (void)enumerateSubscribersUsingBlock:(void (^)(id subscriber))block { NSArray *subscribers; @synchronized (self.subscribers) { subscribers = [self.subscribers copy]; } for (id subscriber in subscribers) { block(subscriber); } } #pragma mark RACSubscriber - (void)sendNext:(id)value { [self enumerateSubscribersUsingBlock:^(id subscriber) { [subscriber sendNext:value]; }]; } - (void)sendError:(NSError *)error { [self.disposable dispose]; [self enumerateSubscribersUsingBlock:^(id subscriber) { [subscriber sendError:error]; }]; } - (void)sendCompleted { [self.disposable dispose]; [self enumerateSubscribersUsingBlock:^(id subscriber) { [subscriber sendCompleted]; }]; } - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)d { if (d.disposed) return; [self.disposable addDisposable:d]; @weakify(self, d); [d addDisposable:[RACDisposable disposableWithBlock:^{ @strongify(self, d); [self.disposable removeDisposable:d]; }]]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSubscriber+Private.h ================================================ // // RACSubscriber+Private.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-06-13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSubscriber.h" // A simple block-based subscriber. @interface RACSubscriber : NSObject // Creates a new subscriber with the given blocks. + (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSubscriber.h ================================================ // // RACSubscriber.h // ReactiveObjC // // Created by Josh Abernathy on 3/1/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACCompoundDisposable; NS_ASSUME_NONNULL_BEGIN /// Represents any object which can directly receive values from a RACSignal. /// /// You generally shouldn't need to implement this protocol. +[RACSignal /// createSignal:], RACSignal's subscription methods, or RACSubject should work /// for most uses. /// /// Implementors of this protocol may receive messages and values from multiple /// threads simultaneously, and so should be thread-safe. Subscribers will also /// be weakly referenced so implementations must allow that. @protocol RACSubscriber @required /// Sends the next value to subscribers. /// /// value - The value to send. This can be `nil`. - (void)sendNext:(nullable id)value; /// Sends the error to subscribers. /// /// error - The error to send. This can be `nil`. /// /// This terminates the subscription, and invalidates the subscriber (such that /// it cannot subscribe to anything else in the future). - (void)sendError:(nullable NSError *)error; /// Sends completed to subscribers. /// /// This terminates the subscription, and invalidates the subscriber (such that /// it cannot subscribe to anything else in the future). - (void)sendCompleted; /// Sends the subscriber a disposable that represents one of its subscriptions. /// /// A subscriber may receive multiple disposables if it gets subscribed to /// multiple signals; however, any error or completed events must terminate _all_ /// subscriptions. - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)disposable; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSubscriber.m ================================================ // // RACSubscriber.m // ReactiveObjC // // Created by Josh Abernathy on 3/1/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSubscriber.h" #import "RACSubscriber+Private.h" #import #import "RACCompoundDisposable.h" @interface RACSubscriber () // These callbacks should only be accessed while synchronized on self. @property (nonatomic, copy) void (^next)(id value); @property (nonatomic, copy) void (^error)(NSError *error); @property (nonatomic, copy) void (^completed)(void); @property (nonatomic, strong, readonly) RACCompoundDisposable *disposable; @end @implementation RACSubscriber #pragma mark Lifecycle + (instancetype)subscriberWithNext:(void (^)(id x))next error:(void (^)(NSError *error))error completed:(void (^)(void))completed { RACSubscriber *subscriber = [[self alloc] init]; subscriber->_next = [next copy]; subscriber->_error = [error copy]; subscriber->_completed = [completed copy]; return subscriber; } - (instancetype)init { self = [super init]; @unsafeify(self); RACDisposable *selfDisposable = [RACDisposable disposableWithBlock:^{ @strongify(self); @synchronized (self) { self.next = nil; self.error = nil; self.completed = nil; } }]; _disposable = [RACCompoundDisposable compoundDisposable]; [_disposable addDisposable:selfDisposable]; return self; } - (void)dealloc { [self.disposable dispose]; } #pragma mark RACSubscriber - (void)sendNext:(id)value { @synchronized (self) { void (^nextBlock)(id) = [self.next copy]; if (nextBlock == nil) return; nextBlock(value); } } - (void)sendError:(NSError *)e { @synchronized (self) { void (^errorBlock)(NSError *) = [self.error copy]; [self.disposable dispose]; if (errorBlock == nil) return; errorBlock(e); } } - (void)sendCompleted { @synchronized (self) { void (^completedBlock)(void) = [self.completed copy]; [self.disposable dispose]; if (completedBlock == nil) return; completedBlock(); } } - (void)didSubscribeWithDisposable:(RACCompoundDisposable *)otherDisposable { if (otherDisposable.disposed) return; RACCompoundDisposable *selfDisposable = self.disposable; [selfDisposable addDisposable:otherDisposable]; @unsafeify(otherDisposable); // If this subscription terminates, purge its disposable to avoid unbounded // memory growth. [otherDisposable addDisposable:[RACDisposable disposableWithBlock:^{ @strongify(otherDisposable); [selfDisposable removeDisposable:otherDisposable]; }]]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSubscriptingAssignmentTrampoline.h ================================================ // // RACSubscriptingAssignmentTrampoline.h // ReactiveObjC // // Created by Josh Abernathy on 9/24/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import #import @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN /// Assigns a signal to an object property, automatically setting the given key /// path on every `next`. When the signal completes, the binding is automatically /// disposed of. /// /// There are two different versions of this macro: /// /// - RAC(TARGET, KEYPATH, NILVALUE) will bind the `KEYPATH` of `TARGET` to the /// given signal. If the signal ever sends a `nil` value, the property will be /// set to `NILVALUE` instead. `NILVALUE` may itself be `nil` for object /// properties, but an NSValue should be used for primitive properties, to /// avoid an exception if `nil` is sent (which might occur if an intermediate /// object is set to `nil`). /// - RAC(TARGET, KEYPATH) is the same as the above, but `NILVALUE` defaults to /// `nil`. /// /// See -[RACSignal setKeyPath:onObject:nilValue:] for more information about the /// binding's semantics. /// /// Examples /// /// RAC(self, objectProperty) = objectSignal; /// RAC(self, stringProperty, @"foobar") = stringSignal; /// RAC(self, integerProperty, @42) = integerSignal; /// /// WARNING: Under certain conditions, use of this macro can be thread-unsafe. /// See the documentation of -setKeyPath:onObject:nilValue:. #define RAC(TARGET, ...) \ metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__)) \ (RAC_(TARGET, __VA_ARGS__, nil)) \ (RAC_(TARGET, __VA_ARGS__)) /// Do not use this directly. Use the RAC macro above. #define RAC_(TARGET, KEYPATH, NILVALUE) \ [[RACSubscriptingAssignmentTrampoline alloc] initWithTarget:(TARGET) nilValue:(NILVALUE)][@keypath(TARGET, KEYPATH)] @interface RACSubscriptingAssignmentTrampoline : NSObject - (nullable instancetype)initWithTarget:(nullable id)target nilValue:(nullable id)nilValue; - (void)setObject:(RACSignal *)signal forKeyedSubscript:(NSString *)keyPath; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSubscriptingAssignmentTrampoline.m ================================================ // // RACSubscriptingAssignmentTrampoline.m // ReactiveObjC // // Created by Josh Abernathy on 9/24/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSubscriptingAssignmentTrampoline.h" #import "RACSignal+Operations.h" @interface RACSubscriptingAssignmentTrampoline () // The object to bind to. @property (nonatomic, strong, readonly) id target; // A value to use when `nil` is sent on the bound signal. @property (nonatomic, strong, readonly) id nilValue; @end @implementation RACSubscriptingAssignmentTrampoline - (instancetype)initWithTarget:(id)target nilValue:(id)nilValue { // This is often a programmer error, but this prevents crashes if the target // object has unexpectedly deallocated. if (target == nil) return nil; self = [super init]; if (self == nil) return nil; _target = target; _nilValue = nilValue; return self; } - (void)setObject:(RACSignal *)signal forKeyedSubscript:(NSString *)keyPath { [signal setKeyPath:keyPath onObject:self.target nilValue:self.nilValue]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSubscriptionScheduler.h ================================================ // // RACSubscriptionScheduler.h // ReactiveObjC // // Created by Josh Abernathy on 11/30/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACScheduler.h" NS_ASSUME_NONNULL_BEGIN // A private scheduler used only for subscriptions. See the private // +[RACScheduler subscriptionScheduler] method for more information. @interface RACSubscriptionScheduler : RACScheduler @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACSubscriptionScheduler.m ================================================ // // RACSubscriptionScheduler.m // ReactiveObjC // // Created by Josh Abernathy on 11/30/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACSubscriptionScheduler.h" #import "RACScheduler+Private.h" @interface RACSubscriptionScheduler () // A private background scheduler on which to subscribe if the +currentScheduler // is unknown. @property (nonatomic, strong, readonly) RACScheduler *backgroundScheduler; @end @implementation RACSubscriptionScheduler #pragma mark Lifecycle - (instancetype)init { self = [super initWithName:@"org.reactivecocoa.ReactiveObjC.RACScheduler.subscriptionScheduler"]; _backgroundScheduler = [RACScheduler scheduler]; return self; } #pragma mark RACScheduler - (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != NULL); if (RACScheduler.currentScheduler == nil) return [self.backgroundScheduler schedule:block]; block(); return nil; } - (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { RACScheduler *scheduler = RACScheduler.currentScheduler ?: self.backgroundScheduler; return [scheduler after:date schedule:block]; } - (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { RACScheduler *scheduler = RACScheduler.currentScheduler ?: self.backgroundScheduler; return [scheduler after:date repeatingEvery:interval withLeeway:leeway schedule:block]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACTargetQueueScheduler.h ================================================ // // RACTargetQueueScheduler.h // ReactiveObjC // // Created by Josh Abernathy on 6/6/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACQueueScheduler.h" NS_ASSUME_NONNULL_BEGIN /// A scheduler that enqueues blocks on a private serial queue, targeting an /// arbitrary GCD queue. @interface RACTargetQueueScheduler : RACQueueScheduler /// Initializes the receiver with a serial queue that will target the given /// `targetQueue`. /// /// name - The name of the scheduler. If nil, a default name will be used. /// targetQueue - The queue to target. Cannot be NULL. /// /// Returns the initialized object. - (instancetype)initWithName:(nullable NSString *)name targetQueue:(dispatch_queue_t)targetQueue; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACTargetQueueScheduler.m ================================================ // // RACTargetQueueScheduler.m // ReactiveObjC // // Created by Josh Abernathy on 6/6/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACTargetQueueScheduler.h" #import "RACQueueScheduler+Subclass.h" @implementation RACTargetQueueScheduler #pragma mark Lifecycle - (instancetype)initWithName:(NSString *)name targetQueue:(dispatch_queue_t)targetQueue { NSCParameterAssert(targetQueue != NULL); if (name == nil) { name = [NSString stringWithFormat:@"org.reactivecocoa.ReactiveObjC.RACTargetQueueScheduler(%s)", dispatch_queue_get_label(targetQueue)]; } dispatch_queue_t queue = dispatch_queue_create(name.UTF8String, DISPATCH_QUEUE_SERIAL); if (queue == NULL) return nil; dispatch_set_target_queue(queue, targetQueue); return [super initWithName:name queue:queue]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACTestScheduler.h ================================================ // // RACTestScheduler.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-07-06. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACScheduler.h" NS_ASSUME_NONNULL_BEGIN /// A special kind of scheduler that steps through virtualized time. /// /// This scheduler class can be used in unit tests to verify asynchronous /// behaviors without spending significant time waiting. /// /// This class can be used from multiple threads, but only one thread can `step` /// through the enqueued actions at a time. Other threads will wait while the /// scheduled blocks are being executed. @interface RACTestScheduler : RACScheduler /// Initializes a new test scheduler. - (instancetype)init; /// Executes the next scheduled block, if any. /// /// This method will block until the scheduled action has completed. - (void)step; /// Executes up to the next `ticks` scheduled blocks. /// /// This method will block until the scheduled actions have completed. /// /// ticks - The number of scheduled blocks to execute. If there aren't this many /// blocks enqueued, all scheduled blocks are executed. - (void)step:(NSUInteger)ticks; /// Executes all of the scheduled blocks on the receiver. /// /// This method will block until the scheduled actions have completed. - (void)stepAll; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACTestScheduler.m ================================================ // // RACTestScheduler.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-07-06. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACTestScheduler.h" #import #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACScheduler+Private.h" @interface RACTestSchedulerAction : NSObject // The date at which the action should be executed. // // This absolute time will not actually be honored. This date is only used for // comparison, to determine which block should be run _next_. @property (nonatomic, copy, readonly) NSDate *date; // The scheduled block. @property (nonatomic, copy, readonly) void (^block)(void); // A disposable for this action. // // When disposed, the action should not start executing if it hasn't already. @property (nonatomic, strong, readonly) RACDisposable *disposable; // Initializes a new scheduler action. - (instancetype)initWithDate:(NSDate *)date block:(void (^)(void))block; @end static CFComparisonResult RACCompareScheduledActions(const void *ptr1, const void *ptr2, void *info) { RACTestSchedulerAction *action1 = (__bridge id)ptr1; RACTestSchedulerAction *action2 = (__bridge id)ptr2; return CFDateCompare((__bridge CFDateRef)action1.date, (__bridge CFDateRef)action2.date, NULL); } static const void *RACRetainScheduledAction(CFAllocatorRef allocator, const void *ptr) { return CFRetain(ptr); } static void RACReleaseScheduledAction(CFAllocatorRef allocator, const void *ptr) { CFRelease(ptr); } @interface RACTestScheduler () // All of the RACTestSchedulerActions that have been enqueued and not yet // executed. // // The minimum value in the heap represents the action to execute next. // // This property should only be used while synchronized on self. @property (nonatomic, assign, readonly) CFBinaryHeapRef scheduledActions; // The number of blocks that have been directly enqueued with -schedule: so // far. // // This is used to ensure unique dates when two blocks are enqueued // simultaneously. // // This property should only be used while synchronized on self. @property (nonatomic, assign) NSUInteger numberOfDirectlyScheduledBlocks; @end @implementation RACTestScheduler #pragma mark Lifecycle - (instancetype)init { self = [super initWithName:@"org.reactivecocoa.ReactiveObjC.RACTestScheduler"]; CFBinaryHeapCallBacks callbacks = (CFBinaryHeapCallBacks){ .version = 0, .retain = &RACRetainScheduledAction, .release = &RACReleaseScheduledAction, .copyDescription = &CFCopyDescription, .compare = &RACCompareScheduledActions }; _scheduledActions = CFBinaryHeapCreate(NULL, 0, &callbacks, NULL); return self; } - (void)dealloc { [self stepAll]; if (_scheduledActions != NULL) { CFBridgingRelease(_scheduledActions); _scheduledActions = NULL; } } #pragma mark Execution - (void)step { [self step:1]; } - (void)step:(NSUInteger)ticks { @synchronized (self) { for (NSUInteger i = 0; i < ticks; i++) { const void *actionPtr = NULL; if (!CFBinaryHeapGetMinimumIfPresent(self.scheduledActions, &actionPtr)) break; RACTestSchedulerAction *action = (__bridge id)actionPtr; CFBinaryHeapRemoveMinimumValue(self.scheduledActions); if (action.disposable.disposed) continue; RACScheduler *previousScheduler = RACScheduler.currentScheduler; NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = self; action.block(); if (previousScheduler != nil) { NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = previousScheduler; } else { [NSThread.currentThread.threadDictionary removeObjectForKey:RACSchedulerCurrentSchedulerKey]; } } } } - (void)stepAll { [self step:NSUIntegerMax]; } #pragma mark RACScheduler - (RACDisposable *)schedule:(void (^)(void))block { NSCParameterAssert(block != nil); @synchronized (self) { NSDate *uniqueDate = [NSDate dateWithTimeIntervalSinceReferenceDate:self.numberOfDirectlyScheduledBlocks]; self.numberOfDirectlyScheduledBlocks++; RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:uniqueDate block:block]; CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action); return action.disposable; } } - (RACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(block != nil); @synchronized (self) { RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:date block:block]; CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action); return action.disposable; } } - (RACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(block != nil); NSCParameterAssert(interval >= 0); NSCParameterAssert(leeway >= 0); RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable]; @weakify(self); @synchronized (self) { __block RACDisposable *thisDisposable = nil; void (^reschedulingBlock)(void) = ^{ @strongify(self); [compoundDisposable removeDisposable:thisDisposable]; // Schedule the next interval. RACDisposable *schedulingDisposable = [self after:[date dateByAddingTimeInterval:interval] repeatingEvery:interval withLeeway:leeway schedule:block]; [compoundDisposable addDisposable:schedulingDisposable]; block(); }; RACTestSchedulerAction *action = [[RACTestSchedulerAction alloc] initWithDate:date block:reschedulingBlock]; CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action); thisDisposable = action.disposable; [compoundDisposable addDisposable:thisDisposable]; } return compoundDisposable; } @end @implementation RACTestSchedulerAction #pragma mark Lifecycle - (instancetype)initWithDate:(NSDate *)date block:(void (^)(void))block { NSCParameterAssert(date != nil); NSCParameterAssert(block != nil); self = [super init]; _date = [date copy]; _block = [block copy]; _disposable = [[RACDisposable alloc] init]; return self; } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p>{ date: %@ }", self.class, self, self.date]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACTuple.h ================================================ // // RACTuple.h // ReactiveObjC // // Created by Josh Abernathy on 4/12/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import #import "RACmetamacros.h" @class RACSequence; /// Creates a new tuple with the given values. At least one value must be given. /// Values can be nil. #define RACTuplePack(...) \ RACTuplePack_(__VA_ARGS__) /// Declares new object variables and unpacks a RACTuple into them. /// /// This macro should be used on the left side of an assignment, with the /// tuple on the right side. Nothing else should appear on the same line, and the /// macro should not be the only statement in a conditional or loop body. /// /// If the tuple has more values than there are variables listed, the excess /// values are ignored. /// /// If the tuple has fewer values than there are variables listed, the excess /// variables are initialized to nil. /// /// Examples /// /// RACTupleUnpack(NSString *string, NSNumber *num) = [RACTuple tupleWithObjects:@"foo", @5, nil]; /// NSLog(@"string: %@", string); /// NSLog(@"num: %@", num); /// /// /* The above is equivalent to: */ /// RACTuple *t = [RACTuple tupleWithObjects:@"foo", @5, nil]; /// NSString *string = t[0]; /// NSNumber *num = t[1]; /// NSLog(@"string: %@", string); /// NSLog(@"num: %@", num); #define RACTupleUnpack(...) \ RACTupleUnpack_(__VA_ARGS__) @class RACTwoTuple<__covariant First, __covariant Second>; @class RACThreeTuple<__covariant First, __covariant Second, __covariant Third>; @class RACFourTuple<__covariant First, __covariant Second, __covariant Third, __covariant Fourth>; @class RACFiveTuple<__covariant First, __covariant Second, __covariant Third, __covariant Fourth, __covariant Fifth>; NS_ASSUME_NONNULL_BEGIN /// A sentinel object that represents nils in the tuple. /// /// It should never be necessary to create a tuple nil yourself. Just use /// +tupleNil. @interface RACTupleNil : NSObject /// A singleton instance. + (RACTupleNil *)tupleNil; @end /// A tuple is an ordered collection of objects. It may contain nils, represented /// by RACTupleNil. @interface RACTuple : NSObject @property (nonatomic, readonly) NSUInteger count; /// These properties all return the object at that index or nil if the number of /// objects is less than the index. @property (nonatomic, readonly, nullable) id first; @property (nonatomic, readonly, nullable) id second; @property (nonatomic, readonly, nullable) id third; @property (nonatomic, readonly, nullable) id fourth; @property (nonatomic, readonly, nullable) id fifth; @property (nonatomic, readonly, nullable) id last; /// Creates a new tuple out of the array. Does not convert nulls to nils. + (instancetype)tupleWithObjectsFromArray:(NSArray *)array; /// Creates a new tuple out of the array. If `convert` is YES, it also converts /// every NSNull to RACTupleNil. + (instancetype)tupleWithObjectsFromArray:(NSArray *)array convertNullsToNils:(BOOL)convert; /// Creates a new tuple with the given objects. Use RACTupleNil to represent /// nils. + (instancetype)tupleWithObjects:(id)object, ... NS_REQUIRES_NIL_TERMINATION; /// Returns the object at `index` or nil if the object is a RACTupleNil. Unlike /// NSArray and friends, it's perfectly fine to ask for the object at an index /// past the tuple's count - 1. It will simply return nil. - (nullable id)objectAtIndex:(NSUInteger)index; /// Returns an array of all the objects. RACTupleNils are converted to NSNulls. - (NSArray *)allObjects; /// Appends `obj` to the receiver. /// /// obj - The object to add to the tuple. This argument may be nil. /// /// Returns a new tuple. - (__kindof RACTuple *)tupleByAddingObject:(nullable id)obj; @end @interface RACTuple (RACSequenceAdditions) /// Returns a sequence of all the objects. RACTupleNils are converted to NSNulls. @property (nonatomic, copy, readonly) RACSequence *rac_sequence; @end @interface RACTuple (ObjectSubscripting) /// Returns the object at that index or nil if the number of objects is less /// than the index. - (nullable id)objectAtIndexedSubscript:(NSUInteger)idx; @end /// A tuple with exactly one generic value. @interface RACOneTuple<__covariant First> : RACTuple + (instancetype)tupleWithObjects:(id)object, ... __attribute((unavailable("Use pack: instead."))); - (RACTwoTuple *)tupleByAddingObject:(nullable id)obj; /// Creates a new tuple with the given values. + (RACOneTuple *)pack:(nullable First)first; @property (nonatomic, readonly, nullable) First first; @end /// A tuple with exactly two generic values. @interface RACTwoTuple<__covariant First, __covariant Second> : RACTuple + (instancetype)tupleWithObjects:(id)object, ... __attribute((unavailable("Use pack:: instead."))); - (RACThreeTuple *)tupleByAddingObject:(nullable id)obj; /// Creates a new tuple with the given value. + (RACTwoTuple *)pack:(nullable First)first :(nullable Second)second; @property (nonatomic, readonly, nullable) First first; @property (nonatomic, readonly, nullable) Second second; @end /// A tuple with exactly three generic values. @interface RACThreeTuple<__covariant First, __covariant Second, __covariant Third> : RACTuple + (instancetype)tupleWithObjects:(id)object, ... __attribute((unavailable("Use pack::: instead."))); - (RACFourTuple *)tupleByAddingObject:(nullable id)obj; /// Creates a new tuple with the given values. + (instancetype)pack:(nullable First)first :(nullable Second)second :(nullable Third)third; @property (nonatomic, readonly, nullable) First first; @property (nonatomic, readonly, nullable) Second second; @property (nonatomic, readonly, nullable) Third third; @end /// A tuple with exactly four generic values. @interface RACFourTuple<__covariant First, __covariant Second, __covariant Third, __covariant Fourth> : RACTuple + (instancetype)tupleWithObjects:(id)object, ... __attribute((unavailable("Use pack:::: instead."))); - (RACFiveTuple *)tupleByAddingObject:(nullable id)obj; /// Creates a new tuple with the given values. + (instancetype)pack:(nullable First)first :(nullable Second)second :(nullable Third)third :(nullable Fourth)fourth; @property (nonatomic, readonly, nullable) First first; @property (nonatomic, readonly, nullable) Second second; @property (nonatomic, readonly, nullable) Third third; @property (nonatomic, readonly, nullable) Fourth fourth; @end /// A tuple with exactly five generic values. @interface RACFiveTuple<__covariant First, __covariant Second, __covariant Third, __covariant Fourth, __covariant Fifth> : RACTuple + (instancetype)tupleWithObjects:(id)object, ... __attribute((unavailable("Use pack::::: instead."))); /// Creates a new tuple with the given values. + (instancetype)pack:(nullable First)first :(nullable Second)second :(nullable Third)third :(nullable Fourth)fourth :(nullable Fifth)fifth; @property (nonatomic, readonly, nullable) First first; @property (nonatomic, readonly, nullable) Second second; @property (nonatomic, readonly, nullable) Third third; @property (nonatomic, readonly, nullable) Fourth fourth; @property (nonatomic, readonly, nullable) Fifth fifth; @end /// This and everything below is for internal use only. /// /// See RACTuplePack() and RACTupleUnpack() instead. #define RACTuplePack_(...) \ ([RACTuplePack_class_name(__VA_ARGS__) tupleWithObjectsFromArray:@[ metamacro_foreach(RACTuplePack_object_or_ractuplenil,, __VA_ARGS__) ]]) #define RACTuplePack_object_or_ractuplenil(INDEX, ARG) \ (ARG) ?: RACTupleNil.tupleNil, /// Returns the class that should be used to create a tuple with the provided /// variadic arguments to RACTuplePack_(). Supports up to 20 arguments. #define RACTuplePack_class_name(...) \ metamacro_at(20, __VA_ARGS__, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACTuple, RACFiveTuple, RACFourTuple, RACThreeTuple, RACTwoTuple, RACOneTuple) #define RACTupleUnpack_(...) \ metamacro_foreach(RACTupleUnpack_decl,, __VA_ARGS__) \ \ int RACTupleUnpack_state = 0; \ \ RACTupleUnpack_after: \ ; \ metamacro_foreach(RACTupleUnpack_assign,, __VA_ARGS__) \ if (RACTupleUnpack_state != 0) RACTupleUnpack_state = 2; \ \ while (RACTupleUnpack_state != 2) \ if (RACTupleUnpack_state == 1) { \ goto RACTupleUnpack_after; \ } else \ for (; RACTupleUnpack_state != 1; RACTupleUnpack_state = 1) \ [RACTupleUnpackingTrampoline trampoline][ @[ metamacro_foreach(RACTupleUnpack_value,, __VA_ARGS__) ] ] #define RACTupleUnpack_state metamacro_concat(RACTupleUnpack_state, __LINE__) #define RACTupleUnpack_after metamacro_concat(RACTupleUnpack_after, __LINE__) #define RACTupleUnpack_loop metamacro_concat(RACTupleUnpack_loop, __LINE__) #define RACTupleUnpack_decl_name(INDEX) \ metamacro_concat(metamacro_concat(RACTupleUnpack, __LINE__), metamacro_concat(_var, INDEX)) #define RACTupleUnpack_decl(INDEX, ARG) \ __strong id RACTupleUnpack_decl_name(INDEX); #define RACTupleUnpack_assign(INDEX, ARG) \ __strong ARG = RACTupleUnpack_decl_name(INDEX); #define RACTupleUnpack_value(INDEX, ARG) \ [NSValue valueWithPointer:&RACTupleUnpack_decl_name(INDEX)], @interface RACTupleUnpackingTrampoline : NSObject + (instancetype)trampoline; - (void)setObject:(nullable RACTuple *)tuple forKeyedSubscript:(NSArray *)variables; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACTuple.m ================================================ // // RACTuple.m // ReactiveObjC // // Created by Josh Abernathy on 4/12/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACTuple.h" #import #import "RACTupleSequence.h" @implementation RACTupleNil + (RACTupleNil *)tupleNil { static dispatch_once_t onceToken; static RACTupleNil *tupleNil = nil; dispatch_once(&onceToken, ^{ tupleNil = [[self alloc] init]; }); return tupleNil; } #pragma mark NSCopying - (id)copyWithZone:(NSZone *)zone { return self; } #pragma mark NSCoding - (instancetype)initWithCoder:(NSCoder *)coder { // Always return the singleton. return self.class.tupleNil; } - (void)encodeWithCoder:(NSCoder *)coder { } @end @interface RACTuple () - (instancetype)initWithBackingArray:(NSArray *)backingArray NS_DESIGNATED_INITIALIZER; @property (nonatomic, readonly) NSArray *backingArray; @end @implementation RACTuple - (instancetype)init { return [self initWithBackingArray:@[]]; } - (instancetype)initWithBackingArray:(NSArray *)backingArray { self = [super init]; _backingArray = [backingArray copy]; return self; } - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, self.allObjects]; } - (BOOL)isEqual:(RACTuple *)object { if (object == self) return YES; if (![object isKindOfClass:self.class]) return NO; return [self.backingArray isEqual:object.backingArray]; } - (NSUInteger)hash { return self.backingArray.hash; } #pragma mark NSFastEnumeration - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained [])buffer count:(NSUInteger)len { return [self.backingArray countByEnumeratingWithState:state objects:buffer count:len]; } #pragma mark NSCopying - (instancetype)copyWithZone:(NSZone *)zone { // we're immutable, bitches! return self; } #pragma mark NSCoding - (instancetype)initWithCoder:(NSCoder *)coder { self = [self init]; _backingArray = [coder decodeObjectForKey:@keypath(self.backingArray)]; return self; } - (void)encodeWithCoder:(NSCoder *)coder { if (self.backingArray != nil) [coder encodeObject:self.backingArray forKey:@keypath(self.backingArray)]; } #pragma mark API + (instancetype)tupleWithObjectsFromArray:(NSArray *)array { return [self tupleWithObjectsFromArray:array convertNullsToNils:NO]; } + (instancetype)tupleWithObjectsFromArray:(NSArray *)array convertNullsToNils:(BOOL)convert { if (!convert) { return [[self alloc] initWithBackingArray:array]; } NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:array.count]; for (id object in array) { [newArray addObject:(object == NSNull.null ? RACTupleNil.tupleNil : object)]; } return [[self alloc] initWithBackingArray:newArray]; } + (instancetype)tupleWithObjects:(id)object, ... { va_list args; va_start(args, object); NSUInteger count = 0; for (id currentObject = object; currentObject != nil; currentObject = va_arg(args, id)) { ++count; } va_end(args); if (count == 0) { return [[self alloc] init]; } NSMutableArray *objects = [[NSMutableArray alloc] initWithCapacity:count]; va_start(args, object); for (id currentObject = object; currentObject != nil; currentObject = va_arg(args, id)) { [objects addObject:currentObject]; } va_end(args); return [[self alloc] initWithBackingArray:objects]; } - (id)objectAtIndex:(NSUInteger)index { if (index >= self.count) return nil; id object = self.backingArray[index]; return (object == RACTupleNil.tupleNil ? nil : object); } - (NSArray *)allObjects { NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:self.backingArray.count]; for (id object in self.backingArray) { [newArray addObject:(object == RACTupleNil.tupleNil ? NSNull.null : object)]; } return newArray; } - (instancetype)tupleByAddingObject:(id)obj { NSArray *newArray = [self.backingArray arrayByAddingObject:obj ?: RACTupleNil.tupleNil]; return [self.class tupleWithObjectsFromArray:newArray]; } - (NSUInteger)count { return self.backingArray.count; } - (id)first { return self[0]; } - (id)second { return self[1]; } - (id)third { return self[2]; } - (id)fourth { return self[3]; } - (id)fifth { return self[4]; } - (id)last { return self[self.count - 1]; } @end @implementation RACTuple (RACSequenceAdditions) - (RACSequence *)rac_sequence { return [RACTupleSequence sequenceWithTupleBackingArray:self.backingArray offset:0]; } @end @implementation RACTuple (ObjectSubscripting) - (id)objectAtIndexedSubscript:(NSUInteger)idx { return [self objectAtIndex:idx]; } @end @implementation RACOneTuple - (instancetype)init { return [self initWithBackingArray:@[ RACTupleNil.tupleNil ]]; } - (instancetype)initWithBackingArray:(NSArray *)backingArray { NSParameterAssert(backingArray.count == 1); return [super initWithBackingArray:backingArray]; } - (RACTwoTuple *)tupleByAddingObject:(id)obj { NSArray *newArray = [self.backingArray arrayByAddingObject:obj ?: RACTupleNil.tupleNil]; return [RACTwoTuple tupleWithObjectsFromArray:newArray]; } + (instancetype)pack:(id)first { return [self tupleWithObjectsFromArray:@[ first ?: RACTupleNil.tupleNil, ]]; } - (BOOL)isEqual:(RACTuple *)object { if (object == self) return YES; // We consider a RACTuple with an identical backing array as equal. if (![object isKindOfClass:RACTuple.class]) return NO; return [self.backingArray isEqual:object.backingArray]; } @dynamic first; @end @implementation RACTwoTuple - (instancetype)init { return [self initWithBackingArray:@[ RACTupleNil.tupleNil, RACTupleNil.tupleNil ]]; } - (instancetype)initWithBackingArray:(NSArray *)backingArray { NSParameterAssert(backingArray.count == 2); return [super initWithBackingArray:backingArray]; } - (RACThreeTuple *)tupleByAddingObject:(id)obj { NSArray *newArray = [self.backingArray arrayByAddingObject:obj ?: RACTupleNil.tupleNil]; return [RACThreeTuple tupleWithObjectsFromArray:newArray]; } + (instancetype)pack:(id)first :(id)second { return [self tupleWithObjectsFromArray:@[ first ?: RACTupleNil.tupleNil, second ?: RACTupleNil.tupleNil, ]]; } - (BOOL)isEqual:(RACTuple *)object { if (object == self) return YES; // We consider a RACTuple with an identical backing array as equal. if (![object isKindOfClass:RACTuple.class]) return NO; return [self.backingArray isEqual:object.backingArray]; } @dynamic first; @dynamic second; @end @implementation RACThreeTuple - (instancetype)init { return [super initWithBackingArray:@[ RACTupleNil.tupleNil, RACTupleNil.tupleNil, RACTupleNil.tupleNil ]]; } - (instancetype)initWithBackingArray:(NSArray *)backingArray { NSParameterAssert(backingArray.count == 3); return [super initWithBackingArray:backingArray]; } - (RACFourTuple *)tupleByAddingObject:(id)obj { NSArray *newArray = [self.backingArray arrayByAddingObject:obj ?: RACTupleNil.tupleNil]; return [RACFourTuple tupleWithObjectsFromArray:newArray]; } + (instancetype)pack:(id)first :(id)second :(id)third { return [self tupleWithObjectsFromArray:@[ first ?: RACTupleNil.tupleNil, second ?: RACTupleNil.tupleNil, third ?: RACTupleNil.tupleNil, ]]; } - (BOOL)isEqual:(RACTuple *)object { if (object == self) return YES; // We consider a RACTuple with an identical backing array as equal. if (![object isKindOfClass:RACTuple.class]) return NO; return [self.backingArray isEqual:object.backingArray]; } @dynamic first; @dynamic second; @dynamic third; @end @implementation RACFourTuple - (instancetype)init { return [self initWithBackingArray:@[ RACTupleNil.tupleNil, RACTupleNil.tupleNil, RACTupleNil.tupleNil, RACTupleNil.tupleNil ]]; } - (instancetype)initWithBackingArray:(NSArray *)backingArray { NSParameterAssert(backingArray.count == 4); return [super initWithBackingArray:backingArray]; } - (RACFiveTuple *)tupleByAddingObject:(id)obj { NSArray *newArray = [self.backingArray arrayByAddingObject:obj ?: RACTupleNil.tupleNil]; return [RACFiveTuple tupleWithObjectsFromArray:newArray]; } + (instancetype)pack:(id)first :(id)second :(id)third :(id)fourth { return [self tupleWithObjectsFromArray:@[ first ?: RACTupleNil.tupleNil, second ?: RACTupleNil.tupleNil, third ?: RACTupleNil.tupleNil, fourth ?: RACTupleNil.tupleNil, ]]; } - (BOOL)isEqual:(RACTuple *)object { if (object == self) return YES; // We consider a RACTuple with an identical backing array as equal. if (![object isKindOfClass:RACTuple.class]) return NO; return [self.backingArray isEqual:object.backingArray]; } @dynamic first; @dynamic second; @dynamic third; @dynamic fourth; @end @implementation RACFiveTuple - (instancetype)init { return [self initWithBackingArray:@[ RACTupleNil.tupleNil, RACTupleNil.tupleNil, RACTupleNil.tupleNil, RACTupleNil.tupleNil, RACTupleNil.tupleNil ]]; } - (instancetype)initWithBackingArray:(NSArray *)backingArray { NSParameterAssert(backingArray.count == 5); return [super initWithBackingArray:backingArray]; } + (instancetype)pack:(id)first :(id)second :(id)third :(id)fourth :(id)fifth { return [self tupleWithObjectsFromArray:@[ first ?: RACTupleNil.tupleNil, second ?: RACTupleNil.tupleNil, third ?: RACTupleNil.tupleNil, fourth ?: RACTupleNil.tupleNil, fifth ?: RACTupleNil.tupleNil, ]]; } - (BOOL)isEqual:(RACTuple *)object { if (object == self) return YES; // We consider a RACTuple with an identical backing array as equal. if (![object isKindOfClass:RACTuple.class]) return NO; return [self.backingArray isEqual:object.backingArray]; } @dynamic first; @dynamic second; @dynamic third; @dynamic fourth; @dynamic fifth; @end @implementation RACTupleUnpackingTrampoline #pragma mark Lifecycle + (instancetype)trampoline { static dispatch_once_t onceToken; static id trampoline = nil; dispatch_once(&onceToken, ^{ trampoline = [[self alloc] init]; }); return trampoline; } - (void)setObject:(RACTuple *)tuple forKeyedSubscript:(NSArray *)variables { NSCParameterAssert(variables != nil); [variables enumerateObjectsUsingBlock:^(NSValue *value, NSUInteger index, BOOL *stop) { __strong id *ptr = (__strong id *)value.pointerValue; *ptr = tuple[index]; }]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACTupleSequence.h ================================================ // // RACTupleSequence.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-05-01. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSequence.h" // Private class that adapts a RACTuple to the RACSequence interface. @interface RACTupleSequence : RACSequence // Returns a sequence for enumerating over the given backing array (from a // RACTuple), starting from the given offset. + (RACSequence *)sequenceWithTupleBackingArray:(NSArray *)backingArray offset:(NSUInteger)offset; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACTupleSequence.m ================================================ // // RACTupleSequence.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-05-01. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACTupleSequence.h" #import "RACTuple.h" @interface RACTupleSequence () // The array being sequenced, as taken from RACTuple.backingArray. @property (nonatomic, strong, readonly) NSArray *tupleBackingArray; // The index in the array from which the sequence starts. @property (nonatomic, assign, readonly) NSUInteger offset; @end @implementation RACTupleSequence #pragma mark Lifecycle + (RACSequence *)sequenceWithTupleBackingArray:(NSArray *)backingArray offset:(NSUInteger)offset { NSCParameterAssert(offset <= backingArray.count); if (offset == backingArray.count) return self.empty; RACTupleSequence *seq = [[self alloc] init]; seq->_tupleBackingArray = backingArray; seq->_offset = offset; return seq; } #pragma mark RACSequence - (id)head { id object = self.tupleBackingArray[self.offset]; return (object == RACTupleNil.tupleNil ? NSNull.null : object); } - (RACSequence *)tail { RACSequence *sequence = [self.class sequenceWithTupleBackingArray:self.tupleBackingArray offset:self.offset + 1]; sequence.name = self.name; return sequence; } - (NSArray *)array { NSRange range = NSMakeRange(self.offset, self.tupleBackingArray.count - self.offset); NSMutableArray *array = [[NSMutableArray alloc] initWithCapacity:range.length]; [self.tupleBackingArray enumerateObjectsAtIndexes:[NSIndexSet indexSetWithIndexesInRange:range] options:0 usingBlock:^(id object, NSUInteger index, BOOL *stop) { id mappedObject = (object == RACTupleNil.tupleNil ? NSNull.null : object); [array addObject:mappedObject]; }]; return array; } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p>{ name = %@, tuple = %@ }", self.class, self, self.name, self.tupleBackingArray]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACUnarySequence.h ================================================ // // RACUnarySequence.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-05-01. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACSequence.h" // Private class representing a sequence of exactly one value. @interface RACUnarySequence : RACSequence + (RACUnarySequence *)return:(id)value; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACUnarySequence.m ================================================ // // RACUnarySequence.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-05-01. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "RACUnarySequence.h" #import #import "NSObject+RACDescription.h" @interface RACUnarySequence () // The single value stored in this sequence. @property (nonatomic, strong, readwrite) id head; @end @implementation RACUnarySequence #pragma mark Properties @synthesize head = _head; #pragma mark Lifecycle + (RACUnarySequence *)return:(id)value { RACUnarySequence *sequence = [[self alloc] init]; sequence.head = value; return [sequence setNameWithFormat:@"+return: %@", RACDescription(value)]; } #pragma mark RACSequence - (RACSequence *)tail { return nil; } - (RACSequence *)bind:(RACSequenceBindBlock (^)(void))block { RACStreamBindBlock bindBlock = block(); BOOL stop = NO; RACSequence *result = (id)[bindBlock(self.head, &stop) setNameWithFormat:@"[%@] -bind:", self.name]; return result ?: self.class.empty; } #pragma mark NSCoding - (Class)classForCoder { // Unary sequences should be encoded as themselves, not array sequences. return self.class; } - (instancetype)initWithCoder:(NSCoder *)coder { id value = [coder decodeObjectForKey:@keypath(self.head)]; return [self.class return:value]; } - (void)encodeWithCoder:(NSCoder *)coder { if (self.head != nil) [coder encodeObject:self.head forKey:@keypath(self.head)]; } #pragma mark NSObject - (NSString *)description { return [NSString stringWithFormat:@"<%@: %p>{ name = %@, head = %@ }", self.class, self, self.name, self.head]; } - (NSUInteger)hash { return [self.head hash]; } - (BOOL)isEqual:(RACUnarySequence *)seq { if (self == seq) return YES; if (![seq isKindOfClass:RACUnarySequence.class]) return NO; return self.head == seq.head || [(NSObject *)self.head isEqual:seq.head]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACUnit.h ================================================ // // RACUnit.h // ReactiveObjC // // Created by Josh Abernathy on 3/27/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import NS_ASSUME_NONNULL_BEGIN /// A unit represents an empty value. /// /// It should never be necessary to create a unit yourself. Just use +defaultUnit. @interface RACUnit : NSObject /// A singleton instance. + (RACUnit *)defaultUnit; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACUnit.m ================================================ // // RACUnit.m // ReactiveObjC // // Created by Josh Abernathy on 3/27/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACUnit.h" @implementation RACUnit #pragma mark API + (RACUnit *)defaultUnit { static dispatch_once_t onceToken; static RACUnit *defaultUnit = nil; dispatch_once(&onceToken, ^{ defaultUnit = [[self alloc] init]; }); return defaultUnit; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACValueTransformer.h ================================================ // // RACValueTransformer.h // ReactiveObjC // // Created by Josh Abernathy on 3/6/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import // A private block based transformer. @interface RACValueTransformer : NSValueTransformer + (instancetype)transformerWithBlock:(id (^)(id value))block; @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/RACValueTransformer.m ================================================ // // RACValueTransformer.m // ReactiveObjC // // Created by Josh Abernathy on 3/6/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "RACValueTransformer.h" @interface RACValueTransformer () @property (nonatomic, copy) id (^transformBlock)(id value); @end @implementation RACValueTransformer #pragma mark NSValueTransformer + (BOOL)allowsReverseTransformation { return NO; } - (id)transformedValue:(id)value { return self.transformBlock(value); } #pragma mark API @synthesize transformBlock; + (instancetype)transformerWithBlock:(id (^)(id value))block { NSCParameterAssert(block != NULL); RACValueTransformer *transformer = [[self alloc] init]; transformer.transformBlock = block; return transformer; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/ReactiveObjC.h ================================================ // // ReactiveObjC.h // ReactiveObjC // // Created by Josh Abernathy on 3/5/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import //! Project version number for ReactiveObjC. FOUNDATION_EXPORT double ReactiveObjCVersionNumber; //! Project version string for ReactiveObjC. FOUNDATION_EXPORT const unsigned char ReactiveObjCVersionString[]; #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #if TARGET_OS_WATCH #elif TARGET_OS_IOS || TARGET_OS_TV #import #import #import #import #import #import #import #import #import #import #if TARGET_OS_IOS #import #import #import #import #import #import #import #import #import #import #endif #elif TARGET_OS_MAC #import #import #import #import #import #endif ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIActionSheet+RACSignalSupport.h ================================================ // // UIActionSheet+RACSignalSupport.h // ReactiveObjC // // Created by Dave Lee on 2013-06-22. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACDelegateProxy; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UIActionSheet (RACSignalSupport) /// A delegate proxy which will be set as the receiver's delegate when any of the /// methods in this category are used. @property (nonatomic, strong, readonly) RACDelegateProxy *rac_delegateProxy; /// Creates a signal for button clicks on the receiver. /// /// When this method is invoked, the `rac_delegateProxy` will become the /// receiver's delegate. Any previous delegate will become the -[RACDelegateProxy /// rac_proxiedDelegate], so that it receives any messages that the proxy doesn't /// know how to handle. Setting the receiver's `delegate` afterward is /// considered undefined behavior. /// /// Returns a signal which will send the index of the specific button clicked. /// The signal will complete when the receiver is deallocated. - (RACSignal *)rac_buttonClickedSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIActionSheet+RACSignalSupport.m ================================================ // // UIActionSheet+RACSignalSupport.m // ReactiveObjC // // Created by Dave Lee on 2013-06-22. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UIActionSheet+RACSignalSupport.h" #import "RACDelegateProxy.h" #import "RACSignal+Operations.h" #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import @implementation UIActionSheet (RACSignalSupport) static void RACUseDelegateProxy(UIActionSheet *self) { if (self.delegate == self.rac_delegateProxy) return; self.rac_delegateProxy.rac_proxiedDelegate = self.delegate; self.delegate = (id)self.rac_delegateProxy; } - (RACDelegateProxy *)rac_delegateProxy { RACDelegateProxy *proxy = objc_getAssociatedObject(self, _cmd); if (proxy == nil) { proxy = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UIActionSheetDelegate)]; objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return proxy; } - (RACSignal *)rac_buttonClickedSignal { RACSignal *signal = [[[[self.rac_delegateProxy signalForSelector:@selector(actionSheet:clickedButtonAtIndex:)] reduceEach:^(UIActionSheet *actionSheet, NSNumber *buttonIndex) { return buttonIndex; }] takeUntil:self.rac_willDeallocSignal] setNameWithFormat:@"%@ -rac_buttonClickedSignal", RACDescription(self)]; RACUseDelegateProxy(self); return signal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIAlertView+RACSignalSupport.h ================================================ // // UIAlertView+RACSignalSupport.h // ReactiveObjC // // Created by Henrik Hodne on 6/16/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACDelegateProxy; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UIAlertView (RACSignalSupport) /// A delegate proxy which will be set as the receiver's delegate when any of the /// methods in this category are used. @property (nonatomic, strong, readonly) RACDelegateProxy *rac_delegateProxy; /// Creates a signal for button clicks on the receiver. /// /// When this method is invoked, the `rac_delegateProxy` will become the /// receiver's delegate. Any previous delegate will become the -[RACDelegateProxy /// rac_proxiedDelegate], so that it receives any messages that the proxy doesn't /// know how to handle. Setting the receiver's `delegate` afterward is considered /// undefined behavior. /// /// Note that this signal will not send a value when the alert is dismissed /// programatically. /// /// Returns a signal which will send the index of the specific button clicked. /// The signal will complete itself when the receiver is deallocated. - (RACSignal *)rac_buttonClickedSignal; /// Creates a signal for dismissal of the receiver. /// /// When this method is invoked, the `rac_delegateProxy` will become the /// receiver's delegate. Any previous delegate will become the -[RACDelegateProxy /// rac_proxiedDelegate], so that it receives any messages that the proxy doesn't /// know how to handle. Setting the receiver's `delegate` afterward is considered /// undefined behavior. /// /// Returns a signal which will send the index of the button associated with the /// dismissal. The signal will complete itself when the receiver is deallocated. - (RACSignal *)rac_willDismissSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIAlertView+RACSignalSupport.m ================================================ // // UIAlertView+RACSignalSupport.m // ReactiveObjC // // Created by Henrik Hodne on 6/16/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UIAlertView+RACSignalSupport.h" #import "RACDelegateProxy.h" #import "RACSignal+Operations.h" #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import @implementation UIAlertView (RACSignalSupport) static void RACUseDelegateProxy(UIAlertView *self) { if (self.delegate == self.rac_delegateProxy) return; self.rac_delegateProxy.rac_proxiedDelegate = self.delegate; self.delegate = (id)self.rac_delegateProxy; } - (RACDelegateProxy *)rac_delegateProxy { RACDelegateProxy *proxy = objc_getAssociatedObject(self, _cmd); if (proxy == nil) { proxy = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UIAlertViewDelegate)]; objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return proxy; } - (RACSignal *)rac_buttonClickedSignal { RACSignal *signal = [[[[self.rac_delegateProxy signalForSelector:@selector(alertView:clickedButtonAtIndex:)] reduceEach:^(UIAlertView *alertView, NSNumber *buttonIndex) { return buttonIndex; }] takeUntil:self.rac_willDeallocSignal] setNameWithFormat:@"%@ -rac_buttonClickedSignal", RACDescription(self)]; RACUseDelegateProxy(self); return signal; } - (RACSignal *)rac_willDismissSignal { RACSignal *signal = [[[[self.rac_delegateProxy signalForSelector:@selector(alertView:willDismissWithButtonIndex:)] reduceEach:^(UIAlertView *alertView, NSNumber *buttonIndex) { return buttonIndex; }] takeUntil:self.rac_willDeallocSignal] setNameWithFormat:@"%@ -rac_willDismissSignal", RACDescription(self)]; RACUseDelegateProxy(self); return signal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIBarButtonItem+RACCommandSupport.h ================================================ // // UIBarButtonItem+RACCommandSupport.h // ReactiveObjC // // Created by Kyle LeNeau on 3/27/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACCommand<__contravariant InputType, __covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UIBarButtonItem (RACCommandSupport) /// Sets the control's command. When the control is clicked, the command is /// executed with the sender of the event. The control's enabledness is bound /// to the command's `canExecute`. /// /// Note: this will reset the control's target and action. @property (nonatomic, strong, nullable) RACCommand<__kindof UIBarButtonItem *, id> *rac_command; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIBarButtonItem+RACCommandSupport.m ================================================ // // UIBarButtonItem+RACCommandSupport.m // ReactiveObjC // // Created by Kyle LeNeau on 3/27/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UIBarButtonItem+RACCommandSupport.h" #import #import "RACCommand.h" #import "RACDisposable.h" #import "RACSignal+Operations.h" #import static void *UIControlRACCommandKey = &UIControlRACCommandKey; static void *UIControlEnabledDisposableKey = &UIControlEnabledDisposableKey; @implementation UIBarButtonItem (RACCommandSupport) - (RACCommand *)rac_command { return objc_getAssociatedObject(self, UIControlRACCommandKey); } - (void)setRac_command:(RACCommand *)command { objc_setAssociatedObject(self, UIControlRACCommandKey, command, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // Check for stored signal in order to remove it and add a new one RACDisposable *disposable = objc_getAssociatedObject(self, UIControlEnabledDisposableKey); [disposable dispose]; if (command == nil) return; disposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self]; objc_setAssociatedObject(self, UIControlEnabledDisposableKey, disposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self rac_hijackActionAndTargetIfNeeded]; } - (void)rac_hijackActionAndTargetIfNeeded { SEL hijackSelector = @selector(rac_commandPerformAction:); if (self.target == self && self.action == hijackSelector) return; if (self.target != nil) NSLog(@"WARNING: UIBarButtonItem.rac_command hijacks the control's existing target and action."); self.target = self; self.action = hijackSelector; } - (void)rac_commandPerformAction:(id)sender { [self.rac_command execute:sender]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIButton+RACCommandSupport.h ================================================ // // UIButton+RACCommandSupport.h // ReactiveObjC // // Created by Ash Furrow on 2013-06-06. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACCommand<__contravariant InputType, __covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UIButton (RACCommandSupport) /// Sets the button's command. When the button is clicked, the command is /// executed with the sender of the event. The button's enabledness is bound /// to the command's `canExecute`. @property (nonatomic, strong, nullable) RACCommand<__kindof UIButton *, id> *rac_command; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIButton+RACCommandSupport.m ================================================ // // UIButton+RACCommandSupport.m // ReactiveObjC // // Created by Ash Furrow on 2013-06-06. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UIButton+RACCommandSupport.h" #import #import "RACCommand.h" #import "RACDisposable.h" #import "RACSignal+Operations.h" #import static void *UIButtonRACCommandKey = &UIButtonRACCommandKey; static void *UIButtonEnabledDisposableKey = &UIButtonEnabledDisposableKey; @implementation UIButton (RACCommandSupport) - (RACCommand *)rac_command { return objc_getAssociatedObject(self, UIButtonRACCommandKey); } - (void)setRac_command:(RACCommand *)command { objc_setAssociatedObject(self, UIButtonRACCommandKey, command, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // Check for stored signal in order to remove it and add a new one RACDisposable *disposable = objc_getAssociatedObject(self, UIButtonEnabledDisposableKey); [disposable dispose]; if (command == nil) return; disposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self]; objc_setAssociatedObject(self, UIButtonEnabledDisposableKey, disposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC); [self rac_hijackActionAndTargetIfNeeded]; } - (void)rac_hijackActionAndTargetIfNeeded { SEL hijackSelector = @selector(rac_commandPerformAction:); for (NSString *selector in [self actionsForTarget:self forControlEvent:UIControlEventTouchUpInside]) { if (hijackSelector == NSSelectorFromString(selector)) { return; } } [self addTarget:self action:hijackSelector forControlEvents:UIControlEventTouchUpInside]; } - (void)rac_commandPerformAction:(id)sender { [self.rac_command execute:sender]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UICollectionReusableView+RACSignalSupport.h ================================================ // // UICollectionReusableView+RACSignalSupport.h // ReactiveObjC // // Created by Kent Wong on 2013-10-04. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACSignal<__covariant ValueType>; @class RACUnit; NS_ASSUME_NONNULL_BEGIN // This category is only applicable to iOS >= 6.0. @interface UICollectionReusableView (RACSignalSupport) /// A signal which will send a RACUnit whenever -prepareForReuse is invoked upon /// the receiver. /// /// Examples /// /// [[[self.cancelButton /// rac_signalForControlEvents:UIControlEventTouchUpInside] /// takeUntil:self.rac_prepareForReuseSignal] /// subscribeNext:^(UIButton *x) { /// // do other things /// }]; @property (nonatomic, strong, readonly) RACSignal *rac_prepareForReuseSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UICollectionReusableView+RACSignalSupport.m ================================================ // // UICollectionReusableView+RACSignalSupport.m // ReactiveObjC // // Created by Kent Wong on 2013-10-04. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UICollectionReusableView+RACSignalSupport.h" #import "NSObject+RACDescription.h" #import "NSObject+RACSelectorSignal.h" #import "RACSignal+Operations.h" #import "RACUnit.h" #import @implementation UICollectionReusableView (RACSignalSupport) - (RACSignal *)rac_prepareForReuseSignal { RACSignal *signal = objc_getAssociatedObject(self, _cmd); if (signal != nil) return signal; signal = [[[self rac_signalForSelector:@selector(prepareForReuse)] mapReplace:RACUnit.defaultUnit] setNameWithFormat:@"%@ -rac_prepareForReuseSignal", RACDescription(self)]; objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return signal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIControl+RACSignalSupport.h ================================================ // // UIControl+RACSignalSupport.h // ReactiveObjC // // Created by Josh Abernathy on 4/17/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UIControl (RACSignalSupport) /// Creates and returns a signal that sends the sender of the control event /// whenever one of the control events is triggered. - (RACSignal<__kindof UIControl *> *)rac_signalForControlEvents:(UIControlEvents)controlEvents; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIControl+RACSignalSupport.m ================================================ // // UIControl+RACSignalSupport.m // ReactiveObjC // // Created by Josh Abernathy on 4/17/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "UIControl+RACSignalSupport.h" #import #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACSignal.h" #import "RACSubscriber.h" #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" @implementation UIControl (RACSignalSupport) - (RACSignal *)rac_signalForControlEvents:(UIControlEvents)controlEvents { @weakify(self); return [[RACSignal createSignal:^(id subscriber) { @strongify(self); [self addTarget:subscriber action:@selector(sendNext:) forControlEvents:controlEvents]; RACDisposable *disposable = [RACDisposable disposableWithBlock:^{ [subscriber sendCompleted]; }]; [self.rac_deallocDisposable addDisposable:disposable]; return [RACDisposable disposableWithBlock:^{ @strongify(self); [self.rac_deallocDisposable removeDisposable:disposable]; [self removeTarget:subscriber action:@selector(sendNext:) forControlEvents:controlEvents]; }]; }] setNameWithFormat:@"%@ -rac_signalForControlEvents: %lx", RACDescription(self), (unsigned long)controlEvents]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIControl+RACSignalSupportPrivate.h ================================================ // // UIControl+RACSignalSupportPrivate.h // ReactiveObjC // // Created by Uri Baghin on 06/08/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACChannelTerminal; NS_ASSUME_NONNULL_BEGIN @interface UIControl (RACSignalSupportPrivate) /// Adds a RACChannel-based interface to the receiver for the given /// UIControlEvents and exposes it. /// /// controlEvents - A mask of UIControlEvents on which to send new values. /// key - The key whose value should be read and set when a control /// event fires and when a value is sent to the /// RACChannelTerminal respectively. /// nilValue - The value to be assigned to the key when `nil` is sent to the /// RACChannelTerminal. This value can itself be nil. /// /// Returns a RACChannelTerminal which will send future values from the receiver, /// and update the receiver when values are sent to the terminal. - (RACChannelTerminal *)rac_channelForControlEvents:(UIControlEvents)controlEvents key:(NSString *)key nilValue:(nullable id)nilValue; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIControl+RACSignalSupportPrivate.m ================================================ // // UIControl+RACSignalSupportPrivate.m // ReactiveObjC // // Created by Uri Baghin on 06/08/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UIControl+RACSignalSupportPrivate.h" #import "NSObject+RACDeallocating.h" #import "NSObject+RACLifting.h" #import "RACChannel.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACSignal+Operations.h" #import "UIControl+RACSignalSupport.h" @implementation UIControl (RACSignalSupportPrivate) - (RACChannelTerminal *)rac_channelForControlEvents:(UIControlEvents)controlEvents key:(NSString *)key nilValue:(id)nilValue { NSCParameterAssert(key.length > 0); key = [key copy]; RACChannel *channel = [[RACChannel alloc] init]; [self.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ [channel.followingTerminal sendCompleted]; }]]; RACSignal *eventSignal = [[[self rac_signalForControlEvents:controlEvents] mapReplace:key] takeUntil:[[channel.followingTerminal ignoreValues] catchTo:RACSignal.empty]]; [[self rac_liftSelector:@selector(valueForKey:) withSignals:eventSignal, nil] subscribe:channel.followingTerminal]; RACSignal *valuesSignal = [channel.followingTerminal map:^(id value) { return value ?: nilValue; }]; [self rac_liftSelector:@selector(setValue:forKey:) withSignals:valuesSignal, [RACSignal return:key], nil]; return channel.leadingTerminal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIDatePicker+RACSignalSupport.h ================================================ // // UIDatePicker+RACSignalSupport.h // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACChannelTerminal; NS_ASSUME_NONNULL_BEGIN @interface UIDatePicker (RACSignalSupport) /// Creates a new RACChannel-based binding to the receiver. /// /// nilValue - The date to set when the terminal receives `nil`. /// /// Returns a RACChannelTerminal that sends the receiver's date whenever the /// UIControlEventValueChanged control event is fired, and sets the date to the /// values it receives. - (RACChannelTerminal *)rac_newDateChannelWithNilValue:(nullable NSDate *)nilValue; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIDatePicker+RACSignalSupport.m ================================================ // // UIDatePicker+RACSignalSupport.m // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UIDatePicker+RACSignalSupport.h" #import #import "UIControl+RACSignalSupportPrivate.h" @implementation UIDatePicker (RACSignalSupport) - (RACChannelTerminal *)rac_newDateChannelWithNilValue:(NSDate *)nilValue { return [self rac_channelForControlEvents:UIControlEventValueChanged key:@keypath(self.date) nilValue:nilValue]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIGestureRecognizer+RACSignalSupport.h ================================================ // // UIGestureRecognizer+RACSignalSupport.h // ReactiveObjC // // Created by Josh Vera on 5/5/13. // Copyright (c) 2013 GitHub. All rights reserved. // #import @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UIGestureRecognizer (RACSignalSupport) /// Returns a signal that sends the receiver when its gesture occurs. - (RACSignal<__kindof UIGestureRecognizer *> *)rac_gestureSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIGestureRecognizer+RACSignalSupport.m ================================================ // // UIGestureRecognizer+RACSignalSupport.m // ReactiveObjC // // Created by Josh Vera on 5/5/13. // Copyright (c) 2013 GitHub. All rights reserved. // #import "UIGestureRecognizer+RACSignalSupport.h" #import #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACSignal.h" #import "RACSubscriber.h" @implementation UIGestureRecognizer (RACSignalSupport) - (RACSignal *)rac_gestureSignal { @weakify(self); return [[RACSignal createSignal:^(id subscriber) { @strongify(self); [self addTarget:subscriber action:@selector(sendNext:)]; [self.rac_deallocDisposable addDisposable:[RACDisposable disposableWithBlock:^{ [subscriber sendCompleted]; }]]; return [RACDisposable disposableWithBlock:^{ @strongify(self); [self removeTarget:subscriber action:@selector(sendNext:)]; }]; }] setNameWithFormat:@"%@ -rac_gestureSignal", RACDescription(self)]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIImagePickerController+RACSignalSupport.h ================================================ // // UIImagePickerController+RACSignalSupport.h // ReactiveObjC // // Created by Timur Kuchkarov on 28.03.14. // Copyright (c) 2014 GitHub. All rights reserved. // #import @class RACDelegateProxy; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UIImagePickerController (RACSignalSupport) /// A delegate proxy which will be set as the receiver's delegate when any of the /// methods in this category are used. @property (nonatomic, strong, readonly) RACDelegateProxy *rac_delegateProxy; /// Creates a signal for every new selected image. /// /// When this method is invoked, the `rac_delegateProxy` will become the /// receiver's delegate. Any previous delegate will become the -[RACDelegateProxy /// rac_proxiedDelegate], so that it receives any messages that the proxy doesn't /// know how to handle. Setting the receiver's `delegate` afterward is considered /// undefined behavior. /// /// Returns a signal which will send the dictionary with info for the selected image. /// Caller is responsible for picker controller dismissal. The signal will complete /// itself when the receiver is deallocated or when user cancels selection. - (RACSignal *)rac_imageSelectedSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIImagePickerController+RACSignalSupport.m ================================================ // // UIImagePickerController+RACSignalSupport.m // ReactiveObjC // // Created by Timur Kuchkarov on 28.03.14. // Copyright (c) 2014 GitHub. All rights reserved. // #import "UIImagePickerController+RACSignalSupport.h" #import "RACDelegateProxy.h" #import "RACSignal+Operations.h" #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import @implementation UIImagePickerController (RACSignalSupport) static void RACUseDelegateProxy(UIImagePickerController *self) { if (self.delegate == self.rac_delegateProxy) return; self.rac_delegateProxy.rac_proxiedDelegate = self.delegate; self.delegate = (id)self.rac_delegateProxy; } - (RACDelegateProxy *)rac_delegateProxy { RACDelegateProxy *proxy = objc_getAssociatedObject(self, _cmd); if (proxy == nil) { proxy = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UIImagePickerControllerDelegate)]; objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return proxy; } - (RACSignal *)rac_imageSelectedSignal { RACSignal *pickerCancelledSignal = [[self.rac_delegateProxy signalForSelector:@selector(imagePickerControllerDidCancel:)] merge:self.rac_willDeallocSignal]; RACSignal *imagePickerSignal = [[[[self.rac_delegateProxy signalForSelector:@selector(imagePickerController:didFinishPickingMediaWithInfo:)] reduceEach:^(UIImagePickerController *pickerController, NSDictionary *userInfo) { return userInfo; }] takeUntil:pickerCancelledSignal] setNameWithFormat:@"%@ -rac_imageSelectedSignal", RACDescription(self)]; RACUseDelegateProxy(self); return imagePickerSignal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIRefreshControl+RACCommandSupport.h ================================================ // // UIRefreshControl+RACCommandSupport.h // ReactiveObjC // // Created by Dave Lee on 2013-10-17. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACCommand<__contravariant InputType, __covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UIRefreshControl (RACCommandSupport) /// Manipulate the RACCommand property associated with this refresh control. /// /// When this refresh control is activated by the user, the command will be /// executed. Upon completion or error of the execution signal, -endRefreshing /// will be invoked. @property (nonatomic, strong, nullable) RACCommand<__kindof UIRefreshControl *, id> *rac_command; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIRefreshControl+RACCommandSupport.m ================================================ // // UIRefreshControl+RACCommandSupport.m // ReactiveObjC // // Created by Dave Lee on 2013-10-17. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UIRefreshControl+RACCommandSupport.h" #import #import "RACCommand.h" #import "RACCompoundDisposable.h" #import "RACDisposable.h" #import "RACSignal.h" #import "RACSignal+Operations.h" #import "UIControl+RACSignalSupport.h" #import static void *UIRefreshControlRACCommandKey = &UIRefreshControlRACCommandKey; static void *UIRefreshControlDisposableKey = &UIRefreshControlDisposableKey; @implementation UIRefreshControl (RACCommandSupport) - (RACCommand *)rac_command { return objc_getAssociatedObject(self, UIRefreshControlRACCommandKey); } - (void)setRac_command:(RACCommand *)command { objc_setAssociatedObject(self, UIRefreshControlRACCommandKey, command, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // Dispose of any active command associations. [objc_getAssociatedObject(self, UIRefreshControlDisposableKey) dispose]; if (command == nil) return; // Like RAC(self, enabled) = command.enabled; but with access to disposable. RACDisposable *enabledDisposable = [command.enabled setKeyPath:@keypath(self.enabled) onObject:self]; RACDisposable *executionDisposable = [[[[[self rac_signalForControlEvents:UIControlEventValueChanged] map:^(UIRefreshControl *x) { return [[[command execute:x] catchTo:[RACSignal empty]] then:^{ return [RACSignal return:x]; }]; }] concat] deliverOnMainThread] subscribeNext:^(UIRefreshControl *x) { [x endRefreshing]; }]; RACDisposable *commandDisposable = [RACCompoundDisposable compoundDisposableWithDisposables:@[ enabledDisposable, executionDisposable ]]; objc_setAssociatedObject(self, UIRefreshControlDisposableKey, commandDisposable, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UISegmentedControl+RACSignalSupport.h ================================================ // // UISegmentedControl+RACSignalSupport.h // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACChannelTerminal; NS_ASSUME_NONNULL_BEGIN @interface UISegmentedControl (RACSignalSupport) /// Creates a new RACChannel-based binding to the receiver. /// /// nilValue - The segment to select when the terminal receives `nil`. /// /// Returns a RACChannelTerminal that sends the receiver's currently selected /// segment's index whenever the UIControlEventValueChanged control event is /// fired, and sets the selected segment index to the values it receives. - (RACChannelTerminal *)rac_newSelectedSegmentIndexChannelWithNilValue:(nullable NSNumber *)nilValue; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UISegmentedControl+RACSignalSupport.m ================================================ // // UISegmentedControl+RACSignalSupport.m // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UISegmentedControl+RACSignalSupport.h" #import #import "UIControl+RACSignalSupportPrivate.h" @implementation UISegmentedControl (RACSignalSupport) - (RACChannelTerminal *)rac_newSelectedSegmentIndexChannelWithNilValue:(NSNumber *)nilValue { return [self rac_channelForControlEvents:UIControlEventValueChanged key:@keypath(self.selectedSegmentIndex) nilValue:nilValue]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UISlider+RACSignalSupport.h ================================================ // // UISlider+RACSignalSupport.h // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACChannelTerminal; NS_ASSUME_NONNULL_BEGIN @interface UISlider (RACSignalSupport) /// Creates a new RACChannel-based binding to the receiver. /// /// nilValue - The value to set when the terminal receives `nil`. /// /// Returns a RACChannelTerminal that sends the receiver's value whenever the /// UIControlEventValueChanged control event is fired, and sets the value to the /// values it receives. - (RACChannelTerminal *)rac_newValueChannelWithNilValue:(nullable NSNumber *)nilValue; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UISlider+RACSignalSupport.m ================================================ // // UISlider+RACSignalSupport.m // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UISlider+RACSignalSupport.h" #import #import "UIControl+RACSignalSupportPrivate.h" @implementation UISlider (RACSignalSupport) - (RACChannelTerminal *)rac_newValueChannelWithNilValue:(NSNumber *)nilValue { return [self rac_channelForControlEvents:UIControlEventValueChanged key:@keypath(self.value) nilValue:nilValue]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIStepper+RACSignalSupport.h ================================================ // // UIStepper+RACSignalSupport.h // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACChannelTerminal; NS_ASSUME_NONNULL_BEGIN @interface UIStepper (RACSignalSupport) /// Creates a new RACChannel-based binding to the receiver. /// /// nilValue - The value to set when the terminal receives `nil`. /// /// Returns a RACChannelTerminal that sends the receiver's value whenever the /// UIControlEventValueChanged control event is fired, and sets the value to the /// values it receives. - (RACChannelTerminal *)rac_newValueChannelWithNilValue:(nullable NSNumber *)nilValue; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UIStepper+RACSignalSupport.m ================================================ // // UIStepper+RACSignalSupport.m // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UIStepper+RACSignalSupport.h" #import #import "UIControl+RACSignalSupportPrivate.h" @implementation UIStepper (RACSignalSupport) - (RACChannelTerminal *)rac_newValueChannelWithNilValue:(NSNumber *)nilValue { return [self rac_channelForControlEvents:UIControlEventValueChanged key:@keypath(self.value) nilValue:nilValue]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UISwitch+RACSignalSupport.h ================================================ // // UISwitch+RACSignalSupport.h // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACChannelTerminal; NS_ASSUME_NONNULL_BEGIN @interface UISwitch (RACSignalSupport) /// Creates a new RACChannel-based binding to the receiver. /// /// Returns a RACChannelTerminal that sends whether the receiver is on whenever /// the UIControlEventValueChanged control event is fired, and sets it on or off /// when it receives @YES or @NO respectively. - (RACChannelTerminal *)rac_newOnChannel; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UISwitch+RACSignalSupport.m ================================================ // // UISwitch+RACSignalSupport.m // ReactiveObjC // // Created by Uri Baghin on 20/07/2013. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UISwitch+RACSignalSupport.h" #import #import "UIControl+RACSignalSupportPrivate.h" @implementation UISwitch (RACSignalSupport) - (RACChannelTerminal *)rac_newOnChannel { return [self rac_channelForControlEvents:UIControlEventValueChanged key:@keypath(self.on) nilValue:@NO]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UITableViewCell+RACSignalSupport.h ================================================ // // UITableViewCell+RACSignalSupport.h // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-07-22. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACSignal<__covariant ValueType>; @class RACUnit; NS_ASSUME_NONNULL_BEGIN @interface UITableViewCell (RACSignalSupport) /// A signal which will send a RACUnit whenever -prepareForReuse is invoked upon /// the receiver. /// /// Examples /// /// [[[self.cancelButton /// rac_signalForControlEvents:UIControlEventTouchUpInside] /// takeUntil:self.rac_prepareForReuseSignal] /// subscribeNext:^(UIButton *x) { /// // do other things /// }]; @property (nonatomic, strong, readonly) RACSignal *rac_prepareForReuseSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UITableViewCell+RACSignalSupport.m ================================================ // // UITableViewCell+RACSignalSupport.m // ReactiveObjC // // Created by Justin Spahr-Summers on 2013-07-22. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UITableViewCell+RACSignalSupport.h" #import "NSObject+RACDescription.h" #import "NSObject+RACSelectorSignal.h" #import "RACSignal+Operations.h" #import "RACUnit.h" #import @implementation UITableViewCell (RACSignalSupport) - (RACSignal *)rac_prepareForReuseSignal { RACSignal *signal = objc_getAssociatedObject(self, _cmd); if (signal != nil) return signal; signal = [[[self rac_signalForSelector:@selector(prepareForReuse)] mapReplace:RACUnit.defaultUnit] setNameWithFormat:@"%@ -rac_prepareForReuseSignal", RACDescription(self)]; objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return signal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UITableViewHeaderFooterView+RACSignalSupport.h ================================================ // // UITableViewHeaderFooterView+RACSignalSupport.h // ReactiveObjC // // Created by Syo Ikeda on 12/30/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import @class RACSignal<__covariant ValueType>; @class RACUnit; NS_ASSUME_NONNULL_BEGIN // This category is only applicable to iOS >= 6.0. @interface UITableViewHeaderFooterView (RACSignalSupport) /// A signal which will send a RACUnit whenever -prepareForReuse is invoked upon /// the receiver. /// /// Examples /// /// [[[self.cancelButton /// rac_signalForControlEvents:UIControlEventTouchUpInside] /// takeUntil:self.rac_prepareForReuseSignal] /// subscribeNext:^(UIButton *x) { /// // do other things /// }]; @property (nonatomic, strong, readonly) RACSignal *rac_prepareForReuseSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UITableViewHeaderFooterView+RACSignalSupport.m ================================================ // // UITableViewHeaderFooterView+RACSignalSupport.m // ReactiveObjC // // Created by Syo Ikeda on 12/30/13. // Copyright (c) 2013 GitHub, Inc. All rights reserved. // #import "UITableViewHeaderFooterView+RACSignalSupport.h" #import "NSObject+RACDescription.h" #import "NSObject+RACSelectorSignal.h" #import "RACSignal+Operations.h" #import "RACUnit.h" #import @implementation UITableViewHeaderFooterView (RACSignalSupport) - (RACSignal *)rac_prepareForReuseSignal { RACSignal *signal = objc_getAssociatedObject(self, _cmd); if (signal != nil) return signal; signal = [[[self rac_signalForSelector:@selector(prepareForReuse)] mapReplace:RACUnit.defaultUnit] setNameWithFormat:@"%@ -rac_prepareForReuseSignal", RACDescription(self)]; objc_setAssociatedObject(self, _cmd, signal, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return signal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UITextField+RACSignalSupport.h ================================================ // // UITextField+RACSignalSupport.h // ReactiveObjC // // Created by Josh Abernathy on 4/17/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import @class RACChannelTerminal; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UITextField (RACSignalSupport) /// Creates and returns a signal for the text of the field. It always starts with /// the current text. The signal sends next when the UIControlEventAllEditingEvents /// control event is fired on the control. - (RACSignal *)rac_textSignal; /// Creates a new RACChannel-based binding to the receiver. /// /// Returns a RACChannelTerminal that sends the receiver's text whenever the /// UIControlEventAllEditingEvents control event is fired, and sets the text /// to the values it receives. - (RACChannelTerminal *)rac_newTextChannel; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UITextField+RACSignalSupport.m ================================================ // // UITextField+RACSignalSupport.m // ReactiveObjC // // Created by Josh Abernathy on 4/17/12. // Copyright (c) 2012 GitHub, Inc. All rights reserved. // #import "UITextField+RACSignalSupport.h" #import #import #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import "RACSignal+Operations.h" #import "UIControl+RACSignalSupport.h" #import "UIControl+RACSignalSupportPrivate.h" @implementation UITextField (RACSignalSupport) - (RACSignal *)rac_textSignal { @weakify(self); return [[[[[RACSignal defer:^{ @strongify(self); return [RACSignal return:self]; }] concat:[self rac_signalForControlEvents:UIControlEventAllEditingEvents]] map:^(UITextField *x) { return x.text; }] takeUntil:self.rac_willDeallocSignal] setNameWithFormat:@"%@ -rac_textSignal", RACDescription(self)]; } - (RACChannelTerminal *)rac_newTextChannel { return [self rac_channelForControlEvents:UIControlEventAllEditingEvents key:@keypath(self.text) nilValue:@""]; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UITextView+RACSignalSupport.h ================================================ // // UITextView+RACSignalSupport.h // ReactiveObjC // // Created by Cody Krieger on 5/18/12. // Copyright (c) 2012 Cody Krieger. All rights reserved. // #import @class RACDelegateProxy; @class RACSignal<__covariant ValueType>; NS_ASSUME_NONNULL_BEGIN @interface UITextView (RACSignalSupport) /// A delegate proxy which will be set as the receiver's delegate when any of the /// methods in this category are used. @property (nonatomic, strong, readonly) RACDelegateProxy *rac_delegateProxy; /// Creates a signal for the text of the receiver. /// /// When this method is invoked, the `rac_delegateProxy` will become the /// receiver's delegate. Any previous delegate will become the -[RACDelegateProxy /// rac_proxiedDelegate], so that it receives any messages that the proxy doesn't /// know how to handle. Setting the receiver's `delegate` afterward is /// considered undefined behavior. /// /// Returns a signal which will send the current text upon subscription, then /// again whenever the receiver's text is changed. The signal will complete when /// the receiver is deallocated. - (RACSignal *)rac_textSignal; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/UITextView+RACSignalSupport.m ================================================ // // UITextView+RACSignalSupport.m // ReactiveObjC // // Created by Cody Krieger on 5/18/12. // Copyright (c) 2012 Cody Krieger. All rights reserved. // #import "UITextView+RACSignalSupport.h" #import #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import "RACDelegateProxy.h" #import "RACSignal+Operations.h" #import "RACTuple.h" #import @implementation UITextView (RACSignalSupport) static void RACUseDelegateProxy(UITextView *self) { if (self.delegate == self.rac_delegateProxy) return; self.rac_delegateProxy.rac_proxiedDelegate = self.delegate; self.delegate = (id)self.rac_delegateProxy; } - (RACDelegateProxy *)rac_delegateProxy { RACDelegateProxy *proxy = objc_getAssociatedObject(self, _cmd); if (proxy == nil) { proxy = [[RACDelegateProxy alloc] initWithProtocol:@protocol(UITextViewDelegate)]; objc_setAssociatedObject(self, _cmd, proxy, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return proxy; } - (RACSignal *)rac_textSignal { @weakify(self); RACSignal *signal = [[[[[RACSignal defer:^{ @strongify(self); return [RACSignal return:RACTuplePack(self)]; }] concat:[self.rac_delegateProxy signalForSelector:@selector(textViewDidChange:)]] reduceEach:^(UITextView *x) { return x.text; }] takeUntil:self.rac_willDeallocSignal] setNameWithFormat:@"%@ -rac_textSignal", RACDescription(self)]; RACUseDelegateProxy(self); return signal; } @end ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/extobjc/RACEXTKeyPathCoding.h ================================================ // // EXTKeyPathCoding.h // extobjc // // Created by Justin Spahr-Summers on 19.06.12. // Copyright (C) 2012 Justin Spahr-Summers. // Released under the MIT license. // #import #import "RACmetamacros.h" /** * \@keypath allows compile-time verification of key paths. Given a real object * receiver and key path: * * @code NSString *UTF8StringPath = @keypath(str.lowercaseString.UTF8String); // => @"lowercaseString.UTF8String" NSString *versionPath = @keypath(NSObject, version); // => @"version" NSString *lowercaseStringPath = @keypath(NSString.new, lowercaseString); // => @"lowercaseString" * @endcode * * ... the macro returns an \c NSString containing all but the first path * component or argument (e.g., @"lowercaseString.UTF8String", @"version"). * * In addition to simply creating a key path, this macro ensures that the key * path is valid at compile-time (causing a syntax error if not), and supports * refactoring, such that changing the name of the property will also update any * uses of \@keypath. */ #define keypath(...) \ metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__)) #define keypath1(PATH) \ (((void)(NO && ((void)PATH, NO)), strchr(# PATH, '.') + 1)) #define keypath2(OBJ, PATH) \ (((void)(NO && ((void)OBJ.PATH, NO)), # PATH)) /** * \@collectionKeypath allows compile-time verification of key paths across collections NSArray/NSSet etc. Given a real object * receiver, collection object receiver and related keypaths: * * @code NSString *employessFirstNamePath = @collectionKeypath(department.employees, Employee.new, firstName) // => @"employees.firstName" NSString *employessFirstNamePath = @collectionKeypath(Department.new, employees, Employee.new, firstName) // => @"employees.firstName" * @endcode * */ #define collectionKeypath(...) \ metamacro_if_eq(3, metamacro_argcount(__VA_ARGS__))(collectionKeypath3(__VA_ARGS__))(collectionKeypath4(__VA_ARGS__)) #define collectionKeypath3(PATH, COLLECTION_OBJECT, COLLECTION_PATH) ([[NSString stringWithFormat:@"%s.%s",keypath(PATH), keypath(COLLECTION_OBJECT, COLLECTION_PATH)] UTF8String]) #define collectionKeypath4(OBJ, PATH, COLLECTION_OBJECT, COLLECTION_PATH) ([[NSString stringWithFormat:@"%s.%s",keypath(OBJ, PATH), keypath(COLLECTION_OBJECT, COLLECTION_PATH)] UTF8String]) ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/extobjc/RACEXTRuntimeExtensions.h ================================================ // // EXTRuntimeExtensions.h // extobjc // // Created by Justin Spahr-Summers on 2011-03-05. // Copyright (C) 2012 Justin Spahr-Summers. // Released under the MIT license. // #import /** * Describes the memory management policy of a property. */ typedef enum { /** * The value is assigned. */ rac_propertyMemoryManagementPolicyAssign = 0, /** * The value is retained. */ rac_propertyMemoryManagementPolicyRetain, /** * The value is copied. */ rac_propertyMemoryManagementPolicyCopy } rac_propertyMemoryManagementPolicy; /** * Describes the attributes and type information of a property. */ typedef struct { /** * Whether this property was declared with the \c readonly attribute. */ BOOL readonly; /** * Whether this property was declared with the \c nonatomic attribute. */ BOOL nonatomic; /** * Whether the property is a weak reference. */ BOOL weak; /** * Whether the property is eligible for garbage collection. */ BOOL canBeCollected; /** * Whether this property is defined with \c \@dynamic. */ BOOL dynamic; /** * The memory management policy for this property. This will always be * #rac_propertyMemoryManagementPolicyAssign if #readonly is \c YES. */ rac_propertyMemoryManagementPolicy memoryManagementPolicy; /** * The selector for the getter of this property. This will reflect any * custom \c getter= attribute provided in the property declaration, or the * inferred getter name otherwise. */ SEL getter; /** * The selector for the setter of this property. This will reflect any * custom \c setter= attribute provided in the property declaration, or the * inferred setter name otherwise. * * @note If #readonly is \c YES, this value will represent what the setter * \e would be, if the property were writable. */ SEL setter; /** * The backing instance variable for this property, or \c NULL if \c * \c @synthesize was not used, and therefore no instance variable exists. This * would also be the case if the property is implemented dynamically. */ const char *ivar; /** * If this property is defined as being an instance of a specific class, * this will be the class object representing it. * * This will be \c nil if the property was defined as type \c id, if the * property is not of an object type, or if the class could not be found at * runtime. */ Class objectClass; /** * The type encoding for the value of this property. This is the type as it * would be returned by the \c \@encode() directive. */ char type[]; } rac_propertyAttributes; /** * Finds the instance method named \a aSelector on \a aClass and returns it, or * returns \c NULL if no such instance method exists. Unlike \c * class_getInstanceMethod(), this does not search superclasses. * * @note To get class methods in this manner, use a metaclass for \a aClass. */ Method rac_getImmediateInstanceMethod (Class aClass, SEL aSelector); /** * Returns a pointer to a structure containing information about \a property. * You must \c free() the returned pointer. Returns \c NULL if there is an error * obtaining information from \a property. */ rac_propertyAttributes *rac_copyPropertyAttributes (objc_property_t property); ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/extobjc/RACEXTRuntimeExtensions.m ================================================ // // EXTRuntimeExtensions.m // extobjc // // Created by Justin Spahr-Summers on 2011-03-05. // Copyright (C) 2012 Justin Spahr-Summers. // Released under the MIT license. // #import "RACEXTRuntimeExtensions.h" #import #import #import #import #import #import #import #import rac_propertyAttributes *rac_copyPropertyAttributes (objc_property_t property) { const char * const attrString = property_getAttributes(property); if (!attrString) { fprintf(stderr, "ERROR: Could not get attribute string from property %s\n", property_getName(property)); return NULL; } if (attrString[0] != 'T') { fprintf(stderr, "ERROR: Expected attribute string \"%s\" for property %s to start with 'T'\n", attrString, property_getName(property)); return NULL; } const char *typeString = attrString + 1; const char *next = NSGetSizeAndAlignment(typeString, NULL, NULL); if (!next) { fprintf(stderr, "ERROR: Could not read past type in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); return NULL; } size_t typeLength = (size_t)(next - typeString); if (!typeLength) { fprintf(stderr, "ERROR: Invalid type in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); return NULL; } // allocate enough space for the structure and the type string (plus a NUL) rac_propertyAttributes *attributes = calloc(1, sizeof(rac_propertyAttributes) + typeLength + 1); if (!attributes) { fprintf(stderr, "ERROR: Could not allocate rac_propertyAttributes structure for attribute string \"%s\" for property %s\n", attrString, property_getName(property)); return NULL; } // copy the type string strncpy(attributes->type, typeString, typeLength); attributes->type[typeLength] = '\0'; // if this is an object type, and immediately followed by a quoted string... if (typeString[0] == *(@encode(id)) && typeString[1] == '"') { // we should be able to extract a class name const char *className = typeString + 2; next = strchr(className, '"'); if (!next) { fprintf(stderr, "ERROR: Could not read class name in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); goto errorOut; } if (className != next) { size_t classNameLength = (size_t)(next - className); char trimmedName[classNameLength + 1]; strncpy(trimmedName, className, classNameLength); trimmedName[classNameLength] = '\0'; // attempt to look up the class in the runtime attributes->objectClass = objc_getClass(trimmedName); } } if (*next != '\0') { // skip past any junk before the first flag next = strchr(next, ','); } while (next && *next == ',') { char flag = next[1]; next += 2; switch (flag) { case '\0': break; case 'R': attributes->readonly = YES; break; case 'C': attributes->memoryManagementPolicy = rac_propertyMemoryManagementPolicyCopy; break; case '&': attributes->memoryManagementPolicy = rac_propertyMemoryManagementPolicyRetain; break; case 'N': attributes->nonatomic = YES; break; case 'G': case 'S': { const char *nextFlag = strchr(next, ','); SEL name = NULL; if (!nextFlag) { // assume that the rest of the string is the selector const char *selectorString = next; next = ""; name = sel_registerName(selectorString); } else { size_t selectorLength = (size_t)(nextFlag - next); if (!selectorLength) { fprintf(stderr, "ERROR: Found zero length selector name in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); goto errorOut; } char selectorString[selectorLength + 1]; strncpy(selectorString, next, selectorLength); selectorString[selectorLength] = '\0'; name = sel_registerName(selectorString); next = nextFlag; } if (flag == 'G') attributes->getter = name; else attributes->setter = name; } break; case 'D': attributes->dynamic = YES; attributes->ivar = NULL; break; case 'V': // assume that the rest of the string (if present) is the ivar name if (*next == '\0') { // if there's nothing there, let's assume this is dynamic attributes->ivar = NULL; } else { attributes->ivar = next; next = ""; } break; case 'W': attributes->weak = YES; break; case 'P': attributes->canBeCollected = YES; break; case 't': fprintf(stderr, "ERROR: Old-style type encoding is unsupported in attribute string \"%s\" for property %s\n", attrString, property_getName(property)); // skip over this type encoding while (*next != ',' && *next != '\0') ++next; break; default: fprintf(stderr, "ERROR: Unrecognized attribute string flag '%c' in attribute string \"%s\" for property %s\n", flag, attrString, property_getName(property)); } } if (next && *next != '\0') { fprintf(stderr, "Warning: Unparsed data \"%s\" in attribute string \"%s\" for property %s\n", next, attrString, property_getName(property)); } if (!attributes->getter) { // use the property name as the getter by default attributes->getter = sel_registerName(property_getName(property)); } if (!attributes->setter) { const char *propertyName = property_getName(property); size_t propertyNameLength = strlen(propertyName); // we want to transform the name to setProperty: style size_t setterLength = propertyNameLength + 4; char setterName[setterLength + 1]; strncpy(setterName, "set", 3); strncpy(setterName + 3, propertyName, propertyNameLength); // capitalize property name for the setter setterName[3] = (char)toupper(setterName[3]); setterName[setterLength - 1] = ':'; setterName[setterLength] = '\0'; attributes->setter = sel_registerName(setterName); } return attributes; errorOut: free(attributes); return NULL; } Method rac_getImmediateInstanceMethod (Class aClass, SEL aSelector) { unsigned methodCount = 0; Method *methods = class_copyMethodList(aClass, &methodCount); Method foundMethod = NULL; for (unsigned methodIndex = 0;methodIndex < methodCount;++methodIndex) { if (method_getName(methods[methodIndex]) == aSelector) { foundMethod = methods[methodIndex]; break; } } free(methods); return foundMethod; } ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/extobjc/RACEXTScope.h ================================================ // // EXTScope.h // extobjc // // Created by Justin Spahr-Summers on 2011-05-04. // Copyright (C) 2012 Justin Spahr-Summers. // Released under the MIT license. // #import "RACmetamacros.h" /** * \@onExit defines some code to be executed when the current scope exits. The * code must be enclosed in braces and terminated with a semicolon, and will be * executed regardless of how the scope is exited, including from exceptions, * \c goto, \c return, \c break, and \c continue. * * Provided code will go into a block to be executed later. Keep this in mind as * it pertains to memory management, restrictions on assignment, etc. Because * the code is used within a block, \c return is a legal (though perhaps * confusing) way to exit the cleanup block early. * * Multiple \@onExit statements in the same scope are executed in reverse * lexical order. This helps when pairing resource acquisition with \@onExit * statements, as it guarantees teardown in the opposite order of acquisition. * * @note This statement cannot be used within scopes defined without braces * (like a one line \c if). In practice, this is not an issue, since \@onExit is * a useless construct in such a case anyways. */ #define onExit \ rac_keywordify \ __strong rac_cleanupBlock_t metamacro_concat(rac_exitBlock_, __LINE__) __attribute__((cleanup(rac_executeCleanupBlock), unused)) = ^ /** * Creates \c __weak shadow variables for each of the variables provided as * arguments, which can later be made strong again with #strongify. * * This is typically used to weakly reference variables in a block, but then * ensure that the variables stay alive during the actual execution of the block * (if they were live upon entry). * * See #strongify for an example of usage. */ #define weakify(...) \ rac_keywordify \ metamacro_foreach_cxt(rac_weakify_,, __weak, __VA_ARGS__) /** * Like #weakify, but uses \c __unsafe_unretained instead, for targets or * classes that do not support weak references. */ #define unsafeify(...) \ rac_keywordify \ metamacro_foreach_cxt(rac_weakify_,, __unsafe_unretained, __VA_ARGS__) /** * Strongly references each of the variables provided as arguments, which must * have previously been passed to #weakify. * * The strong references created will shadow the original variable names, such * that the original names can be used without issue (and a significantly * reduced risk of retain cycles) in the current scope. * * @code id foo = [[NSObject alloc] init]; id bar = [[NSObject alloc] init]; @weakify(foo, bar); // this block will not keep 'foo' or 'bar' alive BOOL (^matchesFooOrBar)(id) = ^ BOOL (id obj){ // but now, upon entry, 'foo' and 'bar' will stay alive until the block has // finished executing @strongify(foo, bar); return [foo isEqual:obj] || [bar isEqual:obj]; }; * @endcode */ #define strongify(...) \ rac_keywordify \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wshadow\"") \ metamacro_foreach(rac_strongify_,, __VA_ARGS__) \ _Pragma("clang diagnostic pop") /*** implementation details follow ***/ typedef void (^rac_cleanupBlock_t)(void); static inline void rac_executeCleanupBlock (__strong rac_cleanupBlock_t *block) { (*block)(); } #define rac_weakify_(INDEX, CONTEXT, VAR) \ CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR); #define rac_strongify_(INDEX, VAR) \ __strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_); // Details about the choice of backing keyword: // // The use of @try/@catch/@finally can cause the compiler to suppress // return-type warnings. // The use of @autoreleasepool {} is not optimized away by the compiler, // resulting in superfluous creation of autorelease pools. // // Since neither option is perfect, and with no other alternatives, the // compromise is to use @autorelease in DEBUG builds to maintain compiler // analysis, and to use @try/@catch otherwise to avoid insertion of unnecessary // autorelease pools. #if DEBUG #define rac_keywordify autoreleasepool {} #else #define rac_keywordify try {} @catch (...) {} #endif ================================================ FILE: Pods/ReactiveObjC/ReactiveObjC/extobjc/RACmetamacros.h ================================================ /** * Macros for metaprogramming * ExtendedC * * Copyright (C) 2012 Justin Spahr-Summers * Released under the MIT license */ /** * Executes one or more expressions (which may have a void type, such as a call * to a function that returns no value) and always returns true. */ #define metamacro_exprify(...) \ ((__VA_ARGS__), true) /** * Returns a string representation of VALUE after full macro expansion. */ #define metamacro_stringify(VALUE) \ metamacro_stringify_(VALUE) /** * Returns A and B concatenated after full macro expansion. */ #define metamacro_concat(A, B) \ metamacro_concat_(A, B) /** * Returns the Nth variadic argument (starting from zero). At least * N + 1 variadic arguments must be given. N must be between zero and twenty, * inclusive. */ #define metamacro_at(N, ...) \ metamacro_concat(metamacro_at, N)(__VA_ARGS__) /** * Returns the number of arguments (up to twenty) provided to the macro. At * least one argument must be provided. * * Inspired by P99: http://p99.gforge.inria.fr */ #define metamacro_argcount(...) \ metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) /** * Identical to #metamacro_foreach_cxt, except that no CONTEXT argument is * given. Only the index and current argument will thus be passed to MACRO. */ #define metamacro_foreach(MACRO, SEP, ...) \ metamacro_foreach_cxt(metamacro_foreach_iter, SEP, MACRO, __VA_ARGS__) /** * For each consecutive variadic argument (up to twenty), MACRO is passed the * zero-based index of the current argument, CONTEXT, and then the argument * itself. The results of adjoining invocations of MACRO are then separated by * SEP. * * Inspired by P99: http://p99.gforge.inria.fr */ #define metamacro_foreach_cxt(MACRO, SEP, CONTEXT, ...) \ metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__) /** * Identical to #metamacro_foreach_cxt. This can be used when the former would * fail due to recursive macro expansion. */ #define metamacro_foreach_cxt_recursive(MACRO, SEP, CONTEXT, ...) \ metamacro_concat(metamacro_foreach_cxt_recursive, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__) /** * In consecutive order, appends each variadic argument (up to twenty) onto * BASE. The resulting concatenations are then separated by SEP. * * This is primarily useful to manipulate a list of macro invocations into instead * invoking a different, possibly related macro. */ #define metamacro_foreach_concat(BASE, SEP, ...) \ metamacro_foreach_cxt(metamacro_foreach_concat_iter, SEP, BASE, __VA_ARGS__) /** * Iterates COUNT times, each time invoking MACRO with the current index * (starting at zero) and CONTEXT. The results of adjoining invocations of MACRO * are then separated by SEP. * * COUNT must be an integer between zero and twenty, inclusive. */ #define metamacro_for_cxt(COUNT, MACRO, SEP, CONTEXT) \ metamacro_concat(metamacro_for_cxt, COUNT)(MACRO, SEP, CONTEXT) /** * Returns the first argument given. At least one argument must be provided. * * This is useful when implementing a variadic macro, where you may have only * one variadic argument, but no way to retrieve it (for example, because \c ... * always needs to match at least one argument). * * @code #define varmacro(...) \ metamacro_head(__VA_ARGS__) * @endcode */ #define metamacro_head(...) \ metamacro_head_(__VA_ARGS__, 0) /** * Returns every argument except the first. At least two arguments must be * provided. */ #define metamacro_tail(...) \ metamacro_tail_(__VA_ARGS__) /** * Returns the first N (up to twenty) variadic arguments as a new argument list. * At least N variadic arguments must be provided. */ #define metamacro_take(N, ...) \ metamacro_concat(metamacro_take, N)(__VA_ARGS__) /** * Removes the first N (up to twenty) variadic arguments from the given argument * list. At least N variadic arguments must be provided. */ #define metamacro_drop(N, ...) \ metamacro_concat(metamacro_drop, N)(__VA_ARGS__) /** * Decrements VAL, which must be a number between zero and twenty, inclusive. * * This is primarily useful when dealing with indexes and counts in * metaprogramming. */ #define metamacro_dec(VAL) \ metamacro_at(VAL, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19) /** * Increments VAL, which must be a number between zero and twenty, inclusive. * * This is primarily useful when dealing with indexes and counts in * metaprogramming. */ #define metamacro_inc(VAL) \ metamacro_at(VAL, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21) /** * If A is equal to B, the next argument list is expanded; otherwise, the * argument list after that is expanded. A and B must be numbers between zero * and twenty, inclusive. Additionally, B must be greater than or equal to A. * * @code // expands to true metamacro_if_eq(0, 0)(true)(false) // expands to false metamacro_if_eq(0, 1)(true)(false) * @endcode * * This is primarily useful when dealing with indexes and counts in * metaprogramming. */ #define metamacro_if_eq(A, B) \ metamacro_concat(metamacro_if_eq, A)(B) /** * Identical to #metamacro_if_eq. This can be used when the former would fail * due to recursive macro expansion. */ #define metamacro_if_eq_recursive(A, B) \ metamacro_concat(metamacro_if_eq_recursive, A)(B) /** * Returns 1 if N is an even number, or 0 otherwise. N must be between zero and * twenty, inclusive. * * For the purposes of this test, zero is considered even. */ #define metamacro_is_even(N) \ metamacro_at(N, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) /** * Returns the logical NOT of B, which must be the number zero or one. */ #define metamacro_not(B) \ metamacro_at(B, 1, 0) // IMPLEMENTATION DETAILS FOLLOW! // Do not write code that depends on anything below this line. #define metamacro_stringify_(VALUE) # VALUE #define metamacro_concat_(A, B) A ## B #define metamacro_foreach_iter(INDEX, MACRO, ARG) MACRO(INDEX, ARG) #define metamacro_head_(FIRST, ...) FIRST #define metamacro_tail_(FIRST, ...) __VA_ARGS__ #define metamacro_consume_(...) #define metamacro_expand_(...) __VA_ARGS__ // implemented from scratch so that metamacro_concat() doesn't end up nesting #define metamacro_foreach_concat_iter(INDEX, BASE, ARG) metamacro_foreach_concat_iter_(BASE, ARG) #define metamacro_foreach_concat_iter_(BASE, ARG) BASE ## ARG // metamacro_at expansions #define metamacro_at0(...) metamacro_head(__VA_ARGS__) #define metamacro_at1(_0, ...) metamacro_head(__VA_ARGS__) #define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__) #define metamacro_at3(_0, _1, _2, ...) metamacro_head(__VA_ARGS__) #define metamacro_at4(_0, _1, _2, _3, ...) metamacro_head(__VA_ARGS__) #define metamacro_at5(_0, _1, _2, _3, _4, ...) metamacro_head(__VA_ARGS__) #define metamacro_at6(_0, _1, _2, _3, _4, _5, ...) metamacro_head(__VA_ARGS__) #define metamacro_at7(_0, _1, _2, _3, _4, _5, _6, ...) metamacro_head(__VA_ARGS__) #define metamacro_at8(_0, _1, _2, _3, _4, _5, _6, _7, ...) metamacro_head(__VA_ARGS__) #define metamacro_at9(_0, _1, _2, _3, _4, _5, _6, _7, _8, ...) metamacro_head(__VA_ARGS__) #define metamacro_at10(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, ...) metamacro_head(__VA_ARGS__) #define metamacro_at11(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, ...) metamacro_head(__VA_ARGS__) #define metamacro_at12(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, ...) metamacro_head(__VA_ARGS__) #define metamacro_at13(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, ...) metamacro_head(__VA_ARGS__) #define metamacro_at14(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, ...) metamacro_head(__VA_ARGS__) #define metamacro_at15(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, ...) metamacro_head(__VA_ARGS__) #define metamacro_at16(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, ...) metamacro_head(__VA_ARGS__) #define metamacro_at17(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, ...) metamacro_head(__VA_ARGS__) #define metamacro_at18(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, ...) metamacro_head(__VA_ARGS__) #define metamacro_at19(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, ...) metamacro_head(__VA_ARGS__) #define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__) // metamacro_foreach_cxt expansions #define metamacro_foreach_cxt0(MACRO, SEP, CONTEXT) #define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0) #define metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \ metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) \ SEP \ MACRO(1, CONTEXT, _1) #define metamacro_foreach_cxt3(MACRO, SEP, CONTEXT, _0, _1, _2) \ metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \ SEP \ MACRO(2, CONTEXT, _2) #define metamacro_foreach_cxt4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \ metamacro_foreach_cxt3(MACRO, SEP, CONTEXT, _0, _1, _2) \ SEP \ MACRO(3, CONTEXT, _3) #define metamacro_foreach_cxt5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \ metamacro_foreach_cxt4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \ SEP \ MACRO(4, CONTEXT, _4) #define metamacro_foreach_cxt6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \ metamacro_foreach_cxt5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \ SEP \ MACRO(5, CONTEXT, _5) #define metamacro_foreach_cxt7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \ metamacro_foreach_cxt6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \ SEP \ MACRO(6, CONTEXT, _6) #define metamacro_foreach_cxt8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \ metamacro_foreach_cxt7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \ SEP \ MACRO(7, CONTEXT, _7) #define metamacro_foreach_cxt9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \ metamacro_foreach_cxt8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \ SEP \ MACRO(8, CONTEXT, _8) #define metamacro_foreach_cxt10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ metamacro_foreach_cxt9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \ SEP \ MACRO(9, CONTEXT, _9) #define metamacro_foreach_cxt11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \ metamacro_foreach_cxt10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ SEP \ MACRO(10, CONTEXT, _10) #define metamacro_foreach_cxt12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \ metamacro_foreach_cxt11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \ SEP \ MACRO(11, CONTEXT, _11) #define metamacro_foreach_cxt13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \ metamacro_foreach_cxt12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \ SEP \ MACRO(12, CONTEXT, _12) #define metamacro_foreach_cxt14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \ metamacro_foreach_cxt13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \ SEP \ MACRO(13, CONTEXT, _13) #define metamacro_foreach_cxt15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \ metamacro_foreach_cxt14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \ SEP \ MACRO(14, CONTEXT, _14) #define metamacro_foreach_cxt16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \ metamacro_foreach_cxt15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \ SEP \ MACRO(15, CONTEXT, _15) #define metamacro_foreach_cxt17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \ metamacro_foreach_cxt16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \ SEP \ MACRO(16, CONTEXT, _16) #define metamacro_foreach_cxt18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \ metamacro_foreach_cxt17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \ SEP \ MACRO(17, CONTEXT, _17) #define metamacro_foreach_cxt19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \ metamacro_foreach_cxt18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \ SEP \ MACRO(18, CONTEXT, _18) #define metamacro_foreach_cxt20(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19) \ metamacro_foreach_cxt19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \ SEP \ MACRO(19, CONTEXT, _19) // metamacro_foreach_cxt_recursive expansions #define metamacro_foreach_cxt_recursive0(MACRO, SEP, CONTEXT) #define metamacro_foreach_cxt_recursive1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0) #define metamacro_foreach_cxt_recursive2(MACRO, SEP, CONTEXT, _0, _1) \ metamacro_foreach_cxt_recursive1(MACRO, SEP, CONTEXT, _0) \ SEP \ MACRO(1, CONTEXT, _1) #define metamacro_foreach_cxt_recursive3(MACRO, SEP, CONTEXT, _0, _1, _2) \ metamacro_foreach_cxt_recursive2(MACRO, SEP, CONTEXT, _0, _1) \ SEP \ MACRO(2, CONTEXT, _2) #define metamacro_foreach_cxt_recursive4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \ metamacro_foreach_cxt_recursive3(MACRO, SEP, CONTEXT, _0, _1, _2) \ SEP \ MACRO(3, CONTEXT, _3) #define metamacro_foreach_cxt_recursive5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \ metamacro_foreach_cxt_recursive4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \ SEP \ MACRO(4, CONTEXT, _4) #define metamacro_foreach_cxt_recursive6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \ metamacro_foreach_cxt_recursive5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \ SEP \ MACRO(5, CONTEXT, _5) #define metamacro_foreach_cxt_recursive7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \ metamacro_foreach_cxt_recursive6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \ SEP \ MACRO(6, CONTEXT, _6) #define metamacro_foreach_cxt_recursive8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \ metamacro_foreach_cxt_recursive7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \ SEP \ MACRO(7, CONTEXT, _7) #define metamacro_foreach_cxt_recursive9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \ metamacro_foreach_cxt_recursive8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \ SEP \ MACRO(8, CONTEXT, _8) #define metamacro_foreach_cxt_recursive10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ metamacro_foreach_cxt_recursive9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \ SEP \ MACRO(9, CONTEXT, _9) #define metamacro_foreach_cxt_recursive11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \ metamacro_foreach_cxt_recursive10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ SEP \ MACRO(10, CONTEXT, _10) #define metamacro_foreach_cxt_recursive12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \ metamacro_foreach_cxt_recursive11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \ SEP \ MACRO(11, CONTEXT, _11) #define metamacro_foreach_cxt_recursive13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \ metamacro_foreach_cxt_recursive12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \ SEP \ MACRO(12, CONTEXT, _12) #define metamacro_foreach_cxt_recursive14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \ metamacro_foreach_cxt_recursive13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \ SEP \ MACRO(13, CONTEXT, _13) #define metamacro_foreach_cxt_recursive15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \ metamacro_foreach_cxt_recursive14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \ SEP \ MACRO(14, CONTEXT, _14) #define metamacro_foreach_cxt_recursive16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \ metamacro_foreach_cxt_recursive15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \ SEP \ MACRO(15, CONTEXT, _15) #define metamacro_foreach_cxt_recursive17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \ metamacro_foreach_cxt_recursive16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \ SEP \ MACRO(16, CONTEXT, _16) #define metamacro_foreach_cxt_recursive18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \ metamacro_foreach_cxt_recursive17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \ SEP \ MACRO(17, CONTEXT, _17) #define metamacro_foreach_cxt_recursive19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \ metamacro_foreach_cxt_recursive18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \ SEP \ MACRO(18, CONTEXT, _18) #define metamacro_foreach_cxt_recursive20(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19) \ metamacro_foreach_cxt_recursive19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \ SEP \ MACRO(19, CONTEXT, _19) // metamacro_for_cxt expansions #define metamacro_for_cxt0(MACRO, SEP, CONTEXT) #define metamacro_for_cxt1(MACRO, SEP, CONTEXT) MACRO(0, CONTEXT) #define metamacro_for_cxt2(MACRO, SEP, CONTEXT) \ metamacro_for_cxt1(MACRO, SEP, CONTEXT) \ SEP \ MACRO(1, CONTEXT) #define metamacro_for_cxt3(MACRO, SEP, CONTEXT) \ metamacro_for_cxt2(MACRO, SEP, CONTEXT) \ SEP \ MACRO(2, CONTEXT) #define metamacro_for_cxt4(MACRO, SEP, CONTEXT) \ metamacro_for_cxt3(MACRO, SEP, CONTEXT) \ SEP \ MACRO(3, CONTEXT) #define metamacro_for_cxt5(MACRO, SEP, CONTEXT) \ metamacro_for_cxt4(MACRO, SEP, CONTEXT) \ SEP \ MACRO(4, CONTEXT) #define metamacro_for_cxt6(MACRO, SEP, CONTEXT) \ metamacro_for_cxt5(MACRO, SEP, CONTEXT) \ SEP \ MACRO(5, CONTEXT) #define metamacro_for_cxt7(MACRO, SEP, CONTEXT) \ metamacro_for_cxt6(MACRO, SEP, CONTEXT) \ SEP \ MACRO(6, CONTEXT) #define metamacro_for_cxt8(MACRO, SEP, CONTEXT) \ metamacro_for_cxt7(MACRO, SEP, CONTEXT) \ SEP \ MACRO(7, CONTEXT) #define metamacro_for_cxt9(MACRO, SEP, CONTEXT) \ metamacro_for_cxt8(MACRO, SEP, CONTEXT) \ SEP \ MACRO(8, CONTEXT) #define metamacro_for_cxt10(MACRO, SEP, CONTEXT) \ metamacro_for_cxt9(MACRO, SEP, CONTEXT) \ SEP \ MACRO(9, CONTEXT) #define metamacro_for_cxt11(MACRO, SEP, CONTEXT) \ metamacro_for_cxt10(MACRO, SEP, CONTEXT) \ SEP \ MACRO(10, CONTEXT) #define metamacro_for_cxt12(MACRO, SEP, CONTEXT) \ metamacro_for_cxt11(MACRO, SEP, CONTEXT) \ SEP \ MACRO(11, CONTEXT) #define metamacro_for_cxt13(MACRO, SEP, CONTEXT) \ metamacro_for_cxt12(MACRO, SEP, CONTEXT) \ SEP \ MACRO(12, CONTEXT) #define metamacro_for_cxt14(MACRO, SEP, CONTEXT) \ metamacro_for_cxt13(MACRO, SEP, CONTEXT) \ SEP \ MACRO(13, CONTEXT) #define metamacro_for_cxt15(MACRO, SEP, CONTEXT) \ metamacro_for_cxt14(MACRO, SEP, CONTEXT) \ SEP \ MACRO(14, CONTEXT) #define metamacro_for_cxt16(MACRO, SEP, CONTEXT) \ metamacro_for_cxt15(MACRO, SEP, CONTEXT) \ SEP \ MACRO(15, CONTEXT) #define metamacro_for_cxt17(MACRO, SEP, CONTEXT) \ metamacro_for_cxt16(MACRO, SEP, CONTEXT) \ SEP \ MACRO(16, CONTEXT) #define metamacro_for_cxt18(MACRO, SEP, CONTEXT) \ metamacro_for_cxt17(MACRO, SEP, CONTEXT) \ SEP \ MACRO(17, CONTEXT) #define metamacro_for_cxt19(MACRO, SEP, CONTEXT) \ metamacro_for_cxt18(MACRO, SEP, CONTEXT) \ SEP \ MACRO(18, CONTEXT) #define metamacro_for_cxt20(MACRO, SEP, CONTEXT) \ metamacro_for_cxt19(MACRO, SEP, CONTEXT) \ SEP \ MACRO(19, CONTEXT) // metamacro_if_eq expansions #define metamacro_if_eq0(VALUE) \ metamacro_concat(metamacro_if_eq0_, VALUE) #define metamacro_if_eq0_0(...) __VA_ARGS__ metamacro_consume_ #define metamacro_if_eq0_1(...) metamacro_expand_ #define metamacro_if_eq0_2(...) metamacro_expand_ #define metamacro_if_eq0_3(...) metamacro_expand_ #define metamacro_if_eq0_4(...) metamacro_expand_ #define metamacro_if_eq0_5(...) metamacro_expand_ #define metamacro_if_eq0_6(...) metamacro_expand_ #define metamacro_if_eq0_7(...) metamacro_expand_ #define metamacro_if_eq0_8(...) metamacro_expand_ #define metamacro_if_eq0_9(...) metamacro_expand_ #define metamacro_if_eq0_10(...) metamacro_expand_ #define metamacro_if_eq0_11(...) metamacro_expand_ #define metamacro_if_eq0_12(...) metamacro_expand_ #define metamacro_if_eq0_13(...) metamacro_expand_ #define metamacro_if_eq0_14(...) metamacro_expand_ #define metamacro_if_eq0_15(...) metamacro_expand_ #define metamacro_if_eq0_16(...) metamacro_expand_ #define metamacro_if_eq0_17(...) metamacro_expand_ #define metamacro_if_eq0_18(...) metamacro_expand_ #define metamacro_if_eq0_19(...) metamacro_expand_ #define metamacro_if_eq0_20(...) metamacro_expand_ #define metamacro_if_eq1(VALUE) metamacro_if_eq0(metamacro_dec(VALUE)) #define metamacro_if_eq2(VALUE) metamacro_if_eq1(metamacro_dec(VALUE)) #define metamacro_if_eq3(VALUE) metamacro_if_eq2(metamacro_dec(VALUE)) #define metamacro_if_eq4(VALUE) metamacro_if_eq3(metamacro_dec(VALUE)) #define metamacro_if_eq5(VALUE) metamacro_if_eq4(metamacro_dec(VALUE)) #define metamacro_if_eq6(VALUE) metamacro_if_eq5(metamacro_dec(VALUE)) #define metamacro_if_eq7(VALUE) metamacro_if_eq6(metamacro_dec(VALUE)) #define metamacro_if_eq8(VALUE) metamacro_if_eq7(metamacro_dec(VALUE)) #define metamacro_if_eq9(VALUE) metamacro_if_eq8(metamacro_dec(VALUE)) #define metamacro_if_eq10(VALUE) metamacro_if_eq9(metamacro_dec(VALUE)) #define metamacro_if_eq11(VALUE) metamacro_if_eq10(metamacro_dec(VALUE)) #define metamacro_if_eq12(VALUE) metamacro_if_eq11(metamacro_dec(VALUE)) #define metamacro_if_eq13(VALUE) metamacro_if_eq12(metamacro_dec(VALUE)) #define metamacro_if_eq14(VALUE) metamacro_if_eq13(metamacro_dec(VALUE)) #define metamacro_if_eq15(VALUE) metamacro_if_eq14(metamacro_dec(VALUE)) #define metamacro_if_eq16(VALUE) metamacro_if_eq15(metamacro_dec(VALUE)) #define metamacro_if_eq17(VALUE) metamacro_if_eq16(metamacro_dec(VALUE)) #define metamacro_if_eq18(VALUE) metamacro_if_eq17(metamacro_dec(VALUE)) #define metamacro_if_eq19(VALUE) metamacro_if_eq18(metamacro_dec(VALUE)) #define metamacro_if_eq20(VALUE) metamacro_if_eq19(metamacro_dec(VALUE)) // metamacro_if_eq_recursive expansions #define metamacro_if_eq_recursive0(VALUE) \ metamacro_concat(metamacro_if_eq_recursive0_, VALUE) #define metamacro_if_eq_recursive0_0(...) __VA_ARGS__ metamacro_consume_ #define metamacro_if_eq_recursive0_1(...) metamacro_expand_ #define metamacro_if_eq_recursive0_2(...) metamacro_expand_ #define metamacro_if_eq_recursive0_3(...) metamacro_expand_ #define metamacro_if_eq_recursive0_4(...) metamacro_expand_ #define metamacro_if_eq_recursive0_5(...) metamacro_expand_ #define metamacro_if_eq_recursive0_6(...) metamacro_expand_ #define metamacro_if_eq_recursive0_7(...) metamacro_expand_ #define metamacro_if_eq_recursive0_8(...) metamacro_expand_ #define metamacro_if_eq_recursive0_9(...) metamacro_expand_ #define metamacro_if_eq_recursive0_10(...) metamacro_expand_ #define metamacro_if_eq_recursive0_11(...) metamacro_expand_ #define metamacro_if_eq_recursive0_12(...) metamacro_expand_ #define metamacro_if_eq_recursive0_13(...) metamacro_expand_ #define metamacro_if_eq_recursive0_14(...) metamacro_expand_ #define metamacro_if_eq_recursive0_15(...) metamacro_expand_ #define metamacro_if_eq_recursive0_16(...) metamacro_expand_ #define metamacro_if_eq_recursive0_17(...) metamacro_expand_ #define metamacro_if_eq_recursive0_18(...) metamacro_expand_ #define metamacro_if_eq_recursive0_19(...) metamacro_expand_ #define metamacro_if_eq_recursive0_20(...) metamacro_expand_ #define metamacro_if_eq_recursive1(VALUE) metamacro_if_eq_recursive0(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive2(VALUE) metamacro_if_eq_recursive1(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive3(VALUE) metamacro_if_eq_recursive2(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive4(VALUE) metamacro_if_eq_recursive3(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive5(VALUE) metamacro_if_eq_recursive4(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive6(VALUE) metamacro_if_eq_recursive5(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive7(VALUE) metamacro_if_eq_recursive6(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive8(VALUE) metamacro_if_eq_recursive7(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive9(VALUE) metamacro_if_eq_recursive8(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive10(VALUE) metamacro_if_eq_recursive9(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive11(VALUE) metamacro_if_eq_recursive10(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive12(VALUE) metamacro_if_eq_recursive11(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive13(VALUE) metamacro_if_eq_recursive12(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive14(VALUE) metamacro_if_eq_recursive13(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive15(VALUE) metamacro_if_eq_recursive14(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive16(VALUE) metamacro_if_eq_recursive15(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive17(VALUE) metamacro_if_eq_recursive16(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive18(VALUE) metamacro_if_eq_recursive17(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive19(VALUE) metamacro_if_eq_recursive18(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive20(VALUE) metamacro_if_eq_recursive19(metamacro_dec(VALUE)) // metamacro_take expansions #define metamacro_take0(...) #define metamacro_take1(...) metamacro_head(__VA_ARGS__) #define metamacro_take2(...) metamacro_head(__VA_ARGS__), metamacro_take1(metamacro_tail(__VA_ARGS__)) #define metamacro_take3(...) metamacro_head(__VA_ARGS__), metamacro_take2(metamacro_tail(__VA_ARGS__)) #define metamacro_take4(...) metamacro_head(__VA_ARGS__), metamacro_take3(metamacro_tail(__VA_ARGS__)) #define metamacro_take5(...) metamacro_head(__VA_ARGS__), metamacro_take4(metamacro_tail(__VA_ARGS__)) #define metamacro_take6(...) metamacro_head(__VA_ARGS__), metamacro_take5(metamacro_tail(__VA_ARGS__)) #define metamacro_take7(...) metamacro_head(__VA_ARGS__), metamacro_take6(metamacro_tail(__VA_ARGS__)) #define metamacro_take8(...) metamacro_head(__VA_ARGS__), metamacro_take7(metamacro_tail(__VA_ARGS__)) #define metamacro_take9(...) metamacro_head(__VA_ARGS__), metamacro_take8(metamacro_tail(__VA_ARGS__)) #define metamacro_take10(...) metamacro_head(__VA_ARGS__), metamacro_take9(metamacro_tail(__VA_ARGS__)) #define metamacro_take11(...) metamacro_head(__VA_ARGS__), metamacro_take10(metamacro_tail(__VA_ARGS__)) #define metamacro_take12(...) metamacro_head(__VA_ARGS__), metamacro_take11(metamacro_tail(__VA_ARGS__)) #define metamacro_take13(...) metamacro_head(__VA_ARGS__), metamacro_take12(metamacro_tail(__VA_ARGS__)) #define metamacro_take14(...) metamacro_head(__VA_ARGS__), metamacro_take13(metamacro_tail(__VA_ARGS__)) #define metamacro_take15(...) metamacro_head(__VA_ARGS__), metamacro_take14(metamacro_tail(__VA_ARGS__)) #define metamacro_take16(...) metamacro_head(__VA_ARGS__), metamacro_take15(metamacro_tail(__VA_ARGS__)) #define metamacro_take17(...) metamacro_head(__VA_ARGS__), metamacro_take16(metamacro_tail(__VA_ARGS__)) #define metamacro_take18(...) metamacro_head(__VA_ARGS__), metamacro_take17(metamacro_tail(__VA_ARGS__)) #define metamacro_take19(...) metamacro_head(__VA_ARGS__), metamacro_take18(metamacro_tail(__VA_ARGS__)) #define metamacro_take20(...) metamacro_head(__VA_ARGS__), metamacro_take19(metamacro_tail(__VA_ARGS__)) // metamacro_drop expansions #define metamacro_drop0(...) __VA_ARGS__ #define metamacro_drop1(...) metamacro_tail(__VA_ARGS__) #define metamacro_drop2(...) metamacro_drop1(metamacro_tail(__VA_ARGS__)) #define metamacro_drop3(...) metamacro_drop2(metamacro_tail(__VA_ARGS__)) #define metamacro_drop4(...) metamacro_drop3(metamacro_tail(__VA_ARGS__)) #define metamacro_drop5(...) metamacro_drop4(metamacro_tail(__VA_ARGS__)) #define metamacro_drop6(...) metamacro_drop5(metamacro_tail(__VA_ARGS__)) #define metamacro_drop7(...) metamacro_drop6(metamacro_tail(__VA_ARGS__)) #define metamacro_drop8(...) metamacro_drop7(metamacro_tail(__VA_ARGS__)) #define metamacro_drop9(...) metamacro_drop8(metamacro_tail(__VA_ARGS__)) #define metamacro_drop10(...) metamacro_drop9(metamacro_tail(__VA_ARGS__)) #define metamacro_drop11(...) metamacro_drop10(metamacro_tail(__VA_ARGS__)) #define metamacro_drop12(...) metamacro_drop11(metamacro_tail(__VA_ARGS__)) #define metamacro_drop13(...) metamacro_drop12(metamacro_tail(__VA_ARGS__)) #define metamacro_drop14(...) metamacro_drop13(metamacro_tail(__VA_ARGS__)) #define metamacro_drop15(...) metamacro_drop14(metamacro_tail(__VA_ARGS__)) #define metamacro_drop16(...) metamacro_drop15(metamacro_tail(__VA_ARGS__)) #define metamacro_drop17(...) metamacro_drop16(metamacro_tail(__VA_ARGS__)) #define metamacro_drop18(...) metamacro_drop17(metamacro_tail(__VA_ARGS__)) #define metamacro_drop19(...) metamacro_drop18(metamacro_tail(__VA_ARGS__)) #define metamacro_drop20(...) metamacro_drop19(metamacro_tail(__VA_ARGS__)) ================================================ FILE: Pods/SDWebImage/LICENSE ================================================ Copyright (c) 2009-2020 Olivier Poitrey rs@dailymotion.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Pods/SDWebImage/README.md ================================================

[![Build Status](https://github.com/SDWebImage/SDWebImage/actions/workflows/CI.yml/badge.svg)](https://github.com/SDWebImage/SDWebImage/actions/workflows/CI.yml) [![Pod Version](http://img.shields.io/cocoapods/v/SDWebImage.svg?style=flat)](http://cocoadocs.org/docsets/SDWebImage/) [![Pod Platform](http://img.shields.io/cocoapods/p/SDWebImage.svg?style=flat)](http://cocoadocs.org/docsets/SDWebImage/) [![Pod License](http://img.shields.io/cocoapods/l/SDWebImage.svg?style=flat)](https://www.apache.org/licenses/LICENSE-2.0.html) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg)](https://github.com/SDWebImage/SDWebImage) [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://swift.org/package-manager/) [![Mac Catalyst compatible](https://img.shields.io/badge/Catalyst-compatible-brightgreen.svg)](https://developer.apple.com/documentation/xcode/creating_a_mac_version_of_your_ipad_app/) [![codecov](https://codecov.io/gh/SDWebImage/SDWebImage/branch/master/graph/badge.svg)](https://codecov.io/gh/SDWebImage/SDWebImage) This library provides an async image downloader with cache support. For convenience, we added categories for UI elements like `UIImageView`, `UIButton`, `MKAnnotationView`. > 💡NOTE: `SD` is the prefix for **Simple Design** (which is the team name in Daily Motion company from the author Olivier Poitrey) ## Features - [x] Categories for `UIImageView`, `UIButton`, `MKAnnotationView` adding web image and cache management - [x] An asynchronous image downloader - [x] An asynchronous memory + disk image caching with automatic cache expiration handling - [x] A background image decompression to avoid frame rate drop - [x] [Progressive image loading](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#progressive-animation) (including animated image, like GIF showing in Web browser) - [x] [Thumbnail image decoding](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#thumbnail-decoding-550) to save CPU && Memory for large images - [x] [Extendable image coder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#custom-coder-420) to support massive image format, like WebP - [x] [Full-stack solution for animated images](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) which keep a balance between CPU && Memory - [x] [Customizable and composable transformations](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#transformer-50) can be applied to the images right after download - [x] [Customizable and multiple caches system](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#custom-cache-50) - [x] [Customizable and multiple loaders system](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#custom-loader-50) to expand the capabilities, like [Photos Library](https://github.com/SDWebImage/SDWebImagePhotosPlugin) - [x] [Image loading indicators](https://github.com/SDWebImage/SDWebImage/wiki/How-to-use#use-view-indicator-50) - [x] [Image loading transition animation](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#image-transition-430) - [x] A guarantee that the same URL won't be downloaded several times - [x] A guarantee that bogus URLs won't be retried again and again - [x] A guarantee that main thread will never be blocked - [x] Modern Objective-C and better Swift support - [x] Performances! ## For Apple visionOS From 5.19+, SDWebImage supports visionOS on all Package Managers (include CocoaPods/Carthage/SPM). Upgrade the related tools if you're facing issues. For 5.18+, SDWebImage can be compiled for visionOS platform. However, it's still in beta and may contains issues unlike the stable iOS UIKit support. Welcome to have a try and [report issue](https://github.com/SDWebImage/SDWebImage/issues). To build on visionOS, currently we only support the standard Xcode integration. See `Installation with Swift Package Manager` and `Manual Installation Guide` below. ## Supported Image Formats - Image formats supported by Apple system (JPEG, PNG, TIFF, BMP, ...), including [GIF](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#gif-coder)/[APNG](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#apng-coder) animated image - HEIC format from iOS 11/macOS 10.13, including animated HEIC from iOS 13/macOS 10.15 via [SDWebImageHEICCoder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#heic-coder). For lower firmware, use coder plugin [SDWebImageHEIFCoder](https://github.com/SDWebImage/SDWebImageHEIFCoder) - WebP format from iOS 14/macOS 11.0 via [SDWebImageAWebPCoder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#awebp-coder). For lower firmware, use coder plugin [SDWebImageWebPCoder](https://github.com/SDWebImage/SDWebImageWebPCoder) - JPEG-XL format from iOS 17/macOS 14.0 built-in. For lower firmware, use coder plugin [SDWebImageJPEGXLCoder](https://github.com/SDWebImage/SDWebImageJPEGXLCoder) - Support extendable coder plugins for new image formats like BPG, AVIF. And vector format like PDF, SVG. See all the list in [Image coder plugin List](https://github.com/SDWebImage/SDWebImage/wiki/Coder-Plugin-List) > 💡NOTE: For new user SDWebImage use [Coder Plugin System](https://github.com/SDWebImage/SDWebImage/wiki/Coder-Plugin-List) to support both Apple's built-in and external image format. For static image we always use Apple's built-in as fallback, but not for animated image. Currently (updated to 5.19.x version) we only register traditional animated format like GIF/APNG by default, without the modern format like AWebP/HEICS/AVIF, even on the latest firmware. If you want these animated image format support, simply register by yourself with one-line code, see more in [WebP Coder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#awebp-coder) and [HEIC Coder](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#heic-coder) In future we will change this behavior by always registering all Apple's built-in animated image format, to make it easy for new user to integrate. ## Additional modules and Ecosystem In order to keep SDWebImage focused and limited to the core features, but also allow extensibility and custom behaviors, during the 5.0 refactoring we focused on modularizing the library. As such, we have moved/built new modules to [SDWebImage org](https://github.com/SDWebImage). #### SwiftUI [SwiftUI](https://developer.apple.com/xcode/swiftui/) is an innovative UI framework written in Swift to build user interfaces across all Apple platforms. We support SwiftUI by building a brand new framework called [SDWebImageSwiftUI](https://github.com/SDWebImage/SDWebImageSwiftUI), which is built on top of SDWebImage core functions (caching, loading and animation). The new framework introduce two View structs `WebImage` and `AnimatedImage` for SwiftUI world, `ImageIndicator` modifier for any View, `ImageManager` observable object for data source. Supports iOS 13+/macOS 10.15+/tvOS 13+/watchOS 6+ and Swift 5.1. Have a nice try and provide feedback! #### Coders for additional image formats - [SDWebImageWebPCoder](https://github.com/SDWebImage/SDWebImageWebPCoder) - coder for WebP format. iOS 9+/macOS 10.11+. Based on [libwebp](https://chromium.googlesource.com/webm/libwebp) - [SDWebImageHEIFCoder](https://github.com/SDWebImage/SDWebImageHEIFCoder) - coder for HEIF format, iOS 9+/macOS 10.11+ support. Based on [libheif](https://github.com/strukturag/libheif) - [SDWebImageBPGCoder](https://github.com/SDWebImage/SDWebImageBPGCoder) - coder for BPG format. Based on [libbpg](https://github.com/mirrorer/libbpg) - [SDWebImageFLIFCoder](https://github.com/SDWebImage/SDWebImageFLIFCoder) - coder for FLIF format. Based on [libflif](https://github.com/FLIF-hub/FLIF) - [SDWebImageAVIFCoder](https://github.com/SDWebImage/SDWebImageAVIFCoder) - coder for AVIF (AV1-based) format. Based on [libavif](https://github.com/AOMediaCodec/libavif) - [SDWebImagePDFCoder](https://github.com/SDWebImage/SDWebImagePDFCoder) - coder for PDF vector format. Using built-in frameworks - [SDWebImageSVGCoder](https://github.com/SDWebImage/SDWebImageSVGCoder) - coder for SVG vector format. Using built-in frameworks - [SDWebImageSVGNativeCoder](https://github.com/SDWebImage/SDWebImageSVGNativeCoder) - coder for SVG-Native vector format. Based on [svg-native](https://github.com/adobe/svg-native-viewer) - [SDWebImageLottieCoder](https://github.com/SDWebImage/SDWebImageLottieCoder) - coder for Lottie animation format. Based on [rlottie](https://github.com/Samsung/rlottie) - [SDWebImageJPEGXLCoder](https://github.com/SDWebImage/SDWebImageJPEGXLCoder) - coder for JPEG-XL format. iOS 9+/macOS 10.11+. Based on [libjxl](https://github.com/libjxl/libjxl) - and more from community! #### Custom Caches - [SDWebImageYYPlugin](https://github.com/SDWebImage/SDWebImageYYPlugin) - plugin to support caching images with [YYCache](https://github.com/ibireme/YYCache) - [SDWebImagePINPlugin](https://github.com/SDWebImage/SDWebImagePINPlugin) - plugin to support caching images with [PINCache](https://github.com/pinterest/PINCache) #### Custom Loaders - [SDWebImagePhotosPlugin](https://github.com/SDWebImage/SDWebImagePhotosPlugin) - plugin to support loading images from Photos (using `Photos.framework`) - [SDWebImageLinkPlugin](https://github.com/SDWebImage/SDWebImageLinkPlugin) - plugin to support loading images from rich link url, as well as `LPLinkView` (using `LinkPresentation.framework`) #### Integration with 3rd party libraries - [SDWebImageLottiePlugin](https://github.com/SDWebImage/SDWebImageLottiePlugin) - plugin to support [Lottie-iOS](https://github.com/airbnb/lottie-ios), vector animation rending with remote JSON files - [SDWebImageSVGKitPlugin](https://github.com/SDWebImage/SDWebImageSVGKitPlugin) - plugin to support [SVGKit](https://github.com/SVGKit/SVGKit), SVG rendering using Core Animation, iOS 9+/macOS 10.11+ support - [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin) - plugin to support [FLAnimatedImage](https://github.com/Flipboard/FLAnimatedImage) as the engine for animated GIFs - [SDWebImageYYPlugin](https://github.com/SDWebImage/SDWebImageYYPlugin) - plugin to integrate [YYImage](https://github.com/ibireme/YYImage) & [YYCache](https://github.com/ibireme/YYCache) for image rendering & caching #### Community driven popular libraries - [FirebaseUI](https://github.com/firebase/FirebaseUI-iOS) - Firebase Storage binding for query images, based on SDWebImage loader system - [react-native-fast-image](https://github.com/DylanVann/react-native-fast-image) - React Native fast image component, based on SDWebImage Animated Image solution - [flutter_image_compress](https://github.com/OpenFlutter/flutter_image_compress) - Flutter compresses image plugin, based on SDWebImage WebP coder plugin #### Make our lives easier - [libwebp-Xcode](https://github.com/SDWebImage/libwebp-Xcode) - A wrapper for [libwebp](https://chromium.googlesource.com/webm/libwebp) + an Xcode project. - [libheif-Xcode](https://github.com/SDWebImage/libheif-Xcode) - A wrapper for [libheif](https://github.com/strukturag/libheif) + an Xcode project. - [libavif-Xcode](https://github.com/SDWebImage/libavif-Xcode) - A wrapper for [libavif](https://github.com/AOMediaCodec/libavif) + an Xcode project. - and more third-party C/C++ image codec libraries with CocoaPods/Carthage/SwiftPM support. You can use those directly, or create similar components of your own, by using the customizable architecture of SDWebImage. ## Requirements - iOS 9.0 or later - tvOS 9.0 or later - watchOS 2.0 or later - macOS 10.11 or later (10.15 for Catalyst) - visionOS 1.0 or later - Xcode 15.0 or later #### Backwards compatibility - For iOS 8, macOS 10.10 or Xcode < 11, use [any 5.x version up to 5.9.5](https://github.com/SDWebImage/SDWebImage/releases/tag/5.9.5) - For iOS 7, macOS 10.9 or Xcode < 8, use [any 4.x version up to 4.4.6](https://github.com/SDWebImage/SDWebImage/releases/tag/4.4.6) - For macOS 10.8, use [any 4.x version up to 4.3.0](https://github.com/SDWebImage/SDWebImage/releases/tag/4.3.0) - For iOS 5 and 6, use [any 3.x version up to 3.7.6](https://github.com/SDWebImage/SDWebImage/releases/tag/3.7.6) - For iOS < 5.0, please use the last [2.0 version](https://github.com/SDWebImage/SDWebImage/tree/2.0-compat). ## Getting Started - Read this Readme doc - Read the [How to use section](https://github.com/SDWebImage/SDWebImage#how-to-use) - Read the [Latest Documentation](https://sdwebimage.github.io/) and [CocoaDocs for old version](http://cocoadocs.org/docsets/SDWebImage/) - Try the example by downloading the project from Github or even easier using CocoaPods try `pod try SDWebImage` - Read the [Installation Guide](https://github.com/SDWebImage/SDWebImage/wiki/Installation-Guide) - Read the [SDWebImage 5.0 Migration Guide](https://github.com/SDWebImage/SDWebImage/blob/master/Docs/SDWebImage-5.0-Migration-guide.md) to get an idea of the changes from 4.x to 5.x - Read the [SDWebImage 4.0 Migration Guide](https://github.com/SDWebImage/SDWebImage/blob/master/Docs/SDWebImage-4.0-Migration-guide.md) to get an idea of the changes from 3.x to 4.x - Read the [Common Problems](https://github.com/SDWebImage/SDWebImage/wiki/Common-Problems) to find the solution for common problems - Go to the [Wiki Page](https://github.com/SDWebImage/SDWebImage/wiki) for more information such as [Advanced Usage](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage) ## Who Uses It - Find out [who uses SDWebImage](https://github.com/SDWebImage/SDWebImage/wiki/Who-Uses-SDWebImage) and add your app to the list. ## Communication - If you **need help**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/sdwebimage). (Tag 'sdwebimage') - If you'd like to **ask a general question**, use [Stack Overflow](http://stackoverflow.com/questions/tagged/sdwebimage). - If you **found a bug**, open an issue. - If you **have a feature request**, open an issue. - If you **need IRC channel**, use [Gitter](https://gitter.im/SDWebImage/community). ## Contribution - If you **want to contribute**, read the [Contributing Guide](https://github.com/SDWebImage/SDWebImage/blob/master/.github/CONTRIBUTING.md) - For **development contribution guide**, read the [How-To-Contribute](https://github.com/SDWebImage/SDWebImage/wiki/How-to-Contribute) - For **understanding code architecture**, read the [Code Architecture Analysis](https://github.com/SDWebImage/SDWebImage/wiki/5.6-Code-Architecture-Analysis) ## How To Use * Objective-C ```objective-c #import ... [imageView sd_setImageWithURL:[NSURL URLWithString:@"http://www.domain.com/path/to/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder.png"]]; ``` * Swift ```swift import SDWebImage imageView.sd_setImage(with: URL(string: "http://www.domain.com/path/to/image.jpg"), placeholderImage: UIImage(named: "placeholder.png")) ``` - For details about how to use the library and clear examples, see [The detailed How to use](https://github.com/SDWebImage/SDWebImage/blob/master/Docs/HowToUse.md) ## Animated Images (GIF) support In 5.0, we introduced a brand new mechanism for supporting animated images. This includes animated image loading, rendering, decoding, and also supports customizations (for advanced users). This animated image solution is available for `iOS`/`tvOS`/`macOS`. The `SDAnimatedImage` is subclass of `UIImage/NSImage`, and `SDAnimatedImageView` is subclass of `UIImageView/NSImageView`, to make them compatible with the common frameworks APIs. The `SDAnimatedImageView` supports the familiar image loading category methods, works like drop-in replacement for `UIImageView/NSImageView`. Don't have `UIView` (like `WatchKit` or `CALayer`)? you can still use `SDAnimatedPlayer` the player engine for advanced playback and rendering. See [Animated Image](https://github.com/SDWebImage/SDWebImage/wiki/Advanced-Usage#animated-image-50) for more detailed information. * Objective-C ```objective-c SDAnimatedImageView *imageView = [SDAnimatedImageView new]; SDAnimatedImage *animatedImage = [SDAnimatedImage imageNamed:@"image.gif"]; imageView.image = animatedImage; ``` * Swift ```swift let imageView = SDAnimatedImageView() let animatedImage = SDAnimatedImage(named: "image.gif") imageView.image = animatedImage ``` #### FLAnimatedImage integration has its own dedicated repo In order to clean up things and make our core project do less things, we decided that the `FLAnimatedImage` integration does not belong here. From 5.0, this will still be available, but under a dedicated repo [SDWebImageFLPlugin](https://github.com/SDWebImage/SDWebImageFLPlugin). ## Installation There are 5 ways to use SDWebImage in your project: - using CocoaPods - using Carthage - using Swift Package Manager - download binary XCFramework - manual install (build frameworks or embed Xcode Project) ### Installation with CocoaPods [CocoaPods](http://cocoapods.org/) is a dependency manager for Objective-C, which automates and simplifies the process of using 3rd-party libraries in your projects. See the [Get Started](http://cocoapods.org/#get_started) section for more details. #### Podfile ``` platform :ios, '8.0' pod 'SDWebImage', '~> 5.0' ``` ##### Swift and static framework Swift project previously had to use `use_frameworks!` to make all Pods into dynamic framework to let CocoaPods work. However, starting with `CocoaPods 1.5.0+` (with `Xcode 9+`), which supports to build both Objective-C && Swift code into static framework. You can use modular headers to use SDWebImage as static framework, without the need of `use_frameworks!`: ``` platform :ios, '8.0' # Uncomment the next line when you want all Pods as static framework # use_modular_headers! pod 'SDWebImage', :modular_headers => true ``` See more on [CocoaPods 1.5.0 — Swift Static Libraries](http://blog.cocoapods.org/CocoaPods-1.5.0/) If not, you still need to add `use_frameworks!` to use SDWebImage as dynamic framework: ``` platform :ios, '8.0' use_frameworks! pod 'SDWebImage' ``` #### Subspecs There are 2 subspecs available now: `Core` and `MapKit` (this means you can install only some of the SDWebImage modules. By default, you get just `Core`, so if you need `MapKit`, you need to specify it). Podfile example: ``` pod 'SDWebImage/MapKit' ``` ### Installation with Carthage [Carthage](https://github.com/Carthage/Carthage) is a lightweight dependency manager for Swift and Objective-C. It leverages CocoaTouch modules and is less invasive than CocoaPods. To install with carthage, follow the instruction on [Carthage](https://github.com/Carthage/Carthage) Carthage users can point to this repository and use whichever generated framework they'd like: SDWebImage, SDWebImageMapKit or both. Make the following entry in your Cartfile: `github "SDWebImage/SDWebImage"` Then run `carthage update` If this is your first time using Carthage in the project, you'll need to go through some additional steps as explained [over at Carthage](https://github.com/Carthage/Carthage#adding-frameworks-to-an-application). > 💡NOTE: At this time, Carthage does not provide a way to build only specific repository subcomponents (or equivalent of CocoaPods's subspecs). All components and their dependencies will be built with the above command. However, you don't need to copy frameworks you aren't using into your project. For instance, if you aren't using `SDWebImageMapKit`, feel free to delete that framework from the Carthage Build directory after `carthage update` completes. > 💡NOTE: [Apple requires SDWebImage contains signatures](https://developer.apple.com/support/third-party-SDK-requirements/). So, by default the `carthage build` binary framework does not do codesign, this will cause validation error. You can sign yourself with the Apple Developer Program identity, or using the binary framework: ``` binary "https://github.com/SDWebImage/SDWebImage/raw/master/SDWebImage.json" ``` ### Installation with Swift Package Manager (Xcode 11+) [Swift Package Manager](https://swift.org/package-manager/) (SwiftPM) is a tool for managing the distribution of Swift code as well as C-family dependency. From Xcode 11, SwiftPM got natively integrated with Xcode. SDWebImage support SwiftPM from version 5.1.0. To use SwiftPM, you should use Xcode 11 to open your project. Click `File` -> `Swift Packages` -> `Add Package Dependency`, enter [SDWebImage repo's URL](https://github.com/SDWebImage/SDWebImage.git). Or you can login Xcode with your GitHub account and just type `SDWebImage` to search. After select the package, you can choose the dependency type (tagged version, branch or commit). Then Xcode will setup all the stuff for you. If you're a framework author and use SDWebImage as a dependency, update your `Package.swift` file: ```swift let package = Package( // 5.1.0 ..< 6.0.0 dependencies: [ .package(url: "https://github.com/SDWebImage/SDWebImage.git", from: "5.1.0") ], // ... ) ``` ### Download binary XCFramework From 5.19.2, SDWebImage provide the canonical official binary XCFramework on [GitHub release pages](https://github.com/SDWebImage/SDWebImage/releases). + Download XCFramework You can choose to download `SDWebImage-dynamic.xcframework.zip` for dynamic linked one, or `SDWebImage-static.xcframework.zip` for static-linked one. + Integrate to Xcode Project Drag the unzipped `.xcframework` into your Xcode Project's Framework tab. + Verify signature of binary XCFramework From Xcode 15 Apple will verify the signature of binary XCFramework, to avoid supply chain attack. The fingerprint currently should be `FC 3B 10 13 86 34 4C 50 DB 70 2A 9A D1 01 6F B5 1A 3E CC 8B 9D A9 B7 AE 47 A0 48 D4 D0 63 39 83` The certificate is stored in the repo [here](https://github.com/SDWebImage/SDWebImage/blob/master/Certificate/SDWebImage%20Signing%20Certificate.cer) The public key is stored in the repo [here](https://github.com/SDWebImage/SDWebImage/blob/master/Certificate/SDWebImage%20Signing%20Certificate.pem) See more: [Verifying the origin of your XCFrameworks](https://developer.apple.com/documentation/Xcode/verifying-the-origin-of-your-xcframeworks) ### Manual Installation Guide + Check your command line Xcode version ``` sudo xcode-select -s /path/to/Xcode.app ``` or ``` export DEVELOPER_DIR=/path/to/Xcode.app/Contents/Developer ``` + Run the script to build frameworks ``` ./Scripts/build-frameworks.sh ``` + Run the script to merge XCFramework ``` ./Scripts/create-xcframework.sh ``` + Use your own certificate to sign XCFramework ``` // https://developer.apple.com/support/third-party-SDK-requirements/ codesign --timestamp -v --sign "your own certificate" SDWebImage.xcframework ``` See more on wiki: [Manual install Guide](https://github.com/SDWebImage/SDWebImage/wiki/Installation-Guide#manual-installation-guide) ### Import headers in your source files In the source files where you need to use the library, import the umbrella header file: ```objective-c #import ``` It's also recommend to use the module import syntax, available for CocoaPods(enable `modular_headers`)/Carthage/SwiftPM. ```objecitivec @import SDWebImage; ``` ### Build Project At this point your workspace should build without error. If you are having problem, post to the Issue and the community can help you solve it. ## Data Collection Practices From Xcode 15, we provide the new `PrivacyInfo.xcprivacy` file for privacy details, see [Describing data use in privacy manifests](https://developer.apple.com/documentation/bundleresources/privacy_manifest_files/describing_data_use_in_privacy_manifests?language=objc) You can exports the privacy report after archive your App by integrate SDWebImage via SwiftPM/XCFramework or CocoaPods (`use_frameworks` set to true). For old version or if you're using static ar archive, as required by the [App privacy details on the App Store](https://developer.apple.com/app-store/app-privacy-details/), here's SDWebImage's list of [Data Collection Practices](https://sdwebimage.github.io/DataCollection/index.html). ## Author - [Olivier Poitrey](https://github.com/rs) ## Collaborators - [Konstantinos K.](https://github.com/mythodeia) - [Bogdan Poplauschi](https://github.com/bpoplauschi) - [Chester Liu](https://github.com/skyline75489) - [DreamPiggy](https://github.com/dreampiggy) - [Wu Zhong](https://github.com/zhongwuzw) ## Credits Thank you to all the people who have already contributed to SDWebImage. [![Contributors](https://opencollective.com/SDWebImage/contributors.svg?width=890)](https://github.com/SDWebImage/SDWebImage/graphs/contributors) ## Licenses All source code is licensed under the [MIT License](https://github.com/SDWebImage/SDWebImage/blob/master/LICENSE). ## Architecture To learn about SDWebImage's architecture design for contribution, read [The Core of SDWebImage v5.6 Architecture](https://github.com/SDWebImage/SDWebImage/wiki/5.6-Code-Architecture-Analysis). Thanks @looseyi for the post and translation. #### High Level Diagram

#### Overall Class Diagram

#### Top Level API Diagram

#### Main Sequence Diagram

#### More detailed diagrams - [Manager API Diagram](https://raw.githubusercontent.com/SDWebImage/SDWebImage/master/Docs/Diagrams/SDWebImageManagerClassDiagram.png) - [Coders API Diagram](https://raw.githubusercontent.com/SDWebImage/SDWebImage/master/Docs/Diagrams/SDWebImageCodersClassDiagram.png) - [Loader API Diagram](https://raw.githubusercontent.com/SDWebImage/SDWebImage/master/Docs/Diagrams/SDWebImageLoaderClassDiagram.png) - [Cache API Diagram](https://raw.githubusercontent.com/SDWebImage/SDWebImage/master/Docs/Diagrams/SDWebImageCacheClassDiagram.png) ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/NSButton+WebCache.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_MAC #import "SDWebImageManager.h" /** * Integrates SDWebImage async downloading and caching of remote images with NSButton. */ @interface NSButton (WebCache) #pragma mark - Image /** * Get the current image URL. */ @property (nonatomic, strong, readonly, nullable) NSURL *sd_currentImageURL; /** * Set the button `image` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. */ - (void)sd_setImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT; /** * Set the button `image` with an `url` and a placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @see sd_setImageWithURL:placeholderImage:options: */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT; /** * Set the button `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT; /** * Set the button `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; /** * Set the button `image` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `image` with an `url`, placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT; /** * Set the button `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; #pragma mark - Alternate Image /** * Get the current alternateImage URL. */ @property (nonatomic, strong, readonly, nullable) NSURL *sd_currentAlternateImageURL; /** * Set the button `alternateImage` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the alternateImage. */ - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT; /** * Set the button `alternateImage` with an `url` and a placeholder. * * The download is asynchronous and cached. * * @param url The url for the alternateImage. * @param placeholder The alternateImage to be set initially, until the alternateImage request finishes. * @see sd_setAlternateImageWithURL:placeholderImage:options: */ - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT; /** * Set the button `alternateImage` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the alternateImage. * @param placeholder The alternateImage to be set initially, until the alternateImage request finishes. * @param options The options to use when downloading the alternateImage. @see SDWebImageOptions for the possible values. */ - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT; /** * Set the button `alternateImage` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the alternateImage. * @param placeholder The alternateImage to be set initially, until the alternateImage request finishes. * @param options The options to use when downloading the alternateImage. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. */ - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; /** * Set the button `alternateImage` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the alternateImage. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the alternateImage parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the alternateImage was retrieved from the local cache or from the network. * The fourth parameter is the original alternateImage url. */ - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `alternateImage` with an `url`, placeholder. * * The download is asynchronous and cached. * * @param url The url for the alternateImage. * @param placeholder The alternateImage to be set initially, until the alternateImage request finishes. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the alternateImage parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the alternateImage was retrieved from the local cache or from the network. * The fourth parameter is the original alternateImage url. */ - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT; /** * Set the button `alternateImage` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the alternateImage. * @param placeholder The alternateImage to be set initially, until the alternateImage request finishes. * @param options The options to use when downloading the alternateImage. @see SDWebImageOptions for the possible values. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the alternateImage parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the alternateImage was retrieved from the local cache or from the network. * The fourth parameter is the original alternateImage url. */ - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `alternateImage` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the alternateImage. * @param placeholder The alternateImage to be set initially, until the alternateImage request finishes. * @param options The options to use when downloading the alternateImage. @see SDWebImageOptions for the possible values. * @param progressBlock A block called while alternateImage is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the alternateImage parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the alternateImage was retrieved from the local cache or from the network. * The fourth parameter is the original alternateImage url. */ - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `alternateImage` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the alternateImage. * @param placeholder The alternateImage to be set initially, until the alternateImage request finishes. * @param options The options to use when downloading the alternateImage. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called while alternateImage is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the alternateImage parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the alternateImage was retrieved from the local cache or from the network. * The fourth parameter is the original alternateImage url. */ - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; #pragma mark - Cancel /** * Cancel the current image download */ - (void)sd_cancelCurrentImageLoad; /** * Cancel the current alternateImage download */ - (void)sd_cancelCurrentAlternateImageLoad; @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/NSButton+WebCache.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "NSButton+WebCache.h" #if SD_MAC #import "objc/runtime.h" #import "UIView+WebCacheOperation.h" #import "UIView+WebCacheState.h" #import "UIView+WebCache.h" #import "SDInternalMacros.h" @implementation NSButton (WebCache) #pragma mark - Image - (void)sd_setImageWithURL:(nullable NSURL *)url { [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options { [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:context progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:nil progress:progressBlock completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options context:context setImageBlock:nil progress:progressBlock completed:^(NSImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { if (completedBlock) { completedBlock(image, error, cacheType, imageURL); } }]; } #pragma mark - Alternate Image - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url { [self sd_setAlternateImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil]; } - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder { [self sd_setAlternateImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil]; } - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options { [self sd_setAlternateImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil]; } - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { [self sd_setAlternateImageWithURL:url placeholderImage:placeholder options:options context:context progress:nil completed:nil]; } - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setAlternateImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock]; } - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setAlternateImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock]; } - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setAlternateImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock]; } - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setAlternateImageWithURL:url placeholderImage:placeholder options:options context:nil progress:progressBlock completed:completedBlock]; } - (void)sd_setAlternateImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { SDWebImageMutableContext *mutableContext; if (context) { mutableContext = [context mutableCopy]; } else { mutableContext = [NSMutableDictionary dictionary]; } mutableContext[SDWebImageContextSetImageOperationKey] = @keypath(self, alternateImage); @weakify(self); [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options context:mutableContext setImageBlock:^(NSImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { @strongify(self); self.alternateImage = image; } progress:progressBlock completed:^(NSImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { if (completedBlock) { completedBlock(image, error, cacheType, imageURL); } }]; } #pragma mark - Cancel - (void)sd_cancelCurrentImageLoad { [self sd_cancelImageLoadOperationWithKey:nil]; } - (void)sd_cancelCurrentAlternateImageLoad { [self sd_cancelImageLoadOperationWithKey:@keypath(self, alternateImage)]; } #pragma mark - State - (NSURL *)sd_currentImageURL { return [self sd_imageLoadStateForKey:nil].url; } #pragma mark - Alternate State - (NSURL *)sd_currentAlternateImageURL { return [self sd_imageLoadStateForKey:@keypath(self, alternateImage)].url; } @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Fabrice Aneche * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /** You can use switch case like normal enum. It's also recommended to add a default case. You should not assume anything about the raw value. For custom coder plugin, it can also extern the enum for supported format. See `SDImageCoder` for more detailed information. */ typedef NSInteger SDImageFormat NS_TYPED_EXTENSIBLE_ENUM; static const SDImageFormat SDImageFormatUndefined = -1; static const SDImageFormat SDImageFormatJPEG = 0; static const SDImageFormat SDImageFormatPNG = 1; static const SDImageFormat SDImageFormatGIF = 2; static const SDImageFormat SDImageFormatTIFF = 3; static const SDImageFormat SDImageFormatWebP = 4; static const SDImageFormat SDImageFormatHEIC = 5; static const SDImageFormat SDImageFormatHEIF = 6; static const SDImageFormat SDImageFormatPDF = 7; static const SDImageFormat SDImageFormatSVG = 8; static const SDImageFormat SDImageFormatBMP = 9; static const SDImageFormat SDImageFormatRAW = 10; /** NSData category about the image content type and UTI. */ @interface NSData (ImageContentType) /** * Return image format * * @param data the input image data * * @return the image format as `SDImageFormat` (enum) */ + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data; /** * Convert SDImageFormat to UTType * * @param format Format as SDImageFormat * @return The UTType as CFStringRef * @note For unknown format, `kSDUTTypeImage` abstract type will return */ + (nonnull CFStringRef)sd_UTTypeFromImageFormat:(SDImageFormat)format CF_RETURNS_NOT_RETAINED NS_SWIFT_NAME(sd_UTType(from:)); /** * Convert UTType to SDImageFormat * * @param uttype The UTType as CFStringRef * @return The Format as SDImageFormat * @note For unknown type, `SDImageFormatUndefined` will return */ + (SDImageFormat)sd_imageFormatFromUTType:(nonnull CFStringRef)uttype; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/NSData+ImageContentType.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Fabrice Aneche * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "NSData+ImageContentType.h" #if SD_MAC #import #else #import #endif #import "SDImageIOAnimatedCoderInternal.h" #define kSVGTagEnd @"" @implementation NSData (ImageContentType) + (SDImageFormat)sd_imageFormatForImageData:(nullable NSData *)data { if (!data) { return SDImageFormatUndefined; } // File signatures table: http://www.garykessler.net/library/file_sigs.html uint8_t c; [data getBytes:&c length:1]; switch (c) { case 0xFF: return SDImageFormatJPEG; case 0x89: return SDImageFormatPNG; case 0x47: return SDImageFormatGIF; case 0x49: case 0x4D: return SDImageFormatTIFF; case 0x42: return SDImageFormatBMP; case 0x52: { if (data.length >= 12) { //RIFF....WEBP NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding]; if ([testString hasPrefix:@"RIFF"] && [testString hasSuffix:@"WEBP"]) { return SDImageFormatWebP; } } break; } case 0x00: { if (data.length >= 12) { //....ftypheic ....ftypheix ....ftyphevc ....ftyphevx NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(4, 8)] encoding:NSASCIIStringEncoding]; if ([testString isEqualToString:@"ftypheic"] || [testString isEqualToString:@"ftypheix"] || [testString isEqualToString:@"ftyphevc"] || [testString isEqualToString:@"ftyphevx"]) { return SDImageFormatHEIC; } //....ftypmif1 ....ftypmsf1 if ([testString isEqualToString:@"ftypmif1"] || [testString isEqualToString:@"ftypmsf1"]) { return SDImageFormatHEIF; } } break; } case 0x25: { if (data.length >= 4) { //%PDF NSString *testString = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(1, 3)] encoding:NSASCIIStringEncoding]; if ([testString isEqualToString:@"PDF"]) { return SDImageFormatPDF; } } break; } case 0x3C: { // Check end with SVG tag if ([data rangeOfData:[kSVGTagEnd dataUsingEncoding:NSUTF8StringEncoding] options:NSDataSearchBackwards range: NSMakeRange(data.length - MIN(100, data.length), MIN(100, data.length))].location != NSNotFound) { return SDImageFormatSVG; } break; } } return SDImageFormatUndefined; } + (nonnull CFStringRef)sd_UTTypeFromImageFormat:(SDImageFormat)format { CFStringRef UTType; switch (format) { case SDImageFormatJPEG: UTType = kSDUTTypeJPEG; break; case SDImageFormatPNG: UTType = kSDUTTypePNG; break; case SDImageFormatGIF: UTType = kSDUTTypeGIF; break; case SDImageFormatTIFF: UTType = kSDUTTypeTIFF; break; case SDImageFormatWebP: UTType = kSDUTTypeWebP; break; case SDImageFormatHEIC: UTType = kSDUTTypeHEIC; break; case SDImageFormatHEIF: UTType = kSDUTTypeHEIF; break; case SDImageFormatPDF: UTType = kSDUTTypePDF; break; case SDImageFormatSVG: UTType = kSDUTTypeSVG; break; case SDImageFormatBMP: UTType = kSDUTTypeBMP; break; case SDImageFormatRAW: UTType = kSDUTTypeRAW; break; default: // default is kUTTypeImage abstract type UTType = kSDUTTypeImage; break; } return UTType; } + (SDImageFormat)sd_imageFormatFromUTType:(CFStringRef)uttype { if (!uttype) { return SDImageFormatUndefined; } SDImageFormat imageFormat; if (CFStringCompare(uttype, kSDUTTypeJPEG, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatJPEG; } else if (CFStringCompare(uttype, kSDUTTypePNG, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatPNG; } else if (CFStringCompare(uttype, kSDUTTypeGIF, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatGIF; } else if (CFStringCompare(uttype, kSDUTTypeTIFF, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatTIFF; } else if (CFStringCompare(uttype, kSDUTTypeWebP, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatWebP; } else if (CFStringCompare(uttype, kSDUTTypeHEIC, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatHEIC; } else if (CFStringCompare(uttype, kSDUTTypeHEIF, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatHEIF; } else if (CFStringCompare(uttype, kSDUTTypePDF, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatPDF; } else if (CFStringCompare(uttype, kSDUTTypeSVG, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatSVG; } else if (CFStringCompare(uttype, kSDUTTypeBMP, 0) == kCFCompareEqualTo) { imageFormat = SDImageFormatBMP; } else if (UTTypeConformsTo(uttype, kSDUTTypeRAW)) { imageFormat = SDImageFormatRAW; } else { imageFormat = SDImageFormatUndefined; } return imageFormat; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/NSImage+Compatibility.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_MAC /** This category is provided to easily write cross-platform(AppKit/UIKit) code. For common usage, see `UIImage+Metadata.h`. */ @interface NSImage (Compatibility) /** The underlying Core Graphics image object. This will actually use `CGImageForProposedRect` with the image size. */ @property (nonatomic, readonly, nullable) CGImageRef CGImage; /** The underlying Core Image data. This will actually use `bestRepresentationForRect` with the image size to find the `NSCIImageRep`. */ @property (nonatomic, readonly, nullable) CIImage *CIImage; /** The scale factor of the image. This wil actually use `bestRepresentationForRect` with image size and pixel size to calculate the scale factor. If failed, use the default value 1.0. Should be greater than or equal to 1.0. */ @property (nonatomic, readonly) CGFloat scale; // These are convenience methods to make AppKit's `NSImage` match UIKit's `UIImage` behavior. The scale factor should be greater than or equal to 1.0. /** Returns an image object with the scale factor and orientation. The representation is created from the Core Graphics image object. @note The difference between this and `initWithCGImage:size` is that `initWithCGImage:size` will actually create a `NSCGImageSnapshotRep` representation and always use `backingScaleFactor` as scale factor. So we should avoid it and use `NSBitmapImageRep` with `initWithCGImage:` instead. @note The difference between this and UIKit's `UIImage` equivalent method is the way to process orientation. If the provided image orientation is not equal to Up orientation, this method will firstly rotate the CGImage to the correct orientation to work compatible with `NSImageView`. However, UIKit will not actually rotate CGImage and just store it as `imageOrientation` property. @param cgImage A Core Graphics image object @param scale The image scale factor @param orientation The orientation of the image data @return The image object */ - (nonnull instancetype)initWithCGImage:(nonnull CGImageRef)cgImage scale:(CGFloat)scale orientation:(CGImagePropertyOrientation)orientation; /** Initializes and returns an image object with the specified Core Image object. The representation is `NSCIImageRep`. @param ciImage A Core Image image object @param scale The image scale factor @param orientation The orientation of the image data @return The image object */ - (nonnull instancetype)initWithCIImage:(nonnull CIImage *)ciImage scale:(CGFloat)scale orientation:(CGImagePropertyOrientation)orientation; /** Returns an image object with the scale factor. The representation is created from the image data. @note The difference between these this and `initWithData:` is that `initWithData:` will always use `backingScaleFactor` as scale factor. @param data The image data @param scale The image scale factor @return The image object */ - (nullable instancetype)initWithData:(nonnull NSData *)data scale:(CGFloat)scale; @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/NSImage+Compatibility.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "NSImage+Compatibility.h" #if SD_MAC #import "SDImageCoderHelper.h" @implementation NSImage (Compatibility) - (nullable CGImageRef)CGImage { NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); CGImageRef cgImage = [self CGImageForProposedRect:&imageRect context:nil hints:nil]; return cgImage; } - (nullable CIImage *)CIImage { NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; if (![imageRep isKindOfClass:NSCIImageRep.class]) { return nil; } return ((NSCIImageRep *)imageRep).CIImage; } - (CGFloat)scale { CGFloat scale = 1; NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; CGFloat width = imageRep.size.width; CGFloat height = imageRep.size.height; CGFloat pixelWidth = (CGFloat)imageRep.pixelsWide; CGFloat pixelHeight = (CGFloat)imageRep.pixelsHigh; if (width > 0 && height > 0) { CGFloat widthScale = pixelWidth / width; CGFloat heightScale = pixelHeight / height; if (widthScale == heightScale && widthScale >= 1) { // Protect because there may be `NSImageRepMatchesDevice` (0) scale = widthScale; } } return scale; } - (instancetype)initWithCGImage:(nonnull CGImageRef)cgImage scale:(CGFloat)scale orientation:(CGImagePropertyOrientation)orientation { NSBitmapImageRep *imageRep; if (orientation != kCGImagePropertyOrientationUp) { // AppKit design is different from UIKit. Where CGImage based image rep does not respect to any orientation. Only data based image rep which contains the EXIF metadata can automatically detect orientation. // This should be nonnull, until the memory is exhausted cause `CGBitmapContextCreate` failed. CGImageRef rotatedCGImage = [SDImageCoderHelper CGImageCreateDecoded:cgImage orientation:orientation]; imageRep = [[NSBitmapImageRep alloc] initWithCGImage:rotatedCGImage]; CGImageRelease(rotatedCGImage); } else { imageRep = [[NSBitmapImageRep alloc] initWithCGImage:cgImage]; } if (scale < 1) { scale = 1; } CGFloat pixelWidth = imageRep.pixelsWide; CGFloat pixelHeight = imageRep.pixelsHigh; NSSize size = NSMakeSize(pixelWidth / scale, pixelHeight / scale); self = [self initWithSize:size]; if (self) { imageRep.size = size; [self addRepresentation:imageRep]; } return self; } - (instancetype)initWithCIImage:(nonnull CIImage *)ciImage scale:(CGFloat)scale orientation:(CGImagePropertyOrientation)orientation { NSCIImageRep *imageRep; if (orientation != kCGImagePropertyOrientationUp) { CIImage *rotatedCIImage = [ciImage imageByApplyingOrientation:orientation]; imageRep = [[NSCIImageRep alloc] initWithCIImage:rotatedCIImage]; } else { imageRep = [[NSCIImageRep alloc] initWithCIImage:ciImage]; } if (scale < 1) { scale = 1; } CGFloat pixelWidth = imageRep.pixelsWide; CGFloat pixelHeight = imageRep.pixelsHigh; NSSize size = NSMakeSize(pixelWidth / scale, pixelHeight / scale); self = [self initWithSize:size]; if (self) { imageRep.size = size; [self addRepresentation:imageRep]; } return self; } - (instancetype)initWithData:(nonnull NSData *)data scale:(CGFloat)scale { NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithData:data]; if (!imageRep) { return nil; } if (scale < 1) { scale = 1; } CGFloat pixelWidth = imageRep.pixelsWide; CGFloat pixelHeight = imageRep.pixelsHigh; NSSize size = NSMakeSize(pixelWidth / scale, pixelHeight / scale); self = [self initWithSize:size]; if (self) { imageRep.size = size; [self addRepresentation:imageRep]; } return self; } @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import "SDImageCoder.h" /** This is the protocol for SDAnimatedImage class only but not for SDAnimatedImageCoder. If you want to provide a custom animated image class with full advanced function, you can conform to this instead of the base protocol. */ @protocol SDAnimatedImage @required /** Initializes and returns the image object with the specified data, scale factor and possible animation decoding options. @note We use this to create animated image instance for normal animation decoding. @param data The data object containing the image data. @param scale The scale factor to assume when interpreting the image data. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property. @param options A dictionary containing any animation decoding options. @return An initialized object */ - (nullable instancetype)initWithData:(nonnull NSData *)data scale:(CGFloat)scale options:(nullable SDImageCoderOptions *)options; /** Initializes the image with an animated coder. You can use the coder to decode the image frame later. @note We use this with animated coder which conforms to `SDProgressiveImageCoder` for progressive animation decoding. @param animatedCoder An animated coder which conform `SDAnimatedImageCoder` protocol @param scale The scale factor to assume when interpreting the image data. Applying a scale factor of 1.0 results in an image whose size matches the pixel-based dimensions of the image. Applying a different scale factor changes the size of the image as reported by the `size` property. @return An initialized object */ - (nullable instancetype)initWithAnimatedCoder:(nonnull id)animatedCoder scale:(CGFloat)scale; @optional // These methods are used for optional advanced feature, like image frame preloading. /** Pre-load all animated image frame into memory. Then later frame image request can directly return the frame for index without decoding. This method may be called on background thread. @note If one image instance is shared by lots of imageViews, the CPU performance for large animated image will drop down because the request frame index will be random (not in order) and the decoder should take extra effort to keep it re-entrant. You can use this to reduce CPU usage if need. Attention this will consume more memory usage. */ - (void)preloadAllFrames; /** Unload all animated image frame from memory if are already pre-loaded. Then later frame image request need decoding. You can use this to free up the memory usage if need. */ - (void)unloadAllFrames; /** Returns a Boolean value indicating whether all animated image frames are already pre-loaded into memory. */ @property (nonatomic, assign, readonly, getter=isAllFramesLoaded) BOOL allFramesLoaded; /** Return the animated image coder if the image is created with `initWithAnimatedCoder:scale:` method. @note We use this with animated coder which conforms to `SDProgressiveImageCoder` for progressive animation decoding. */ @property (nonatomic, strong, readonly, nullable) id animatedCoder; @end /** The image class which supports animating on `SDAnimatedImageView`. You can also use it on normal UIImageView/NSImageView. */ NS_SWIFT_NONISOLATED @interface SDAnimatedImage : UIImage // This class override these methods from UIImage(NSImage), and it supports NSSecureCoding. // You should use these methods to create a new animated image. Use other methods just call super instead. // @note Before 5.19, these initializer will return nil for static image (when all candidate SDAnimatedImageCoder returns nil instance), like JPEG data. After 5.19, these initializer will retry for static image as well, so JPEG data will return non-nil instance. For vector image(PDF/SVG), always return nil. // @note When the animated image frame count <= 1, all the `SDAnimatedImageProvider` protocol methods will return nil or 0 value, you'd better check the frame count before usage and keep fallback. + (nullable instancetype)imageNamed:(nonnull NSString *)name; // Cache in memory, no Asset Catalog support #if __has_include() && !SD_WATCH + (nullable instancetype)imageNamed:(nonnull NSString *)name inBundle:(nullable NSBundle *)bundle compatibleWithTraitCollection:(nullable UITraitCollection *)traitCollection; // Cache in memory, no Asset Catalog support #else + (nullable instancetype)imageNamed:(nonnull NSString *)name inBundle:(nullable NSBundle *)bundle; // Cache in memory, no Asset Catalog support #endif + (nullable instancetype)imageWithContentsOfFile:(nonnull NSString *)path; + (nullable instancetype)imageWithData:(nonnull NSData *)data; + (nullable instancetype)imageWithData:(nonnull NSData *)data scale:(CGFloat)scale; - (nullable instancetype)initWithContentsOfFile:(nonnull NSString *)path; - (nullable instancetype)initWithData:(nonnull NSData *)data; - (nullable instancetype)initWithData:(nonnull NSData *)data scale:(CGFloat)scale; /** Current animated image format. @note This format is only valid when `animatedImageData` not nil. @note This actually just call `[NSData sd_imageFormatForImageData:self.animatedImageData]` */ @property (nonatomic, assign, readonly) SDImageFormat animatedImageFormat; /** Current animated image data, you can use this to grab the compressed format data and create another animated image instance. If this image instance is an animated image created by using animated image coder (which means using the API listed above or using `initWithAnimatedCoder:scale:`), this property is non-nil. */ @property (nonatomic, copy, readonly, nullable) NSData *animatedImageData; /** The scale factor of the image. @note For UIKit, this just call super instead. @note For AppKit, `NSImage` can contains multiple image representations with different scales. However, this class does not do that from the design. We process the scale like UIKit. This will actually be calculated from image size and pixel size. */ @property (nonatomic, readonly) CGFloat scale; // By default, animated image frames are returned by decoding just in time without keeping into memory. But you can choose to preload them into memory as well, See the description in `SDAnimatedImage` protocol. // After preloaded, there is no huge difference on performance between this and UIImage's `animatedImageWithImages:duration:`. But UIImage's animation have some issues such like blanking and pausing during segue when using in `UIImageView`. It's recommend to use only if need. /** Pre-load all animated image frame into memory. Then later frame image request can directly return the frame for index without decoding. This method may be called on background thread. @note If one image instance is shared by lots of imageViews, the CPU performance for large animated image will drop down because the request frame index will be random (not in order) and the decoder should take extra effort to keep it re-entrant. You can use this to reduce CPU usage if need. Attention this will consume more memory usage. */ - (void)preloadAllFrames; /** Unload all animated image frame from memory if are already pre-loaded. Then later frame image request need decoding. You can use this to free up the memory usage if need. */ - (void)unloadAllFrames; /** Returns a Boolean value indicating whether all animated image frames are already pre-loaded into memory. */ @property (nonatomic, assign, readonly, getter=isAllFramesLoaded) BOOL allFramesLoaded; /** Return the animated image coder if the image is created with `initWithAnimatedCoder:scale:` method. @note We use this with animated coder which conforms to `SDProgressiveImageCoder` for progressive animation decoding. */ @property (nonatomic, strong, readonly, nullable) id animatedCoder; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImage.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDAnimatedImage.h" #import "NSImage+Compatibility.h" #import "SDImageCoder.h" #import "SDImageCodersManager.h" #import "SDImageFrame.h" #import "UIImage+MemoryCacheCost.h" #import "UIImage+Metadata.h" #import "UIImage+MultiFormat.h" #import "SDImageCoderHelper.h" #import "SDImageAssetManager.h" #import "objc/runtime.h" static CGFloat SDImageScaleFromPath(NSString *string) { if (string.length == 0 || [string hasSuffix:@"/"]) return 1; NSString *name = string.stringByDeletingPathExtension; __block CGFloat scale = 1; NSRegularExpression *pattern = [NSRegularExpression regularExpressionWithPattern:@"@[0-9]+\\.?[0-9]*x$" options:NSRegularExpressionAnchorsMatchLines error:nil]; [pattern enumerateMatchesInString:name options:kNilOptions range:NSMakeRange(0, name.length) usingBlock:^(NSTextCheckingResult *result, NSMatchingFlags flags, BOOL *stop) { scale = [string substringWithRange:NSMakeRange(result.range.location + 1, result.range.length - 2)].doubleValue; }]; return scale; } @interface SDAnimatedImage () @property (nonatomic, strong) id animatedCoder; @property (atomic, copy) NSArray *loadedAnimatedImageFrames; // Mark as atomic to keep thread-safe @property (nonatomic, assign, getter=isAllFramesLoaded) BOOL allFramesLoaded; @end @implementation SDAnimatedImage @dynamic scale; // call super #pragma mark - UIImage override method + (instancetype)imageNamed:(NSString *)name { #if __has_include() && !SD_WATCH return [self imageNamed:name inBundle:nil compatibleWithTraitCollection:nil]; #else return [self imageNamed:name inBundle:nil]; #endif } #if __has_include() && !SD_WATCH + (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle compatibleWithTraitCollection:(UITraitCollection *)traitCollection { #if SD_VISION if (!traitCollection) { traitCollection = UITraitCollection.currentTraitCollection; } #else if (!traitCollection) { traitCollection = UIScreen.mainScreen.traitCollection; } #endif CGFloat scale = traitCollection.displayScale; return [self imageNamed:name inBundle:bundle scale:scale]; } #else + (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle { return [self imageNamed:name inBundle:bundle scale:0]; } #endif // 0 scale means automatically check + (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle scale:(CGFloat)scale { if (!name) { return nil; } if (!bundle) { bundle = [NSBundle mainBundle]; } SDImageAssetManager *assetManager = [SDImageAssetManager sharedAssetManager]; SDAnimatedImage *image = (SDAnimatedImage *)[assetManager imageForName:name]; if ([image isKindOfClass:[SDAnimatedImage class]]) { return image; } NSString *path = [assetManager getPathForName:name bundle:bundle preferredScale:&scale]; if (!path) { return image; } NSData *data = [NSData dataWithContentsOfFile:path]; if (!data) { return image; } image = [[self alloc] initWithData:data scale:scale]; if (image) { [assetManager storeImage:image forName:name]; } return image; } + (instancetype)imageWithContentsOfFile:(NSString *)path { return [[self alloc] initWithContentsOfFile:path]; } + (instancetype)imageWithData:(NSData *)data { return [[self alloc] initWithData:data]; } + (instancetype)imageWithData:(NSData *)data scale:(CGFloat)scale { return [[self alloc] initWithData:data scale:scale]; } - (instancetype)initWithContentsOfFile:(NSString *)path { NSData *data = [NSData dataWithContentsOfFile:path]; if (!data) { return nil; } CGFloat scale = SDImageScaleFromPath(path); // path extension may be useful for coder like raw-image NSString *fileExtensionHint = path.pathExtension; // without dot if (fileExtensionHint.length == 0) { // Ignore file extension which is empty fileExtensionHint = nil; } SDImageCoderMutableOptions *mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:1]; mutableCoderOptions[SDImageCoderDecodeFileExtensionHint] = fileExtensionHint; return [self initWithData:data scale:scale options:[mutableCoderOptions copy]]; } - (instancetype)initWithData:(NSData *)data { return [self initWithData:data scale:1]; } - (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale { return [self initWithData:data scale:scale options:nil]; } - (instancetype)initWithData:(NSData *)data scale:(CGFloat)scale options:(SDImageCoderOptions *)options { if (!data || data.length == 0) { return nil; } // Vector image does not supported, guard firstly SDImageFormat format = [NSData sd_imageFormatForImageData:data]; if (format == SDImageFormatSVG || format == SDImageFormatPDF) { return nil; } id animatedCoder = nil; SDImageCoderMutableOptions *mutableCoderOptions; if (options != nil) { mutableCoderOptions = [NSMutableDictionary dictionaryWithDictionary:options]; } else { mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:1]; } mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale); options = [mutableCoderOptions copy]; for (idcoder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) { if ([coder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) { if ([coder canDecodeFromData:data]) { animatedCoder = [[[coder class] alloc] initWithAnimatedImageData:data options:options]; break; } } } if (animatedCoder) { // Animated Image return [self initWithAnimatedCoder:animatedCoder scale:scale]; } else { // Static Image (Before 5.19 this code path return nil) UIImage *image = [[SDImageCodersManager sharedManager] decodedImageWithData:data options:options]; if (!image) { return nil; } // Vector image does not supported, guard secondly if (image.sd_isVector) { return nil; } #if SD_MAC self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:kCGImagePropertyOrientationUp]; #else self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation]; #endif // Defines the associated object that holds the format for static images super.sd_imageFormat = format; return self; } } - (instancetype)initWithAnimatedCoder:(id)animatedCoder scale:(CGFloat)scale { if (!animatedCoder) { return nil; } UIImage *image = [animatedCoder animatedImageFrameAtIndex:0]; if (!image) { return nil; } #if SD_MAC self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:kCGImagePropertyOrientationUp]; #else self = [super initWithCGImage:image.CGImage scale:MAX(scale, 1) orientation:image.imageOrientation]; #endif if (self) { // Only keep the animated coder if frame count > 1, save RAM usage for non-animated image format (APNG/WebP) if (animatedCoder.animatedImageFrameCount > 1) { _animatedCoder = animatedCoder; } } return self; } - (SDImageFormat)animatedImageFormat { return [NSData sd_imageFormatForImageData:self.animatedImageData]; } #pragma mark - Preload - (void)preloadAllFrames { if (!_animatedCoder) { return; } if (!self.isAllFramesLoaded) { NSMutableArray *frames = [NSMutableArray arrayWithCapacity:self.animatedImageFrameCount]; for (size_t i = 0; i < self.animatedImageFrameCount; i++) { UIImage *image = [self animatedImageFrameAtIndex:i]; NSTimeInterval duration = [self animatedImageDurationAtIndex:i]; SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration]; // through the image should be nonnull, used as nullable for `animatedImageFrameAtIndex:` [frames addObject:frame]; } self.loadedAnimatedImageFrames = frames; self.allFramesLoaded = YES; } } - (void)unloadAllFrames { if (!_animatedCoder) { return; } if (self.isAllFramesLoaded) { self.loadedAnimatedImageFrames = nil; self.allFramesLoaded = NO; } } #pragma mark - NSSecureCoding - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { NSData *animatedImageData = [aDecoder decodeObjectOfClass:[NSData class] forKey:NSStringFromSelector(@selector(animatedImageData))]; if (!animatedImageData) { return self; } CGFloat scale = self.scale; id animatedCoder = nil; for (idcoder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) { if ([coder conformsToProtocol:@protocol(SDAnimatedImageCoder)]) { if ([coder canDecodeFromData:animatedImageData]) { animatedCoder = [[[coder class] alloc] initWithAnimatedImageData:animatedImageData options:@{SDImageCoderDecodeScaleFactor : @(scale)}]; break; } } } if (!animatedCoder) { return self; } if (animatedCoder.animatedImageFrameCount > 1) { _animatedCoder = animatedCoder; } } return self; } - (void)encodeWithCoder:(NSCoder *)aCoder { [super encodeWithCoder:aCoder]; NSData *animatedImageData = self.animatedImageData; if (animatedImageData) { [aCoder encodeObject:animatedImageData forKey:NSStringFromSelector(@selector(animatedImageData))]; } } + (BOOL)supportsSecureCoding { return YES; } #pragma mark - SDAnimatedImageProvider - (NSData *)animatedImageData { return [self.animatedCoder animatedImageData]; } - (NSUInteger)animatedImageLoopCount { return [self.animatedCoder animatedImageLoopCount]; } - (NSUInteger)animatedImageFrameCount { return [self.animatedCoder animatedImageFrameCount]; } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { if (index >= self.animatedImageFrameCount) { return nil; } if (self.isAllFramesLoaded) { SDImageFrame *frame = [self.loadedAnimatedImageFrames objectAtIndex:index]; return frame.image; } return [self.animatedCoder animatedImageFrameAtIndex:index]; } - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index { if (index >= self.animatedImageFrameCount) { return 0; } if (self.isAllFramesLoaded) { SDImageFrame *frame = [self.loadedAnimatedImageFrames objectAtIndex:index]; return frame.duration; } return [self.animatedCoder animatedImageDurationAtIndex:index]; } @end @implementation SDAnimatedImage (MemoryCacheCost) - (NSUInteger)sd_memoryCost { NSNumber *value = objc_getAssociatedObject(self, @selector(sd_memoryCost)); if (value != nil) { return value.unsignedIntegerValue; } CGImageRef imageRef = self.CGImage; if (!imageRef) { return 0; } NSUInteger bytesPerFrame = CGImageGetBytesPerRow(imageRef) * CGImageGetHeight(imageRef); NSUInteger frameCount = 1; if (self.isAllFramesLoaded) { frameCount = self.animatedImageFrameCount; } frameCount = frameCount > 0 ? frameCount : 1; NSUInteger cost = bytesPerFrame * frameCount; return cost; } @end @implementation SDAnimatedImage (Metadata) - (BOOL)sd_isAnimated { return self.animatedImageFrameCount > 1; } - (NSUInteger)sd_imageLoopCount { return self.animatedImageLoopCount; } - (void)setSd_imageLoopCount:(NSUInteger)sd_imageLoopCount { return; } - (NSUInteger)sd_imageFrameCount { NSUInteger frameCount = self.animatedImageFrameCount; if (frameCount > 1) { return frameCount; } else { return 1; } } - (SDImageFormat)sd_imageFormat { NSData *animatedImageData = self.animatedImageData; if (animatedImageData) { return [NSData sd_imageFormatForImageData:animatedImageData]; } else { return [super sd_imageFormat]; } } - (BOOL)sd_isVector { return NO; } @end @implementation SDAnimatedImage (MultiFormat) + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data { return [self sd_imageWithData:data scale:1]; } + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data scale:(CGFloat)scale { return [self sd_imageWithData:data scale:scale firstFrameOnly:NO]; } + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data scale:(CGFloat)scale firstFrameOnly:(BOOL)firstFrameOnly { if (!data) { return nil; } return [[self alloc] initWithData:data scale:scale options:@{SDImageCoderDecodeFirstFrameOnly : @(firstFrameOnly)}]; } - (nullable NSData *)sd_imageData { NSData *imageData = self.animatedImageData; if (imageData) { return imageData; } else { return [self sd_imageDataAsFormat:self.animatedImageFormat]; } } - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat { return [self sd_imageDataAsFormat:imageFormat compressionQuality:1]; } - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality { return [self sd_imageDataAsFormat:imageFormat compressionQuality:compressionQuality firstFrameOnly:NO]; } - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality firstFrameOnly:(BOOL)firstFrameOnly { // Protect when user input the imageFormat == self.animatedImageFormat && compressionQuality == 1 // This should be treated as grabbing `self.animatedImageData` as well :) NSData *imageData; if (imageFormat == self.animatedImageFormat && compressionQuality == 1) { imageData = self.animatedImageData; } if (imageData) return imageData; SDImageCoderOptions *options = @{SDImageCoderEncodeCompressionQuality : @(compressionQuality), SDImageCoderEncodeFirstFrameOnly : @(firstFrameOnly)}; NSUInteger frameCount = self.animatedImageFrameCount; if (frameCount <= 1) { // Static image imageData = [SDImageCodersManager.sharedManager encodedDataWithImage:self format:imageFormat options:options]; } if (imageData) return imageData; NSUInteger loopCount = self.animatedImageLoopCount; // Keep animated image encoding, loop each frame. NSMutableArray *frames = [NSMutableArray arrayWithCapacity:frameCount]; for (size_t i = 0; i < frameCount; i++) { UIImage *image = [self animatedImageFrameAtIndex:i]; NSTimeInterval duration = [self animatedImageDurationAtIndex:i]; SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration]; [frames addObject:frame]; } imageData = [SDImageCodersManager.sharedManager encodedDataWithFrames:frames loopCount:loopCount format:imageFormat options:options]; return imageData; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" #import "SDImageCoder.h" /// Animated image playback mode typedef NS_ENUM(NSUInteger, SDAnimatedImagePlaybackMode) { /** * From first to last frame and stop or next loop. */ SDAnimatedImagePlaybackModeNormal = 0, /** * From last frame to first frame and stop or next loop. */ SDAnimatedImagePlaybackModeReverse, /** * From first frame to last frame and reverse again, like reciprocating. */ SDAnimatedImagePlaybackModeBounce, /** * From last frame to first frame and reverse again, like reversed reciprocating. */ SDAnimatedImagePlaybackModeReversedBounce, }; /// A player to control the playback of animated image, which can be used to drive Animated ImageView or any rendering usage, like CALayer/WatchKit/SwiftUI rendering. @interface SDAnimatedImagePlayer : NSObject /// Current playing frame image. This value is KVO Compliance. @property (nonatomic, readonly, nullable) UIImage *currentFrame; /// Current frame index, zero based. This value is KVO Compliance. @property (nonatomic, readonly) NSUInteger currentFrameIndex; /// Current loop count since its latest animating. This value is KVO Compliance. @property (nonatomic, readonly) NSUInteger currentLoopCount; /// Total frame count for animated image rendering. Defaults is animated image's frame count. /// @note For progressive animation, you can update this value when your provider receive more frames. @property (nonatomic, assign) NSUInteger totalFrameCount; /// Total loop count for animated image rendering. Default is animated image's loop count. @property (nonatomic, assign) NSUInteger totalLoopCount; /// The animation playback rate. Default is 1.0 /// `1.0` means the normal speed. /// `0.0` means stopping the animation. /// `0.0-1.0` means the slow speed. /// `> 1.0` means the fast speed. /// `< 0.0` is not supported currently and stop animation. (may support reverse playback in the future) @property (nonatomic, assign) double playbackRate; /// Asynchronous setup animation playback mode. Default mode is SDAnimatedImagePlaybackModeNormal. @property (nonatomic, assign) SDAnimatedImagePlaybackMode playbackMode; /// Provide a max buffer size by bytes. This is used to adjust frame buffer count and can be useful when the decoding cost is expensive (such as Animated WebP software decoding). Default is 0. /// `0` means automatically adjust by calculating current memory usage. /// `1` means without any buffer cache, each of frames will be decoded and then be freed after rendering. (Lowest Memory and Highest CPU) /// `NSUIntegerMax` means cache all the buffer. (Lowest CPU and Highest Memory) @property (nonatomic, assign) NSUInteger maxBufferSize; /// You can specify a runloop mode to let it rendering. /// Default is NSRunLoopCommonModes on multi-core device, NSDefaultRunLoopMode on single-core device @property (nonatomic, copy, nonnull) NSRunLoopMode runLoopMode; /// Create a player with animated image provider. If the provider's `animatedImageFrameCount` is less than 1, returns nil. /// The provider can be any protocol implementation, like `SDAnimatedImage`, `SDImageGIFCoder`, etc. /// @note This provider can represent mutable content, like progressive animated loading. But you need to update the frame count by yourself /// @param provider The animated provider - (nullable instancetype)initWithProvider:(nonnull id)provider; /// Create a player with animated image provider. If the provider's `animatedImageFrameCount` is less than 1, returns nil. /// The provider can be any protocol implementation, like `SDAnimatedImage` or `SDImageGIFCoder`, etc. /// @note This provider can represent mutable content, like progressive animated loading. But you need to update the frame count by yourself /// @param provider The animated provider + (nullable instancetype)playerWithProvider:(nonnull id)provider; /// The handler block when current frame and index changed. @property (nonatomic, copy, nullable) void (^animationFrameHandler)(NSUInteger index, UIImage * _Nonnull frame); /// The handler block when one loop count finished. @property (nonatomic, copy, nullable) void (^animationLoopHandler)(NSUInteger loopCount); /// Return the status whether animation is playing. @property (nonatomic, readonly) BOOL isPlaying; /// Start the animation. Or resume the previously paused animation. - (void)startPlaying; /// Pause the animation. Keep the current frame index and loop count. - (void)pausePlaying; /// Stop the animation. Reset the current frame index and loop count. - (void)stopPlaying; /// Seek to the desired frame index and loop count. /// @note This can be used for advanced control like progressive loading, or skipping specify frames. /// @param index The frame index /// @param loopCount The loop count - (void)seekToFrameAtIndex:(NSUInteger)index loopCount:(NSUInteger)loopCount; /// Clear the frame cache buffer. The frame cache buffer size can be controlled by `maxBufferSize`. /// By default, when stop or pause the animation, the frame buffer is still kept to ready for the next restart - (void)clearFrameBuffer; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImagePlayer.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDAnimatedImagePlayer.h" #import "NSImage+Compatibility.h" #import "SDDisplayLink.h" #import "SDDeviceHelper.h" #import "SDImageFramePool.h" #import "SDInternalMacros.h" @interface SDAnimatedImagePlayer () { NSRunLoopMode _runLoopMode; } @property (nonatomic, strong) SDImageFramePool *framePool; @property (nonatomic, strong, readwrite) UIImage *currentFrame; @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; @property (nonatomic, assign, readwrite) NSUInteger currentLoopCount; @property (nonatomic, strong) id animatedProvider; @property (nonatomic, assign) NSUInteger currentFrameBytes; @property (nonatomic, assign) NSTimeInterval currentTime; @property (nonatomic, assign) BOOL bufferMiss; @property (nonatomic, assign) BOOL needsDisplayWhenImageBecomesAvailable; @property (nonatomic, assign) BOOL shouldReverse; @property (nonatomic, strong) SDDisplayLink *displayLink; @end @implementation SDAnimatedImagePlayer - (instancetype)initWithProvider:(id)provider { self = [super init]; if (self) { NSUInteger animatedImageFrameCount = provider.animatedImageFrameCount; // Check the frame count if (animatedImageFrameCount <= 1) { return nil; } self.totalFrameCount = animatedImageFrameCount; // Get the current frame and loop count. self.totalLoopCount = provider.animatedImageLoopCount; self.animatedProvider = provider; self.playbackRate = 1.0; self.framePool = [SDImageFramePool registerProvider:provider]; } return self; } + (instancetype)playerWithProvider:(id)provider { SDAnimatedImagePlayer *player = [[SDAnimatedImagePlayer alloc] initWithProvider:provider]; return player; } - (void)dealloc { // Dereference the frame pool, when zero the frame pool for provider will dealloc [SDImageFramePool unregisterProvider:self.animatedProvider]; } #pragma mark - Private - (SDDisplayLink *)displayLink { if (!_displayLink) { _displayLink = [SDDisplayLink displayLinkWithTarget:self selector:@selector(displayDidRefresh:)]; [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:self.runLoopMode]; [_displayLink stop]; } return _displayLink; } - (void)setRunLoopMode:(NSRunLoopMode)runLoopMode { if ([_runLoopMode isEqual:runLoopMode]) { return; } if (_displayLink) { if (_runLoopMode) { [_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_runLoopMode]; } if (runLoopMode.length > 0) { [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode]; } } _runLoopMode = [runLoopMode copy]; } - (NSRunLoopMode)runLoopMode { if (!_runLoopMode) { _runLoopMode = [[self class] defaultRunLoopMode]; } return _runLoopMode; } #pragma mark - State Control - (void)setupCurrentFrame { if (self.currentFrameIndex != 0) { return; } if (self.playbackMode == SDAnimatedImagePlaybackModeReverse || self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) { self.currentFrameIndex = self.totalFrameCount - 1; } if (!self.currentFrame && [self.animatedProvider isKindOfClass:[UIImage class]]) { UIImage *image = (UIImage *)self.animatedProvider; // Cache the poster image if available, but should not callback to avoid caller thread issues #if SD_MAC UIImage *posterFrame = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp]; #else UIImage *posterFrame = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation]; #endif if (posterFrame) { // Calculate max buffer size [self calculateMaxBufferCountWithFrame:posterFrame]; // HACK: The first frame should not check duration and immediately display self.needsDisplayWhenImageBecomesAvailable = YES; [self.framePool setFrame:posterFrame atIndex:self.currentFrameIndex]; } } } - (void)resetCurrentFrameStatus { // These should not trigger KVO, user don't need to receive an `index == 0, image == nil` callback. _currentFrame = nil; _currentFrameIndex = 0; _currentLoopCount = 0; _currentTime = 0; _bufferMiss = NO; _needsDisplayWhenImageBecomesAvailable = NO; } - (void)clearFrameBuffer { [self.framePool removeAllFrames]; } #pragma mark - Animation Control - (void)startPlaying { [self.displayLink start]; // Setup frame [self setupCurrentFrame]; } - (void)stopPlaying { // Using `_displayLink` here because when UIImageView dealloc, it may trigger `[self stopAnimating]`, we already release the display link in SDAnimatedImageView's dealloc method. [_displayLink stop]; // We need to reset the frame status, but not trigger any handle. This can ensure next time's playing status correct. [self resetCurrentFrameStatus]; } - (void)pausePlaying { [_displayLink stop]; } - (BOOL)isPlaying { return _displayLink.isRunning; } - (void)seekToFrameAtIndex:(NSUInteger)index loopCount:(NSUInteger)loopCount { if (index >= self.totalFrameCount) { return; } self.currentFrameIndex = index; self.currentLoopCount = loopCount; self.currentFrame = [self.animatedProvider animatedImageFrameAtIndex:index]; [self handleFrameChange]; } #pragma mark - Core Render - (void)displayDidRefresh:(SDDisplayLink *)displayLink { // If for some reason a wild call makes it through when we shouldn't be animating, bail. // Early return! if (!self.isPlaying) { return; } NSUInteger totalFrameCount = self.totalFrameCount; if (totalFrameCount <= 1) { // Total frame count less than 1, wrong configuration and stop animating [self stopPlaying]; return; } NSTimeInterval playbackRate = self.playbackRate; if (playbackRate <= 0) { // Does not support <= 0 play rate [self stopPlaying]; return; } // Calculate refresh duration NSTimeInterval duration = self.displayLink.duration; NSUInteger currentFrameIndex = self.currentFrameIndex; NSUInteger nextFrameIndex = (currentFrameIndex + 1) % totalFrameCount; if (self.playbackMode == SDAnimatedImagePlaybackModeReverse) { nextFrameIndex = currentFrameIndex == 0 ? (totalFrameCount - 1) : (currentFrameIndex - 1) % totalFrameCount; } else if (self.playbackMode == SDAnimatedImagePlaybackModeBounce || self.playbackMode == SDAnimatedImagePlaybackModeReversedBounce) { if (currentFrameIndex == 0) { self.shouldReverse = NO; } else if (currentFrameIndex == totalFrameCount - 1) { self.shouldReverse = YES; } nextFrameIndex = self.shouldReverse ? (currentFrameIndex - 1) : (currentFrameIndex + 1); nextFrameIndex %= totalFrameCount; } // Check if we need to display new frame firstly if (self.needsDisplayWhenImageBecomesAvailable) { UIImage *currentFrame = [self.framePool frameAtIndex:currentFrameIndex]; // Update the current frame if (currentFrame) { // Update the current frame immediately self.currentFrame = currentFrame; [self handleFrameChange]; self.bufferMiss = NO; self.needsDisplayWhenImageBecomesAvailable = NO; } else { self.bufferMiss = YES; } } // Check if we have the frame buffer if (!self.bufferMiss) { // Then check if timestamp is reached self.currentTime += duration; NSTimeInterval currentDuration = [self.animatedProvider animatedImageDurationAtIndex:currentFrameIndex]; currentDuration = currentDuration / playbackRate; if (self.currentTime < currentDuration) { // Current frame timestamp not reached, prefetch frame in advance. [self prefetchFrameAtIndex:currentFrameIndex nextIndex:nextFrameIndex]; return; } // Otherwise, we should be ready to display next frame self.needsDisplayWhenImageBecomesAvailable = YES; self.currentFrameIndex = nextFrameIndex; self.currentTime -= currentDuration; NSTimeInterval nextDuration = [self.animatedProvider animatedImageDurationAtIndex:nextFrameIndex]; nextDuration = nextDuration / playbackRate; if (self.currentTime > nextDuration) { // Do not skip frame self.currentTime = nextDuration; } // Update the loop count when last frame rendered if (nextFrameIndex == 0) { // Update the loop count self.currentLoopCount++; [self handleLoopChange]; // if reached the max loop count, stop animating, 0 means loop indefinitely NSUInteger maxLoopCount = self.totalLoopCount; if (maxLoopCount != 0 && (self.currentLoopCount >= maxLoopCount)) { [self stopPlaying]; return; } } } // Since we support handler, check animating state again if (!self.isPlaying) { return; } [self prefetchFrameAtIndex:currentFrameIndex nextIndex:nextFrameIndex]; } // Check if we should prefetch next frame or current frame // When buffer miss, means the decode speed is slower than render speed, we fetch current miss frame // Or, most cases, the decode speed is faster than render speed, we fetch next frame - (void)prefetchFrameAtIndex:(NSUInteger)currentIndex nextIndex:(NSUInteger)nextIndex { NSUInteger fetchFrameIndex = currentIndex; UIImage *fetchFrame = nil; if (!self.bufferMiss) { fetchFrameIndex = nextIndex; fetchFrame = [self.framePool frameAtIndex:nextIndex]; } BOOL bufferFull = NO; if (self.framePool.currentFrameCount == self.totalFrameCount) { bufferFull = YES; } if (!fetchFrame && !bufferFull) { // Calculate max buffer size [self calculateMaxBufferCountWithFrame:self.currentFrame]; // Prefetch next frame [self.framePool prefetchFrameAtIndex:fetchFrameIndex]; } } - (void)handleFrameChange { if (self.animationFrameHandler) { self.animationFrameHandler(self.currentFrameIndex, self.currentFrame); } } - (void)handleLoopChange { if (self.animationLoopHandler) { self.animationLoopHandler(self.currentLoopCount); } } #pragma mark - Util - (void)calculateMaxBufferCountWithFrame:(nonnull UIImage *)frame { NSUInteger bytes = self.currentFrameBytes; if (bytes == 0) { bytes = CGImageGetBytesPerRow(frame.CGImage) * CGImageGetHeight(frame.CGImage); if (bytes == 0) { bytes = 1024; } else { // Cache since most animated image each frame bytes is the same self.currentFrameBytes = bytes; } } NSUInteger max = 0; if (self.maxBufferSize > 0) { max = self.maxBufferSize; } else { // Calculate based on current memory, these factors are by experience NSUInteger total = [SDDeviceHelper totalMemory]; NSUInteger free = [SDDeviceHelper freeMemory]; max = MIN(total * 0.2, free * 0.6); } NSUInteger maxBufferCount = (double)max / (double)bytes; if (!maxBufferCount) { // At least 1 frame maxBufferCount = 1; } self.framePool.maxBufferCount = maxBufferCount; } + (NSString *)defaultRunLoopMode { // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations. return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageRep.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_MAC #import "NSData+ImageContentType.h" /** A subclass of `NSBitmapImageRep` to fix that GIF duration issue because `NSBitmapImageRep` will reset `NSImageCurrentFrameDuration` by using `kCGImagePropertyGIFDelayTime` but not `kCGImagePropertyGIFUnclampedDelayTime`. This also fix the GIF loop count issue, which will use the Netscape standard (See http://www6.uniovi.es/gifanim/gifabout.htm) to only place once when the `kCGImagePropertyGIFLoopCount` is nil. This is what modern browser's behavior. Built in GIF coder use this instead of `NSBitmapImageRep` for better GIF rendering. If you do not want this, only enable `SDImageIOCoder`, which just call `NSImage` API and actually use `NSBitmapImageRep` for GIF image. This also support APNG format using `SDImageAPNGCoder`. Which provide full alpha-channel support and the correct duration match the `kCGImagePropertyAPNGUnclampedDelayTime`. */ @interface SDAnimatedImageRep : NSBitmapImageRep /// Current animated image format. /// @note This format is only valid when `animatedImageData` not nil @property (nonatomic, assign, readonly) SDImageFormat animatedImageFormat; /// This allows to retrive the compressed data like GIF using `sd_imageData` on parent `NSImage`, without re-encoding (waste CPU and RAM) /// @note This is typically nonnull when you create with `initWithData:`, even it's marked as weak, because ImageIO retain it @property (nonatomic, readonly, nullable, weak) NSData *animatedImageData; @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageRep.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDAnimatedImageRep.h" #if SD_MAC #import "SDImageIOAnimatedCoderInternal.h" #import "SDImageGIFCoder.h" #import "SDImageAPNGCoder.h" #import "SDImageHEICCoder.h" #import "SDImageAWebPCoder.h" @interface SDAnimatedImageRep () /// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`). @property (nonatomic, readwrite, weak) NSArray *frames; @property (nonatomic, assign, readwrite) SDImageFormat animatedImageFormat; @end @implementation SDAnimatedImageRep { CGImageSourceRef _imageSource; } - (void)dealloc { if (_imageSource) { CFRelease(_imageSource); _imageSource = NULL; } } - (instancetype)copyWithZone:(NSZone *)zone { SDAnimatedImageRep *imageRep = [super copyWithZone:zone]; // super will copy all ivars if (imageRep->_imageSource) { CFRetain(imageRep->_imageSource); } return imageRep; } // `NSBitmapImageRep`'s `imageRepWithData:` is not designed initializer + (instancetype)imageRepWithData:(NSData *)data { SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data]; return imageRep; } // We should override init method for `NSBitmapImageRep` to do initialize about animated image format #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" - (instancetype)initWithData:(NSData *)data { self = [super initWithData:data]; if (self) { CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef) data, NULL); if (!imageSource) { return self; } _imageSource = imageSource; NSUInteger frameCount = CGImageSourceGetCount(imageSource); if (frameCount <= 1) { return self; } CFStringRef type = CGImageSourceGetType(imageSource); if (!type) { return self; } _animatedImageData = data; // CGImageSource will retain the data internally, no extra copy SDImageFormat format = SDImageFormatUndefined; if (CFStringCompare(type, kSDUTTypeGIF, 0) == kCFCompareEqualTo) { // GIF // Fix the `NSBitmapImageRep` GIF loop count calculation issue // Which will use 0 when there are no loop count information metadata in GIF data format = SDImageFormatGIF; NSUInteger loopCount = [SDImageGIFCoder imageLoopCountWithSource:imageSource]; [self setProperty:NSImageLoopCount withValue:@(loopCount)]; } else if (CFStringCompare(type, kSDUTTypePNG, 0) == kCFCompareEqualTo) { // APNG // Do initialize about frame count, current frame/duration and loop count format = SDImageFormatPNG; [self setProperty:NSImageFrameCount withValue:@(frameCount)]; [self setProperty:NSImageCurrentFrame withValue:@(0)]; NSUInteger loopCount = [SDImageAPNGCoder imageLoopCountWithSource:imageSource]; [self setProperty:NSImageLoopCount withValue:@(loopCount)]; } else if (CFStringCompare(type, kSDUTTypeHEICS, 0) == kCFCompareEqualTo) { // HEIC // Do initialize about frame count, current frame/duration and loop count format = SDImageFormatHEIC; [self setProperty:NSImageFrameCount withValue:@(frameCount)]; [self setProperty:NSImageCurrentFrame withValue:@(0)]; NSUInteger loopCount = [SDImageHEICCoder imageLoopCountWithSource:imageSource]; [self setProperty:NSImageLoopCount withValue:@(loopCount)]; } else if (CFStringCompare(type, kSDUTTypeWebP, 0) == kCFCompareEqualTo) { // WebP // Do initialize about frame count, current frame/duration and loop count format = SDImageFormatWebP; [self setProperty:NSImageFrameCount withValue:@(frameCount)]; [self setProperty:NSImageCurrentFrame withValue:@(0)]; NSUInteger loopCount = [SDImageAWebPCoder imageLoopCountWithSource:imageSource]; [self setProperty:NSImageLoopCount withValue:@(loopCount)]; } else { format = [NSData sd_imageFormatForImageData:data]; } _animatedImageFormat = format; } return self; } // `NSBitmapImageRep` will use `kCGImagePropertyGIFDelayTime` whenever you call `setProperty:withValue:` with `NSImageCurrentFrame` to change the current frame. We override it and use the actual `kCGImagePropertyGIFUnclampedDelayTime` if need. - (void)setProperty:(NSBitmapImageRepPropertyKey)property withValue:(id)value { [super setProperty:property withValue:value]; if ([property isEqualToString:NSImageCurrentFrame]) { // Access the image source CGImageSourceRef imageSource = _imageSource; if (!imageSource) { return; } // Check format type CFStringRef type = CGImageSourceGetType(imageSource); if (!type) { return; } NSUInteger index = [value unsignedIntegerValue]; NSTimeInterval frameDuration = 0; if (CFStringCompare(type, kSDUTTypeGIF, 0) == kCFCompareEqualTo) { // GIF frameDuration = [SDImageGIFCoder frameDurationAtIndex:index source:imageSource]; } else if (CFStringCompare(type, kSDUTTypePNG, 0) == kCFCompareEqualTo) { // APNG frameDuration = [SDImageAPNGCoder frameDurationAtIndex:index source:imageSource]; } else if (CFStringCompare(type, kSDUTTypeHEICS, 0) == kCFCompareEqualTo) { // HEIC frameDuration = [SDImageHEICCoder frameDurationAtIndex:index source:imageSource]; } else if (CFStringCompare(type, kSDUTTypeWebP, 0) == kCFCompareEqualTo) { // WebP frameDuration = [SDImageAWebPCoder frameDurationAtIndex:index source:imageSource]; } if (!frameDuration) { return; } // Reset super frame duration with the actual frame duration [super setProperty:NSImageCurrentFrameDuration withValue:@(frameDuration)]; } } #pragma clang diagnostic pop @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView+WebCache.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDAnimatedImageView.h" #if SD_UIKIT || SD_MAC #import "SDWebImageManager.h" /** Integrates SDWebImage async downloading and caching of remote images with SDAnimatedImageView. */ @interface SDAnimatedImageView (WebCache) /** * Set the imageView `image` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. */ - (void)sd_setImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT; /** * Set the imageView `image` with an `url` and a placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @see sd_setImageWithURL:placeholderImage:options: */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT; /** * Set the imageView `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT; /** * Set the imageView `image` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; /** * Set the imageView `image` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the imageView `image` with an `url`, placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT; /** * Set the imageView `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the imageView `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the imageView `image` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView+WebCache.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDAnimatedImageView+WebCache.h" #if SD_UIKIT || SD_MAC #import "UIView+WebCache.h" #import "SDAnimatedImage.h" @implementation SDAnimatedImageView (WebCache) - (void)sd_setImageWithURL:(nullable NSURL *)url { [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options { [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:context progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:nil progress:progressBlock completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { Class animatedImageClass = [SDAnimatedImage class]; SDWebImageMutableContext *mutableContext; if (context) { mutableContext = [context mutableCopy]; } else { mutableContext = [NSMutableDictionary dictionary]; } mutableContext[SDWebImageContextAnimatedImageClass] = animatedImageClass; [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options context:mutableContext setImageBlock:nil progress:progressBlock completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { if (completedBlock) { completedBlock(image, error, cacheType, imageURL); } }]; } @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_UIKIT || SD_MAC #import "SDAnimatedImage.h" #import "SDAnimatedImagePlayer.h" #import "SDImageTransformer.h" /** A drop-in replacement for UIImageView/NSImageView, you can use this for animated image rendering. Call `setImage:` with `UIImage(NSImage)` which conforms to `SDAnimatedImage` protocol will start animated image rendering. Call with normal UIImage(NSImage) will back to normal UIImageView(NSImageView) rendering For UIKit: use `-startAnimating`, `-stopAnimating` to control animating. `isAnimating` to check animation state. For AppKit: use `-setAnimates:` to control animating, `animates` to check animation state. This view is layer-backed. */ NS_SWIFT_UI_ACTOR @interface SDAnimatedImageView : UIImageView /** The internal animation player. This property is only used for advanced usage, like inspecting/debugging animation status, control progressive loading, complicated animation frame index control, etc. @warning Pay attention if you directly update the player's property like `totalFrameCount`, `totalLoopCount`, the same property on `SDAnimatedImageView` may not get synced. */ @property (nonatomic, strong, readonly, nullable) SDAnimatedImagePlayer *player; /** The transformer for each decoded animated image frame. We supports post-transform on animated image frame from version 5.20. When you configure the transformer on `SDAnimatedImageView` and animation is playing, the `transformedImageWithImage:forKey:` will be called just after the frame is decoded. (note: The `key` arg is always empty for backward-compatible and may be removed in the future) Example to tint the alpha animated image frame with a black color: * @code imageView.animationTransformer = [SDImageTintTransformer transformerWithColor:UIColor.blackColor]; * @endcode @note The `transformerKey` property is used to ensure the buffer cache available. So make sure it's correct value match the actual logic on transformer. Which means, for the `same frame index + same transformer key`, the transformed image should always be the same. */ @property (nonatomic, strong, nullable) id animationTransformer; /** Current display frame image. This value is KVO Compliance. */ @property (nonatomic, strong, readonly, nullable) UIImage *currentFrame; /** Current frame index, zero based. This value is KVO Compliance. */ @property (nonatomic, assign, readonly) NSUInteger currentFrameIndex; /** Current loop count since its latest animating. This value is KVO Compliance. */ @property (nonatomic, assign, readonly) NSUInteger currentLoopCount; /** YES to choose `animationRepeatCount` property for animation loop count. No to use animated image's `animatedImageLoopCount` instead. Default is NO. */ @property (nonatomic, assign) BOOL shouldCustomLoopCount; /** Total loop count for animated image rendering. Default is animated image's loop count. If you need to set custom loop count, set `shouldCustomLoopCount` to YES and change this value. This class override UIImageView's `animationRepeatCount` property on iOS, use this property as well. */ @property (nonatomic, assign) NSInteger animationRepeatCount; /** The animation playback rate. Default is 1.0. `1.0` means the normal speed. `0.0` means stopping the animation. `0.0-1.0` means the slow speed. `> 1.0` means the fast speed. `< 0.0` is not supported currently and stop animation. (may support reverse playback in the future) */ @property (nonatomic, assign) double playbackRate; /// Asynchronous setup animation playback mode. Default mode is SDAnimatedImagePlaybackModeNormal. @property (nonatomic, assign) SDAnimatedImagePlaybackMode playbackMode; /** Provide a max buffer size by bytes. This is used to adjust frame buffer count and can be useful when the decoding cost is expensive (such as Animated WebP software decoding). Default is 0. `0` means automatically adjust by calculating current memory usage. `1` means without any buffer cache, each of frames will be decoded and then be freed after rendering. (Lowest Memory and Highest CPU) `NSUIntegerMax` means cache all the buffer. (Lowest CPU and Highest Memory) */ @property (nonatomic, assign) NSUInteger maxBufferSize; /** Whehter or not to enable incremental image load for animated image. This is for the animated image which `sd_isIncremental` is YES (See `UIImage+Metadata.h`). If enable, animated image rendering will stop at the last frame available currently, and continue when another `setImage:` trigger, where the new animated image's `animatedImageData` should be updated from the previous one. If the `sd_isIncremental` is NO. The incremental image load stop. @note If you are confused about this description, open Chrome browser to view some large GIF images with low network speed to see the animation behavior. @note The best practice to use incremental load is using `initWithAnimatedCoder:scale:` in `SDAnimatedImage` with animated coder which conform to `SDProgressiveImageCoder` as well. Then call incremental update and incremental decode method to produce the image. Default is YES. Set to NO to only render the static poster for incremental animated image. */ @property (nonatomic, assign) BOOL shouldIncrementalLoad; /** Whether or not to clear the frame buffer cache when animation stopped. See `maxBufferSize` This is useful when you want to limit the memory usage during frequently visibility changes (such as image view inside a list view, then push and pop) Default is NO. */ @property (nonatomic, assign) BOOL clearBufferWhenStopped; /** Whether or not to reset the current frame index when animation stopped. For some of use case, you may want to reset the frame index to 0 when stop, but some other want to keep the current frame index. Default is NO. */ @property (nonatomic, assign) BOOL resetFrameIndexWhenStopped; /** If the image which conforms to `SDAnimatedImage` protocol has more than one frame, set this value to `YES` will automatically play/stop the animation when the view become visible/invisible. Default is YES. */ @property (nonatomic, assign) BOOL autoPlayAnimatedImage; /** You can specify a runloop mode to let it rendering. Default is NSRunLoopCommonModes on multi-core device, NSDefaultRunLoopMode on single-core device @note This is useful for some cases, for example, always specify NSDefaultRunLoopMode, if you want to pause the animation when user scroll (for Mac user, drag the mouse or touchpad) */ @property (nonatomic, copy, nonnull) NSRunLoopMode runLoopMode; @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDAnimatedImageView.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDAnimatedImageView.h" #if SD_UIKIT || SD_MAC #import "UIImage+Metadata.h" #import "NSImage+Compatibility.h" #import "SDInternalMacros.h" #import "objc/runtime.h" // A wrapper to implements the transformer on animated image, like tint color @interface SDAnimatedImageFrameProvider : NSObject @property (nonatomic, strong) id provider; @property (nonatomic, strong) id transformer; @end @implementation SDAnimatedImageFrameProvider - (instancetype)initWithProvider:(id)provider transformer:(id)transformer { self = [super init]; if (self) { _provider = provider; _transformer = transformer; } return self; } - (NSUInteger)hash { NSUInteger prime = 31; NSUInteger result = 1; NSUInteger providerHash = self.provider.hash; NSUInteger transformerHash = self.transformer.transformerKey.hash; result = prime * result + providerHash; result = prime * result + transformerHash; return result; } - (BOOL)isEqual:(id)object { if (nil == object) { return NO; } if (self == object) { return YES; } if (![object isKindOfClass:[self class]]) { return NO; } return self.provider == [object provider] && [self.transformer.transformerKey isEqualToString:[object transformer].transformerKey]; } - (NSData *)animatedImageData { return self.provider.animatedImageData; } - (NSUInteger)animatedImageFrameCount { return self.provider.animatedImageFrameCount; } - (NSUInteger)animatedImageLoopCount { return self.provider.animatedImageLoopCount; } - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index { return [self.provider animatedImageDurationAtIndex:index]; } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { UIImage *frame = [self.provider animatedImageFrameAtIndex:index]; return [self.transformer transformedImageWithImage:frame forKey:@""]; } @end @interface UIImageView () @end @interface SDAnimatedImageView () { BOOL _initFinished; // Extra flag to mark the `commonInit` is called NSRunLoopMode _runLoopMode; NSUInteger _maxBufferSize; double _playbackRate; SDAnimatedImagePlaybackMode _playbackMode; } @property (nonatomic, strong, readwrite) SDAnimatedImagePlayer *player; @property (nonatomic, strong, readwrite) UIImage *currentFrame; @property (nonatomic, assign, readwrite) NSUInteger currentFrameIndex; @property (nonatomic, assign, readwrite) NSUInteger currentLoopCount; @property (nonatomic, assign) BOOL shouldAnimate; @property (nonatomic, assign) BOOL isProgressive; @property (nonatomic) CALayer *imageViewLayer; // The actual rendering layer. @end @implementation SDAnimatedImageView #if SD_UIKIT @dynamic animationRepeatCount; // we re-use this property from `UIImageView` super class on iOS. #endif #pragma mark - Initializers #if SD_MAC + (instancetype)imageViewWithImage:(NSImage *)image { NSRect frame = NSMakeRect(0, 0, image.size.width, image.size.height); SDAnimatedImageView *imageView = [[SDAnimatedImageView alloc] initWithFrame:frame]; [imageView setImage:image]; return imageView; } #else // -initWithImage: isn't documented as a designated initializer of UIImageView, but it actually seems to be. // Using -initWithImage: doesn't call any of the other designated initializers. - (instancetype)initWithImage:(UIImage *)image { self = [super initWithImage:image]; if (self) { [self commonInit]; } return self; } // -initWithImage:highlightedImage: also isn't documented as a designated initializer of UIImageView, but it doesn't call any other designated initializers. - (instancetype)initWithImage:(UIImage *)image highlightedImage:(UIImage *)highlightedImage { self = [super initWithImage:image highlightedImage:highlightedImage]; if (self) { [self commonInit]; } return self; } #endif - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self commonInit]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self commonInit]; } return self; } - (void)commonInit { // Pay attention that UIKit's `initWithImage:` will trigger a `setImage:` during initialization before this `commonInit`. // So the properties which rely on this order, should using lazy-evaluation or do extra check in `setImage:`. self.autoPlayAnimatedImage = YES; self.shouldCustomLoopCount = NO; self.shouldIncrementalLoad = YES; self.playbackRate = 1.0; #if SD_MAC self.wantsLayer = YES; #endif // Mark commonInit finished _initFinished = YES; } #pragma mark - Accessors #pragma mark Public - (void)setImage:(UIImage *)image { if (self.image == image) { return; } // Check Progressive rendering [self updateIsProgressiveWithImage:image]; if (!self.isProgressive) { // Stop animating self.player = nil; self.currentFrame = nil; self.currentFrameIndex = 0; self.currentLoopCount = 0; } // We need call super method to keep function. This will impliedly call `setNeedsDisplay`. But we have no way to avoid this when using animated image. So we call `setNeedsDisplay` again at the end. super.image = image; if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)] && [(id)image animatedImageFrameCount] > 1) { if (!self.player) { id provider; // Check progressive loading if (self.isProgressive) { provider = [(id)image animatedCoder]; } else { provider = (id)image; } // Create animated player if (self.animationTransformer) { // Check if post-transform animation available provider = [[SDAnimatedImageFrameProvider alloc] initWithProvider:provider transformer:self.animationTransformer]; self.player = [SDAnimatedImagePlayer playerWithProvider:provider]; } else { // Normal animation without post-transform self.player = [SDAnimatedImagePlayer playerWithProvider:provider]; } } else { // Update Frame Count self.player.totalFrameCount = [(id)image animatedImageFrameCount]; } if (!self.player) { // animated player nil means the image format is not supported, or frame count <= 1 return; } // Custom Loop Count if (self.shouldCustomLoopCount) { self.player.totalLoopCount = self.animationRepeatCount; } // RunLoop Mode self.player.runLoopMode = self.runLoopMode; // Max Buffer Size self.player.maxBufferSize = self.maxBufferSize; // Play Rate self.player.playbackRate = self.playbackRate; // Play Mode self.player.playbackMode = self.playbackMode; // Setup handler @weakify(self); self.player.animationFrameHandler = ^(NSUInteger index, UIImage * frame) { @strongify(self); self.currentFrameIndex = index; self.currentFrame = frame; [self.imageViewLayer setNeedsDisplay]; }; self.player.animationLoopHandler = ^(NSUInteger loopCount) { @strongify(self); // Progressive image reach the current last frame index. Keep the state and pause animating. Wait for later restart if (self.isProgressive) { NSUInteger lastFrameIndex = self.player.totalFrameCount - 1; [self.player seekToFrameAtIndex:lastFrameIndex loopCount:0]; [self.player pausePlaying]; } else { self.currentLoopCount = loopCount; } }; // Ensure disabled highlighting; it's not supported (see `-setHighlighted:`). super.highlighted = NO; [self stopAnimating]; [self checkPlay]; } [self.imageViewLayer setNeedsDisplay]; } #pragma mark - Configuration - (void)setRunLoopMode:(NSRunLoopMode)runLoopMode { _runLoopMode = [runLoopMode copy]; self.player.runLoopMode = runLoopMode; } - (NSRunLoopMode)runLoopMode { if (!_runLoopMode) { _runLoopMode = [[self class] defaultRunLoopMode]; } return _runLoopMode; } + (NSString *)defaultRunLoopMode { // Key off `activeProcessorCount` (as opposed to `processorCount`) since the system could shut down cores in certain situations. return [NSProcessInfo processInfo].activeProcessorCount > 1 ? NSRunLoopCommonModes : NSDefaultRunLoopMode; } - (void)setMaxBufferSize:(NSUInteger)maxBufferSize { _maxBufferSize = maxBufferSize; self.player.maxBufferSize = maxBufferSize; } - (NSUInteger)maxBufferSize { return _maxBufferSize; // Defaults to 0 } - (void)setPlaybackRate:(double)playbackRate { _playbackRate = playbackRate; self.player.playbackRate = playbackRate; } - (double)playbackRate { if (!_initFinished) { return 1.0; // Defaults to 1.0 } return _playbackRate; } - (void)setPlaybackMode:(SDAnimatedImagePlaybackMode)playbackMode { _playbackMode = playbackMode; self.player.playbackMode = playbackMode; } - (SDAnimatedImagePlaybackMode)playbackMode { if (!_initFinished) { return SDAnimatedImagePlaybackModeNormal; // Default mode is normal } return _playbackMode; } - (BOOL)shouldIncrementalLoad { if (!_initFinished) { return YES; // Defaults to YES } return _initFinished; } #pragma mark - UIView Method Overrides #pragma mark Observing View-Related Changes #if SD_MAC - (void)viewDidMoveToSuperview #else - (void)didMoveToSuperview #endif { #if SD_MAC [super viewDidMoveToSuperview]; #else [super didMoveToSuperview]; #endif [self checkPlay]; } #if SD_MAC - (void)viewDidMoveToWindow #else - (void)didMoveToWindow #endif { #if SD_MAC [super viewDidMoveToWindow]; #else [super didMoveToWindow]; #endif [self checkPlay]; } #if SD_MAC - (void)setAlphaValue:(CGFloat)alphaValue #else - (void)setAlpha:(CGFloat)alpha #endif { #if SD_MAC [super setAlphaValue:alphaValue]; #else [super setAlpha:alpha]; #endif [self checkPlay]; } - (void)setHidden:(BOOL)hidden { [super setHidden:hidden]; [self checkPlay]; } #pragma mark - UIImageView Method Overrides #pragma mark Image Data - (void)setAnimationRepeatCount:(NSInteger)animationRepeatCount { #if SD_UIKIT [super setAnimationRepeatCount:animationRepeatCount]; #else _animationRepeatCount = animationRepeatCount; #endif if (self.shouldCustomLoopCount) { self.player.totalLoopCount = animationRepeatCount; } } - (void)startAnimating { if (self.player) { [self updateShouldAnimate]; if (self.shouldAnimate) { [self.player startPlaying]; } } else { #if SD_UIKIT [super startAnimating]; #else [super setAnimates:YES]; #endif } } - (void)stopAnimating { if (self.player) { if (self.resetFrameIndexWhenStopped) { [self.player stopPlaying]; } else { [self.player pausePlaying]; } if (self.clearBufferWhenStopped) { [self.player clearFrameBuffer]; } } else { #if SD_UIKIT [super stopAnimating]; #else [super setAnimates:NO]; #endif } } #if SD_UIKIT - (BOOL)isAnimating { if (self.player) { return self.player.isPlaying; } else { return [super isAnimating]; } } #endif #if SD_MAC - (BOOL)animates { if (self.player) { return self.player.isPlaying; } else { return [super animates]; } } - (void)setAnimates:(BOOL)animates { if (animates) { [self startAnimating]; } else { [self stopAnimating]; } } #endif #pragma mark Highlighted Image Unsupport - (void)setHighlighted:(BOOL)highlighted { // Highlighted image is unsupported for animated images, but implementing it breaks the image view when embedded in a UICollectionViewCell. if (!self.player) { [super setHighlighted:highlighted]; } } #pragma mark - Private Methods #pragma mark Animation /// Check if it should be played - (void)checkPlay { // Only handle for SDAnimatedImage, leave UIAnimatedImage or animationImages for super implementation control if (self.player && self.autoPlayAnimatedImage) { [self updateShouldAnimate]; if (self.shouldAnimate) { [self startAnimating]; } else { [self stopAnimating]; } } } // Don't repeatedly check our window & superview in `-displayDidRefresh:` for performance reasons. // Just update our cached value whenever the animated image or visibility (window, superview, hidden, alpha) is changed. - (void)updateShouldAnimate { #if SD_MAC BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alphaValue > 0.0; #else BOOL isVisible = self.window && self.superview && ![self isHidden] && self.alpha > 0.0; #endif self.shouldAnimate = self.player && isVisible; } // Update progressive status only after `setImage:` call. - (void)updateIsProgressiveWithImage:(UIImage *)image { self.isProgressive = NO; if (!self.shouldIncrementalLoad) { // Early return return; } // We must use `image.class conformsToProtocol:` instead of `image conformsToProtocol:` here // Because UIKit on macOS, using internal hard-coded override method, which returns NO id currentAnimatedCoder = [self progressiveAnimatedCoderForImage:image]; if (currentAnimatedCoder) { UIImage *previousImage = self.image; if (!previousImage) { // If current animated coder supports progressive, and no previous image to check, start progressive loading self.isProgressive = YES; } else { id previousAnimatedCoder = [self progressiveAnimatedCoderForImage:previousImage]; if (previousAnimatedCoder == currentAnimatedCoder) { // If current animated coder is the same as previous, start progressive loading self.isProgressive = YES; } } } } // Check if image can represent a `Progressive Animated Image` during loading - (id)progressiveAnimatedCoderForImage:(UIImage *)image { if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)] && image.sd_isIncremental && [image respondsToSelector:@selector(animatedCoder)]) { id animatedCoder = [(id)image animatedCoder]; if ([animatedCoder respondsToSelector:@selector(initIncrementalWithOptions:)]) { return (id)animatedCoder; } } return nil; } #pragma mark Providing the Layer's Content #pragma mark - CALayerDelegate - (void)displayLayer:(CALayer *)layer { UIImage *currentFrame = self.currentFrame; if (currentFrame) { layer.contentsScale = currentFrame.scale; layer.contents = (__bridge id)currentFrame.CGImage; } else { // If we have no animation frames, call super implementation. iOS 14+ UIImageView use this delegate method for rendering. if ([UIImageView instancesRespondToSelector:@selector(displayLayer:)]) { [super displayLayer:layer]; } else { // Fallback to implements the static image rendering by ourselves (like macOS or before iOS 14) currentFrame = super.image; layer.contentsScale = currentFrame.scale; layer.contents = (__bridge id)currentFrame.CGImage; } } } #if SD_UIKIT - (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection { // See: #3635 // From iOS 17, when UIImageView entering the background, it will receive the trait collection changes, and modify the CALayer.contents by `self.image.CGImage` // However, For animated image, `self.image.CGImge != self.currentFrame.CGImage`, right ? // So this cause the render issue, we need to reset the CALayer.contents again [super traitCollectionDidChange:previousTraitCollection]; [self.imageViewLayer setNeedsDisplay]; } #endif #if SD_MAC // NSImageView use a subview. We need this subview's layer for actual rendering. // Why using this design may because of properties like `imageAlignment` and `imageScaling`, which it's not available for UIImageView.contentMode (it's impossible to align left and keep aspect ratio at the same time) - (NSView *)imageView { NSImageView *imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageView)); if (!imageView) { // macOS 10.14 imageView = objc_getAssociatedObject(self, SD_SEL_SPI(imageSubview)); } return imageView; } // on macOS, it's the imageView subview's layer (we use layer-hosting view to let CALayerDelegate works) - (CALayer *)imageViewLayer { NSView *imageView = self.imageView; if (!imageView) { return nil; } if (!_imageViewLayer) { _imageViewLayer = [CALayer new]; _imageViewLayer.delegate = self; imageView.layer = _imageViewLayer; imageView.wantsLayer = YES; } return _imageViewLayer; } #else // on iOS, it's the imageView itself's layer - (CALayer *)imageViewLayer { return self.layer; } #endif @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDCallbackQueue.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" /// SDCallbackPolicy controls how we execute the block on the queue, like whether to use `dispatch_async/dispatch_sync`, check if current queue match target queue, or just invoke without any context. typedef NS_ENUM(NSUInteger, SDCallbackPolicy) { /// When the current queue is equal to callback queue, sync/async will just invoke `block` directly without dispatch. Else it use `dispatch_async`/`dispatch_sync` to dispatch block on queue. This is useful for UIKit rendering to ensure all blocks executed in the same runloop SDCallbackPolicySafeExecute = 0, /// Follow async/sync using the correspond `dispatch_async`/`dispatch_sync` to dispatch block on queue SDCallbackPolicyDispatch = 1, /// Ignore any async/sync and just directly invoke `block` in current queue (without `dispatch_async`/`dispatch_sync`) SDCallbackPolicyInvoke = 2, /// Ensure callback in main thread. Do `dispatch_async` if the `NSThread.isMainTrhead == true` ; else do invoke `block`. Never use `dispatch_sync`, suitable for special UI-related code SDCallbackPolicySafeAsyncMainThread = 3, }; /// SDCallbackQueue is a wrapper used to control how the completionBlock should perform on queues, used by our `Cache`/`Manager`/`Loader`. /// Useful when you call SDWebImage in non-main queue and want to avoid it callback into main queue, which may cause issue. @interface SDCallbackQueue : NSObject /// The main queue. This is the default value, has the same effect when passing `nil` to `SDWebImageContextCallbackQueue` /// The policy defaults to `SDCallbackPolicySafeAsyncMainThread` @property (nonnull, class, readonly) SDCallbackQueue *mainQueue; /// The caller current queue. Using `dispatch_get_current_queue`. This is not a dynamic value and only keep the first call time queue. /// The policy defaults to `SDCallbackPolicySafeExecute` @property (nonnull, class, readonly) SDCallbackQueue *currentQueue; /// The global concurrent queue (user-initiated QoS). Using `dispatch_get_global_queue`. /// The policy defaults to `SDCallbackPolicySafeExecute` @property (nonnull, class, readonly) SDCallbackQueue *globalQueue; /// The current queue's callback policy. @property (nonatomic, assign, readwrite) SDCallbackPolicy policy; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; /// Create the callback queue with a GCD queue. The policy defaults to `SDCallbackPolicySafeExecute` /// - Parameter queue: The GCD queue, should not be NULL - (nonnull instancetype)initWithDispatchQueue:(nonnull dispatch_queue_t)queue NS_DESIGNATED_INITIALIZER; #pragma mark - Execution Entry /// Submits a block for execution and returns after that block finishes executing. /// - Parameter block: The block that contains the work to perform. - (void)sync:(nonnull dispatch_block_t)block API_DEPRECATED("No longer use, may cause deadlock when misused", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0));; /// Schedules a block asynchronously for execution. /// - Parameter block: The block that contains the work to perform. - (void)async:(nonnull dispatch_block_t)block; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDCallbackQueue.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDCallbackQueue.h" @interface SDCallbackQueue () @property (nonatomic, strong, nonnull) dispatch_queue_t queue; @end static inline void SDSafeAsyncMainThread(dispatch_block_t _Nonnull block) { if (NSThread.isMainThread) { block(); } else { dispatch_async(dispatch_get_main_queue(), block); } } static void SDSafeExecute(dispatch_queue_t queue, dispatch_block_t _Nonnull block, BOOL async) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" dispatch_queue_t currentQueue = dispatch_get_current_queue(); #pragma clang diagnostic pop if (queue == currentQueue) { block(); return; } // Special handle for main queue only if (NSThread.isMainThread && queue == dispatch_get_main_queue()) { block(); return; } if (async) { dispatch_async(queue, block); } else { dispatch_sync(queue, block); } } @implementation SDCallbackQueue - (instancetype)initWithDispatchQueue:(dispatch_queue_t)queue { self = [super init]; if (self) { NSCParameterAssert(queue); _queue = queue; _policy = SDCallbackPolicySafeExecute; } return self; } + (SDCallbackQueue *)mainQueue { SDCallbackQueue *queue = [[SDCallbackQueue alloc] initWithDispatchQueue:dispatch_get_main_queue()]; queue->_policy = SDCallbackPolicySafeAsyncMainThread; return queue; } + (SDCallbackQueue *)currentQueue { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" SDCallbackQueue *queue = [[SDCallbackQueue alloc] initWithDispatchQueue:dispatch_get_current_queue()]; #pragma clang diagnostic pop return queue; } + (SDCallbackQueue *)globalQueue { SDCallbackQueue *queue = [[SDCallbackQueue alloc] initWithDispatchQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)]; return queue; } - (void)sync:(nonnull dispatch_block_t)block { switch (self.policy) { case SDCallbackPolicySafeExecute: SDSafeExecute(self.queue, block, NO); break; case SDCallbackPolicyDispatch: dispatch_sync(self.queue, block); break; case SDCallbackPolicyInvoke: block(); break; case SDCallbackPolicySafeAsyncMainThread: SDSafeAsyncMainThread(block); break; default: NSCAssert(NO, @"unexpected policy %tu", self.policy); break; } } - (void)async:(nonnull dispatch_block_t)block { switch (self.policy) { case SDCallbackPolicySafeExecute: SDSafeExecute(self.queue, block, YES); break; case SDCallbackPolicyDispatch: dispatch_async(self.queue, block); break; case SDCallbackPolicyInvoke: block(); break; case SDCallbackPolicySafeAsyncMainThread: SDSafeAsyncMainThread(block); break; default: NSCAssert(NO, @"unexpected policy %tu", self.policy); break; } } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDDiskCache.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" @class SDImageCacheConfig; /** A protocol to allow custom disk cache used in SDImageCache. */ @protocol SDDiskCache // All of these method are called from the same global queue to avoid blocking on main queue and thread-safe problem. But it's also recommend to ensure thread-safe yourself using lock or other ways. @required /** Create a new disk cache based on the specified path. You can check `maxDiskSize` and `maxDiskAge` used for disk cache. @param cachePath Full path of a directory in which the cache will write data. Once initialized you should not read and write to this directory. @param config The cache config to be used to create the cache. @return A new cache object, or nil if an error occurs. */ - (nullable instancetype)initWithCachePath:(nonnull NSString *)cachePath config:(nonnull SDImageCacheConfig *)config; /** Returns a boolean value that indicates whether a given key is in cache. This method may blocks the calling thread until file read finished. @param key A string identifying the data. If nil, just return NO. @return Whether the key is in cache. */ - (BOOL)containsDataForKey:(nonnull NSString *)key; /** Returns the data associated with a given key. This method may blocks the calling thread until file read finished. @param key A string identifying the data. If nil, just return nil. @return The value associated with key, or nil if no value is associated with key. */ - (nullable NSData *)dataForKey:(nonnull NSString *)key; /** Sets the value of the specified key in the cache. This method may blocks the calling thread until file write finished. @param data The data to be stored in the cache. @param key The key with which to associate the value. If nil, this method has no effect. */ - (void)setData:(nullable NSData *)data forKey:(nonnull NSString *)key; /** Returns the extended data associated with a given key. This method may blocks the calling thread until file read finished. @param key A string identifying the data. If nil, just return nil. @return The value associated with key, or nil if no value is associated with key. */ - (nullable NSData *)extendedDataForKey:(nonnull NSString *)key; /** Set extended data with a given key. @discussion You can set any extended data to exist cache key. Without override the exist disk file data. on UNIX, the common way for this is to use the Extended file attributes (xattr) @param extendedData The extended data (pass nil to remove). @param key The key with which to associate the value. If nil, this method has no effect. */ - (void)setExtendedData:(nullable NSData *)extendedData forKey:(nonnull NSString *)key; /** Removes the value of the specified key in the cache. This method may blocks the calling thread until file delete finished. @param key The key identifying the value to be removed. If nil, this method has no effect. */ - (void)removeDataForKey:(nonnull NSString *)key; /** Empties the cache. This method may blocks the calling thread until file delete finished. */ - (void)removeAllData; /** Removes the expired data from the cache. You can choose the data to remove base on `ageLimit`, `countLimit` and `sizeLimit` options. */ - (void)removeExpiredData; /** The cache path for key @param key A string identifying the value @return The cache path for key. Or nil if the key can not associate to a path */ - (nullable NSString *)cachePathForKey:(nonnull NSString *)key; /** Returns the number of data in this cache. This method may blocks the calling thread until file read finished. @return The total data count. */ - (NSUInteger)totalCount; /** Returns the total size (in bytes) of data in this cache. This method may blocks the calling thread until file read finished. @return The total data size in bytes. */ - (NSUInteger)totalSize; @end /** The built-in disk cache. */ @interface SDDiskCache : NSObject /** Cache Config object - storing all kind of settings. */ @property (nonatomic, strong, readonly, nonnull) SDImageCacheConfig *config; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; /** Move the cache directory from old location to new location, the old location will be removed after finish. If the old location does not exist, does nothing. If the new location does not exist, only do a movement of directory. If the new location does exist, will move and merge the files from old location. If the new location does exist, but is not a directory, will remove it and do a movement of directory. @param srcPath old location of cache directory @param dstPath new location of cache directory */ - (void)moveCacheDirectoryFromPath:(nonnull NSString *)srcPath toPath:(nonnull NSString *)dstPath; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDDiskCache.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDDiskCache.h" #import "SDImageCacheConfig.h" #import "SDFileAttributeHelper.h" #import static NSString * const SDDiskCacheExtendedAttributeName = @"com.hackemist.SDDiskCache"; @interface SDDiskCache () @property (nonatomic, copy) NSString *diskCachePath; @property (nonatomic, strong, nonnull) NSFileManager *fileManager; @end @implementation SDDiskCache - (instancetype)init { NSAssert(NO, @"Use `initWithCachePath:` with the disk cache path"); return nil; } #pragma mark - SDcachePathForKeyDiskCache Protocol - (instancetype)initWithCachePath:(NSString *)cachePath config:(nonnull SDImageCacheConfig *)config { if (self = [super init]) { _diskCachePath = cachePath; _config = config; [self commonInit]; } return self; } - (void)commonInit { if (self.config.fileManager) { self.fileManager = self.config.fileManager; } else { self.fileManager = [NSFileManager new]; } [self createDirectory]; } - (BOOL)containsDataForKey:(NSString *)key { NSParameterAssert(key); NSString *filePath = [self cachePathForKey:key]; BOOL exists = [self.fileManager fileExistsAtPath:filePath]; // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name // checking the key with and without the extension if (!exists) { exists = [self.fileManager fileExistsAtPath:filePath.stringByDeletingPathExtension]; } return exists; } - (NSData *)dataForKey:(NSString *)key { NSParameterAssert(key); NSString *filePath = [self cachePathForKey:key]; // if filePath is nil or (null),framework will crash with this: // Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[_NSPlaceholderData initWithContentsOfFile:options:maxLength:error:]: nil file argument' if (filePath == nil || [@"(null)" isEqualToString: filePath]) { return nil; } NSData *data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil]; if (data) { [[NSURL fileURLWithPath:filePath] setResourceValue:[NSDate date] forKey:NSURLContentAccessDateKey error:nil]; return data; } // fallback because of https://github.com/rs/SDWebImage/pull/976 that added the extension to the disk file name // checking the key with and without the extension filePath = filePath.stringByDeletingPathExtension; data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil]; if (data) { [[NSURL fileURLWithPath:filePath] setResourceValue:[NSDate date] forKey:NSURLContentAccessDateKey error:nil]; return data; } return nil; } - (void)setData:(NSData *)data forKey:(NSString *)key { NSParameterAssert(data); NSParameterAssert(key); // get cache Path for image key NSString *cachePathForKey = [self cachePathForKey:key]; // transform to NSURL NSURL *fileURL = [NSURL fileURLWithPath:cachePathForKey isDirectory:NO]; [data writeToURL:fileURL options:self.config.diskCacheWritingOptions error:nil]; } - (NSData *)extendedDataForKey:(NSString *)key { NSParameterAssert(key); // get cache Path for image key NSString *cachePathForKey = [self cachePathForKey:key]; NSData *extendedData = [SDFileAttributeHelper extendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; return extendedData; } - (void)setExtendedData:(NSData *)extendedData forKey:(NSString *)key { NSParameterAssert(key); // get cache Path for image key NSString *cachePathForKey = [self cachePathForKey:key]; if (!extendedData) { // Remove [SDFileAttributeHelper removeExtendedAttribute:SDDiskCacheExtendedAttributeName atPath:cachePathForKey traverseLink:NO error:nil]; } else { // Override [SDFileAttributeHelper setExtendedAttribute:SDDiskCacheExtendedAttributeName value:extendedData atPath:cachePathForKey traverseLink:NO overwrite:YES error:nil]; } } - (void)removeDataForKey:(NSString *)key { NSParameterAssert(key); NSString *filePath = [self cachePathForKey:key]; [self.fileManager removeItemAtPath:filePath error:nil]; } - (void)removeAllData { [self.fileManager removeItemAtPath:self.diskCachePath error:nil]; [self createDirectory]; } - (void)createDirectory { [self.fileManager createDirectoryAtPath:self.diskCachePath withIntermediateDirectories:YES attributes:nil error:NULL]; // disable iCloud backup if (self.config.shouldDisableiCloud) { // ignore iCloud backup resource value error [[NSURL fileURLWithPath:self.diskCachePath isDirectory:YES] setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil]; } } - (void)removeExpiredData { NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; // Compute content date key to be used for tests NSURLResourceKey cacheContentDateKey; switch (self.config.diskCacheExpireType) { case SDImageCacheConfigExpireTypeModificationDate: cacheContentDateKey = NSURLContentModificationDateKey; break; case SDImageCacheConfigExpireTypeCreationDate: cacheContentDateKey = NSURLCreationDateKey; break; case SDImageCacheConfigExpireTypeChangeDate: cacheContentDateKey = NSURLAttributeModificationDateKey; break; case SDImageCacheConfigExpireTypeAccessDate: default: cacheContentDateKey = NSURLContentAccessDateKey; break; } NSArray *resourceKeys = @[NSURLIsDirectoryKey, cacheContentDateKey, NSURLTotalFileAllocatedSizeKey]; // This enumerator prefetches useful properties for our cache files. NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:resourceKeys options:NSDirectoryEnumerationSkipsHiddenFiles errorHandler:NULL]; NSDate *expirationDate = (self.config.maxDiskAge < 0) ? nil: [NSDate dateWithTimeIntervalSinceNow:-self.config.maxDiskAge]; NSMutableDictionary *> *cacheFiles = [NSMutableDictionary dictionary]; NSUInteger currentCacheSize = 0; // Enumerate all of the files in the cache directory. This loop has two purposes: // // 1. Removing files that are older than the expiration date. // 2. Storing file attributes for the size-based cleanup pass. NSMutableArray *urlsToDelete = [[NSMutableArray alloc] init]; for (NSURL *fileURL in fileEnumerator) { @autoreleasepool { NSError *error; NSDictionary *resourceValues = [fileURL resourceValuesForKeys:resourceKeys error:&error]; // Skip directories and errors. if (error || !resourceValues || [resourceValues[NSURLIsDirectoryKey] boolValue]) { continue; } // Remove files that are older than the expiration date; NSDate *modifiedDate = resourceValues[cacheContentDateKey]; if (expirationDate && [[modifiedDate laterDate:expirationDate] isEqualToDate:expirationDate]) { [urlsToDelete addObject:fileURL]; continue; } // Store a reference to this file and account for its total size. NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize += totalAllocatedSize.unsignedIntegerValue; cacheFiles[fileURL] = resourceValues; } } for (NSURL *fileURL in urlsToDelete) { [self.fileManager removeItemAtURL:fileURL error:nil]; } // If our remaining disk cache exceeds a configured maximum size, perform a second // size-based cleanup pass. We delete the oldest files first. NSUInteger maxDiskSize = self.config.maxDiskSize; if (maxDiskSize > 0 && currentCacheSize > maxDiskSize) { // Target half of our maximum cache size for this cleanup pass. const NSUInteger desiredCacheSize = maxDiskSize / 2; // Sort the remaining cache files by their last modification time or last access time (oldest first). NSArray *sortedFiles = [cacheFiles keysSortedByValueWithOptions:NSSortConcurrent usingComparator:^NSComparisonResult(id obj1, id obj2) { return [obj1[cacheContentDateKey] compare:obj2[cacheContentDateKey]]; }]; // Delete files until we fall below our desired cache size. for (NSURL *fileURL in sortedFiles) { if ([self.fileManager removeItemAtURL:fileURL error:nil]) { NSDictionary *resourceValues = cacheFiles[fileURL]; NSNumber *totalAllocatedSize = resourceValues[NSURLTotalFileAllocatedSizeKey]; currentCacheSize -= totalAllocatedSize.unsignedIntegerValue; if (currentCacheSize < desiredCacheSize) { break; } } } } } - (nullable NSString *)cachePathForKey:(NSString *)key { NSParameterAssert(key); return [self cachePathForKey:key inPath:self.diskCachePath]; } - (NSUInteger)totalSize { NSUInteger size = 0; // Use URL-based enumerator instead of Path(NSString *)-based enumerator to reduce // those objects(ex. NSPathStore2/_NSCFString/NSConcreteData) created during traversal. // Even worse, those objects are added into AutoreleasePool, in background threads, // the time to release those objects is undifined(according to the usage of CPU) // It will truely consumes a lot of VM, up to cause OOMs. @autoreleasepool { NSURL *pathURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:pathURL includingPropertiesForKeys:@[NSURLFileSizeKey] options:(NSDirectoryEnumerationOptions)0 errorHandler:NULL]; for (NSURL *fileURL in fileEnumerator) { @autoreleasepool { NSNumber *fileSize; [fileURL getResourceValue:&fileSize forKey:NSURLFileSizeKey error:NULL]; size += fileSize.unsignedIntegerValue; } } } return size; } - (NSUInteger)totalCount { NSUInteger count = 0; @autoreleasepool { NSURL *diskCacheURL = [NSURL fileURLWithPath:self.diskCachePath isDirectory:YES]; NSDirectoryEnumerator *fileEnumerator = [self.fileManager enumeratorAtURL:diskCacheURL includingPropertiesForKeys:@[] options:(NSDirectoryEnumerationOptions)0 errorHandler:nil]; count = fileEnumerator.allObjects.count; } return count; } #pragma mark - Cache paths - (nullable NSString *)cachePathForKey:(nullable NSString *)key inPath:(nonnull NSString *)path { NSString *filename = SDDiskCacheFileNameForKey(key); return [path stringByAppendingPathComponent:filename]; } - (void)moveCacheDirectoryFromPath:(nonnull NSString *)srcPath toPath:(nonnull NSString *)dstPath { NSParameterAssert(srcPath); NSParameterAssert(dstPath); // Check if old path is equal to new path if ([srcPath isEqualToString:dstPath]) { return; } BOOL isDirectory; // Check if old path is directory if (![self.fileManager fileExistsAtPath:srcPath isDirectory:&isDirectory] || !isDirectory) { return; } // Check if new path is directory if (![self.fileManager fileExistsAtPath:dstPath isDirectory:&isDirectory] || !isDirectory) { if (!isDirectory) { // New path is not directory, remove file [self.fileManager removeItemAtPath:dstPath error:nil]; } NSString *dstParentPath = [dstPath stringByDeletingLastPathComponent]; // Creates any non-existent parent directories as part of creating the directory in path if (![self.fileManager fileExistsAtPath:dstParentPath]) { [self.fileManager createDirectoryAtPath:dstParentPath withIntermediateDirectories:YES attributes:nil error:NULL]; } // New directory does not exist, rename directory [self.fileManager moveItemAtPath:srcPath toPath:dstPath error:nil]; // disable iCloud backup if (self.config.shouldDisableiCloud) { // ignore iCloud backup resource value error [[NSURL fileURLWithPath:dstPath isDirectory:YES] setResourceValue:@YES forKey:NSURLIsExcludedFromBackupKey error:nil]; } } else { // New directory exist, merge the files NSURL *srcURL = [NSURL fileURLWithPath:srcPath isDirectory:YES]; NSDirectoryEnumerator *srcDirEnumerator = [self.fileManager enumeratorAtURL:srcURL includingPropertiesForKeys:@[] options:(NSDirectoryEnumerationOptions)0 errorHandler:NULL]; for (NSURL *url in srcDirEnumerator) { @autoreleasepool { NSString *dstFilePath = [dstPath stringByAppendingPathComponent:url.lastPathComponent]; NSURL *dstFileURL = [NSURL fileURLWithPath:dstFilePath isDirectory:NO]; [self.fileManager moveItemAtURL:url toURL:dstFileURL error:nil]; } } // Remove the old path [self.fileManager removeItemAtURL:srcURL error:nil]; } } #pragma mark - Hash static inline NSString *SDSanitizeFileNameString(NSString * _Nullable fileName) { if ([fileName length] == 0) { return fileName; } // note: `:` is the only invalid char on Apple file system // but `/` or `\` is valid // \0 is also special case (which cause Foundation API treat the C string as EOF) NSCharacterSet* illegalFileNameCharacters = [NSCharacterSet characterSetWithCharactersInString:@"\0:"]; return [[fileName componentsSeparatedByCharactersInSet:illegalFileNameCharacters] componentsJoinedByString:@""]; } #define SD_MAX_FILE_EXTENSION_LENGTH (NAME_MAX - CC_MD5_DIGEST_LENGTH * 2 - 1) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" static inline NSString * _Nonnull SDDiskCacheFileNameForKey(NSString * _Nullable key) { const char *str = key.UTF8String; if (str == NULL) { str = ""; } unsigned char r[CC_MD5_DIGEST_LENGTH]; CC_MD5(str, (CC_LONG)strlen(str), r); NSString *ext; // 1. Use URL path extname if valid NSURL *keyURL = [NSURL URLWithString:key]; if (keyURL) { ext = keyURL.pathExtension; } // 2. Use file extname if valid if (!ext) { ext = key.pathExtension; } // 3. Check if extname valid on file system ext = SDSanitizeFileNameString(ext); // File system has file name length limit, we need to check if ext is too long, we don't add it to the filename if (ext.length > SD_MAX_FILE_EXTENSION_LENGTH) { ext = nil; } NSString *filename = [NSString stringWithFormat:@"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%@", r[0], r[1], r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9], r[10], r[11], r[12], r[13], r[14], r[15], ext.length == 0 ? @"" : [NSString stringWithFormat:@".%@", ext]]; return filename; } #pragma clang diagnostic pop @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" /** These following class are provided to use `UIGraphicsImageRenderer` with polyfill, which allows write cross-platform(AppKit/UIKit) code and avoid runtime version check. Compared to `UIGraphicsBeginImageContext`, `UIGraphicsImageRenderer` use dynamic bitmap from your draw code to generate CGContext, not always use ARGB8888, which is more performant on RAM usage. Which means, if you draw CGImage/CIImage which contains grayscale only, the underlaying bitmap context use grayscale, it's managed by system and not a fixed type. (actually, the `kCGContextTypeAutomatic`) For usage, See more in Apple's documentation: https://developer.apple.com/documentation/uikit/uigraphicsimagerenderer For UIKit on iOS/tvOS 10+, these method just use the same `UIGraphicsImageRenderer` API. For others (macOS/watchOS or iOS/tvOS 10-), these method use the `SDImageGraphics.h` to implements the same behavior (but without dynamic bitmap support) */ /// A closure for drawing an image. typedef void (^SDGraphicsImageDrawingActions)(CGContextRef _Nonnull context); /// Constants that specify the color range of the image renderer context. typedef NS_ENUM(NSInteger, SDGraphicsImageRendererFormatRange) { /// The image renderer context doesn’t specify a color range. SDGraphicsImageRendererFormatRangeUnspecified = -1, /// The system automatically chooses the image renderer context’s pixel format according to the color range of its content. SDGraphicsImageRendererFormatRangeAutomatic = 0, /// The image renderer context supports wide color. SDGraphicsImageRendererFormatRangeExtended, /// The image renderer context doesn’t support extended colors. SDGraphicsImageRendererFormatRangeStandard }; /// A set of drawing attributes that represent the configuration of an image renderer context. @interface SDGraphicsImageRendererFormat : NSObject #if SD_UIKIT /// The underlying uiformat for UIKit. This usage of this API should be careful, which may cause out of sync. @property (nonatomic, strong, nonnull) UIGraphicsImageRendererFormat *uiformat API_AVAILABLE(ios(10.0), tvos(10.0)); #endif /// The display scale of the image renderer context. /// The default value is equal to the scale of the main screen. @property (nonatomic) CGFloat scale; /// A Boolean value indicating whether the underlying Core Graphics context has an alpha channel. /// The default value is NO. @property (nonatomic) BOOL opaque; /// Specifying whether the bitmap context should use extended color. /// For iOS 12+, the value is from system `preferredRange` property /// For iOS 10-11, the value is from system `prefersExtendedRange` property /// For iOS 9-, the value is `.standard` @property (nonatomic) SDGraphicsImageRendererFormatRange preferredRange; /// Init the default format. See each properties's default value. - (nonnull instancetype)init; /// Returns a new format best suited for the main screen’s current configuration. + (nonnull instancetype)preferredFormat; @end /// A graphics renderer for creating Core Graphics-backed images. @interface SDGraphicsImageRenderer : NSObject /// Creates an image renderer for drawing images of a given size. /// @param size The size of images output from the renderer, specified in points. /// @return An initialized image renderer. - (nonnull instancetype)initWithSize:(CGSize)size; /// Creates a new image renderer with a given size and format. /// @param size The size of images output from the renderer, specified in points. /// @param format A SDGraphicsImageRendererFormat object that encapsulates the format used to create the renderer context. /// @return An initialized image renderer. - (nonnull instancetype)initWithSize:(CGSize)size format:(nonnull SDGraphicsImageRendererFormat *)format; /// Creates an image by following a set of drawing instructions. /// @param actions A SDGraphicsImageDrawingActions block that, when invoked by the renderer, executes a set of drawing instructions to create the output image. /// @note You should not retain or use the context outside the block, it's non-escaping. /// @return A UIImage object created by the supplied drawing actions. - (nonnull UIImage *)imageWithActions:(nonnull NS_NOESCAPE SDGraphicsImageDrawingActions)actions; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDGraphicsImageRenderer.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDGraphicsImageRenderer.h" #import "SDImageGraphics.h" #import "SDDeviceHelper.h" @implementation SDGraphicsImageRendererFormat @synthesize scale = _scale; @synthesize opaque = _opaque; @synthesize preferredRange = _preferredRange; #pragma mark - Property - (CGFloat)scale { #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.10, *)) { return self.uiformat.scale; } else { return _scale; } #else return _scale; #endif } - (void)setScale:(CGFloat)scale { #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.10, *)) { self.uiformat.scale = scale; } else { _scale = scale; } #else _scale = scale; #endif } - (BOOL)opaque { #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.10, *)) { return self.uiformat.opaque; } else { return _opaque; } #else return _opaque; #endif } - (void)setOpaque:(BOOL)opaque { #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.10, *)) { self.uiformat.opaque = opaque; } else { _opaque = opaque; } #else _opaque = opaque; #endif } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (SDGraphicsImageRendererFormatRange)preferredRange { #if SD_VISION return (SDGraphicsImageRendererFormatRange)self.uiformat.preferredRange; #elif SD_UIKIT if (@available(iOS 10.0, tvOS 10.10, *)) { if (@available(iOS 12.0, tvOS 12.0, *)) { return (SDGraphicsImageRendererFormatRange)self.uiformat.preferredRange; } else { BOOL prefersExtendedRange = self.uiformat.prefersExtendedRange; if (prefersExtendedRange) { return SDGraphicsImageRendererFormatRangeExtended; } else { return SDGraphicsImageRendererFormatRangeStandard; } } } else { return _preferredRange; } #else return _preferredRange; #endif } - (void)setPreferredRange:(SDGraphicsImageRendererFormatRange)preferredRange { #if SD_VISION self.uiformat.preferredRange = (UIGraphicsImageRendererFormatRange)preferredRange; #elif SD_UIKIT if (@available(iOS 10.0, tvOS 10.10, *)) { if (@available(iOS 12.0, tvOS 12.0, *)) { self.uiformat.preferredRange = (UIGraphicsImageRendererFormatRange)preferredRange; } else { switch (preferredRange) { case SDGraphicsImageRendererFormatRangeExtended: self.uiformat.prefersExtendedRange = YES; break; case SDGraphicsImageRendererFormatRangeStandard: self.uiformat.prefersExtendedRange = NO; default: // Automatic means default break; } } } else { _preferredRange = preferredRange; } #else _preferredRange = preferredRange; #endif } #pragma clang diagnostic pop - (instancetype)init { self = [super init]; if (self) { #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.10, *)) { UIGraphicsImageRendererFormat *uiformat = [[UIGraphicsImageRendererFormat alloc] init]; self.uiformat = uiformat; } else { #endif CGFloat screenScale = SDDeviceHelper.screenScale; self.scale = screenScale; self.opaque = NO; #if SD_UIKIT } #endif } return self; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" - (instancetype)initForMainScreen { self = [super init]; if (self) { #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.0, *)) { UIGraphicsImageRendererFormat *uiformat; // iOS 11.0.0 GM does have `preferredFormat`, but iOS 11 betas did not (argh!) if ([UIGraphicsImageRenderer respondsToSelector:@selector(preferredFormat)]) { uiformat = [UIGraphicsImageRendererFormat preferredFormat]; } else { uiformat = [UIGraphicsImageRendererFormat defaultFormat]; } self.uiformat = uiformat; } else { #endif CGFloat screenScale = SDDeviceHelper.screenScale; self.scale = screenScale; self.opaque = NO; #if SD_UIKIT } #endif } return self; } #pragma clang diagnostic pop + (instancetype)preferredFormat { SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] initForMainScreen]; return format; } @end @interface SDGraphicsImageRenderer () @property (nonatomic, assign) CGSize size; @property (nonatomic, strong) SDGraphicsImageRendererFormat *format; #if SD_UIKIT @property (nonatomic, strong) UIGraphicsImageRenderer *uirenderer API_AVAILABLE(ios(10.0), tvos(10.0)); #endif @end @implementation SDGraphicsImageRenderer - (instancetype)initWithSize:(CGSize)size { return [self initWithSize:size format:SDGraphicsImageRendererFormat.preferredFormat]; } - (instancetype)initWithSize:(CGSize)size format:(SDGraphicsImageRendererFormat *)format { NSParameterAssert(format); self = [super init]; if (self) { self.size = size; self.format = format; #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.0, *)) { UIGraphicsImageRendererFormat *uiformat = format.uiformat; self.uirenderer = [[UIGraphicsImageRenderer alloc] initWithSize:size format:uiformat]; } #endif } return self; } - (UIImage *)imageWithActions:(NS_NOESCAPE SDGraphicsImageDrawingActions)actions { NSParameterAssert(actions); #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.0, *)) { UIGraphicsImageDrawingActions uiactions = ^(UIGraphicsImageRendererContext *rendererContext) { if (actions) { actions(rendererContext.CGContext); } }; return [self.uirenderer imageWithActions:uiactions]; } else { #endif SDGraphicsBeginImageContextWithOptions(self.size, self.format.opaque, self.format.scale); CGContextRef context = SDGraphicsGetCurrentContext(); if (actions) { actions(context); } UIImage *image = SDGraphicsGetImageFromCurrentImageContext(); SDGraphicsEndImageContext(); return image; #if SD_UIKIT } #endif } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageAPNGCoder.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDImageIOAnimatedCoder.h" /** Built in coder using ImageIO that supports APNG encoding/decoding */ @interface SDImageAPNGCoder : SDImageIOAnimatedCoder @property (nonatomic, class, readonly, nonnull) SDImageAPNGCoder *sharedCoder; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageAPNGCoder.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageAPNGCoder.h" #import "SDImageIOAnimatedCoderInternal.h" #if SD_MAC #import #else #import #endif @implementation SDImageAPNGCoder + (instancetype)sharedCoder { static SDImageAPNGCoder *coder; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ coder = [[SDImageAPNGCoder alloc] init]; }); return coder; } #pragma mark - Subclass Override + (SDImageFormat)imageFormat { return SDImageFormatPNG; } + (NSString *)imageUTType { return (__bridge NSString *)kSDUTTypePNG; } + (NSString *)dictionaryProperty { return (__bridge NSString *)kCGImagePropertyPNGDictionary; } + (NSString *)unclampedDelayTimeProperty { return (__bridge NSString *)kCGImagePropertyAPNGUnclampedDelayTime; } + (NSString *)delayTimeProperty { return (__bridge NSString *)kCGImagePropertyAPNGDelayTime; } + (NSString *)loopCountProperty { return (__bridge NSString *)kCGImagePropertyAPNGLoopCount; } + (NSUInteger)defaultLoopCount { return 0; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageAWebPCoder.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDImageIOAnimatedCoder.h" /** This coder is used for Google WebP and Animated WebP(AWebP) image format. Image/IO provide the WebP decoding support in iOS 14/macOS 11/tvOS 14/watchOS 7+. @note Currently Image/IO seems does not supports WebP encoding, if you need WebP encoding, use the custom codec below. @note If you need to support lower firmware version for WebP, you can have a try at https://github.com/SDWebImage/SDWebImageWebPCoder */ API_AVAILABLE(ios(14.0), tvos(14.0), macos(11.0), watchos(7.0)) @interface SDImageAWebPCoder : SDImageIOAnimatedCoder @property (nonatomic, class, readonly, nonnull) SDImageAWebPCoder *sharedCoder; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageAWebPCoder.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageAWebPCoder.h" #import "SDImageIOAnimatedCoderInternal.h" // These constants are available from iOS 14+ and Xcode 12. This raw value is used for toolchain and firmware compatibility static NSString * kSDCGImagePropertyWebPDictionary = @"{WebP}"; static NSString * kSDCGImagePropertyWebPLoopCount = @"LoopCount"; static NSString * kSDCGImagePropertyWebPDelayTime = @"DelayTime"; static NSString * kSDCGImagePropertyWebPUnclampedDelayTime = @"UnclampedDelayTime"; @implementation SDImageAWebPCoder + (void)initialize { #if __IPHONE_14_0 || __TVOS_14_0 || __MAC_11_0 || __WATCHOS_7_0 // Xcode 12 if (@available(iOS 14, tvOS 14, macOS 11, watchOS 7, *)) { // Use SDK instead of raw value kSDCGImagePropertyWebPDictionary = (__bridge NSString *)kCGImagePropertyWebPDictionary; kSDCGImagePropertyWebPLoopCount = (__bridge NSString *)kCGImagePropertyWebPLoopCount; kSDCGImagePropertyWebPDelayTime = (__bridge NSString *)kCGImagePropertyWebPDelayTime; kSDCGImagePropertyWebPUnclampedDelayTime = (__bridge NSString *)kCGImagePropertyWebPUnclampedDelayTime; } #endif } + (instancetype)sharedCoder { static SDImageAWebPCoder *coder; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ coder = [[SDImageAWebPCoder alloc] init]; }); return coder; } #pragma mark - SDImageCoder - (BOOL)canDecodeFromData:(nullable NSData *)data { switch ([NSData sd_imageFormatForImageData:data]) { case SDImageFormatWebP: // Check WebP decoding compatibility return [self.class canDecodeFromFormat:SDImageFormatWebP]; default: return NO; } } - (BOOL)canIncrementalDecodeFromData:(NSData *)data { return [self canDecodeFromData:data]; } - (BOOL)canEncodeToFormat:(SDImageFormat)format { switch (format) { case SDImageFormatWebP: // Check WebP encoding compatibility return [self.class canEncodeToFormat:SDImageFormatWebP]; default: return NO; } } #pragma mark - Subclass Override + (SDImageFormat)imageFormat { return SDImageFormatWebP; } + (NSString *)imageUTType { return (__bridge NSString *)kSDUTTypeWebP; } + (NSString *)dictionaryProperty { return kSDCGImagePropertyWebPDictionary; } + (NSString *)unclampedDelayTimeProperty { return kSDCGImagePropertyWebPUnclampedDelayTime; } + (NSString *)delayTimeProperty { return kSDCGImagePropertyWebPDelayTime; } + (NSString *)loopCountProperty { return kSDCGImagePropertyWebPLoopCount; } + (NSUInteger)defaultLoopCount { return 0; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCache.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" #import "SDWebImageDefine.h" #import "SDImageCacheConfig.h" #import "SDImageCacheDefine.h" #import "SDMemoryCache.h" #import "SDDiskCache.h" /// Image Cache Options typedef NS_OPTIONS(NSUInteger, SDImageCacheOptions) { /** * By default, we do not query image data when the image is already cached in memory. This mask can force to query image data at the same time. However, this query is asynchronously unless you specify `SDImageCacheQueryMemoryDataSync` */ SDImageCacheQueryMemoryData = 1 << 0, /** * By default, when you only specify `SDImageCacheQueryMemoryData`, we query the memory image data asynchronously. Combined this mask as well to query the memory image data synchronously. */ SDImageCacheQueryMemoryDataSync = 1 << 1, /** * By default, when the memory cache miss, we query the disk cache asynchronously. This mask can force to query disk cache (when memory cache miss) synchronously. @note These 3 query options can be combined together. For the full list about these masks combination, see wiki page. */ SDImageCacheQueryDiskDataSync = 1 << 2, /** * By default, images are decoded respecting their original size. On iOS, this flag will scale down the * images to a size compatible with the constrained memory of devices. */ SDImageCacheScaleDownLargeImages = 1 << 3, /** * By default, we will decode the image in the background during cache query and download from the network. This can help to improve performance because when rendering image on the screen, it need to be firstly decoded. But this happen on the main queue by Core Animation. * However, this process may increase the memory usage as well. If you are experiencing a issue due to excessive memory consumption, This flag can prevent decode the image. * @note 5.14.0 introduce `SDImageCoderDecodeUseLazyDecoding`, use that for better control from codec, instead of post-processing. Which acts the similar like this option but works for SDAnimatedImage as well (this one does not) * @deprecated Deprecated in v5.17.0, if you don't want force-decode, pass [.imageForceDecodePolicy] = SDImageForceDecodePolicy.never.rawValue in context option */ SDImageCacheAvoidDecodeImage API_DEPRECATED("Use SDWebImageContextImageForceDecodePolicy instead", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0)) = 1 << 4, /** * By default, we decode the animated image. This flag can force decode the first frame only and produce the static image. */ SDImageCacheDecodeFirstFrameOnly = 1 << 5, /** * By default, for `SDAnimatedImage`, we decode the animated image frame during rendering to reduce memory usage. This flag actually trigger `preloadAllAnimatedImageFrames = YES` after image load from disk cache */ SDImageCachePreloadAllFrames = 1 << 6, /** * By default, when you use `SDWebImageContextAnimatedImageClass` context option (like using `SDAnimatedImageView` which designed to use `SDAnimatedImage`), we may still use `UIImage` when the memory cache hit, or image decoder is not available, to behave as a fallback solution. * Using this option, can ensure we always produce image with your provided class. If failed, an error with code `SDWebImageErrorBadImageData` will be used. * Note this options is not compatible with `SDImageCacheDecodeFirstFrameOnly`, which always produce a UIImage/NSImage. */ SDImageCacheMatchAnimatedImageClass = 1 << 7, }; /** * A token associated with each cache query. Can be used to cancel a cache query */ @interface SDImageCacheToken : NSObject /** Cancel the current cache query. */ - (void)cancel; /** The query's cache key. */ @property (nonatomic, strong, nullable, readonly) NSString *key; @end /** * SDImageCache maintains a memory cache and a disk cache. Disk cache write operations are performed * asynchronous so it doesn’t add unnecessary latency to the UI. */ @interface SDImageCache : NSObject #pragma mark - Properties /** * Cache Config object - storing all kind of settings. * The property is copy so change of current config will not accidentally affect other cache's config. */ @property (nonatomic, copy, nonnull, readonly) SDImageCacheConfig *config; /** * The memory cache implementation object used for current image cache. * By default we use `SDMemoryCache` class, you can also use this to call your own implementation class method. * @note To customize this class, check `SDImageCacheConfig.memoryCacheClass` property. */ @property (nonatomic, strong, readonly, nonnull) id memoryCache; /** * The disk cache implementation object used for current image cache. * By default we use `SDMemoryCache` class, you can also use this to call your own implementation class method. * @note To customize this class, check `SDImageCacheConfig.diskCacheClass` property. * @warning When calling method about read/write in disk cache, be sure to either make your disk cache implementation IO-safe or using the same access queue to avoid issues. */ @property (nonatomic, strong, readonly, nonnull) id diskCache; /** * The disk cache's root path */ @property (nonatomic, copy, nonnull, readonly) NSString *diskCachePath; /** * The additional disk cache path to check if the query from disk cache not exist; * The `key` param is the image cache key. The returned file path will be used to load the disk cache. If return nil, ignore it. * Useful if you want to bundle pre-loaded images with your app */ @property (nonatomic, copy, nullable) SDImageCacheAdditionalCachePathBlock additionalCachePathBlock; #pragma mark - Singleton and initialization /** * Returns global shared cache instance */ @property (nonatomic, class, readonly, nonnull) SDImageCache *sharedImageCache; /** * Control the default disk cache directory. This will effect all the SDImageCache instance created after modification, even for shared image cache. * This can be used to share the same disk cache with the App and App Extension (Today/Notification Widget) using `- [NSFileManager.containerURLForSecurityApplicationGroupIdentifier:]`. * @note If you pass nil, the value will be reset to `~/Library/Caches/com.hackemist.SDImageCache`. * @note We still preserve the `namespace` arg, which means, if you change this property into `/path/to/use`, the `SDImageCache.sharedImageCache.diskCachePath` should be `/path/to/use/default` because shared image cache use `default` as namespace. * Defaults to nil. */ @property (nonatomic, class, readwrite, null_resettable) NSString *defaultDiskCacheDirectory; /** * Init a new cache store with a specific namespace * The final disk cache directory should looks like ($directory/$namespace). And the default config of shared cache, should result in (~/Library/Caches/com.hackemist.SDImageCache/default/) * * @param ns The namespace to use for this cache store */ - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns; /** * Init a new cache store with a specific namespace and directory. * The final disk cache directory should looks like ($directory/$namespace). And the default config of shared cache, should result in (~/Library/Caches/com.hackemist.SDImageCache/default/) * * @param ns The namespace to use for this cache store * @param directory Directory to cache disk images in */ - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nullable NSString *)directory; /** * Init a new cache store with a specific namespace, directory and config. * The final disk cache directory should looks like ($directory/$namespace). And the default config of shared cache, should result in (~/Library/Caches/com.hackemist.SDImageCache/default/) * * @param ns The namespace to use for this cache store * @param directory Directory to cache disk images in * @param config The cache config to be used to create the cache. You can provide custom memory cache or disk cache class in the cache config */ - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nullable NSString *)directory config:(nullable SDImageCacheConfig *)config NS_DESIGNATED_INITIALIZER; #pragma mark - Cache paths /** Get the cache path for a certain key @param key The unique image cache key @return The cache path. You can check `lastPathComponent` to grab the file name. */ - (nullable NSString *)cachePathForKey:(nullable NSString *)key; #pragma mark - Store Ops /** * Asynchronously store an image into memory and disk cache at the given key. * * @param image The image to store * @param key The unique image cache key, usually it's image absolute URL * @param completionBlock A block executed after the operation is finished */ - (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key completion:(nullable SDWebImageNoParamsBlock)completionBlock; /** * Asynchronously store an image into memory and disk cache at the given key. * * @param image The image to store * @param key The unique image cache key, usually it's image absolute URL * @param toDisk Store the image to disk cache if YES. If NO, the completion block is called synchronously * @param completionBlock A block executed after the operation is finished * @note If no image data is provided and encode to disk, we will try to detect the image format (using either `sd_imageFormat` or `SDAnimatedImage` protocol method) and animation status, to choose the best matched format, including GIF, JPEG or PNG. */ - (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock; /** * Asynchronously store an image data into disk cache at the given key. * * @param imageData The image data to store * @param key The unique image cache key, usually it's image absolute URL * @param completionBlock A block executed after the operation is finished */ - (void)storeImageData:(nullable NSData *)imageData forKey:(nullable NSString *)key completion:(nullable SDWebImageNoParamsBlock)completionBlock; /** * Asynchronously store an image into memory and disk cache at the given key. * * @param image The image to store * @param imageData The image data as returned by the server, this representation will be used for disk storage * instead of converting the given image object into a storable/compressed image format in order * to save quality and CPU * @param key The unique image cache key, usually it's image absolute URL * @param toDisk Store the image to disk cache if YES. If NO, the completion block is called synchronously * @param completionBlock A block executed after the operation is finished * @note If no image data is provided and encode to disk, we will try to detect the image format (using either `sd_imageFormat` or `SDAnimatedImage` protocol method) and animation status, to choose the best matched format, including GIF, JPEG or PNG. */ - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock; /** * Asynchronously store an image into memory and disk cache at the given key. * * @param image The image to store * @param imageData The image data as returned by the server, this representation will be used for disk storage * instead of converting the given image object into a storable/compressed image format in order * to save quality and CPU * @param key The unique image cache key, usually it's image absolute URL * @param options A mask to specify options to use for this store * @param context The context options to use. Pass `.callbackQueue` to control callback queue * @param cacheType The image store op cache type * @param completionBlock A block executed after the operation is finished * @note If no image data is provided and encode to disk, we will try to detect the image format (using either `sd_imageFormat` or `SDAnimatedImage` protocol method) and animation status, to choose the best matched format, including GIF, JPEG or PNG. */ - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock; /** * Synchronously store an image into memory cache at the given key. * * @param image The image to store * @param key The unique image cache key, usually it's image absolute URL */ - (void)storeImageToMemory:(nullable UIImage*)image forKey:(nullable NSString *)key; /** * Synchronously store an image data into disk cache at the given key. * * @param imageData The image data to store * @param key The unique image cache key, usually it's image absolute URL */ - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key; #pragma mark - Contains and Check Ops /** * Asynchronously check if image exists in disk cache already (does not load the image) * * @param key the key describing the url * @param completionBlock the block to be executed when the check is done. * @note the completion block will be always executed on the main queue */ - (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDImageCacheCheckCompletionBlock)completionBlock; /** * Synchronously check if image data exists in disk cache already (does not load the image) * * @param key the key describing the url */ - (BOOL)diskImageDataExistsWithKey:(nullable NSString *)key; #pragma mark - Query and Retrieve Ops /** * Synchronously query the image data for the given key in disk cache. You can decode the image data to image after loaded. * * @param key The unique key used to store the wanted image * @return The image data for the given key, or nil if not found. */ - (nullable NSData *)diskImageDataForKey:(nullable NSString *)key; /** * Asynchronously query the image data for the given key in disk cache. You can decode the image data to image after loaded. * * @param key The unique key used to store the wanted image * @param completionBlock the block to be executed when the query is done. * @note the completion block will be always executed on the main queue */ - (void)diskImageDataQueryForKey:(nullable NSString *)key completion:(nullable SDImageCacheQueryDataCompletionBlock)completionBlock; /** * Asynchronously queries the cache with operation and call the completion when done. * * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. * @param doneBlock The completion block. Will not get called if the operation is cancelled * * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancelled */ - (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key done:(nullable SDImageCacheQueryCompletionBlock)doneBlock; /** * Asynchronously queries the cache with operation and call the completion when done. * * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. * @param options A mask to specify options to use for this cache query * @param doneBlock The completion block. Will not get called if the operation is cancelled * * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancelled * @warning If you query with thumbnail cache key, you'd better not pass the thumbnail pixel size context, which is undefined behavior. */ - (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options done:(nullable SDImageCacheQueryCompletionBlock)doneBlock; /** * Asynchronously queries the cache with operation and call the completion when done. * * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. * @param options A mask to specify options to use for this cache query * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param doneBlock The completion block. Will not get called if the operation is cancelled * * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancellederation, will callback immediately when cancelled * @warning If you query with thumbnail cache key, you'd better not pass the thumbnail pixel size context, which is undefined behavior. */ - (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock; /** * Asynchronously queries the cache with operation and call the completion when done. * * @param key The unique key used to store the wanted image. If you want transformed or thumbnail image, calculate the key with `SDTransformedKeyForKey`, `SDThumbnailedKeyForKey`, or generate the cache key from url with `cacheKeyForURL:context:`. * @param options A mask to specify options to use for this cache query * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param queryCacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediately. * @param doneBlock The completion block. Will not get called if the operation is cancelled * * @return a SDImageCacheToken instance containing the cache operation, will callback immediately when cancelled * @warning If you query with thumbnail cache key, you'd better not pass the thumbnail pixel size context, which is undefined behavior. */ - (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock; /** * Synchronously query the memory cache. * * @param key The unique key used to store the image * @return The image for the given key, or nil if not found. */ - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key; /** * Synchronously query the disk cache. * * @param key The unique key used to store the image * @return The image for the given key, or nil if not found. */ - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key; /** * Synchronously query the disk cache. With the options and context which may effect the image generation. (Such as transformer, animated image, thumbnail, etc) * * @param key The unique key used to store the image * @param options A mask to specify options to use for this cache query * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @return The image for the given key, or nil if not found. */ - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context; /** * Synchronously query the cache (memory and or disk) after checking the memory cache. * * @param key The unique key used to store the image * @return The image for the given key, or nil if not found. */ - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key; /** * Synchronously query the cache (memory and or disk) after checking the memory cache. With the options and context which may effect the image generation. (Such as transformer, animated image, thumbnail, etc) * * @param key The unique key used to store the image * @param options A mask to specify options to use for this cache query * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @return The image for the given key, or nil if not found. */ - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context; #pragma mark - Remove Ops /** * Asynchronously remove the image from memory and disk cache * * @param key The unique image cache key * @param completion A block that should be executed after the image has been removed (optional) */ - (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion; /** * Asynchronously remove the image from memory and optionally disk cache * * @param key The unique image cache key * @param fromDisk Also remove cache entry from disk if YES. If NO, the completion block is called synchronously * @param completion A block that should be executed after the image has been removed (optional) */ - (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion; /** Synchronously remove the image from memory cache. @param key The unique image cache key */ - (void)removeImageFromMemoryForKey:(nullable NSString *)key; /** Synchronously remove the image from disk cache. @param key The unique image cache key */ - (void)removeImageFromDiskForKey:(nullable NSString *)key; #pragma mark - Cache clean Ops /** * Synchronously Clear all memory cached images */ - (void)clearMemory; /** * Asynchronously clear all disk cached images. Non-blocking method - returns immediately. * @param completion A block that should be executed after cache expiration completes (optional) */ - (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion; /** * Asynchronously remove all expired cached image from disk. Non-blocking method - returns immediately. * @param completionBlock A block that should be executed after cache expiration completes (optional) */ - (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock; #pragma mark - Cache Info /** * Get the total bytes size of images in the disk cache */ - (NSUInteger)totalDiskSize; /** * Get the number of images in the disk cache */ - (NSUInteger)totalDiskCount; /** * Asynchronously calculate the disk cache's size. */ - (void)calculateSizeWithCompletionBlock:(nullable SDImageCacheCalculateSizeBlock)completionBlock; @end /** * SDImageCache is the built-in image cache implementation for web image manager. It adopts `SDImageCache` protocol to provide the function for web image manager to use for image loading process. */ @interface SDImageCache (SDImageCache) @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCache.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageCache.h" #import "SDInternalMacros.h" #import "NSImage+Compatibility.h" #import "SDImageCodersManager.h" #import "SDImageCoderHelper.h" #import "SDAnimatedImage.h" #import "UIImage+MemoryCacheCost.h" #import "UIImage+Metadata.h" #import "UIImage+ExtendedCacheData.h" #import "SDCallbackQueue.h" #import "SDImageTransformer.h" // TODO, remove this // TODO, remove this static BOOL SDIsThumbnailKey(NSString *key) { if ([key rangeOfString:@"-Thumbnail("].location != NSNotFound) { return YES; } return NO; } @interface SDImageCacheToken () @property (nonatomic, strong, nullable, readwrite) NSString *key; @property (nonatomic, assign, getter=isCancelled) BOOL cancelled; @property (nonatomic, copy, nullable) SDImageCacheQueryCompletionBlock doneBlock; @property (nonatomic, strong, nullable) SDCallbackQueue *callbackQueue; @end @implementation SDImageCacheToken -(instancetype)initWithDoneBlock:(nullable SDImageCacheQueryCompletionBlock)doneBlock { self = [super init]; if (self) { self.doneBlock = doneBlock; } return self; } - (void)cancel { @synchronized (self) { if (self.isCancelled) { return; } self.cancelled = YES; SDImageCacheQueryCompletionBlock doneBlock = self.doneBlock; self.doneBlock = nil; if (doneBlock) { [(self.callbackQueue ?: SDCallbackQueue.mainQueue) async:^{ doneBlock(nil, nil, SDImageCacheTypeNone); }]; } } } @end static NSString * _defaultDiskCacheDirectory; @interface SDImageCache () #pragma mark - Properties @property (nonatomic, strong, readwrite, nonnull) id memoryCache; @property (nonatomic, strong, readwrite, nonnull) id diskCache; @property (nonatomic, copy, readwrite, nonnull) SDImageCacheConfig *config; @property (nonatomic, copy, readwrite, nonnull) NSString *diskCachePath; @property (nonatomic, strong, nonnull) dispatch_queue_t ioQueue; @end @implementation SDImageCache #pragma mark - Singleton, init, dealloc + (nonnull instancetype)sharedImageCache { static dispatch_once_t once; static id instance; dispatch_once(&once, ^{ instance = [self new]; }); return instance; } + (NSString *)defaultDiskCacheDirectory { if (!_defaultDiskCacheDirectory) { _defaultDiskCacheDirectory = [[self userCacheDirectory] stringByAppendingPathComponent:@"com.hackemist.SDImageCache"]; } return _defaultDiskCacheDirectory; } + (void)setDefaultDiskCacheDirectory:(NSString *)defaultDiskCacheDirectory { _defaultDiskCacheDirectory = [defaultDiskCacheDirectory copy]; } - (instancetype)init { return [self initWithNamespace:@"default"]; } - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns { return [self initWithNamespace:ns diskCacheDirectory:nil]; } - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nullable NSString *)directory { return [self initWithNamespace:ns diskCacheDirectory:directory config:SDImageCacheConfig.defaultCacheConfig]; } - (nonnull instancetype)initWithNamespace:(nonnull NSString *)ns diskCacheDirectory:(nullable NSString *)directory config:(nullable SDImageCacheConfig *)config { if ((self = [super init])) { NSAssert(ns, @"Cache namespace should not be nil"); if (!config) { config = SDImageCacheConfig.defaultCacheConfig; } _config = [config copy]; // Create IO queue dispatch_queue_attr_t ioQueueAttributes = _config.ioQueueAttributes; _ioQueue = dispatch_queue_create("com.hackemist.SDImageCache.ioQueue", ioQueueAttributes); NSAssert(_ioQueue, @"The IO queue should not be nil. Your configured `ioQueueAttributes` may be wrong"); // Init the memory cache NSAssert([config.memoryCacheClass conformsToProtocol:@protocol(SDMemoryCache)], @"Custom memory cache class must conform to `SDMemoryCache` protocol"); _memoryCache = [[config.memoryCacheClass alloc] initWithConfig:_config]; // Init the disk cache if (!directory) { // Use default disk cache directory directory = [self.class defaultDiskCacheDirectory]; } _diskCachePath = [directory stringByAppendingPathComponent:ns]; NSAssert([config.diskCacheClass conformsToProtocol:@protocol(SDDiskCache)], @"Custom disk cache class must conform to `SDDiskCache` protocol"); _diskCache = [[config.diskCacheClass alloc] initWithCachePath:_diskCachePath config:_config]; // Check and migrate disk cache directory if need [self migrateDiskCacheDirectory]; #if SD_UIKIT // Subscribe to app events [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; #endif #if SD_MAC [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:NSApplicationWillTerminateNotification object:nil]; #endif } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Cache paths - (nullable NSString *)cachePathForKey:(nullable NSString *)key { if (!key) { return nil; } return [self.diskCache cachePathForKey:key]; } + (nullable NSString *)userCacheDirectory { NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES); return paths.firstObject; } - (void)migrateDiskCacheDirectory { if ([self.diskCache isKindOfClass:[SDDiskCache class]]) { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // ~/Library/Caches/com.hackemist.SDImageCache/default/ NSString *newDefaultPath = [[[self.class userCacheDirectory] stringByAppendingPathComponent:@"com.hackemist.SDImageCache"] stringByAppendingPathComponent:@"default"]; // ~/Library/Caches/default/com.hackemist.SDWebImageCache.default/ NSString *oldDefaultPath = [[[self.class userCacheDirectory] stringByAppendingPathComponent:@"default"] stringByAppendingPathComponent:@"com.hackemist.SDWebImageCache.default"]; dispatch_async(self.ioQueue, ^{ [((SDDiskCache *)self.diskCache) moveCacheDirectoryFromPath:oldDefaultPath toPath:newDefaultPath]; }); }); } } #pragma mark - Store Ops - (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key completion:(nullable SDWebImageNoParamsBlock)completionBlock { [self storeImage:image imageData:nil forKey:key options:0 context:nil cacheType:SDImageCacheTypeAll completion:completionBlock]; } - (void)storeImage:(nullable UIImage *)image forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock { [self storeImage:image imageData:nil forKey:key options:0 context:nil cacheType:(toDisk ? SDImageCacheTypeAll : SDImageCacheTypeMemory) completion:completionBlock]; } - (void)storeImageData:(nullable NSData *)imageData forKey:(nullable NSString *)key completion:(nullable SDWebImageNoParamsBlock)completionBlock { [self storeImage:nil imageData:imageData forKey:key options:0 context:nil cacheType:SDImageCacheTypeAll completion:completionBlock]; } - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key toDisk:(BOOL)toDisk completion:(nullable SDWebImageNoParamsBlock)completionBlock { [self storeImage:image imageData:imageData forKey:key options:0 context:nil cacheType:(toDisk ? SDImageCacheTypeAll : SDImageCacheTypeMemory) completion:completionBlock]; } - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { if ((!image && !imageData) || !key) { if (completionBlock) { completionBlock(); } return; } BOOL toMemory = cacheType == SDImageCacheTypeMemory || cacheType == SDImageCacheTypeAll; BOOL toDisk = cacheType == SDImageCacheTypeDisk || cacheType == SDImageCacheTypeAll; // if memory cache is enabled if (image && toMemory && self.config.shouldCacheImagesInMemory) { NSUInteger cost = image.sd_memoryCost; [self.memoryCache setObject:image forKey:key cost:cost]; } if (!toDisk) { if (completionBlock) { completionBlock(); } return; } NSData *data = imageData; if (!data && [image respondsToSelector:@selector(animatedImageData)]) { // If image is custom animated image class, prefer its original animated data data = [((id)image) animatedImageData]; } SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue]; if (!data && image) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // Check image's associated image format, may return .undefined SDImageFormat format = image.sd_imageFormat; if (format == SDImageFormatUndefined) { // If image is animated, use GIF (APNG may be better, but has bugs before macOS 10.14) if (image.sd_imageFrameCount > 1) { format = SDImageFormatGIF; } else { // If we do not have any data to detect image format, check whether it contains alpha channel to use PNG or JPEG format format = [SDImageCoderHelper CGImageContainsAlpha:image.CGImage] ? SDImageFormatPNG : SDImageFormatJPEG; } } NSData *encodedData = [[SDImageCodersManager sharedManager] encodedDataWithImage:image format:format options:context[SDWebImageContextImageEncodeOptions]]; dispatch_async(self.ioQueue, ^{ [self _storeImageDataToDisk:encodedData forKey:key]; [self _archivedDataWithImage:image forKey:key]; if (completionBlock) { [(queue ?: SDCallbackQueue.mainQueue) async:^{ completionBlock(); }]; } }); }); } else { dispatch_async(self.ioQueue, ^{ [self _storeImageDataToDisk:data forKey:key]; [self _archivedDataWithImage:image forKey:key]; if (completionBlock) { [(queue ?: SDCallbackQueue.mainQueue) async:^{ completionBlock(); }]; } }); } } - (void)_archivedDataWithImage:(UIImage *)image forKey:(NSString *)key { if (!image || !key) { return; } // Check extended data id extendedObject = image.sd_extendedObject; if (![extendedObject conformsToProtocol:@protocol(NSCoding)]) { return; } NSData *extendedData; if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) { NSError *error; extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject requiringSecureCoding:NO error:&error]; if (error) { SD_LOG("NSKeyedArchiver archive failed with error: %@", error); } } else { @try { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" extendedData = [NSKeyedArchiver archivedDataWithRootObject:extendedObject]; #pragma clang diagnostic pop } @catch (NSException *exception) { SD_LOG("NSKeyedArchiver archive failed with exception: %@", exception); } } if (extendedData) { [self.diskCache setExtendedData:extendedData forKey:key]; } } - (void)storeImageToMemory:(UIImage *)image forKey:(NSString *)key { if (!image || !key) { return; } NSUInteger cost = image.sd_memoryCost; [self.memoryCache setObject:image forKey:key cost:cost]; } - (void)storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { if (!imageData || !key) { return; } dispatch_sync(self.ioQueue, ^{ [self _storeImageDataToDisk:imageData forKey:key]; }); } // Make sure to call from io queue by caller - (void)_storeImageDataToDisk:(nullable NSData *)imageData forKey:(nullable NSString *)key { if (!imageData || !key) { return; } [self.diskCache setData:imageData forKey:key]; } #pragma mark - Query and Retrieve Ops - (void)diskImageExistsWithKey:(nullable NSString *)key completion:(nullable SDImageCacheCheckCompletionBlock)completionBlock { dispatch_async(self.ioQueue, ^{ BOOL exists = [self _diskImageDataExistsWithKey:key]; if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(exists); }); } }); } - (BOOL)diskImageDataExistsWithKey:(nullable NSString *)key { if (!key) { return NO; } __block BOOL exists = NO; dispatch_sync(self.ioQueue, ^{ exists = [self _diskImageDataExistsWithKey:key]; }); return exists; } // Make sure to call from io queue by caller - (BOOL)_diskImageDataExistsWithKey:(nullable NSString *)key { if (!key) { return NO; } return [self.diskCache containsDataForKey:key]; } - (void)diskImageDataQueryForKey:(NSString *)key completion:(SDImageCacheQueryDataCompletionBlock)completionBlock { dispatch_async(self.ioQueue, ^{ NSData *imageData = [self diskImageDataBySearchingAllPathsForKey:key]; if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(imageData); }); } }); } - (nullable NSData *)diskImageDataForKey:(nullable NSString *)key { if (!key) { return nil; } __block NSData *imageData = nil; dispatch_sync(self.ioQueue, ^{ imageData = [self diskImageDataBySearchingAllPathsForKey:key]; }); return imageData; } - (nullable UIImage *)imageFromMemoryCacheForKey:(nullable NSString *)key { return [self.memoryCache objectForKey:key]; } - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key { return [self imageFromDiskCacheForKey:key options:0 context:nil]; } - (nullable UIImage *)imageFromDiskCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context { if (!key) { return nil; } NSData *data = [self diskImageDataForKey:key]; UIImage *diskImage = [self diskImageForKey:key data:data options:options context:context]; BOOL shouldCacheToMemory = YES; if (context[SDWebImageContextStoreCacheType]) { SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue]; shouldCacheToMemory = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory); } if (shouldCacheToMemory) { // check if we need sync logic [self _syncDiskToMemoryWithImage:diskImage forKey:key]; } return diskImage; } - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key { return [self imageFromCacheForKey:key options:0 context:nil]; } - (nullable UIImage *)imageFromCacheForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context { // First check the in-memory cache... UIImage *image = [self imageFromMemoryCacheForKey:key]; if (image) { if (options & SDImageCacheDecodeFirstFrameOnly) { // Ensure static image if (image.sd_imageFrameCount > 1) { #if SD_MAC image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp]; #else image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation]; #endif } } else if (options & SDImageCacheMatchAnimatedImageClass) { // Check image class matching Class animatedImageClass = image.class; Class desiredImageClass = context[SDWebImageContextAnimatedImageClass]; if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) { image = nil; } } } // Since we don't need to query imageData, return image if exist if (image) { return image; } // Second check the disk cache... image = [self imageFromDiskCacheForKey:key options:options context:context]; return image; } - (nullable NSData *)diskImageDataBySearchingAllPathsForKey:(nullable NSString *)key { if (!key) { return nil; } NSData *data = [self.diskCache dataForKey:key]; if (data) { return data; } // Addtional cache path for custom pre-load cache if (self.additionalCachePathBlock) { NSString *filePath = self.additionalCachePathBlock(key); if (filePath) { data = [NSData dataWithContentsOfFile:filePath options:self.config.diskCacheReadingOptions error:nil]; } } return data; } - (nullable UIImage *)diskImageForKey:(nullable NSString *)key { if (!key) { return nil; } NSData *data = [self diskImageDataForKey:key]; return [self diskImageForKey:key data:data options:0 context:nil]; } - (nullable UIImage *)diskImageForKey:(nullable NSString *)key data:(nullable NSData *)data options:(SDImageCacheOptions)options context:(SDWebImageContext *)context { if (!data) { return nil; } UIImage *image = SDImageCacheDecodeImageData(data, key, [[self class] imageOptionsFromCacheOptions:options], context); [self _unarchiveObjectWithImage:image forKey:key]; return image; } - (void)_syncDiskToMemoryWithImage:(UIImage *)diskImage forKey:(NSString *)key { // earily check if (!self.config.shouldCacheImagesInMemory) { return; } if (!diskImage) { return; } // The disk -> memory sync logic, which should only store thumbnail image with thumbnail key // However, caller (like SDWebImageManager) will query full key, with thumbnail size, and get thubmnail image // We should add a check here, currently it's a hack if (diskImage.sd_isThumbnail && !SDIsThumbnailKey(key)) { SDImageCoderOptions *options = diskImage.sd_decodeOptions; CGSize thumbnailSize = CGSizeZero; NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; if (thumbnailSizeValue != nil) { #if SD_MAC thumbnailSize = thumbnailSizeValue.sizeValue; #else thumbnailSize = thumbnailSizeValue.CGSizeValue; #endif } BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; } // Calculate the actual thumbnail key NSString *thumbnailKey = SDThumbnailedKeyForKey(key, thumbnailSize, preserveAspectRatio); // Override the sync key key = thumbnailKey; } NSUInteger cost = diskImage.sd_memoryCost; [self.memoryCache setObject:diskImage forKey:key cost:cost]; } - (void)_unarchiveObjectWithImage:(UIImage *)image forKey:(NSString *)key { if (!image || !key) { return; } // Check extended data NSData *extendedData = [self.diskCache extendedDataForKey:key]; if (!extendedData) { return; } id extendedObject; if (@available(iOS 11, tvOS 11, macOS 10.13, watchOS 4, *)) { NSError *error; NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingFromData:extendedData error:&error]; unarchiver.requiresSecureCoding = NO; extendedObject = [unarchiver decodeTopLevelObjectForKey:NSKeyedArchiveRootObjectKey error:&error]; if (error) { SD_LOG("NSKeyedUnarchiver unarchive failed with error: %@", error); } } else { @try { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" extendedObject = [NSKeyedUnarchiver unarchiveObjectWithData:extendedData]; #pragma clang diagnostic pop } @catch (NSException *exception) { SD_LOG("NSKeyedUnarchiver unarchive failed with exception: %@", exception); } } image.sd_extendedObject = extendedObject; } - (nullable SDImageCacheToken *)queryCacheOperationForKey:(NSString *)key done:(SDImageCacheQueryCompletionBlock)doneBlock { return [self queryCacheOperationForKey:key options:0 done:doneBlock]; } - (nullable SDImageCacheToken *)queryCacheOperationForKey:(NSString *)key options:(SDImageCacheOptions)options done:(SDImageCacheQueryCompletionBlock)doneBlock { return [self queryCacheOperationForKey:key options:options context:nil done:doneBlock]; } - (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context done:(nullable SDImageCacheQueryCompletionBlock)doneBlock { return [self queryCacheOperationForKey:key options:options context:context cacheType:SDImageCacheTypeAll done:doneBlock]; } - (nullable SDImageCacheToken *)queryCacheOperationForKey:(nullable NSString *)key options:(SDImageCacheOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType done:(nullable SDImageCacheQueryCompletionBlock)doneBlock { if (!key) { if (doneBlock) { doneBlock(nil, nil, SDImageCacheTypeNone); } return nil; } // Invalid cache type if (queryCacheType == SDImageCacheTypeNone) { if (doneBlock) { doneBlock(nil, nil, SDImageCacheTypeNone); } return nil; } // First check the in-memory cache... UIImage *image; BOOL shouldQueryDiskOnly = (queryCacheType == SDImageCacheTypeDisk); if (!shouldQueryDiskOnly) { image = [self imageFromMemoryCacheForKey:key]; } if (image) { if (options & SDImageCacheDecodeFirstFrameOnly) { // Ensure static image if (image.sd_imageFrameCount > 1) { #if SD_MAC image = [[NSImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:kCGImagePropertyOrientationUp]; #else image = [[UIImage alloc] initWithCGImage:image.CGImage scale:image.scale orientation:image.imageOrientation]; #endif } } else if (options & SDImageCacheMatchAnimatedImageClass) { // Check image class matching Class animatedImageClass = image.class; Class desiredImageClass = context[SDWebImageContextAnimatedImageClass]; if (desiredImageClass && ![animatedImageClass isSubclassOfClass:desiredImageClass]) { image = nil; } } } BOOL shouldQueryMemoryOnly = (queryCacheType == SDImageCacheTypeMemory) || (image && !(options & SDImageCacheQueryMemoryData)); if (shouldQueryMemoryOnly) { if (doneBlock) { doneBlock(image, nil, SDImageCacheTypeMemory); } return nil; } // Second check the disk cache... SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue]; SDImageCacheToken *operation = [[SDImageCacheToken alloc] initWithDoneBlock:doneBlock]; operation.key = key; operation.callbackQueue = queue; // Check whether we need to synchronously query disk // 1. in-memory cache hit & memoryDataSync // 2. in-memory cache miss & diskDataSync BOOL shouldQueryDiskSync = ((image && options & SDImageCacheQueryMemoryDataSync) || (!image && options & SDImageCacheQueryDiskDataSync)); NSData* (^queryDiskDataBlock)(void) = ^NSData* { @synchronized (operation) { if (operation.isCancelled) { return nil; } } return [self diskImageDataBySearchingAllPathsForKey:key]; }; UIImage* (^queryDiskImageBlock)(NSData*) = ^UIImage*(NSData* diskData) { @synchronized (operation) { if (operation.isCancelled) { return nil; } } UIImage *diskImage; if (image) { // the image is from in-memory cache, but need image data diskImage = image; } else if (diskData) { // the image memory cache miss, need image data and image BOOL shouldCacheToMemory = YES; if (context[SDWebImageContextStoreCacheType]) { SDImageCacheType cacheType = [context[SDWebImageContextStoreCacheType] integerValue]; shouldCacheToMemory = (cacheType == SDImageCacheTypeAll || cacheType == SDImageCacheTypeMemory); } // Special case: If user query image in list for the same URL, to avoid decode and write **same** image object into disk cache multiple times, we query and check memory cache here again. See: #3523 // This because disk operation can be async, previous sync check of `memory cache miss`, does not gurantee current check of `memory cache miss` if (!shouldQueryDiskSync) { // First check the in-memory cache... if (!shouldQueryDiskOnly) { diskImage = [self imageFromMemoryCacheForKey:key]; } } // decode image data only if in-memory cache missed if (!diskImage) { diskImage = [self diskImageForKey:key data:diskData options:options context:context]; // check if we need sync logic if (shouldCacheToMemory) { [self _syncDiskToMemoryWithImage:diskImage forKey:key]; } } } return diskImage; }; // Query in ioQueue to keep IO-safe if (shouldQueryDiskSync) { __block NSData* diskData; __block UIImage* diskImage; dispatch_sync(self.ioQueue, ^{ diskData = queryDiskDataBlock(); diskImage = queryDiskImageBlock(diskData); }); if (doneBlock) { doneBlock(diskImage, diskData, SDImageCacheTypeDisk); } } else { dispatch_async(self.ioQueue, ^{ NSData* diskData = queryDiskDataBlock(); UIImage* diskImage = queryDiskImageBlock(diskData); @synchronized (operation) { if (operation.isCancelled) { return; } } if (doneBlock) { [(queue ?: SDCallbackQueue.mainQueue) async:^{ // Dispatch from IO queue to main queue need time, user may call cancel during the dispatch timing // This check is here to avoid double callback (one is from `SDImageCacheToken` in sync) @synchronized (operation) { if (operation.isCancelled) { return; } } doneBlock(diskImage, diskData, SDImageCacheTypeDisk); }]; } }); } return operation; } #pragma mark - Remove Ops - (void)removeImageForKey:(nullable NSString *)key withCompletion:(nullable SDWebImageNoParamsBlock)completion { [self removeImageForKey:key fromDisk:YES withCompletion:completion]; } - (void)removeImageForKey:(nullable NSString *)key fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion { [self removeImageForKey:key fromMemory:YES fromDisk:fromDisk withCompletion:completion]; } - (void)removeImageForKey:(nullable NSString *)key fromMemory:(BOOL)fromMemory fromDisk:(BOOL)fromDisk withCompletion:(nullable SDWebImageNoParamsBlock)completion { if (!key) { return; } if (fromMemory && self.config.shouldCacheImagesInMemory) { [self.memoryCache removeObjectForKey:key]; } if (fromDisk) { dispatch_async(self.ioQueue, ^{ [self.diskCache removeDataForKey:key]; if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } }); } else if (completion) { completion(); } } - (void)removeImageFromMemoryForKey:(NSString *)key { if (!key) { return; } [self.memoryCache removeObjectForKey:key]; } - (void)removeImageFromDiskForKey:(NSString *)key { if (!key) { return; } dispatch_sync(self.ioQueue, ^{ [self _removeImageFromDiskForKey:key]; }); } // Make sure to call from io queue by caller - (void)_removeImageFromDiskForKey:(NSString *)key { if (!key) { return; } [self.diskCache removeDataForKey:key]; } #pragma mark - Cache clean Ops - (void)clearMemory { [self.memoryCache removeAllObjects]; } - (void)clearDiskOnCompletion:(nullable SDWebImageNoParamsBlock)completion { dispatch_async(self.ioQueue, ^{ [self.diskCache removeAllData]; if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(); }); } }); } - (void)deleteOldFilesWithCompletionBlock:(nullable SDWebImageNoParamsBlock)completionBlock { dispatch_async(self.ioQueue, ^{ [self.diskCache removeExpiredData]; if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(); }); } }); } #pragma mark - UIApplicationWillTerminateNotification #if SD_UIKIT || SD_MAC - (void)applicationWillTerminate:(NSNotification *)notification { // On iOS/macOS, the async opeartion to remove exipred data will be terminated quickly // Try using the sync operation to ensure we reomve the exipred data if (!self.config.shouldRemoveExpiredDataWhenTerminate) { return; } dispatch_sync(self.ioQueue, ^{ [self.diskCache removeExpiredData]; }); } #endif #pragma mark - UIApplicationDidEnterBackgroundNotification #if SD_UIKIT - (void)applicationDidEnterBackground:(NSNotification *)notification { if (!self.config.shouldRemoveExpiredDataWhenEnterBackground) { return; } Class UIApplicationClass = NSClassFromString(@"UIApplication"); if(!UIApplicationClass || ![UIApplicationClass respondsToSelector:@selector(sharedApplication)]) { return; } UIApplication *application = [UIApplication performSelector:@selector(sharedApplication)]; __block UIBackgroundTaskIdentifier bgTask = [application beginBackgroundTaskWithExpirationHandler:^{ // Clean up any unfinished task business by marking where you // stopped or ending the task outright. [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; // Start the long-running task and return immediately. [self deleteOldFilesWithCompletionBlock:^{ [application endBackgroundTask:bgTask]; bgTask = UIBackgroundTaskInvalid; }]; } #endif #pragma mark - Cache Info - (NSUInteger)totalDiskSize { __block NSUInteger size = 0; dispatch_sync(self.ioQueue, ^{ size = [self.diskCache totalSize]; }); return size; } - (NSUInteger)totalDiskCount { __block NSUInteger count = 0; dispatch_sync(self.ioQueue, ^{ count = [self.diskCache totalCount]; }); return count; } - (void)calculateSizeWithCompletionBlock:(nullable SDImageCacheCalculateSizeBlock)completionBlock { dispatch_async(self.ioQueue, ^{ NSUInteger fileCount = [self.diskCache totalCount]; NSUInteger totalSize = [self.diskCache totalSize]; if (completionBlock) { dispatch_async(dispatch_get_main_queue(), ^{ completionBlock(fileCount, totalSize); }); } }); } #pragma mark - Helper #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" + (SDWebImageOptions)imageOptionsFromCacheOptions:(SDImageCacheOptions)cacheOptions { SDWebImageOptions options = 0; if (cacheOptions & SDImageCacheScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages; if (cacheOptions & SDImageCacheDecodeFirstFrameOnly) options |= SDWebImageDecodeFirstFrameOnly; if (cacheOptions & SDImageCachePreloadAllFrames) options |= SDWebImagePreloadAllFrames; if (cacheOptions & SDImageCacheAvoidDecodeImage) options |= SDWebImageAvoidDecodeImage; if (cacheOptions & SDImageCacheMatchAnimatedImageClass) options |= SDWebImageMatchAnimatedImageClass; return options; } #pragma clang diagnostic pop @end @implementation SDImageCache (SDImageCache) #pragma mark - SDImageCache - (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { return [self queryImageForKey:key options:options context:context cacheType:SDImageCacheTypeAll completion:completionBlock]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock { SDImageCacheOptions cacheOptions = 0; if (options & SDWebImageQueryMemoryData) cacheOptions |= SDImageCacheQueryMemoryData; if (options & SDWebImageQueryMemoryDataSync) cacheOptions |= SDImageCacheQueryMemoryDataSync; if (options & SDWebImageQueryDiskDataSync) cacheOptions |= SDImageCacheQueryDiskDataSync; if (options & SDWebImageScaleDownLargeImages) cacheOptions |= SDImageCacheScaleDownLargeImages; if (options & SDWebImageAvoidDecodeImage) cacheOptions |= SDImageCacheAvoidDecodeImage; if (options & SDWebImageDecodeFirstFrameOnly) cacheOptions |= SDImageCacheDecodeFirstFrameOnly; if (options & SDWebImagePreloadAllFrames) cacheOptions |= SDImageCachePreloadAllFrames; if (options & SDWebImageMatchAnimatedImageClass) cacheOptions |= SDImageCacheMatchAnimatedImageClass; return [self queryCacheOperationForKey:key options:cacheOptions context:context cacheType:cacheType done:completionBlock]; } #pragma clang diagnostic pop - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { [self storeImage:image imageData:imageData forKey:key options:0 context:nil cacheType:cacheType completion:completionBlock]; } - (void)removeImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock { switch (cacheType) { case SDImageCacheTypeNone: { [self removeImageForKey:key fromMemory:NO fromDisk:NO withCompletion:completionBlock]; } break; case SDImageCacheTypeMemory: { [self removeImageForKey:key fromMemory:YES fromDisk:NO withCompletion:completionBlock]; } break; case SDImageCacheTypeDisk: { [self removeImageForKey:key fromMemory:NO fromDisk:YES withCompletion:completionBlock]; } break; case SDImageCacheTypeAll: { [self removeImageForKey:key fromMemory:YES fromDisk:YES withCompletion:completionBlock]; } break; default: { if (completionBlock) { completionBlock(); } } break; } } - (void)containsImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheContainsCompletionBlock)completionBlock { switch (cacheType) { case SDImageCacheTypeNone: { if (completionBlock) { completionBlock(SDImageCacheTypeNone); } } break; case SDImageCacheTypeMemory: { BOOL isInMemoryCache = ([self imageFromMemoryCacheForKey:key] != nil); if (completionBlock) { completionBlock(isInMemoryCache ? SDImageCacheTypeMemory : SDImageCacheTypeNone); } } break; case SDImageCacheTypeDisk: { [self diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) { if (completionBlock) { completionBlock(isInDiskCache ? SDImageCacheTypeDisk : SDImageCacheTypeNone); } }]; } break; case SDImageCacheTypeAll: { BOOL isInMemoryCache = ([self imageFromMemoryCacheForKey:key] != nil); if (isInMemoryCache) { if (completionBlock) { completionBlock(SDImageCacheTypeMemory); } return; } [self diskImageExistsWithKey:key completion:^(BOOL isInDiskCache) { if (completionBlock) { completionBlock(isInDiskCache ? SDImageCacheTypeDisk : SDImageCacheTypeNone); } }]; } break; default: if (completionBlock) { completionBlock(SDImageCacheTypeNone); } break; } } - (void)clearWithCacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock { switch (cacheType) { case SDImageCacheTypeNone: { if (completionBlock) { completionBlock(); } } break; case SDImageCacheTypeMemory: { [self clearMemory]; if (completionBlock) { completionBlock(); } } break; case SDImageCacheTypeDisk: { [self clearDiskOnCompletion:completionBlock]; } break; case SDImageCacheTypeAll: { [self clearMemory]; [self clearDiskOnCompletion:completionBlock]; } break; default: { if (completionBlock) { completionBlock(); } } break; } } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /// Image Cache Expire Type typedef NS_ENUM(NSUInteger, SDImageCacheConfigExpireType) { /** * When the image cache is accessed it will update this value */ SDImageCacheConfigExpireTypeAccessDate, /** * When the image cache is created or modified it will update this value (Default) */ SDImageCacheConfigExpireTypeModificationDate, /** * When the image cache is created it will update this value */ SDImageCacheConfigExpireTypeCreationDate, /** * When the image cache is created, modified, renamed, file attribute updated (like permission, xattr) it will update this value */ SDImageCacheConfigExpireTypeChangeDate, }; /** The class contains all the config for image cache @note This class conform to NSCopying, make sure to add the property in `copyWithZone:` as well. */ @interface SDImageCacheConfig : NSObject /** Gets the default cache config used for shared instance or initialization when it does not provide any cache config. Such as `SDImageCache.sharedImageCache`. @note You can modify the property on default cache config, which can be used for later created cache instance. The already created cache instance does not get affected. */ @property (nonatomic, class, readonly, nonnull) SDImageCacheConfig *defaultCacheConfig; /** * Whether or not to disable iCloud backup * Defaults to YES. */ @property (assign, nonatomic) BOOL shouldDisableiCloud; /** * Whether or not to use memory cache * @note When the memory cache is disabled, the weak memory cache will also be disabled. * Defaults to YES. */ @property (assign, nonatomic) BOOL shouldCacheImagesInMemory; /* * The option to control weak memory cache for images. When enable, `SDImageCache`'s memory cache will use a weak maptable to store the image at the same time when it stored to memory, and get removed at the same time. * However when memory warning is triggered, since the weak maptable does not hold a strong reference to image instance, even when the memory cache itself is purged, some images which are held strongly by UIImageViews or other live instances can be recovered again, to avoid later re-query from disk cache or network. This may be helpful for the case, for example, when app enter background and memory is purged, cause cell flashing after re-enter foreground. * When enabling this option, we will sync back the image from weak maptable to strong cache during next time top level `sd_setImage` function call. * Defaults to NO (YES before 5.12.0 version). You can change this option dynamically. */ @property (assign, nonatomic) BOOL shouldUseWeakMemoryCache; /** * Whether or not to remove the expired disk data when application entering the background. (Not works for macOS) * Defaults to YES. */ @property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenEnterBackground; /** * Whether or not to remove the expired disk data when application been terminated. This operation is processed in sync to ensure clean up. * Defaults to YES. */ @property (assign, nonatomic) BOOL shouldRemoveExpiredDataWhenTerminate; /** * The reading options while reading cache from disk. * Defaults to 0. You can set this to `NSDataReadingMappedIfSafe` to improve performance. */ @property (assign, nonatomic) NSDataReadingOptions diskCacheReadingOptions; /** * The writing options while writing cache to disk. * Defaults to `NSDataWritingAtomic`. You can set this to `NSDataWritingWithoutOverwriting` to prevent overwriting an existing file. */ @property (assign, nonatomic) NSDataWritingOptions diskCacheWritingOptions; /** * The maximum length of time to keep an image in the disk cache, in seconds. * Setting this to a negative value means no expiring. * Setting this to zero means that all cached files would be removed when do expiration check. * Defaults to 1 week. */ @property (assign, nonatomic) NSTimeInterval maxDiskAge; /** * The maximum size of the disk cache, in bytes. * Defaults to 0. Which means there is no cache size limit. */ @property (assign, nonatomic) NSUInteger maxDiskSize; /** * The maximum "total cost" of the in-memory image cache. The cost function is the bytes size held in memory. * @note The memory cost is bytes size in memory, but not simple pixels count. For common ARGB8888 image, one pixel is 4 bytes (32 bits). * Defaults to 0. Which means there is no memory cost limit. */ @property (assign, nonatomic) NSUInteger maxMemoryCost; /** * The maximum number of objects in-memory image cache should hold. * Defaults to 0. Which means there is no memory count limit. */ @property (assign, nonatomic) NSUInteger maxMemoryCount; /* * The attribute which the clear cache will be checked against when clearing the disk cache * Default is Access Date */ @property (assign, nonatomic) SDImageCacheConfigExpireType diskCacheExpireType; /** * The custom file manager for disk cache. Pass nil to let disk cache choose the proper file manager. * Defaults to nil. * @note This value does not support dynamic changes. Which means further modification on this value after cache initialized has no effect. * @note Since `NSFileManager` does not support `NSCopying`. We just pass this by reference during copying. So it's not recommend to set this value on `defaultCacheConfig`. */ @property (strong, nonatomic, nullable) NSFileManager *fileManager; /** * The dispatch queue attr for ioQueue. You can config the QoS and concurrent/serial to internal IO queue. The ioQueue is used by SDImageCache to access read/write for disk data. * Defaults we use `DISPATCH_QUEUE_SERIAL`(NULL) under iOS 10, `DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL` above and equal iOS 10, using serial dispatch queue is to ensure single access for disk data. It's safe but may be slow. * @note You can override this to use `DISPATCH_QUEUE_CONCURRENT`, use concurrent queue. * @warning **MAKE SURE** to keep `diskCacheWritingOptions` to use `NSDataWritingAtomic`, or concurrent queue may cause corrupted disk data (because multiple threads read/write same file without atomic is not IO-safe). * @note This value does not support dynamic changes. Which means further modification on this value after cache initialized has no effect. */ @property (strong, nonatomic, nullable) dispatch_queue_attr_t ioQueueAttributes; /** * The custom memory cache class. Provided class instance must conform to `SDMemoryCache` protocol to allow usage. * Defaults to built-in `SDMemoryCache` class. * @note This value does not support dynamic changes. Which means further modification on this value after cache initialized has no effect. */ @property (assign, nonatomic, nonnull) Class memoryCacheClass; /** * The custom disk cache class. Provided class instance must conform to `SDDiskCache` protocol to allow usage. * Defaults to built-in `SDDiskCache` class. * @note This value does not support dynamic changes. Which means further modification on this value after cache initialized has no effect. */ @property (assign ,nonatomic, nonnull) Class diskCacheClass; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCacheConfig.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageCacheConfig.h" #import "SDMemoryCache.h" #import "SDDiskCache.h" static SDImageCacheConfig *_defaultCacheConfig; static const NSInteger kDefaultCacheMaxDiskAge = 60 * 60 * 24 * 7; // 1 week @implementation SDImageCacheConfig + (SDImageCacheConfig *)defaultCacheConfig { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _defaultCacheConfig = [SDImageCacheConfig new]; }); return _defaultCacheConfig; } - (instancetype)init { if (self = [super init]) { _shouldDisableiCloud = YES; _shouldCacheImagesInMemory = YES; _shouldUseWeakMemoryCache = NO; _shouldRemoveExpiredDataWhenEnterBackground = YES; _shouldRemoveExpiredDataWhenTerminate = YES; _diskCacheReadingOptions = 0; _diskCacheWritingOptions = NSDataWritingAtomic; _maxDiskAge = kDefaultCacheMaxDiskAge; _maxDiskSize = 0; _diskCacheExpireType = SDImageCacheConfigExpireTypeAccessDate; _fileManager = nil; if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, watchOS 3.0, *)) { _ioQueueAttributes = DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL; // DISPATCH_AUTORELEASE_FREQUENCY_WORK_ITEM } else { _ioQueueAttributes = DISPATCH_QUEUE_SERIAL; // NULL } _memoryCacheClass = [SDMemoryCache class]; _diskCacheClass = [SDDiskCache class]; } return self; } - (id)copyWithZone:(NSZone *)zone { SDImageCacheConfig *config = [[[self class] allocWithZone:zone] init]; config.shouldDisableiCloud = self.shouldDisableiCloud; config.shouldCacheImagesInMemory = self.shouldCacheImagesInMemory; config.shouldUseWeakMemoryCache = self.shouldUseWeakMemoryCache; config.shouldRemoveExpiredDataWhenEnterBackground = self.shouldRemoveExpiredDataWhenEnterBackground; config.shouldRemoveExpiredDataWhenTerminate = self.shouldRemoveExpiredDataWhenTerminate; config.diskCacheReadingOptions = self.diskCacheReadingOptions; config.diskCacheWritingOptions = self.diskCacheWritingOptions; config.maxDiskAge = self.maxDiskAge; config.maxDiskSize = self.maxDiskSize; config.maxMemoryCost = self.maxMemoryCost; config.maxMemoryCount = self.maxMemoryCount; config.diskCacheExpireType = self.diskCacheExpireType; config.fileManager = self.fileManager; // NSFileManager does not conform to NSCopying, just pass the reference config.ioQueueAttributes = self.ioQueueAttributes; // Pass the reference config.memoryCacheClass = self.memoryCacheClass; config.diskCacheClass = self.diskCacheClass; return config; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" #import "SDWebImageOperation.h" #import "SDWebImageDefine.h" #import "SDImageCoder.h" /// Image Cache Type typedef NS_ENUM(NSInteger, SDImageCacheType) { /** * For query and contains op in response, means the image isn't available in the image cache * For op in request, this type is not available and take no effect. */ SDImageCacheTypeNone, /** * For query and contains op in response, means the image was obtained from the disk cache. * For op in request, means process only disk cache. */ SDImageCacheTypeDisk, /** * For query and contains op in response, means the image was obtained from the memory cache. * For op in request, means process only memory cache. */ SDImageCacheTypeMemory, /** * For query and contains op in response, this type is not available and take no effect. * For op in request, means process both memory cache and disk cache. */ SDImageCacheTypeAll }; typedef void(^SDImageCacheCheckCompletionBlock)(BOOL isInCache); typedef void(^SDImageCacheQueryDataCompletionBlock)(NSData * _Nullable data); typedef void(^SDImageCacheCalculateSizeBlock)(NSUInteger fileCount, NSUInteger totalSize); typedef NSString * _Nullable (^SDImageCacheAdditionalCachePathBlock)(NSString * _Nonnull key); typedef void(^SDImageCacheQueryCompletionBlock)(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType); typedef void(^SDImageCacheContainsCompletionBlock)(SDImageCacheType containsCacheType); /** This is the built-in decoding process for image query from cache. @note If you want to implement your custom loader with `queryImageForKey:options:context:completion:` API, but also want to keep compatible with SDWebImage's behavior, you'd better use this to produce image. @param imageData The image data from the cache. Should not be nil @param cacheKey The image cache key from the input. Should not be nil @param options The options arg from the input @param context The context arg from the input @return The decoded image for current image data query from cache */ FOUNDATION_EXPORT UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context); /// Get the decode options from the loading context options and cache key. This is the built-in translate between the web loading part to the decoding part (which does not depends on). /// @param context The context arg from the input /// @param options The options arg from the input /// @param cacheKey The image cache key from the input. Should not be nil FOUNDATION_EXPORT SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey); /// Set the decode options to the loading context options. This is the built-in translate between the web loading part from the decoding part (which does not depends on). /// @param mutableContext The context arg to override /// @param mutableOptions The options arg to override /// @param decodeOptions The image decoding options FOUNDATION_EXPORT void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableContext, SDWebImageOptions * _Nonnull mutableOptions, SDImageCoderOptions * _Nonnull decodeOptions); /** This is the image cache protocol to provide custom image cache for `SDWebImageManager`. Though the best practice to custom image cache, is to write your own class which conform `SDMemoryCache` or `SDDiskCache` protocol for `SDImageCache` class (See more on `SDImageCacheConfig.memoryCacheClass & SDImageCacheConfig.diskCacheClass`). However, if your own cache implementation contains more advanced feature beyond `SDImageCache` itself, you can consider to provide this instead. For example, you can even use a cache manager like `SDImageCachesManager` to register multiple caches. */ @protocol SDImageCache @required /** Query the cached image from image cache for given key. The operation can be used to cancel the query. If image is cached in memory, completion is called synchronously, else asynchronously and depends on the options arg (See `SDWebImageQueryDiskSync`) @param key The image cache key @param options A mask to specify options to use for this query @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. Pass `.callbackQueue` to control callback queue @param completionBlock The completion block. Will not get called if the operation is cancelled @return The operation for this query */ - (nullable id)queryImageForKey:(nullable NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock API_DEPRECATED_WITH_REPLACEMENT("queryImageForKey:options:context:cacheType:completion:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); @optional /** Query the cached image from image cache for given key. The operation can be used to cancel the query. If image is cached in memory, completion is called synchronously, else asynchronously and depends on the options arg (See `SDWebImageQueryDiskSync`) @param key The image cache key @param options A mask to specify options to use for this query @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. Pass `.callbackQueue` to control callback queue @param cacheType Specify where to query the cache from. By default we use `.all`, which means both memory cache and disk cache. You can choose to query memory only or disk only as well. Pass `.none` is invalid and callback with nil immediately. @param completionBlock The completion block. Will not get called if the operation is cancelled @return The operation for this query */ - (nullable id)queryImageForKey:(nullable NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock; @required /** Store the image into image cache for the given key. If cache type is memory only, completion is called synchronously, else asynchronously. @param image The image to store @param imageData The image data to be used for disk storage @param key The image cache key @param cacheType The image store op cache type @param completionBlock A block executed after the operation is finished */ - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock API_DEPRECATED_WITH_REPLACEMENT("storeImage:imageData:forKey:options:context:cacheType:completion:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); @optional /** Store the image into image cache for the given key. If cache type is memory only, completion is called synchronously, else asynchronously. @param image The image to store @param imageData The image data to be used for disk storage @param key The image cache key @param options A mask to specify options to use for this store @param context The context options to use. Pass `.callbackQueue` to control callback queue @param cacheType The image store op cache type @param completionBlock A block executed after the operation is finished */ - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData forKey:(nullable NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock; #pragma mark - Deprecated because SDWebImageManager does not use these APIs /** Remove the image from image cache for the given key. If cache type is memory only, completion is called synchronously, else asynchronously. @param key The image cache key @param cacheType The image remove op cache type @param completionBlock A block executed after the operation is finished */ - (void)removeImageForKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock API_DEPRECATED("No longer use. Cast to cache instance and call its API", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); /** Check if image cache contains the image for the given key (does not load the image). If image is cached in memory, completion is called synchronously, else asynchronously. @param key The image cache key @param cacheType The image contains op cache type @param completionBlock A block executed after the operation is finished. */ - (void)containsImageForKey:(nullable NSString *)key cacheType:(SDImageCacheType)cacheType completion:(nullable SDImageCacheContainsCompletionBlock)completionBlock API_DEPRECATED("No longer use. Cast to cache instance and call its API", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); /** Clear all the cached images for image cache. If cache type is memory only, completion is called synchronously, else asynchronously. @param cacheType The image clear op cache type @param completionBlock A block executed after the operation is finished */ - (void)clearWithCacheType:(SDImageCacheType)cacheType completion:(nullable SDWebImageNoParamsBlock)completionBlock API_DEPRECATED("No longer use. Cast to cache instance and call its API", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCacheDefine.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageCacheDefine.h" #import "SDImageCodersManager.h" #import "SDImageCoderHelper.h" #import "SDAnimatedImage.h" #import "UIImage+Metadata.h" #import "SDInternalMacros.h" #import "SDDeviceHelper.h" #import SDImageCoderOptions * _Nonnull SDGetDecodeOptionsFromContext(SDWebImageContext * _Nullable context, SDWebImageOptions options, NSString * _Nonnull cacheKey) { BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); NSNumber *scaleValue = context[SDWebImageContextImageScaleFactor]; CGFloat scale = scaleValue.doubleValue >= 1 ? scaleValue.doubleValue : SDImageScaleFactorForKey(cacheKey); // Use cache key to detect scale NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; NSValue *thumbnailSizeValue; BOOL shouldScaleDown = SD_OPTIONS_CONTAINS(options, SDWebImageScaleDownLargeImages); NSNumber *scaleDownLimitBytesValue = context[SDWebImageContextImageScaleDownLimitBytes]; if (scaleDownLimitBytesValue == nil && shouldScaleDown) { // Use the default limit bytes scaleDownLimitBytesValue = @(SDImageCoderHelper.defaultScaleDownLimitBytes); } if (context[SDWebImageContextImageThumbnailPixelSize]) { thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; } NSString *typeIdentifierHint = context[SDWebImageContextImageTypeIdentifierHint]; NSString *fileExtensionHint; if (!typeIdentifierHint) { // UTI has high priority fileExtensionHint = cacheKey.pathExtension; // without dot if (fileExtensionHint.length == 0) { // Ignore file extension which is empty fileExtensionHint = nil; } } // First check if user provided decode options SDImageCoderMutableOptions *mutableCoderOptions; if (context[SDWebImageContextImageDecodeOptions] != nil) { mutableCoderOptions = [NSMutableDictionary dictionaryWithDictionary:context[SDWebImageContextImageDecodeOptions]]; } else { mutableCoderOptions = [NSMutableDictionary dictionaryWithCapacity:6]; } // Some options need preserve the custom decode options NSNumber *decodeToHDR = context[SDWebImageContextImageDecodeToHDR]; if (decodeToHDR == nil) { decodeToHDR = mutableCoderOptions[SDImageCoderDecodeToHDR]; } // Override individual options mutableCoderOptions[SDImageCoderDecodeFirstFrameOnly] = @(decodeFirstFrame); mutableCoderOptions[SDImageCoderDecodeScaleFactor] = @(scale); mutableCoderOptions[SDImageCoderDecodePreserveAspectRatio] = preserveAspectRatioValue; mutableCoderOptions[SDImageCoderDecodeThumbnailPixelSize] = thumbnailSizeValue; mutableCoderOptions[SDImageCoderDecodeTypeIdentifierHint] = typeIdentifierHint; mutableCoderOptions[SDImageCoderDecodeFileExtensionHint] = fileExtensionHint; mutableCoderOptions[SDImageCoderDecodeScaleDownLimitBytes] = scaleDownLimitBytesValue; mutableCoderOptions[SDImageCoderDecodeToHDR] = decodeToHDR; return [mutableCoderOptions copy]; } void SDSetDecodeOptionsToContext(SDWebImageMutableContext * _Nonnull mutableContext, SDWebImageOptions * _Nonnull mutableOptions, SDImageCoderOptions * _Nonnull decodeOptions) { if ([decodeOptions[SDImageCoderDecodeFirstFrameOnly] boolValue]) { *mutableOptions |= SDWebImageDecodeFirstFrameOnly; } else { *mutableOptions &= ~SDWebImageDecodeFirstFrameOnly; } mutableContext[SDWebImageContextImageScaleFactor] = decodeOptions[SDImageCoderDecodeScaleFactor]; mutableContext[SDWebImageContextImagePreserveAspectRatio] = decodeOptions[SDImageCoderDecodePreserveAspectRatio]; mutableContext[SDWebImageContextImageThumbnailPixelSize] = decodeOptions[SDImageCoderDecodeThumbnailPixelSize]; mutableContext[SDWebImageContextImageScaleDownLimitBytes] = decodeOptions[SDImageCoderDecodeScaleDownLimitBytes]; mutableContext[SDWebImageContextImageDecodeToHDR] = decodeOptions[SDImageCoderDecodeToHDR]; NSString *typeIdentifierHint = decodeOptions[SDImageCoderDecodeTypeIdentifierHint]; if (!typeIdentifierHint) { NSString *fileExtensionHint = decodeOptions[SDImageCoderDecodeFileExtensionHint]; if (fileExtensionHint) { typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage); // Ignore dynamic UTI if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) { typeIdentifierHint = nil; } } } mutableContext[SDWebImageContextImageTypeIdentifierHint] = typeIdentifierHint; } UIImage * _Nullable SDImageCacheDecodeImageData(NSData * _Nonnull imageData, NSString * _Nonnull cacheKey, SDWebImageOptions options, SDWebImageContext * _Nullable context) { NSCParameterAssert(imageData); NSCParameterAssert(cacheKey); UIImage *image; SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey); BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue]; // Grab the image coder id imageCoder = context[SDWebImageContextImageCoder]; if (!imageCoder) { imageCoder = [SDImageCodersManager sharedManager]; } if (!decodeFirstFrame) { Class animatedImageClass = context[SDWebImageContextAnimatedImageClass]; // check whether we should use `SDAnimatedImage` if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) { image = [[animatedImageClass alloc] initWithData:imageData scale:scale options:coderOptions]; if (image) { // Preload frames if supported if (options & SDWebImagePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) { [((id)image) preloadAllFrames]; } } else { // Check image class matching if (options & SDWebImageMatchAnimatedImageClass) { return nil; } } } } if (!image) { image = [imageCoder decodedImageWithData:imageData options:coderOptions]; } if (image) { SDImageForceDecodePolicy policy = SDImageForceDecodePolicyAutomatic; NSNumber *policyValue = context[SDWebImageContextImageForceDecodePolicy]; if (policyValue != nil) { policy = policyValue.unsignedIntegerValue; } // TODO: Deprecated, remove in SD 6.0... #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage)) { policy = SDImageForceDecodePolicyNever; } #pragma clang diagnostic pop image = [SDImageCoderHelper decodedImageWithImage:image policy:policy]; // assign the decode options, to let manager check whether to re-decode if needed image.sd_decodeOptions = coderOptions; } return image; } ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCachesManager.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDImageCacheDefine.h" /// Policy for cache operation typedef NS_ENUM(NSUInteger, SDImageCachesManagerOperationPolicy) { SDImageCachesManagerOperationPolicySerial, // process all caches serially (from the highest priority to the lowest priority cache by order) SDImageCachesManagerOperationPolicyConcurrent, // process all caches concurrently SDImageCachesManagerOperationPolicyHighestOnly, // process the highest priority cache only SDImageCachesManagerOperationPolicyLowestOnly // process the lowest priority cache only }; /** A caches manager to manage multiple caches. */ @interface SDImageCachesManager : NSObject /** Returns the global shared caches manager instance. By default we will set [`SDImageCache.sharedImageCache`] into the caches array. */ @property (nonatomic, class, readonly, nonnull) SDImageCachesManager *sharedManager; // These are op policy for cache manager. /** Operation policy for query op. Defaults to `Serial`, means query all caches serially (one completion called then next begin) until one cache query success (`image` != nil). */ @property (nonatomic, assign) SDImageCachesManagerOperationPolicy queryOperationPolicy; /** Operation policy for store op. Defaults to `HighestOnly`, means store to the highest priority cache only. */ @property (nonatomic, assign) SDImageCachesManagerOperationPolicy storeOperationPolicy; /** Operation policy for remove op. Defaults to `Concurrent`, means remove all caches concurrently. */ @property (nonatomic, assign) SDImageCachesManagerOperationPolicy removeOperationPolicy; /** Operation policy for contains op. Defaults to `Serial`, means check all caches serially (one completion called then next begin) until one cache check success (`containsCacheType` != None). */ @property (nonatomic, assign) SDImageCachesManagerOperationPolicy containsOperationPolicy; /** Operation policy for clear op. Defaults to `Concurrent`, means clear all caches concurrently. */ @property (nonatomic, assign) SDImageCachesManagerOperationPolicy clearOperationPolicy; /** All caches in caches manager. The caches array is a priority queue, which means the later added cache will have the highest priority */ @property (nonatomic, copy, nullable) NSArray> *caches; /** Add a new cache to the end of caches array. Which has the highest priority. @param cache cache */ - (void)addCache:(nonnull id)cache; /** Remove a cache in the caches array. @param cache cache */ - (void)removeCache:(nonnull id)cache; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCachesManager.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageCachesManager.h" #import "SDImageCachesManagerOperation.h" #import "SDImageCache.h" #import "SDInternalMacros.h" @interface SDImageCachesManager () @property (nonatomic, strong, nonnull) NSMutableArray> *imageCaches; @end @implementation SDImageCachesManager { SD_LOCK_DECLARE(_cachesLock); } + (SDImageCachesManager *)sharedManager { static dispatch_once_t onceToken; static SDImageCachesManager *manager; dispatch_once(&onceToken, ^{ manager = [[SDImageCachesManager alloc] init]; }); return manager; } - (instancetype)init { self = [super init]; if (self) { self.queryOperationPolicy = SDImageCachesManagerOperationPolicySerial; self.storeOperationPolicy = SDImageCachesManagerOperationPolicyHighestOnly; self.removeOperationPolicy = SDImageCachesManagerOperationPolicyConcurrent; self.containsOperationPolicy = SDImageCachesManagerOperationPolicySerial; self.clearOperationPolicy = SDImageCachesManagerOperationPolicyConcurrent; // initialize with default image caches _imageCaches = [NSMutableArray arrayWithObject:[SDImageCache sharedImageCache]]; SD_LOCK_INIT(_cachesLock); } return self; } - (NSArray> *)caches { SD_LOCK(_cachesLock); NSArray> *caches = [_imageCaches copy]; SD_UNLOCK(_cachesLock); return caches; } - (void)setCaches:(NSArray> *)caches { SD_LOCK(_cachesLock); [_imageCaches removeAllObjects]; if (caches.count) { [_imageCaches addObjectsFromArray:caches]; } SD_UNLOCK(_cachesLock); } #pragma mark - Cache IO operations - (void)addCache:(id)cache { if (![cache conformsToProtocol:@protocol(SDImageCache)]) { return; } SD_LOCK(_cachesLock); [_imageCaches addObject:cache]; SD_UNLOCK(_cachesLock); } - (void)removeCache:(id)cache { if (![cache conformsToProtocol:@protocol(SDImageCache)]) { return; } SD_LOCK(_cachesLock); [_imageCaches removeObject:cache]; SD_UNLOCK(_cachesLock); } #pragma mark - SDImageCache - (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context completion:(SDImageCacheQueryCompletionBlock)completionBlock { return [self queryImageForKey:key options:options context:context cacheType:SDImageCacheTypeAll completion:completionBlock]; } - (id)queryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDImageCacheQueryCompletionBlock)completionBlock { if (!key) { return nil; } NSArray> *caches = self.caches; NSUInteger count = caches.count; if (count == 0) { return nil; } else if (count == 1) { return [caches.firstObject queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } switch (self.queryOperationPolicy) { case SDImageCachesManagerOperationPolicyHighestOnly: { id cache = caches.lastObject; return [cache queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyLowestOnly: { id cache = caches.firstObject; return [cache queryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyConcurrent: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; [self concurrentQueryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; return operation; } break; case SDImageCachesManagerOperationPolicySerial: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; [self serialQueryImageForKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; return operation; } break; default: return nil; break; } } - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock { [self storeImage:image imageData:imageData forKey:key options:0 context:nil cacheType:cacheType completion:completionBlock]; } - (void)storeImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock { if (!key) { return; } NSArray> *caches = self.caches; NSUInteger count = caches.count; if (count == 0) { return; } else if (count == 1) { [caches.firstObject storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock]; return; } switch (self.storeOperationPolicy) { case SDImageCachesManagerOperationPolicyHighestOnly: { id cache = caches.lastObject; [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyLowestOnly: { id cache = caches.firstObject; [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyConcurrent: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; [self concurrentStoreImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; } break; case SDImageCachesManagerOperationPolicySerial: { [self serialStoreImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator]; } break; default: break; } } - (void)removeImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock { if (!key) { return; } NSArray> *caches = self.caches; NSUInteger count = caches.count; if (count == 0) { return; } else if (count == 1) { [caches.firstObject removeImageForKey:key cacheType:cacheType completion:completionBlock]; return; } switch (self.removeOperationPolicy) { case SDImageCachesManagerOperationPolicyHighestOnly: { id cache = caches.lastObject; [cache removeImageForKey:key cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyLowestOnly: { id cache = caches.firstObject; [cache removeImageForKey:key cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyConcurrent: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; [self concurrentRemoveImageForKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; } break; case SDImageCachesManagerOperationPolicySerial: { [self serialRemoveImageForKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator]; } break; default: break; } } - (void)containsImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDImageCacheContainsCompletionBlock)completionBlock { if (!key) { return; } NSArray> *caches = self.caches; NSUInteger count = caches.count; if (count == 0) { return; } else if (count == 1) { [caches.firstObject containsImageForKey:key cacheType:cacheType completion:completionBlock]; return; } switch (self.clearOperationPolicy) { case SDImageCachesManagerOperationPolicyHighestOnly: { id cache = caches.lastObject; [cache containsImageForKey:key cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyLowestOnly: { id cache = caches.firstObject; [cache containsImageForKey:key cacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyConcurrent: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; [self concurrentContainsImageForKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; } break; case SDImageCachesManagerOperationPolicySerial: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; [self serialContainsImageForKey:key cacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; } break; default: break; } } - (void)clearWithCacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock { NSArray> *caches = self.caches; NSUInteger count = caches.count; if (count == 0) { return; } else if (count == 1) { [caches.firstObject clearWithCacheType:cacheType completion:completionBlock]; return; } switch (self.clearOperationPolicy) { case SDImageCachesManagerOperationPolicyHighestOnly: { id cache = caches.lastObject; [cache clearWithCacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyLowestOnly: { id cache = caches.firstObject; [cache clearWithCacheType:cacheType completion:completionBlock]; } break; case SDImageCachesManagerOperationPolicyConcurrent: { SDImageCachesManagerOperation *operation = [SDImageCachesManagerOperation new]; [operation beginWithTotalCount:caches.count]; [self concurrentClearWithCacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator operation:operation]; } break; case SDImageCachesManagerOperationPolicySerial: { [self serialClearWithCacheType:cacheType completion:completionBlock enumerator:caches.reverseObjectEnumerator]; } break; default: break; } } #pragma mark - Concurrent Operation - (void)concurrentQueryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType completion:(SDImageCacheQueryCompletionBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { NSParameterAssert(enumerator); NSParameterAssert(operation); for (id cache in enumerator) { [cache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { if (operation.isCancelled) { // Cancelled return; } if (operation.isFinished) { // Finished return; } [operation completeOne]; if (image) { // Success [operation done]; if (completionBlock) { completionBlock(image, data, cacheType); } return; } if (operation.pendingCount == 0) { // Complete [operation done]; if (completionBlock) { completionBlock(nil, nil, SDImageCacheTypeNone); } } }]; } } - (void)concurrentStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { NSParameterAssert(enumerator); NSParameterAssert(operation); for (id cache in enumerator) { [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:^{ if (operation.isCancelled) { // Cancelled return; } if (operation.isFinished) { // Finished return; } [operation completeOne]; if (operation.pendingCount == 0) { // Complete [operation done]; if (completionBlock) { completionBlock(); } } }]; } } - (void)concurrentRemoveImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { NSParameterAssert(enumerator); NSParameterAssert(operation); for (id cache in enumerator) { [cache removeImageForKey:key cacheType:cacheType completion:^{ if (operation.isCancelled) { // Cancelled return; } if (operation.isFinished) { // Finished return; } [operation completeOne]; if (operation.pendingCount == 0) { // Complete [operation done]; if (completionBlock) { completionBlock(); } } }]; } } - (void)concurrentContainsImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDImageCacheContainsCompletionBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { NSParameterAssert(enumerator); NSParameterAssert(operation); for (id cache in enumerator) { [cache containsImageForKey:key cacheType:cacheType completion:^(SDImageCacheType containsCacheType) { if (operation.isCancelled) { // Cancelled return; } if (operation.isFinished) { // Finished return; } [operation completeOne]; if (containsCacheType != SDImageCacheTypeNone) { // Success [operation done]; if (completionBlock) { completionBlock(containsCacheType); } return; } if (operation.pendingCount == 0) { // Complete [operation done]; if (completionBlock) { completionBlock(SDImageCacheTypeNone); } } }]; } } - (void)concurrentClearWithCacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { NSParameterAssert(enumerator); NSParameterAssert(operation); for (id cache in enumerator) { [cache clearWithCacheType:cacheType completion:^{ if (operation.isCancelled) { // Cancelled return; } if (operation.isFinished) { // Finished return; } [operation completeOne]; if (operation.pendingCount == 0) { // Complete [operation done]; if (completionBlock) { completionBlock(); } } }]; } } #pragma mark - Serial Operation - (void)serialQueryImageForKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)queryCacheType completion:(SDImageCacheQueryCompletionBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { NSParameterAssert(enumerator); NSParameterAssert(operation); id cache = enumerator.nextObject; if (!cache) { // Complete [operation done]; if (completionBlock) { completionBlock(nil, nil, SDImageCacheTypeNone); } return; } @weakify(self); [cache queryImageForKey:key options:options context:context cacheType:queryCacheType completion:^(UIImage * _Nullable image, NSData * _Nullable data, SDImageCacheType cacheType) { @strongify(self); if (operation.isCancelled) { // Cancelled return; } if (operation.isFinished) { // Finished return; } [operation completeOne]; if (image) { // Success [operation done]; if (completionBlock) { completionBlock(image, data, cacheType); } return; } // Next [self serialQueryImageForKey:key options:options context:context cacheType:queryCacheType completion:completionBlock enumerator:enumerator operation:operation]; }]; } - (void)serialStoreImage:(UIImage *)image imageData:(NSData *)imageData forKey:(NSString *)key options:(SDWebImageOptions)options context:(SDWebImageContext *)context cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator> *)enumerator { NSParameterAssert(enumerator); id cache = enumerator.nextObject; if (!cache) { // Complete if (completionBlock) { completionBlock(); } return; } @weakify(self); [cache storeImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:^{ @strongify(self); // Next [self serialStoreImage:image imageData:imageData forKey:key options:options context:context cacheType:cacheType completion:completionBlock enumerator:enumerator]; }]; } - (void)serialRemoveImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator> *)enumerator { NSParameterAssert(enumerator); id cache = enumerator.nextObject; if (!cache) { // Complete if (completionBlock) { completionBlock(); } return; } @weakify(self); [cache removeImageForKey:key cacheType:cacheType completion:^{ @strongify(self); // Next [self serialRemoveImageForKey:key cacheType:cacheType completion:completionBlock enumerator:enumerator]; }]; } - (void)serialContainsImageForKey:(NSString *)key cacheType:(SDImageCacheType)cacheType completion:(SDImageCacheContainsCompletionBlock)completionBlock enumerator:(NSEnumerator> *)enumerator operation:(SDImageCachesManagerOperation *)operation { NSParameterAssert(enumerator); NSParameterAssert(operation); id cache = enumerator.nextObject; if (!cache) { // Complete [operation done]; if (completionBlock) { completionBlock(SDImageCacheTypeNone); } return; } @weakify(self); [cache containsImageForKey:key cacheType:cacheType completion:^(SDImageCacheType containsCacheType) { @strongify(self); if (operation.isCancelled) { // Cancelled return; } if (operation.isFinished) { // Finished return; } [operation completeOne]; if (containsCacheType != SDImageCacheTypeNone) { // Success [operation done]; if (completionBlock) { completionBlock(containsCacheType); } return; } // Next [self serialContainsImageForKey:key cacheType:cacheType completion:completionBlock enumerator:enumerator operation:operation]; }]; } - (void)serialClearWithCacheType:(SDImageCacheType)cacheType completion:(SDWebImageNoParamsBlock)completionBlock enumerator:(NSEnumerator> *)enumerator { NSParameterAssert(enumerator); id cache = enumerator.nextObject; if (!cache) { // Complete if (completionBlock) { completionBlock(); } return; } @weakify(self); [cache clearWithCacheType:cacheType completion:^{ @strongify(self); // Next [self serialClearWithCacheType:cacheType completion:completionBlock enumerator:enumerator]; }]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCoder.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" #import "NSData+ImageContentType.h" #import "SDImageFrame.h" /// Image Decoding/Encoding Options typedef NSString * SDImageCoderOption NS_STRING_ENUM; typedef NSDictionary SDImageCoderOptions; typedef NSMutableDictionary SDImageCoderMutableOptions; #pragma mark - Image Decoding Options // These options are for image decoding /** A Boolean value indicating whether to decode the first frame only for animated image during decoding. (NSNumber). If not provide, decode animated image if need. @note works for `SDImageCoder`. */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFirstFrameOnly; /** A CGFloat value which is greater than or equal to 1.0. This value specify the image scale factor for decoding. If not provide, use 1.0. (NSNumber) @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleFactor; /** A Boolean value indicating whether to keep the original aspect ratio when generating thumbnail images (or bitmap images from vector format). Defaults to YES. @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodePreserveAspectRatio; /** A CGSize value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.preserveAspectRatio`) the value size. Defaults to CGSizeZero, which means no thumbnail generation at all. @note Supports for animated image as well. @note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both. @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeThumbnailPixelSize; /** A NSString value indicating the source image's file extension. Example: "jpg", "nef", "tif", don't prefix the dot Some image file format share the same data structure but has different tag explanation, like TIFF and NEF/SRW, see https://en.wikipedia.org/wiki/TIFF Changing the file extension cause the different image result. The coder (like ImageIO) may use file extension to choose the correct parser @note However, different UTType may share the same file extension, like `public.jpeg` and `public.jpeg-2000` both use `.jpg`. If you want detail control, use `TypeIdentifierHint` below */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeFileExtensionHint; /** A NSString value (UTI) indicating the source image's file extension. Example: "public.jpeg-2000", "com.nikon.raw-image", "public.tiff" Some image file format share the same data structure but has different tag explanation, like TIFF and NEF/SRW, see https://en.wikipedia.org/wiki/TIFF Changing the file extension cause the different image result. The coder (like ImageIO) may use file extension to choose the correct parser @note If you provide `TypeIdentifierHint`, the `FileExtensionHint` option above will be ignored (because UTType has high priority) @note If you really don't want any hint which effect the image result, pass `NSNull.null` instead */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeTypeIdentifierHint; /** A BOOL value indicating whether to use lazy-decoding. Defaults to NO on animated image coder, but defaults to YES on static image coder. CGImageRef, this image object typically support lazy-decoding, via the `CGDataProviderCreateDirectAccess` or `CGDataProviderCreateSequential` Which allows you to provide a lazy-called callback to access bitmap buffer, so that you can achieve lazy-decoding when consumer actually need bitmap buffer UIKit on iOS use heavy on this and ImageIO codec prefers to lazy-decoding for common Hardware-Accelerate format like JPEG/PNG/HEIC But however, the consumer may access bitmap buffer when running on main queue, like CoreAnimation layer render image. So this is a trade-off You can force us to disable the lazy-decoding and always allocate bitmap buffer on RAM, but this may have higher ratio of OOM (out of memory) @note The default value is NO for animated image coder (means `animatedImageFrameAtIndex:`) @note The default value is YES for static image coder (means `decodedImageWithData:`) @note works for `SDImageCoder`, `SDProgressiveImageCoder`, `SDAnimatedImageCoder`. */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeUseLazyDecoding; /** A NSUInteger value to provide the limit bytes during decoding. This can help to avoid OOM on large frame count animated image or large pixel static image when you don't know how much RAM it occupied before decoding The decoder will do these logic based on limit bytes: 1. Get the total frame count (static image means 1) 2. Calculate the `framePixelSize` width/height to `sqrt(limitBytes / frameCount / bytesPerPixel)`, keeping aspect ratio (at least 1x1) 3. If the `framePixelSize < originalImagePixelSize`, then do thumbnail decoding (see `SDImageCoderDecodeThumbnailPixelSize`) use the `framePixelSize` and `preseveAspectRatio = YES` 4. Else, use the full pixel decoding (small than limit bytes) 5. Whatever result, this does not effect the animated/static behavior of image. So even if you set `limitBytes = 1 && frameCount = 100`, we will stll create animated image with each frame `1x1` pixel size. @note You can use the logic from `+[SDImageCoder scaledSizeWithImageSize:limitBytes:bytesPerPixel:frameCount:]` @note This option has higher priority than `.decodeThumbnailPixelSize` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeScaleDownLimitBytes; /** A Boolean (`SDImageHDRType.rawValue`) value (stored inside NSNumber) to provide converting to HDR during decoding. Currently if number is 0 (`SDImageHDRTypeSDR`), use SDR, else use HDR. But we may extend this option to represent `SDImageHDRType` all cases in the future (means, think this options as uint number, but not actual boolean) @note Supported by iOS 17 and above when using ImageIO coder (third-party coder can support lower firmware) Defaults to @(NO), decoder will automatically convert SDR. @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderDecodeToHDR; #pragma mark - Image Encoding Options /** A NSUInteger (`SDImageHDRType.rawValue`) value (stored inside NSNumber) to provide converting to HDR during encoding. Read the below carefully to choose the value. @note 0(`SDImageHDRTypeSDR`) means SDR; 1(`SDImageHDRTypeISOHDR`) means ISO HDR (at least using 10 bits per components or above, supported by AVIF/HEIF/JPEG-XL); 2(`SDImageHDRTypeISOGainMap`) means ISO Gain Map HDR (may use 8 bits per components, supported by AVIF/HEIF/JPEG-XL, as well as traditional JPEG) @note Gain Map like a mask image with metadata, which contains the depth/bright information for pixels (1/4 resolution), which used to convert between HDR and SDR. @note If you use CIImage as HDR pipeline, you can export as CGImage for encoding. (But it's also recommanded to use CIImage's `JPEGRepresentationOfImage` or `HEIFRepresentationOfImage`) @note Supported by iOS 18 and above when using ImageIO coder (third-party coder can support lower firmware) Defaults to @(0), encoder will automatically convert SDR. @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeToHDR; /** A Boolean value indicating whether to encode the first frame only for animated image during encoding. (NSNumber). If not provide, encode animated image if need. @note works for `SDImageCoder`. */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeFirstFrameOnly; /** A double value between 0.0-1.0 indicating the encode compression quality to produce the image data. 1.0 resulting in no compression and 0.0 resulting in the maximum compression possible. If not provide, use 1.0. (NSNumber) @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeCompressionQuality; /** A UIColor(NSColor) value to used for non-alpha image encoding when the input image has alpha channel, the background color will be used to compose the alpha one. If not provide, use white color. @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeBackgroundColor; /** A CGSize value indicating the max image resolution in pixels during encoding. For vector image, this also effect the output vector data information about width and height. The encoder will not generate the encoded image larger than this limit. Note it always use the aspect ratio of input image.. Defaults to CGSizeZero, which means no max size limit at all. @note Supports for animated image as well. @note The output image's width is limited to pixel size's width, the output image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both. @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxPixelSize; /** A NSUInteger value specify the max output data bytes size after encoding. Some lossy format like JPEG/HEIF supports the hint for codec to automatically reduce the quality and match the file size you want. Note this option will override the `SDImageCoderEncodeCompressionQuality`, because now the quality is decided by the encoder. (NSNumber) @note This is a hint, no guarantee for output size because of compression algorithm limit. And this options does not works for vector images. @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeMaxFileSize; /** A Boolean value indicating the encoding format should contains a thumbnail image into the output data. Only some of image format (like JPEG/HEIF/AVIF) support this behavior. The embed thumbnail will be used during next time thumbnail decoding (provided `.thumbnailPixelSize`), which is faster than full image thumbnail decoding. (NSNumber) Defaults to NO, which does not embed any thumbnail. @note The thumbnail image's pixel size is not defined, the encoder can choose the proper pixel size which is suitable for encoding quality. @note works for `SDImageCoder` */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderEncodeEmbedThumbnail; /** A SDWebImageContext object which hold the original context options from top-level API. (SDWebImageContext) This option is ignored for all built-in coders and take no effect. But this may be useful for some custom coders, because some business logic may dependent on things other than image or image data information only. Only the unknown context from top-level API (See SDWebImageDefine.h) may be passed in during image loading. See `SDWebImageContext` for more detailed information. @warning Deprecated. This does nothing from 5.14.0. Use `SDWebImageContextImageDecodeOptions` to pass additional information in top-level API, and use `SDImageCoderOptions` to retrieve options from coder. */ FOUNDATION_EXPORT SDImageCoderOption _Nonnull const SDImageCoderWebImageContext API_DEPRECATED("No longer supported. Use SDWebImageContextDecodeOptions in loader API to provide options. Use SDImageCoderOptions in coder API to retrieve options.", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0)); #pragma mark - Coder /** This is the image coder protocol to provide custom image decoding/encoding. These methods are all required to implement. @note Pay attention that these methods are not called from main queue. */ @protocol SDImageCoder @required #pragma mark - Decoding /** Returns YES if this coder can decode some data. Otherwise, the data should be passed to another coder. @param data The image data so we can look at it @return YES if this coder can decode the data, NO otherwise */ - (BOOL)canDecodeFromData:(nullable NSData *)data; /** Decode the image data to image. @note This protocol may supports decode animated image frames. You can use `+[SDImageCoderHelper animatedImageWithFrames:]` to produce an animated image with frames. @param data The image data to be decoded @param options A dictionary containing any decoding options. Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for image. Pass @{SDImageCoderDecodeFirstFrameOnly: @(YES)} to decode the first frame only. @return The decoded image from data */ - (nullable UIImage *)decodedImageWithData:(nullable NSData *)data options:(nullable SDImageCoderOptions *)options; #pragma mark - Encoding /** Returns YES if this coder can encode some image. Otherwise, it should be passed to another coder. For custom coder which introduce new image format, you'd better define a new `SDImageFormat` using like this. If you're creating public coder plugin for new image format, also update `https://github.com/rs/SDWebImage/wiki/Coder-Plugin-List` to avoid same value been defined twice. * @code static const SDImageFormat SDImageFormatHEIF = 10; * @endcode @param format The image format @return YES if this coder can encode the image, NO otherwise */ - (BOOL)canEncodeToFormat:(SDImageFormat)format NS_SWIFT_NAME(canEncode(to:)); /** Encode the image to image data. @note This protocol may supports encode animated image frames. You can use `+[SDImageCoderHelper framesFromAnimatedImage:]` to assemble an animated image with frames. But this consume time is not always reversible. In 5.15.0, we introduce `encodedDataWithFrames` API for better animated image encoding. Use that instead. @note Which means, this just forward to `encodedDataWithFrames([SDImageFrame(image: image, duration: 0], image.sd_imageLoopCount))` @param image The image to be encoded @param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible @param options A dictionary containing any encoding options. Pass @{SDImageCoderEncodeCompressionQuality: @(1)} to specify compression quality. @return The encoded image data */ - (nullable NSData *)encodedDataWithImage:(nullable UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options; #pragma mark - Animated Encoding @optional /** Encode the animated image frames to image data. @param frames The animated image frames to be encoded, should be at least 1 element, or it will fallback to static image encode. @param loopCount The final animated image loop count. 0 means infinity loop. This config ignore each frame's `sd_imageLoopCount` @param format The image format to encode, you should note `SDImageFormatUndefined` format is also possible @param options A dictionary containing any encoding options. Pass @{SDImageCoderEncodeCompressionQuality: @(1)} to specify compression quality. @return The encoded image data */ - (nullable NSData *)encodedDataWithFrames:(nonnull NSArray*)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options; @end #pragma mark - Progressive Coder /** This is the image coder protocol to provide custom progressive image decoding. These methods are all required to implement. @note Pay attention that these methods are not called from main queue. */ @protocol SDProgressiveImageCoder @required /** Returns YES if this coder can incremental decode some data. Otherwise, it should be passed to another coder. @param data The image data so we can look at it @return YES if this coder can decode the data, NO otherwise */ - (BOOL)canIncrementalDecodeFromData:(nullable NSData *)data; /** Because incremental decoding need to keep the decoded context, we will alloc a new instance with the same class for each download operation to avoid conflicts This init method should not return nil @param options A dictionary containing any progressive decoding options (instance-level). Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for progressive animated image (each frames should use the same scale). @return A new instance to do incremental decoding for the specify image format */ - (nonnull instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options; /** Update the incremental decoding when new image data available @param data The image data has been downloaded so far @param finished Whether the download has finished */ - (void)updateIncrementalData:(nullable NSData *)data finished:(BOOL)finished; /** Incremental decode the current image data to image. @note Due to the performance issue for progressive decoding and the integration for image view. This method may only return the first frame image even if the image data is animated image. If you want progressive animated image decoding, conform to `SDAnimatedImageCoder` protocol as well and use `animatedImageFrameAtIndex:` instead. @param options A dictionary containing any progressive decoding options. Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for progressive image @return The decoded image from current data */ - (nullable UIImage *)incrementalDecodedImageWithOptions:(nullable SDImageCoderOptions *)options; @end #pragma mark - Animated Image Provider /** This is the animated image protocol to provide the basic function for animated image rendering. It's adopted by `SDAnimatedImage` and `SDAnimatedImageCoder` */ @protocol SDAnimatedImageProvider @required /** The original animated image data for current image. If current image is not an animated format, return nil. We may use this method to grab back the original image data if need, such as NSCoding or compare. @return The animated image data */ @property (nonatomic, copy, readonly, nullable) NSData *animatedImageData; /** Total animated frame count. If the frame count is less than 1, then the methods below will be ignored. @return Total animated frame count. */ @property (nonatomic, assign, readonly) NSUInteger animatedImageFrameCount; /** Animation loop count, 0 means infinite looping. @return Animation loop count */ @property (nonatomic, assign, readonly) NSUInteger animatedImageLoopCount; /** Returns the frame image from a specified index. @note The index maybe randomly if one image was set to different imageViews, keep it re-entrant. (It's not recommend to store the images into array because it's memory consuming) @param index Frame index (zero based). @return Frame's image */ - (nullable UIImage *)animatedImageFrameAtIndex:(NSUInteger)index; /** Returns the frames's duration from a specified index. @note The index maybe randomly if one image was set to different imageViews, keep it re-entrant. (It's recommend to store the durations into array because it's not memory-consuming) @param index Frame index (zero based). @return Frame's duration */ - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index; @end #pragma mark - Animated Coder /** This is the animated image coder protocol for custom animated image class like `SDAnimatedImage`. Through it inherit from `SDImageCoder`. We currentlly only use the method `canDecodeFromData:` to detect the proper coder for specify animated image format. */ @protocol SDAnimatedImageCoder @required /** Because animated image coder should keep the original data, we will alloc a new instance with the same class for the specify animated image data The init method should return nil if it can't decode the specify animated image data to produce any frame. After the instance created, we may call methods in `SDAnimatedImageProvider` to produce animated image frame. @param data The animated image data to be decode @param options A dictionary containing any animated decoding options (instance-level). Pass @{SDImageCoderDecodeScaleFactor: @(1.0)} to specify scale factor for animated image (each frames should use the same scale). @return A new instance to do animated decoding for specify image data */ - (nullable instancetype)initWithAnimatedImageData:(nullable NSData *)data options:(nullable SDImageCoderOptions *)options; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCoder.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageCoder.h" SDImageCoderOption const SDImageCoderDecodeFirstFrameOnly = @"decodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderDecodeScaleFactor = @"decodeScaleFactor"; SDImageCoderOption const SDImageCoderDecodePreserveAspectRatio = @"decodePreserveAspectRatio"; SDImageCoderOption const SDImageCoderDecodeThumbnailPixelSize = @"decodeThumbnailPixelSize"; SDImageCoderOption const SDImageCoderDecodeFileExtensionHint = @"decodeFileExtensionHint"; SDImageCoderOption const SDImageCoderDecodeTypeIdentifierHint = @"decodeTypeIdentifierHint"; SDImageCoderOption const SDImageCoderDecodeUseLazyDecoding = @"decodeUseLazyDecoding"; SDImageCoderOption const SDImageCoderDecodeScaleDownLimitBytes = @"decodeScaleDownLimitBytes"; SDImageCoderOption const SDImageCoderDecodeToHDR = @"decodeToHDR"; SDImageCoderOption const SDImageCoderEncodeToHDR = @"encodeToHDR"; SDImageCoderOption const SDImageCoderEncodeFirstFrameOnly = @"encodeFirstFrameOnly"; SDImageCoderOption const SDImageCoderEncodeCompressionQuality = @"encodeCompressionQuality"; SDImageCoderOption const SDImageCoderEncodeBackgroundColor = @"encodeBackgroundColor"; SDImageCoderOption const SDImageCoderEncodeMaxPixelSize = @"encodeMaxPixelSize"; SDImageCoderOption const SDImageCoderEncodeMaxFileSize = @"encodeMaxFileSize"; SDImageCoderOption const SDImageCoderEncodeEmbedThumbnail = @"encodeEmbedThumbnail"; SDImageCoderOption const SDImageCoderWebImageContext = @"webImageContext"; ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" #import "SDImageFrame.h" /// The options controls how we force pre-draw the image (to avoid lazy-decoding). Which need OS's framework compatibility typedef NS_ENUM(NSUInteger, SDImageCoderDecodeSolution) { /// automatically choose the solution based on image format, hardware, OS version. This keep balance for compatibility and performance. Default after SDWebImage 5.13.0 SDImageCoderDecodeSolutionAutomatic, /// always use CoreGraphics to draw on bitmap context and trigger decode. Best compatibility. Default before SDWebImage 5.13.0 SDImageCoderDecodeSolutionCoreGraphics, /// available on iOS/tvOS 15+, use UIKit's new CGImageDecompressor/CMPhoto to decode. Best performance. If failed, will fallback to CoreGraphics as well SDImageCoderDecodeSolutionUIKit }; /// The policy to force-decode the origin CGImage (produced by Image Coder Plugin) /// Some CGImage may be lazy, or not lazy, but need extra copy to render on screen /// The force-decode step help to `pre-process` to get the best suitable CGImage to render, which can increase frame rate /// The downside is that force-decode may consume RAM and CPU, and may loss the `lazy` support (lazy CGImage can be purged when memory warning, and re-created if need), see more: `SDImageCoderDecodeUseLazyDecoding` typedef NS_ENUM(NSUInteger, SDImageForceDecodePolicy) { /// Based on input CGImage's colorspace, alignment, bitmapinfo, if it may trigger `CA::copy_image` extra copy, we will force-decode, else don't SDImageForceDecodePolicyAutomatic, /// Never force decode input CGImage SDImageForceDecodePolicyNever, /// Always force decode input CGImage (only once) SDImageForceDecodePolicyAlways }; /// These enum is used to represent the High Dynamic Range type during image encoding/decoding. /// There are alao other HDR type in history before ISO Standard (ISO 21496-1), including Google and Apple's old OSs captured photos, but which is non-standard and we do not support. typedef NS_ENUM(NSUInteger, SDImageHDRType) { /// SDR, mostly only 8 bits color per components, RGBA8 SDImageHDRTypeSDR = 0, /// ISO HDR (supported by modern format only, like HEIF/AVIF/JPEG-XL) SDImageHDRTypeISOHDR = 1, /// ISO Gain Map based HDR (supported by nearly all format, including tranditional JPEG, which stored the gain map into XMP) SDImageHDRTypeISOGainMap = 2, }; /// Byte alignment the bytes size with alignment /// - Parameters: /// - size: The bytes size /// - alignment: The alignment, in bytes static inline size_t SDByteAlign(size_t size, size_t alignment) { return ((size + (alignment - 1)) / alignment) * alignment; } /// The pixel format about the information to call `CGImageCreate` suitable for current hardware rendering typedef struct SDImagePixelFormat { /// Typically is pre-multiplied RGBA8888 for alpha image, RGBX8888 for non-alpha image. CGBitmapInfo bitmapInfo; /// Typically is 32, the 8 pixels bytesPerRow. size_t alignment; } SDImagePixelFormat; /** Provide some common helper methods for building the image decoder/encoder. */ @interface SDImageCoderHelper : NSObject /** Return an animated image with frames array. For UIKit, this will apply the patch and then create animated UIImage. The patch is because that `+[UIImage animatedImageWithImages:duration:]` just use the average of duration for each image. So it will not work if different frame has different duration. Therefore we repeat the specify frame for specify times to let it work. For AppKit, NSImage does not support animates other than GIF. This will try to encode the frames to GIF format and then create an animated NSImage for rendering. Attention the animated image may loss some detail if the input frames contain full alpha channel because GIF only supports 1 bit alpha channel. (For 1 pixel, either transparent or not) @param frames The frames array. If no frames or frames is empty, return nil @return A animated image for rendering on UIImageView(UIKit) or NSImageView(AppKit) */ + (UIImage * _Nullable)animatedImageWithFrames:(NSArray * _Nullable)frames; /** Return frames array from an animated image. For UIKit, this will unapply the patch for the description above and then create frames array. This will also work for normal animated UIImage. For AppKit, NSImage does not support animates other than GIF. This will try to decode the GIF imageRep and then create frames array. @param animatedImage A animated image. If it's not animated, return nil @return The frames array */ + (NSArray * _Nullable)framesFromAnimatedImage:(UIImage * _Nullable)animatedImage NS_SWIFT_NAME(frames(from:)); #pragma mark - Preferred Rendering Format /// For coders who use `CGImageCreate`, use the information below to create an effient CGImage which can be render on GPU without Core Animation's extra copy (`CA::Render::copy_image`), which can be debugged using `Color Copied Image` in Xcode Instruments /// `CGImageCreate`'s `bytesPerRow`, `space`, `bitmapInfo` params should use the information below. /** Return the shared device-dependent RGB color space. This follows The Get Rule. Because it's shared, you should not retain or release this object. Typically is sRGB for iOS, screen color space (like Color LCD) for macOS. @return The device-dependent RGB color space */ + (CGColorSpaceRef _Nonnull)colorSpaceGetDeviceRGB CF_RETURNS_NOT_RETAINED; /** Tthis returns the pixel format **Preferred from current hardward && OS using runtime detection** @param containsAlpha Whether the image to render contains alpha channel */ + (SDImagePixelFormat)preferredPixelFormat:(BOOL)containsAlpha; /** Check whether CGImage is hardware supported to rendering on screen, without the trigger of `CA::Render::copy_image` You can debug the copied image by using Xcode's `Color Copied Image`, the copied image will turn Cyan and occupy double RAM for bitmap buffer. Typically, when the CGImage's using the method above (`colorspace` / `alignment` / `bitmapInfo`) can render withtout the copy. */ + (BOOL)CGImageIsHardwareSupported:(_Nonnull CGImageRef)cgImage; /** Check whether CGImage contains alpha channel. @param cgImage The CGImage @return Return YES if CGImage contains alpha channel, otherwise return NO */ + (BOOL)CGImageContainsAlpha:(_Nonnull CGImageRef)cgImage; /** Detect whether the CGImage is lazy and not-yet decoded. (lazy means, only when the caller access the underlying bitmap buffer via provider like `CGDataProviderCopyData` or `CGDataProviderRetainBytePtr`, the decoder will allocate memory, it's a lazy allocation) The implementation use the Core Graphics internal to check whether the CGImage is `CGImageProvider` based, or `CGDataProvider` based. The `CGDataProvider` based is treated as non-lazy. */ + (BOOL)CGImageIsLazy:(_Nonnull CGImageRef)cgImage; /** Check if the CGImage is using HDR color space. @note This use the same implementation like Apple, to checkl if color space uses transfer functions defined in ITU Rec.2100 */ + (BOOL)CGImageIsHDR:(_Nonnull CGImageRef)cgImage; /** Create a decoded CGImage by the provided CGImage. This follows The Create Rule and you are response to call release after usage. It will detect whether image contains alpha channel, then create a new bitmap context with the same size of image, and draw it. This can ensure that the image do not need extra decoding after been set to the imageView. @note This actually call `CGImageCreateDecoded:orientation:` with the Up orientation. @param cgImage The CGImage @return A new created decoded image */ + (CGImageRef _Nullable)CGImageCreateDecoded:(_Nonnull CGImageRef)cgImage CF_RETURNS_RETAINED; /** Create a decoded CGImage by the provided CGImage and orientation. This follows The Create Rule and you are response to call release after usage. It will detect whether image contains alpha channel, then create a new bitmap context with the same size of image, and draw it. This can ensure that the image do not need extra decoding after been set to the imageView. @param cgImage The CGImage @param orientation The EXIF image orientation. @return A new created decoded image */ + (CGImageRef _Nullable)CGImageCreateDecoded:(_Nonnull CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation CF_RETURNS_RETAINED; /** Create a scaled CGImage by the provided CGImage and size. This follows The Create Rule and you are response to call release after usage. It will detect whether the image size matching the scale size, if not, stretch the image to the target size. @note If you need to keep aspect ratio, you can calculate the scale size by using `scaledSizeWithImageSize` first. @note This scale does not change bits per components (which means RGB888 in, RGB888 out), supports 8/16/32(float) bpc. But the method in UIImage+Transform does not gurantee this. @note All supported CGImage pixel format: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_context/dq_context.html#//apple_ref/doc/uid/TP30001066-CH203-BCIBHHBB @param cgImage The CGImage @param size The scale size in pixel. @return A new created scaled image */ + (CGImageRef _Nullable)CGImageCreateScaled:(_Nonnull CGImageRef)cgImage size:(CGSize)size CF_RETURNS_RETAINED; /** Scale the image size based on provided scale size, whether or not to preserve aspect ratio, whether or not to scale up. @note For example, if you implements thumnail decoding, pass `shouldScaleUp` to NO to avoid the calculated size larger than image size. @param imageSize The image size (in pixel or point defined by caller) @param scaleSize The scale size (in pixel or point defined by caller) @param preserveAspectRatio Whether or not to preserve aspect ratio @param shouldScaleUp Whether or not to scale up (or scale down only) */ + (CGSize)scaledSizeWithImageSize:(CGSize)imageSize scaleSize:(CGSize)scaleSize preserveAspectRatio:(BOOL)preserveAspectRatio shouldScaleUp:(BOOL)shouldScaleUp; /// Calculate the limited image size with the bytes, when using `SDImageCoderDecodeScaleDownLimitBytes`. This preserve aspect ratio and never scale up /// @param imageSize The image size (in pixel or point defined by caller) /// @param limitBytes The limit bytes /// @param bytesPerPixel The bytes per pixel /// @param frameCount The image frame count, 0 means 1 frame as well + (CGSize)scaledSizeWithImageSize:(CGSize)imageSize limitBytes:(NSUInteger)limitBytes bytesPerPixel:(NSUInteger)bytesPerPixel frameCount:(NSUInteger)frameCount; /** Return the decoded image by the provided image. This one unlike `CGImageCreateDecoded:`, will not decode the image which contains alpha channel or animated image. On iOS 15+, this may use `UIImage.preparingForDisplay()` to use CMPhoto for better performance than the old solution. @param image The image to be decoded @note This translate to `decodedImageWithImage:policy:` with automatic policy @return The decoded image */ + (UIImage * _Nullable)decodedImageWithImage:(UIImage * _Nullable)image; /** Return the decoded image by the provided image. This one unlike `CGImageCreateDecoded:`, will not decode the image which contains alpha channel or animated image. On iOS 15+, this may use `UIImage.preparingForDisplay()` to use CMPhoto for better performance than the old solution. @param image The image to be decoded @param policy The force decode policy to decode image, will effect the check whether input image need decode @return The decoded image */ + (UIImage * _Nullable)decodedImageWithImage:(UIImage * _Nullable)image policy:(SDImageForceDecodePolicy)policy; /** Return the decoded and probably scaled down image by the provided image. If the image pixels bytes size large than the limit bytes, will try to scale down. Or just works as `decodedImageWithImage:`, never scale up. @warning You should not pass too small bytes, the suggestion value should be larger than 1MB. Even we use Tile Decoding to avoid OOM, however, small bytes will consume much more CPU time because we need to iterate more times to draw each tile. @param image The image to be decoded and scaled down @param bytes The limit bytes size. Provide 0 to use the build-in limit. @note This translate to `decodedAndScaledDownImageWithImage:limitBytes:policy:` with automatic policy @return The decoded and probably scaled down image */ + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes; /** Return the decoded and probably scaled down image by the provided image. If the image pixels bytes size large than the limit bytes, will try to scale down. Or just works as `decodedImageWithImage:`, never scale up. @warning You should not pass too small bytes, the suggestion value should be larger than 1MB. Even we use Tile Decoding to avoid OOM, however, small bytes will consume much more CPU time because we need to iterate more times to draw each tile. @param image The image to be decoded and scaled down @param bytes The limit bytes size. Provide 0 to use the build-in limit. @param policy The force decode policy to decode image, will effect the check whether input image need decode @return The decoded and probably scaled down image */ + (UIImage * _Nullable)decodedAndScaledDownImageWithImage:(UIImage * _Nullable)image limitBytes:(NSUInteger)bytes policy:(SDImageForceDecodePolicy)policy; /** Control the default force decode solution. Available solutions in `SDImageCoderDecodeSolution`. @note Defaults to `SDImageCoderDecodeSolutionAutomatic`, which prefers to use UIKit for JPEG/HEIF, and fallback on CoreGraphics. If you want control on your hand, set the other solution. */ @property (class, readwrite) SDImageCoderDecodeSolution defaultDecodeSolution; /** Control the default limit bytes to scale down largest images. This value must be larger than 4 Bytes (at least 1x1 pixel). Defaults to 60MB on iOS/tvOS, 90MB on macOS, 30MB on watchOS. */ @property (class, readwrite) NSUInteger defaultScaleDownLimitBytes; #if SD_UIKIT || SD_WATCH /** Convert an EXIF image orientation to an iOS one. @param exifOrientation EXIF orientation @return iOS orientation */ + (UIImageOrientation)imageOrientationFromEXIFOrientation:(CGImagePropertyOrientation)exifOrientation NS_SWIFT_NAME(imageOrientation(from:)); /** Convert an iOS orientation to an EXIF image orientation. @param imageOrientation iOS orientation @return EXIF orientation */ + (CGImagePropertyOrientation)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation; #endif @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCoderHelper.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageCoderHelper.h" #import "SDImageFrame.h" #import "NSImage+Compatibility.h" #import "NSData+ImageContentType.h" #import "SDAnimatedImageRep.h" #import "UIImage+ForceDecode.h" #import "SDAssociatedObject.h" #import "UIImage+Metadata.h" #import "SDInternalMacros.h" #import "SDGraphicsImageRenderer.h" #import "SDInternalMacros.h" #import "SDDeviceHelper.h" #import "SDImageIOAnimatedCoderInternal.h" #import #define kCGColorSpaceDeviceRGB CFSTR("kCGColorSpaceDeviceRGB") #if SD_UIKIT static inline UIImage *SDImageDecodeUIKit(UIImage *image) { // See: https://developer.apple.com/documentation/uikit/uiimage/3750834-imagebypreparingfordisplay // Need CGImage-based if (@available(iOS 15, tvOS 15, *)) { UIImage *decodedImage = [image imageByPreparingForDisplay]; if (decodedImage) { SDImageCopyAssociatedObject(image, decodedImage); decodedImage.sd_isDecoded = YES; return decodedImage; } } return nil; } static inline UIImage *SDImageDecodeAndScaleDownUIKit(UIImage *image, CGSize destResolution) { // See: https://developer.apple.com/documentation/uikit/uiimage/3750835-imagebypreparingthumbnailofsize // Need CGImage-based if (@available(iOS 15, tvOS 15, *)) { // Calculate thumbnail point size CGFloat scale = image.scale ?: 1; CGSize thumbnailSize = CGSizeMake(destResolution.width / scale, destResolution.height / scale); UIImage *decodedImage = [image imageByPreparingThumbnailOfSize:thumbnailSize]; if (decodedImage) { SDImageCopyAssociatedObject(image, decodedImage); decodedImage.sd_isDecoded = YES; return decodedImage; } } return nil; } static inline BOOL SDImageSupportsHardwareHEVCDecoder(void) { static dispatch_once_t onceToken; static BOOL supportsHardware = NO; dispatch_once(&onceToken, ^{ SEL DeviceInfoSelector = SD_SEL_SPI(deviceInfoForKey:); NSString *HEVCDecoder8bitSupported = @"N8lZxRgC7lfdRS3dRLn+Ag"; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" if ([UIDevice.currentDevice respondsToSelector:DeviceInfoSelector] && [UIDevice.currentDevice performSelector:DeviceInfoSelector withObject:HEVCDecoder8bitSupported]) { supportsHardware = YES; } #pragma clang diagnostic pop }); return supportsHardware; } #endif static UIImage * _Nonnull SDImageGetAlphaDummyImage(void) { static dispatch_once_t onceToken; static UIImage *dummyImage; dispatch_once(&onceToken, ^{ SDGraphicsImageRendererFormat *format = [SDGraphicsImageRendererFormat preferredFormat]; format.scale = 1; format.opaque = NO; CGSize size = CGSizeMake(1, 1); SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; dummyImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) { CGContextSetFillColorWithColor(context, UIColor.redColor.CGColor); CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height)); }]; NSCAssert(dummyImage, @"The sample alpha image (1x1 pixels) returns nil, OS bug ?"); }); return dummyImage; } static UIImage * _Nonnull SDImageGetNonAlphaDummyImage(void) { static dispatch_once_t onceToken; static UIImage *dummyImage; dispatch_once(&onceToken, ^{ SDGraphicsImageRendererFormat *format = [SDGraphicsImageRendererFormat preferredFormat]; format.scale = 1; format.opaque = YES; CGSize size = CGSizeMake(1, 1); SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; dummyImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) { CGContextSetFillColorWithColor(context, UIColor.redColor.CGColor); CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height)); }]; NSCAssert(dummyImage, @"The sample non-alpha image (1x1 pixels) returns nil, OS bug ?"); }); return dummyImage; } static SDImageCoderDecodeSolution kDefaultDecodeSolution = SDImageCoderDecodeSolutionAutomatic; static const size_t kBytesPerPixel = 4; static const size_t kBitsPerComponent = 8; static const CGFloat kBytesPerMB = 1024.0f * 1024.0f; /* * Defines the maximum size in MB of the decoded image when the flag `SDWebImageScaleDownLargeImages` is set * Suggested value for iPad1 and iPhone 3GS: 60. * Suggested value for iPad2 and iPhone 4: 120. * Suggested value for iPhone 3G and iPod 2 and earlier devices: 30. */ #if SD_MAC static CGFloat kDestImageLimitBytes = 90.f * kBytesPerMB; #elif SD_UIKIT static CGFloat kDestImageLimitBytes = 60.f * kBytesPerMB; #elif SD_WATCH static CGFloat kDestImageLimitBytes = 30.f * kBytesPerMB; #endif static const CGFloat kDestSeemOverlap = 2.0f; // the numbers of pixels to overlap the seems where tiles meet. #if SD_MAC @interface SDAnimatedImageRep (Private) /// This wrap the animated image frames for legacy animated image coder API (`encodedDataWithImage:`). @property (nonatomic, readwrite, weak) NSArray *frames; @end #endif @implementation SDImageCoderHelper + (UIImage *)animatedImageWithFrames:(NSArray *)frames { NSUInteger frameCount = frames.count; if (frameCount == 0) { return nil; } UIImage *animatedImage; #if SD_UIKIT || SD_WATCH NSUInteger durations[frameCount]; for (size_t i = 0; i < frameCount; i++) { durations[i] = frames[i].duration * 1000; } NSUInteger const gcd = gcdArray(frameCount, durations); __block NSTimeInterval totalDuration = 0; NSMutableArray *animatedImages = [NSMutableArray arrayWithCapacity:frameCount]; [frames enumerateObjectsUsingBlock:^(SDImageFrame * _Nonnull frame, NSUInteger idx, BOOL * _Nonnull stop) { UIImage *image = frame.image; NSUInteger duration = frame.duration * 1000; totalDuration += frame.duration; NSUInteger repeatCount; if (gcd) { repeatCount = duration / gcd; } else { repeatCount = 1; } for (size_t i = 0; i < repeatCount; ++i) { [animatedImages addObject:image]; } }]; animatedImage = [UIImage animatedImageWithImages:animatedImages duration:totalDuration]; #else NSMutableData *imageData = [NSMutableData data]; CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:SDImageFormatGIF]; // Create an image destination. GIF does not support EXIF image orientation CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, frameCount, NULL); if (!imageDestination) { // Handle failure. return nil; } for (size_t i = 0; i < frameCount; i++) { SDImageFrame *frame = frames[i]; NSTimeInterval frameDuration = frame.duration; CGImageRef frameImageRef = frame.image.CGImage; NSDictionary *frameProperties = @{(__bridge NSString *)kCGImagePropertyGIFDictionary : @{(__bridge NSString *)kCGImagePropertyGIFDelayTime : @(frameDuration)}}; CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)frameProperties); } // Finalize the destination. if (CGImageDestinationFinalize(imageDestination) == NO) { // Handle failure. CFRelease(imageDestination); return nil; } CFRelease(imageDestination); CGFloat scale = MAX(frames.firstObject.image.scale, 1); SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:imageData]; NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale); imageRep.size = size; imageRep.frames = frames; // Weak assign to avoid effect lazy semantic of NSBitmapImageRep animatedImage = [[NSImage alloc] initWithSize:size]; [animatedImage addRepresentation:imageRep]; #endif return animatedImage; } + (NSArray *)framesFromAnimatedImage:(UIImage *)animatedImage { if (!animatedImage) { return nil; } NSMutableArray *frames; NSUInteger frameCount = 0; #if SD_UIKIT || SD_WATCH NSArray *animatedImages = animatedImage.images; frameCount = animatedImages.count; if (frameCount == 0) { return nil; } frames = [NSMutableArray arrayWithCapacity:frameCount]; NSTimeInterval avgDuration = animatedImage.duration / frameCount; if (avgDuration == 0) { avgDuration = 0.1; // if it's a animated image but no duration, set it to default 100ms (this do not have that 10ms limit like GIF or WebP to allow custom coder provide the limit) } __block NSUInteger repeatCount = 1; __block UIImage *previousImage = animatedImages.firstObject; [animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) { // ignore first if (idx == 0) { return; } if ([image isEqual:previousImage]) { repeatCount++; } else { SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount]; [frames addObject:frame]; repeatCount = 1; } previousImage = image; }]; // last one SDImageFrame *frame = [SDImageFrame frameWithImage:previousImage duration:avgDuration * repeatCount]; [frames addObject:frame]; #else NSRect imageRect = NSMakeRect(0, 0, animatedImage.size.width, animatedImage.size.height); NSImageRep *imageRep = [animatedImage bestRepresentationForRect:imageRect context:nil hints:nil]; // Check weak assigned frames firstly if ([imageRep isKindOfClass:[SDAnimatedImageRep class]]) { SDAnimatedImageRep *animatedImageRep = (SDAnimatedImageRep *)imageRep; if (animatedImageRep.frames) { return animatedImageRep.frames; } } NSBitmapImageRep *bitmapImageRep; if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { bitmapImageRep = (NSBitmapImageRep *)imageRep; } if (!bitmapImageRep) { return nil; } frameCount = [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue]; if (frameCount == 0) { return nil; } frames = [NSMutableArray arrayWithCapacity:frameCount]; CGFloat scale = animatedImage.scale; for (size_t i = 0; i < frameCount; i++) { // NSBitmapImageRep need to manually change frame. "Good taste" API [bitmapImageRep setProperty:NSImageCurrentFrame withValue:@(i)]; NSTimeInterval frameDuration = [[bitmapImageRep valueForProperty:NSImageCurrentFrameDuration] doubleValue]; NSImage *frameImage = [[NSImage alloc] initWithCGImage:bitmapImageRep.CGImage scale:scale orientation:kCGImagePropertyOrientationUp]; SDImageFrame *frame = [SDImageFrame frameWithImage:frameImage duration:frameDuration]; [frames addObject:frame]; } #endif return [frames copy]; } + (CGColorSpaceRef)colorSpaceGetDeviceRGB { #if SD_MAC NSScreen *mainScreen = nil; if (@available(macOS 10.12, *)) { mainScreen = [NSScreen mainScreen]; } else { mainScreen = [NSScreen screens].firstObject; } CGColorSpaceRef colorSpace = mainScreen.colorSpace.CGColorSpace; return colorSpace; #else static CGColorSpaceRef colorSpace; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceSRGB); }); return colorSpace; #endif } + (SDImagePixelFormat)preferredPixelFormat:(BOOL)containsAlpha { CGImageRef cgImage; if (containsAlpha) { cgImage = SDImageGetAlphaDummyImage().CGImage; } else { cgImage = SDImageGetNonAlphaDummyImage().CGImage; } CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage); size_t bitsPerComponent = 8; if (SD_OPTIONS_CONTAINS(bitmapInfo, kCGBitmapFloatComponents)) { bitsPerComponent = 16; } size_t components = 4; // Hardcode now // https://github.com/path/FastImageCache#byte-alignment // A properly aligned bytes-per-row value must be a multiple of 8 pixels × bytes per pixel. size_t alignment = (bitsPerComponent / 8) * components * 8; SDImagePixelFormat pixelFormat = { .bitmapInfo = bitmapInfo, .alignment = alignment }; return pixelFormat; } + (BOOL)CGImageIsHardwareSupported:(CGImageRef)cgImage { BOOL supported = YES; // 1. Check byte alignment size_t bytesPerRow = CGImageGetBytesPerRow(cgImage); BOOL hasAlpha = [self CGImageContainsAlpha:cgImage]; SDImagePixelFormat pixelFormat = [self preferredPixelFormat:hasAlpha]; if (SDByteAlign(bytesPerRow, pixelFormat.alignment) == bytesPerRow) { // byte aligned, OK supported &= YES; } else { // not aligned supported &= NO; } if (!supported) return supported; // 2. Check color space CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage); CGColorSpaceRef perferredColorSpace = [self colorSpaceGetDeviceRGB]; if (colorSpace == perferredColorSpace) { return supported; } else { if (@available(iOS 10.0, tvOS 10.0, macOS 10.6, watchOS 3.0, *)) { NSString *colorspaceName = (__bridge_transfer NSString *)CGColorSpaceCopyName(colorSpace); // Seems sRGB/deviceRGB always supported, P3 not always if ([colorspaceName isEqualToString:(__bridge NSString *)kCGColorSpaceDeviceRGB] || [colorspaceName isEqualToString:(__bridge NSString *)kCGColorSpaceSRGB]) { supported &= YES; } else { supported &= NO; } return supported; } else { // Fallback on earlier versions return supported; } } } + (BOOL)CGImageContainsAlpha:(CGImageRef)cgImage { if (!cgImage) { return NO; } CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage); BOOL hasAlpha = !(alphaInfo == kCGImageAlphaNone || alphaInfo == kCGImageAlphaNoneSkipFirst || alphaInfo == kCGImageAlphaNoneSkipLast); return hasAlpha; } + (BOOL)CGImageIsLazy:(CGImageRef)cgImage { if (!cgImage) { return NO; } // CoreGraphics use CGImage's C struct filed (offset 0xd8 on iOS 17.0) // But since the description of `CGImageRef` always contains the `[DP]` (DataProvider) and `[IP]` (ImageProvider), we can use this as a hint NSString *description = (__bridge_transfer NSString *)CFCopyDescription(cgImage); if (description) { // Solution 1: Parse the description to get provider // (IP) -> YES // (DP) -> NO NSArray *lines = [description componentsSeparatedByString:@"\n"]; if (lines.count > 0) { NSString *firstLine = lines[0]; NSRange startRange = [firstLine rangeOfString:@"("]; NSRange endRange = [firstLine rangeOfString:@")"]; if (startRange.location != NSNotFound && endRange.location != NSNotFound) { NSRange resultRange = NSMakeRange(startRange.location + 1, endRange.location - startRange.location - 1); NSString *providerString = [firstLine substringWithRange:resultRange]; if ([providerString isEqualToString:@"IP"]) { return YES; } else if ([providerString isEqualToString:@"DP"]) { return NO; } else { // New cases ? fallback } } } } // Solution 2: Use UTI metadata CFStringRef uttype = CGImageGetUTType(cgImage); if (uttype) { // Only ImageIO can set `com.apple.ImageIO.imageSourceTypeIdentifier` metadata for lazy decoded CGImage return YES; } else { return NO; } } + (BOOL)CGImageIsHDR:(_Nonnull CGImageRef)cgImage { if (!cgImage) { return NO; } if (@available(macOS 11.0, iOS 14, tvOS 14, watchOS 7.0, *)) { CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage); if (colorSpace) { // Actually `CGColorSpaceIsHDR` use the same impl, but deprecated return CGColorSpaceUsesITUR_2100TF(colorSpace); } } return NO; } + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage { return [self CGImageCreateDecoded:cgImage orientation:kCGImagePropertyOrientationUp]; } + (CGImageRef)CGImageCreateDecoded:(CGImageRef)cgImage orientation:(CGImagePropertyOrientation)orientation { if (!cgImage) { return NULL; } size_t width = CGImageGetWidth(cgImage); size_t height = CGImageGetHeight(cgImage); if (width == 0 || height == 0) return NULL; size_t newWidth; size_t newHeight; switch (orientation) { case kCGImagePropertyOrientationLeft: case kCGImagePropertyOrientationLeftMirrored: case kCGImagePropertyOrientationRight: case kCGImagePropertyOrientationRightMirrored: { // These orientation should swap width & height newWidth = height; newHeight = width; } break; default: { newWidth = width; newHeight = height; } break; } BOOL hasAlpha = [self CGImageContainsAlpha:cgImage]; // kCGImageAlphaNone is not supported in CGBitmapContextCreate. // Check #3330 for more detail about why this bitmap is choosen. // From v5.17.0, use runtime detection of bitmap info instead of hardcode. CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo; CGContextRef context = CGBitmapContextCreate(NULL, newWidth, newHeight, 8, 0, [self colorSpaceGetDeviceRGB], bitmapInfo); if (!context) { return NULL; } // Apply transform CGAffineTransform transform = SDCGContextTransformFromOrientation(orientation, CGSizeMake(newWidth, newHeight)); CGContextConcatCTM(context, transform); CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage); // The rect is bounding box of CGImage, don't swap width & height CGImageRef newImageRef = CGBitmapContextCreateImage(context); CGContextRelease(context); return newImageRef; } + (CGImageRef)CGImageCreateScaled:(CGImageRef)cgImage size:(CGSize)size { if (!cgImage) { return NULL; } if (size.width == 0 || size.height == 0) { return NULL; } size_t width = CGImageGetWidth(cgImage); size_t height = CGImageGetHeight(cgImage); if (width == size.width && height == size.height) { // Already same size CGImageRetain(cgImage); return cgImage; } size_t bitsPerComponent = CGImageGetBitsPerComponent(cgImage); if (bitsPerComponent != 8 && bitsPerComponent != 16 && bitsPerComponent != 32) { // Unsupported return NULL; } size_t bitsPerPixel = CGImageGetBitsPerPixel(cgImage); CGColorSpaceRef colorSpace = CGImageGetColorSpace(cgImage); CGColorRenderingIntent renderingIntent = CGImageGetRenderingIntent(cgImage); CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage); CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; CGImageByteOrderInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; CGBitmapInfo alphaBitmapInfo = (uint32_t)byteOrderInfo; // Input need to convert with alpha if (alphaInfo == kCGImageAlphaNone) { // Convert RGB8/16/F -> ARGB8/16/F alphaBitmapInfo |= kCGImageAlphaFirst; } else { alphaBitmapInfo |= alphaInfo; } uint32_t components; if (alphaInfo == kCGImageAlphaOnly) { // Alpha only, simple to 1 channel components = 1; } else { components = 4; } if (SD_OPTIONS_CONTAINS(bitmapInfo, kCGBitmapFloatComponents)) { // Keep float components alphaBitmapInfo |= kCGBitmapFloatComponents; } __block vImage_Buffer input_buffer = {}, output_buffer = {}; @onExit { if (input_buffer.data) free(input_buffer.data); if (output_buffer.data) free(output_buffer.data); }; // Always provide alpha channel vImage_CGImageFormat format = (vImage_CGImageFormat) { .bitsPerComponent = (uint32_t)bitsPerComponent, .bitsPerPixel = (uint32_t)bitsPerComponent * components, .colorSpace = colorSpace, .bitmapInfo = alphaBitmapInfo, .version = 0, .decode = NULL, .renderingIntent = renderingIntent }; // input vImage_Error ret = vImageBuffer_InitWithCGImage(&input_buffer, &format, NULL, cgImage, kvImageNoFlags); if (ret != kvImageNoError) return NULL; // output vImageBuffer_Init(&output_buffer, size.height, size.width, (uint32_t)bitsPerComponent * components, kvImageNoFlags); if (!output_buffer.data) return NULL; if (components == 4) { if (bitsPerComponent == 32) { ret = vImageScale_ARGBFFFF(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); } else if (bitsPerComponent == 16) { ret = vImageScale_ARGB16U(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); } else if (bitsPerComponent == 8) { ret = vImageScale_ARGB8888(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); } } else { if (bitsPerComponent == 32) { ret = vImageScale_PlanarF(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); } else if (bitsPerComponent == 16) { ret = vImageScale_Planar16U(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); } else if (bitsPerComponent == 8) { ret = vImageScale_Planar8(&input_buffer, &output_buffer, NULL, kvImageHighQualityResampling); } } if (ret != kvImageNoError) return NULL; // Convert back to non-alpha for RGB input to preserve pixel format if (alphaInfo == kCGImageAlphaNone) { // in-place, no extra allocation if (bitsPerComponent == 32) { ret = vImageConvert_ARGBFFFFtoRGBFFF(&output_buffer, &output_buffer, kvImageNoFlags); } else if (bitsPerComponent == 16) { ret = vImageConvert_ARGB16UtoRGB16U(&output_buffer, &output_buffer, kvImageNoFlags); } else if (bitsPerComponent == 8) { ret = vImageConvert_ARGB8888toRGB888(&output_buffer, &output_buffer, kvImageNoFlags); } if (ret != kvImageNoError) return NULL; } vImage_CGImageFormat output_format = (vImage_CGImageFormat) { .bitsPerComponent = (uint32_t)bitsPerComponent, .bitsPerPixel = (uint32_t)bitsPerPixel, .colorSpace = colorSpace, .bitmapInfo = bitmapInfo, .version = 0, .decode = NULL, .renderingIntent = renderingIntent }; CGImageRef outputImage = vImageCreateCGImageFromBuffer(&output_buffer, &output_format, NULL, NULL, kvImageNoFlags, &ret); if (ret != kvImageNoError) { CGImageRelease(outputImage); return NULL; } return outputImage; } + (CGSize)scaledSizeWithImageSize:(CGSize)imageSize scaleSize:(CGSize)scaleSize preserveAspectRatio:(BOOL)preserveAspectRatio shouldScaleUp:(BOOL)shouldScaleUp { CGFloat width = imageSize.width; CGFloat height = imageSize.height; CGFloat resultWidth; CGFloat resultHeight; if (width <= 0 || height <= 0 || scaleSize.width <= 0 || scaleSize.height <= 0) { // Protect resultWidth = width; resultHeight = height; } else { // Scale to fit if (preserveAspectRatio) { CGFloat pixelRatio = width / height; CGFloat scaleRatio = scaleSize.width / scaleSize.height; if (pixelRatio > scaleRatio) { resultWidth = scaleSize.width; resultHeight = ceil(scaleSize.width / pixelRatio); } else { resultHeight = scaleSize.height; resultWidth = ceil(scaleSize.height * pixelRatio); } } else { // Stretch resultWidth = scaleSize.width; resultHeight = scaleSize.height; } if (!shouldScaleUp) { // Scale down only resultWidth = MIN(width, resultWidth); resultHeight = MIN(height, resultHeight); } } return CGSizeMake(resultWidth, resultHeight); } + (CGSize)scaledSizeWithImageSize:(CGSize)imageSize limitBytes:(NSUInteger)limitBytes bytesPerPixel:(NSUInteger)bytesPerPixel frameCount:(NSUInteger)frameCount { if (CGSizeEqualToSize(imageSize, CGSizeZero)) return CGSizeMake(1, 1); NSUInteger totalFramePixelSize = limitBytes / bytesPerPixel / (frameCount ?: 1); CGFloat ratio = imageSize.height / imageSize.width; CGFloat width = sqrt(totalFramePixelSize / ratio); CGFloat height = width * ratio; width = MAX(1, floor(width)); height = MAX(1, floor(height)); CGSize size = CGSizeMake(width, height); return size; } + (UIImage *)decodedImageWithImage:(UIImage *)image { return [self decodedImageWithImage:image policy:SDImageForceDecodePolicyAutomatic]; } + (UIImage *)decodedImageWithImage:(UIImage *)image policy:(SDImageForceDecodePolicy)policy { if (![self shouldDecodeImage:image policy:policy]) { return image; } UIImage *decodedImage; SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution; #if SD_UIKIT if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) { // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :( SDImageFormat format = image.sd_imageFormat; if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) { decodedImage = SDImageDecodeUIKit(image); } else if (format == SDImageFormatJPEG) { decodedImage = SDImageDecodeUIKit(image); } } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) { // Arbitrarily call CMPhoto decodedImage = SDImageDecodeUIKit(image); } if (decodedImage) { return decodedImage; } #endif CGImageRef imageRef = image.CGImage; if (!imageRef) { // Only decode for CGImage-based return image; } if (decodeSolution == SDImageCoderDecodeSolutionCoreGraphics) { CGImageRef decodedImageRef = [self CGImageCreateDecoded:imageRef]; #if SD_MAC decodedImage = [[UIImage alloc] initWithCGImage:decodedImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp]; #else decodedImage = [[UIImage alloc] initWithCGImage:decodedImageRef scale:image.scale orientation:image.imageOrientation]; #endif CGImageRelease(decodedImageRef); } else { // Prefer to use new Image Renderer to re-draw image, instead of low-level CGBitmapContext and CGContextDrawImage // This can keep both OS compatible and don't fight with Apple's performance optimization SDGraphicsImageRendererFormat *format = SDGraphicsImageRendererFormat.preferredFormat; // To support most OS compatible like Dynamic Range, prefer the image level format #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.0, *)) { format.uiformat = image.imageRendererFormat; } else { #endif format.opaque = ![self CGImageContainsAlpha:imageRef];; format.scale = image.scale; #if SD_UIKIT } #endif CGSize imageSize = image.size; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:imageSize format:format]; decodedImage = [renderer imageWithActions:^(CGContextRef _Nonnull context) { [image drawInRect:CGRectMake(0, 0, imageSize.width, imageSize.height)]; }]; } SDImageCopyAssociatedObject(image, decodedImage); decodedImage.sd_isDecoded = YES; return decodedImage; } + (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes { return [self decodedAndScaledDownImageWithImage:image limitBytes:bytes policy:SDImageForceDecodePolicyAutomatic]; } + (UIImage *)decodedAndScaledDownImageWithImage:(UIImage *)image limitBytes:(NSUInteger)bytes policy:(SDImageForceDecodePolicy)policy { if (![self shouldDecodeImage:image policy:policy]) { return image; } CGFloat destTotalPixels; CGFloat tileTotalPixels; if (bytes == 0) { bytes = [self defaultScaleDownLimitBytes]; } bytes = MAX(bytes, kBytesPerPixel); destTotalPixels = bytes / kBytesPerPixel; tileTotalPixels = destTotalPixels / 3; CGImageRef sourceImageRef = image.CGImage; if (!sourceImageRef) { // Only decode for CGImage-based return image; } CGSize sourceResolution = CGSizeZero; sourceResolution.width = CGImageGetWidth(sourceImageRef); sourceResolution.height = CGImageGetHeight(sourceImageRef); if (![self shouldScaleDownImagePixelSize:sourceResolution limitBytes:bytes]) { return [self decodedImageWithImage:image]; } CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height; // Determine the scale ratio to apply to the input image // that results in an output image of the defined size. // see kDestImageSizeMB, and how it relates to destTotalPixels. CGFloat imageScale = sqrt(destTotalPixels / sourceTotalPixels); CGSize destResolution = CGSizeZero; destResolution.width = MAX(1, (int)(sourceResolution.width * imageScale)); destResolution.height = MAX(1, (int)(sourceResolution.height * imageScale)); UIImage *decodedImage; #if SD_UIKIT SDImageCoderDecodeSolution decodeSolution = self.defaultDecodeSolution; if (decodeSolution == SDImageCoderDecodeSolutionAutomatic) { // See #3365, CMPhoto iOS 15 only supports JPEG/HEIF format, or it will print an error log :( SDImageFormat format = image.sd_imageFormat; if ((format == SDImageFormatHEIC || format == SDImageFormatHEIF) && SDImageSupportsHardwareHEVCDecoder()) { decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution); } else if (format == SDImageFormatJPEG) { decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution); } } else if (decodeSolution == SDImageCoderDecodeSolutionUIKit) { // Arbitrarily call CMPhoto decodedImage = SDImageDecodeAndScaleDownUIKit(image, destResolution); } if (decodedImage) { return decodedImage; } #endif // autorelease the bitmap context and all vars to help system to free memory when there are memory warning. // on iOS7, do not forget to call [[SDImageCache sharedImageCache] clearMemory]; @autoreleasepool { // device color space CGColorSpaceRef colorspaceRef = [self colorSpaceGetDeviceRGB]; BOOL hasAlpha = [self CGImageContainsAlpha:sourceImageRef]; // kCGImageAlphaNone is not supported in CGBitmapContextCreate. // Check #3330 for more detail about why this bitmap is choosen. // From v5.17.0, use runtime detection of bitmap info instead of hardcode. CGBitmapInfo bitmapInfo = [SDImageCoderHelper preferredPixelFormat:hasAlpha].bitmapInfo; CGContextRef destContext = CGBitmapContextCreate(NULL, destResolution.width, destResolution.height, kBitsPerComponent, 0, colorspaceRef, bitmapInfo); if (destContext == NULL) { return image; } CGContextSetInterpolationQuality(destContext, kCGInterpolationHigh); // Now define the size of the rectangle to be used for the // incremental bits from the input image to the output image. // we use a source tile width equal to the width of the source // image due to the way that iOS retrieves image data from disk. // iOS must decode an image from disk in full width 'bands', even // if current graphics context is clipped to a subrect within that // band. Therefore we fully utilize all of the pixel data that results // from a decoding operation by anchoring our tile size to the full // width of the input image. CGRect sourceTile = CGRectZero; sourceTile.size.width = sourceResolution.width; // The source tile height is dynamic. Since we specified the size // of the source tile in MB, see how many rows of pixels high it // can be given the input image width. sourceTile.size.height = MAX(1, (int)(tileTotalPixels / sourceTile.size.width)); sourceTile.origin.x = 0.0f; // The output tile is the same proportions as the input tile, but // scaled to image scale. CGRect destTile; destTile.size.width = destResolution.width; destTile.size.height = sourceTile.size.height * imageScale; destTile.origin.x = 0.0f; // The source seem overlap is proportionate to the destination seem overlap. // this is the amount of pixels to overlap each tile as we assemble the output image. float sourceSeemOverlap = (int)((kDestSeemOverlap/destResolution.height)*sourceResolution.height); CGImageRef sourceTileImageRef; // calculate the number of read/write operations required to assemble the // output image. int iterations = (int)( sourceResolution.height / sourceTile.size.height ); // If tile height doesn't divide the image height evenly, add another iteration // to account for the remaining pixels. int remainder = (int)sourceResolution.height % (int)sourceTile.size.height; if(remainder) { iterations++; } // Add seem overlaps to the tiles, but save the original tile height for y coordinate calculations. float sourceTileHeightMinusOverlap = sourceTile.size.height; sourceTile.size.height += sourceSeemOverlap; destTile.size.height += kDestSeemOverlap; for( int y = 0; y < iterations; ++y ) { sourceTile.origin.y = y * sourceTileHeightMinusOverlap + sourceSeemOverlap; destTile.origin.y = destResolution.height - (( y + 1 ) * sourceTileHeightMinusOverlap * imageScale + kDestSeemOverlap); sourceTileImageRef = CGImageCreateWithImageInRect( sourceImageRef, sourceTile ); if( y == iterations - 1 && remainder ) { float dify = destTile.size.height; destTile.size.height = CGImageGetHeight( sourceTileImageRef ) * imageScale + kDestSeemOverlap; dify -= destTile.size.height; destTile.origin.y = MIN(0, destTile.origin.y + dify); } CGContextDrawImage( destContext, destTile, sourceTileImageRef ); CGImageRelease( sourceTileImageRef ); } CGImageRef destImageRef = CGBitmapContextCreateImage(destContext); CGContextRelease(destContext); if (destImageRef == NULL) { return image; } #if SD_MAC decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:kCGImagePropertyOrientationUp]; #else decodedImage = [[UIImage alloc] initWithCGImage:destImageRef scale:image.scale orientation:image.imageOrientation]; #endif CGImageRelease(destImageRef); SDImageCopyAssociatedObject(image, decodedImage); decodedImage.sd_isDecoded = YES; return decodedImage; } } + (SDImageCoderDecodeSolution)defaultDecodeSolution { return kDefaultDecodeSolution; } + (void)setDefaultDecodeSolution:(SDImageCoderDecodeSolution)defaultDecodeSolution { kDefaultDecodeSolution = defaultDecodeSolution; } + (NSUInteger)defaultScaleDownLimitBytes { return kDestImageLimitBytes; } + (void)setDefaultScaleDownLimitBytes:(NSUInteger)defaultScaleDownLimitBytes { if (defaultScaleDownLimitBytes < kBytesPerPixel) { return; } kDestImageLimitBytes = defaultScaleDownLimitBytes; } #if SD_UIKIT || SD_WATCH // Convert an EXIF image orientation to an iOS one. + (UIImageOrientation)imageOrientationFromEXIFOrientation:(CGImagePropertyOrientation)exifOrientation { UIImageOrientation imageOrientation = UIImageOrientationUp; switch (exifOrientation) { case kCGImagePropertyOrientationUp: imageOrientation = UIImageOrientationUp; break; case kCGImagePropertyOrientationDown: imageOrientation = UIImageOrientationDown; break; case kCGImagePropertyOrientationLeft: imageOrientation = UIImageOrientationLeft; break; case kCGImagePropertyOrientationRight: imageOrientation = UIImageOrientationRight; break; case kCGImagePropertyOrientationUpMirrored: imageOrientation = UIImageOrientationUpMirrored; break; case kCGImagePropertyOrientationDownMirrored: imageOrientation = UIImageOrientationDownMirrored; break; case kCGImagePropertyOrientationLeftMirrored: imageOrientation = UIImageOrientationLeftMirrored; break; case kCGImagePropertyOrientationRightMirrored: imageOrientation = UIImageOrientationRightMirrored; break; default: break; } return imageOrientation; } // Convert an iOS orientation to an EXIF image orientation. + (CGImagePropertyOrientation)exifOrientationFromImageOrientation:(UIImageOrientation)imageOrientation { CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp; switch (imageOrientation) { case UIImageOrientationUp: exifOrientation = kCGImagePropertyOrientationUp; break; case UIImageOrientationDown: exifOrientation = kCGImagePropertyOrientationDown; break; case UIImageOrientationLeft: exifOrientation = kCGImagePropertyOrientationLeft; break; case UIImageOrientationRight: exifOrientation = kCGImagePropertyOrientationRight; break; case UIImageOrientationUpMirrored: exifOrientation = kCGImagePropertyOrientationUpMirrored; break; case UIImageOrientationDownMirrored: exifOrientation = kCGImagePropertyOrientationDownMirrored; break; case UIImageOrientationLeftMirrored: exifOrientation = kCGImagePropertyOrientationLeftMirrored; break; case UIImageOrientationRightMirrored: exifOrientation = kCGImagePropertyOrientationRightMirrored; break; default: break; } return exifOrientation; } #endif #pragma mark - Helper Function + (BOOL)shouldDecodeImage:(nullable UIImage *)image policy:(SDImageForceDecodePolicy)policy { // Prevent "CGBitmapContextCreateImage: invalid context 0x0" error if (image == nil) { return NO; } // Check policy (never) if (policy == SDImageForceDecodePolicyNever) { return NO; } // Avoid extra decode if (image.sd_isDecoded) { return NO; } // do not decode animated images if (image.sd_isAnimated) { return NO; } // do not decode vector images if (image.sd_isVector) { return NO; } // FIXME: currently our force decode solution is buggy on HDR CGImage if (image.sd_isHighDynamicRange) { return NO; } // Check policy (always) if (policy == SDImageForceDecodePolicyAlways) { return YES; } else { // Check policy (automatic) CGImageRef cgImage = image.CGImage; if (cgImage) { // Check if it's lazy CGImage wrapper or not BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:cgImage]; if (isLazy) { // Lazy CGImage should trigger force decode before rendering return YES; } else { // Now, let's check if this non-lazy CGImage is hardware supported (not byte-aligned will cause extra copy) BOOL isSupported = [SDImageCoderHelper CGImageIsHardwareSupported:cgImage]; return !isSupported; } } } return YES; } + (BOOL)shouldScaleDownImagePixelSize:(CGSize)sourceResolution limitBytes:(NSUInteger)bytes { BOOL shouldScaleDown = YES; CGFloat sourceTotalPixels = sourceResolution.width * sourceResolution.height; if (sourceTotalPixels <= 0) { return NO; } CGFloat destTotalPixels; if (bytes == 0) { bytes = [self defaultScaleDownLimitBytes]; } bytes = MAX(bytes, kBytesPerPixel); destTotalPixels = bytes / kBytesPerPixel; CGFloat imageScale = destTotalPixels / sourceTotalPixels; if (imageScale < 1) { shouldScaleDown = YES; } else { shouldScaleDown = NO; } return shouldScaleDown; } static inline CGAffineTransform SDCGContextTransformFromOrientation(CGImagePropertyOrientation orientation, CGSize size) { // Inspiration from @libfeihu // We need to calculate the proper transformation to make the image upright. // We do it in 2 steps: Rotate if Left/Right/Down, and then flip if Mirrored. CGAffineTransform transform = CGAffineTransformIdentity; switch (orientation) { case kCGImagePropertyOrientationDown: case kCGImagePropertyOrientationDownMirrored: transform = CGAffineTransformTranslate(transform, size.width, size.height); transform = CGAffineTransformRotate(transform, M_PI); break; case kCGImagePropertyOrientationLeft: case kCGImagePropertyOrientationLeftMirrored: transform = CGAffineTransformTranslate(transform, size.width, 0); transform = CGAffineTransformRotate(transform, M_PI_2); break; case kCGImagePropertyOrientationRight: case kCGImagePropertyOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, 0, size.height); transform = CGAffineTransformRotate(transform, -M_PI_2); break; case kCGImagePropertyOrientationUp: case kCGImagePropertyOrientationUpMirrored: break; } switch (orientation) { case kCGImagePropertyOrientationUpMirrored: case kCGImagePropertyOrientationDownMirrored: transform = CGAffineTransformTranslate(transform, size.width, 0); transform = CGAffineTransformScale(transform, -1, 1); break; case kCGImagePropertyOrientationLeftMirrored: case kCGImagePropertyOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, size.height, 0); transform = CGAffineTransformScale(transform, -1, 1); break; case kCGImagePropertyOrientationUp: case kCGImagePropertyOrientationDown: case kCGImagePropertyOrientationLeft: case kCGImagePropertyOrientationRight: break; } return transform; } #if SD_UIKIT || SD_WATCH static NSUInteger gcd(NSUInteger a, NSUInteger b) { NSUInteger c; while (a != 0) { c = a; a = b % a; b = c; } return b; } static NSUInteger gcdArray(size_t const count, NSUInteger const * const values) { if (count == 0) { return 0; } NSUInteger result = values[0]; for (size_t i = 1; i < count; ++i) { result = gcd(values[i], result); } return result; } #endif @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCodersManager.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDImageCoder.h" /** Global object holding the array of coders, so that we avoid passing them from object to object. Uses a priority queue behind scenes, which means the latest added coders have the highest priority. This is done so when encoding/decoding something, we go through the list and ask each coder if they can handle the current data. That way, users can add their custom coders while preserving our existing prebuilt ones Note: the `coders` getter will return the coders in their reversed order Example: - by default we internally set coders = `IOCoder`, `GIFCoder`, `APNGCoder` - calling `coders` will return `@[IOCoder, GIFCoder, APNGCoder]` - call `[addCoder:[MyCrazyCoder new]]` - calling `coders` now returns `@[IOCoder, GIFCoder, APNGCoder, MyCrazyCoder]` Coders ------ A coder must conform to the `SDImageCoder` protocol or even to `SDProgressiveImageCoder` if it supports progressive decoding Conformance is important because that way, they will implement `canDecodeFromData` or `canEncodeToFormat` Those methods are called on each coder in the array (using the priority order) until one of them returns YES. That means that coder can decode that data / encode to that format */ @interface SDImageCodersManager : NSObject /** Returns the global shared coders manager instance. */ @property (nonatomic, class, readonly, nonnull) SDImageCodersManager *sharedManager; /** All coders in coders manager. The coders array is a priority queue, which means the later added coder will have the highest priority */ @property (nonatomic, copy, nullable) NSArray> *coders; /** Add a new coder to the end of coders array. Which has the highest priority. @param coder coder */ - (void)addCoder:(nonnull id)coder; /** Remove a coder in the coders array. @param coder coder */ - (void)removeCoder:(nonnull id)coder; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageCodersManager.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageCodersManager.h" #import "SDImageIOCoder.h" #import "SDImageGIFCoder.h" #import "SDImageAPNGCoder.h" #import "SDImageHEICCoder.h" #import "SDInternalMacros.h" @interface SDImageCodersManager () @property (nonatomic, strong, nonnull) NSMutableArray> *imageCoders; @end @implementation SDImageCodersManager { SD_LOCK_DECLARE(_codersLock); } + (nonnull instancetype)sharedManager { static dispatch_once_t once; static id instance; dispatch_once(&once, ^{ instance = [self new]; }); return instance; } - (instancetype)init { if (self = [super init]) { // initialize with default coders _imageCoders = [NSMutableArray arrayWithArray:@[[SDImageIOCoder sharedCoder], [SDImageGIFCoder sharedCoder], [SDImageAPNGCoder sharedCoder]]]; SD_LOCK_INIT(_codersLock); } return self; } - (NSArray> *)coders { SD_LOCK(_codersLock); NSArray> *coders = [_imageCoders copy]; SD_UNLOCK(_codersLock); return coders; } - (void)setCoders:(NSArray> *)coders { SD_LOCK(_codersLock); [_imageCoders removeAllObjects]; if (coders.count) { [_imageCoders addObjectsFromArray:coders]; } SD_UNLOCK(_codersLock); } #pragma mark - Coder IO operations - (void)addCoder:(nonnull id)coder { if (![coder conformsToProtocol:@protocol(SDImageCoder)]) { return; } SD_LOCK(_codersLock); [_imageCoders addObject:coder]; SD_UNLOCK(_codersLock); } - (void)removeCoder:(nonnull id)coder { if (![coder conformsToProtocol:@protocol(SDImageCoder)]) { return; } SD_LOCK(_codersLock); [_imageCoders removeObject:coder]; SD_UNLOCK(_codersLock); } #pragma mark - SDImageCoder - (BOOL)canDecodeFromData:(NSData *)data { NSArray> *coders = self.coders; for (id coder in coders.reverseObjectEnumerator) { if ([coder canDecodeFromData:data]) { return YES; } } return NO; } - (BOOL)canEncodeToFormat:(SDImageFormat)format { NSArray> *coders = self.coders; for (id coder in coders.reverseObjectEnumerator) { if ([coder canEncodeToFormat:format]) { return YES; } } return NO; } - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options { if (!data) { return nil; } UIImage *image; NSArray> *coders = self.coders; for (id coder in coders.reverseObjectEnumerator) { if ([coder canDecodeFromData:data]) { image = [coder decodedImageWithData:data options:options]; break; } } return image; } - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options { if (!image) { return nil; } NSArray> *coders = self.coders; for (id coder in coders.reverseObjectEnumerator) { if ([coder canEncodeToFormat:format]) { return [coder encodedDataWithImage:image format:format options:options]; } } return nil; } - (NSData *)encodedDataWithFrames:(NSArray *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options { if (!frames || frames.count < 1) { return nil; } NSArray> *coders = self.coders; for (id coder in coders.reverseObjectEnumerator) { if ([coder canEncodeToFormat:format]) { if ([coder respondsToSelector:@selector(encodedDataWithFrames:loopCount:format:options:)]) { return [coder encodedDataWithFrames:frames loopCount:loopCount format:format options:options]; } } } return nil; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageFrame.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /** This class is used for creating animated images via `animatedImageWithFrames` in `SDImageCoderHelper`. @note If you need to specify animated images loop count, use `sd_imageLoopCount` property in `UIImage+Metadata.h`. */ @interface SDImageFrame : NSObject /** The image of current frame. You should not set an animated image. */ @property (nonatomic, strong, readonly, nonnull) UIImage *image; /** The duration of current frame to be displayed. The number is seconds but not milliseconds. You should not set this to zero. */ @property (nonatomic, readonly, assign) NSTimeInterval duration; /// Create a frame instance with specify image and duration /// @param image current frame's image /// @param duration current frame's duration - (nonnull instancetype)initWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration; /** Create a frame instance with specify image and duration @param image current frame's image @param duration current frame's duration @return frame instance */ + (nonnull instancetype)frameWithImage:(nonnull UIImage *)image duration:(NSTimeInterval)duration; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageFrame.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageFrame.h" @interface SDImageFrame () @property (nonatomic, strong, readwrite, nonnull) UIImage *image; @property (nonatomic, readwrite, assign) NSTimeInterval duration; @end @implementation SDImageFrame - (instancetype)initWithImage:(UIImage *)image duration:(NSTimeInterval)duration { self = [super init]; if (self) { _image = image; _duration = duration; } return self; } + (instancetype)frameWithImage:(UIImage *)image duration:(NSTimeInterval)duration { SDImageFrame *frame = [[SDImageFrame alloc] initWithImage:image duration:duration]; return frame; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageGIFCoder.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDImageIOAnimatedCoder.h" /** Built in coder using ImageIO that supports animated GIF encoding/decoding @note `SDImageIOCoder` supports GIF but only as static (will use the 1st frame). @note Use `SDImageGIFCoder` for fully animated GIFs. For `UIImageView`, it will produce animated `UIImage`(`NSImage` on macOS) for rendering. For `SDAnimatedImageView`, it will use `SDAnimatedImage` for rendering. @note The recommended approach for animated GIFs is using `SDAnimatedImage` with `SDAnimatedImageView`. It's more performant than `UIImageView` for GIF displaying(especially on memory usage) */ @interface SDImageGIFCoder : SDImageIOAnimatedCoder @property (nonatomic, class, readonly, nonnull) SDImageGIFCoder *sharedCoder; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageGIFCoder.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageGIFCoder.h" #import "SDImageIOAnimatedCoderInternal.h" #if SD_MAC #import #else #import #endif @implementation SDImageGIFCoder + (instancetype)sharedCoder { static SDImageGIFCoder *coder; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ coder = [[SDImageGIFCoder alloc] init]; }); return coder; } #pragma mark - Subclass Override + (SDImageFormat)imageFormat { return SDImageFormatGIF; } + (NSString *)imageUTType { return (__bridge NSString *)kSDUTTypeGIF; } + (NSString *)dictionaryProperty { return (__bridge NSString *)kCGImagePropertyGIFDictionary; } + (NSString *)unclampedDelayTimeProperty { return (__bridge NSString *)kCGImagePropertyGIFUnclampedDelayTime; } + (NSString *)delayTimeProperty { return (__bridge NSString *)kCGImagePropertyGIFDelayTime; } + (NSString *)loopCountProperty { return (__bridge NSString *)kCGImagePropertyGIFLoopCount; } + (NSUInteger)defaultLoopCount { return 1; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageGraphics.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import /** These following graphics context method are provided to easily write cross-platform(AppKit/UIKit) code. For UIKit, these methods just call the same method in `UIGraphics.h`. See the documentation for usage. For AppKit, these methods use `NSGraphicsContext` to create image context and match the behavior like UIKit. @note If you don't care bitmap format (ARGB8888) and just draw image, use `SDGraphicsImageRenderer` instead. It's more performant on RAM usage.` */ /// Returns the current graphics context. FOUNDATION_EXPORT CGContextRef __nullable SDGraphicsGetCurrentContext(void) CF_RETURNS_NOT_RETAINED; /// Creates a bitmap-based graphics context and makes it the current context. FOUNDATION_EXPORT void SDGraphicsBeginImageContext(CGSize size); /// Creates a bitmap-based graphics context with the specified options. FOUNDATION_EXPORT void SDGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale); /// Removes the current bitmap-based graphics context from the top of the stack. FOUNDATION_EXPORT void SDGraphicsEndImageContext(void); /// Returns an image based on the contents of the current bitmap-based graphics context. FOUNDATION_EXPORT UIImage * __nullable SDGraphicsGetImageFromCurrentImageContext(void); ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageGraphics.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageGraphics.h" #import "NSImage+Compatibility.h" #import "SDImageCoderHelper.h" #import "objc/runtime.h" #if SD_MAC static void *kNSGraphicsContextScaleFactorKey; static CGContextRef SDCGContextCreateBitmapContext(CGSize size, BOOL opaque, CGFloat scale) { if (scale == 0) { // Match `UIGraphicsBeginImageContextWithOptions`, reset to the scale factor of the device’s main screen if scale is 0. NSScreen *mainScreen = nil; if (@available(macOS 10.12, *)) { mainScreen = [NSScreen mainScreen]; } else { mainScreen = [NSScreen screens].firstObject; } scale = mainScreen.backingScaleFactor ?: 1.0f; } size_t width = ceil(size.width * scale); size_t height = ceil(size.height * scale); if (width < 1 || height < 1) return NULL; CGColorSpaceRef space = [SDImageCoderHelper colorSpaceGetDeviceRGB]; // kCGImageAlphaNone is not supported in CGBitmapContextCreate. // Check #3330 for more detail about why this bitmap is choosen. // From v5.17.0, use runtime detection of bitmap info instead of hardcode. // However, macOS's runtime detection will also call this function, cause recursive, so still hardcode here CGBitmapInfo bitmapInfo; if (!opaque) { // [NSImage imageWithSize:flipped:drawingHandler:] returns float(16-bits) RGBA8888 on alpha image, which we don't need bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedLast; } else { bitmapInfo = kCGBitmapByteOrderDefault | kCGImageAlphaNoneSkipLast; } CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, space, bitmapInfo); if (!context) { return NULL; } CGContextScaleCTM(context, scale, scale); return context; } #endif CGContextRef SDGraphicsGetCurrentContext(void) { #if SD_UIKIT || SD_WATCH return UIGraphicsGetCurrentContext(); #else return NSGraphicsContext.currentContext.CGContext; #endif } void SDGraphicsBeginImageContext(CGSize size) { #if SD_UIKIT || SD_WATCH UIGraphicsBeginImageContext(size); #else SDGraphicsBeginImageContextWithOptions(size, NO, 1.0); #endif } void SDGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale) { #if SD_UIKIT || SD_WATCH UIGraphicsBeginImageContextWithOptions(size, opaque, scale); #else CGContextRef context = SDCGContextCreateBitmapContext(size, opaque, scale); if (!context) { return; } NSGraphicsContext *graphicsContext = [NSGraphicsContext graphicsContextWithCGContext:context flipped:NO]; objc_setAssociatedObject(graphicsContext, &kNSGraphicsContextScaleFactorKey, @(scale), OBJC_ASSOCIATION_RETAIN); CGContextRelease(context); [NSGraphicsContext saveGraphicsState]; NSGraphicsContext.currentContext = graphicsContext; #endif } void SDGraphicsEndImageContext(void) { #if SD_UIKIT || SD_WATCH UIGraphicsEndImageContext(); #else [NSGraphicsContext restoreGraphicsState]; #endif } UIImage * SDGraphicsGetImageFromCurrentImageContext(void) { #if SD_UIKIT || SD_WATCH return UIGraphicsGetImageFromCurrentImageContext(); #else NSGraphicsContext *context = NSGraphicsContext.currentContext; CGContextRef contextRef = context.CGContext; if (!contextRef) { return nil; } CGImageRef imageRef = CGBitmapContextCreateImage(contextRef); if (!imageRef) { return nil; } CGFloat scale = 0; NSNumber *scaleFactor = objc_getAssociatedObject(context, &kNSGraphicsContextScaleFactorKey); if ([scaleFactor isKindOfClass:[NSNumber class]]) { scale = scaleFactor.doubleValue; } if (!scale) { // reset to the scale factor of the device’s main screen if scale is 0. NSScreen *mainScreen = nil; if (@available(macOS 10.12, *)) { mainScreen = [NSScreen mainScreen]; } else { mainScreen = [NSScreen screens].firstObject; } scale = mainScreen.backingScaleFactor ?: 1.0f; } NSImage *image = [[NSImage alloc] initWithCGImage:imageRef scale:scale orientation:kCGImagePropertyOrientationUp]; CGImageRelease(imageRef); return image; #endif } ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageHEICCoder.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDImageIOAnimatedCoder.h" /** This coder is used for HEIC (HEIF with HEVC container codec) image format. Image/IO provide the static HEIC (.heic) support in iOS 11/macOS 10.13/tvOS 11/watchOS 4+. Image/IO provide the animated HEIC (.heics) support in iOS 13/macOS 10.15/tvOS 13/watchOS 6+. See https://nokiatech.github.io/heif/technical.html for the standard. @note This coder is not in the default coder list for now, since HEIC animated image is really rare, and Apple's implementation still contains performance issues. You can enable if you need this. @note If you need to support lower firmware version for HEIF, you can have a try at https://github.com/SDWebImage/SDWebImageHEIFCoder */ API_AVAILABLE(ios(13.0), tvos(13.0), macos(10.15), watchos(6.0)) @interface SDImageHEICCoder : SDImageIOAnimatedCoder @property (nonatomic, class, readonly, nonnull) SDImageHEICCoder *sharedCoder; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageHEICCoder.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageHEICCoder.h" #import "SDImageIOAnimatedCoderInternal.h" // These constants are available from iOS 13+ and Xcode 11. This raw value is used for toolchain and firmware compatibility static NSString * kSDCGImagePropertyHEICSDictionary = @"{HEICS}"; static NSString * kSDCGImagePropertyHEICSLoopCount = @"LoopCount"; static NSString * kSDCGImagePropertyHEICSDelayTime = @"DelayTime"; static NSString * kSDCGImagePropertyHEICSUnclampedDelayTime = @"UnclampedDelayTime"; @implementation SDImageHEICCoder + (void)initialize { if (@available(iOS 13, tvOS 13, macOS 10.15, watchOS 6, *)) { // Use SDK instead of raw value kSDCGImagePropertyHEICSDictionary = (__bridge NSString *)kCGImagePropertyHEICSDictionary; kSDCGImagePropertyHEICSLoopCount = (__bridge NSString *)kCGImagePropertyHEICSLoopCount; kSDCGImagePropertyHEICSDelayTime = (__bridge NSString *)kCGImagePropertyHEICSDelayTime; kSDCGImagePropertyHEICSUnclampedDelayTime = (__bridge NSString *)kCGImagePropertyHEICSUnclampedDelayTime; } } + (instancetype)sharedCoder { static SDImageHEICCoder *coder; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ coder = [[SDImageHEICCoder alloc] init]; }); return coder; } #pragma mark - SDImageCoder - (BOOL)canDecodeFromData:(nullable NSData *)data { switch ([NSData sd_imageFormatForImageData:data]) { case SDImageFormatHEIC: // Check HEIC decoding compatibility return [self.class canDecodeFromFormat:SDImageFormatHEIC]; case SDImageFormatHEIF: // Check HEIF decoding compatibility return [self.class canDecodeFromFormat:SDImageFormatHEIF]; default: return NO; } } - (BOOL)canIncrementalDecodeFromData:(NSData *)data { return [self canDecodeFromData:data]; } - (BOOL)canEncodeToFormat:(SDImageFormat)format { switch (format) { case SDImageFormatHEIC: // Check HEIC encoding compatibility return [self.class canEncodeToFormat:SDImageFormatHEIC]; case SDImageFormatHEIF: // Check HEIF encoding compatibility return [self.class canEncodeToFormat:SDImageFormatHEIF]; default: return NO; } } #pragma mark - Subclass Override + (SDImageFormat)imageFormat { return SDImageFormatHEIC; } + (NSString *)imageUTType { // See: https://nokiatech.github.io/heif/technical.html // Actually HEIC has another concept called `non-timed Image Sequence`, which can be encoded using `public.heic` return (__bridge NSString *)kSDUTTypeHEIC; } + (NSString *)animatedImageUTType { // See: https://nokiatech.github.io/heif/technical.html // We use `timed Image Sequence`, means, `public.heics` for animated image encoding return (__bridge NSString *)kSDUTTypeHEICS; } + (NSString *)dictionaryProperty { return kSDCGImagePropertyHEICSDictionary; } + (NSString *)unclampedDelayTimeProperty { return kSDCGImagePropertyHEICSUnclampedDelayTime; } + (NSString *)delayTimeProperty { return kSDCGImagePropertyHEICSDelayTime; } + (NSString *)loopCountProperty { return kSDCGImagePropertyHEICSLoopCount; } + (NSUInteger)defaultLoopCount { return 0; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDImageCoder.h" /** This is the abstract class for all animated coder, which use the Image/IO API. You can not use this directly as real coders. A exception will be raised if you use this class. All of the properties need the subclass to implement and works as expected. For Image/IO, See Apple's documentation: https://developer.apple.com/documentation/imageio */ @interface SDImageIOAnimatedCoder : NSObject #pragma mark - Subclass Override /** The supported animated image format. Such as `SDImageFormatGIF`. @note Subclass override. */ @property (class, readonly) SDImageFormat imageFormat; /** The supported image format UTI Type. Such as `kSDUTTypeGIF`. This can be used for cases when we can not detect `SDImageFormat. Such as progressive decoding's hint format `kCGImageSourceTypeIdentifierHint`. @note Subclass override. */ @property (class, readonly, nonnull) NSString *imageUTType; /** Some image codec use different UTI Type between animated image and static image. For this case, override this method and return the UTI for animated image encoding. @note Defaults to use the value of `imageUTType`, so it's @optional actually. @note Subclass override. */ @property (class, readonly, nonnull) NSString *animatedImageUTType; /** The image container property key used in Image/IO API. Such as `kCGImagePropertyGIFDictionary`. @note Subclass override. */ @property (class, readonly, nonnull) NSString *dictionaryProperty; /** The image unclamped delay time property key used in Image/IO API. Such as `kCGImagePropertyGIFUnclampedDelayTime` @note Subclass override. */ @property (class, readonly, nonnull) NSString *unclampedDelayTimeProperty; /** The image delay time property key used in Image/IO API. Such as `kCGImagePropertyGIFDelayTime`. @note Subclass override. */ @property (class, readonly, nonnull) NSString *delayTimeProperty; /** The image loop count property key used in Image/IO API. Such as `kCGImagePropertyGIFLoopCount`. @note Subclass override. */ @property (class, readonly, nonnull) NSString *loopCountProperty; /** The default loop count when there are no any loop count information inside image container metadata. For example, for GIF format, the standard use 1 (play once). For APNG format, the standard use 0 (infinity loop). @note Subclass override. */ @property (class, readonly) NSUInteger defaultLoopCount; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageIOAnimatedCoder.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageIOAnimatedCoder.h" #import "SDImageIOAnimatedCoderInternal.h" #import "NSImage+Compatibility.h" #import "UIImage+Metadata.h" #import "NSData+ImageContentType.h" #import "SDImageCoderHelper.h" #import "SDAnimatedImageRep.h" #import "UIImage+ForceDecode.h" #import "SDInternalMacros.h" #import #import #if SD_CHECK_CGIMAGE_RETAIN_SOURCE #import // SPI to check thread safe during Example and Test static CGImageSourceRef (*SDCGImageGetImageSource)(CGImageRef); #endif // Specify File Size for lossy format encoding, like JPEG static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize"; // Support Xcode 15 SDK, use raw value instead of symbol static NSString * kSDCGImageDestinationEncodeRequest = @"kCGImageDestinationEncodeRequest"; static NSString * kSDCGImageDestinationEncodeToSDR = @"kCGImageDestinationEncodeToSDR"; static NSString * kSDCGImageDestinationEncodeToISOHDR = @"kCGImageDestinationEncodeToISOHDR"; static NSString * kSDCGImageDestinationEncodeToISOGainmap = @"kCGImageDestinationEncodeToISOGainmap"; // This strip the un-wanted CGImageProperty, like the internal CGImageSourceRef in iOS 15+ // However, CGImageCreateCopy still keep those CGImageProperty, not suit for our use case static CGImageRef __nullable SDCGImageCreateMutableCopy(CGImageRef cg_nullable image, CGBitmapInfo bitmapInfo) { if (!image) return nil; size_t width = CGImageGetWidth(image); size_t height = CGImageGetHeight(image); size_t bitsPerComponent = CGImageGetBitsPerComponent(image); size_t bitsPerPixel = CGImageGetBitsPerPixel(image); size_t bytesPerRow = CGImageGetBytesPerRow(image); CGColorSpaceRef space = CGImageGetColorSpace(image); CGDataProviderRef provider = CGImageGetDataProvider(image); const CGFloat *decode = CGImageGetDecode(image); bool shouldInterpolate = CGImageGetShouldInterpolate(image); CGColorRenderingIntent intent = CGImageGetRenderingIntent(image); CGImageRef newImage = CGImageCreate(width, height, bitsPerComponent, bitsPerPixel, bytesPerRow, space, bitmapInfo, provider, decode, shouldInterpolate, intent); return newImage; } static inline BOOL SDCGImageIs8Bit(CGImageRef cg_nullable image) { return CGImageGetBitsPerComponent(image) == 8; } static inline CGImageRef __nullable SDCGImageCreateCopy(CGImageRef cg_nullable image) { if (!image) return nil; return SDCGImageCreateMutableCopy(image, CGImageGetBitmapInfo(image)); } static BOOL SDLoadOnePixelBitmapBuffer(CGImageRef imageRef, uint8_t *r, uint8_t *g, uint8_t *b, uint8_t *a) { CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; // Get pixels CGDataProviderRef provider = CGImageGetDataProvider(imageRef); if (!provider) { return NO; } CFDataRef data = CGDataProviderCopyData(provider); if (!data) { return NO; } CFRange range = CFRangeMake(0, 4); // one pixel if (CFDataGetLength(data) < range.location + range.length) { CFRelease(data); return NO; } uint8_t pixel[4] = {0}; CFDataGetBytes(data, range, pixel); CFRelease(data); BOOL byteOrderNormal = NO; switch (byteOrderInfo) { case kCGBitmapByteOrderDefault: { byteOrderNormal = YES; } break; case kCGBitmapByteOrder16Little: case kCGBitmapByteOrder32Little: { } break; case kCGBitmapByteOrder16Big: case kCGBitmapByteOrder32Big: { byteOrderNormal = YES; } break; default: break; } switch (alphaInfo) { case kCGImageAlphaPremultipliedFirst: case kCGImageAlphaFirst: { if (byteOrderNormal) { // ARGB8888 *a = pixel[0]; *r = pixel[1]; *g = pixel[2]; *b = pixel[3]; } else { // BGRA8888 *b = pixel[0]; *g = pixel[1]; *r = pixel[2]; *a = pixel[3]; } } break; case kCGImageAlphaPremultipliedLast: case kCGImageAlphaLast: { if (byteOrderNormal) { // RGBA8888 *r = pixel[0]; *g = pixel[1]; *b = pixel[2]; *a = pixel[3]; } else { // ABGR8888 *a = pixel[0]; *b = pixel[1]; *g = pixel[2]; *r = pixel[3]; } } break; case kCGImageAlphaNone: { if (byteOrderNormal) { // RGB *r = pixel[0]; *g = pixel[1]; *b = pixel[2]; } else { // BGR *b = pixel[0]; *g = pixel[1]; *r = pixel[2]; } } break; case kCGImageAlphaNoneSkipLast: { if (byteOrderNormal) { // RGBX *r = pixel[0]; *g = pixel[1]; *b = pixel[2]; } else { // XBGR *b = pixel[1]; *g = pixel[2]; *r = pixel[3]; } } break; case kCGImageAlphaNoneSkipFirst: { if (byteOrderNormal) { // XRGB *r = pixel[1]; *g = pixel[2]; *b = pixel[3]; } else { // BGRX *b = pixel[0]; *g = pixel[1]; *r = pixel[2]; } } break; case kCGImageAlphaOnly: { // A *a = pixel[0]; } break; default: break; } return YES; } static CGImageRef SDImageIOPNGPluginBuggyCreateWorkaround(CGImageRef cgImage) CF_RETURNS_RETAINED { CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(cgImage); CGImageAlphaInfo alphaInfo = (bitmapInfo & kCGBitmapAlphaInfoMask); CGImageAlphaInfo newAlphaInfo = alphaInfo; if (alphaInfo == kCGImageAlphaLast) { newAlphaInfo = kCGImageAlphaPremultipliedLast; } else if (alphaInfo == kCGImageAlphaFirst) { newAlphaInfo = kCGImageAlphaPremultipliedFirst; } if (newAlphaInfo != alphaInfo) { CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; CGBitmapInfo newBitmapInfo = newAlphaInfo | byteOrderInfo; if (SD_OPTIONS_CONTAINS(bitmapInfo, kCGBitmapFloatComponents)) { // Keep float components newBitmapInfo |= kCGBitmapFloatComponents; } // Create new CGImage with corrected alpha info... CGImageRef newCGImage = SDCGImageCreateMutableCopy(cgImage, newBitmapInfo); return newCGImage; } else { CGImageRetain(cgImage); return cgImage; } } static BOOL SDImageIOPNGPluginBuggyNeedWorkaround(void) { // See: #3605 FB13322459 // ImageIO on iOS 17 (17.0~17.2), there is one serious problem on ImageIO PNG plugin. The decode result for indexed color PNG use the wrong CGImageAlphaInfo // The returned CGImageAlphaInfo is alpha last, but the actual bitmap data is premultiplied alpha last, which cause many runtime render bug. // The bug only exists on 8-bits indexed color, not about 16-bits // So, we do a hack workaround: // 1. Decode a indexed color PNG in runtime // 2. If the bitmap is premultiplied alpha, then assume it's buggy // 3. If buggy, then all premultiplied `CGImageAlphaInfo` will assume to be non-premultiplied // :) if (@available(iOS 17, tvOS 17, macOS 14, watchOS 11, *)) { // Continue } else { return NO; } static BOOL isBuggy = NO; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *base64String = @"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEUyMjKlMgnVAAAAAXRSTlMyiDGJ5gAAAApJREFUCNdjYAAAAAIAAeIhvDMAAAAASUVORK5CYII="; NSData *onePixelIndexedPNGData = [[NSData alloc] initWithBase64EncodedString:base64String options:NSDataBase64DecodingIgnoreUnknownCharacters]; CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)onePixelIndexedPNGData, nil); NSCParameterAssert(source); CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil); NSCParameterAssert(cgImage); uint8_t r, g, b, a; r = g = b = a = 0; BOOL success = SDLoadOnePixelBitmapBuffer(cgImage, &r, &g, &b, &a); if (!success) { isBuggy = NO; // Impossible... } else { if (r == 50 && g == 50 && b == 50 && a == 50) { // Correct value isBuggy = NO; } else { SD_LOG("%@", @"Detected the current OS's ImageIO PNG Decoder is buggy on indexed color PNG. Perform workaround solution..."); isBuggy = YES; } } CFRelease(source); CGImageRelease(cgImage); }); return isBuggy; } @interface SDImageIOCoderFrame : NSObject @property (nonatomic, assign) NSUInteger index; // Frame index (zero based) @property (nonatomic, assign) NSTimeInterval duration; // Frame duration in seconds @end @implementation SDImageIOCoderFrame @end @implementation SDImageIOAnimatedCoder { size_t _width, _height; CGImageSourceRef _imageSource; BOOL _incremental; SD_LOCK_DECLARE(_lock); // Lock only apply for incremental animation decoding NSData *_imageData; CGFloat _scale; NSUInteger _loopCount; NSUInteger _frameCount; NSArray *_frames; BOOL _finished; BOOL _preserveAspectRatio; CGSize _thumbnailSize; NSUInteger _limitBytes; BOOL _lazyDecode; BOOL _decodeToHDR; } #if SD_IMAGEIO_HDR_ENCODING + (void)initialize { if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) { // Use SDK instead of raw value kSDCGImageDestinationEncodeRequest = (__bridge NSString *)kCGImageDestinationEncodeRequest; kSDCGImageDestinationEncodeToSDR = (__bridge NSString *)kCGImageDestinationEncodeToSDR; kSDCGImageDestinationEncodeToISOHDR = (__bridge NSString *)kCGImageDestinationEncodeToISOHDR; kSDCGImageDestinationEncodeToISOGainmap = (__bridge NSString *)kCGImageDestinationEncodeToISOGainmap; } } #endif - (void)dealloc { if (_imageSource) { CFRelease(_imageSource); _imageSource = NULL; } #if SD_UIKIT [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } - (void)didReceiveMemoryWarning:(NSNotification *)notification { if (_imageSource) { for (size_t i = 0; i < _frameCount; i++) { CGImageSourceRemoveCacheAtIndex(_imageSource, i); } } } #pragma mark - Subclass Override + (SDImageFormat)imageFormat { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)] userInfo:nil]; } + (NSString *)imageUTType { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)] userInfo:nil]; } + (NSString *)animatedImageUTType { return [self imageUTType]; } + (NSString *)dictionaryProperty { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)] userInfo:nil]; } + (NSString *)unclampedDelayTimeProperty { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)] userInfo:nil]; } + (NSString *)delayTimeProperty { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)] userInfo:nil]; } + (NSString *)loopCountProperty { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)] userInfo:nil]; } + (NSUInteger)defaultLoopCount { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"For `SDImageIOAnimatedCoder` subclass, you must override %@ method", NSStringFromSelector(_cmd)] userInfo:nil]; } #pragma mark - Utils + (BOOL)canDecodeFromFormat:(SDImageFormat)format { static dispatch_once_t onceToken; static NSSet *imageUTTypeSet; dispatch_once(&onceToken, ^{ NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageSourceCopyTypeIdentifiers(); imageUTTypeSet = [NSSet setWithArray:imageUTTypes]; }); CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format]; if ([imageUTTypeSet containsObject:(__bridge NSString *)(imageUTType)]) { // Can decode from target format return YES; } return NO; } + (BOOL)canEncodeToFormat:(SDImageFormat)format { static dispatch_once_t onceToken; static NSSet *imageUTTypeSet; dispatch_once(&onceToken, ^{ NSArray *imageUTTypes = (__bridge_transfer NSArray *)CGImageDestinationCopyTypeIdentifiers(); imageUTTypeSet = [NSSet setWithArray:imageUTTypes]; }); CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format]; if ([imageUTTypeSet containsObject:(__bridge NSString *)(imageUTType)]) { // Can encode to target format return YES; } return NO; } + (NSUInteger)imageLoopCountWithSource:(CGImageSourceRef)source { NSUInteger loopCount = self.defaultLoopCount; NSDictionary *imageProperties = (__bridge_transfer NSDictionary *)CGImageSourceCopyProperties(source, NULL); NSDictionary *containerProperties = imageProperties[self.dictionaryProperty]; if (containerProperties) { NSNumber *containerLoopCount = containerProperties[self.loopCountProperty]; if (containerLoopCount != nil) { loopCount = containerLoopCount.unsignedIntegerValue; } } return loopCount; } + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(CGImageSourceRef)source { NSTimeInterval frameDuration = 0.1; CFDictionaryRef cfFrameProperties = CGImageSourceCopyPropertiesAtIndex(source, index, NULL); if (!cfFrameProperties) { return frameDuration; } NSDictionary *frameProperties = (__bridge NSDictionary *)cfFrameProperties; NSDictionary *containerProperties = frameProperties[self.dictionaryProperty]; NSNumber *delayTimeUnclampedProp = containerProperties[self.unclampedDelayTimeProperty]; if (delayTimeUnclampedProp != nil) { frameDuration = [delayTimeUnclampedProp doubleValue]; } else { NSNumber *delayTimeProp = containerProperties[self.delayTimeProperty]; if (delayTimeProp != nil) { frameDuration = [delayTimeProp doubleValue]; } } // Many annoying ads specify a 0 duration to make an image flash as quickly as possible. // We follow Firefox's behavior and use a duration of 100 ms for any frames that specify // a duration of <= 10 ms. See and // for more information. if (frameDuration < 0.011) { frameDuration = 0.1; } CFRelease(cfFrameProperties); return frameDuration; } + (UIImage *)createFrameAtIndex:(NSUInteger)index source:(CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage decodeToHDR:(BOOL)decodeToHDR { // `animatedImage` means called from `SDAnimatedImageProvider.animatedImageFrameAtIndex` NSDictionary *options; if (animatedImage) { if (!lazyDecode) { options = @{ // image decoding and caching should happen at image creation time. (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(YES), }; } else { options = @{ // image decoding will happen at rendering time (__bridge NSString *)kCGImageSourceShouldCacheImmediately : @(NO), }; } } // Parse the image properties NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, index, NULL); CGFloat pixelWidth = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue]; CGFloat pixelHeight = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue]; CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp; NSNumber *exifOrientationValue = properties[(__bridge NSString *)kCGImagePropertyOrientation]; if (exifOrientationValue != NULL) { exifOrientation = [exifOrientationValue unsignedIntValue]; } NSMutableDictionary *decodingOptions; if (options) { decodingOptions = [NSMutableDictionary dictionaryWithDictionary:options]; } else { decodingOptions = [NSMutableDictionary dictionary]; } if (@available(macOS 14, iOS 17, tvOS 17, watchOS 10, *)) { if (decodeToHDR) { decodingOptions[(__bridge NSString *)kCGImageSourceDecodeRequest] = (__bridge NSString *)kCGImageSourceDecodeToHDR; } else { decodingOptions[(__bridge NSString *)kCGImageSourceDecodeRequest] = (__bridge NSString *)kCGImageSourceDecodeToSDR; } } CGImageRef imageRef; BOOL createFullImage = thumbnailSize.width == 0 || thumbnailSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= thumbnailSize.width && pixelHeight <= thumbnailSize.height); if (createFullImage) { imageRef = CGImageSourceCreateImageAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]); } else { decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailWithTransform] = @(preserveAspectRatio); CGFloat maxPixelSize; if (preserveAspectRatio) { CGFloat pixelRatio = pixelWidth / pixelHeight; CGFloat thumbnailRatio = thumbnailSize.width / thumbnailSize.height; if (pixelRatio > thumbnailRatio) { maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.width / pixelRatio); } else { maxPixelSize = MAX(thumbnailSize.height, thumbnailSize.height * pixelRatio); } } else { maxPixelSize = MAX(thumbnailSize.width, thumbnailSize.height); } decodingOptions[(__bridge NSString *)kCGImageSourceThumbnailMaxPixelSize] = @(maxPixelSize); decodingOptions[(__bridge NSString *)kCGImageSourceCreateThumbnailFromImageAlways] = @(YES); imageRef = CGImageSourceCreateThumbnailAtIndex(source, index, (__bridge CFDictionaryRef)[decodingOptions copy]); } if (!imageRef) { return nil; } BOOL isHDRImage = [SDImageCoderHelper CGImageIsHDR:imageRef]; // Thumbnail image post-process if (!createFullImage) { if (preserveAspectRatio) { // kCGImageSourceCreateThumbnailWithTransform will apply EXIF transform as well, we should not apply twice exifOrientation = kCGImagePropertyOrientationUp; } else { // `CGImageSourceCreateThumbnailAtIndex` take only pixel dimension, if not `preserveAspectRatio`, we should manual scale to the target size CGImageRef scaledImageRef = [SDImageCoderHelper CGImageCreateScaled:imageRef size:thumbnailSize]; if (scaledImageRef) { CGImageRelease(imageRef); imageRef = scaledImageRef; } } } // Check whether output CGImage is decoded BOOL isLazy = [SDImageCoderHelper CGImageIsLazy:imageRef]; if (!lazyDecode && !isHDRImage) { if (isLazy) { // Use CoreGraphics to trigger immediately decode to drop lazy CGImage CGImageRef decodedImageRef = [SDImageCoderHelper CGImageCreateDecoded:imageRef]; if (decodedImageRef) { CGImageRelease(imageRef); imageRef = decodedImageRef; isLazy = NO; } } } else if (animatedImage && !isHDRImage) { // iOS 15+, CGImageRef now retains CGImageSourceRef internally. To workaround its thread-safe issue, we have to strip CGImageSourceRef, using Force-Decode (or have to use SPI `CGImageSetImageSource`), See: https://github.com/SDWebImage/SDWebImage/issues/3273 if (@available(iOS 15, tvOS 15, *)) { // User pass `lazyDecode == YES`, but we still have to strip the CGImageSourceRef // CGImageRef newImageRef = CGImageCreateCopy(imageRef); // This one does not strip the CGImageProperty CGImageRef newImageRef = SDCGImageCreateCopy(imageRef); if (newImageRef) { CGImageRelease(imageRef); imageRef = newImageRef; } #if SD_CHECK_CGIMAGE_RETAIN_SOURCE // Assert here to check CGImageRef should not retain the CGImageSourceRef and has possible thread-safe issue (this is behavior on iOS 15+) // If assert hit, fire issue to https://github.com/SDWebImage/SDWebImage/issues and we update the condition for this behavior check static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ SDCGImageGetImageSource = dlsym(RTLD_DEFAULT, "CGImageGetImageSource"); }); if (SDCGImageGetImageSource) { NSCAssert(!SDCGImageGetImageSource(imageRef), @"Animated Coder created CGImageRef should not retain CGImageSourceRef, which may cause thread-safe issue without lock"); } #endif } } // :) CFStringRef uttype = CGImageSourceGetType(source); SDImageFormat imageFormat = [NSData sd_imageFormatFromUTType:uttype]; if (imageFormat == SDImageFormatPNG && SDCGImageIs8Bit(imageRef) && SDImageIOPNGPluginBuggyNeedWorkaround()) { CGImageRef newImageRef = SDImageIOPNGPluginBuggyCreateWorkaround(imageRef); CGImageRelease(imageRef); imageRef = newImageRef; } #if SD_UIKIT || SD_WATCH UIImageOrientation imageOrientation = [SDImageCoderHelper imageOrientationFromEXIFOrientation:exifOrientation]; UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:scale orientation:exifOrientation]; #endif CGImageRelease(imageRef); image.sd_isDecoded = !isLazy; return image; } #pragma mark - Decode - (BOOL)canDecodeFromData:(nullable NSData *)data { return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat); } - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options { if (!data) { return nil; } CGFloat scale = 1; NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } CGSize thumbnailSize = CGSizeZero; NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; if (thumbnailSizeValue != nil) { #if SD_MAC thumbnailSize = thumbnailSizeValue.sizeValue; #else thumbnailSize = thumbnailSizeValue.CGSizeValue; #endif } BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; } BOOL lazyDecode = YES; // Defaults YES for static image coder NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; if (lazyDecodeValue != nil) { lazyDecode = lazyDecodeValue.boolValue; } NSUInteger limitBytes = 0; NSNumber *limitBytesValue = options[SDImageCoderDecodeScaleDownLimitBytes]; if (limitBytesValue != nil) { limitBytes = limitBytesValue.unsignedIntegerValue; } BOOL decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue]; #if SD_MAC // If don't use thumbnail, prefers the built-in generation of frames (GIF/APNG) // Which decode frames in time and reduce memory usage if (limitBytes == 0 && (thumbnailSize.width == 0 || thumbnailSize.height == 0)) { SDAnimatedImageRep *imageRep = [[SDAnimatedImageRep alloc] initWithData:data]; if (imageRep) { NSSize size = NSMakeSize(imageRep.pixelsWide / scale, imageRep.pixelsHigh / scale); imageRep.size = size; NSImage *animatedImage = [[NSImage alloc] initWithSize:size]; [animatedImage addRepresentation:imageRep]; animatedImage.sd_imageFormat = self.class.imageFormat; return animatedImage; } } #endif NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint]; if (!typeIdentifierHint) { // Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension NSString *fileExtensionHint = options[SDImageCoderDecodeFileExtensionHint]; if (fileExtensionHint) { typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage); // Ignore dynamic UTI if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) { typeIdentifierHint = nil; } } } else if ([typeIdentifierHint isEqual:NSNull.null]) { // Hack if user don't want to imply file extension typeIdentifierHint = nil; } NSDictionary *creatingOptions = nil; if (typeIdentifierHint) { creatingOptions = @{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : typeIdentifierHint}; } CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)creatingOptions); if (!source) { // Try again without UTType hint, the call site from user may provide the wrong UTType source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil); } if (!source) { return nil; } size_t frameCount = CGImageSourceGetCount(source); UIImage *animatedImage; // Parse the image properties NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(source, 0, NULL); size_t width = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue]; size_t height = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue]; // Scale down to limit bytes if need if (limitBytes > 0) { // Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage CGSize imageSize = CGSizeMake(width, height); CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:limitBytes bytesPerPixel:4 frameCount:frameCount]; // Override thumbnail size thumbnailSize = framePixelSize; preserveAspectRatio = YES; } BOOL decodeFirstFrame = [options[SDImageCoderDecodeFirstFrameOnly] boolValue]; if (decodeFirstFrame || frameCount <= 1) { animatedImage = [self.class createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO decodeToHDR:decodeToHDR]; } else { NSMutableArray *frames = [NSMutableArray arrayWithCapacity:frameCount]; for (size_t i = 0; i < frameCount; i++) { UIImage *image = [self.class createFrameAtIndex:i source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO decodeToHDR:decodeToHDR]; if (!image) { continue; } NSTimeInterval duration = [self.class frameDurationAtIndex:i source:source]; SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:duration]; [frames addObject:frame]; } NSUInteger loopCount = [self.class imageLoopCountWithSource:source]; animatedImage = [SDImageCoderHelper animatedImageWithFrames:frames]; animatedImage.sd_imageLoopCount = loopCount; } animatedImage.sd_imageFormat = self.class.imageFormat; CFRelease(source); return animatedImage; } #pragma mark - Progressive Decode - (BOOL)canIncrementalDecodeFromData:(NSData *)data { return ([NSData sd_imageFormatForImageData:data] == self.class.imageFormat); } - (instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options { self = [super init]; if (self) { NSString *imageUTType = self.class.imageUTType; _imageSource = CGImageSourceCreateIncremental((__bridge CFDictionaryRef)@{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : imageUTType}); _incremental = YES; CGFloat scale = 1; NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } _scale = scale; CGSize thumbnailSize = CGSizeZero; NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; if (thumbnailSizeValue != nil) { #if SD_MAC thumbnailSize = thumbnailSizeValue.sizeValue; #else thumbnailSize = thumbnailSizeValue.CGSizeValue; #endif } _thumbnailSize = thumbnailSize; BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; } _preserveAspectRatio = preserveAspectRatio; NSUInteger limitBytes = 0; NSNumber *limitBytesValue = options[SDImageCoderDecodeScaleDownLimitBytes]; if (limitBytesValue != nil) { limitBytes = limitBytesValue.unsignedIntegerValue; } _limitBytes = limitBytes; BOOL lazyDecode = NO; // Defaults NO for animated image coder NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; if (lazyDecodeValue != nil) { lazyDecode = lazyDecodeValue.boolValue; } _lazyDecode = lazyDecode; _decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue]; SD_LOCK_INIT(_lock); #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } return self; } - (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished { NSCParameterAssert(_incremental); if (_finished) { return; } _imageData = data; _finished = finished; // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ // Thanks to the author @Nyx0uf // Update the data source, we must pass ALL the data, not just the new bytes CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished); if (_width + _height == 0) { CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL); if (properties) { CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); if (val) CFNumberGetValue(val, kCFNumberLongType, &_height); val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); if (val) CFNumberGetValue(val, kCFNumberLongType, &_width); CFRelease(properties); } } SD_LOCK(_lock); // For animated image progressive decoding because the frame count and duration may be changed. [self scanAndCheckFramesValidWithImageSource:_imageSource]; SD_UNLOCK(_lock); // Scale down to limit bytes if need if (_limitBytes > 0) { // Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage CGSize imageSize = CGSizeMake(_width, _height); CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:_limitBytes bytesPerPixel:4 frameCount:_frameCount]; // Override thumbnail size _thumbnailSize = framePixelSize; _preserveAspectRatio = YES; } } - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options { NSCParameterAssert(_incremental); UIImage *image; if (_width + _height > 0) { // Create the image CGFloat scale = _scale; NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } image = [self.class createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO decodeToHDR:_finished ? _decodeToHDR : NO]; if (image) { image.sd_imageFormat = self.class.imageFormat; } } return image; } #pragma mark - Encode - (BOOL)canEncodeToFormat:(SDImageFormat)format { return (format == self.class.imageFormat); } - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options { if (!image) { return nil; } if (format != self.class.imageFormat) { return nil; } NSArray *frames = [SDImageCoderHelper framesFromAnimatedImage:image]; if (!frames || frames.count == 0) { SDImageFrame *frame = [SDImageFrame frameWithImage:image duration:0]; frames = @[frame]; } return [self encodedDataWithFrames:frames loopCount:image.sd_imageLoopCount format:format options:options]; } - (NSData *)encodedDataWithFrames:(NSArray *)frames loopCount:(NSUInteger)loopCount format:(SDImageFormat)format options:(SDImageCoderOptions *)options { UIImage *image = frames.firstObject.image; // Primary image if (!image) { return nil; } CGImageRef imageRef = image.CGImage; if (!imageRef) { // Earily return, supports CGImage only return nil; } BOOL onlyEncodeOnce = [options[SDImageCoderEncodeFirstFrameOnly] boolValue] || frames.count <= 1; NSMutableData *imageData = [NSMutableData data]; NSString *imageUTType; if (onlyEncodeOnce) { imageUTType = self.class.imageUTType; } else { imageUTType = self.class.animatedImageUTType; } // Create an image destination. Animated Image does not support EXIF image orientation TODO // The `CGImageDestinationCreateWithData` will log a warning when count is 0, use 1 instead. CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, (__bridge CFStringRef)imageUTType, frames.count ?: 1, NULL); if (!imageDestination) { // Handle failure. return nil; } NSMutableDictionary *properties = [NSMutableDictionary dictionary]; #if SD_UIKIT || SD_WATCH CGImagePropertyOrientation exifOrientation = [SDImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation]; #else CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp; #endif if (exifOrientation != kCGImagePropertyOrientationUp) { properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation); } // Encoding Options double compressionQuality = 1; if (options[SDImageCoderEncodeCompressionQuality]) { compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue]; } properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality); CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor]; if (backgroundColor) { properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor); } CGSize maxPixelSize = CGSizeZero; NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize]; if (maxPixelSizeValue != nil) { #if SD_MAC maxPixelSize = maxPixelSizeValue.sizeValue; #else maxPixelSize = maxPixelSizeValue.CGSizeValue; #endif } // HDR Encoding NSUInteger encodeToHDR = 0; if (options[SDImageCoderEncodeToHDR]) { encodeToHDR = [options[SDImageCoderEncodeToHDR] unsignedIntegerValue]; } if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) { if (encodeToHDR == SDImageHDRTypeISOHDR) { properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToISOHDR; } else if (encodeToHDR == SDImageHDRTypeISOGainMap) { properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToISOGainmap; } else { properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToSDR; } } CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef); CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef); CGFloat finalPixelSize = 0; BOOL encodeFullImage = maxPixelSize.width == 0 || maxPixelSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= maxPixelSize.width && pixelHeight <= maxPixelSize.height); if (!encodeFullImage) { // Thumbnail Encoding CGFloat pixelRatio = pixelWidth / pixelHeight; CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height; if (pixelRatio > maxPixelSizeRatio) { finalPixelSize = MAX(maxPixelSize.width, maxPixelSize.width / pixelRatio); } else { finalPixelSize = MAX(maxPixelSize.height, maxPixelSize.height * pixelRatio); } properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize); } NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue]; if (maxFileSize > 0) { properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize); // Remove the quality if we have file size limit properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil; } BOOL embedThumbnail = NO; if (options[SDImageCoderEncodeEmbedThumbnail]) { embedThumbnail = [options[SDImageCoderEncodeEmbedThumbnail] boolValue]; } properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail); if (onlyEncodeOnce) { // for static single images CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties); } else { // for animated images NSDictionary *containerProperties = @{ self.class.dictionaryProperty: @{self.class.loopCountProperty : @(loopCount)} }; // container level properties (applies for `CGImageDestinationSetProperties`, not individual frames) CGImageDestinationSetProperties(imageDestination, (__bridge CFDictionaryRef)containerProperties); for (size_t i = 0; i < frames.count; i++) { SDImageFrame *frame = frames[i]; NSTimeInterval frameDuration = frame.duration; CGImageRef frameImageRef = frame.image.CGImage; properties[self.class.dictionaryProperty] = @{self.class.delayTimeProperty : @(frameDuration)}; CGImageDestinationAddImage(imageDestination, frameImageRef, (__bridge CFDictionaryRef)properties); } } // Finalize the destination. if (CGImageDestinationFinalize(imageDestination) == NO) { // Handle failure. imageData = nil; } CFRelease(imageDestination); // In some beta version, ImageIO `CGImageDestinationFinalize` returns success, but the data buffer is 0 bytes length. if (imageData.length == 0) { return nil; } return [imageData copy]; } #pragma mark - SDAnimatedImageCoder - (nullable instancetype)initWithAnimatedImageData:(nullable NSData *)data options:(nullable SDImageCoderOptions *)options { if (!data) { return nil; } self = [super init]; if (self) { CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, NULL); if (!imageSource) { return nil; } BOOL framesValid = [self scanAndCheckFramesValidWithImageSource:imageSource]; if (!framesValid) { CFRelease(imageSource); return nil; } CGFloat scale = 1; NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } _scale = scale; CGSize thumbnailSize = CGSizeZero; NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; if (thumbnailSizeValue != nil) { #if SD_MAC thumbnailSize = thumbnailSizeValue.sizeValue; #else thumbnailSize = thumbnailSizeValue.CGSizeValue; #endif } _thumbnailSize = thumbnailSize; BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; } _preserveAspectRatio = preserveAspectRatio; NSUInteger limitBytes = 0; NSNumber *limitBytesValue = options[SDImageCoderDecodeScaleDownLimitBytes]; if (limitBytesValue != nil) { limitBytes = limitBytesValue.unsignedIntegerValue; } _limitBytes = limitBytes; // Parse the image properties NSDictionary *properties = (__bridge_transfer NSDictionary *)CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL); _width = [properties[(__bridge NSString *)kCGImagePropertyPixelWidth] doubleValue]; _height = [properties[(__bridge NSString *)kCGImagePropertyPixelHeight] doubleValue]; // Scale down to limit bytes if need if (_limitBytes > 0) { // Hack since ImageIO public API (not CGImageDecompressor/CMPhoto) always return back RGBA8888 CGImage CGSize imageSize = CGSizeMake(_width, _height); CGSize framePixelSize = [SDImageCoderHelper scaledSizeWithImageSize:imageSize limitBytes:_limitBytes bytesPerPixel:4 frameCount:_frameCount]; // Override thumbnail size _thumbnailSize = framePixelSize; _preserveAspectRatio = YES; } BOOL lazyDecode = NO; // Defaults NO for animated image coder NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; if (lazyDecodeValue != nil) { lazyDecode = lazyDecodeValue.boolValue; } _lazyDecode = lazyDecode; _decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue]; _imageSource = imageSource; _imageData = data; #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } return self; } - (BOOL)scanAndCheckFramesValidWithImageSource:(CGImageSourceRef)imageSource { if (!imageSource) { return NO; } NSUInteger frameCount = CGImageSourceGetCount(imageSource); NSUInteger loopCount = [self.class imageLoopCountWithSource:imageSource]; _loopCount = loopCount; NSMutableArray *frames = [NSMutableArray arrayWithCapacity:frameCount]; for (size_t i = 0; i < frameCount; i++) { SDImageIOCoderFrame *frame = [[SDImageIOCoderFrame alloc] init]; frame.index = i; frame.duration = [self.class frameDurationAtIndex:i source:imageSource]; [frames addObject:frame]; } if (frames.count != frameCount) { // frames not match, do not override current value return NO; } _frameCount = frameCount; _frames = [frames copy]; return YES; } - (NSData *)animatedImageData { return _imageData; } - (NSUInteger)animatedImageLoopCount { return _loopCount; } - (NSUInteger)animatedImageFrameCount { return _frameCount; } - (NSTimeInterval)animatedImageDurationAtIndex:(NSUInteger)index { NSTimeInterval duration; // Incremental Animation decoding may update frames when new bytes available // Which should use lock to ensure frame count and frames match, ensure atomic logic if (_incremental) { SD_LOCK(_lock); if (index >= _frames.count) { SD_UNLOCK(_lock); return 0; } duration = _frames[index].duration; SD_UNLOCK(_lock); } else { if (index >= _frames.count) { return 0; } duration = _frames[index].duration; } return duration; } - (UIImage *)animatedImageFrameAtIndex:(NSUInteger)index { UIImage *image; // Incremental Animation decoding may update frames when new bytes available // Which should use lock to ensure frame count and frames match, ensure atomic logic if (_incremental) { SD_LOCK(_lock); if (index >= _frames.count) { SD_UNLOCK(_lock); return nil; } image = [self safeAnimatedImageFrameAtIndex:index]; SD_UNLOCK(_lock); } else { if (index >= _frames.count) { return nil; } image = [self safeAnimatedImageFrameAtIndex:index]; } return image; } - (UIImage *)safeAnimatedImageFrameAtIndex:(NSUInteger)index { UIImage *image = [self.class createFrameAtIndex:index source:_imageSource scale:_scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:YES decodeToHDR:!_incremental || _finished ? _decodeToHDR : NO]; if (!image) { return nil; } image.sd_imageFormat = self.class.imageFormat; return image; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageIOCoder.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDImageCoder.h" /** Built in coder that supports PNG, JPEG, TIFF, includes support for progressive decoding. GIF Also supports static GIF (meaning will only handle the 1st frame). For a full GIF support, we recommend `SDAnimatedImageView` to keep both CPU and memory balanced. HEIC This coder also supports HEIC format because ImageIO supports it natively. But it depends on the system capabilities, so it won't work on all devices, see: https://devstreaming-cdn.apple.com/videos/wwdc/2017/511tj33587vdhds/511/511_working_with_heif_and_hevc.pdf Decode(Software): !Simulator && (iOS 11 || tvOS 11 || macOS 10.13) Decode(Hardware): !Simulator && ((iOS 11 && A9Chip) || (macOS 10.13 && 6thGenerationIntelCPU)) Encode(Software): macOS 10.13 Encode(Hardware): !Simulator && ((iOS 11 && A10FusionChip) || (macOS 10.13 && 6thGenerationIntelCPU)) */ @interface SDImageIOCoder : NSObject @property (nonatomic, class, readonly, nonnull) SDImageIOCoder *sharedCoder; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageIOCoder.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageIOCoder.h" #import "SDImageCoderHelper.h" #import "NSImage+Compatibility.h" #import "UIImage+Metadata.h" #import "SDImageGraphics.h" #import "SDImageIOAnimatedCoderInternal.h" #import #import // Specify File Size for lossy format encoding, like JPEG static NSString * kSDCGImageDestinationRequestedFileSize = @"kCGImageDestinationRequestedFileSize"; // Support Xcode 15 SDK, use raw value instead of symbol static NSString * kSDCGImageDestinationEncodeRequest = @"kCGImageDestinationEncodeRequest"; static NSString * kSDCGImageDestinationEncodeToSDR = @"kCGImageDestinationEncodeToSDR"; static NSString * kSDCGImageDestinationEncodeToISOHDR = @"kCGImageDestinationEncodeToISOHDR"; static NSString * kSDCGImageDestinationEncodeToISOGainmap = @"kCGImageDestinationEncodeToISOGainmap"; @implementation SDImageIOCoder { size_t _width, _height; CGImagePropertyOrientation _orientation; CGImageSourceRef _imageSource; CGFloat _scale; BOOL _finished; BOOL _preserveAspectRatio; CGSize _thumbnailSize; BOOL _lazyDecode; BOOL _decodeToHDR; } #if SD_IMAGEIO_HDR_ENCODING + (void)initialize { if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) { // Use SDK instead of raw value kSDCGImageDestinationEncodeRequest = (__bridge NSString *)kCGImageDestinationEncodeRequest; kSDCGImageDestinationEncodeToSDR = (__bridge NSString *)kCGImageDestinationEncodeToSDR; kSDCGImageDestinationEncodeToISOHDR = (__bridge NSString *)kCGImageDestinationEncodeToISOHDR; kSDCGImageDestinationEncodeToISOGainmap = (__bridge NSString *)kCGImageDestinationEncodeToISOGainmap; } } #endif - (void)dealloc { if (_imageSource) { CFRelease(_imageSource); _imageSource = NULL; } #if SD_UIKIT [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } - (void)didReceiveMemoryWarning:(NSNotification *)notification { if (_imageSource) { CGImageSourceRemoveCacheAtIndex(_imageSource, 0); } } + (instancetype)sharedCoder { static SDImageIOCoder *coder; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ coder = [[SDImageIOCoder alloc] init]; }); return coder; } #pragma mark - Bitmap PDF representation + (UIImage *)createBitmapPDFWithData:(nonnull NSData *)data pageNumber:(NSUInteger)pageNumber targetSize:(CGSize)targetSize preserveAspectRatio:(BOOL)preserveAspectRatio { NSParameterAssert(data); UIImage *image; CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)data); if (!provider) { return nil; } CGPDFDocumentRef document = CGPDFDocumentCreateWithProvider(provider); CGDataProviderRelease(provider); if (!document) { return nil; } // `CGPDFDocumentGetPage` page number is 1-indexed. CGPDFPageRef page = CGPDFDocumentGetPage(document, pageNumber + 1); if (!page) { CGPDFDocumentRelease(document); return nil; } CGPDFBox box = kCGPDFMediaBox; CGRect rect = CGPDFPageGetBoxRect(page, box); CGRect targetRect = rect; if (!CGSizeEqualToSize(targetSize, CGSizeZero)) { targetRect = CGRectMake(0, 0, targetSize.width, targetSize.height); } CGFloat xRatio = targetRect.size.width / rect.size.width; CGFloat yRatio = targetRect.size.height / rect.size.height; CGFloat xScale = preserveAspectRatio ? MIN(xRatio, yRatio) : xRatio; CGFloat yScale = preserveAspectRatio ? MIN(xRatio, yRatio) : yRatio; // `CGPDFPageGetDrawingTransform` will only scale down, but not scale up, so we need calculate the actual scale again CGRect drawRect = CGRectMake( 0, 0, targetRect.size.width / xScale, targetRect.size.height / yScale); CGAffineTransform scaleTransform = CGAffineTransformMakeScale(xScale, yScale); CGAffineTransform transform = CGPDFPageGetDrawingTransform(page, box, drawRect, 0, preserveAspectRatio); SDGraphicsBeginImageContextWithOptions(targetRect.size, NO, 0); CGContextRef context = SDGraphicsGetCurrentContext(); #if SD_UIKIT || SD_WATCH // Core Graphics coordinate system use the bottom-left, UIKit use the flipped one CGContextTranslateCTM(context, 0, targetRect.size.height); CGContextScaleCTM(context, 1, -1); #endif CGContextConcatCTM(context, scaleTransform); CGContextConcatCTM(context, transform); CGContextDrawPDFPage(context, page); image = SDGraphicsGetImageFromCurrentImageContext(); SDGraphicsEndImageContext(); CGPDFDocumentRelease(document); return image; } #pragma mark - Decode - (BOOL)canDecodeFromData:(nullable NSData *)data { return YES; } - (UIImage *)decodedImageWithData:(NSData *)data options:(nullable SDImageCoderOptions *)options { if (!data) { return nil; } CGFloat scale = 1; NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1) ; } CGSize thumbnailSize = CGSizeZero; NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; if (thumbnailSizeValue != nil) { #if SD_MAC thumbnailSize = thumbnailSizeValue.sizeValue; #else thumbnailSize = thumbnailSizeValue.CGSizeValue; #endif } BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; } // Check vector format if ([NSData sd_imageFormatForImageData:data] == SDImageFormatPDF) { // History before iOS 16, ImageIO can decode PDF with rasterization size, but can't ever :( // So, use CoreGraphics to decode PDF (copy code from SDWebImagePDFCoder, may do refactor in the future) UIImage *image; NSUInteger pageNumber = 0; // Still use first page, may added options is user want #if SD_MAC // If don't use thumbnail, prefers the built-in generation of vector image // macOS's `NSImage` supports PDF built-in rendering if (thumbnailSize.width == 0 || thumbnailSize.height == 0) { NSPDFImageRep *imageRep = [[NSPDFImageRep alloc] initWithData:data]; if (imageRep) { imageRep.currentPage = pageNumber; image = [[NSImage alloc] initWithSize:imageRep.size]; [image addRepresentation:imageRep]; image.sd_imageFormat = SDImageFormatPDF; return image; } } #endif image = [self.class createBitmapPDFWithData:data pageNumber:pageNumber targetSize:thumbnailSize preserveAspectRatio:preserveAspectRatio]; image.sd_imageFormat = SDImageFormatPDF; return image; } BOOL lazyDecode = YES; // Defaults YES for static image coder NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; if (lazyDecodeValue != nil) { lazyDecode = lazyDecodeValue.boolValue; } BOOL decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue]; NSString *typeIdentifierHint = options[SDImageCoderDecodeTypeIdentifierHint]; if (!typeIdentifierHint) { // Check file extension and convert to UTI, from: https://stackoverflow.com/questions/1506251/getting-an-uniform-type-identifier-for-a-given-extension NSString *fileExtensionHint = options[SDImageCoderDecodeFileExtensionHint]; if (fileExtensionHint) { typeIdentifierHint = (__bridge_transfer NSString *)UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, (__bridge CFStringRef)fileExtensionHint, kUTTypeImage); // Ignore dynamic UTI if (UTTypeIsDynamic((__bridge CFStringRef)typeIdentifierHint)) { typeIdentifierHint = nil; } } } else if ([typeIdentifierHint isEqual:NSNull.null]) { // Hack if user don't want to imply file extension typeIdentifierHint = nil; } NSDictionary *creatingOptions = nil; if (typeIdentifierHint) { creatingOptions = @{(__bridge NSString *)kCGImageSourceTypeIdentifierHint : typeIdentifierHint}; } CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)data, (__bridge CFDictionaryRef)creatingOptions); if (!source) { // Try again without UTType hint, the call site from user may provide the wrong UTType source = CGImageSourceCreateWithData((__bridge CFDataRef)data, nil); } if (!source) { return nil; } CFStringRef uttype = CGImageSourceGetType(source); SDImageFormat imageFormat = [NSData sd_imageFormatFromUTType:uttype]; UIImage *image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:source scale:scale preserveAspectRatio:preserveAspectRatio thumbnailSize:thumbnailSize lazyDecode:lazyDecode animatedImage:NO decodeToHDR:decodeToHDR]; CFRelease(source); image.sd_imageFormat = imageFormat; return image; } #pragma mark - Progressive Decode - (BOOL)canIncrementalDecodeFromData:(NSData *)data { return [self canDecodeFromData:data]; } - (instancetype)initIncrementalWithOptions:(nullable SDImageCoderOptions *)options { self = [super init]; if (self) { _imageSource = CGImageSourceCreateIncremental(NULL); CGFloat scale = 1; NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } _scale = scale; CGSize thumbnailSize = CGSizeZero; NSValue *thumbnailSizeValue = options[SDImageCoderDecodeThumbnailPixelSize]; if (thumbnailSizeValue != nil) { #if SD_MAC thumbnailSize = thumbnailSizeValue.sizeValue; #else thumbnailSize = thumbnailSizeValue.CGSizeValue; #endif } _thumbnailSize = thumbnailSize; BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = options[SDImageCoderDecodePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; } _preserveAspectRatio = preserveAspectRatio; BOOL lazyDecode = YES; // Defaults YES for static image coder NSNumber *lazyDecodeValue = options[SDImageCoderDecodeUseLazyDecoding]; if (lazyDecodeValue != nil) { lazyDecode = lazyDecodeValue.boolValue; } _lazyDecode = lazyDecode; _decodeToHDR = [options[SDImageCoderDecodeToHDR] boolValue]; #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } return self; } - (void)updateIncrementalData:(NSData *)data finished:(BOOL)finished { if (_finished) { return; } _finished = finished; // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/ // Thanks to the author @Nyx0uf // Update the data source, we must pass ALL the data, not just the new bytes CGImageSourceUpdateData(_imageSource, (__bridge CFDataRef)data, finished); if (_width + _height == 0) { CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(_imageSource, 0, NULL); if (properties) { NSInteger orientationValue = 1; CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight); if (val) CFNumberGetValue(val, kCFNumberLongType, &_height); val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth); if (val) CFNumberGetValue(val, kCFNumberLongType, &_width); val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation); if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue); CFRelease(properties); // When we draw to Core Graphics, we lose orientation information, // which means the image below born of initWithCGIImage will be // oriented incorrectly sometimes. (Unlike the image born of initWithData // in didCompleteWithError.) So save it here and pass it on later. _orientation = (CGImagePropertyOrientation)orientationValue; } } } - (UIImage *)incrementalDecodedImageWithOptions:(SDImageCoderOptions *)options { UIImage *image; if (_width + _height > 0) { // Create the image CGFloat scale = _scale; NSNumber *scaleFactor = options[SDImageCoderDecodeScaleFactor]; if (scaleFactor != nil) { scale = MAX([scaleFactor doubleValue], 1); } image = [SDImageIOAnimatedCoder createFrameAtIndex:0 source:_imageSource scale:scale preserveAspectRatio:_preserveAspectRatio thumbnailSize:_thumbnailSize lazyDecode:_lazyDecode animatedImage:NO decodeToHDR:_finished ? _decodeToHDR : NO]; if (image) { CFStringRef uttype = CGImageSourceGetType(_imageSource); image.sd_imageFormat = [NSData sd_imageFormatFromUTType:uttype]; } } return image; } #pragma mark - Encode - (BOOL)canEncodeToFormat:(SDImageFormat)format { return YES; } - (NSData *)encodedDataWithImage:(UIImage *)image format:(SDImageFormat)format options:(nullable SDImageCoderOptions *)options { if (!image) { return nil; } CGImageRef imageRef = image.CGImage; if (!imageRef) { // Earily return, supports CGImage only return nil; } if (format == SDImageFormatUndefined) { BOOL hasAlpha = [SDImageCoderHelper CGImageContainsAlpha:imageRef]; if (hasAlpha) { format = SDImageFormatPNG; } else { format = SDImageFormatJPEG; } } NSMutableData *imageData = [NSMutableData data]; CFStringRef imageUTType = [NSData sd_UTTypeFromImageFormat:format]; // Create an image destination. CGImageDestinationRef imageDestination = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)imageData, imageUTType, 1, NULL); if (!imageDestination) { // Handle failure. return nil; } NSMutableDictionary *properties = [NSMutableDictionary dictionary]; #if SD_UIKIT || SD_WATCH CGImagePropertyOrientation exifOrientation = [SDImageCoderHelper exifOrientationFromImageOrientation:image.imageOrientation]; #else CGImagePropertyOrientation exifOrientation = kCGImagePropertyOrientationUp; #endif properties[(__bridge NSString *)kCGImagePropertyOrientation] = @(exifOrientation); // Encoding Options double compressionQuality = 1; if (options[SDImageCoderEncodeCompressionQuality]) { compressionQuality = [options[SDImageCoderEncodeCompressionQuality] doubleValue]; } properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = @(compressionQuality); CGColorRef backgroundColor = [options[SDImageCoderEncodeBackgroundColor] CGColor]; if (backgroundColor) { properties[(__bridge NSString *)kCGImageDestinationBackgroundColor] = (__bridge id)(backgroundColor); } CGSize maxPixelSize = CGSizeZero; NSValue *maxPixelSizeValue = options[SDImageCoderEncodeMaxPixelSize]; if (maxPixelSizeValue != nil) { #if SD_MAC maxPixelSize = maxPixelSizeValue.sizeValue; #else maxPixelSize = maxPixelSizeValue.CGSizeValue; #endif } // HDR Encoding NSUInteger encodeToHDR = 0; if (options[SDImageCoderEncodeToHDR]) { encodeToHDR = [options[SDImageCoderEncodeToHDR] unsignedIntegerValue]; } if (@available(macOS 15, iOS 18, tvOS 18, watchOS 11, *)) { if (encodeToHDR == SDImageHDRTypeISOHDR) { properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToISOHDR; } else if (encodeToHDR == SDImageHDRTypeISOGainMap) { properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToISOGainmap; } else { properties[kSDCGImageDestinationEncodeRequest] = kSDCGImageDestinationEncodeToSDR; } } CGFloat pixelWidth = (CGFloat)CGImageGetWidth(imageRef); CGFloat pixelHeight = (CGFloat)CGImageGetHeight(imageRef); CGFloat finalPixelSize = 0; BOOL encodeFullImage = maxPixelSize.width == 0 || maxPixelSize.height == 0 || pixelWidth == 0 || pixelHeight == 0 || (pixelWidth <= maxPixelSize.width && pixelHeight <= maxPixelSize.height); if (!encodeFullImage) { // Thumbnail Encoding CGFloat pixelRatio = pixelWidth / pixelHeight; CGFloat maxPixelSizeRatio = maxPixelSize.width / maxPixelSize.height; if (pixelRatio > maxPixelSizeRatio) { finalPixelSize = MAX(maxPixelSize.width, maxPixelSize.width / pixelRatio); } else { finalPixelSize = MAX(maxPixelSize.height, maxPixelSize.height * pixelRatio); } properties[(__bridge NSString *)kCGImageDestinationImageMaxPixelSize] = @(finalPixelSize); } NSUInteger maxFileSize = [options[SDImageCoderEncodeMaxFileSize] unsignedIntegerValue]; if (maxFileSize > 0) { properties[kSDCGImageDestinationRequestedFileSize] = @(maxFileSize); // Remove the quality if we have file size limit properties[(__bridge NSString *)kCGImageDestinationLossyCompressionQuality] = nil; } BOOL embedThumbnail = NO; if (options[SDImageCoderEncodeEmbedThumbnail]) { embedThumbnail = [options[SDImageCoderEncodeEmbedThumbnail] boolValue]; } properties[(__bridge NSString *)kCGImageDestinationEmbedThumbnail] = @(embedThumbnail); // Add your image to the destination. CGImageDestinationAddImage(imageDestination, imageRef, (__bridge CFDictionaryRef)properties); // Finalize the destination. if (CGImageDestinationFinalize(imageDestination) == NO) { // Handle failure. imageData = nil; } CFRelease(imageDestination); return [imageData copy]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageLoader.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import "SDWebImageDefine.h" #import "SDWebImageOperation.h" #import "SDImageCoder.h" typedef void(^SDImageLoaderProgressBlock)(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL); typedef void(^SDImageLoaderCompletedBlock)(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, BOOL finished); #pragma mark - Context Options /** A `UIImage` instance from `SDWebImageManager` when you specify `SDWebImageRefreshCached` and image cache hit. This can be a hint for image loader to load the image from network and refresh the image from remote location if needed. If the image from remote location does not change, you should call the completion with `SDWebImageErrorCacheNotModified` error. (UIImage) @note If you don't implement `SDWebImageRefreshCached` support, you do not need to care about this context option. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextLoaderCachedImage; #pragma mark - Helper method /** This is the built-in decoding process for image download from network or local file. @note If you want to implement your custom loader with `requestImageWithURL:options:context:progress:completed:` API, but also want to keep compatible with SDWebImage's behavior, you'd better use this to produce image. @param imageData The image data from the network. Should not be nil @param imageURL The image URL from the input. Should not be nil @param options The options arg from the input @param context The context arg from the input @return The decoded image for current image data load from the network */ FOUNDATION_EXPORT UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, SDWebImageOptions options, SDWebImageContext * _Nullable context); /** This is the built-in decoding process for image progressive download from network. It's used when `SDWebImageProgressiveLoad` option is set. (It's not required when your loader does not support progressive image loading) @note If you want to implement your custom loader with `requestImageWithURL:options:context:progress:completed:` API, but also want to keep compatible with SDWebImage's behavior, you'd better use this to produce image. @param imageData The image data from the network so far. Should not be nil @param imageURL The image URL from the input. Should not be nil @param finished Pass NO to specify the download process has not finished. Pass YES when all image data has finished. @param operation The loader operation associated with current progressive download. Why to provide this is because progressive decoding need to store the partial decoded context for each operation to avoid conflict. You should provide the operation from `loadImageWithURL:` method return value. @param options The options arg from the input @param context The context arg from the input @return The decoded progressive image for current image data load from the network */ FOUNDATION_EXPORT UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, BOOL finished, id _Nonnull operation, SDWebImageOptions options, SDWebImageContext * _Nullable context); /** This function get the progressive decoder for current loading operation. If no progressive decoding is happended or decoder is not able to construct, return nil. @return The progressive decoder associated with the loading operation. */ FOUNDATION_EXPORT id _Nullable SDImageLoaderGetProgressiveCoder(id _Nonnull operation); /** This function set the progressive decoder for current loading operation. If no progressive decoding is happended, pass nil. @param progressiveCoder The loading operation to associate the progerssive decoder. */ FOUNDATION_EXPORT void SDImageLoaderSetProgressiveCoder(id _Nonnull operation, id _Nullable progressiveCoder); #pragma mark - SDImageLoader /** This is the protocol to specify custom image load process. You can create your own class to conform this protocol and use as a image loader to load image from network or any available remote resources defined by yourself. If you want to implement custom loader for image download from network or local file, you just need to concentrate on image data download only. After the download finish, call `SDImageLoaderDecodeImageData` or `SDImageLoaderDecodeProgressiveImageData` to use the built-in decoding process and produce image (Remember to call in the global queue). And finally callback the completion block. If you directly get the image instance using some third-party SDKs, such as image directly from Photos framework. You can process the image data and image instance by yourself without that built-in decoding process. And finally callback the completion block. @note It's your responsibility to load the image in the desired global queue(to avoid block main queue). We do not dispatch these method call in a global queue but just from the call queue (For `SDWebImageManager`, it typically call from the main queue). */ @protocol SDImageLoader @required /** Whether current image loader supports to load the provide image URL. This will be checked every time a new image request come for loader. If this return NO, we will mark this image load as failed. If return YES, we will start to call `requestImageWithURL:options:context:progress:completed:`. @param url The image URL to be loaded. @return YES to continue download, NO to stop download. */ - (BOOL)canRequestImageForURL:(nullable NSURL *)url API_DEPRECATED_WITH_REPLACEMENT("canRequestImageForURL:options:context:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); @optional /** Whether current image loader supports to load the provide image URL, with associated options and context. This will be checked every time a new image request come for loader. If this return NO, we will mark this image load as failed. If return YES, we will start to call `requestImageWithURL:options:context:progress:completed:`. @param url The image URL to be loaded. @param options A mask to specify options to use for this request @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. @return YES to continue download, NO to stop download. */ - (BOOL)canRequestImageForURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; @required /** Load the image and image data with the given URL and return the image data. You're responsible for producing the image instance. @param url The URL represent the image. Note this may not be a HTTP URL @param options A mask to specify options to use for this request @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue @param completedBlock A block called when operation has been completed. @return An operation which allow the user to cancel the current request. */ - (nullable id)requestImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDImageLoaderCompletedBlock)completedBlock; /** Whether the error from image loader should be marked indeed un-recoverable or not. If this return YES, failed URL which does not using `SDWebImageRetryFailed` will be blocked into black list. Else not. @param url The URL represent the image. Note this may not be a HTTP URL @param error The URL's loading error, from previous `requestImageWithURL:options:context:progress:completed:` completedBlock's error. @return Whether to block this url or not. Return YES to mark this URL as failed. */ - (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url error:(nonnull NSError *)error API_DEPRECATED_WITH_REPLACEMENT("shouldBlockFailedURLWithURL:error:options:context:", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); @optional /** Whether the error from image loader should be marked indeed un-recoverable or not, with associated options and context. If this return YES, failed URL which does not using `SDWebImageRetryFailed` will be blocked into black list. Else not. @param url The URL represent the image. Note this may not be a HTTP URL @param error The URL's loading error, from previous `requestImageWithURL:options:context:progress:completed:` completedBlock's error. @param options A mask to specify options to use for this request @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. @return Whether to block this url or not. Return YES to mark this URL as failed. */ - (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url error:(nonnull NSError *)error options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageLoader.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageLoader.h" #import "SDWebImageCacheKeyFilter.h" #import "SDImageCodersManager.h" #import "SDImageCoderHelper.h" #import "SDAnimatedImage.h" #import "UIImage+Metadata.h" #import "SDInternalMacros.h" #import "SDImageCacheDefine.h" #import "objc/runtime.h" SDWebImageContextOption const SDWebImageContextLoaderCachedImage = @"loaderCachedImage"; static void * SDImageLoaderProgressiveCoderKey = &SDImageLoaderProgressiveCoderKey; id SDImageLoaderGetProgressiveCoder(id operation) { NSCParameterAssert(operation); return objc_getAssociatedObject(operation, SDImageLoaderProgressiveCoderKey); } void SDImageLoaderSetProgressiveCoder(id operation, id progressiveCoder) { NSCParameterAssert(operation); objc_setAssociatedObject(operation, SDImageLoaderProgressiveCoderKey, progressiveCoder, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } UIImage * _Nullable SDImageLoaderDecodeImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, SDWebImageOptions options, SDWebImageContext * _Nullable context) { NSCParameterAssert(imageData); NSCParameterAssert(imageURL); UIImage *image; id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; NSString *cacheKey; if (cacheKeyFilter) { cacheKey = [cacheKeyFilter cacheKeyForURL:imageURL]; } else { cacheKey = imageURL.absoluteString; } SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey); BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue]; // Grab the image coder id imageCoder = context[SDWebImageContextImageCoder]; if (!imageCoder) { imageCoder = [SDImageCodersManager sharedManager]; } if (!decodeFirstFrame) { // check whether we should use `SDAnimatedImage` Class animatedImageClass = context[SDWebImageContextAnimatedImageClass]; if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)]) { image = [[animatedImageClass alloc] initWithData:imageData scale:scale options:coderOptions]; if (image) { // Preload frames if supported if (options & SDWebImagePreloadAllFrames && [image respondsToSelector:@selector(preloadAllFrames)]) { [((id)image) preloadAllFrames]; } } else { // Check image class matching if (options & SDWebImageMatchAnimatedImageClass) { return nil; } } } } if (!image) { image = [imageCoder decodedImageWithData:imageData options:coderOptions]; } if (image) { SDImageForceDecodePolicy policy = SDImageForceDecodePolicyAutomatic; NSNumber *policyValue = context[SDWebImageContextImageForceDecodePolicy]; if (policyValue != nil) { policy = policyValue.unsignedIntegerValue; } // TODO: Deprecated, remove in SD 6.0... #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage)) { policy = SDImageForceDecodePolicyNever; } #pragma clang diagnostic pop image = [SDImageCoderHelper decodedImageWithImage:image policy:policy]; // assign the decode options, to let manager check whether to re-decode if needed image.sd_decodeOptions = coderOptions; } return image; } UIImage * _Nullable SDImageLoaderDecodeProgressiveImageData(NSData * _Nonnull imageData, NSURL * _Nonnull imageURL, BOOL finished, id _Nonnull operation, SDWebImageOptions options, SDWebImageContext * _Nullable context) { NSCParameterAssert(imageData); NSCParameterAssert(imageURL); NSCParameterAssert(operation); UIImage *image; id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; NSString *cacheKey; if (cacheKeyFilter) { cacheKey = [cacheKeyFilter cacheKeyForURL:imageURL]; } else { cacheKey = imageURL.absoluteString; } SDImageCoderOptions *coderOptions = SDGetDecodeOptionsFromContext(context, options, cacheKey); BOOL decodeFirstFrame = SD_OPTIONS_CONTAINS(options, SDWebImageDecodeFirstFrameOnly); CGFloat scale = [coderOptions[SDImageCoderDecodeScaleFactor] doubleValue]; // Grab the progressive image coder id progressiveCoder = SDImageLoaderGetProgressiveCoder(operation); if (!progressiveCoder) { id imageCoder = context[SDWebImageContextImageCoder]; // Check the progressive coder if provided if ([imageCoder respondsToSelector:@selector(initIncrementalWithOptions:)]) { progressiveCoder = [[[imageCoder class] alloc] initIncrementalWithOptions:coderOptions]; } else { // We need to create a new instance for progressive decoding to avoid conflicts for (id coder in [SDImageCodersManager sharedManager].coders.reverseObjectEnumerator) { if ([coder conformsToProtocol:@protocol(SDProgressiveImageCoder)] && [((id)coder) canIncrementalDecodeFromData:imageData]) { progressiveCoder = [[[coder class] alloc] initIncrementalWithOptions:coderOptions]; break; } } } SDImageLoaderSetProgressiveCoder(operation, progressiveCoder); } // If we can't find any progressive coder, disable progressive download if (!progressiveCoder) { return nil; } [progressiveCoder updateIncrementalData:imageData finished:finished]; if (!decodeFirstFrame) { // check whether we should use `SDAnimatedImage` Class animatedImageClass = context[SDWebImageContextAnimatedImageClass]; if ([animatedImageClass isSubclassOfClass:[UIImage class]] && [animatedImageClass conformsToProtocol:@protocol(SDAnimatedImage)] && [progressiveCoder respondsToSelector:@selector(animatedImageFrameAtIndex:)]) { image = [[animatedImageClass alloc] initWithAnimatedCoder:(id)progressiveCoder scale:scale]; if (image) { // Progressive decoding does not preload frames } else { // Check image class matching if (options & SDWebImageMatchAnimatedImageClass) { return nil; } } } } if (!image) { image = [progressiveCoder incrementalDecodedImageWithOptions:coderOptions]; } if (image) { SDImageForceDecodePolicy policy = SDImageForceDecodePolicyAutomatic; NSNumber *policyValue = context[SDWebImageContextImageForceDecodePolicy]; if (policyValue != nil) { policy = policyValue.unsignedIntegerValue; } // TODO: Deprecated, remove in SD 6.0... #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" if (SD_OPTIONS_CONTAINS(options, SDWebImageAvoidDecodeImage)) { policy = SDImageForceDecodePolicyNever; } #pragma clang diagnostic pop image = [SDImageCoderHelper decodedImageWithImage:image policy:policy]; // assign the decode options, to let manager check whether to re-decode if needed image.sd_decodeOptions = coderOptions; // mark the image as progressive (completed one are not mark as progressive) image.sd_isIncremental = !finished; } return image; } ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageLoadersManager.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageLoader.h" /** A loaders manager to manage multiple loaders */ @interface SDImageLoadersManager : NSObject /** Returns the global shared loaders manager instance. By default we will set [`SDWebImageDownloader.sharedDownloader`] into the loaders array. */ @property (nonatomic, class, readonly, nonnull) SDImageLoadersManager *sharedManager; /** All image loaders in manager. The loaders array is a priority queue, which means the later added loader will have the highest priority */ @property (nonatomic, copy, nullable) NSArray>* loaders; /** Add a new image loader to the end of loaders array. Which has the highest priority. @param loader loader */ - (void)addLoader:(nonnull id)loader; /** Remove an image loader in the loaders array. @param loader loader */ - (void)removeLoader:(nonnull id)loader; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageLoadersManager.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageLoadersManager.h" #import "SDWebImageDownloader.h" #import "SDInternalMacros.h" @interface SDImageLoadersManager () @property (nonatomic, strong, nonnull) NSMutableArray> *imageLoaders; @end @implementation SDImageLoadersManager { SD_LOCK_DECLARE(_loadersLock); } + (SDImageLoadersManager *)sharedManager { static dispatch_once_t onceToken; static SDImageLoadersManager *manager; dispatch_once(&onceToken, ^{ manager = [[SDImageLoadersManager alloc] init]; }); return manager; } - (instancetype)init { self = [super init]; if (self) { // initialize with default image loaders _imageLoaders = [NSMutableArray arrayWithObject:[SDWebImageDownloader sharedDownloader]]; SD_LOCK_INIT(_loadersLock); } return self; } - (NSArray> *)loaders { SD_LOCK(_loadersLock); NSArray>* loaders = [_imageLoaders copy]; SD_UNLOCK(_loadersLock); return loaders; } - (void)setLoaders:(NSArray> *)loaders { SD_LOCK(_loadersLock); [_imageLoaders removeAllObjects]; if (loaders.count) { [_imageLoaders addObjectsFromArray:loaders]; } SD_UNLOCK(_loadersLock); } #pragma mark - Loader Property - (void)addLoader:(id)loader { if (![loader conformsToProtocol:@protocol(SDImageLoader)]) { return; } SD_LOCK(_loadersLock); [_imageLoaders addObject:loader]; SD_UNLOCK(_loadersLock); } - (void)removeLoader:(id)loader { if (![loader conformsToProtocol:@protocol(SDImageLoader)]) { return; } SD_LOCK(_loadersLock); [_imageLoaders removeObject:loader]; SD_UNLOCK(_loadersLock); } #pragma mark - SDImageLoader - (BOOL)canRequestImageForURL:(nullable NSURL *)url { return [self canRequestImageForURL:url options:0 context:nil]; } - (BOOL)canRequestImageForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context { NSArray> *loaders = self.loaders; for (id loader in loaders.reverseObjectEnumerator) { if ([loader respondsToSelector:@selector(canRequestImageForURL:options:context:)]) { if ([loader canRequestImageForURL:url options:options context:context]) { return YES; } } else { if ([loader canRequestImageForURL:url]) { return YES; } } } return NO; } - (id)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock { if (!url) { return nil; } NSArray> *loaders = self.loaders; for (id loader in loaders.reverseObjectEnumerator) { if ([loader canRequestImageForURL:url]) { return [loader requestImageWithURL:url options:options context:context progress:progressBlock completed:completedBlock]; } } return nil; } - (BOOL)shouldBlockFailedURLWithURL:(NSURL *)url error:(NSError *)error { NSArray> *loaders = self.loaders; for (id loader in loaders.reverseObjectEnumerator) { if ([loader canRequestImageForURL:url]) { return [loader shouldBlockFailedURLWithURL:url error:error]; } } return NO; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageTransformer.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import "UIImage+Transform.h" /** Return the transformed cache key which applied with specify transformerKey. @param key The original cache key @param transformerKey The transformer key from the transformer @return The transformed cache key */ FOUNDATION_EXPORT NSString * _Nullable SDTransformedKeyForKey(NSString * _Nullable key, NSString * _Nonnull transformerKey); /** Return the thumbnailed cache key which applied with specify thumbnailSize and preserveAspectRatio control. @param key The original cache key @param thumbnailPixelSize The thumbnail pixel size @param preserveAspectRatio The preserve aspect ratio option @return The thumbnailed cache key @note If you have both transformer and thumbnail applied for image, call `SDThumbnailedKeyForKey` firstly and then with `SDTransformedKeyForKey`.` */ FOUNDATION_EXPORT NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullable key, CGSize thumbnailPixelSize, BOOL preserveAspectRatio); /** A transformer protocol to transform the image load from cache or from download. You can provide transformer to cache and manager (Through the `transformer` property or context option `SDWebImageContextImageTransformer`). From v5.20, the transformer class also can be used on animated image frame post-transform logic, see `SDAnimatedImageView`. @note The transform process is called from a global queue in order to not to block the main queue. */ @protocol SDImageTransformer @optional /** Defaults to YES if you don't implements this method. We keep some metadata like Image Format (`sd_imageFormat`)/ Animated Loop Count (`sd_imageLoopCount`) via associated object on UIImage instance. When transformer generate a new UIImage instance, in most cases you still want to keep these information. So this is what for during the image loading pipeline. If the value is YES, we will keep and override the metadata **After you generate the UIImage** If the value is NO, we will not touch the UIImage metadata and it's controlled by you during the generation. Read `UIImage+Medata.h` and pick the metadata you want for the new generated UIImage. */ @property (nonatomic, assign, readonly) BOOL preserveImageMetadata; @required /** For each transformer, it must contains its cache key to used to store the image cache or query from the cache. This key will be appened after the original cache key generated by URL or from user. Which means, the cache should match what your transformer logic do. The same `input image` + `transformer key`, should always generate the same `output image`. @return The cache key to appended after the original cache key. Should not be nil. */ @property (nonatomic, copy, readonly, nonnull) NSString *transformerKey; /** Transform the image to another image. @param image The image to be transformed @param key The cache key associated to the image. This arg is a hint for image source, not always useful and should be nullable. In the future we will remove this arg. @return The transformed image, or nil if transform failed */ - (nullable UIImage *)transformedImageWithImage:(nonnull UIImage *)image forKey:(nonnull NSString *)key API_DEPRECATED("The key arg will be removed in the future. Update your code and don't rely on that.", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); @end #pragma mark - Pipeline /** Pipeline transformer. Which you can bind multiple transformers together to let the image to be transformed one by one in order and generate the final image. @note Because transformers are lightweight, if you want to append or arrange transformers, create another pipeline transformer instead. This class is considered as immutable. */ @interface SDImagePipelineTransformer : NSObject /// For pipeline transformer, this property is readonly and always return NO. We handle each transformer's choice inside implementation @property (nonatomic, assign, readonly) BOOL preserveImageMetadata; /** All transformers in pipeline */ @property (nonatomic, copy, readonly, nonnull) NSArray> *transformers; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithTransformers:(nonnull NSArray> *)transformers; @end #pragma mark - Base /// This is the base class for our built-in concrete transformers. You should not use this class directlly, use cconcrete subclass (like `SDImageRoundCornerTransformer`) instead. @interface SDImageBaseTransformer : NSObject /// For concrete transformer, this property is readwrite and defaults to YES. You can choose whether to preserve image metadata **After you generate the UIImage** @property (nonatomic, assign, readwrite) BOOL preserveImageMetadata; @end // There are some built-in transformers based on the `UIImage+Transformer` category to provide the common image geometry, image blending and image effect process. Those transform are useful for static image only but you can create your own to support animated image as well. // Because transformers are lightweight, these class are considered as immutable. #pragma mark - Image Geometry /** Image round corner transformer */ @interface SDImageRoundCornerTransformer: SDImageBaseTransformer /** The radius of each corner oval. Values larger than half the rectangle's width or height are clamped appropriately to half the width or height. */ @property (nonatomic, assign, readonly) CGFloat cornerRadius; /** A bitmask value that identifies the corners that you want rounded. You can use this parameter to round only a subset of the corners of the rectangle. */ @property (nonatomic, assign, readonly) SDRectCorner corners; /** The inset border line width. Values larger than half the rectangle's width or height are clamped appropriately to half the width or height. */ @property (nonatomic, assign, readonly) CGFloat borderWidth; /** The border stroke color. nil means clear color. */ @property (nonatomic, strong, readonly, nullable) UIColor *borderColor; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithRadius:(CGFloat)cornerRadius corners:(SDRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(nullable UIColor *)borderColor; @end /** Image resizing transformer */ @interface SDImageResizingTransformer : SDImageBaseTransformer /** The new size to be resized, values should be positive. */ @property (nonatomic, assign, readonly) CGSize size; /** The scale mode for image content. */ @property (nonatomic, assign, readonly) SDImageScaleMode scaleMode; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithSize:(CGSize)size scaleMode:(SDImageScaleMode)scaleMode; @end /** Image cropping transformer */ @interface SDImageCroppingTransformer : SDImageBaseTransformer /** Image's inner rect. */ @property (nonatomic, assign, readonly) CGRect rect; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithRect:(CGRect)rect; @end /** Image flipping transformer */ @interface SDImageFlippingTransformer : SDImageBaseTransformer /** YES to flip the image horizontally. ⇋ */ @property (nonatomic, assign, readonly) BOOL horizontal; /** YES to flip the image vertically. ⥯ */ @property (nonatomic, assign, readonly) BOOL vertical; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical; @end /** Image rotation transformer */ @interface SDImageRotationTransformer : SDImageBaseTransformer /** Rotated radians in counterclockwise.⟲ */ @property (nonatomic, assign, readonly) CGFloat angle; /** YES: new image's size is extend to fit all content. NO: image's size will not change, content may be clipped. */ @property (nonatomic, assign, readonly) BOOL fitSize; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithAngle:(CGFloat)angle fitSize:(BOOL)fitSize; @end #pragma mark - Image Blending /** Image tint color transformer */ @interface SDImageTintTransformer : SDImageBaseTransformer /** The tint color. */ @property (nonatomic, strong, readonly, nonnull) UIColor *tintColor; /// The blend mode, defaults to `sourceIn` if you use the initializer without blend mode @property (nonatomic, assign, readonly) CGBlendMode blendMode; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor; + (nonnull instancetype)transformerWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode; @end #pragma mark - Image Effect /** Image blur effect transformer */ @interface SDImageBlurTransformer : SDImageBaseTransformer /** The radius of the blur in points, 0 means no blur effect. */ @property (nonatomic, assign, readonly) CGFloat blurRadius; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithRadius:(CGFloat)blurRadius; @end #if SD_UIKIT || SD_MAC /** Core Image filter transformer */ @interface SDImageFilterTransformer: SDImageBaseTransformer /** The CIFilter to be applied to the image. */ @property (nonatomic, strong, readonly, nonnull) CIFilter *filter; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; + (nonnull instancetype)transformerWithFilter:(nonnull CIFilter *)filter; @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDImageTransformer.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageTransformer.h" #import "UIColor+SDHexString.h" #import "SDAssociatedObject.h" #if SD_UIKIT || SD_MAC #import #endif // Separator for different transformerKey, for example, `image.png` |> flip(YES,NO) |> rotate(pi/4,YES) => 'image-SDImageFlippingTransformer(1,0)-SDImageRotationTransformer(0.78539816339,1).png' static NSString * const SDImageTransformerKeySeparator = @"-"; NSString * _Nullable SDTransformedKeyForKey(NSString * _Nullable key, NSString * _Nonnull transformerKey) { if (!key || !transformerKey) { return nil; } // Find the file extension NSURL *keyURL = [NSURL URLWithString:key]; NSString *ext = keyURL ? keyURL.pathExtension : key.pathExtension; if (ext.length > 0) { // For non-file URL if (keyURL && !keyURL.isFileURL) { // keep anything except path (like URL query) NSURLComponents *component = [NSURLComponents componentsWithURL:keyURL resolvingAgainstBaseURL:NO]; component.path = [[[component.path.stringByDeletingPathExtension stringByAppendingString:SDImageTransformerKeySeparator] stringByAppendingString:transformerKey] stringByAppendingPathExtension:ext]; return component.URL.absoluteString; } else { // file URL return [[[key.stringByDeletingPathExtension stringByAppendingString:SDImageTransformerKeySeparator] stringByAppendingString:transformerKey] stringByAppendingPathExtension:ext]; } } else { return [[key stringByAppendingString:SDImageTransformerKeySeparator] stringByAppendingString:transformerKey]; } } NSString * _Nullable SDThumbnailedKeyForKey(NSString * _Nullable key, CGSize thumbnailPixelSize, BOOL preserveAspectRatio) { NSString *thumbnailKey = [NSString stringWithFormat:@"Thumbnail({%f,%f},%d)", thumbnailPixelSize.width, thumbnailPixelSize.height, preserveAspectRatio]; return SDTransformedKeyForKey(key, thumbnailKey); } @interface SDImagePipelineTransformer () @property (nonatomic, copy, readwrite, nonnull) NSArray> *transformers; @property (nonatomic, copy, readwrite) NSString *transformerKey; @end @implementation SDImagePipelineTransformer + (instancetype)transformerWithTransformers:(NSArray> *)transformers { SDImagePipelineTransformer *transformer = [SDImagePipelineTransformer new]; transformer.transformers = transformers; transformer.transformerKey = [[self class] cacheKeyForTransformers:transformers]; return transformer; } + (NSString *)cacheKeyForTransformers:(NSArray> *)transformers { if (transformers.count == 0) { return @""; } NSMutableArray *cacheKeys = [NSMutableArray arrayWithCapacity:transformers.count]; [transformers enumerateObjectsUsingBlock:^(id _Nonnull transformer, NSUInteger idx, BOOL * _Nonnull stop) { NSString *cacheKey = transformer.transformerKey; [cacheKeys addObject:cacheKey]; }]; return [cacheKeys componentsJoinedByString:SDImageTransformerKeySeparator]; } - (BOOL)preserveImageMetadata { return NO; // We handle this logic inside `transformedImageWithImage` below } - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { if (!image) { return nil; } UIImage *transformedImage = image; for (id transformer in self.transformers) { UIImage *newImage = [transformer transformedImageWithImage:transformedImage forKey:key]; // Handle each transformer's preserveImageMetadata choice BOOL preserveImageMetadata = YES; if ([transformer respondsToSelector:@selector(preserveImageMetadata)]) { preserveImageMetadata = transformer.preserveImageMetadata; } if (preserveImageMetadata) { SDImageCopyAssociatedObject(transformedImage, newImage); } transformedImage = newImage; } return transformedImage; } @end @implementation SDImageBaseTransformer - (instancetype)init { self = [super init]; if (self) { _preserveImageMetadata = YES; } return self; } - (NSString *)transformerKey { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"For `SDImageBaseTransformer` subclass, you must override %@ method", NSStringFromSelector(_cmd)] userInfo:nil]; } - (nullable UIImage *)transformedImageWithImage:(nonnull UIImage *)image forKey:(nonnull NSString *)key { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[NSString stringWithFormat:@"For `SDImageBaseTransformer` subclass, you must override %@ method", NSStringFromSelector(_cmd)] userInfo:nil]; } @end @interface SDImageRoundCornerTransformer () @property (nonatomic, assign) CGFloat cornerRadius; @property (nonatomic, assign) SDRectCorner corners; @property (nonatomic, assign) CGFloat borderWidth; @property (nonatomic, strong, nullable) UIColor *borderColor; @end @implementation SDImageRoundCornerTransformer + (instancetype)transformerWithRadius:(CGFloat)cornerRadius corners:(SDRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(UIColor *)borderColor { SDImageRoundCornerTransformer *transformer = [SDImageRoundCornerTransformer new]; transformer.cornerRadius = cornerRadius; transformer.corners = corners; transformer.borderWidth = borderWidth; transformer.borderColor = borderColor; return transformer; } - (NSString *)transformerKey { return [NSString stringWithFormat:@"SDImageRoundCornerTransformer(%f,%lu,%f,%@)", self.cornerRadius, (unsigned long)self.corners, self.borderWidth, self.borderColor.sd_hexString]; } - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { if (!image) { return nil; } return [image sd_roundedCornerImageWithRadius:self.cornerRadius corners:self.corners borderWidth:self.borderWidth borderColor:self.borderColor]; } @end @interface SDImageResizingTransformer () @property (nonatomic, assign) CGSize size; @property (nonatomic, assign) SDImageScaleMode scaleMode; @end @implementation SDImageResizingTransformer + (instancetype)transformerWithSize:(CGSize)size scaleMode:(SDImageScaleMode)scaleMode { SDImageResizingTransformer *transformer = [SDImageResizingTransformer new]; transformer.size = size; transformer.scaleMode = scaleMode; return transformer; } - (NSString *)transformerKey { CGSize size = self.size; return [NSString stringWithFormat:@"SDImageResizingTransformer({%f,%f},%lu)", size.width, size.height, (unsigned long)self.scaleMode]; } - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { if (!image) { return nil; } return [image sd_resizedImageWithSize:self.size scaleMode:self.scaleMode]; } @end @interface SDImageCroppingTransformer () @property (nonatomic, assign) CGRect rect; @end @implementation SDImageCroppingTransformer + (instancetype)transformerWithRect:(CGRect)rect { SDImageCroppingTransformer *transformer = [SDImageCroppingTransformer new]; transformer.rect = rect; return transformer; } - (NSString *)transformerKey { CGRect rect = self.rect; return [NSString stringWithFormat:@"SDImageCroppingTransformer({%f,%f,%f,%f})", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height]; } - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { if (!image) { return nil; } return [image sd_croppedImageWithRect:self.rect]; } @end @interface SDImageFlippingTransformer () @property (nonatomic, assign) BOOL horizontal; @property (nonatomic, assign) BOOL vertical; @end @implementation SDImageFlippingTransformer + (instancetype)transformerWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical { SDImageFlippingTransformer *transformer = [SDImageFlippingTransformer new]; transformer.horizontal = horizontal; transformer.vertical = vertical; return transformer; } - (NSString *)transformerKey { return [NSString stringWithFormat:@"SDImageFlippingTransformer(%d,%d)", self.horizontal, self.vertical]; } - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { if (!image) { return nil; } return [image sd_flippedImageWithHorizontal:self.horizontal vertical:self.vertical]; } @end @interface SDImageRotationTransformer () @property (nonatomic, assign) CGFloat angle; @property (nonatomic, assign) BOOL fitSize; @end @implementation SDImageRotationTransformer + (instancetype)transformerWithAngle:(CGFloat)angle fitSize:(BOOL)fitSize { SDImageRotationTransformer *transformer = [SDImageRotationTransformer new]; transformer.angle = angle; transformer.fitSize = fitSize; return transformer; } - (NSString *)transformerKey { return [NSString stringWithFormat:@"SDImageRotationTransformer(%f,%d)", self.angle, self.fitSize]; } - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { if (!image) { return nil; } return [image sd_rotatedImageWithAngle:self.angle fitSize:self.fitSize]; } @end #pragma mark - Image Blending @interface SDImageTintTransformer () @property (nonatomic, strong, nonnull) UIColor *tintColor; @property (nonatomic, assign) CGBlendMode blendMode; @end @implementation SDImageTintTransformer + (instancetype)transformerWithColor:(UIColor *)tintColor { return [self transformerWithColor:tintColor blendMode:kCGBlendModeSourceIn]; } + (instancetype)transformerWithColor:(UIColor *)tintColor blendMode:(CGBlendMode)blendMode { SDImageTintTransformer *transformer = [SDImageTintTransformer new]; transformer.tintColor = tintColor; transformer.blendMode = blendMode; return transformer; } - (NSString *)transformerKey { return [NSString stringWithFormat:@"SDImageTintTransformer(%@,%d)", self.tintColor.sd_hexString, self.blendMode]; } - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { if (!image) { return nil; } return [image sd_tintedImageWithColor:self.tintColor]; } @end #pragma mark - Image Effect @interface SDImageBlurTransformer () @property (nonatomic, assign) CGFloat blurRadius; @end @implementation SDImageBlurTransformer + (instancetype)transformerWithRadius:(CGFloat)blurRadius { SDImageBlurTransformer *transformer = [SDImageBlurTransformer new]; transformer.blurRadius = blurRadius; return transformer; } - (NSString *)transformerKey { return [NSString stringWithFormat:@"SDImageBlurTransformer(%f)", self.blurRadius]; } - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { if (!image) { return nil; } return [image sd_blurredImageWithRadius:self.blurRadius]; } @end #if SD_UIKIT || SD_MAC @interface SDImageFilterTransformer () @property (nonatomic, strong, nonnull) CIFilter *filter; @end @implementation SDImageFilterTransformer + (instancetype)transformerWithFilter:(CIFilter *)filter { SDImageFilterTransformer *transformer = [SDImageFilterTransformer new]; transformer.filter = filter; return transformer; } - (NSString *)transformerKey { return [NSString stringWithFormat:@"SDImageFilterTransformer(%@)", self.filter.name]; } - (UIImage *)transformedImageWithImage:(UIImage *)image forKey:(NSString *)key { if (!image) { return nil; } return [image sd_filteredImageWithFilter:self.filter]; } @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDMemoryCache.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" @class SDImageCacheConfig; /** A protocol to allow custom memory cache used in SDImageCache. */ @protocol SDMemoryCache @required /** Create a new memory cache instance with the specify cache config. You can check `maxMemoryCost` and `maxMemoryCount` used for memory cache. @param config The cache config to be used to create the cache. @return The new memory cache instance. */ - (nonnull instancetype)initWithConfig:(nonnull SDImageCacheConfig *)config; /** Returns the value associated with a given key. @param key An object identifying the value. If nil, just return nil. @return The value associated with key, or nil if no value is associated with key. */ - (nullable id)objectForKey:(nonnull id)key; /** Sets the value of the specified key in the cache (0 cost). @param object The object to be stored in the cache. If nil, it calls `removeObjectForKey:`. @param key The key with which to associate the value. If nil, this method has no effect. @discussion Unlike an NSMutableDictionary object, a cache does not copy the key objects that are put into it. */ - (void)setObject:(nullable id)object forKey:(nonnull id)key; /** Sets the value of the specified key in the cache, and associates the key-value pair with the specified cost. @param object The object to store in the cache. If nil, it calls `removeObjectForKey`. @param key The key with which to associate the value. If nil, this method has no effect. @param cost The cost with which to associate the key-value pair. @discussion Unlike an NSMutableDictionary object, a cache does not copy the key objects that are put into it. */ - (void)setObject:(nullable id)object forKey:(nonnull id)key cost:(NSUInteger)cost; /** Removes the value of the specified key in the cache. @param key The key identifying the value to be removed. If nil, this method has no effect. */ - (void)removeObjectForKey:(nonnull id)key; /** Empties the cache immediately. */ - (void)removeAllObjects; @end /** A memory cache which auto purge the cache on memory warning and support weak cache. */ @interface SDMemoryCache : NSCache @property (nonatomic, strong, nonnull, readonly) SDImageCacheConfig *config; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDMemoryCache.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDMemoryCache.h" #import "SDImageCacheConfig.h" #import "UIImage+MemoryCacheCost.h" #import "SDInternalMacros.h" static void * SDMemoryCacheContext = &SDMemoryCacheContext; @interface SDMemoryCache () { #if SD_UIKIT SD_LOCK_DECLARE(_weakCacheLock); // a lock to keep the access to `weakCache` thread-safe #endif } @property (nonatomic, strong, nullable) SDImageCacheConfig *config; #if SD_UIKIT @property (nonatomic, strong, nonnull) NSMapTable *weakCache; // strong-weak cache #endif @end @implementation SDMemoryCache - (void)dealloc { [_config removeObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCost)) context:SDMemoryCacheContext]; [_config removeObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCount)) context:SDMemoryCacheContext]; #if SD_UIKIT [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif self.delegate = nil; } - (instancetype)init { self = [super init]; if (self) { _config = [[SDImageCacheConfig alloc] init]; [self commonInit]; } return self; } - (instancetype)initWithConfig:(SDImageCacheConfig *)config { self = [super init]; if (self) { _config = config; [self commonInit]; } return self; } - (void)commonInit { SDImageCacheConfig *config = self.config; self.totalCostLimit = config.maxMemoryCost; self.countLimit = config.maxMemoryCount; [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCost)) options:0 context:SDMemoryCacheContext]; [config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxMemoryCount)) options:0 context:SDMemoryCacheContext]; #if SD_UIKIT self.weakCache = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0]; SD_LOCK_INIT(_weakCacheLock); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } // Current this seems no use on macOS (macOS use virtual memory and do not clear cache when memory warning). So we only override on iOS/tvOS platform. #if SD_UIKIT - (void)didReceiveMemoryWarning:(NSNotification *)notification { // Only remove cache, but keep weak cache [super removeAllObjects]; } // `setObject:forKey:` just call this with 0 cost. Override this is enough - (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g { [super setObject:obj forKey:key cost:g]; if (!self.config.shouldUseWeakMemoryCache) { return; } if (key && obj) { // Store weak cache SD_LOCK(_weakCacheLock); [self.weakCache setObject:obj forKey:key]; SD_UNLOCK(_weakCacheLock); } } - (id)objectForKey:(id)key { id obj = [super objectForKey:key]; if (!self.config.shouldUseWeakMemoryCache) { return obj; } if (key && !obj) { // Check weak cache SD_LOCK(_weakCacheLock); obj = [self.weakCache objectForKey:key]; SD_UNLOCK(_weakCacheLock); if (obj) { // Sync cache NSUInteger cost = 0; if ([obj isKindOfClass:[UIImage class]]) { cost = [(UIImage *)obj sd_memoryCost]; } [super setObject:obj forKey:key cost:cost]; } } return obj; } - (void)removeObjectForKey:(id)key { [super removeObjectForKey:key]; if (!self.config.shouldUseWeakMemoryCache) { return; } if (key) { // Remove weak cache SD_LOCK(_weakCacheLock); [self.weakCache removeObjectForKey:key]; SD_UNLOCK(_weakCacheLock); } } - (void)removeAllObjects { [super removeAllObjects]; if (!self.config.shouldUseWeakMemoryCache) { return; } // Manually remove should also remove weak cache SD_LOCK(_weakCacheLock); [self.weakCache removeAllObjects]; SD_UNLOCK(_weakCacheLock); } #endif #pragma mark - KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == SDMemoryCacheContext) { if ([keyPath isEqualToString:NSStringFromSelector(@selector(maxMemoryCost))]) { self.totalCostLimit = self.config.maxMemoryCost; } else if ([keyPath isEqualToString:NSStringFromSelector(@selector(maxMemoryCount))]) { self.countLimit = self.config.maxMemoryCount; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheKeyFilter.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" typedef NSString * _Nullable(^SDWebImageCacheKeyFilterBlock)(NSURL * _Nonnull url); /** This is the protocol for cache key filter. We can use a block to specify the cache key filter. But Using protocol can make this extensible, and allow Swift user to use it easily instead of using `@convention(block)` to store a block into context options. */ @protocol SDWebImageCacheKeyFilter - (nullable NSString *)cacheKeyForURL:(nonnull NSURL *)url; @end /** A cache key filter class with block. */ @interface SDWebImageCacheKeyFilter : NSObject - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; - (nonnull instancetype)initWithBlock:(nonnull SDWebImageCacheKeyFilterBlock)block; + (nonnull instancetype)cacheKeyFilterWithBlock:(nonnull SDWebImageCacheKeyFilterBlock)block; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheKeyFilter.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCacheKeyFilter.h" @interface SDWebImageCacheKeyFilter () @property (nonatomic, copy, nonnull) SDWebImageCacheKeyFilterBlock block; @end @implementation SDWebImageCacheKeyFilter - (instancetype)initWithBlock:(SDWebImageCacheKeyFilterBlock)block { self = [super init]; if (self) { self.block = block; } return self; } + (instancetype)cacheKeyFilterWithBlock:(SDWebImageCacheKeyFilterBlock)block { SDWebImageCacheKeyFilter *cacheKeyFilter = [[SDWebImageCacheKeyFilter alloc] initWithBlock:block]; return cacheKeyFilter; } - (NSString *)cacheKeyForURL:(NSURL *)url { if (!self.block) { return nil; } return self.block(url); } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheSerializer.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" typedef NSData * _Nullable(^SDWebImageCacheSerializerBlock)(UIImage * _Nonnull image, NSData * _Nullable data, NSURL * _Nullable imageURL); /** This is the protocol for cache serializer. We can use a block to specify the cache serializer. But Using protocol can make this extensible, and allow Swift user to use it easily instead of using `@convention(block)` to store a block into context options. */ @protocol SDWebImageCacheSerializer /// Provide the image data associated to the image and store to disk cache /// @param image The loaded image /// @param data The original loaded image data. May be nil when image is transformed (UIImage.sd_isTransformed = YES) /// @param imageURL The image URL - (nullable NSData *)cacheDataWithImage:(nonnull UIImage *)image originalData:(nullable NSData *)data imageURL:(nullable NSURL *)imageURL; @end /** A cache serializer class with block. */ @interface SDWebImageCacheSerializer : NSObject - (nonnull instancetype)initWithBlock:(nonnull SDWebImageCacheSerializerBlock)block; + (nonnull instancetype)cacheSerializerWithBlock:(nonnull SDWebImageCacheSerializerBlock)block; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageCacheSerializer.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCacheSerializer.h" @interface SDWebImageCacheSerializer () @property (nonatomic, copy, nonnull) SDWebImageCacheSerializerBlock block; @end @implementation SDWebImageCacheSerializer - (instancetype)initWithBlock:(SDWebImageCacheSerializerBlock)block { self = [super init]; if (self) { self.block = block; } return self; } + (instancetype)cacheSerializerWithBlock:(SDWebImageCacheSerializerBlock)block { SDWebImageCacheSerializer *cacheSerializer = [[SDWebImageCacheSerializer alloc] initWithBlock:block]; return cacheSerializer; } - (NSData *)cacheDataWithImage:(UIImage *)image originalData:(NSData *)data imageURL:(nullable NSURL *)imageURL { if (!self.block) { return nil; } return self.block(image, data, imageURL); } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageCompat.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Jamie Pinkham * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #ifdef __OBJC_GC__ #error SDWebImage does not support Objective-C Garbage Collection #endif // Seems like TARGET_OS_MAC is always defined (on all platforms). // To determine if we are running on macOS, use TARGET_OS_OSX in Xcode 8 #if TARGET_OS_OSX #define SD_MAC 1 #else #define SD_MAC 0 #endif #if TARGET_OS_IOS #define SD_IOS 1 #else #define SD_IOS 0 #endif #if TARGET_OS_TV #define SD_TV 1 #else #define SD_TV 0 #endif #if TARGET_OS_WATCH #define SD_WATCH 1 #else #define SD_WATCH 0 #endif // Supports Xcode 14 to suppress warning #ifdef TARGET_OS_VISION #if TARGET_OS_VISION #define SD_VISION 1 #endif #endif // iOS/tvOS/visionOS are very similar, UIKit exists on both platforms // Note: watchOS also has UIKit, but it's very limited #if SD_IOS || SD_TV || SD_VISION #define SD_UIKIT 1 #else #define SD_UIKIT 0 #endif #if SD_MAC #import #ifndef UIImage #define UIImage NSImage #endif #ifndef UIImageView #define UIImageView NSImageView #endif #ifndef UIView #define UIView NSView #endif #ifndef UIColor #define UIColor NSColor #endif #else #if SD_UIKIT #import #endif #if SD_WATCH #import #ifndef UIView #define UIView WKInterfaceObject #endif #ifndef UIImageView #define UIImageView WKInterfaceImage #endif #endif #endif #ifndef NS_ENUM #define NS_ENUM(_type, _name) enum _name : _type _name; enum _name : _type #endif #ifndef NS_OPTIONS #define NS_OPTIONS(_type, _name) enum _name : _type _name; enum _name : _type #endif #ifndef dispatch_main_async_safe #define dispatch_main_async_safe(block)\ if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == dispatch_queue_get_label(dispatch_get_main_queue())) {\ block();\ } else {\ dispatch_async(dispatch_get_main_queue(), block);\ } #pragma clang deprecated(dispatch_main_async_safe, "Use SDCallbackQueue instead") #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageCompat.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if !__has_feature(objc_arc) #error SDWebImage is ARC only. Either turn on ARC for the project or use -fobjc-arc flag #endif #if !OS_OBJECT_USE_OBJC #error SDWebImage need ARC for dispatch object #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" typedef void(^SDWebImageNoParamsBlock)(void); /// Image Loading context option typedef NSString * SDWebImageContextOption NS_EXTENSIBLE_STRING_ENUM; typedef NSDictionary SDWebImageContext; typedef NSMutableDictionary SDWebImageMutableContext; #pragma mark - Image scale /** Return the image scale factor for the specify key, supports file name and url key. This is the built-in way to check the scale factor when we have no context about it. Because scale factor is not stored in image data (It's typically from filename). However, you can also provide custom scale factor as well, see `SDWebImageContextImageScaleFactor`. @param key The image cache key @return The scale factor for image */ FOUNDATION_EXPORT CGFloat SDImageScaleFactorForKey(NSString * _Nullable key); /** Scale the image with the scale factor for the specify key. If no need to scale, return the original image. This works for `UIImage`(UIKit) or `NSImage`(AppKit). And this function also preserve the associated value in `UIImage+Metadata.h`. @note This is actually a convenience function, which firstly call `SDImageScaleFactorForKey` and then call `SDScaledImageForScaleFactor`, kept for backward compatibility. @param key The image cache key @param image The image @return The scaled image */ FOUNDATION_EXPORT UIImage * _Nullable SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image); /** Scale the image with the scale factor. If no need to scale, return the original image. This works for `UIImage`(UIKit) or `NSImage`(AppKit). And this function also preserve the associated value in `UIImage+Metadata.h`. @param scale The image scale factor @param image The image @return The scaled image */ FOUNDATION_EXPORT UIImage * _Nullable SDScaledImageForScaleFactor(CGFloat scale, UIImage * _Nullable image); #pragma mark - WebCache Options /// WebCache options typedef NS_OPTIONS(NSUInteger, SDWebImageOptions) { /** * By default, when a URL fail to be downloaded, the URL is blacklisted so the library won't keep trying. * This flag disable this blacklisting. */ SDWebImageRetryFailed = 1 << 0, /** * By default, image downloads are started during UI interactions, this flags disable this feature, * leading to delayed download on UIScrollView deceleration for instance. */ SDWebImageLowPriority = 1 << 1, /** * This flag enables progressive download, the image is displayed progressively during download as a browser would do. * By default, the image is only displayed once completely downloaded. */ SDWebImageProgressiveLoad = 1 << 2, /** * Even if the image is cached, respect the HTTP response cache control, and refresh the image from remote location if needed. * The disk caching will be handled by NSURLCache instead of SDWebImage leading to slight performance degradation. * This option helps deal with images changing behind the same request URL, e.g. Facebook graph api profile pics. * If a cached image is refreshed, the completion block is called once with the cached image and again with the final image. * * Use this flag only if you can't make your URLs static with embedded cache busting parameter. */ SDWebImageRefreshCached = 1 << 3, /** * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for * extra time in background to let the request finish. If the background task expires the operation will be cancelled. */ SDWebImageContinueInBackground = 1 << 4, /** * Handles cookies stored in NSHTTPCookieStore by setting * NSMutableURLRequest.HTTPShouldHandleCookies = YES; */ SDWebImageHandleCookies = 1 << 5, /** * Enable to allow untrusted SSL certificates. * Useful for testing purposes. Use with caution in production. */ SDWebImageAllowInvalidSSLCertificates = 1 << 6, /** * By default, images are loaded in the order in which they were queued. This flag moves them to * the front of the queue. */ SDWebImageHighPriority = 1 << 7, /** * By default, placeholder images are loaded while the image is loading. This flag will delay the loading * of the placeholder image until after the image has finished loading. * @note This is used to treate placeholder as an **Error Placeholder** but not **Loading Placeholder** by defaults. if the image loading is cancelled or error, the placeholder will be always set. * @note Therefore, if you want both **Error Placeholder** and **Loading Placeholder** exist, use `SDWebImageAvoidAutoSetImage` to manually set the two placeholders and final loaded image by your hand depends on loading result. * @note This options is UI level options, has no usage on ImageManager or other components. */ SDWebImageDelayPlaceholder = 1 << 8, /** * We usually don't apply transform on animated images as most transformers could not manage animated images. * Use this flag to transform them anyway. */ SDWebImageTransformAnimatedImage = 1 << 9, /** * By default, image is added to the imageView after download. But in some cases, we want to * have the hand before setting the image (apply a filter or add it with cross-fade animation for instance) * Use this flag if you want to manually set the image in the completion when success * @note This options is UI level options, has no usage on ImageManager or other components. */ SDWebImageAvoidAutoSetImage = 1 << 10, /** * By default, images are decoded respecting their original size. * This flag will scale down the images to a size compatible with the constrained memory of devices. * To control the limit memory bytes, check `SDImageCoderHelper.defaultScaleDownLimitBytes` (Defaults to 60MB on iOS) * (from 5.16.0) This will actually translate to use context option `SDWebImageContextImageScaleDownLimitBytes`, which check and calculate the thumbnail pixel size occupied small than limit bytes (including animated image) * (from 5.5.0) This flags effect the progressive and animated images as well * @note If you need detail controls, it's better to use context option `imageScaleDownBytes` instead. * @warning This does not effect the cache key. So which means, this will effect the global cache even next time you query without this option. Pay attention when you use this on global options (It's always recommended to use request-level option for different pipeline) */ SDWebImageScaleDownLargeImages = 1 << 11, /** * By default, we do not query image data when the image is already cached in memory. This mask can force to query image data at the same time. However, this query is asynchronously unless you specify `SDWebImageQueryMemoryDataSync` */ SDWebImageQueryMemoryData = 1 << 12, /** * By default, when you only specify `SDWebImageQueryMemoryData`, we query the memory image data asynchronously. Combined this mask as well to query the memory image data synchronously. * @note Query data synchronously is not recommend, unless you want to ensure the image is loaded in the same runloop to avoid flashing during cell reusing. */ SDWebImageQueryMemoryDataSync = 1 << 13, /** * By default, when the memory cache miss, we query the disk cache asynchronously. This mask can force to query disk cache (when memory cache miss) synchronously. * @note These 3 query options can be combined together. For the full list about these masks combination, see wiki page. * @note Query data synchronously is not recommend, unless you want to ensure the image is loaded in the same runloop to avoid flashing during cell reusing. */ SDWebImageQueryDiskDataSync = 1 << 14, /** * By default, when the cache missed, the image is load from the loader. This flag can prevent this to load from cache only. */ SDWebImageFromCacheOnly = 1 << 15, /** * By default, we query the cache before the image is load from the loader. This flag can prevent this to load from loader only. */ SDWebImageFromLoaderOnly = 1 << 16, /** * By default, when you use `SDWebImageTransition` to do some view transition after the image load finished, this transition is only applied for image when the callback from manager is asynchronous (from network, or disk cache query) * This mask can force to apply view transition for any cases, like memory cache query, or sync disk cache query. * @note This options is UI level options, has no usage on ImageManager or other components. */ SDWebImageForceTransition = 1 << 17, /** * By default, we will decode the image in the background during cache query and download from the network. This can help to improve performance because when rendering image on the screen, it need to be firstly decoded. But this happen on the main queue by Core Animation. * However, this process may increase the memory usage as well. If you are experiencing an issue due to excessive memory consumption, This flag can prevent decode the image. * @note 5.14.0 introduce `SDImageCoderDecodeUseLazyDecoding`, use that for better control from codec, instead of post-processing. Which acts the similar like this option but works for SDAnimatedImage as well (this one does not) * @deprecated Deprecated in v5.17.0, if you don't want force-decode, pass [.imageForceDecodePolicy] = SDImageForceDecodePolicy.never.rawValue in context option */ SDWebImageAvoidDecodeImage API_DEPRECATED("Use SDWebImageContextImageForceDecodePolicy instead", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0)) = 1 << 18, /** * By default, we decode the animated image. This flag can force decode the first frame only and produce the static image. */ SDWebImageDecodeFirstFrameOnly = 1 << 19, /** * By default, for `SDAnimatedImage`, we decode the animated image frame during rendering to reduce memory usage. However, you can specify to preload all frames into memory to reduce CPU usage when the animated image is shared by lots of imageViews. * This will actually trigger `preloadAllAnimatedImageFrames` in the background queue(Disk Cache & Download only). */ SDWebImagePreloadAllFrames = 1 << 20, /** * By default, when you use `SDWebImageContextAnimatedImageClass` context option (like using `SDAnimatedImageView` which designed to use `SDAnimatedImage`), we may still use `UIImage` when the memory cache hit, or image decoder is not available to produce one exactlly matching your custom class as a fallback solution. * Using this option, can ensure we always callback image with your provided class. If failed to produce one, a error with code `SDWebImageErrorBadImageData` will been used. * Note this options is not compatible with `SDWebImageDecodeFirstFrameOnly`, which always produce a UIImage/NSImage. */ SDWebImageMatchAnimatedImageClass = 1 << 21, /** * By default, when we load the image from network, the image will be written to the cache (memory and disk, controlled by your `storeCacheType` context option) * This maybe an asynchronously operation and the final `SDInternalCompletionBlock` callback does not guarantee the disk cache written is finished and may cause logic error. (For example, you modify the disk data just in completion block, however, the disk cache is not ready) * If you need to process with the disk cache in the completion block, you should use this option to ensure the disk cache already been written when callback. * Note if you use this when using the custom cache serializer, or using the transformer, we will also wait until the output image data written is finished. */ SDWebImageWaitStoreCache = 1 << 22, /** * We usually don't apply transform on vector images, because vector images supports dynamically changing to any size, rasterize to a fixed size will loss details. To modify vector images, you can process the vector data at runtime (such as modifying PDF tag / SVG element). * Use this flag to transform them anyway. */ SDWebImageTransformVectorImage = 1 << 23, /** * By defaults, when you use UI-level category like `sd_setImageWithURL:` on UIImageView, it will cancel the loading image requests. * However, some users may choose to not cancel the loading image requests and always start new pipeline. * Use this flag to disable automatic cancel behavior. * @note This options is UI level options, has no usage on ImageManager or other components. */ SDWebImageAvoidAutoCancelImage = 1 << 24, /** * By defaults, for `SDWebImageTransition`, we just submit to UI transition and inmeediatelly callback the final `completedBlock` (`SDExternalCompletionBlock/SDInternalCompletionBlock`). * This may cause un-wanted behavior if you do another transition inside `completedBlock`, because the previous transition is still runnning and un-cancellable, which mass-up the UI status. * For this case, you can pass this option, we will delay the final callback, until your transition end. So when you inside `completedBlock`, no any transition is running on image view and safe to submit new transition. * @note Currently we do not support `pausable/cancellable` transition. But possible in the future by using the https://developer.apple.com/documentation/uikit/uiviewpropertyanimator. * @note If you have complicated transition animation, just use `SDWebImageManager` and do UI state management by yourself, do not use the top-level API (`sd_setImageWithURL:`) */ SDWebImageWaitTransition = 1 << 25, }; #pragma mark - Manager Context Options /** A String to be used as the operation key for view category to store the image load operation. This is used for view instance which supports different image loading process. If nil, will use the class name as operation key. (NSString *) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextSetImageOperationKey; /** A SDWebImageManager instance to control the image download and cache process using in UIImageView+WebCache category and likes. If not provided, use the shared manager (SDWebImageManager *) @deprecated Deprecated in the future. This context options can be replaced by other context option control like `.imageCache`, `.imageLoader`, `.imageTransformer` (See below), which already matches all the properties in SDWebImageManager. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCustomManager API_DEPRECATED("Use individual context option like .imageCache, .imageLoader and .imageTransformer instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); /** A `SDCallbackQueue` instance which controls the `Cache`/`Manager`/`Loader`'s callback queue for their completionBlock. This is useful for user who call these 3 components in non-main queue and want to avoid callback in main queue. @note For UI callback (`sd_setImageWithURL`), we will still use main queue to dispatch, means if you specify a global queue, it will enqueue from the global queue to main queue. @note This does not effect the components' working queue (for example, `Cache` still query disk on internal ioQueue, `Loader` still do network on URLSessionConfiguration.delegateQueue), change those config if you need. Defaults to nil. Which means main queue. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCallbackQueue; /** A id instance which conforms to `SDImageCache` protocol. It's used to override the image manager's cache during the image loading pipeline. In other word, if you just want to specify a custom cache during image loading, you don't need to re-create a dummy SDWebImageManager instance with the cache. If not provided, use the image manager's cache (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageCache; /** A id instance which conforms to `SDImageLoader` protocol. It's used to override the image manager's loader during the image loading pipeline. In other word, if you just want to specify a custom loader during image loading, you don't need to re-create a dummy SDWebImageManager instance with the loader. If not provided, use the image manager's cache (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageLoader; /** A id instance which conforms to `SDImageCoder` protocol. It's used to override the default image coder for image decoding(including progressive) and encoding during the image loading process. If you use this context option, we will not always use `SDImageCodersManager.shared` to loop through all registered coders and find the suitable one. Instead, we will arbitrarily use the exact provided coder without extra checking (We may not call `canDecodeFromData:`). @note This is only useful for cases which you can ensure the loading url matches your coder, or you find it's too hard to write a common coder which can used for generic usage. This will bind the loading url with the coder logic, which is not always a good design, but possible. (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageCoder; /** A id instance which conforms `SDImageTransformer` protocol. It's used for image transform after the image load finished and store the transformed image to cache. If you provide one, it will ignore the `transformer` in manager and use provided one instead. If you pass NSNull, the transformer feature will be disabled. (id) @note When this value is used, we will trigger image transform after downloaded, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageTransformer; #pragma mark - Force Decode Options /** A NSNumber instance which store the`SDImageForceDecodePolicy` enum. This is used to control how current image loading should force-decode the decoded image (CGImage, typically). See more what's force-decode means in `SDImageForceDecodePolicy` comment. Defaults to `SDImageForceDecodePolicyAutomatic`, which will detect the input CGImage's metadata, and only force-decode if the input CGImage can not directly render on screen (need extra CoreAnimation Copied Image and increase RAM usage). @note If you want to always the force-decode for this image request, pass `SDImageForceDecodePolicyAlways`, for example, some WebP images which does not created by ImageIO. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageForceDecodePolicy; #pragma mark - Image Decoder Context Options /** A Dictionary (SDImageCoderOptions) value, which pass the extra decoding options to the SDImageCoder. Introduced in SDWebImage 5.14.0 You can pass additional decoding related options to the decoder, extensible and control by you. And pay attention this dictionary may be retained by decoded image via `UIImage.sd_decodeOptions` This context option replace the deprecated `SDImageCoderWebImageContext`, which may cause retain cycle (cache -> image -> options -> context -> cache) @note There are already individual options below like `.imageScaleFactor`, `.imagePreserveAspectRatio`, each of individual options will override the same filed for this dictionary. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageDecodeOptions; /** A CGFloat raw value which specify the image scale factor. The number should be greater than or equal to 1.0. If not provide or the number is invalid, we will use the cache key to specify the scale factor. (NSNumber) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageScaleFactor; /** A Boolean value indicating whether to keep the original aspect ratio when generating thumbnail images (or bitmap images from vector format). Defaults to YES. (NSNumber) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImagePreserveAspectRatio; /** A CGSize raw value indicating whether or not to generate the thumbnail images (or bitmap images from vector format). When this value is provided, the decoder will generate a thumbnail image which pixel size is smaller than or equal to (depends the `.imagePreserveAspectRatio`) the value size. @note When you pass `.preserveAspectRatio == NO`, the thumbnail image is stretched to match each dimension. When `.preserveAspectRatio == YES`, the thumbnail image's width is limited to pixel size's width, the thumbnail image's height is limited to pixel size's height. For common cases, you can just pass a square size to limit both. Defaults to CGSizeZero, which means no thumbnail generation at all. (NSValue) @note When this value is used, we will trigger thumbnail decoding for url, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageThumbnailPixelSize; /** A NSString value (UTI) indicating the source image's file extension. Example: "public.jpeg-2000", "com.nikon.raw-image", "public.tiff" Some image file format share the same data structure but has different tag explanation, like TIFF and NEF/SRW, see https://en.wikipedia.org/wiki/TIFF Changing the file extension cause the different image result. The coder (like ImageIO) may use file extension to choose the correct parser @note If you don't provide this option, we will use the `URL.path` as file extension to calculate the UTI hint @note If you really don't want any hint which effect the image result, pass `NSNull.null` instead */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageTypeIdentifierHint; /** A NSUInteger value to provide the limit bytes during decoding. This can help to avoid OOM on large frame count animated image or large pixel static image when you don't know how much RAM it occupied before decoding The decoder will do these logic based on limit bytes: 1. Get the total frame count (static image means 1) 2. Calculate the `framePixelSize` width/height to `sqrt(limitBytes / frameCount / bytesPerPixel)`, keeping aspect ratio (at least 1x1) 3. If the `framePixelSize < originalImagePixelSize`, then do thumbnail decoding (see `SDImageCoderDecodeThumbnailPixelSize`) use the `framePixelSize` and `preseveAspectRatio = YES` 4. Else, use the full pixel decoding (small than limit bytes) 5. Whatever result, this does not effect the animated/static behavior of image. So even if you set `limitBytes = 1 && frameCount = 100`, we will stll create animated image with each frame `1x1` pixel size. @note This option has higher priority than `.imageThumbnailPixelSize` @warning This does not effect the cache key. So which means, this will effect the global cache even next time you query without this option. Pay attention when you use this on global options (It's always recommended to use request-level option for different pipeline) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageScaleDownLimitBytes; /** A Boolean value (NSNumber) to provide converting to HDR during decoding. Currently if number is 0, use SDR, else use HDR. But we may extend this option to use `NSUInteger` in the future (means, think this options as int number, but not actual boolean) @note Supported by iOS 17 and above when using ImageIO coder (third-party coder can support lower firmware) Defaults to @(NO), decoder will automatically convert SDR. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageDecodeToHDR; #pragma mark - Cache Context Options /** A Dictionary (SDImageCoderOptions) value, which pass the extra encode options to the SDImageCoder. Introduced in SDWebImage 5.15.0 You can pass encode options like `compressionQuality`, `maxFileSize`, `maxPixelSize` to control the encoding related thing, this is used inside `SDImageCache` during store logic. @note For developer who use custom cache protocol (not SDImageCache instance), they need to upgrade and use these options for encoding. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextImageEncodeOptions; /** A SDImageCacheType raw value which specify the source of cache to query. Specify `SDImageCacheTypeDisk` to query from disk cache only; `SDImageCacheTypeMemory` to query from memory only. And `SDImageCacheTypeAll` to query from both memory cache and disk cache. Specify `SDImageCacheTypeNone` is invalid and totally ignore the cache query. If not provide or the value is invalid, we will use `SDImageCacheTypeAll`. (NSNumber) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextQueryCacheType; /** A SDImageCacheType raw value which specify the store cache type when the image has just been downloaded and will be stored to the cache. Specify `SDImageCacheTypeNone` to disable cache storage; `SDImageCacheTypeDisk` to store in disk cache only; `SDImageCacheTypeMemory` to store in memory only. And `SDImageCacheTypeAll` to store in both memory cache and disk cache. If you use image transformer feature, this actually apply for the transformed image, but not the original image itself. Use `SDWebImageContextOriginalStoreCacheType` if you want to control the original image's store cache type at the same time. If not provide or the value is invalid, we will use `SDImageCacheTypeAll`. (NSNumber) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextStoreCacheType; /** The same behavior like `SDWebImageContextQueryCacheType`, but control the query cache type for the original image when you use image transformer feature. This allows the detail control of cache query for these two images. For example, if you want to query the transformed image from both memory/disk cache, query the original image from disk cache only, use `[.queryCacheType : .all, .originalQueryCacheType : .disk]` If not provide or the value is invalid, we will use `SDImageCacheTypeDisk`, which query the original full image data from disk cache after transformed image cache miss. This is suitable for most common cases to avoid re-downloading the full data for different transform variants. (NSNumber) @note Which means, if you set this value to not be `.none`, we will query the original image from cache, then do transform with transformer, instead of actual downloading, which can save bandwidth usage. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextOriginalQueryCacheType; /** The same behavior like `SDWebImageContextStoreCacheType`, but control the store cache type for the original image when you use image transformer feature. This allows the detail control of cache storage for these two images. For example, if you want to store the transformed image into both memory/disk cache, store the original image into disk cache only, use `[.storeCacheType : .all, .originalStoreCacheType : .disk]` If not provide or the value is invalid, we will use `SDImageCacheTypeDisk`, which store the original full image data into disk cache after storing the transformed image. This is suitable for most common cases to avoid re-downloading the full data for different transform variants. (NSNumber) @note This only store the original image, if you want to use the original image without downloading in next query, specify `SDWebImageContextOriginalQueryCacheType` as well. */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextOriginalStoreCacheType; /** A id instance which conforms to `SDImageCache` protocol. It's used to control the cache for original image when using the transformer. If you provide one, the original image (full size image) will query and write from that cache instance instead, the transformed image will query and write from the default `SDWebImageContextImageCache` instead. (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextOriginalImageCache; /** A Class object which the instance is a `UIImage/NSImage` subclass and adopt `SDAnimatedImage` protocol. We will call `initWithData:scale:options:` to create the instance (or `initWithAnimatedCoder:scale:` when using progressive download) . If the instance create failed, fallback to normal `UIImage/NSImage`. This can be used to improve animated images rendering performance (especially memory usage on big animated images) with `SDAnimatedImageView` (Class). */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextAnimatedImageClass; #pragma mark - Download Context Options /** A id instance to modify the image download request. It's used for downloader to modify the original request from URL and options. If you provide one, it will ignore the `requestModifier` in downloader and use provided one instead. (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextDownloadRequestModifier; /** A id instance to modify the image download response. It's used for downloader to modify the original response from URL and options. If you provide one, it will ignore the `responseModifier` in downloader and use provided one instead. (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextDownloadResponseModifier; /** A id instance to decrypt the image download data. This can be used for image data decryption, such as Base64 encoded image. If you provide one, it will ignore the `decryptor` in downloader and use provided one instead. (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextDownloadDecryptor; /** A id instance to convert an URL into a cache key. It's used when manager need cache key to use image cache. If you provide one, it will ignore the `cacheKeyFilter` in manager and use provided one instead. (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCacheKeyFilter; /** A id instance to convert the decoded image, the source downloaded data, to the actual data. It's used for manager to store image to the disk cache. If you provide one, it will ignore the `cacheSerializer` in manager and use provided one instead. (id) */ FOUNDATION_EXPORT SDWebImageContextOption _Nonnull const SDWebImageContextCacheSerializer; ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDefine.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageDefine.h" #import "UIImage+Metadata.h" #import "NSImage+Compatibility.h" #import "SDAnimatedImage.h" #import "SDAssociatedObject.h" #pragma mark - Image scale static inline NSArray * _Nonnull SDImageScaleFactors(void) { return @[@2, @3]; } inline CGFloat SDImageScaleFactorForKey(NSString * _Nullable key) { CGFloat scale = 1; if (!key) { return scale; } // Now all OS supports retina display scale system { // a@2x.png -> 8 if (key.length >= 8) { // Fast check BOOL isURL = [key hasPrefix:@"http://"] || [key hasPrefix:@"https://"]; for (NSNumber *scaleFactor in SDImageScaleFactors()) { // @2x. for file name and normal url NSString *fileScale = [NSString stringWithFormat:@"@%@x.", scaleFactor]; if ([key containsString:fileScale]) { scale = scaleFactor.doubleValue; return scale; } if (isURL) { // %402x. for url encode NSString *urlScale = [NSString stringWithFormat:@"%%40%@x.", scaleFactor]; if ([key containsString:urlScale]) { scale = scaleFactor.doubleValue; return scale; } } } } } return scale; } inline UIImage * _Nullable SDScaledImageForKey(NSString * _Nullable key, UIImage * _Nullable image) { if (!image) { return nil; } CGFloat scale = SDImageScaleFactorForKey(key); return SDScaledImageForScaleFactor(scale, image); } inline UIImage * _Nullable SDScaledImageForScaleFactor(CGFloat scale, UIImage * _Nullable image) { if (!image) { return nil; } if (scale <= 1) { return image; } if (scale == image.scale) { return image; } UIImage *scaledImage; // Check SDAnimatedImage support for shortcut if ([image.class conformsToProtocol:@protocol(SDAnimatedImage)]) { if ([image respondsToSelector:@selector(animatedCoder)]) { id coder = [(id)image animatedCoder]; if (coder) { scaledImage = [[image.class alloc] initWithAnimatedCoder:coder scale:scale]; } } else { // Some class impl does not support `animatedCoder`, keep for compatibility NSData *data = [(id)image animatedImageData]; if (data) { scaledImage = [[image.class alloc] initWithData:data scale:scale]; } } } if (scaledImage) { SDImageCopyAssociatedObject(image, scaledImage); return scaledImage; } if (image.sd_isAnimated) { UIImage *animatedImage; #if SD_UIKIT || SD_WATCH // `UIAnimatedImage` images share the same size and scale. NSArray *images = image.images; NSMutableArray *scaledImages = [NSMutableArray arrayWithCapacity:images.count]; for (UIImage *tempImage in images) { UIImage *tempScaledImage = [[UIImage alloc] initWithCGImage:tempImage.CGImage scale:scale orientation:tempImage.imageOrientation]; [scaledImages addObject:tempScaledImage]; } animatedImage = [UIImage animatedImageWithImages:scaledImages duration:image.duration]; #else // Animated GIF for `NSImage` need to grab `NSBitmapImageRep`; NSRect imageRect = NSMakeRect(0, 0, image.size.width, image.size.height); NSImageRep *imageRep = [image bestRepresentationForRect:imageRect context:nil hints:nil]; NSBitmapImageRep *bitmapImageRep; if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { bitmapImageRep = (NSBitmapImageRep *)imageRep; } if (bitmapImageRep) { NSSize size = NSMakeSize(image.size.width / scale, image.size.height / scale); animatedImage = [[NSImage alloc] initWithSize:size]; bitmapImageRep.size = size; [animatedImage addRepresentation:bitmapImageRep]; } #endif scaledImage = animatedImage; } else { #if SD_UIKIT || SD_WATCH scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:image.imageOrientation]; #else scaledImage = [[UIImage alloc] initWithCGImage:image.CGImage scale:scale orientation:kCGImagePropertyOrientationUp]; #endif } if (scaledImage) { SDImageCopyAssociatedObject(image, scaledImage); return scaledImage; } return nil; } #pragma mark - Context option SDWebImageContextOption const SDWebImageContextSetImageOperationKey = @"setImageOperationKey"; SDWebImageContextOption const SDWebImageContextCustomManager = @"customManager"; SDWebImageContextOption const SDWebImageContextCallbackQueue = @"callbackQueue"; SDWebImageContextOption const SDWebImageContextImageCache = @"imageCache"; SDWebImageContextOption const SDWebImageContextImageLoader = @"imageLoader"; SDWebImageContextOption const SDWebImageContextImageCoder = @"imageCoder"; SDWebImageContextOption const SDWebImageContextImageTransformer = @"imageTransformer"; SDWebImageContextOption const SDWebImageContextImageForceDecodePolicy = @"imageForceDecodePolicy"; SDWebImageContextOption const SDWebImageContextImageDecodeOptions = @"imageDecodeOptions"; SDWebImageContextOption const SDWebImageContextImageScaleFactor = @"imageScaleFactor"; SDWebImageContextOption const SDWebImageContextImagePreserveAspectRatio = @"imagePreserveAspectRatio"; SDWebImageContextOption const SDWebImageContextImageThumbnailPixelSize = @"imageThumbnailPixelSize"; SDWebImageContextOption const SDWebImageContextImageTypeIdentifierHint = @"imageTypeIdentifierHint"; SDWebImageContextOption const SDWebImageContextImageScaleDownLimitBytes = @"imageScaleDownLimitBytes"; SDWebImageContextOption const SDWebImageContextImageDecodeToHDR = @"imageDecodeToHDR"; SDWebImageContextOption const SDWebImageContextImageEncodeOptions = @"imageEncodeOptions"; SDWebImageContextOption const SDWebImageContextQueryCacheType = @"queryCacheType"; SDWebImageContextOption const SDWebImageContextStoreCacheType = @"storeCacheType"; SDWebImageContextOption const SDWebImageContextOriginalQueryCacheType = @"originalQueryCacheType"; SDWebImageContextOption const SDWebImageContextOriginalStoreCacheType = @"originalStoreCacheType"; SDWebImageContextOption const SDWebImageContextOriginalImageCache = @"originalImageCache"; SDWebImageContextOption const SDWebImageContextAnimatedImageClass = @"animatedImageClass"; SDWebImageContextOption const SDWebImageContextDownloadRequestModifier = @"downloadRequestModifier"; SDWebImageContextOption const SDWebImageContextDownloadResponseModifier = @"downloadResponseModifier"; SDWebImageContextOption const SDWebImageContextDownloadDecryptor = @"downloadDecryptor"; SDWebImageContextOption const SDWebImageContextCacheKeyFilter = @"cacheKeyFilter"; SDWebImageContextOption const SDWebImageContextCacheSerializer = @"cacheSerializer"; ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" #import "SDWebImageDefine.h" #import "SDWebImageOperation.h" #import "SDWebImageDownloaderConfig.h" #import "SDWebImageDownloaderRequestModifier.h" #import "SDWebImageDownloaderResponseModifier.h" #import "SDWebImageDownloaderDecryptor.h" #import "SDImageLoader.h" /// Downloader options typedef NS_OPTIONS(NSUInteger, SDWebImageDownloaderOptions) { /** * Put the download in the low queue priority and task priority. */ SDWebImageDownloaderLowPriority = 1 << 0, /** * This flag enables progressive download, the image is displayed progressively during download as a browser would do. */ SDWebImageDownloaderProgressiveLoad = 1 << 1, /** * By default, request prevent the use of NSURLCache. With this flag, NSURLCache * is used with default policies. */ SDWebImageDownloaderUseNSURLCache = 1 << 2, /** * Call completion block with nil image/imageData if the image was read from NSURLCache * And the error code is `SDWebImageErrorCacheNotModified` * This flag should be combined with `SDWebImageDownloaderUseNSURLCache`. */ SDWebImageDownloaderIgnoreCachedResponse = 1 << 3, /** * In iOS 4+, continue the download of the image if the app goes to background. This is achieved by asking the system for * extra time in background to let the request finish. If the background task expires the operation will be cancelled. */ SDWebImageDownloaderContinueInBackground = 1 << 4, /** * Handles cookies stored in NSHTTPCookieStore by setting * NSMutableURLRequest.HTTPShouldHandleCookies = YES; */ SDWebImageDownloaderHandleCookies = 1 << 5, /** * Enable to allow untrusted SSL certificates. * Useful for testing purposes. Use with caution in production. */ SDWebImageDownloaderAllowInvalidSSLCertificates = 1 << 6, /** * Put the download in the high queue priority and task priority. */ SDWebImageDownloaderHighPriority = 1 << 7, /** * By default, images are decoded respecting their original size. On iOS, this flag will scale down the * images to a size compatible with the constrained memory of devices. * This flag take no effect if `SDWebImageDownloaderAvoidDecodeImage` is set. And it will be ignored if `SDWebImageDownloaderProgressiveLoad` is set. */ SDWebImageDownloaderScaleDownLargeImages = 1 << 8, /** * By default, we will decode the image in the background during cache query and download from the network. This can help to improve performance because when rendering image on the screen, it need to be firstly decoded. But this happen on the main queue by Core Animation. * However, this process may increase the memory usage as well. If you are experiencing a issue due to excessive memory consumption, This flag can prevent decode the image. * @note 5.14.0 introduce `SDImageCoderDecodeUseLazyDecoding`, use that for better control from codec, instead of post-processing. Which acts the similar like this option but works for SDAnimatedImage as well (this one does not) * @deprecated Deprecated in v5.17.0, if you don't want force-decode, pass [.imageForceDecodePolicy] = SDImageForceDecodePolicy.never.rawValue in context option */ SDWebImageDownloaderAvoidDecodeImage API_DEPRECATED("Use SDWebImageContextImageForceDecodePolicy instead", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0)) = 1 << 9, /** * By default, we decode the animated image. This flag can force decode the first frame only and produce the static image. */ SDWebImageDownloaderDecodeFirstFrameOnly = 1 << 10, /** * By default, for `SDAnimatedImage`, we decode the animated image frame during rendering to reduce memory usage. This flag actually trigger `preloadAllAnimatedImageFrames = YES` after image load from network */ SDWebImageDownloaderPreloadAllFrames = 1 << 11, /** * By default, when you use `SDWebImageContextAnimatedImageClass` context option (like using `SDAnimatedImageView` which designed to use `SDAnimatedImage`), we may still use `UIImage` when the memory cache hit, or image decoder is not available, to behave as a fallback solution. * Using this option, can ensure we always produce image with your provided class. If failed, a error with code `SDWebImageErrorBadImageData` will been used. * Note this options is not compatible with `SDWebImageDownloaderDecodeFirstFrameOnly`, which always produce a UIImage/NSImage. */ SDWebImageDownloaderMatchAnimatedImageClass = 1 << 12, }; /// Posed when URLSessionTask started (`resume` called)) FOUNDATION_EXPORT NSNotificationName _Nonnull const SDWebImageDownloadStartNotification; /// Posed when URLSessionTask get HTTP response (`didReceiveResponse:completionHandler:` called) FOUNDATION_EXPORT NSNotificationName _Nonnull const SDWebImageDownloadReceiveResponseNotification; /// Posed when URLSessionTask stoped (`didCompleteWithError:` with error or `cancel` called) FOUNDATION_EXPORT NSNotificationName _Nonnull const SDWebImageDownloadStopNotification; /// Posed when URLSessionTask finished with success (`didCompleteWithError:` without error) FOUNDATION_EXPORT NSNotificationName _Nonnull const SDWebImageDownloadFinishNotification; typedef SDImageLoaderProgressBlock SDWebImageDownloaderProgressBlock; typedef SDImageLoaderCompletedBlock SDWebImageDownloaderCompletedBlock; /** * A token associated with each download. Can be used to cancel a download */ @interface SDWebImageDownloadToken : NSObject /** Cancel the current download. */ - (void)cancel; /** The download's URL. */ @property (nonatomic, strong, nullable, readonly) NSURL *url; /** The download's request. */ @property (nonatomic, strong, nullable, readonly) NSURLRequest *request; /** The download's response. */ @property (nonatomic, strong, nullable, readonly) NSURLResponse *response; /** The download's metrics. This will be nil if download operation does not support metrics. */ @property (nonatomic, strong, nullable, readonly) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0)); @end /** * Asynchronous downloader dedicated and optimized for image loading. */ @interface SDWebImageDownloader : NSObject /** * Downloader Config object - storing all kind of settings. * Most config properties support dynamic changes during download, except something like `sessionConfiguration`, see `SDWebImageDownloaderConfig` for more detail. */ @property (nonatomic, copy, readonly, nonnull) SDWebImageDownloaderConfig *config; /** * Set the request modifier to modify the original download request before image load. * This request modifier method will be called for each downloading image request. Return the original request means no modification. Return nil will cancel the download request. * Defaults to nil, means does not modify the original download request. * @note If you want to modify single request, consider using `SDWebImageContextDownloadRequestModifier` context option. */ @property (nonatomic, strong, nullable) id requestModifier; /** * Set the response modifier to modify the original download response during image load. * This response modifier method will be called for each downloading image response. Return the original response means no modification. Return nil will mark current download as cancelled. * Defaults to nil, means does not modify the original download response. * @note If you want to modify single response, consider using `SDWebImageContextDownloadResponseModifier` context option. */ @property (nonatomic, strong, nullable) id responseModifier; /** * Set the decryptor to decrypt the original download data before image decoding. This can be used for encrypted image data, like Base64. * This decryptor method will be called for each downloading image data. Return the original data means no modification. Return nil will mark this download failed. * Defaults to nil, means does not modify the original download data. * @note When using decryptor, progressive decoding will be disabled, to avoid data corrupt issue. * @note If you want to decrypt single download data, consider using `SDWebImageContextDownloadDecryptor` context option. */ @property (nonatomic, strong, nullable) id decryptor; /** * The configuration in use by the internal NSURLSession. If you want to provide a custom sessionConfiguration, use `SDWebImageDownloaderConfig.sessionConfiguration` and create a new downloader instance. @note This is immutable according to NSURLSession's documentation. Mutating this object directly has no effect. */ @property (nonatomic, readonly, nonnull) NSURLSessionConfiguration *sessionConfiguration; /** * Gets/Sets the download queue suspension state. */ @property (nonatomic, assign, getter=isSuspended) BOOL suspended; /** * Shows the current amount of downloads that still need to be downloaded */ @property (nonatomic, assign, readonly) NSUInteger currentDownloadCount; /** * Returns the global shared downloader instance. Which use the `SDWebImageDownloaderConfig.defaultDownloaderConfig` config. */ @property (nonatomic, class, readonly, nonnull) SDWebImageDownloader *sharedDownloader; /** Creates an instance of a downloader with specified downloader config. You can specify session configuration, timeout or operation class through downloader config. @param config The downloader config. If you specify nil, the `defaultDownloaderConfig` will be used. @return new instance of downloader class */ - (nonnull instancetype)initWithConfig:(nullable SDWebImageDownloaderConfig *)config NS_DESIGNATED_INITIALIZER; /** * Set a value for a HTTP header to be appended to each download HTTP request. * * @param value The value for the header field. Use `nil` value to remove the header field. * @param field The name of the header field to set. */ - (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field; /** * Returns the value of the specified HTTP header field. * * @return The value associated with the header field field, or `nil` if there is no corresponding header field. */ - (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field; /** * Creates a SDWebImageDownloader async downloader instance with a given URL * * The delegate will be informed when the image is finish downloaded or an error has happen. * * @see SDWebImageDownloaderDelegate * * @param url The URL to the image to download * @param completedBlock A block called once the download is completed. * If the download succeeded, the image parameter is set, in case of error, * error parameter is set with the error. The last parameter is always YES * if SDWebImageDownloaderProgressiveDownload isn't use. With the * SDWebImageDownloaderProgressiveDownload option, this block is called * repeatedly with the partial image object and the finished argument set to NO * before to be called a last time with the full image and finished argument * set to YES. In case of error, the finished argument is always YES. * * @return A token (SDWebImageDownloadToken) that can be used to cancel this operation */ - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock; /** * Creates a SDWebImageDownloader async downloader instance with a given URL * * The delegate will be informed when the image is finish downloaded or an error has happen. * * @see SDWebImageDownloaderDelegate * * @param url The URL to the image to download * @param options The options to be used for this download * @param progressBlock A block called repeatedly while the image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called once the download is completed. * If the download succeeded, the image parameter is set, in case of error, * error parameter is set with the error. The last parameter is always YES * if SDWebImageDownloaderProgressiveLoad isn't use. With the * SDWebImageDownloaderProgressiveLoad option, this block is called * repeatedly with the partial image object and the finished argument set to NO * before to be called a last time with the full image and finished argument * set to YES. In case of error, the finished argument is always YES. * * @return A token (SDWebImageDownloadToken) that can be used to cancel this operation */ - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock; /** * Creates a SDWebImageDownloader async downloader instance with a given URL * * The delegate will be informed when the image is finish downloaded or an error has happen. * * @see SDWebImageDownloaderDelegate * * @param url The URL to the image to download * @param options The options to be used for this download * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called repeatedly while the image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called once the download is completed. * * @return A token (SDWebImageDownloadToken) that can be used to cancel this operation */ - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock; /** * Cancels all download operations in the queue */ - (void)cancelAllDownloads; /** * Invalidates the managed session, optionally canceling pending operations. * @note If you use custom downloader instead of the shared downloader, you need call this method when you do not use it to avoid memory leak * @param cancelPendingOperations Whether or not to cancel pending operations. * @note Calling this method on the shared downloader has no effect. */ - (void)invalidateSessionAndCancel:(BOOL)cancelPendingOperations; @end /** SDWebImageDownloader is the built-in image loader conform to `SDImageLoader`. Which provide the HTTP/HTTPS/FTP download, or local file URL using NSURLSession. However, this downloader class itself also support customization for advanced users. You can specify `operationClass` in download config to custom download operation, See `SDWebImageDownloaderOperation`. If you want to provide some image loader which beyond network or local file, consider to create your own custom class conform to `SDImageLoader`. */ @interface SDWebImageDownloader (SDImageLoader) @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloader.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageDownloader.h" #import "SDWebImageDownloaderConfig.h" #import "SDWebImageDownloaderOperation.h" #import "SDWebImageError.h" #import "SDWebImageCacheKeyFilter.h" #import "SDImageCacheDefine.h" #import "SDInternalMacros.h" #import "objc/runtime.h" NSNotificationName const SDWebImageDownloadStartNotification = @"SDWebImageDownloadStartNotification"; NSNotificationName const SDWebImageDownloadReceiveResponseNotification = @"SDWebImageDownloadReceiveResponseNotification"; NSNotificationName const SDWebImageDownloadStopNotification = @"SDWebImageDownloadStopNotification"; NSNotificationName const SDWebImageDownloadFinishNotification = @"SDWebImageDownloadFinishNotification"; static void * SDWebImageDownloaderContext = &SDWebImageDownloaderContext; @interface SDWebImageDownloadToken () @property (nonatomic, strong, nullable, readwrite) NSURL *url; @property (nonatomic, strong, nullable, readwrite) NSURLRequest *request; @property (nonatomic, strong, nullable, readwrite) NSURLResponse *response; @property (nonatomic, strong, nullable, readwrite) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0)); @property (nonatomic, weak, nullable, readwrite) id downloadOperationCancelToken; @property (nonatomic, weak, nullable) NSOperation *downloadOperation; @property (nonatomic, assign, getter=isCancelled) BOOL cancelled; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; - (nonnull instancetype)initWithDownloadOperation:(nullable NSOperation *)downloadOperation; @end @interface SDWebImageDownloader () @property (strong, nonatomic, nonnull) NSOperationQueue *downloadQueue; @property (strong, nonatomic, nonnull) NSMutableDictionary *> *URLOperations; @property (strong, nonatomic, nullable) NSMutableDictionary *HTTPHeaders; // The session in which data tasks will run @property (strong, nonatomic) NSURLSession *session; @end @implementation SDWebImageDownloader { SD_LOCK_DECLARE(_HTTPHeadersLock); // A lock to keep the access to `HTTPHeaders` thread-safe SD_LOCK_DECLARE(_operationsLock); // A lock to keep the access to `URLOperations` thread-safe } + (void)initialize { // Bind SDNetworkActivityIndicator if available (download it here: http://github.com/rs/SDNetworkActivityIndicator ) // To use it, just add #import "SDNetworkActivityIndicator.h" in addition to the SDWebImage import if (NSClassFromString(@"SDNetworkActivityIndicator")) { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")]; #pragma clang diagnostic pop // Remove observer in case it was previously added. [[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStartNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:activityIndicator name:SDWebImageDownloadStopNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:activityIndicator selector:NSSelectorFromString(@"startActivity") name:SDWebImageDownloadStartNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:activityIndicator selector:NSSelectorFromString(@"stopActivity") name:SDWebImageDownloadStopNotification object:nil]; } } + (nonnull instancetype)sharedDownloader { static dispatch_once_t once; static id instance; dispatch_once(&once, ^{ instance = [self new]; }); return instance; } - (nonnull instancetype)init { return [self initWithConfig:SDWebImageDownloaderConfig.defaultDownloaderConfig]; } - (instancetype)initWithConfig:(SDWebImageDownloaderConfig *)config { self = [super init]; if (self) { if (!config) { config = SDWebImageDownloaderConfig.defaultDownloaderConfig; } _config = [config copy]; [_config addObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) options:0 context:SDWebImageDownloaderContext]; _downloadQueue = [NSOperationQueue new]; _downloadQueue.maxConcurrentOperationCount = _config.maxConcurrentDownloads; _downloadQueue.name = @"com.hackemist.SDWebImageDownloader.downloadQueue"; _URLOperations = [NSMutableDictionary new]; NSMutableDictionary *headerDictionary = [NSMutableDictionary dictionary]; NSString *userAgent = nil; // User-Agent Header; see http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.43 #if SD_VISION userAgent = [NSString stringWithFormat:@"%@/%@ (%@; visionOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], UITraitCollection.currentTraitCollection.displayScale]; #elif SD_UIKIT userAgent = [NSString stringWithFormat:@"%@/%@ (%@; iOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[UIDevice currentDevice] model], [[UIDevice currentDevice] systemVersion], [[UIScreen mainScreen] scale]]; #elif SD_WATCH userAgent = [NSString stringWithFormat:@"%@/%@ (%@; watchOS %@; Scale/%0.2f)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[WKInterfaceDevice currentDevice] model], [[WKInterfaceDevice currentDevice] systemVersion], [[WKInterfaceDevice currentDevice] screenScale]]; #elif SD_MAC userAgent = [NSString stringWithFormat:@"%@/%@ (Mac OS X %@)", [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleExecutableKey] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleIdentifierKey], [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"] ?: [[NSBundle mainBundle] infoDictionary][(__bridge NSString *)kCFBundleVersionKey], [[NSProcessInfo processInfo] operatingSystemVersionString]]; #endif if (userAgent) { if (![userAgent canBeConvertedToEncoding:NSASCIIStringEncoding]) { NSMutableString *mutableUserAgent = [userAgent mutableCopy]; if (CFStringTransform((__bridge CFMutableStringRef)(mutableUserAgent), NULL, (__bridge CFStringRef)@"Any-Latin; Latin-ASCII; [:^ASCII:] Remove", false)) { userAgent = mutableUserAgent; } } headerDictionary[@"User-Agent"] = userAgent; } headerDictionary[@"Accept"] = @"image/*,*/*;q=0.8"; _HTTPHeaders = headerDictionary; SD_LOCK_INIT(_HTTPHeadersLock); SD_LOCK_INIT(_operationsLock); NSURLSessionConfiguration *sessionConfiguration = _config.sessionConfiguration; if (!sessionConfiguration) { sessionConfiguration = [NSURLSessionConfiguration defaultSessionConfiguration]; } /** * Create the session for this task * We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate * method calls and completion handler calls. */ _session = [NSURLSession sessionWithConfiguration:sessionConfiguration delegate:self delegateQueue:nil]; } return self; } - (void)dealloc { [self.downloadQueue cancelAllOperations]; [self.config removeObserver:self forKeyPath:NSStringFromSelector(@selector(maxConcurrentDownloads)) context:SDWebImageDownloaderContext]; // Invalide the URLSession after all operations been cancelled [self.session invalidateAndCancel]; self.session = nil; } - (void)invalidateSessionAndCancel:(BOOL)cancelPendingOperations { if (self == [SDWebImageDownloader sharedDownloader]) { return; } if (cancelPendingOperations) { [self.session invalidateAndCancel]; } else { [self.session finishTasksAndInvalidate]; } } - (void)setValue:(nullable NSString *)value forHTTPHeaderField:(nullable NSString *)field { if (!field) { return; } SD_LOCK(_HTTPHeadersLock); [self.HTTPHeaders setValue:value forKey:field]; SD_UNLOCK(_HTTPHeadersLock); } - (nullable NSString *)valueForHTTPHeaderField:(nullable NSString *)field { if (!field) { return nil; } SD_LOCK(_HTTPHeadersLock); NSString *value = [self.HTTPHeaders objectForKey:field]; SD_UNLOCK(_HTTPHeadersLock); return value; } - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url completed:(SDWebImageDownloaderCompletedBlock)completedBlock { return [self downloadImageWithURL:url options:0 progress:nil completed:completedBlock]; } - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(NSURL *)url options:(SDWebImageDownloaderOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageDownloaderCompletedBlock)completedBlock { return [self downloadImageWithURL:url options:options context:nil progress:progressBlock completed:completedBlock]; } - (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock { // The URL will be used as the key to the callbacks dictionary so it cannot be nil. If it is nil immediately call the completed block with no image or data. if (url == nil) { if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}]; completedBlock(nil, nil, error, YES); } return nil; } id downloadOperationCancelToken; // When different thumbnail size download with same url, we need to make sure each callback called with desired size id cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; NSString *cacheKey; if (cacheKeyFilter) { cacheKey = [cacheKeyFilter cacheKeyForURL:url]; } else { cacheKey = url.absoluteString; } SDImageCoderOptions *decodeOptions = SDGetDecodeOptionsFromContext(context, [self.class imageOptionsFromDownloaderOptions:options], cacheKey); SD_LOCK(_operationsLock); NSOperation *operation = [self.URLOperations objectForKey:url]; // There is a case that the operation may be marked as finished or cancelled, but not been removed from `self.URLOperations`. BOOL shouldNotReuseOperation; if (operation) { @synchronized (operation) { shouldNotReuseOperation = operation.isFinished || operation.isCancelled; } } else { shouldNotReuseOperation = YES; } if (shouldNotReuseOperation) { operation = [self createDownloaderOperationWithUrl:url options:options context:context]; if (!operation) { SD_UNLOCK(_operationsLock); if (completedBlock) { NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Downloader operation is nil"}]; completedBlock(nil, nil, error, YES); } return nil; } @weakify(self); operation.completionBlock = ^{ @strongify(self); if (!self) { return; } SD_LOCK(self->_operationsLock); [self.URLOperations removeObjectForKey:url]; SD_UNLOCK(self->_operationsLock); }; [self.URLOperations setObject:operation forKey:url]; // Add the handlers before submitting to operation queue, avoid the race condition that operation finished before setting handlers. downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions]; // Add operation to operation queue only after all configuration done according to Apple's doc. // `addOperation:` does not synchronously execute the `operation.completionBlock` so this will not cause deadlock. [self.downloadQueue addOperation:operation]; } else { // When we reuse the download operation to attach more callbacks, there may be thread safe issue because the getter of callbacks may in another queue (decoding queue or delegate queue) // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes. @synchronized (operation) { downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:decodeOptions]; } } SD_UNLOCK(_operationsLock); SDWebImageDownloadToken *token = [[SDWebImageDownloadToken alloc] initWithDownloadOperation:operation]; token.url = url; token.request = operation.request; token.downloadOperationCancelToken = downloadOperationCancelToken; return token; } #pragma mark Helper methods #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" + (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions { SDWebImageOptions options = 0; if (downloadOptions & SDWebImageDownloaderScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages; if (downloadOptions & SDWebImageDownloaderDecodeFirstFrameOnly) options |= SDWebImageDecodeFirstFrameOnly; if (downloadOptions & SDWebImageDownloaderPreloadAllFrames) options |= SDWebImagePreloadAllFrames; if (downloadOptions & SDWebImageDownloaderAvoidDecodeImage) options |= SDWebImageAvoidDecodeImage; if (downloadOptions & SDWebImageDownloaderMatchAnimatedImageClass) options |= SDWebImageMatchAnimatedImageClass; return options; } #pragma clang diagnostic pop - (nullable NSOperation *)createDownloaderOperationWithUrl:(nonnull NSURL *)url options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context { NSTimeInterval timeoutInterval = self.config.downloadTimeout; if (timeoutInterval == 0.0) { timeoutInterval = 15.0; } // In order to prevent from potential duplicate caching (NSURLCache + SDImageCache) we disable the cache for image requests if told otherwise NSURLRequestCachePolicy cachePolicy = options & SDWebImageDownloaderUseNSURLCache ? NSURLRequestUseProtocolCachePolicy : NSURLRequestReloadIgnoringLocalCacheData; NSMutableURLRequest *mutableRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:cachePolicy timeoutInterval:timeoutInterval]; mutableRequest.HTTPShouldHandleCookies = SD_OPTIONS_CONTAINS(options, SDWebImageDownloaderHandleCookies); mutableRequest.HTTPShouldUsePipelining = YES; SD_LOCK(_HTTPHeadersLock); mutableRequest.allHTTPHeaderFields = self.HTTPHeaders; SD_UNLOCK(_HTTPHeadersLock); // Context Option SDWebImageMutableContext *mutableContext; if (context) { mutableContext = [context mutableCopy]; } else { mutableContext = [NSMutableDictionary dictionary]; } // Request Modifier id requestModifier; if ([context valueForKey:SDWebImageContextDownloadRequestModifier]) { requestModifier = [context valueForKey:SDWebImageContextDownloadRequestModifier]; } else { requestModifier = self.requestModifier; } NSURLRequest *request; if (requestModifier) { NSURLRequest *modifiedRequest = [requestModifier modifiedRequestWithRequest:[mutableRequest copy]]; // If modified request is nil, early return if (!modifiedRequest) { return nil; } else { request = [modifiedRequest copy]; } } else { request = [mutableRequest copy]; } // Response Modifier id responseModifier; if ([context valueForKey:SDWebImageContextDownloadResponseModifier]) { responseModifier = [context valueForKey:SDWebImageContextDownloadResponseModifier]; } else { responseModifier = self.responseModifier; } if (responseModifier) { mutableContext[SDWebImageContextDownloadResponseModifier] = responseModifier; } // Decryptor id decryptor; if ([context valueForKey:SDWebImageContextDownloadDecryptor]) { decryptor = [context valueForKey:SDWebImageContextDownloadDecryptor]; } else { decryptor = self.decryptor; } if (decryptor) { mutableContext[SDWebImageContextDownloadDecryptor] = decryptor; } context = [mutableContext copy]; // Operation Class Class operationClass = self.config.operationClass; if (!operationClass) { operationClass = [SDWebImageDownloaderOperation class]; } NSOperation *operation = [[operationClass alloc] initWithRequest:request inSession:self.session options:options context:context]; if ([operation respondsToSelector:@selector(setCredential:)]) { if (self.config.urlCredential) { operation.credential = self.config.urlCredential; } else if (self.config.username && self.config.password) { operation.credential = [NSURLCredential credentialWithUser:self.config.username password:self.config.password persistence:NSURLCredentialPersistenceForSession]; } } if ([operation respondsToSelector:@selector(setMinimumProgressInterval:)]) { operation.minimumProgressInterval = MIN(MAX(self.config.minimumProgressInterval, 0), 1); } if ([operation respondsToSelector:@selector(setAcceptableStatusCodes:)]) { operation.acceptableStatusCodes = self.config.acceptableStatusCodes; } if ([operation respondsToSelector:@selector(setAcceptableContentTypes:)]) { operation.acceptableContentTypes = self.config.acceptableContentTypes; } if (options & SDWebImageDownloaderHighPriority) { operation.queuePriority = NSOperationQueuePriorityHigh; } else if (options & SDWebImageDownloaderLowPriority) { operation.queuePriority = NSOperationQueuePriorityLow; } if (self.config.executionOrder == SDWebImageDownloaderLIFOExecutionOrder) { // Emulate LIFO execution order by systematically, each previous adding operation can dependency the new operation // This can gurantee the new operation to be execulated firstly, even if when some operations finished, meanwhile you appending new operations // Just make last added operation dependents new operation can not solve this problem. See test case #test15DownloaderLIFOExecutionOrder for (NSOperation *pendingOperation in self.downloadQueue.operations) { [pendingOperation addDependency:operation]; } } return operation; } - (void)cancelAllDownloads { [self.downloadQueue cancelAllOperations]; } #pragma mark - Properties - (BOOL)isSuspended { return self.downloadQueue.isSuspended; } - (void)setSuspended:(BOOL)suspended { self.downloadQueue.suspended = suspended; } - (NSUInteger)currentDownloadCount { return self.downloadQueue.operationCount; } - (NSURLSessionConfiguration *)sessionConfiguration { return self.session.configuration; } #pragma mark - KVO - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context == SDWebImageDownloaderContext) { if ([keyPath isEqualToString:NSStringFromSelector(@selector(maxConcurrentDownloads))]) { self.downloadQueue.maxConcurrentOperationCount = self.config.maxConcurrentDownloads; } } else { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; } } #pragma mark Helper methods - (NSOperation *)operationWithTask:(NSURLSessionTask *)task { NSOperation *returnOperation = nil; for (NSOperation *operation in self.downloadQueue.operations) { if ([operation respondsToSelector:@selector(dataTask)]) { // So we lock the operation here, and in `SDWebImageDownloaderOperation`, we use `@synchonzied (self)`, to ensure the thread safe between these two classes. NSURLSessionTask *operationTask; @synchronized (operation) { operationTask = operation.dataTask; } if (operationTask.taskIdentifier == task.taskIdentifier) { returnOperation = operation; break; } } } return returnOperation; } #pragma mark NSURLSessionDataDelegate - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { // Identify the operation that runs this task and pass it the delegate method NSOperation *dataOperation = [self operationWithTask:dataTask]; if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveResponse:completionHandler:)]) { [dataOperation URLSession:session dataTask:dataTask didReceiveResponse:response completionHandler:completionHandler]; } else { if (completionHandler) { completionHandler(NSURLSessionResponseAllow); } } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { // Identify the operation that runs this task and pass it the delegate method NSOperation *dataOperation = [self operationWithTask:dataTask]; if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:didReceiveData:)]) { [dataOperation URLSession:session dataTask:dataTask didReceiveData:data]; } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { // Identify the operation that runs this task and pass it the delegate method NSOperation *dataOperation = [self operationWithTask:dataTask]; if ([dataOperation respondsToSelector:@selector(URLSession:dataTask:willCacheResponse:completionHandler:)]) { [dataOperation URLSession:session dataTask:dataTask willCacheResponse:proposedResponse completionHandler:completionHandler]; } else { if (completionHandler) { completionHandler(proposedResponse); } } } #pragma mark NSURLSessionTaskDelegate - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { // Identify the operation that runs this task and pass it the delegate method NSOperation *dataOperation = [self operationWithTask:task]; if ([dataOperation respondsToSelector:@selector(URLSession:task:didCompleteWithError:)]) { [dataOperation URLSession:session task:task didCompleteWithError:error]; } } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)request completionHandler:(void (^)(NSURLRequest * _Nullable))completionHandler { // Identify the operation that runs this task and pass it the delegate method NSOperation *dataOperation = [self operationWithTask:task]; if ([dataOperation respondsToSelector:@selector(URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler:)]) { [dataOperation URLSession:session task:task willPerformHTTPRedirection:response newRequest:request completionHandler:completionHandler]; } else { if (completionHandler) { completionHandler(request); } } } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { // Identify the operation that runs this task and pass it the delegate method NSOperation *dataOperation = [self operationWithTask:task]; if ([dataOperation respondsToSelector:@selector(URLSession:task:didReceiveChallenge:completionHandler:)]) { [dataOperation URLSession:session task:task didReceiveChallenge:challenge completionHandler:completionHandler]; } else { if (completionHandler) { completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil); } } } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { // Identify the operation that runs this task and pass it the delegate method NSOperation *dataOperation = [self operationWithTask:task]; if ([dataOperation respondsToSelector:@selector(URLSession:task:didFinishCollectingMetrics:)]) { [dataOperation URLSession:session task:task didFinishCollectingMetrics:metrics]; } } @end @implementation SDWebImageDownloadToken - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self name:SDWebImageDownloadReceiveResponseNotification object:nil]; [[NSNotificationCenter defaultCenter] removeObserver:self name:SDWebImageDownloadStopNotification object:nil]; } - (instancetype)initWithDownloadOperation:(NSOperation *)downloadOperation { self = [super init]; if (self) { _downloadOperation = downloadOperation; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadDidReceiveResponse:) name:SDWebImageDownloadReceiveResponseNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(downloadDidStop:) name:SDWebImageDownloadStopNotification object:nil]; } return self; } - (void)downloadDidReceiveResponse:(NSNotification *)notification { NSOperation *downloadOperation = notification.object; if (downloadOperation && downloadOperation == self.downloadOperation) { self.response = downloadOperation.response; } } - (void)downloadDidStop:(NSNotification *)notification { NSOperation *downloadOperation = notification.object; if (downloadOperation && downloadOperation == self.downloadOperation) { if ([downloadOperation respondsToSelector:@selector(metrics)]) { if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, watchOS 3.0, *)) { self.metrics = downloadOperation.metrics; } } } } - (void)cancel { @synchronized (self) { if (self.isCancelled) { return; } self.cancelled = YES; [self.downloadOperation cancel:self.downloadOperationCancelToken]; self.downloadOperationCancelToken = nil; } } @end @implementation SDWebImageDownloader (SDImageLoader) - (BOOL)canRequestImageForURL:(NSURL *)url { return [self canRequestImageForURL:url options:0 context:nil]; } - (BOOL)canRequestImageForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context { if (!url) { return NO; } // Always pass YES to let URLSession or custom download operation to determine return YES; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (id)requestImageWithURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDImageLoaderCompletedBlock)completedBlock { UIImage *cachedImage = context[SDWebImageContextLoaderCachedImage]; SDWebImageDownloaderOptions downloaderOptions = 0; if (options & SDWebImageLowPriority) downloaderOptions |= SDWebImageDownloaderLowPriority; if (options & SDWebImageProgressiveLoad) downloaderOptions |= SDWebImageDownloaderProgressiveLoad; if (options & SDWebImageRefreshCached) downloaderOptions |= SDWebImageDownloaderUseNSURLCache; if (options & SDWebImageContinueInBackground) downloaderOptions |= SDWebImageDownloaderContinueInBackground; if (options & SDWebImageHandleCookies) downloaderOptions |= SDWebImageDownloaderHandleCookies; if (options & SDWebImageAllowInvalidSSLCertificates) downloaderOptions |= SDWebImageDownloaderAllowInvalidSSLCertificates; if (options & SDWebImageHighPriority) downloaderOptions |= SDWebImageDownloaderHighPriority; if (options & SDWebImageScaleDownLargeImages) downloaderOptions |= SDWebImageDownloaderScaleDownLargeImages; if (options & SDWebImageAvoidDecodeImage) downloaderOptions |= SDWebImageDownloaderAvoidDecodeImage; if (options & SDWebImageDecodeFirstFrameOnly) downloaderOptions |= SDWebImageDownloaderDecodeFirstFrameOnly; if (options & SDWebImagePreloadAllFrames) downloaderOptions |= SDWebImageDownloaderPreloadAllFrames; if (options & SDWebImageMatchAnimatedImageClass) downloaderOptions |= SDWebImageDownloaderMatchAnimatedImageClass; if (cachedImage && options & SDWebImageRefreshCached) { // force progressive off if image already cached but forced refreshing downloaderOptions &= ~SDWebImageDownloaderProgressiveLoad; // ignore image read from NSURLCache if image if cached but force refreshing downloaderOptions |= SDWebImageDownloaderIgnoreCachedResponse; } return [self downloadImageWithURL:url options:downloaderOptions context:context progress:progressBlock completed:completedBlock]; } #pragma clang diagnostic pop - (BOOL)shouldBlockFailedURLWithURL:(NSURL *)url error:(NSError *)error { return [self shouldBlockFailedURLWithURL:url error:error options:0 context:nil]; } - (BOOL)shouldBlockFailedURLWithURL:(NSURL *)url error:(NSError *)error options:(SDWebImageOptions)options context:(SDWebImageContext *)context { BOOL shouldBlockFailedURL; // Filter the error domain and check error codes if ([error.domain isEqualToString:SDWebImageErrorDomain]) { shouldBlockFailedURL = ( error.code == SDWebImageErrorInvalidURL || error.code == SDWebImageErrorBadImageData); } else if ([error.domain isEqualToString:NSURLErrorDomain]) { shouldBlockFailedURL = ( error.code != NSURLErrorNotConnectedToInternet && error.code != NSURLErrorCancelled && error.code != NSURLErrorTimedOut && error.code != NSURLErrorInternationalRoamingOff && error.code != NSURLErrorDataNotAllowed && error.code != NSURLErrorCannotFindHost && error.code != NSURLErrorCannotConnectToHost && error.code != NSURLErrorNetworkConnectionLost); } else { shouldBlockFailedURL = NO; } return shouldBlockFailedURL; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /// Operation execution order typedef NS_ENUM(NSInteger, SDWebImageDownloaderExecutionOrder) { /** * Default value. All download operations will execute in queue style (first-in-first-out). */ SDWebImageDownloaderFIFOExecutionOrder, /** * All download operations will execute in stack style (last-in-first-out). */ SDWebImageDownloaderLIFOExecutionOrder }; /** The class contains all the config for image downloader @note This class conform to NSCopying, make sure to add the property in `copyWithZone:` as well. */ @interface SDWebImageDownloaderConfig : NSObject /** Gets the default downloader config used for shared instance or initialization when it does not provide any downloader config. Such as `SDWebImageDownloader.sharedDownloader`. @note You can modify the property on default downloader config, which can be used for later created downloader instance. The already created downloader instance does not get affected. */ @property (nonatomic, class, readonly, nonnull) SDWebImageDownloaderConfig *defaultDownloaderConfig; /** * The maximum number of concurrent downloads. * Defaults to 6. */ @property (nonatomic, assign) NSInteger maxConcurrentDownloads; /** * The timeout value (in seconds) for each download operation. * Defaults to 15.0. */ @property (nonatomic, assign) NSTimeInterval downloadTimeout; /** * The minimum interval about progress percent during network downloading. Which means the next progress callback and current progress callback's progress percent difference should be larger or equal to this value. However, the final finish download progress callback does not get effected. * The value should be 0.0-1.0. * @note If you're using progressive decoding feature, this will also effect the image refresh rate. * @note This value may enhance the performance if you don't want progress callback too frequently. * Defaults to 0, which means each time we receive the new data from URLSession, we callback the progressBlock immediately. */ @property (nonatomic, assign) double minimumProgressInterval; /** * The custom session configuration in use by NSURLSession. If you don't provide one, we will use `defaultSessionConfiguration` instead. * Defatuls to nil. * @note This property does not support dynamic changes, means it's immutable after the downloader instance initialized. */ @property (nonatomic, strong, nullable) NSURLSessionConfiguration *sessionConfiguration; /** * Gets/Sets a subclass of `SDWebImageDownloaderOperation` as the default * `NSOperation` to be used each time SDWebImage constructs a request * operation to download an image. * Defaults to nil. * @note Passing `NSOperation` to set as default. Passing `nil` will revert to `SDWebImageDownloaderOperation`. */ @property (nonatomic, assign, nullable) Class operationClass; /** * Changes download operations execution order. * Defaults to `SDWebImageDownloaderFIFOExecutionOrder`. */ @property (nonatomic, assign) SDWebImageDownloaderExecutionOrder executionOrder; /** * Set the default URL credential to be set for request operations. * Defaults to nil. */ @property (nonatomic, copy, nullable) NSURLCredential *urlCredential; /** * Set username using for HTTP Basic authentication. * Defaults to nil. */ @property (nonatomic, copy, nullable) NSString *username; /** * Set password using for HTTP Basic authentication. * Defaults to nil. */ @property (nonatomic, copy, nullable) NSString *password; /** * Set the acceptable HTTP Response status code. The status code which beyond the range will mark the download operation failed. * For example, if we config [200, 400) but server response is 503, the download will fail with error code `SDWebImageErrorInvalidDownloadStatusCode`. * Defaults to [200,400). Nil means no validation at all. */ @property (nonatomic, copy, nullable) NSIndexSet *acceptableStatusCodes; /** * Set the acceptable HTTP Response content type. The content type beyond the set will mark the download operation failed. * For example, if we config ["image/png"] but server response is "application/json", the download will fail with error code `SDWebImageErrorInvalidDownloadContentType`. * Normally you don't need this for image format detection because we use image's data file signature magic bytes: https://en.wikipedia.org/wiki/List_of_file_signatures * Defaults to nil. Nil means no validation at all. */ @property (nonatomic, copy, nullable) NSSet *acceptableContentTypes; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderConfig.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageDownloaderConfig.h" #import "SDWebImageDownloaderOperation.h" static SDWebImageDownloaderConfig * _defaultDownloaderConfig; @implementation SDWebImageDownloaderConfig + (SDWebImageDownloaderConfig *)defaultDownloaderConfig { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _defaultDownloaderConfig = [SDWebImageDownloaderConfig new]; }); return _defaultDownloaderConfig; } - (instancetype)init { self = [super init]; if (self) { _maxConcurrentDownloads = 6; _downloadTimeout = 15.0; _executionOrder = SDWebImageDownloaderFIFOExecutionOrder; _acceptableStatusCodes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(200, 100)]; } return self; } - (id)copyWithZone:(NSZone *)zone { SDWebImageDownloaderConfig *config = [[[self class] allocWithZone:zone] init]; config.maxConcurrentDownloads = self.maxConcurrentDownloads; config.downloadTimeout = self.downloadTimeout; config.minimumProgressInterval = self.minimumProgressInterval; config.sessionConfiguration = [self.sessionConfiguration copyWithZone:zone]; config.operationClass = self.operationClass; config.executionOrder = self.executionOrder; config.urlCredential = self.urlCredential; config.username = self.username; config.password = self.password; config.acceptableStatusCodes = self.acceptableStatusCodes; config.acceptableContentTypes = self.acceptableContentTypes; return config; } - (void)setOperationClass:(Class)operationClass { if (operationClass) { NSAssert([operationClass isSubclassOfClass:[NSOperation class]] && [operationClass conformsToProtocol:@protocol(SDWebImageDownloaderOperation)], @"Custom downloader operation class must subclass NSOperation and conform to `SDWebImageDownloaderOperation` protocol"); } _operationClass = operationClass; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderDecryptor.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" typedef NSData * _Nullable (^SDWebImageDownloaderDecryptorBlock)(NSData * _Nonnull data, NSURLResponse * _Nullable response); /** This is the protocol for downloader decryptor. Which decrypt the original encrypted data before decoding. Note progressive decoding is not compatible for decryptor. We can use a block to specify the downloader decryptor. But Using protocol can make this extensible, and allow Swift user to use it easily instead of using `@convention(block)` to store a block into context options. */ @protocol SDWebImageDownloaderDecryptor /// Decrypt the original download data and return a new data. You can use this to decrypt the data using your preferred algorithm. /// @param data The original download data /// @param response The URL response for data. If you modify the original URL response via response modifier, the modified version will be here. This arg is nullable. /// @note If nil is returned, the image download will be marked as failed with error `SDWebImageErrorBadImageData` - (nullable NSData *)decryptedDataWithData:(nonnull NSData *)data response:(nullable NSURLResponse *)response; @end /** A downloader response modifier class with block. */ @interface SDWebImageDownloaderDecryptor : NSObject /// Create the data decryptor with block /// @param block A block to control decrypt logic - (nonnull instancetype)initWithBlock:(nonnull SDWebImageDownloaderDecryptorBlock)block; /// Create the data decryptor with block /// @param block A block to control decrypt logic + (nonnull instancetype)decryptorWithBlock:(nonnull SDWebImageDownloaderDecryptorBlock)block; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; @end /// Convenience way to create decryptor for common data encryption. @interface SDWebImageDownloaderDecryptor (Conveniences) /// Base64 Encoded image data decryptor @property (class, readonly, nonnull) SDWebImageDownloaderDecryptor *base64Decryptor; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderDecryptor.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageDownloaderDecryptor.h" @interface SDWebImageDownloaderDecryptor () @property (nonatomic, copy, nonnull) SDWebImageDownloaderDecryptorBlock block; @end @implementation SDWebImageDownloaderDecryptor - (instancetype)initWithBlock:(SDWebImageDownloaderDecryptorBlock)block { self = [super init]; if (self) { self.block = block; } return self; } + (instancetype)decryptorWithBlock:(SDWebImageDownloaderDecryptorBlock)block { SDWebImageDownloaderDecryptor *decryptor = [[SDWebImageDownloaderDecryptor alloc] initWithBlock:block]; return decryptor; } - (nullable NSData *)decryptedDataWithData:(nonnull NSData *)data response:(nullable NSURLResponse *)response { if (!self.block) { return nil; } return self.block(data, response); } @end @implementation SDWebImageDownloaderDecryptor (Conveniences) + (SDWebImageDownloaderDecryptor *)base64Decryptor { static SDWebImageDownloaderDecryptor *decryptor; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ decryptor = [SDWebImageDownloaderDecryptor decryptorWithBlock:^NSData * _Nullable(NSData * _Nonnull data, NSURLResponse * _Nullable response) { NSData *modifiedData = [[NSData alloc] initWithBase64EncodedData:data options:NSDataBase64DecodingIgnoreUnknownCharacters]; return modifiedData; }]; }); return decryptor; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageDownloader.h" #import "SDWebImageOperation.h" /** Describes a downloader operation. If one wants to use a custom downloader op, it needs to inherit from `NSOperation` and conform to this protocol For the description about these methods, see `SDWebImageDownloaderOperation` @note If your custom operation class does not use `NSURLSession` at all, do not implement the optional methods and session delegate methods. */ @protocol SDWebImageDownloaderOperation @required - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session options:(SDWebImageDownloaderOptions)options; - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context; - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock; - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock decodeOptions:(nullable SDImageCoderOptions *)decodeOptions; - (BOOL)cancel:(nullable id)token; @property (strong, nonatomic, readonly, nullable) NSURLRequest *request; @property (strong, nonatomic, readonly, nullable) NSURLResponse *response; @optional @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask; @property (strong, nonatomic, readonly, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0)); // These operation-level config was inherited from downloader. See `SDWebImageDownloaderConfig` for documentation. @property (strong, nonatomic, nullable) NSURLCredential *credential; @property (assign, nonatomic) double minimumProgressInterval; @property (copy, nonatomic, nullable) NSIndexSet *acceptableStatusCodes; @property (copy, nonatomic, nullable) NSSet *acceptableContentTypes; @end /** The download operation class for SDWebImageDownloader. */ @interface SDWebImageDownloaderOperation : NSOperation /** * The request used by the operation's task. */ @property (strong, nonatomic, readonly, nullable) NSURLRequest *request; /** * The response returned by the operation's task. */ @property (strong, nonatomic, readonly, nullable) NSURLResponse *response; /** * The operation's task */ @property (strong, nonatomic, readonly, nullable) NSURLSessionTask *dataTask; /** * The collected metrics from `-URLSession:task:didFinishCollectingMetrics:`. * This can be used to collect the network metrics like download duration, DNS lookup duration, SSL handshake duration, etc. See Apple's documentation: https://developer.apple.com/documentation/foundation/urlsessiontaskmetrics */ @property (strong, nonatomic, readonly, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0)); /** * The credential used for authentication challenges in `-URLSession:task:didReceiveChallenge:completionHandler:`. * * This will be overridden by any shared credentials that exist for the username or password of the request URL, if present. */ @property (strong, nonatomic, nullable) NSURLCredential *credential; /** * The minimum interval about progress percent during network downloading. Which means the next progress callback and current progress callback's progress percent difference should be larger or equal to this value. However, the final finish download progress callback does not get effected. * The value should be 0.0-1.0. * @note If you're using progressive decoding feature, this will also effect the image refresh rate. * @note This value may enhance the performance if you don't want progress callback too frequently. * Defaults to 0, which means each time we receive the new data from URLSession, we callback the progressBlock immediately. */ @property (assign, nonatomic) double minimumProgressInterval; /** * Set the acceptable HTTP Response status code. The status code which beyond the range will mark the download operation failed. * For example, if we config [200, 400) but server response is 503, the download will fail with error code `SDWebImageErrorInvalidDownloadStatusCode`. * Defaults to [200,400). Nil means no validation at all. */ @property (copy, nonatomic, nullable) NSIndexSet *acceptableStatusCodes; /** * Set the acceptable HTTP Response content type. The content type beyond the set will mark the download operation failed. * For example, if we config ["image/png"] but server response is "application/json", the download will fail with error code `SDWebImageErrorInvalidDownloadContentType`. * Normally you don't need this for image format detection because we use image's data file signature magic bytes: https://en.wikipedia.org/wiki/List_of_file_signatures * Defaults to nil. Nil means no validation at all. */ @property (copy, nonatomic, nullable) NSSet *acceptableContentTypes; /** * The options for the receiver. */ @property (assign, nonatomic, readonly) SDWebImageDownloaderOptions options; /** * The context for the receiver. */ @property (copy, nonatomic, readonly, nullable) SDWebImageContext *context; /** * Initializes a `SDWebImageDownloaderOperation` object * * @see SDWebImageDownloaderOperation * * @param request the URL request * @param session the URL session in which this operation will run * @param options downloader options * * @return the initialized instance */ - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session options:(SDWebImageDownloaderOptions)options; /** * Initializes a `SDWebImageDownloaderOperation` object * * @see SDWebImageDownloaderOperation * * @param request the URL request * @param session the URL session in which this operation will run * @param options downloader options * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * * @return the initialized instance */ - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context NS_DESIGNATED_INITIALIZER; /** * Adds handlers for progress and completion. Returns a token that can be passed to -cancel: to cancel this set of * callbacks. * * @param progressBlock the block executed when a new chunk of data arrives. * @note the progress block is executed on a background queue * @param completedBlock the block executed when the download is done. * @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue * * @return the token to use to cancel this set of handlers */ - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock; /** * Adds handlers for progress and completion, and optional decode options (which need another image other than the initial one). Returns a token that can be passed to -cancel: to cancel this set of * callbacks. * * @param progressBlock the block executed when a new chunk of data arrives. * @note the progress block is executed on a background queue * @param completedBlock the block executed when the download is done. * @note the completed block is executed on the main queue for success. If errors are found, there is a chance the block will be executed on a background queue * @param decodeOptions The optional decode options, used when in thumbnail decoding for current completion block callback. For example, request and then , we may callback these two completion block with different size. * @return the token to use to cancel this set of handlers */ - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock decodeOptions:(nullable SDImageCoderOptions *)decodeOptions; /** * Cancels a set of callbacks. Once all callbacks are canceled, the operation is cancelled. * * @param token the token representing a set of callbacks to cancel * * @return YES if the operation was stopped because this was the last token to be canceled. NO otherwise. */ - (BOOL)cancel:(nullable id)token; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderOperation.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageDownloaderOperation.h" #import "SDWebImageError.h" #import "SDInternalMacros.h" #import "SDWebImageDownloaderResponseModifier.h" #import "SDWebImageDownloaderDecryptor.h" #import "SDImageCacheDefine.h" #import "SDCallbackQueue.h" // A handler to represent individual request @interface SDWebImageDownloaderOperationToken : NSObject @property (nonatomic, copy, nullable) SDWebImageDownloaderCompletedBlock completedBlock; @property (nonatomic, copy, nullable) SDWebImageDownloaderProgressBlock progressBlock; @property (nonatomic, copy, nullable) SDImageCoderOptions *decodeOptions; @end @implementation SDWebImageDownloaderOperationToken - (BOOL)isEqual:(id)other { if (nil == other) { return NO; } if (self == other) { return YES; } if (![other isKindOfClass:[self class]]) { return NO; } SDWebImageDownloaderOperationToken *object = (SDWebImageDownloaderOperationToken *)other; // warn: only compare decodeOptions, ignore pointer, use `removeObjectIdenticalTo` BOOL result = [self.decodeOptions isEqualToDictionary:object.decodeOptions]; return result; } @end @interface SDWebImageDownloaderOperation () @property (strong, nonatomic, nonnull) NSMutableArray *callbackTokens; @property (assign, nonatomic, readwrite) SDWebImageDownloaderOptions options; @property (copy, nonatomic, readwrite, nullable) SDWebImageContext *context; @property (assign, nonatomic, getter = isExecuting) BOOL executing; @property (assign, nonatomic, getter = isFinished) BOOL finished; @property (strong, nonatomic, nullable) NSMutableData *imageData; @property (copy, nonatomic, nullable) NSData *cachedData; // for `SDWebImageDownloaderIgnoreCachedResponse` @property (assign, nonatomic) NSUInteger expectedSize; // may be 0 @property (assign, nonatomic) NSUInteger receivedSize; @property (strong, nonatomic, nullable, readwrite) NSURLResponse *response; @property (strong, nonatomic, nullable) NSError *responseError; @property (assign, nonatomic) double previousProgress; // previous progress percent @property (assign, nonatomic, getter = isDownloadCompleted) BOOL downloadCompleted; @property (strong, nonatomic, nullable) id responseModifier; // modify original URLResponse @property (strong, nonatomic, nullable) id decryptor; // decrypt image data // This is weak because it is injected by whoever manages this session. If this gets nil-ed out, we won't be able to run // the task associated with this operation @property (weak, nonatomic, nullable) NSURLSession *unownedSession; // This is set if we're using not using an injected NSURLSession. We're responsible of invalidating this one @property (strong, nonatomic, nullable) NSURLSession *ownedSession; @property (strong, nonatomic, readwrite, nullable) NSURLSessionTask *dataTask; @property (strong, nonatomic, readwrite, nullable) NSURLSessionTaskMetrics *metrics API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0)); @property (strong, nonatomic, nonnull) NSOperationQueue *coderQueue; // the serial operation queue to do image decoding @property (strong, nonatomic, nonnull) NSMapTable *imageMap; // each variant of image is weak-referenced to avoid too many re-decode during downloading #if SD_UIKIT @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId; #endif @end @implementation SDWebImageDownloaderOperation @synthesize executing = _executing; @synthesize finished = _finished; - (nonnull instancetype)init { return [self initWithRequest:nil inSession:nil options:0]; } - (instancetype)initWithRequest:(NSURLRequest *)request inSession:(NSURLSession *)session options:(SDWebImageDownloaderOptions)options { return [self initWithRequest:request inSession:session options:options context:nil]; } - (nonnull instancetype)initWithRequest:(nullable NSURLRequest *)request inSession:(nullable NSURLSession *)session options:(SDWebImageDownloaderOptions)options context:(nullable SDWebImageContext *)context { if ((self = [super init])) { _request = [request copy]; _options = options; _context = [context copy]; _callbackTokens = [NSMutableArray new]; _responseModifier = context[SDWebImageContextDownloadResponseModifier]; _decryptor = context[SDWebImageContextDownloadDecryptor]; _executing = NO; _finished = NO; _expectedSize = 0; _unownedSession = session; _downloadCompleted = NO; _coderQueue = [[NSOperationQueue alloc] init]; _coderQueue.maxConcurrentOperationCount = 1; _coderQueue.name = @"com.hackemist.SDWebImageDownloaderOperation.coderQueue"; _imageMap = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:1]; #if SD_UIKIT _backgroundTaskId = UIBackgroundTaskInvalid; #endif } return self; } - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock { return [self addHandlersForProgress:progressBlock completed:completedBlock decodeOptions:nil]; } - (nullable id)addHandlersForProgress:(nullable SDWebImageDownloaderProgressBlock)progressBlock completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock decodeOptions:(nullable SDImageCoderOptions *)decodeOptions { if (!completedBlock && !progressBlock && !decodeOptions) return nil; SDWebImageDownloaderOperationToken *token = [SDWebImageDownloaderOperationToken new]; token.completedBlock = completedBlock; token.progressBlock = progressBlock; token.decodeOptions = decodeOptions; @synchronized (self) { [self.callbackTokens addObject:token]; } return token; } - (BOOL)cancel:(nullable id)token { if (![token isKindOfClass:SDWebImageDownloaderOperationToken.class]) return NO; BOOL shouldCancel = NO; @synchronized (self) { NSArray *tokens = self.callbackTokens; if (tokens.count == 1 && [tokens indexOfObjectIdenticalTo:token] != NSNotFound) { shouldCancel = YES; } } if (shouldCancel) { // Cancel operation running and callback last token's completion block [self cancel]; } else { // Only callback this token's completion block @synchronized (self) { [self.callbackTokens removeObjectIdenticalTo:token]; } [self callCompletionBlockWithToken:token image:nil imageData:nil error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] finished:YES]; } return shouldCancel; } - (void)start { @synchronized (self) { if (self.isCancelled) { if (!self.isFinished) self.finished = YES; // Operation cancelled by user before sending the request [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user before sending the request"}]]; [self reset]; return; } #if SD_UIKIT Class UIApplicationClass = NSClassFromString(@"UIApplication"); BOOL hasApplication = UIApplicationClass && [UIApplicationClass respondsToSelector:@selector(sharedApplication)]; if (hasApplication && [self shouldContinueWhenAppEntersBackground]) { __weak typeof(self) wself = self; UIApplication * app = [UIApplicationClass performSelector:@selector(sharedApplication)]; self.backgroundTaskId = [app beginBackgroundTaskWithExpirationHandler:^{ [wself cancel]; }]; } #endif NSURLSession *session = self.unownedSession; if (!session) { NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration]; sessionConfig.timeoutIntervalForRequest = 15; /** * Create the session for this task * We send nil as delegate queue so that the session creates a serial operation queue for performing all delegate * method calls and completion handler calls. */ session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil]; self.ownedSession = session; } if (self.options & SDWebImageDownloaderIgnoreCachedResponse) { // Grab the cached data for later check NSURLCache *URLCache = session.configuration.URLCache; if (!URLCache) { URLCache = [NSURLCache sharedURLCache]; } NSCachedURLResponse *cachedResponse; // NSURLCache's `cachedResponseForRequest:` is not thread-safe, see https://developer.apple.com/documentation/foundation/nsurlcache#2317483 @synchronized (URLCache) { cachedResponse = [URLCache cachedResponseForRequest:self.request]; } if (cachedResponse) { self.cachedData = cachedResponse.data; self.response = cachedResponse.response; } } if (!session.delegate) { // Session been invalid and has no delegate at all [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Session delegate is nil and invalid"}]]; [self reset]; return; } self.dataTask = [session dataTaskWithRequest:self.request]; self.executing = YES; } if (self.dataTask) { if (self.options & SDWebImageDownloaderHighPriority) { self.dataTask.priority = NSURLSessionTaskPriorityHigh; } else if (self.options & SDWebImageDownloaderLowPriority) { self.dataTask.priority = NSURLSessionTaskPriorityLow; } else { self.dataTask.priority = NSURLSessionTaskPriorityDefault; } [self.dataTask resume]; NSArray *tokens; @synchronized (self) { tokens = [self.callbackTokens copy]; } for (SDWebImageDownloaderOperationToken *token in tokens) { if (token.progressBlock) { token.progressBlock(0, NSURLResponseUnknownLength, self.request.URL); } } __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:strongSelf]; }); } else { if (!self.isFinished) self.finished = YES; [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadOperation userInfo:@{NSLocalizedDescriptionKey : @"Task can't be initialized"}]]; [self reset]; } } - (void)cancel { @synchronized (self) { [self cancelInternal]; } } - (void)cancelInternal { if (self.isFinished) return; [super cancel]; __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf]; }); if (self.dataTask) { // Cancel the URLSession, `URLSession:task:didCompleteWithError:` delegate callback will be ignored [self.dataTask cancel]; self.dataTask = nil; } // NSOperation disallow setFinished=YES **before** operation's start method been called // We check for the initialized status, which is isExecuting == NO && isFinished = NO // Ony update for non-intialized status, which is !(isExecuting == NO && isFinished = NO), or if (self.isExecuting || self.isFinished) {...} if (self.isExecuting || self.isFinished) { if (self.isExecuting) self.executing = NO; if (!self.isFinished) self.finished = YES; } // Operation cancelled by user during sending the request [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}]]; [self reset]; } - (void)done { self.finished = YES; self.executing = NO; [self reset]; } - (void)reset { @synchronized (self) { [self.callbackTokens removeAllObjects]; self.dataTask = nil; if (self.ownedSession) { [self.ownedSession invalidateAndCancel]; self.ownedSession = nil; } #if SD_UIKIT if (self.backgroundTaskId != UIBackgroundTaskInvalid) { // If backgroundTaskId != UIBackgroundTaskInvalid, sharedApplication is always exist UIApplication * app = [UIApplication performSelector:@selector(sharedApplication)]; [app endBackgroundTask:self.backgroundTaskId]; self.backgroundTaskId = UIBackgroundTaskInvalid; } #endif } } - (void)setFinished:(BOOL)finished { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; } - (void)setExecuting:(BOOL)executing { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } - (BOOL)isAsynchronous { return YES; } // Check for unprocessed tokens. // if all tokens have been processed call [self done]. - (void)checkDoneWithImageData:(NSData *)imageData finishedTokens:(NSArray *)finishedTokens { @synchronized (self) { NSMutableArray *tokens = [self.callbackTokens mutableCopy]; [finishedTokens enumerateObjectsUsingBlock:^(SDWebImageDownloaderOperationToken * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { [tokens removeObjectIdenticalTo:obj]; }]; if (tokens.count == 0) { [self done]; } else { // If there are new tokens added during the decoding operation, the decoding operation is supplemented with these new tokens. [self startCoderOperationWithImageData:imageData pendingTokens:tokens finishedTokens:finishedTokens]; } } } - (void)startCoderOperationWithImageData:(NSData *)imageData pendingTokens:(NSArray *)pendingTokens finishedTokens:(NSArray *)finishedTokens { @weakify(self); for (SDWebImageDownloaderOperationToken *token in pendingTokens) { [self.coderQueue addOperationWithBlock:^{ @strongify(self); if (!self) { return; } UIImage *image; // check if we already decode this variant of image for current callback if (token.decodeOptions) { image = [self.imageMap objectForKey:token.decodeOptions]; } if (!image) { // check if we already use progressive decoding, use that to produce faster decoding id progressiveCoder = SDImageLoaderGetProgressiveCoder(self); SDWebImageOptions options = [[self class] imageOptionsFromDownloaderOptions:self.options]; SDWebImageContext *context; if (token.decodeOptions) { SDWebImageMutableContext *mutableContext = [NSMutableDictionary dictionaryWithDictionary:self.context]; SDSetDecodeOptionsToContext(mutableContext, &options, token.decodeOptions); context = [mutableContext copy]; } else { context = self.context; } if (progressiveCoder) { image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, YES, self, options, context); } else { image = SDImageLoaderDecodeImageData(imageData, self.request.URL, options, context); } if (image && token.decodeOptions) { [self.imageMap setObject:image forKey:token.decodeOptions]; } } CGSize imageSize = image.size; if (imageSize.width == 0 || imageSize.height == 0) { NSString *description = image == nil ? @"Downloaded image decode failed" : @"Downloaded image has 0 pixels"; NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : description}]; [self callCompletionBlockWithToken:token image:nil imageData:nil error:error finished:YES]; } else { [self callCompletionBlockWithToken:token image:image imageData:imageData error:nil finished:YES]; } }]; } // call [self done] after all completed block was dispatched dispatch_block_t doneBlock = ^{ @strongify(self); if (!self) { return; } // Check for new tokens added during the decode operation. [self checkDoneWithImageData:imageData finishedTokens:[finishedTokens arrayByAddingObjectsFromArray:pendingTokens]]; }; if (@available(iOS 13, tvOS 13, macOS 10.15, watchOS 6, *)) { // seems faster than `addOperationWithBlock` [self.coderQueue addBarrierBlock:doneBlock]; } else { // serial queue, this does the same effect in semantics [self.coderQueue addOperationWithBlock:doneBlock]; } } #pragma mark NSURLSessionDataDelegate - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition disposition))completionHandler { NSURLSessionResponseDisposition disposition = NSURLSessionResponseAllow; // Check response modifier, if return nil, will marked as cancelled. BOOL valid = YES; if (self.responseModifier && response) { response = [self.responseModifier modifiedResponseWithResponse:response]; if (!response) { valid = NO; self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadResponse userInfo:@{NSLocalizedDescriptionKey : @"Download marked as failed because response is nil"}]; } } NSInteger expected = (NSInteger)response.expectedContentLength; expected = expected > 0 ? expected : 0; self.expectedSize = expected; self.response = response; // Check status code valid (defaults [200,400)) NSInteger statusCode = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).statusCode : 0; BOOL statusCodeValid = YES; if (valid && statusCode > 0 && self.acceptableStatusCodes) { statusCodeValid = [self.acceptableStatusCodes containsIndex:statusCode]; } if (!statusCodeValid) { valid = NO; self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadStatusCode userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Download marked as failed because of invalid response status code %ld", (long)statusCode], SDWebImageErrorDownloadStatusCodeKey : @(statusCode), SDWebImageErrorDownloadResponseKey : response}]; } // Check content type valid (defaults nil) NSString *contentType = [response isKindOfClass:NSHTTPURLResponse.class] ? ((NSHTTPURLResponse *)response).MIMEType : nil; BOOL contentTypeValid = YES; if (valid && contentType.length > 0 && self.acceptableContentTypes) { contentTypeValid = [self.acceptableContentTypes containsObject:contentType]; } if (!contentTypeValid) { valid = NO; self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidDownloadContentType userInfo:@{NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Download marked as failed because of invalid response content type %@", contentType], SDWebImageErrorDownloadContentTypeKey : contentType, SDWebImageErrorDownloadResponseKey : response}]; } //'304 Not Modified' is an exceptional one //URLSession current behavior will return 200 status code when the server respond 304 and URLCache hit. But this is not a standard behavior and we just add a check if (valid && statusCode == 304 && !self.cachedData) { valid = NO; self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey: @"Download response status code is 304 not modified and ignored", SDWebImageErrorDownloadResponseKey : response}]; } if (valid) { NSArray *tokens; @synchronized (self) { tokens = [self.callbackTokens copy]; } for (SDWebImageDownloaderOperationToken *token in tokens) { if (token.progressBlock) { token.progressBlock(0, expected, self.request.URL); } } } else { // Status code invalid and marked as cancelled. Do not call `[self.dataTask cancel]` which may mass up URLSession life cycle disposition = NSURLSessionResponseCancel; } __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadReceiveResponseNotification object:strongSelf]; }); if (completionHandler) { completionHandler(disposition); } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { if (!self.imageData) { self.imageData = [[NSMutableData alloc] initWithCapacity:self.expectedSize]; } [self.imageData appendData:data]; self.receivedSize = self.imageData.length; NSArray *tokens; @synchronized (self) { tokens = [self.callbackTokens copy]; } if (self.expectedSize == 0) { // Unknown expectedSize, immediately call progressBlock and return for (SDWebImageDownloaderOperationToken *token in tokens) { if (token.progressBlock) { token.progressBlock(self.receivedSize, self.expectedSize, self.request.URL); } } return; } // Get the finish status BOOL finished = (self.receivedSize >= self.expectedSize); // Get the current progress double currentProgress = (double)self.receivedSize / (double)self.expectedSize; double previousProgress = self.previousProgress; double progressInterval = currentProgress - previousProgress; // Check if we need callback progress if (!finished && (progressInterval < self.minimumProgressInterval)) { return; } self.previousProgress = currentProgress; // Using data decryptor will disable the progressive decoding, since there are no support for progressive decrypt BOOL supportProgressive = (self.options & SDWebImageDownloaderProgressiveLoad) && !self.decryptor; // When multiple thumbnail decoding use different size, this progressive decoding will cause issue because each callback assume called with different size's image, can not share the same decoding part // We currently only pick the first thumbnail size, see #3423 talks // Progressive decoding Only decode partial image, full image in `URLSession:task:didCompleteWithError:` if (supportProgressive && !finished) { // Get the image data NSData *imageData = self.imageData; // keep maximum one progressive decode process during download if (imageData && self.coderQueue.operationCount == 0) { // NSOperation have autoreleasepool, don't need to create extra one @weakify(self); [self.coderQueue addOperationWithBlock:^{ @strongify(self); if (!self) { return; } // When cancelled or transfer finished (`didCompleteWithError`), cancel the progress callback, only completed block is called and enough @synchronized (self) { if (self.isCancelled || self.isDownloadCompleted) { return; } } UIImage *image = SDImageLoaderDecodeProgressiveImageData(imageData, self.request.URL, NO, self, [[self class] imageOptionsFromDownloaderOptions:self.options], self.context); if (image) { // We do not keep the progressive decoding image even when `finished`=YES. Because they are for view rendering but not take full function from downloader options. And some coders implementation may not keep consistent between progressive decoding and normal decoding. [self callCompletionBlocksWithImage:image imageData:nil error:nil finished:NO]; } }]; } } for (SDWebImageDownloaderOperationToken *token in tokens) { if (token.progressBlock) { token.progressBlock(self.receivedSize, self.expectedSize, self.request.URL); } } } - (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask willCacheResponse:(NSCachedURLResponse *)proposedResponse completionHandler:(void (^)(NSCachedURLResponse *cachedResponse))completionHandler { NSCachedURLResponse *cachedResponse = proposedResponse; if (!(self.options & SDWebImageDownloaderUseNSURLCache)) { // Prevents caching of responses cachedResponse = nil; } if (completionHandler) { completionHandler(cachedResponse); } } #pragma mark NSURLSessionTaskDelegate - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error { // If we already cancel the operation or anything mark the operation finished, don't callback twice if (self.isFinished) return; self.downloadCompleted = YES; NSArray *tokens; @synchronized (self) { tokens = [self.callbackTokens copy]; self.dataTask = nil; __block typeof(self) strongSelf = self; dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:strongSelf]; if (!error) { [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadFinishNotification object:strongSelf]; } }); } // make sure to call `[self done]` to mark operation as finished if (error) { // custom error instead of URLSession error if (self.responseError) { error = self.responseError; } [self callCompletionBlocksWithError:error]; [self done]; } else { if (tokens.count > 0) { NSData *imageData = self.imageData; // data decryptor if (imageData && self.decryptor) { imageData = [self.decryptor decryptedDataWithData:imageData response:self.response]; } if (imageData) { /** if you specified to only use cached data via `SDWebImageDownloaderIgnoreCachedResponse`, * then we should check if the cached data is equal to image data */ if (self.options & SDWebImageDownloaderIgnoreCachedResponse && [self.cachedData isEqualToData:imageData]) { self.responseError = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCacheNotModified userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image is not modified and ignored", SDWebImageErrorDownloadResponseKey : self.response}]; // call completion block with not modified error [self callCompletionBlocksWithError:self.responseError]; [self done]; } else { // decode the image in coder queue, cancel all previous decoding process [self.coderQueue cancelAllOperations]; [self startCoderOperationWithImageData:imageData pendingTokens:tokens finishedTokens:@[]]; } } else { [self callCompletionBlocksWithError:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorBadImageData userInfo:@{NSLocalizedDescriptionKey : @"Image data is nil"}]]; [self done]; } } else { [self done]; } } } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler { NSURLSessionAuthChallengeDisposition disposition = NSURLSessionAuthChallengePerformDefaultHandling; __block NSURLCredential *credential = nil; if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates)) { disposition = NSURLSessionAuthChallengePerformDefaultHandling; } else { credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; disposition = NSURLSessionAuthChallengeUseCredential; } } else { if (challenge.previousFailureCount == 0) { if (self.credential) { credential = self.credential; disposition = NSURLSessionAuthChallengeUseCredential; } else { // Web Server like Nginx can set `ssl_verify_client` to optional but not always on // We'd better use default handling here disposition = NSURLSessionAuthChallengePerformDefaultHandling; } } else { disposition = NSURLSessionAuthChallengeCancelAuthenticationChallenge; } } if (completionHandler) { completionHandler(disposition, credential); } } - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics API_AVAILABLE(macos(10.12), ios(10.0), watchos(3.0), tvos(10.0)) { self.metrics = metrics; } #pragma mark Helper methods #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" + (SDWebImageOptions)imageOptionsFromDownloaderOptions:(SDWebImageDownloaderOptions)downloadOptions { SDWebImageOptions options = 0; if (downloadOptions & SDWebImageDownloaderScaleDownLargeImages) options |= SDWebImageScaleDownLargeImages; if (downloadOptions & SDWebImageDownloaderDecodeFirstFrameOnly) options |= SDWebImageDecodeFirstFrameOnly; if (downloadOptions & SDWebImageDownloaderPreloadAllFrames) options |= SDWebImagePreloadAllFrames; if (downloadOptions & SDWebImageDownloaderAvoidDecodeImage) options |= SDWebImageAvoidDecodeImage; if (downloadOptions & SDWebImageDownloaderMatchAnimatedImageClass) options |= SDWebImageMatchAnimatedImageClass; return options; } #pragma clang diagnostic pop - (BOOL)shouldContinueWhenAppEntersBackground { return SD_OPTIONS_CONTAINS(self.options, SDWebImageDownloaderContinueInBackground); } - (void)callCompletionBlocksWithError:(nullable NSError *)error { [self callCompletionBlocksWithImage:nil imageData:nil error:error finished:YES]; } - (void)callCompletionBlocksWithImage:(nullable UIImage *)image imageData:(nullable NSData *)imageData error:(nullable NSError *)error finished:(BOOL)finished { NSArray *tokens; @synchronized (self) { tokens = [self.callbackTokens copy]; } for (SDWebImageDownloaderOperationToken *token in tokens) { SDWebImageDownloaderCompletedBlock completedBlock = token.completedBlock; if (completedBlock) { SDCallbackQueue *queue = self.context[SDWebImageContextCallbackQueue]; [(queue ?: SDCallbackQueue.mainQueue) async:^{ completedBlock(image, imageData, error, finished); }]; } } } - (void)callCompletionBlockWithToken:(nonnull SDWebImageDownloaderOperationToken *)token image:(nullable UIImage *)image imageData:(nullable NSData *)imageData error:(nullable NSError *)error finished:(BOOL)finished { SDWebImageDownloaderCompletedBlock completedBlock = token.completedBlock; if (completedBlock) { SDCallbackQueue *queue = self.context[SDWebImageContextCallbackQueue]; [(queue ?: SDCallbackQueue.mainQueue) async:^{ completedBlock(image, imageData, error, finished); }]; } } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderRequestModifier.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" typedef NSURLRequest * _Nullable (^SDWebImageDownloaderRequestModifierBlock)(NSURLRequest * _Nonnull request); /** This is the protocol for downloader request modifier. We can use a block to specify the downloader request modifier. But Using protocol can make this extensible, and allow Swift user to use it easily instead of using `@convention(block)` to store a block into context options. */ @protocol SDWebImageDownloaderRequestModifier /// Modify the original URL request and return a new one instead. You can modify the HTTP header, cachePolicy, etc for this URL. /// @param request The original URL request for image loading /// @note If return nil, the URL request will be cancelled. - (nullable NSURLRequest *)modifiedRequestWithRequest:(nonnull NSURLRequest *)request; @end /** A downloader request modifier class with block. */ @interface SDWebImageDownloaderRequestModifier : NSObject /// Create the request modifier with block /// @param block A block to control modifier logic - (nonnull instancetype)initWithBlock:(nonnull SDWebImageDownloaderRequestModifierBlock)block; /// Create the request modifier with block /// @param block A block to control modifier logic + (nonnull instancetype)requestModifierWithBlock:(nonnull SDWebImageDownloaderRequestModifierBlock)block; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; @end /** A convenient request modifier to provide the HTTP request including HTTP Method, Headers and Body. */ @interface SDWebImageDownloaderRequestModifier (Conveniences) /// Create the request modifier with HTTP Method. /// @param method HTTP Method, nil means to GET. /// @note This is for convenience, if you need code to control the logic, use block API instead. - (nonnull instancetype)initWithMethod:(nullable NSString *)method; /// Create the request modifier with HTTP Headers. /// @param headers HTTP Headers. Case insensitive according to HTTP/1.1(HTTP/2) standard. The headers will override the same fields from original request. /// @note This is for convenience, if you need code to control the logic, use block API instead. - (nonnull instancetype)initWithHeaders:(nullable NSDictionary *)headers; /// Create the request modifier with HTTP Body. /// @param body HTTP Body. /// @note This is for convenience, if you need code to control the logic, use block API instead. - (nonnull instancetype)initWithBody:(nullable NSData *)body; /// Create the request modifier with HTTP Method, Headers and Body. /// @param method HTTP Method, nil means to GET. /// @param headers HTTP Headers. Case insensitive according to HTTP/1.1(HTTP/2) standard. The headers will override the same fields from original request. /// @param body HTTP Body. /// @note This is for convenience, if you need code to control the logic, use block API instead. - (nonnull instancetype)initWithMethod:(nullable NSString *)method headers:(nullable NSDictionary *)headers body:(nullable NSData *)body; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderRequestModifier.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageDownloaderRequestModifier.h" @interface SDWebImageDownloaderRequestModifier () @property (nonatomic, copy, nonnull) SDWebImageDownloaderRequestModifierBlock block; @end @implementation SDWebImageDownloaderRequestModifier - (instancetype)initWithBlock:(SDWebImageDownloaderRequestModifierBlock)block { self = [super init]; if (self) { self.block = block; } return self; } + (instancetype)requestModifierWithBlock:(SDWebImageDownloaderRequestModifierBlock)block { SDWebImageDownloaderRequestModifier *requestModifier = [[SDWebImageDownloaderRequestModifier alloc] initWithBlock:block]; return requestModifier; } - (NSURLRequest *)modifiedRequestWithRequest:(NSURLRequest *)request { if (!self.block) { return nil; } return self.block(request); } @end @implementation SDWebImageDownloaderRequestModifier (Conveniences) - (instancetype)initWithMethod:(NSString *)method { return [self initWithMethod:method headers:nil body:nil]; } - (instancetype)initWithHeaders:(NSDictionary *)headers { return [self initWithMethod:nil headers:headers body:nil]; } - (instancetype)initWithBody:(NSData *)body { return [self initWithMethod:nil headers:nil body:body]; } - (instancetype)initWithMethod:(NSString *)method headers:(NSDictionary *)headers body:(NSData *)body { method = method ? [method copy] : @"GET"; headers = [headers copy]; body = [body copy]; return [self initWithBlock:^NSURLRequest * _Nullable(NSURLRequest * _Nonnull request) { NSMutableURLRequest *mutableRequest = [request mutableCopy]; mutableRequest.HTTPMethod = method; mutableRequest.HTTPBody = body; for (NSString *header in headers) { NSString *value = headers[header]; [mutableRequest setValue:value forHTTPHeaderField:header]; } return [mutableRequest copy]; }]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderResponseModifier.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" typedef NSURLResponse * _Nullable (^SDWebImageDownloaderResponseModifierBlock)(NSURLResponse * _Nonnull response); /** This is the protocol for downloader response modifier. We can use a block to specify the downloader response modifier. But Using protocol can make this extensible, and allow Swift user to use it easily instead of using `@convention(block)` to store a block into context options. */ @protocol SDWebImageDownloaderResponseModifier /// Modify the original URL response and return a new response. You can use this to check MIME-Type, mock server response, etc. /// @param response The original URL response, note for HTTP request it's actually a `NSHTTPURLResponse` instance /// @note If nil is returned, the image download will marked as cancelled with error `SDWebImageErrorInvalidDownloadResponse` - (nullable NSURLResponse *)modifiedResponseWithResponse:(nonnull NSURLResponse *)response; @end /** A downloader response modifier class with block. */ @interface SDWebImageDownloaderResponseModifier : NSObject /// Create the response modifier with block /// @param block A block to control modifier logic - (nonnull instancetype)initWithBlock:(nonnull SDWebImageDownloaderResponseModifierBlock)block; /// Create the response modifier with block /// @param block A block to control modifier logic + (nonnull instancetype)responseModifierWithBlock:(nonnull SDWebImageDownloaderResponseModifierBlock)block; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; @end /** A convenient response modifier to provide the HTTP response including HTTP Status Code, Version and Headers. */ @interface SDWebImageDownloaderResponseModifier (Conveniences) /// Create the response modifier with HTTP Status code. /// @param statusCode HTTP Status Code. /// @note This is for convenience, if you need code to control the logic, use block API instead. - (nonnull instancetype)initWithStatusCode:(NSInteger)statusCode; /// Create the response modifier with HTTP Version. Status code defaults to 200. /// @param version HTTP Version, nil means "HTTP/1.1". /// @note This is for convenience, if you need code to control the logic, use block API instead. - (nonnull instancetype)initWithVersion:(nullable NSString *)version; /// Create the response modifier with HTTP Headers. Status code defaults to 200. /// @param headers HTTP Headers. Case insensitive according to HTTP/1.1(HTTP/2) standard. The headers will override the same fields from original response. /// @note This is for convenience, if you need code to control the logic, use block API instead. - (nonnull instancetype)initWithHeaders:(nullable NSDictionary *)headers; /// Create the response modifier with HTTP Status Code, Version and Headers. /// @param statusCode HTTP Status Code. /// @param version HTTP Version, nil means "HTTP/1.1". /// @param headers HTTP Headers. Case insensitive according to HTTP/1.1(HTTP/2) standard. The headers will override the same fields from original response. /// @note This is for convenience, if you need code to control the logic, use block API instead. - (nonnull instancetype)initWithStatusCode:(NSInteger)statusCode version:(nullable NSString *)version headers:(nullable NSDictionary *)headers; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageDownloaderResponseModifier.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageDownloaderResponseModifier.h" @interface SDWebImageDownloaderResponseModifier () @property (nonatomic, copy, nonnull) SDWebImageDownloaderResponseModifierBlock block; @end @implementation SDWebImageDownloaderResponseModifier - (instancetype)initWithBlock:(SDWebImageDownloaderResponseModifierBlock)block { self = [super init]; if (self) { self.block = block; } return self; } + (instancetype)responseModifierWithBlock:(SDWebImageDownloaderResponseModifierBlock)block { SDWebImageDownloaderResponseModifier *responseModifier = [[SDWebImageDownloaderResponseModifier alloc] initWithBlock:block]; return responseModifier; } - (nullable NSURLResponse *)modifiedResponseWithResponse:(nonnull NSURLResponse *)response { if (!self.block) { return nil; } return self.block(response); } @end @implementation SDWebImageDownloaderResponseModifier (Conveniences) - (instancetype)initWithStatusCode:(NSInteger)statusCode { return [self initWithStatusCode:statusCode version:nil headers:nil]; } - (instancetype)initWithVersion:(NSString *)version { return [self initWithStatusCode:200 version:version headers:nil]; } - (instancetype)initWithHeaders:(NSDictionary *)headers { return [self initWithStatusCode:200 version:nil headers:headers]; } - (instancetype)initWithStatusCode:(NSInteger)statusCode version:(NSString *)version headers:(NSDictionary *)headers { version = version ? [version copy] : @"HTTP/1.1"; headers = [headers copy]; return [self initWithBlock:^NSURLResponse * _Nullable(NSURLResponse * _Nonnull response) { if (![response isKindOfClass:NSHTTPURLResponse.class]) { return response; } NSMutableDictionary *mutableHeaders = [((NSHTTPURLResponse *)response).allHeaderFields mutableCopy]; for (NSString *header in headers) { NSString *value = headers[header]; mutableHeaders[header] = value; } NSHTTPURLResponse *httpResponse = [[NSHTTPURLResponse alloc] initWithURL:response.URL statusCode:statusCode HTTPVersion:version headerFields:[mutableHeaders copy]]; return httpResponse; }]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageError.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Jamie Pinkham * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" /// An error domain represent SDWebImage loading system with custom codes FOUNDATION_EXPORT NSErrorDomain const _Nonnull SDWebImageErrorDomain; /// The response instance for invalid download response (NSURLResponse *) FOUNDATION_EXPORT NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadResponseKey; /// The HTTP status code for invalid download response (NSNumber *) FOUNDATION_EXPORT NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadStatusCodeKey; /// The HTTP MIME content type for invalid download response (NSString *) FOUNDATION_EXPORT NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadContentTypeKey; /// SDWebImage error domain and codes typedef NS_ERROR_ENUM(SDWebImageErrorDomain, SDWebImageError) { SDWebImageErrorInvalidURL = 1000, // The URL is invalid, such as nil URL or corrupted URL SDWebImageErrorBadImageData = 1001, // The image data can not be decoded to image, or the image data is empty SDWebImageErrorCacheNotModified = 1002, // The remote location specify that the cached image is not modified, such as the HTTP response 304 code. It's useful for `SDWebImageRefreshCached` SDWebImageErrorBlackListed = 1003, // The URL is blacklisted because of unrecoverable failure marked by downloader (such as 404), you can use `.retryFailed` option to avoid this SDWebImageErrorInvalidDownloadOperation = 2000, // The image download operation is invalid, such as nil operation or unexpected error occur when operation initialized SDWebImageErrorInvalidDownloadStatusCode = 2001, // The image download response a invalid status code. You can check the status code in error's userInfo under `SDWebImageErrorDownloadStatusCodeKey` SDWebImageErrorCancelled = 2002, // The image loading operation is cancelled before finished, during either async disk cache query, or waiting before actual network request. For actual network request error, check `NSURLErrorDomain` error domain and code. SDWebImageErrorInvalidDownloadResponse = 2003, // When using response modifier, the modified download response is nil and marked as failed. SDWebImageErrorInvalidDownloadContentType = 2004, // The image download response a invalid content type. You can check the MIME content type in error's userInfo under `SDWebImageErrorDownloadContentTypeKey` }; ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageError.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Jamie Pinkham * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageError.h" NSErrorDomain const _Nonnull SDWebImageErrorDomain = @"SDWebImageErrorDomain"; NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadResponseKey = @"SDWebImageErrorDownloadResponseKey"; NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadStatusCodeKey = @"SDWebImageErrorDownloadStatusCodeKey"; NSErrorUserInfoKey const _Nonnull SDWebImageErrorDownloadContentTypeKey = @"SDWebImageErrorDownloadContentTypeKey"; ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageIndicator.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_UIKIT || SD_MAC /** A protocol to custom the indicator during the image loading. All of these methods are called from main queue. */ @protocol SDWebImageIndicator @required /** The view associate to the indicator. @return The indicator view */ @property (nonatomic, strong, readonly, nonnull) UIView *indicatorView; /** Start the animating for indicator. */ - (void)startAnimatingIndicator; /** Stop the animating for indicator. */ - (void)stopAnimatingIndicator; @optional /** Update the loading progress (0-1.0) for indicator. Optional @param progress The progress, value between 0 and 1.0 */ - (void)updateIndicatorProgress:(double)progress; @end #pragma mark - Activity Indicator /** Activity indicator class. for UIKit(macOS), it use a `UIActivityIndicatorView`. for AppKit(macOS), it use a `NSProgressIndicator` with the spinning style. */ @interface SDWebImageActivityIndicator : NSObject #if SD_UIKIT @property (nonatomic, strong, readonly, nonnull) UIActivityIndicatorView *indicatorView; #else @property (nonatomic, strong, readonly, nonnull) NSProgressIndicator *indicatorView; #endif @end /** Convenience way to use activity indicator. */ @interface SDWebImageActivityIndicator (Conveniences) #if !SD_VISION /// These indicator use the fixed color without dark mode support /// gray-style activity indicator @property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *grayIndicator; /// large gray-style activity indicator @property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *grayLargeIndicator; /// white-style activity indicator @property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *whiteIndicator; /// large white-style activity indicator @property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *whiteLargeIndicator; #endif /// These indicator use the system style, supports dark mode if available (iOS 13+/macOS 10.14+) /// large activity indicator @property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *largeIndicator; /// medium activity indicator @property (nonatomic, class, nonnull, readonly) SDWebImageActivityIndicator *mediumIndicator; @end #pragma mark - Progress Indicator /** Progress indicator class. for UIKit(macOS), it use a `UIProgressView`. for AppKit(macOS), it use a `NSProgressIndicator` with the bar style. */ @interface SDWebImageProgressIndicator : NSObject #if SD_UIKIT @property (nonatomic, strong, readonly, nonnull) UIProgressView *indicatorView; #else @property (nonatomic, strong, readonly, nonnull) NSProgressIndicator *indicatorView; #endif @end /** Convenience way to create progress indicator. Remember to specify the indicator width or use layout constraint if need. */ @interface SDWebImageProgressIndicator (Conveniences) /// default-style progress indicator @property (nonatomic, class, nonnull, readonly) SDWebImageProgressIndicator *defaultIndicator; #if SD_UIKIT /// bar-style progress indicator @property (nonatomic, class, nonnull, readonly) SDWebImageProgressIndicator *barIndicator API_UNAVAILABLE(tvos); #endif @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageIndicator.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageIndicator.h" #if SD_UIKIT || SD_MAC #if SD_MAC #import #import #endif #pragma mark - Activity Indicator @interface SDWebImageActivityIndicator () #if SD_UIKIT @property (nonatomic, strong, readwrite, nonnull) UIActivityIndicatorView *indicatorView; #else @property (nonatomic, strong, readwrite, nonnull) NSProgressIndicator *indicatorView; #endif @end @implementation SDWebImageActivityIndicator - (instancetype)init { self = [super init]; if (self) { [self commonInit]; } return self; } #if SD_UIKIT #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" - (void)commonInit { #if SD_VISION UIActivityIndicatorViewStyle style = UIActivityIndicatorViewStyleMedium; #else UIActivityIndicatorViewStyle style; if (@available(iOS 13.0, tvOS 13.0, *)) { style = UIActivityIndicatorViewStyleMedium; } else { style = UIActivityIndicatorViewStyleWhite; } #endif self.indicatorView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:style]; self.indicatorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; } #pragma clang diagnostic pop #endif #if SD_MAC - (void)commonInit { self.indicatorView = [[NSProgressIndicator alloc] initWithFrame:NSZeroRect]; self.indicatorView.style = NSProgressIndicatorStyleSpinning; self.indicatorView.controlSize = NSControlSizeSmall; [self.indicatorView sizeToFit]; self.indicatorView.autoresizingMask = NSViewMaxXMargin | NSViewMinXMargin | NSViewMaxYMargin | NSViewMinYMargin; } #endif - (void)startAnimatingIndicator { #if SD_UIKIT [self.indicatorView startAnimating]; #else [self.indicatorView startAnimation:nil]; #endif self.indicatorView.hidden = NO; } - (void)stopAnimatingIndicator { #if SD_UIKIT [self.indicatorView stopAnimating]; #else [self.indicatorView stopAnimation:nil]; #endif self.indicatorView.hidden = YES; } @end @implementation SDWebImageActivityIndicator (Conveniences) #if !SD_VISION #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" + (SDWebImageActivityIndicator *)grayIndicator { SDWebImageActivityIndicator *indicator = [SDWebImageActivityIndicator new]; #if SD_UIKIT #if SD_IOS indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; #else indicator.indicatorView.color = [UIColor colorWithWhite:0 alpha:0.45]; // Color from `UIActivityIndicatorViewStyleGray` #endif #else indicator.indicatorView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; // Disable dark mode support #endif return indicator; } + (SDWebImageActivityIndicator *)grayLargeIndicator { SDWebImageActivityIndicator *indicator = SDWebImageActivityIndicator.grayIndicator; #if SD_UIKIT UIColor *grayColor = indicator.indicatorView.color; indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge; indicator.indicatorView.color = grayColor; #else indicator.indicatorView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; // Disable dark mode support indicator.indicatorView.controlSize = NSControlSizeRegular; #endif [indicator.indicatorView sizeToFit]; return indicator; } + (SDWebImageActivityIndicator *)whiteIndicator { SDWebImageActivityIndicator *indicator = [SDWebImageActivityIndicator new]; #if SD_UIKIT indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite; #else indicator.indicatorView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; // Disable dark mode support CIFilter *lighten = [CIFilter filterWithName:@"CIColorControls"]; [lighten setDefaults]; [lighten setValue:@(1) forKey:kCIInputBrightnessKey]; indicator.indicatorView.contentFilters = @[lighten]; #endif return indicator; } + (SDWebImageActivityIndicator *)whiteLargeIndicator { SDWebImageActivityIndicator *indicator = SDWebImageActivityIndicator.whiteIndicator; #if SD_UIKIT indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge; #else indicator.indicatorView.appearance = [NSAppearance appearanceNamed:NSAppearanceNameAqua]; // Disable dark mode support indicator.indicatorView.controlSize = NSControlSizeRegular; [indicator.indicatorView sizeToFit]; #endif return indicator; } #endif + (SDWebImageActivityIndicator *)largeIndicator { SDWebImageActivityIndicator *indicator = [SDWebImageActivityIndicator new]; #if SD_VISION indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleLarge; #elif SD_UIKIT if (@available(iOS 13.0, tvOS 13.0, *)) { indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleLarge; } else { indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhiteLarge; } #else indicator.indicatorView.controlSize = NSControlSizeRegular; [indicator.indicatorView sizeToFit]; #endif return indicator; } + (SDWebImageActivityIndicator *)mediumIndicator { SDWebImageActivityIndicator *indicator = [SDWebImageActivityIndicator new]; #if SD_VISION indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium; #elif SD_UIKIT if (@available(iOS 13.0, tvOS 13.0, *)) { indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleMedium; } else { indicator.indicatorView.activityIndicatorViewStyle = UIActivityIndicatorViewStyleWhite; } #else indicator.indicatorView.controlSize = NSControlSizeSmall; [indicator.indicatorView sizeToFit]; #endif return indicator; } #pragma clang diagnostic pop @end #pragma mark - Progress Indicator @interface SDWebImageProgressIndicator () #if SD_UIKIT @property (nonatomic, strong, readwrite, nonnull) UIProgressView *indicatorView; #else @property (nonatomic, strong, readwrite, nonnull) NSProgressIndicator *indicatorView; #endif @end @implementation SDWebImageProgressIndicator - (instancetype)init { self = [super init]; if (self) { [self commonInit]; } return self; } #if SD_UIKIT - (void)commonInit { self.indicatorView = [[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault]; self.indicatorView.autoresizingMask = UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleBottomMargin; } #endif #if SD_MAC - (void)commonInit { self.indicatorView = [[NSProgressIndicator alloc] initWithFrame:NSMakeRect(0, 0, 160, 0)]; // Width from `UIProgressView` default width self.indicatorView.style = NSProgressIndicatorStyleBar; self.indicatorView.controlSize = NSControlSizeSmall; [self.indicatorView sizeToFit]; self.indicatorView.autoresizingMask = NSViewMaxXMargin | NSViewMinXMargin | NSViewMaxYMargin | NSViewMinYMargin; } #endif #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" - (void)startAnimatingIndicator { self.indicatorView.hidden = NO; #if SD_UIKIT if ([self.indicatorView respondsToSelector:@selector(observedProgress)] && self.indicatorView.observedProgress) { // Ignore NSProgress } else { self.indicatorView.progress = 0; } #else self.indicatorView.indeterminate = YES; self.indicatorView.doubleValue = 0; [self.indicatorView startAnimation:nil]; #endif } - (void)stopAnimatingIndicator { self.indicatorView.hidden = YES; #if SD_UIKIT if ([self.indicatorView respondsToSelector:@selector(observedProgress)] && self.indicatorView.observedProgress) { // Ignore NSProgress } else { self.indicatorView.progress = 1; } #else self.indicatorView.indeterminate = NO; self.indicatorView.doubleValue = 100; [self.indicatorView stopAnimation:nil]; #endif } - (void)updateIndicatorProgress:(double)progress { #if SD_UIKIT if ([self.indicatorView respondsToSelector:@selector(observedProgress)] && self.indicatorView.observedProgress) { // Ignore NSProgress } else { [self.indicatorView setProgress:progress animated:YES]; } #else self.indicatorView.indeterminate = progress > 0 ? NO : YES; self.indicatorView.doubleValue = progress * 100; #endif } #pragma clang diagnostic pop @end @implementation SDWebImageProgressIndicator (Conveniences) + (SDWebImageProgressIndicator *)defaultIndicator { SDWebImageProgressIndicator *indicator = [SDWebImageProgressIndicator new]; return indicator; } #if SD_UIKIT + (SDWebImageProgressIndicator *)barIndicator API_UNAVAILABLE(tvos) { SDWebImageProgressIndicator *indicator = [SDWebImageProgressIndicator new]; indicator.indicatorView.progressViewStyle = UIProgressViewStyleBar; return indicator; } #endif @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import "SDWebImageOperation.h" #import "SDImageCacheDefine.h" #import "SDImageLoader.h" #import "SDImageTransformer.h" #import "SDWebImageCacheKeyFilter.h" #import "SDWebImageCacheSerializer.h" #import "SDWebImageOptionsProcessor.h" typedef void(^SDExternalCompletionBlock)(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL); typedef void(^SDInternalCompletionBlock)(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL); /** A combined operation representing the cache and loader operation. You can use it to cancel the load process. */ @interface SDWebImageCombinedOperation : NSObject /** Cancel the current operation, including cache and loader process */ - (void)cancel; /// Whether the operation has been cancelled. @property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled; /** The cache operation from the image cache query */ @property (strong, nonatomic, nullable, readonly) id cacheOperation; /** The loader operation from the image loader (such as download operation) */ @property (strong, nonatomic, nullable, readonly) id loaderOperation; @end @class SDWebImageManager; /** The manager delegate protocol. */ @protocol SDWebImageManagerDelegate @optional /** * Controls which image should be downloaded when the image is not found in the cache. * * @param imageManager The current `SDWebImageManager` * @param imageURL The url of the image to be downloaded * * @return Return NO to prevent the downloading of the image on cache misses. If not implemented, YES is implied. */ - (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldDownloadImageForURL:(nonnull NSURL *)imageURL; /** * Controls the complicated logic to mark as failed URLs when download error occur. * If the delegate implement this method, we will not use the built-in way to mark URL as failed based on error code; @param imageManager The current `SDWebImageManager` @param imageURL The url of the image @param error The download error for the url @return Whether to block this url or not. Return YES to mark this URL as failed. */ - (BOOL)imageManager:(nonnull SDWebImageManager *)imageManager shouldBlockFailedURL:(nonnull NSURL *)imageURL withError:(nonnull NSError *)error; @end /** * The SDWebImageManager is the class behind the UIImageView+WebCache category and likes. * It ties the asynchronous downloader (SDWebImageDownloader) with the image cache store (SDImageCache). * You can use this class directly to benefit from web image downloading with caching in another context than * a UIView. * * Here is a simple example of how to use SDWebImageManager: * * @code SDWebImageManager *manager = [SDWebImageManager sharedManager]; [manager loadImageWithURL:imageURL options:0 progress:nil completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { if (image) { // do something with image } }]; * @endcode */ @interface SDWebImageManager : NSObject /** * The delegate for manager. Defaults to nil. */ @property (weak, nonatomic, nullable) id delegate; /** * The image cache used by manager to query image cache. */ @property (strong, nonatomic, readonly, nonnull) id imageCache; /** * The image loader used by manager to load image. */ @property (strong, nonatomic, readonly, nonnull) id imageLoader; /** The image transformer for manager. It's used for image transform after the image load finished and store the transformed image to cache, see `SDImageTransformer`. Defaults to nil, which means no transform is applied. @note This will affect all the load requests for this manager if you provide. However, you can pass `SDWebImageContextImageTransformer` in context arg to explicitly use that transformer instead. */ @property (strong, nonatomic, nullable) id transformer; /** * The cache filter is used to convert an URL into a cache key each time SDWebImageManager need cache key to use image cache. * * The following example sets a filter in the application delegate that will remove any query-string from the * URL before to use it as a cache key: * * @code SDWebImageManager.sharedManager.cacheKeyFilter =[SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:^NSString * _Nullable(NSURL * _Nonnull url) { url = [[NSURL alloc] initWithScheme:url.scheme host:url.host path:url.path]; return [url absoluteString]; }]; * @endcode */ @property (nonatomic, strong, nullable) id cacheKeyFilter; /** * The cache serializer is used to convert the decoded image, the source downloaded data, to the actual data used for storing to the disk cache. If you return nil, means to generate the data from the image instance, see `SDImageCache`. * For example, if you are using WebP images and facing the slow decoding time issue when later retrieving from disk cache again. You can try to encode the decoded image to JPEG/PNG format to disk cache instead of source downloaded data. * @note The `image` arg is nonnull, but when you also provide an image transformer and the image is transformed, the `data` arg may be nil, take attention to this case. * @note This method is called from a global queue in order to not to block the main thread. * @code SDWebImageManager.sharedManager.cacheSerializer = [SDWebImageCacheSerializer cacheSerializerWithBlock:^NSData * _Nullable(UIImage * _Nonnull image, NSData * _Nullable data, NSURL * _Nullable imageURL) { SDImageFormat format = [NSData sd_imageFormatForImageData:data]; switch (format) { case SDImageFormatWebP: return image.images ? data : nil; default: return data; } }]; * @endcode * The default value is nil. Means we just store the source downloaded data to disk cache. */ @property (nonatomic, strong, nullable) id cacheSerializer; /** The options processor is used, to have a global control for all the image request options and context option for current manager. @note If you use `transformer`, `cacheKeyFilter` or `cacheSerializer` property of manager, the input context option already apply those properties before passed. This options processor is a better replacement for those property in common usage. For example, you can control the global options, based on the URL or original context option like the below code. * @code SDWebImageManager.sharedManager.optionsProcessor = [SDWebImageOptionsProcessor optionsProcessorWithBlock:^SDWebImageOptionsResult * _Nullable(NSURL * _Nullable url, SDWebImageOptions options, SDWebImageContext * _Nullable context) { // Only do animation on `SDAnimatedImageView` if (!context[SDWebImageContextAnimatedImageClass]) { options |= SDWebImageDecodeFirstFrameOnly; } // Do not force decode for png url if ([url.lastPathComponent isEqualToString:@"png"]) { options |= SDWebImageAvoidDecodeImage; } // Always use screen scale factor SDWebImageMutableContext *mutableContext = [NSDictionary dictionaryWithDictionary:context]; mutableContext[SDWebImageContextImageScaleFactor] = @(UIScreen.mainScreen.scale); context = [mutableContext copy]; return [[SDWebImageOptionsResult alloc] initWithOptions:options context:context]; }]; * @endcode */ @property (nonatomic, strong, nullable) id optionsProcessor; /** * Check one or more operations running */ @property (nonatomic, assign, readonly, getter=isRunning) BOOL running; /** The default image cache when the manager which is created with no arguments. Such as shared manager or init. Defaults to nil. Means using `SDImageCache.sharedImageCache` */ @property (nonatomic, class, nullable) id defaultImageCache; /** The default image loader for manager which is created with no arguments. Such as shared manager or init. Defaults to nil. Means using `SDWebImageDownloader.sharedDownloader` */ @property (nonatomic, class, nullable) id defaultImageLoader; /** * Returns global shared manager instance. */ @property (nonatomic, class, readonly, nonnull) SDWebImageManager *sharedManager; /** * Allows to specify instance of cache and image loader used with image manager. * @return new instance of `SDWebImageManager` with specified cache and loader. */ - (nonnull instancetype)initWithCache:(nonnull id)cache loader:(nonnull id)loader NS_DESIGNATED_INITIALIZER; /** * Downloads the image at the given URL if not present in cache or return the cached version otherwise. * * @param url The URL to the image * @param options A mask to specify options to use for this request * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. * * This parameter is required. * * This block has no return value and takes the requested UIImage as first parameter and the NSData representation as second parameter. * In case of error the image parameter is nil and the third parameter may contain an NSError. * * The forth parameter is an `SDImageCacheType` enum indicating if the image was retrieved from the local cache * or from the memory cache or from the network. * * The fifth parameter is set to NO when the SDWebImageProgressiveLoad option is used and the image is * downloading. This block is thus called repeatedly with a partial image. When image is fully downloaded, the * block is called a last time with the full image and the last parameter set to YES. * * The last parameter is the original image URL * * @return Returns an instance of SDWebImageCombinedOperation, which you can cancel the loading process. */ - (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nonnull SDInternalCompletionBlock)completedBlock; /** * Downloads the image at the given URL if not present in cache or return the cached version otherwise. * * @param url The URL to the image * @param options A mask to specify options to use for this request * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. * * @return Returns an instance of SDWebImageCombinedOperation, which you can cancel the loading process. */ - (nullable SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nonnull SDInternalCompletionBlock)completedBlock; /** * Cancel all current operations */ - (void)cancelAll; /** * Remove the specify URL from failed black list. * @param url The failed URL. */ - (void)removeFailedURL:(nonnull NSURL *)url; /** * Remove all the URL from failed black list. */ - (void)removeAllFailedURLs; /** * Return the cache key for a given URL, does not considerate transformer or thumbnail. * @note This method does not have context option, only use the url and manager level cacheKeyFilter to generate the cache key. */ - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url; /** * Return the cache key for a given URL and context option. * @note The context option like `.thumbnailPixelSize` and `.imageTransformer` will effect the generated cache key, using this if you have those context associated. */ - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageManager.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageManager.h" #import "SDImageCache.h" #import "SDWebImageDownloader.h" #import "UIImage+Metadata.h" #import "SDAssociatedObject.h" #import "SDWebImageError.h" #import "SDInternalMacros.h" #import "SDCallbackQueue.h" static id _defaultImageCache; static id _defaultImageLoader; @interface SDWebImageCombinedOperation () @property (assign, nonatomic, getter = isCancelled) BOOL cancelled; @property (strong, nonatomic, readwrite, nullable) id loaderOperation; @property (strong, nonatomic, readwrite, nullable) id cacheOperation; @property (weak, nonatomic, nullable) SDWebImageManager *manager; @end @interface SDWebImageManager () { SD_LOCK_DECLARE(_failedURLsLock); // a lock to keep the access to `failedURLs` thread-safe SD_LOCK_DECLARE(_runningOperationsLock); // a lock to keep the access to `runningOperations` thread-safe } @property (strong, nonatomic, readwrite, nonnull) SDImageCache *imageCache; @property (strong, nonatomic, readwrite, nonnull) id imageLoader; @property (strong, nonatomic, nonnull) NSMutableSet *failedURLs; @property (strong, nonatomic, nonnull) NSMutableSet *runningOperations; @end @implementation SDWebImageManager + (id)defaultImageCache { return _defaultImageCache; } + (void)setDefaultImageCache:(id)defaultImageCache { if (defaultImageCache && ![defaultImageCache conformsToProtocol:@protocol(SDImageCache)]) { return; } _defaultImageCache = defaultImageCache; } + (id)defaultImageLoader { return _defaultImageLoader; } + (void)setDefaultImageLoader:(id)defaultImageLoader { if (defaultImageLoader && ![defaultImageLoader conformsToProtocol:@protocol(SDImageLoader)]) { return; } _defaultImageLoader = defaultImageLoader; } + (nonnull instancetype)sharedManager { static dispatch_once_t once; static id instance; dispatch_once(&once, ^{ instance = [self new]; }); return instance; } - (nonnull instancetype)init { id cache = [[self class] defaultImageCache]; if (!cache) { cache = [SDImageCache sharedImageCache]; } id loader = [[self class] defaultImageLoader]; if (!loader) { loader = [SDWebImageDownloader sharedDownloader]; } return [self initWithCache:cache loader:loader]; } - (nonnull instancetype)initWithCache:(nonnull id)cache loader:(nonnull id)loader { if ((self = [super init])) { _imageCache = cache; _imageLoader = loader; _failedURLs = [NSMutableSet new]; SD_LOCK_INIT(_failedURLsLock); _runningOperations = [NSMutableSet new]; SD_LOCK_INIT(_runningOperationsLock); } return self; } - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url { if (!url) { return @""; } NSString *key; // Cache Key Filter id cacheKeyFilter = self.cacheKeyFilter; if (cacheKeyFilter) { key = [cacheKeyFilter cacheKeyForURL:url]; } else { key = url.absoluteString; } return key; } - (nullable NSString *)originalCacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context { if (!url) { return @""; } NSString *key; // Cache Key Filter id cacheKeyFilter = self.cacheKeyFilter; if (context[SDWebImageContextCacheKeyFilter]) { cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; } if (cacheKeyFilter) { key = [cacheKeyFilter cacheKeyForURL:url]; } else { key = url.absoluteString; } return key; } - (nullable NSString *)cacheKeyForURL:(nullable NSURL *)url context:(nullable SDWebImageContext *)context { if (!url) { return @""; } NSString *key; // Cache Key Filter id cacheKeyFilter = self.cacheKeyFilter; if (context[SDWebImageContextCacheKeyFilter]) { cacheKeyFilter = context[SDWebImageContextCacheKeyFilter]; } if (cacheKeyFilter) { key = [cacheKeyFilter cacheKeyForURL:url]; } else { key = url.absoluteString; } // Thumbnail Key Appending NSValue *thumbnailSizeValue = context[SDWebImageContextImageThumbnailPixelSize]; if (thumbnailSizeValue != nil) { CGSize thumbnailSize = CGSizeZero; #if SD_MAC thumbnailSize = thumbnailSizeValue.sizeValue; #else thumbnailSize = thumbnailSizeValue.CGSizeValue; #endif BOOL preserveAspectRatio = YES; NSNumber *preserveAspectRatioValue = context[SDWebImageContextImagePreserveAspectRatio]; if (preserveAspectRatioValue != nil) { preserveAspectRatio = preserveAspectRatioValue.boolValue; } key = SDThumbnailedKeyForKey(key, thumbnailSize, preserveAspectRatio); } // Transformer Key Appending id transformer = self.transformer; if (context[SDWebImageContextImageTransformer]) { transformer = context[SDWebImageContextImageTransformer]; if ([transformer isEqual:NSNull.null]) { transformer = nil; } } if (transformer) { key = SDTransformedKeyForKey(key, transformer.transformerKey); } return key; } - (SDWebImageCombinedOperation *)loadImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(SDImageLoaderProgressBlock)progressBlock completed:(SDInternalCompletionBlock)completedBlock { return [self loadImageWithURL:url options:options context:nil progress:progressBlock completed:completedBlock]; } - (SDWebImageCombinedOperation *)loadImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nonnull SDInternalCompletionBlock)completedBlock { // Invoking this method without a completedBlock is pointless NSAssert(completedBlock != nil, @"If you mean to prefetch the image, use -[SDWebImagePrefetcher prefetchURLs] instead"); // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString. if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url]; } // Prevents app crashing on argument type error like sending NSNull instead of NSURL if (![url isKindOfClass:NSURL.class]) { url = nil; } SDWebImageCombinedOperation *operation = [SDWebImageCombinedOperation new]; operation.manager = self; BOOL isFailedUrl = NO; if (url) { SD_LOCK(_failedURLsLock); isFailedUrl = [self.failedURLs containsObject:url]; SD_UNLOCK(_failedURLsLock); } // Preprocess the options and context arg to decide the final the result for manager SDWebImageOptionsResult *result = [self processedResultForURL:url options:options context:context]; if (url.absoluteString.length == 0 || (!(options & SDWebImageRetryFailed) && isFailedUrl)) { NSString *description = isFailedUrl ? @"Image url is blacklisted" : @"Image url is nil"; NSInteger code = isFailedUrl ? SDWebImageErrorBlackListed : SDWebImageErrorInvalidURL; [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:code userInfo:@{NSLocalizedDescriptionKey : description}] queue:result.context[SDWebImageContextCallbackQueue] url:url]; return operation; } SD_LOCK(_runningOperationsLock); [self.runningOperations addObject:operation]; SD_UNLOCK(_runningOperationsLock); // Start the entry to load image from cache, the longest steps are below // Steps without transformer: // 1. query image from cache, miss // 2. download data and image // 3. store image to cache // Steps with transformer: // 1. query transformed image from cache, miss // 2. query original image from cache, miss // 3. download data and image // 4. do transform in CPU // 5. store original image to cache // 6. store transformed image to cache [self callCacheProcessForOperation:operation url:url options:result.options context:result.context progress:progressBlock completed:completedBlock]; return operation; } - (void)cancelAll { SD_LOCK(_runningOperationsLock); NSSet *copiedOperations = [self.runningOperations copy]; SD_UNLOCK(_runningOperationsLock); [copiedOperations makeObjectsPerformSelector:@selector(cancel)]; // This will call `safelyRemoveOperationFromRunning:` and remove from the array } - (BOOL)isRunning { BOOL isRunning = NO; SD_LOCK(_runningOperationsLock); isRunning = (self.runningOperations.count > 0); SD_UNLOCK(_runningOperationsLock); return isRunning; } - (void)removeFailedURL:(NSURL *)url { if (!url) { return; } SD_LOCK(_failedURLsLock); [self.failedURLs removeObject:url]; SD_UNLOCK(_failedURLsLock); } - (void)removeAllFailedURLs { SD_LOCK(_failedURLsLock); [self.failedURLs removeAllObjects]; SD_UNLOCK(_failedURLsLock); } #pragma mark - Private // Query normal cache process - (void)callCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { // Grab the image cache to use id imageCache = context[SDWebImageContextImageCache]; if (!imageCache) { imageCache = self.imageCache; } // Get the query cache type SDImageCacheType queryCacheType = SDImageCacheTypeAll; if (context[SDWebImageContextQueryCacheType]) { queryCacheType = [context[SDWebImageContextQueryCacheType] integerValue]; } // Check whether we should query cache BOOL shouldQueryCache = !SD_OPTIONS_CONTAINS(options, SDWebImageFromLoaderOnly); if (shouldQueryCache) { // transformed cache key NSString *key = [self cacheKeyForURL:url context:context]; // to avoid the SDImageCache's sync logic use the mismatched cache key // we should strip the `thumbnail` related context SDWebImageMutableContext *mutableContext = [context mutableCopy]; mutableContext[SDWebImageContextImageThumbnailPixelSize] = nil; mutableContext[SDWebImageContextImagePreserveAspectRatio] = nil; @weakify(operation); operation.cacheOperation = [imageCache queryImageForKey:key options:options context:mutableContext cacheType:queryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] queue:context[SDWebImageContextCallbackQueue] url:url]; [self safelyRemoveOperationFromRunning:operation]; return; } else if (!cachedImage) { NSString *originKey = [self originalCacheKeyForURL:url context:context]; BOOL mayInOriginalCache = ![key isEqualToString:originKey]; // Have a chance to query original cache instead of downloading, then applying transform // Thumbnail decoding is done inside SDImageCache's decoding part, which does not need post processing for transform if (mayInOriginalCache) { [self callOriginalCacheProcessForOperation:operation url:url options:options context:context progress:progressBlock completed:completedBlock]; return; } } // Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:cachedImage cachedData:cachedData cacheType:cacheType progress:progressBlock completed:completedBlock]; }]; } else { // Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock]; } } // Query original cache process - (void)callOriginalCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { // Grab the image cache to use, choose standalone original cache firstly id imageCache = context[SDWebImageContextOriginalImageCache]; if (!imageCache) { // if no standalone cache available, use default cache imageCache = context[SDWebImageContextImageCache]; if (!imageCache) { imageCache = self.imageCache; } } // Get the original query cache type SDImageCacheType originalQueryCacheType = SDImageCacheTypeDisk; if (context[SDWebImageContextOriginalQueryCacheType]) { originalQueryCacheType = [context[SDWebImageContextOriginalQueryCacheType] integerValue]; } // Check whether we should query original cache BOOL shouldQueryOriginalCache = (originalQueryCacheType != SDImageCacheTypeNone); if (shouldQueryOriginalCache) { // Get original cache key generation without transformer NSString *key = [self originalCacheKeyForURL:url context:context]; @weakify(operation); operation.cacheOperation = [imageCache queryImageForKey:key options:options context:context cacheType:originalQueryCacheType completion:^(UIImage * _Nullable cachedImage, NSData * _Nullable cachedData, SDImageCacheType cacheType) { @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during querying the cache"}] queue:context[SDWebImageContextCallbackQueue] url:url]; [self safelyRemoveOperationFromRunning:operation]; return; } else if (!cachedImage) { // Original image cache miss. Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock]; return; } // Skip downloading and continue transform process, and ignore .refreshCached option for now [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:cachedImage originalData:cachedData cacheType:cacheType finished:YES completed:completedBlock]; [self safelyRemoveOperationFromRunning:operation]; }]; } else { // Continue download process [self callDownloadProcessForOperation:operation url:url options:options context:context cachedImage:nil cachedData:nil cacheType:SDImageCacheTypeNone progress:progressBlock completed:completedBlock]; } } // Download process - (void)callDownloadProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context cachedImage:(nullable UIImage *)cachedImage cachedData:(nullable NSData *)cachedData cacheType:(SDImageCacheType)cacheType progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { // Mark the cache operation end @synchronized (operation) { operation.cacheOperation = nil; } // Grab the image loader to use id imageLoader = context[SDWebImageContextImageLoader]; if (!imageLoader) { imageLoader = self.imageLoader; } // Check whether we should download image from network BOOL shouldDownload = !SD_OPTIONS_CONTAINS(options, SDWebImageFromCacheOnly); shouldDownload &= (!cachedImage || options & SDWebImageRefreshCached); shouldDownload &= (![self.delegate respondsToSelector:@selector(imageManager:shouldDownloadImageForURL:)] || [self.delegate imageManager:self shouldDownloadImageForURL:url]); if ([imageLoader respondsToSelector:@selector(canRequestImageForURL:options:context:)]) { shouldDownload &= [imageLoader canRequestImageForURL:url options:options context:context]; } else { shouldDownload &= [imageLoader canRequestImageForURL:url]; } if (shouldDownload) { if (cachedImage && options & SDWebImageRefreshCached) { // If image was found in the cache but SDWebImageRefreshCached is provided, notify about the cached image // AND try to re-download it in order to let a chance to NSURLCache to refresh it from server. [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url]; // Pass the cached image to the image loader. The image loader should check whether the remote image is equal to the cached image. SDWebImageMutableContext *mutableContext; if (context) { mutableContext = [context mutableCopy]; } else { mutableContext = [NSMutableDictionary dictionary]; } mutableContext[SDWebImageContextLoaderCachedImage] = cachedImage; context = [mutableContext copy]; } @weakify(operation); operation.loaderOperation = [imageLoader requestImageWithURL:url options:options context:context progress:progressBlock completed:^(UIImage *downloadedImage, NSData *downloadedData, NSError *error, BOOL finished) { @strongify(operation); if (!operation || operation.isCancelled) { // Image combined operation cancelled by user [self callCompletionBlockForOperation:operation completion:completedBlock error:[NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorCancelled userInfo:@{NSLocalizedDescriptionKey : @"Operation cancelled by user during sending the request"}] queue:context[SDWebImageContextCallbackQueue] url:url]; } else if (cachedImage && options & SDWebImageRefreshCached && [error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCacheNotModified) { // Image refresh hit the NSURLCache cache, do not call the completion block } else if ([error.domain isEqualToString:SDWebImageErrorDomain] && error.code == SDWebImageErrorCancelled) { // Download operation cancelled by user before sending the request, don't block failed URL [self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url]; } else if (error) { [self callCompletionBlockForOperation:operation completion:completedBlock error:error queue:context[SDWebImageContextCallbackQueue] url:url]; BOOL shouldBlockFailedURL = [self shouldBlockFailedURLWithURL:url error:error options:options context:context]; if (shouldBlockFailedURL) { SD_LOCK(self->_failedURLsLock); [self.failedURLs addObject:url]; SD_UNLOCK(self->_failedURLsLock); } } else { if ((options & SDWebImageRetryFailed)) { SD_LOCK(self->_failedURLsLock); [self.failedURLs removeObject:url]; SD_UNLOCK(self->_failedURLsLock); } // Continue transform process [self callTransformProcessForOperation:operation url:url options:options context:context originalImage:downloadedImage originalData:downloadedData cacheType:SDImageCacheTypeNone finished:finished completed:completedBlock]; } if (finished) { [self safelyRemoveOperationFromRunning:operation]; } }]; } else if (cachedImage) { [self callCompletionBlockForOperation:operation completion:completedBlock image:cachedImage data:cachedData error:nil cacheType:cacheType finished:YES queue:context[SDWebImageContextCallbackQueue] url:url]; [self safelyRemoveOperationFromRunning:operation]; } else { // Image not in cache and download disallowed by delegate [self callCompletionBlockForOperation:operation completion:completedBlock image:nil data:nil error:nil cacheType:SDImageCacheTypeNone finished:YES queue:context[SDWebImageContextCallbackQueue] url:url]; [self safelyRemoveOperationFromRunning:operation]; } } // Transform process - (void)callTransformProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context originalImage:(nullable UIImage *)originalImage originalData:(nullable NSData *)originalData cacheType:(SDImageCacheType)cacheType finished:(BOOL)finished completed:(nullable SDInternalCompletionBlock)completedBlock { id transformer = context[SDWebImageContextImageTransformer]; if ([transformer isEqual:NSNull.null]) { transformer = nil; } // transformer check BOOL shouldTransformImage = originalImage && transformer; shouldTransformImage = shouldTransformImage && (!originalImage.sd_isAnimated || (options & SDWebImageTransformAnimatedImage)); shouldTransformImage = shouldTransformImage && (!originalImage.sd_isVector || (options & SDWebImageTransformVectorImage)); // thumbnail check BOOL isThumbnail = originalImage.sd_isThumbnail; NSData *cacheData = originalData; UIImage *cacheImage = originalImage; if (isThumbnail) { cacheData = nil; // thumbnail don't store full size data originalImage = nil; // thumbnail don't have full size image } if (shouldTransformImage) { // transformed cache key NSString *key = [self cacheKeyForURL:url context:context]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ // Case that transformer on thumbnail, which this time need full pixel image UIImage *transformedImage = [transformer transformedImageWithImage:cacheImage forKey:key]; if (transformedImage) { // We need keep some metadata from the full size image when needed // Because most of our transformer does not care about these information // So we add a **post-process** logic here, not a good design :( BOOL preserveImageMetadata = YES; if ([transformer respondsToSelector:@selector(preserveImageMetadata)]) { preserveImageMetadata = transformer.preserveImageMetadata; } if (preserveImageMetadata) { SDImageCopyAssociatedObject(cacheImage, transformedImage); } // Mark the transformed transformedImage.sd_isTransformed = YES; [self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:transformedImage originalData:originalData cacheData:nil cacheType:cacheType finished:finished completed:completedBlock]; } else { [self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:cacheImage originalData:originalData cacheData:cacheData cacheType:cacheType finished:finished completed:completedBlock]; } }); } else { [self callStoreOriginCacheProcessForOperation:operation url:url options:options context:context originalImage:originalImage cacheImage:cacheImage originalData:originalData cacheData:cacheData cacheType:cacheType finished:finished completed:completedBlock]; } } // Store origin cache process - (void)callStoreOriginCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context originalImage:(nullable UIImage *)originalImage cacheImage:(nullable UIImage *)cacheImage originalData:(nullable NSData *)originalData cacheData:(nullable NSData *)cacheData cacheType:(SDImageCacheType)cacheType finished:(BOOL)finished completed:(nullable SDInternalCompletionBlock)completedBlock { // Grab the image cache to use, choose standalone original cache firstly id imageCache = context[SDWebImageContextOriginalImageCache]; if (!imageCache) { // if no standalone cache available, use default cache imageCache = context[SDWebImageContextImageCache]; if (!imageCache) { imageCache = self.imageCache; } } // the original store image cache type SDImageCacheType originalStoreCacheType = SDImageCacheTypeDisk; if (context[SDWebImageContextOriginalStoreCacheType]) { originalStoreCacheType = [context[SDWebImageContextOriginalStoreCacheType] integerValue]; } id cacheSerializer = context[SDWebImageContextCacheSerializer]; // If the original cacheType is disk, since we don't need to store the original data again // Strip the disk from the originalStoreCacheType if (cacheType == SDImageCacheTypeDisk) { if (originalStoreCacheType == SDImageCacheTypeDisk) originalStoreCacheType = SDImageCacheTypeNone; if (originalStoreCacheType == SDImageCacheTypeAll) originalStoreCacheType = SDImageCacheTypeMemory; } // Get original cache key generation without transformer NSString *key = [self originalCacheKeyForURL:url context:context]; if (finished && cacheSerializer && (originalStoreCacheType == SDImageCacheTypeDisk || originalStoreCacheType == SDImageCacheTypeAll)) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSData *newOriginalData = [cacheSerializer cacheDataWithImage:originalImage originalData:originalData imageURL:url]; // Store original image and data [self storeImage:originalImage imageData:newOriginalData forKey:key options:options context:context imageCache:imageCache cacheType:originalStoreCacheType finished:finished completion:^{ // Continue store cache process, transformed data is nil [self callStoreCacheProcessForOperation:operation url:url options:options context:context image:cacheImage data:cacheData cacheType:cacheType finished:finished completed:completedBlock]; }]; }); } else { // Store original image and data [self storeImage:originalImage imageData:originalData forKey:key options:options context:context imageCache:imageCache cacheType:originalStoreCacheType finished:finished completion:^{ // Continue store cache process, transformed data is nil [self callStoreCacheProcessForOperation:operation url:url options:options context:context image:cacheImage data:cacheData cacheType:cacheType finished:finished completed:completedBlock]; }]; } } // Store normal cache process - (void)callStoreCacheProcessForOperation:(nonnull SDWebImageCombinedOperation *)operation url:(nonnull NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context image:(nullable UIImage *)image data:(nullable NSData *)data cacheType:(SDImageCacheType)cacheType finished:(BOOL)finished completed:(nullable SDInternalCompletionBlock)completedBlock { // Grab the image cache to use id imageCache = context[SDWebImageContextImageCache]; if (!imageCache) { imageCache = self.imageCache; } // the target image store cache type SDImageCacheType storeCacheType = SDImageCacheTypeAll; if (context[SDWebImageContextStoreCacheType]) { storeCacheType = [context[SDWebImageContextStoreCacheType] integerValue]; } id cacheSerializer = context[SDWebImageContextCacheSerializer]; // transformed cache key NSString *key = [self cacheKeyForURL:url context:context]; if (finished && cacheSerializer && (storeCacheType == SDImageCacheTypeDisk || storeCacheType == SDImageCacheTypeAll)) { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSData *newData = [cacheSerializer cacheDataWithImage:image originalData:data imageURL:url]; // Store image and data [self storeImage:image imageData:newData forKey:key options:options context:context imageCache:imageCache cacheType:storeCacheType finished:finished completion:^{ [self callCompletionBlockForOperation:operation completion:completedBlock image:image data:data error:nil cacheType:cacheType finished:finished queue:context[SDWebImageContextCallbackQueue] url:url]; }]; }); } else { // Store image and data [self storeImage:image imageData:data forKey:key options:options context:context imageCache:imageCache cacheType:storeCacheType finished:finished completion:^{ [self callCompletionBlockForOperation:operation completion:completedBlock image:image data:data error:nil cacheType:cacheType finished:finished queue:context[SDWebImageContextCallbackQueue] url:url]; }]; } } #pragma mark - Helper - (void)safelyRemoveOperationFromRunning:(nullable SDWebImageCombinedOperation*)operation { if (!operation) { return; } SD_LOCK(_runningOperationsLock); [self.runningOperations removeObject:operation]; SD_UNLOCK(_runningOperationsLock); } - (void)storeImage:(nullable UIImage *)image imageData:(nullable NSData *)data forKey:(nullable NSString *)key options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context imageCache:(nonnull id)imageCache cacheType:(SDImageCacheType)cacheType finished:(BOOL)finished completion:(nullable SDWebImageNoParamsBlock)completion { BOOL waitStoreCache = SD_OPTIONS_CONTAINS(options, SDWebImageWaitStoreCache); // Ignore progressive data cache if (!finished) { if (completion) { completion(); } return; } // Check whether we should wait the store cache finished. If not, callback immediately if ([imageCache respondsToSelector:@selector(storeImage:imageData:forKey:options:context:cacheType:completion:)]) { [imageCache storeImage:image imageData:data forKey:key options:options context:context cacheType:cacheType completion:^{ if (waitStoreCache) { if (completion) { completion(); } } }]; } else { [imageCache storeImage:image imageData:data forKey:key cacheType:cacheType completion:^{ if (waitStoreCache) { if (completion) { completion(); } } }]; } if (!waitStoreCache) { if (completion) { completion(); } } } - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation completion:(nullable SDInternalCompletionBlock)completionBlock error:(nullable NSError *)error queue:(nullable SDCallbackQueue *)queue url:(nullable NSURL *)url { [self callCompletionBlockForOperation:operation completion:completionBlock image:nil data:nil error:error cacheType:SDImageCacheTypeNone finished:YES queue:queue url:url]; } - (void)callCompletionBlockForOperation:(nullable SDWebImageCombinedOperation*)operation completion:(nullable SDInternalCompletionBlock)completionBlock image:(nullable UIImage *)image data:(nullable NSData *)data error:(nullable NSError *)error cacheType:(SDImageCacheType)cacheType finished:(BOOL)finished queue:(nullable SDCallbackQueue *)queue url:(nullable NSURL *)url { if (completionBlock) { [(queue ?: SDCallbackQueue.mainQueue) async:^{ completionBlock(image, data, error, cacheType, finished, url); }]; } } - (BOOL)shouldBlockFailedURLWithURL:(nonnull NSURL *)url error:(nonnull NSError *)error options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { id imageLoader = context[SDWebImageContextImageLoader]; if (!imageLoader) { imageLoader = self.imageLoader; } // Check whether we should block failed url BOOL shouldBlockFailedURL; if ([self.delegate respondsToSelector:@selector(imageManager:shouldBlockFailedURL:withError:)]) { shouldBlockFailedURL = [self.delegate imageManager:self shouldBlockFailedURL:url withError:error]; } else { if ([imageLoader respondsToSelector:@selector(shouldBlockFailedURLWithURL:error:options:context:)]) { shouldBlockFailedURL = [imageLoader shouldBlockFailedURLWithURL:url error:error options:options context:context]; } else { shouldBlockFailedURL = [imageLoader shouldBlockFailedURLWithURL:url error:error]; } } return shouldBlockFailedURL; } - (SDWebImageOptionsResult *)processedResultForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context { SDWebImageOptionsResult *result; SDWebImageMutableContext *mutableContext = [SDWebImageMutableContext dictionary]; // Image Transformer from manager if (!context[SDWebImageContextImageTransformer]) { id transformer = self.transformer; [mutableContext setValue:transformer forKey:SDWebImageContextImageTransformer]; } // Cache key filter from manager if (!context[SDWebImageContextCacheKeyFilter]) { id cacheKeyFilter = self.cacheKeyFilter; [mutableContext setValue:cacheKeyFilter forKey:SDWebImageContextCacheKeyFilter]; } // Cache serializer from manager if (!context[SDWebImageContextCacheSerializer]) { id cacheSerializer = self.cacheSerializer; [mutableContext setValue:cacheSerializer forKey:SDWebImageContextCacheSerializer]; } if (mutableContext.count > 0) { if (context) { [mutableContext addEntriesFromDictionary:context]; } context = [mutableContext copy]; } // Apply options processor if (self.optionsProcessor) { result = [self.optionsProcessor processedResultForURL:url options:options context:context]; } if (!result) { // Use default options result result = [[SDWebImageOptionsResult alloc] initWithOptions:options context:context]; } return result; } @end @implementation SDWebImageCombinedOperation - (BOOL)isCancelled { // Need recursive lock (user's cancel block may check isCancelled), do not use SD_LOCK @synchronized (self) { return _cancelled; } } - (void)cancel { // Need recursive lock (user's cancel block may check isCancelled), do not use SD_LOCK @synchronized(self) { if (_cancelled) { return; } _cancelled = YES; if (self.cacheOperation) { [self.cacheOperation cancel]; self.cacheOperation = nil; } if (self.loaderOperation) { [self.loaderOperation cancel]; self.loaderOperation = nil; } [self.manager safelyRemoveOperationFromRunning:self]; } } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageOperation.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import /// A protocol represents cancelable operation. @protocol SDWebImageOperation /// Cancel the operation - (void)cancel; @optional /// Whether the operation has been cancelled. @property (nonatomic, assign, readonly, getter=isCancelled) BOOL cancelled; @end /// NSOperation conform to `SDWebImageOperation`. @interface NSOperation (SDWebImageOperation) @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageOperation.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageOperation.h" /// NSOperation conform to `SDWebImageOperation`. @implementation NSOperation (SDWebImageOperation) @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageOptionsProcessor.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" #import "SDWebImageDefine.h" @class SDWebImageOptionsResult; typedef SDWebImageOptionsResult * _Nullable(^SDWebImageOptionsProcessorBlock)(NSURL * _Nullable url, SDWebImageOptions options, SDWebImageContext * _Nullable context); /** The options result contains both options and context. */ @interface SDWebImageOptionsResult : NSObject /** WebCache options. */ @property (nonatomic, assign, readonly) SDWebImageOptions options; /** Context options. */ @property (nonatomic, copy, readonly, nullable) SDWebImageContext *context; /** Create a new options result. @param options options @param context context @return The options result contains both options and context. */ - (nonnull instancetype)initWithOptions:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; @end /** This is the protocol for options processor. Options processor can be used, to control the final result for individual image request's `SDWebImageOptions` and `SDWebImageContext` Implements the protocol to have a global control for each indivadual image request's option. */ @protocol SDWebImageOptionsProcessor /** Return the processed options result for specify image URL, with its options and context @param url The URL to the image @param options A mask to specify options to use for this request @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. @return The processed result, contains both options and context */ - (nullable SDWebImageOptionsResult *)processedResultForURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; @end /** A options processor class with block. */ @interface SDWebImageOptionsProcessor : NSObject - (nonnull instancetype)initWithBlock:(nonnull SDWebImageOptionsProcessorBlock)block; + (nonnull instancetype)optionsProcessorWithBlock:(nonnull SDWebImageOptionsProcessorBlock)block; - (nonnull instancetype)init NS_UNAVAILABLE; + (nonnull instancetype)new NS_UNAVAILABLE; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageOptionsProcessor.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageOptionsProcessor.h" @interface SDWebImageOptionsResult () @property (nonatomic, assign) SDWebImageOptions options; @property (nonatomic, copy, nullable) SDWebImageContext *context; @end @implementation SDWebImageOptionsResult - (instancetype)initWithOptions:(SDWebImageOptions)options context:(SDWebImageContext *)context { self = [super init]; if (self) { self.options = options; self.context = context; } return self; } @end @interface SDWebImageOptionsProcessor () @property (nonatomic, copy, nonnull) SDWebImageOptionsProcessorBlock block; @end @implementation SDWebImageOptionsProcessor - (instancetype)initWithBlock:(SDWebImageOptionsProcessorBlock)block { self = [super init]; if (self) { self.block = block; } return self; } + (instancetype)optionsProcessorWithBlock:(SDWebImageOptionsProcessorBlock)block { SDWebImageOptionsProcessor *optionsProcessor = [[SDWebImageOptionsProcessor alloc] initWithBlock:block]; return optionsProcessor; } - (SDWebImageOptionsResult *)processedResultForURL:(NSURL *)url options:(SDWebImageOptions)options context:(SDWebImageContext *)context { if (!self.block) { return nil; } return self.block(url, options, context); } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageManager.h" @class SDWebImagePrefetcher; /** A token represents a list of URLs, can be used to cancel the download. */ @interface SDWebImagePrefetchToken : NSObject /** * Cancel the current prefetching. */ - (void)cancel; /** list of URLs of current prefetching. */ @property (nonatomic, copy, readonly, nullable) NSArray *urls; @end /** The prefetcher delegate protocol */ @protocol SDWebImagePrefetcherDelegate @optional /** * Called when an image was prefetched. Which means it's called when one URL from any of prefetching finished. * * @param imagePrefetcher The current image prefetcher * @param imageURL The image url that was prefetched * @param finishedCount The total number of images that were prefetched (successful or not) * @param totalCount The total number of images that were to be prefetched */ - (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didPrefetchURL:(nullable NSURL *)imageURL finishedCount:(NSUInteger)finishedCount totalCount:(NSUInteger)totalCount; /** * Called when all images are prefetched. Which means it's called when all URLs from all of prefetching finished. * @param imagePrefetcher The current image prefetcher * @param totalCount The total number of images that were prefetched (whether successful or not) * @param skippedCount The total number of images that were skipped */ - (void)imagePrefetcher:(nonnull SDWebImagePrefetcher *)imagePrefetcher didFinishWithTotalCount:(NSUInteger)totalCount skippedCount:(NSUInteger)skippedCount; @end typedef void(^SDWebImagePrefetcherProgressBlock)(NSUInteger noOfFinishedUrls, NSUInteger noOfTotalUrls); typedef void(^SDWebImagePrefetcherCompletionBlock)(NSUInteger noOfFinishedUrls, NSUInteger noOfSkippedUrls); /** * Prefetch some URLs in the cache for future use. Images are downloaded in low priority. */ @interface SDWebImagePrefetcher : NSObject /** * The web image manager used by prefetcher to prefetch images. * @note You can specify a standalone manager and downloader with custom configuration suitable for image prefetching. Such as `currentDownloadCount` or `downloadTimeout`. */ @property (strong, nonatomic, readonly, nonnull) SDWebImageManager *manager; /** * Maximum number of URLs to prefetch at the same time. Defaults to 3. */ @property (nonatomic, assign) NSUInteger maxConcurrentPrefetchCount; /** * The options for prefetcher. Defaults to SDWebImageLowPriority. * @deprecated Prefetcher is designed to be used shared and should not effect others. So in 5.15.0 we added API `prefetchURLs:options:context:`. If you want global control, try to use `SDWebImageOptionsProcessor` in manager level. */ @property (nonatomic, assign) SDWebImageOptions options API_DEPRECATED("Use individual prefetch options param instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); /** * The context for prefetcher. Defaults to nil. * @deprecated Prefetcher is designed to be used shared and should not effect others. So in 5.15.0 we added API `prefetchURLs:options:context:`. If you want global control, try to use `SDWebImageOptionsProcessor` in `SDWebImageManager.optionsProcessor`. */ @property (nonatomic, copy, nullable) SDWebImageContext *context API_DEPRECATED("Use individual prefetch context param instead", macos(10.10, API_TO_BE_DEPRECATED), ios(8.0, API_TO_BE_DEPRECATED), tvos(9.0, API_TO_BE_DEPRECATED), watchos(2.0, API_TO_BE_DEPRECATED)); /** * Queue options for prefetcher when call the progressBlock, completionBlock and delegate methods. Defaults to Main Queue. * @deprecated 5.15.0 introduce SDCallbackQueue, use that is preferred and has higher priority. The set/get to this property will translate to that instead. * @note The call is asynchronously to avoid blocking target queue. (see SDCallbackPolicyDispatch) * @note The delegate queue should be set before any prefetching start and may not be changed during prefetching to avoid thread-safe problem. */ @property (strong, nonatomic, nonnull) dispatch_queue_t delegateQueue API_DEPRECATED("Use SDWebImageContextCallbackQueue context param instead, see SDCallbackQueue", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0)); /** * The delegate for the prefetcher. Defaults to nil. */ @property (weak, nonatomic, nullable) id delegate; /** * Returns the global shared image prefetcher instance. It use a standalone manager which is different from shared manager. */ @property (nonatomic, class, readonly, nonnull) SDWebImagePrefetcher *sharedImagePrefetcher; /** * Allows you to instantiate a prefetcher with any arbitrary image manager. */ - (nonnull instancetype)initWithImageManager:(nonnull SDWebImageManager *)manager NS_DESIGNATED_INITIALIZER; /** * Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching. It based on the image manager so the image may from the cache and network according to the `options` property. * Prefetching is separate to each other, which means the progressBlock and completionBlock you provide is bind to the prefetching for the list of urls. * Attention that call this will not cancel previous fetched urls. You should keep the token return by this to cancel or cancel all the prefetch. * * @param urls list of URLs to prefetch * @return the token to cancel the current prefetching. */ - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray *)urls; /** * Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching. It based on the image manager so the image may from the cache and network according to the `options` property. * Prefetching is separate to each other, which means the progressBlock and completionBlock you provide is bind to the prefetching for the list of urls. * Attention that call this will not cancel previous fetched urls. You should keep the token return by this to cancel or cancel all the prefetch. * * @param urls list of URLs to prefetch * @param progressBlock block to be called when progress updates; * first parameter is the number of completed (successful or not) requests, * second parameter is the total number of images originally requested to be prefetched * @param completionBlock block to be called when the current prefetching is completed * first param is the number of completed (successful or not) requests, * second parameter is the number of skipped requests * @return the token to cancel the current prefetching. */ - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray *)urls progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock; /** * Assign list of URLs to let SDWebImagePrefetcher to queue the prefetching. It based on the image manager so the image may from the cache and network according to the `options` property. * Prefetching is separate to each other, which means the progressBlock and completionBlock you provide is bind to the prefetching for the list of urls. * Attention that call this will not cancel previous fetched urls. You should keep the token return by this to cancel or cancel all the prefetch. * * @param urls list of URLs to prefetch * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock block to be called when progress updates; * first parameter is the number of completed (successful or not) requests, * second parameter is the total number of images originally requested to be prefetched * @param completionBlock block to be called when the current prefetching is completed * first param is the number of completed (successful or not) requests, * second parameter is the number of skipped requests * @return the token to cancel the current prefetching. */ - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray *)urls options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock; /** * Remove and cancel all the prefeching for the prefetcher. */ - (void)cancelPrefetching; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImagePrefetcher.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImagePrefetcher.h" #import "SDAsyncBlockOperation.h" #import "SDCallbackQueue.h" #import "SDInternalMacros.h" #import @interface SDCallbackQueue () @property (nonatomic, strong, nonnull) dispatch_queue_t queue; @end @interface SDWebImagePrefetchToken () { @public // Though current implementation, `SDWebImageManager` completion block is always on main queue. But however, there is no guarantee in docs. And we may introduce config to specify custom queue in the future. // These value are just used as incrementing counter, keep thread-safe using memory_order_relaxed for performance. atomic_ulong _skippedCount; atomic_ulong _finishedCount; atomic_flag _isAllFinished; unsigned long _totalCount; // Used to ensure NSPointerArray thread safe SD_LOCK_DECLARE(_prefetchOperationsLock); SD_LOCK_DECLARE(_loadOperationsLock); } @property (nonatomic, copy, readwrite) NSArray *urls; @property (nonatomic, strong) NSPointerArray *loadOperations; @property (nonatomic, strong) NSPointerArray *prefetchOperations; @property (nonatomic, weak) SDWebImagePrefetcher *prefetcher; @property (nonatomic, assign) SDWebImageOptions options; @property (nonatomic, copy, nullable) SDWebImageContext *context; @property (nonatomic, copy, nullable) SDWebImagePrefetcherCompletionBlock completionBlock; @property (nonatomic, copy, nullable) SDWebImagePrefetcherProgressBlock progressBlock; @end @interface SDWebImagePrefetcher () @property (strong, nonatomic, nonnull) SDWebImageManager *manager; @property (strong, atomic, nonnull) NSMutableSet *runningTokens; @property (strong, nonatomic, nonnull) NSOperationQueue *prefetchQueue; @property (strong, nonatomic, nullable) SDCallbackQueue *callbackQueue; @end @implementation SDWebImagePrefetcher + (nonnull instancetype)sharedImagePrefetcher { static dispatch_once_t once; static id instance; dispatch_once(&once, ^{ instance = [self new]; }); return instance; } - (nonnull instancetype)init { return [self initWithImageManager:[SDWebImageManager new]]; } - (nonnull instancetype)initWithImageManager:(SDWebImageManager *)manager { if ((self = [super init])) { _manager = manager; _runningTokens = [NSMutableSet set]; _options = SDWebImageLowPriority; _prefetchQueue = [NSOperationQueue new]; self.maxConcurrentPrefetchCount = 3; } return self; } - (void)setMaxConcurrentPrefetchCount:(NSUInteger)maxConcurrentPrefetchCount { self.prefetchQueue.maxConcurrentOperationCount = maxConcurrentPrefetchCount; } - (NSUInteger)maxConcurrentPrefetchCount { return self.prefetchQueue.maxConcurrentOperationCount; } - (void)setDelegateQueue:(dispatch_queue_t)delegateQueue { // Deprecate and translate to SDCallbackQueue _callbackQueue = [[SDCallbackQueue alloc] initWithDispatchQueue:delegateQueue]; _callbackQueue.policy = SDCallbackPolicyDispatch; } - (dispatch_queue_t)delegateQueue { // Deprecate and translate to SDCallbackQueue return (_callbackQueue ?: SDCallbackQueue.mainQueue).queue; } #pragma mark - Prefetch - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray *)urls { return [self prefetchURLs:urls progress:nil completed:nil]; } - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray *)urls progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock { return [self prefetchURLs:urls options:self.options context:self.context progress:progressBlock completed:completionBlock]; } - (nullable SDWebImagePrefetchToken *)prefetchURLs:(nullable NSArray *)urls options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDWebImagePrefetcherProgressBlock)progressBlock completed:(nullable SDWebImagePrefetcherCompletionBlock)completionBlock { if (!urls || urls.count == 0) { if (completionBlock) { completionBlock(0, 0); } return nil; } SDWebImagePrefetchToken *token = [SDWebImagePrefetchToken new]; token.prefetcher = self; token.urls = urls; token.options = options; token.context = context; token->_skippedCount = 0; token->_finishedCount = 0; token->_totalCount = token.urls.count; atomic_flag_clear(&(token->_isAllFinished)); token.loadOperations = [NSPointerArray weakObjectsPointerArray]; token.prefetchOperations = [NSPointerArray weakObjectsPointerArray]; token.progressBlock = progressBlock; token.completionBlock = completionBlock; [self addRunningToken:token]; [self startPrefetchWithToken:token]; return token; } - (void)startPrefetchWithToken:(SDWebImagePrefetchToken * _Nonnull)token { for (NSURL *url in token.urls) { @weakify(self); SDAsyncBlockOperation *prefetchOperation = [SDAsyncBlockOperation blockOperationWithBlock:^(SDAsyncBlockOperation * _Nonnull asyncOperation) { @strongify(self); if (!self || asyncOperation.isCancelled) { return; } id operation = [self.manager loadImageWithURL:url options:token.options context:token.context progress:nil completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { @strongify(self); if (!self) { return; } if (!finished) { return; } atomic_fetch_add_explicit(&(token->_finishedCount), 1, memory_order_relaxed); if (error) { // Add last failed atomic_fetch_add_explicit(&(token->_skippedCount), 1, memory_order_relaxed); } // Current operation finished [self callProgressBlockForToken:token imageURL:imageURL]; if (atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed) == token->_totalCount) { // All finished if (!atomic_flag_test_and_set_explicit(&(token->_isAllFinished), memory_order_relaxed)) { [self callCompletionBlockForToken:token]; [self removeRunningToken:token]; } } [asyncOperation complete]; }]; NSAssert(operation != nil, @"Operation should not be nil, [SDWebImageManager loadImageWithURL:options:context:progress:completed:] break prefetch logic"); SD_LOCK(token->_loadOperationsLock); [token.loadOperations addPointer:(__bridge void *)operation]; SD_UNLOCK(token->_loadOperationsLock); }]; SD_LOCK(token->_prefetchOperationsLock); [token.prefetchOperations addPointer:(__bridge void *)prefetchOperation]; SD_UNLOCK(token->_prefetchOperationsLock); [self.prefetchQueue addOperation:prefetchOperation]; } } #pragma mark - Cancel - (void)cancelPrefetching { @synchronized(self.runningTokens) { NSSet *copiedTokens = [self.runningTokens copy]; [copiedTokens makeObjectsPerformSelector:@selector(cancel)]; [self.runningTokens removeAllObjects]; } } - (void)callProgressBlockForToken:(SDWebImagePrefetchToken *)token imageURL:(NSURL *)url { if (!token) { return; } BOOL shouldCallDelegate = [self.delegate respondsToSelector:@selector(imagePrefetcher:didPrefetchURL:finishedCount:totalCount:)]; NSUInteger tokenFinishedCount = [self tokenFinishedCount]; NSUInteger tokenTotalCount = [self tokenTotalCount]; NSUInteger finishedCount = atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed); NSUInteger totalCount = token->_totalCount; SDCallbackQueue *queue = token.context[SDWebImageContextCallbackQueue]; if (!queue) { queue = self.callbackQueue; } [(queue ?: SDCallbackQueue.mainQueue) async:^{ if (shouldCallDelegate) { [self.delegate imagePrefetcher:self didPrefetchURL:url finishedCount:tokenFinishedCount totalCount:tokenTotalCount]; } if (token.progressBlock) { token.progressBlock(finishedCount, totalCount); } }]; } - (void)callCompletionBlockForToken:(SDWebImagePrefetchToken *)token { if (!token) { return; } BOOL shoulCallDelegate = [self.delegate respondsToSelector:@selector(imagePrefetcher:didFinishWithTotalCount:skippedCount:)] && ([self countOfRunningTokens] == 1); // last one NSUInteger tokenTotalCount = [self tokenTotalCount]; NSUInteger tokenSkippedCount = [self tokenSkippedCount]; NSUInteger finishedCount = atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed); NSUInteger skippedCount = atomic_load_explicit(&(token->_skippedCount), memory_order_relaxed); SDCallbackQueue *queue = token.context[SDWebImageContextCallbackQueue]; if (!queue) { queue = self.callbackQueue; } [(queue ?: SDCallbackQueue.mainQueue) async:^{ if (shoulCallDelegate) { [self.delegate imagePrefetcher:self didFinishWithTotalCount:tokenTotalCount skippedCount:tokenSkippedCount]; } if (token.completionBlock) { token.completionBlock(finishedCount, skippedCount); } }]; } #pragma mark - Helper - (NSUInteger)tokenTotalCount { NSUInteger tokenTotalCount = 0; @synchronized (self.runningTokens) { for (SDWebImagePrefetchToken *token in self.runningTokens) { tokenTotalCount += token->_totalCount; } } return tokenTotalCount; } - (NSUInteger)tokenSkippedCount { NSUInteger tokenSkippedCount = 0; @synchronized (self.runningTokens) { for (SDWebImagePrefetchToken *token in self.runningTokens) { tokenSkippedCount += atomic_load_explicit(&(token->_skippedCount), memory_order_relaxed); } } return tokenSkippedCount; } - (NSUInteger)tokenFinishedCount { NSUInteger tokenFinishedCount = 0; @synchronized (self.runningTokens) { for (SDWebImagePrefetchToken *token in self.runningTokens) { tokenFinishedCount += atomic_load_explicit(&(token->_finishedCount), memory_order_relaxed); } } return tokenFinishedCount; } - (void)addRunningToken:(SDWebImagePrefetchToken *)token { if (!token) { return; } @synchronized (self.runningTokens) { [self.runningTokens addObject:token]; } } - (void)removeRunningToken:(SDWebImagePrefetchToken *)token { if (!token) { return; } @synchronized (self.runningTokens) { [self.runningTokens removeObject:token]; } } - (NSUInteger)countOfRunningTokens { NSUInteger count = 0; @synchronized (self.runningTokens) { count = self.runningTokens.count; } return count; } @end @implementation SDWebImagePrefetchToken - (instancetype)init { self = [super init]; if (self) { SD_LOCK_INIT(_prefetchOperationsLock); SD_LOCK_INIT(_loadOperationsLock); } return self; } - (void)cancel { SD_LOCK(_prefetchOperationsLock); [self.prefetchOperations compact]; for (id operation in self.prefetchOperations) { id strongOperation = operation; if (strongOperation) { [strongOperation cancel]; } } self.prefetchOperations.count = 0; SD_UNLOCK(_prefetchOperationsLock); SD_LOCK(_loadOperationsLock); [self.loadOperations compact]; for (id operation in self.loadOperations) { id strongOperation = operation; if (strongOperation) { [strongOperation cancel]; } } self.loadOperations.count = 0; SD_UNLOCK(_loadOperationsLock); self.completionBlock = nil; self.progressBlock = nil; [self.prefetcher removeRunningToken:self]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageTransition.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_UIKIT || SD_MAC #import "SDImageCache.h" #if SD_UIKIT typedef UIViewAnimationOptions SDWebImageAnimationOptions; #else typedef NS_OPTIONS(NSUInteger, SDWebImageAnimationOptions) { SDWebImageAnimationOptionAllowsImplicitAnimation = 1 << 0, // specify `allowsImplicitAnimation` for the `NSAnimationContext` SDWebImageAnimationOptionCurveEaseInOut = 0 << 16, // default SDWebImageAnimationOptionCurveEaseIn = 1 << 16, SDWebImageAnimationOptionCurveEaseOut = 2 << 16, SDWebImageAnimationOptionCurveLinear = 3 << 16, SDWebImageAnimationOptionTransitionNone = 0 << 20, // default SDWebImageAnimationOptionTransitionFlipFromLeft = 1 << 20, SDWebImageAnimationOptionTransitionFlipFromRight = 2 << 20, SDWebImageAnimationOptionTransitionCurlUp = 3 << 20, SDWebImageAnimationOptionTransitionCurlDown = 4 << 20, SDWebImageAnimationOptionTransitionCrossDissolve = 5 << 20, SDWebImageAnimationOptionTransitionFlipFromTop = 6 << 20, SDWebImageAnimationOptionTransitionFlipFromBottom = 7 << 20, }; #endif typedef void (^SDWebImageTransitionPreparesBlock)(__kindof UIView * _Nonnull view, UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL); typedef void (^SDWebImageTransitionAnimationsBlock)(__kindof UIView * _Nonnull view, UIImage * _Nullable image); typedef void (^SDWebImageTransitionCompletionBlock)(BOOL finished); /** This class is used to provide a transition animation after the view category load image finished. Use this on `sd_imageTransition` in UIView+WebCache.h for UIKit(iOS & tvOS), we use `+[UIView transitionWithView:duration:options:animations:completion]` for transition animation. for AppKit(macOS), we use `+[NSAnimationContext runAnimationGroup:completionHandler:]` for transition animation. You can call `+[NSAnimationContext currentContext]` to grab the context during animations block. @note These transition are provided for basic usage. If you need complicated animation, consider to directly use Core Animation or use `SDWebImageAvoidAutoSetImage` and implement your own after image load finished. */ @interface SDWebImageTransition : NSObject /** By default, we set the image to the view at the beginning of the animations. You can disable this and provide custom set image process */ @property (nonatomic, assign) BOOL avoidAutoSetImage; /** The duration of the transition animation, measured in seconds. Defaults to 0.5. */ @property (nonatomic, assign) NSTimeInterval duration; /** The timing function used for all animations within this transition animation (macOS). */ @property (nonatomic, strong, nullable) CAMediaTimingFunction *timingFunction API_UNAVAILABLE(ios, tvos, watchos) API_DEPRECATED("Use SDWebImageAnimationOptions instead, or grab NSAnimationContext.currentContext and modify the timingFunction", macos(10.10, 10.10)); /** A mask of options indicating how you want to perform the animations. */ @property (nonatomic, assign) SDWebImageAnimationOptions animationOptions; /** A block object to be executed before the animation sequence starts. */ @property (nonatomic, copy, nullable) SDWebImageTransitionPreparesBlock prepares; /** A block object that contains the changes you want to make to the specified view. */ @property (nonatomic, copy, nullable) SDWebImageTransitionAnimationsBlock animations; /** A block object to be executed when the animation sequence ends. */ @property (nonatomic, copy, nullable) SDWebImageTransitionCompletionBlock completion; @end /** Convenience way to create transition. Remember to specify the duration if needed. for UIKit, these transition just use the correspond `animationOptions`. By default we enable `UIViewAnimationOptionAllowUserInteraction` to allow user interaction during transition. for AppKit, these transition use Core Animation in `animations`. So your view must be layer-backed. Set `wantsLayer = YES` before you apply it. */ @interface SDWebImageTransition (Conveniences) /// Fade-in transition. @property (nonatomic, class, nonnull, readonly) SDWebImageTransition *fadeTransition; /// Flip from left transition. @property (nonatomic, class, nonnull, readonly) SDWebImageTransition *flipFromLeftTransition; /// Flip from right transition. @property (nonatomic, class, nonnull, readonly) SDWebImageTransition *flipFromRightTransition; /// Flip from top transition. @property (nonatomic, class, nonnull, readonly) SDWebImageTransition *flipFromTopTransition; /// Flip from bottom transition. @property (nonatomic, class, nonnull, readonly) SDWebImageTransition *flipFromBottomTransition; /// Curl up transition. @property (nonatomic, class, nonnull, readonly) SDWebImageTransition *curlUpTransition; /// Curl down transition. @property (nonatomic, class, nonnull, readonly) SDWebImageTransition *curlDownTransition; /// Fade-in transition with duration. /// @param duration transition duration, use ease-in-out + (nonnull instancetype)fadeTransitionWithDuration:(NSTimeInterval)duration NS_SWIFT_NAME(fade(duration:)); /// Flip from left transition with duration. /// @param duration transition duration, use ease-in-out + (nonnull instancetype)flipFromLeftTransitionWithDuration:(NSTimeInterval)duration NS_SWIFT_NAME(flipFromLeft(duration:)); /// Flip from right transition with duration. /// @param duration transition duration, use ease-in-out + (nonnull instancetype)flipFromRightTransitionWithDuration:(NSTimeInterval)duration NS_SWIFT_NAME(flipFromRight(duration:)); /// Flip from top transition with duration. /// @param duration transition duration, use ease-in-out + (nonnull instancetype)flipFromTopTransitionWithDuration:(NSTimeInterval)duration NS_SWIFT_NAME(flipFromTop(duration:)); /// Flip from bottom transition with duration. /// @param duration transition duration, use ease-in-out + (nonnull instancetype)flipFromBottomTransitionWithDuration:(NSTimeInterval)duration NS_SWIFT_NAME(flipFromBottom(duration:)); /// Curl up transition with duration. /// @param duration transition duration, use ease-in-out + (nonnull instancetype)curlUpTransitionWithDuration:(NSTimeInterval)duration NS_SWIFT_NAME(curlUp(duration:)); /// Curl down transition with duration. /// @param duration transition duration, use ease-in-out + (nonnull instancetype)curlDownTransitionWithDuration:(NSTimeInterval)duration NS_SWIFT_NAME(curlDown(duration:)); @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/SDWebImageTransition.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageTransition.h" #if SD_UIKIT || SD_MAC #if SD_MAC #import "SDWebImageTransitionInternal.h" #import "SDInternalMacros.h" CAMediaTimingFunction * SDTimingFunctionFromAnimationOptions(SDWebImageAnimationOptions options) { if (SD_OPTIONS_CONTAINS(SDWebImageAnimationOptionCurveLinear, options)) { return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; } else if (SD_OPTIONS_CONTAINS(SDWebImageAnimationOptionCurveEaseIn, options)) { return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn]; } else if (SD_OPTIONS_CONTAINS(SDWebImageAnimationOptionCurveEaseOut, options)) { return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]; } else if (SD_OPTIONS_CONTAINS(SDWebImageAnimationOptionCurveEaseInOut, options)) { return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; } else { return [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault]; } } CATransition * SDTransitionFromAnimationOptions(SDWebImageAnimationOptions options) { if (SD_OPTIONS_CONTAINS(options, SDWebImageAnimationOptionTransitionCrossDissolve)) { CATransition *trans = [CATransition animation]; trans.type = kCATransitionFade; return trans; } else if (SD_OPTIONS_CONTAINS(options, SDWebImageAnimationOptionTransitionFlipFromLeft)) { CATransition *trans = [CATransition animation]; trans.type = kCATransitionPush; trans.subtype = kCATransitionFromLeft; return trans; } else if (SD_OPTIONS_CONTAINS(options, SDWebImageAnimationOptionTransitionFlipFromRight)) { CATransition *trans = [CATransition animation]; trans.type = kCATransitionPush; trans.subtype = kCATransitionFromRight; return trans; } else if (SD_OPTIONS_CONTAINS(options, SDWebImageAnimationOptionTransitionFlipFromTop)) { CATransition *trans = [CATransition animation]; trans.type = kCATransitionPush; trans.subtype = kCATransitionFromTop; return trans; } else if (SD_OPTIONS_CONTAINS(options, SDWebImageAnimationOptionTransitionFlipFromBottom)) { CATransition *trans = [CATransition animation]; trans.type = kCATransitionPush; trans.subtype = kCATransitionFromBottom; return trans; } else if (SD_OPTIONS_CONTAINS(options, SDWebImageAnimationOptionTransitionCurlUp)) { CATransition *trans = [CATransition animation]; trans.type = kCATransitionReveal; trans.subtype = kCATransitionFromTop; return trans; } else if (SD_OPTIONS_CONTAINS(options, SDWebImageAnimationOptionTransitionCurlDown)) { CATransition *trans = [CATransition animation]; trans.type = kCATransitionReveal; trans.subtype = kCATransitionFromBottom; return trans; } else { return nil; } } #endif @implementation SDWebImageTransition - (instancetype)init { self = [super init]; if (self) { self.duration = 0.5; } return self; } @end @implementation SDWebImageTransition (Conveniences) + (SDWebImageTransition *)fadeTransition { return [self fadeTransitionWithDuration:0.5]; } + (SDWebImageTransition *)fadeTransitionWithDuration:(NSTimeInterval)duration { SDWebImageTransition *transition = [SDWebImageTransition new]; transition.duration = duration; #if SD_UIKIT transition.animationOptions = UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionAllowUserInteraction; #else transition.animationOptions = SDWebImageAnimationOptionTransitionCrossDissolve; #endif return transition; } + (SDWebImageTransition *)flipFromLeftTransition { return [self flipFromLeftTransitionWithDuration:0.5]; } + (SDWebImageTransition *)flipFromLeftTransitionWithDuration:(NSTimeInterval)duration { SDWebImageTransition *transition = [SDWebImageTransition new]; transition.duration = duration; #if SD_UIKIT transition.animationOptions = UIViewAnimationOptionTransitionFlipFromLeft | UIViewAnimationOptionAllowUserInteraction; #else transition.animationOptions = SDWebImageAnimationOptionTransitionFlipFromLeft; #endif return transition; } + (SDWebImageTransition *)flipFromRightTransition { return [self flipFromRightTransitionWithDuration:0.5]; } + (SDWebImageTransition *)flipFromRightTransitionWithDuration:(NSTimeInterval)duration { SDWebImageTransition *transition = [SDWebImageTransition new]; transition.duration = duration; #if SD_UIKIT transition.animationOptions = UIViewAnimationOptionTransitionFlipFromRight | UIViewAnimationOptionAllowUserInteraction; #else transition.animationOptions = SDWebImageAnimationOptionTransitionFlipFromRight; #endif return transition; } + (SDWebImageTransition *)flipFromTopTransition { return [self flipFromTopTransitionWithDuration:0.5]; } + (SDWebImageTransition *)flipFromTopTransitionWithDuration:(NSTimeInterval)duration { SDWebImageTransition *transition = [SDWebImageTransition new]; transition.duration = duration; #if SD_UIKIT transition.animationOptions = UIViewAnimationOptionTransitionFlipFromTop | UIViewAnimationOptionAllowUserInteraction; #else transition.animationOptions = SDWebImageAnimationOptionTransitionFlipFromTop; #endif return transition; } + (SDWebImageTransition *)flipFromBottomTransition { return [self flipFromBottomTransitionWithDuration:0.5]; } + (SDWebImageTransition *)flipFromBottomTransitionWithDuration:(NSTimeInterval)duration { SDWebImageTransition *transition = [SDWebImageTransition new]; transition.duration = duration; #if SD_UIKIT transition.animationOptions = UIViewAnimationOptionTransitionFlipFromBottom | UIViewAnimationOptionAllowUserInteraction; #else transition.animationOptions = SDWebImageAnimationOptionTransitionFlipFromBottom; #endif return transition; } + (SDWebImageTransition *)curlUpTransition { return [self curlUpTransitionWithDuration:0.5]; } + (SDWebImageTransition *)curlUpTransitionWithDuration:(NSTimeInterval)duration { SDWebImageTransition *transition = [SDWebImageTransition new]; transition.duration = duration; #if SD_UIKIT transition.animationOptions = UIViewAnimationOptionTransitionCurlUp | UIViewAnimationOptionAllowUserInteraction; #else transition.animationOptions = SDWebImageAnimationOptionTransitionCurlUp; #endif return transition; } + (SDWebImageTransition *)curlDownTransition { return [self curlDownTransitionWithDuration:0.5]; } + (SDWebImageTransition *)curlDownTransitionWithDuration:(NSTimeInterval)duration { SDWebImageTransition *transition = [SDWebImageTransition new]; transition.duration = duration; #if SD_UIKIT transition.animationOptions = UIViewAnimationOptionTransitionCurlDown | UIViewAnimationOptionAllowUserInteraction; #else transition.animationOptions = SDWebImageAnimationOptionTransitionCurlDown; #endif transition.duration = duration; return transition; } @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIButton+WebCache.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_UIKIT #import "SDWebImageManager.h" /** * Integrates SDWebImage async downloading and caching of remote images with UIButton. */ @interface UIButton (WebCache) #pragma mark - Image /** * Get the current image URL. * This simply translate to `[self sd_imageURLForState:self.state]` from v5.18.0 */ @property (nonatomic, strong, readonly, nullable) NSURL *sd_currentImageURL; /** * Get the image URL for a control state. * * @param state Which state you want to know the URL for. The values are described in UIControlState. */ - (nullable NSURL *)sd_imageURLForState:(UIControlState)state; /** * Get the image operation key for a control state. * * @param state Which state you want to know the URL for. The values are described in UIControlState. */ - (nonnull NSString *)sd_imageOperationKeyForState:(UIControlState)state; /** * Set the button `image` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. */ - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state NS_REFINED_FOR_SWIFT; /** * Set the button `image` with an `url` and a placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @see sd_setImageWithURL:placeholderImage:options: */ - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT; /** * Set the button `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. */ - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT; /** * Set the button `image` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. */ - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; /** * Set the button `image` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `image` with an `url`, placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT; /** * Set the button `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `image` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; #pragma mark - Background Image /** * Get the current background image URL. */ @property (nonatomic, strong, readonly, nullable) NSURL *sd_currentBackgroundImageURL; /** * Get the background image operation key for a control state. * * @param state Which state you want to know the URL for. The values are described in UIControlState. */ - (nonnull NSString *)sd_backgroundImageOperationKeyForState:(UIControlState)state; /** * Get the background image URL for a control state. * * @param state Which state you want to know the URL for. The values are described in UIControlState. */ - (nullable NSURL *)sd_backgroundImageURLForState:(UIControlState)state; /** * Set the button `backgroundImage` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. */ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state NS_REFINED_FOR_SWIFT; /** * Set the button `backgroundImage` with an `url` and a placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @see sd_setImageWithURL:placeholderImage:options: */ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT; /** * Set the button `backgroundImage` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. */ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT; /** * Set the button `backgroundImage` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. */ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; /** * Set the button `backgroundImage` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `backgroundImage` with an `url`, placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param state The state that uses the specified title. The values are described in UIControlState. * @param placeholder The image to be set initially, until the image request finishes. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT; /** * Set the button `backgroundImage` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `backgroundImage` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the button `backgroundImage` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; #pragma mark - Cancel /** * Cancel the current image download */ - (void)sd_cancelImageLoadForState:(UIControlState)state; /** * Cancel the current backgroundImage download */ - (void)sd_cancelBackgroundImageLoadForState:(UIControlState)state; @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIButton+WebCache.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIButton+WebCache.h" #if SD_UIKIT #import "objc/runtime.h" #import "UIView+WebCacheOperation.h" #import "UIView+WebCacheState.h" #import "UIView+WebCache.h" #import "SDInternalMacros.h" @implementation UIButton (WebCache) #pragma mark - Image - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state { [self sd_setImageWithURL:url forState:state placeholderImage:nil options:0 completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder { [self sd_setImageWithURL:url forState:state placeholderImage:placeholder options:0 completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options { [self sd_setImageWithURL:url forState:state placeholderImage:placeholder options:options progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { [self sd_setImageWithURL:url forState:state placeholderImage:placeholder options:options context:context progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url forState:state placeholderImage:nil options:0 completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url forState:state placeholderImage:placeholder options:0 completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url forState:state placeholderImage:placeholder options:options progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url forState:state placeholderImage:placeholder options:options context:nil progress:progressBlock completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { SDWebImageMutableContext *mutableContext; if (context) { mutableContext = [context mutableCopy]; } else { mutableContext = [NSMutableDictionary dictionary]; } mutableContext[SDWebImageContextSetImageOperationKey] = [self sd_imageOperationKeyForState:state]; @weakify(self); [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options context:mutableContext setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { @strongify(self); [self setImage:image forState:state]; } progress:progressBlock completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { if (completedBlock) { completedBlock(image, error, cacheType, imageURL); } }]; } #pragma mark - Background Image - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state { [self sd_setBackgroundImageWithURL:url forState:state placeholderImage:nil options:0 completed:nil]; } - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder { [self sd_setBackgroundImageWithURL:url forState:state placeholderImage:placeholder options:0 completed:nil]; } - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options { [self sd_setBackgroundImageWithURL:url forState:state placeholderImage:placeholder options:options progress:nil completed:nil]; } - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { [self sd_setBackgroundImageWithURL:url forState:state placeholderImage:placeholder options:options context:context progress:nil completed:nil]; } - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setBackgroundImageWithURL:url forState:state placeholderImage:nil options:0 completed:completedBlock]; } - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setBackgroundImageWithURL:url forState:state placeholderImage:placeholder options:0 completed:completedBlock]; } - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setBackgroundImageWithURL:url forState:state placeholderImage:placeholder options:options progress:nil completed:completedBlock]; } - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setBackgroundImageWithURL:url forState:state placeholderImage:placeholder options:options context:nil progress:progressBlock completed:completedBlock]; } - (void)sd_setBackgroundImageWithURL:(nullable NSURL *)url forState:(UIControlState)state placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { SDWebImageMutableContext *mutableContext; if (context) { mutableContext = [context mutableCopy]; } else { mutableContext = [NSMutableDictionary dictionary]; } mutableContext[SDWebImageContextSetImageOperationKey] = [self sd_backgroundImageOperationKeyForState:state]; @weakify(self); [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options context:mutableContext setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { @strongify(self); [self setBackgroundImage:image forState:state]; } progress:progressBlock completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { if (completedBlock) { completedBlock(image, error, cacheType, imageURL); } }]; } #pragma mark - Cancel - (void)sd_cancelImageLoadForState:(UIControlState)state { [self sd_cancelImageLoadOperationWithKey:[self sd_imageOperationKeyForState:state]]; } - (void)sd_cancelBackgroundImageLoadForState:(UIControlState)state { [self sd_cancelImageLoadOperationWithKey:[self sd_backgroundImageOperationKeyForState:state]]; } #pragma mark - State - (NSString *)sd_imageOperationKeyForState:(UIControlState)state { return [NSString stringWithFormat:@"UIButtonImageOperation%lu", (unsigned long)state]; } - (NSString *)sd_backgroundImageOperationKeyForState:(UIControlState)state { return [NSString stringWithFormat:@"UIButtonBackgroundImageOperation%lu", (unsigned long)state]; } - (NSURL *)sd_currentImageURL { NSURL *url = [self sd_imageURLForState:self.state]; if (!url) { [self sd_imageURLForState:UIControlStateNormal]; } return url; } - (NSURL *)sd_imageURLForState:(UIControlState)state { return [self sd_imageLoadStateForKey:[self sd_imageOperationKeyForState:state]].url; } #pragma mark - Background State - (NSURL *)sd_currentBackgroundImageURL { NSURL *url = [self sd_backgroundImageURLForState:self.state]; if (!url) { url = [self sd_backgroundImageURLForState:UIControlStateNormal]; } return url; } - (NSURL *)sd_backgroundImageURLForState:(UIControlState)state { return [self sd_imageLoadStateForKey:[self sd_backgroundImageOperationKeyForState:state]].url; } @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+ExtendedCacheData.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Fabrice Aneche * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" @interface UIImage (ExtendedCacheData) /** Read and Write the extended object and bind it to the image. Which can hold some extra metadata like Image's scale factor, URL rich link, date, etc. The extended object should conforms to NSCoding, which we use `NSKeyedArchiver` and `NSKeyedUnarchiver` to archive it to data, and write to disk cache. @note The disk cache preserve both of the data and extended data with the same cache key. For manual query, use the `SDDiskCache` protocol method `extendedDataForKey:` instead. @note You can specify arbitrary object conforms to NSCoding (NSObject protocol here is used to support object using `NS_ROOT_CLASS`, which is not NSObject subclass). If you load image from disk cache, you should check the extended object class to avoid corrupted data. @warning This object don't need to implements NSSecureCoding (but it's recommended), because we allows arbitrary class. */ @property (nonatomic, strong, nullable) id sd_extendedObject; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+ExtendedCacheData.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Fabrice Aneche * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIImage+ExtendedCacheData.h" #import @implementation UIImage (ExtendedCacheData) - (id)sd_extendedObject { return objc_getAssociatedObject(self, @selector(sd_extendedObject)); } - (void)setSd_extendedObject:(id)sd_extendedObject { objc_setAssociatedObject(self, @selector(sd_extendedObject), sd_extendedObject, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" /** UIImage category about force decode feature (avoid Image/IO's lazy decoding during rendering behavior). */ @interface UIImage (ForceDecode) /** A bool value indicating whether the image has already been decoded. This can help to avoid extra force decode. Force decode is used for 2 cases: -- 1. for ImageIO created image (via `CGImageCreateWithImageSource` SPI), it's lazy and we trigger the decode before rendering -- 2. for non-ImageIO created image (via `CGImageCreate` API), we can ensure it's alignment is suitable to render on screen without copy by CoreAnimation @note For coder plugin developer, always use the SDImageCoderHelper's `colorSpaceGetDeviceRGB`/`preferredPixelFormat` to create CGImage. @note For more information why force decode, see: https://github.com/path/FastImageCache#byte-alignment @note From v5.17.0, the default value is always NO. Use `SDImageForceDecodePolicy` to control complicated policy. */ @property (nonatomic, assign) BOOL sd_isDecoded; /** Decode the provided image. This is useful if you want to force decode the image before rendering to improve performance. @param image The image to be decoded @return The decoded image */ + (nullable UIImage *)sd_decodedImageWithImage:(nullable UIImage *)image; /** Decode and scale down the provided image @param image The image to be decoded @return The decoded and scaled down image */ + (nullable UIImage *)sd_decodedAndScaledDownImageWithImage:(nullable UIImage *)image; /** Decode and scale down the provided image with limit bytes @param image The image to be decoded @param bytes The limit bytes size. Provide 0 to use the build-in limit. @return The decoded and scaled down image */ + (nullable UIImage *)sd_decodedAndScaledDownImageWithImage:(nullable UIImage *)image limitBytes:(NSUInteger)bytes; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+ForceDecode.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIImage+ForceDecode.h" #import "SDImageCoderHelper.h" #import "objc/runtime.h" #import "NSImage+Compatibility.h" @implementation UIImage (ForceDecode) - (BOOL)sd_isDecoded { NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isDecoded)); return [value boolValue]; } - (void)setSd_isDecoded:(BOOL)sd_isDecoded { objc_setAssociatedObject(self, @selector(sd_isDecoded), @(sd_isDecoded), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } + (nullable UIImage *)sd_decodedImageWithImage:(nullable UIImage *)image { if (!image) { return nil; } return [SDImageCoderHelper decodedImageWithImage:image]; } + (nullable UIImage *)sd_decodedAndScaledDownImageWithImage:(nullable UIImage *)image { return [self sd_decodedAndScaledDownImageWithImage:image limitBytes:0]; } + (nullable UIImage *)sd_decodedAndScaledDownImageWithImage:(nullable UIImage *)image limitBytes:(NSUInteger)bytes { if (!image) { return nil; } return [SDImageCoderHelper decodedAndScaledDownImageWithImage:image limitBytes:bytes]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+GIF.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Laurin Brandner * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" /** This category is just use as a convenience method. For more detail control, use methods in `UIImage+MultiFormat.h` or directly use `SDImageCoder`. */ @interface UIImage (GIF) /** Creates an animated UIImage from an NSData. This will create animated image if the data is Animated GIF. And will create a static image is the data is Static GIF. @param data The GIF data @return The created image */ + (nullable UIImage *)sd_imageWithGIFData:(nullable NSData *)data; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+GIF.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Laurin Brandner * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIImage+GIF.h" #import "SDImageGIFCoder.h" @implementation UIImage (GIF) + (nullable UIImage *)sd_imageWithGIFData:(nullable NSData *)data { if (!data) { return nil; } return [[SDImageGIFCoder sharedCoder] decodedImageWithData:data options:0]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+MemoryCacheCost.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" /** UIImage category for memory cache cost. */ @interface UIImage (MemoryCacheCost) /** The memory cache cost for specify image used by image cache. The cost function is the bytes size held in memory. If you set some associated object to `UIImage`, you can set the custom value to indicate the memory cost. For `UIImage`, this method return the single frame bytes size when `image.images` is nil for static image. Return full frame bytes size when `image.images` is not nil for animated image. For `NSImage`, this method return the single frame bytes size because `NSImage` does not store all frames in memory. @note Note that because of the limitations of category this property can get out of sync if you create another instance with CGImage or other methods. @note For custom animated class conforms to `SDAnimatedImage`, you can override this getter method in your subclass to return a more proper value instead, which representing the current frame's total bytes. */ @property (assign, nonatomic) NSUInteger sd_memoryCost; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+MemoryCacheCost.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIImage+MemoryCacheCost.h" #import "objc/runtime.h" #import "NSImage+Compatibility.h" FOUNDATION_STATIC_INLINE NSUInteger SDMemoryCacheCostForImage(UIImage *image) { CGImageRef imageRef = image.CGImage; if (!imageRef) { return 0; } NSUInteger bytesPerFrame = CGImageGetBytesPerRow(imageRef) * CGImageGetHeight(imageRef); NSUInteger frameCount; #if SD_MAC frameCount = 1; #elif SD_UIKIT || SD_WATCH // Filter the same frame in `_UIAnimatedImage`. frameCount = image.images.count > 1 ? [NSSet setWithArray:image.images].count : 1; #endif NSUInteger cost = bytesPerFrame * frameCount; return cost; } @implementation UIImage (MemoryCacheCost) - (NSUInteger)sd_memoryCost { NSNumber *value = objc_getAssociatedObject(self, @selector(sd_memoryCost)); NSUInteger memoryCost; if (value != nil) { memoryCost = [value unsignedIntegerValue]; } else { memoryCost = SDMemoryCacheCostForImage(self); } return memoryCost; } - (void)setSd_memoryCost:(NSUInteger)sd_memoryCost { objc_setAssociatedObject(self, @selector(sd_memoryCost), @(sd_memoryCost), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import "NSData+ImageContentType.h" #import "SDImageCoder.h" /** UIImage category for image metadata, including animation, loop count, format, incremental, etc. */ @interface UIImage (Metadata) /** * UIKit: * For static image format, this value is always 0. * For animated image format, 0 means infinite looping. * Note that because of the limitations of categories this property can get out of sync if you create another instance with CGImage or other methods. * AppKit: * NSImage currently only support animated via `NSBitmapImageRep`(GIF) or `SDAnimatedImageRep`(APNG/GIF/WebP) unlike UIImage. * The getter of this property will get the loop count from animated imageRep * The setter of this property will set the loop count from animated imageRep * SDAnimatedImage: * Returns `animatedImageLoopCount` */ @property (nonatomic, assign) NSUInteger sd_imageLoopCount; /** * UIKit: * Returns the `images`'s count by unapply the patch for the different frame durations. Which matches the real visible frame count when displaying on UIImageView. * See more in `SDImageCoderHelper.animatedImageWithFrames`. * Returns 1 for static image. * AppKit: * Returns the underlaying `NSBitmapImageRep` or `SDAnimatedImageRep` frame count. * Returns 1 for static image. * SDAnimatedImage: * Returns `animatedImageFrameCount` for animated image, 1 for static image. */ @property (nonatomic, assign, readonly) NSUInteger sd_imageFrameCount; /** * UIKit: * Check the `images` array property. * AppKit: * NSImage currently only support animated via GIF imageRep unlike UIImage. It will check the imageRep's frame count > 1. * SDAnimatedImage: * Check `animatedImageFrameCount` > 1 */ @property (nonatomic, assign, readonly) BOOL sd_isAnimated; /** * UIKit: * Check the `isSymbolImage` property. Also check the system PDF(iOS 11+) && SVG(iOS 13+) support. * AppKit: * NSImage supports PDF && SVG && EPS imageRep, check the imageRep class. * SDAnimatedImage: * Returns `NO` */ @property (nonatomic, assign, readonly) BOOL sd_isVector; /** * The image format represent the original compressed image data format. * If you don't manually specify a format, this information is retrieve from CGImage using `CGImageGetUTType`, which may return nil for non-CG based image. At this time it will return `SDImageFormatUndefined` as default value. * @note Note that because of the limitations of categories this property can get out of sync if you create another instance with CGImage or other methods. * @note For `SDAnimatedImage`, returns `animatedImageFormat` when animated, or fallback when static. */ @property (nonatomic, assign) SDImageFormat sd_imageFormat; /** A bool value indicating whether the image is during incremental decoding and may not contains full pixels. */ @property (nonatomic, assign) BOOL sd_isIncremental; /** A bool value indicating that the image is transformed from original image, so the image data may not always match original download one. */ @property (nonatomic, assign) BOOL sd_isTransformed; /** A bool value indicating that the image is using thumbnail decode with smaller size, so the image data may not always match original download one. @note This just check `sd_decodeOptions[.decodeThumbnailPixelSize] > CGSize.zero` */ @property (nonatomic, assign, readonly) BOOL sd_isThumbnail; /** A dictionary value contains the decode options when decoded from SDWebImage loading system (say, `SDImageCacheDecodeImageData/SDImageLoaderDecode[Progressive]ImageData`) It may not always available and only image decoding related options will be saved. (including [.decodeScaleFactor, .decodeThumbnailPixelSize, .decodePreserveAspectRatio, .decodeFirstFrameOnly]) @note This is used to identify and check the image is from thumbnail decoding, and the callback's data **will be nil** (because this time the data saved to disk does not match the image return to you. If you need full size data, query the cache with full size url key) @warning You should not store object inside which keep strong reference to image itself, which will cause retain cycle. @warning This API exist only because of current SDWebImageDownloader bad design which does not callback the context we call it. There will be refactor in future (API break), use with caution. */ @property (nonatomic, copy) SDImageCoderOptions *sd_decodeOptions; /** A bool value indicating that the image is using HDR @note Only valid for CGImage based, for CIImage based, the returned value is not correct. */ @property (nonatomic, assign, readonly) BOOL sd_isHighDynamicRange; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+Metadata.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIImage+Metadata.h" #import "NSImage+Compatibility.h" #import "SDInternalMacros.h" #import "objc/runtime.h" #import "SDImageCoderHelper.h" @implementation UIImage (Metadata) #if SD_UIKIT || SD_WATCH - (NSUInteger)sd_imageLoopCount { NSUInteger imageLoopCount = 0; NSNumber *value = objc_getAssociatedObject(self, @selector(sd_imageLoopCount)); if ([value isKindOfClass:[NSNumber class]]) { imageLoopCount = value.unsignedIntegerValue; } return imageLoopCount; } - (void)setSd_imageLoopCount:(NSUInteger)sd_imageLoopCount { NSNumber *value = @(sd_imageLoopCount); objc_setAssociatedObject(self, @selector(sd_imageLoopCount), value, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSUInteger)sd_imageFrameCount { NSArray *animatedImages = self.images; if (!animatedImages || animatedImages.count <= 1) { return 1; } NSNumber *value = objc_getAssociatedObject(self, @selector(sd_imageFrameCount)); if ([value isKindOfClass:[NSNumber class]]) { return [value unsignedIntegerValue]; } __block NSUInteger frameCount = 1; __block UIImage *previousImage = animatedImages.firstObject; [animatedImages enumerateObjectsUsingBlock:^(UIImage * _Nonnull image, NSUInteger idx, BOOL * _Nonnull stop) { // ignore first if (idx == 0) { return; } if (![image isEqual:previousImage]) { frameCount++; } previousImage = image; }]; objc_setAssociatedObject(self, @selector(sd_imageFrameCount), @(frameCount), OBJC_ASSOCIATION_RETAIN_NONATOMIC); return frameCount; } - (BOOL)sd_isAnimated { return (self.images != nil); } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" - (BOOL)sd_isVector { if (@available(iOS 13.0, tvOS 13.0, watchOS 6.0, *)) { // Xcode 11 supports symbol image, keep Xcode 10 compatible currently SEL SymbolSelector = NSSelectorFromString(@"isSymbolImage"); if ([self respondsToSelector:SymbolSelector] && [self performSelector:SymbolSelector]) { return YES; } // SVG SEL SVGSelector = SD_SEL_SPI(CGSVGDocument); if ([self respondsToSelector:SVGSelector] && [self performSelector:SVGSelector]) { return YES; } } if (@available(iOS 11.0, tvOS 11.0, watchOS 4.0, *)) { // PDF SEL PDFSelector = SD_SEL_SPI(CGPDFPage); if ([self respondsToSelector:PDFSelector] && [self performSelector:PDFSelector]) { return YES; } } return NO; } #pragma clang diagnostic pop #else - (NSUInteger)sd_imageLoopCount { NSUInteger imageLoopCount = 0; NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; NSBitmapImageRep *bitmapImageRep; if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { bitmapImageRep = (NSBitmapImageRep *)imageRep; } if (bitmapImageRep) { imageLoopCount = [[bitmapImageRep valueForProperty:NSImageLoopCount] unsignedIntegerValue]; } return imageLoopCount; } - (void)setSd_imageLoopCount:(NSUInteger)sd_imageLoopCount { NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; NSBitmapImageRep *bitmapImageRep; if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { bitmapImageRep = (NSBitmapImageRep *)imageRep; } if (bitmapImageRep) { [bitmapImageRep setProperty:NSImageLoopCount withValue:@(sd_imageLoopCount)]; } } - (NSUInteger)sd_imageFrameCount { NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; NSBitmapImageRep *bitmapImageRep; if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { bitmapImageRep = (NSBitmapImageRep *)imageRep; } if (bitmapImageRep) { return [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue]; } return 1; } - (BOOL)sd_isAnimated { BOOL isAnimated = NO; NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; NSBitmapImageRep *bitmapImageRep; if ([imageRep isKindOfClass:[NSBitmapImageRep class]]) { bitmapImageRep = (NSBitmapImageRep *)imageRep; } if (bitmapImageRep) { NSUInteger frameCount = [[bitmapImageRep valueForProperty:NSImageFrameCount] unsignedIntegerValue]; isAnimated = frameCount > 1 ? YES : NO; } return isAnimated; } - (BOOL)sd_isVector { NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); // This may returns a NSProxy, so don't use `class` to check NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; if ([imageRep isKindOfClass:[NSPDFImageRep class]]) { return YES; } if ([imageRep isKindOfClass:[NSEPSImageRep class]]) { return YES; } Class NSSVGImageRepClass = NSClassFromString([NSString stringWithFormat:@"_%@", SD_NSSTRING(NSSVGImageRep)]); if ([imageRep isKindOfClass:NSSVGImageRepClass]) { return YES; } return NO; } #endif - (SDImageFormat)sd_imageFormat { SDImageFormat imageFormat = SDImageFormatUndefined; NSNumber *value = objc_getAssociatedObject(self, @selector(sd_imageFormat)); if ([value isKindOfClass:[NSNumber class]]) { imageFormat = value.integerValue; return imageFormat; } // Check CGImage's UTType, may return nil for non-Image/IO based image CFStringRef uttype = CGImageGetUTType(self.CGImage); imageFormat = [NSData sd_imageFormatFromUTType:uttype]; return imageFormat; } - (void)setSd_imageFormat:(SDImageFormat)sd_imageFormat { objc_setAssociatedObject(self, @selector(sd_imageFormat), @(sd_imageFormat), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)setSd_isIncremental:(BOOL)sd_isIncremental { objc_setAssociatedObject(self, @selector(sd_isIncremental), @(sd_isIncremental), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)sd_isIncremental { NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isIncremental)); return value.boolValue; } - (void)setSd_isTransformed:(BOOL)sd_isTransformed { objc_setAssociatedObject(self, @selector(sd_isTransformed), @(sd_isTransformed), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)sd_isTransformed { NSNumber *value = objc_getAssociatedObject(self, @selector(sd_isTransformed)); return value.boolValue; } - (void)setSd_decodeOptions:(SDImageCoderOptions *)sd_decodeOptions { objc_setAssociatedObject(self, @selector(sd_decodeOptions), sd_decodeOptions, OBJC_ASSOCIATION_COPY_NONATOMIC); } -(BOOL)sd_isThumbnail { CGSize thumbnailSize = CGSizeZero; NSValue *thumbnailSizeValue = self.sd_decodeOptions[SDImageCoderDecodeThumbnailPixelSize]; if (thumbnailSizeValue != nil) { #if SD_MAC thumbnailSize = thumbnailSizeValue.sizeValue; #else thumbnailSize = thumbnailSizeValue.CGSizeValue; #endif } return thumbnailSize.width > 0 && thumbnailSize.height > 0; } - (SDImageCoderOptions *)sd_decodeOptions { SDImageCoderOptions *value = objc_getAssociatedObject(self, @selector(sd_decodeOptions)); if ([value isKindOfClass:NSDictionary.class]) { return value; } return nil; } - (BOOL)sd_isHighDynamicRange { #if SD_MAC return [SDImageCoderHelper CGImageIsHDR:self.CGImage]; #else if (@available(iOS 17, tvOS 17, watchOS 10, *)) { return self.isHighDynamicRange; } else { return [SDImageCoderHelper CGImageIsHDR:self.CGImage]; } #endif } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+MultiFormat.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import "NSData+ImageContentType.h" /** UIImage category for convenient image format decoding/encoding. */ @interface UIImage (MultiFormat) #pragma mark - Decode /** Create and decode a image with the specify image data @param data The image data @return The created image */ + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data; /** Create and decode a image with the specify image data and scale @param data The image data @param scale The image scale factor. Should be greater than or equal to 1.0. @return The created image */ + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data scale:(CGFloat)scale; /** Create and decode a image with the specify image data and scale, allow specify animate/static control @param data The image data @param scale The image scale factor. Should be greater than or equal to 1.0. @param firstFrameOnly Even if the image data is animated image format, decode the first frame only as static image. @return The created image */ + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data scale:(CGFloat)scale firstFrameOnly:(BOOL)firstFrameOnly; #pragma mark - Encode /** Encode the current image to the data, the image format is unspecified @note If the receiver is `SDAnimatedImage`, this will return the animated image data if available. No more extra encoding process. @note For macOS, if the receiver contains only `SDAnimatedImageRep`, this will return the animated image data if available. No more extra encoding process. @return The encoded data. If can't encode, return nil */ - (nullable NSData *)sd_imageData; /** Encode the current image to data with the specify image format @param imageFormat The specify image format @return The encoded data. If can't encode, return nil */ - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat NS_SWIFT_NAME(sd_imageData(as:)); /** Encode the current image to data with the specify image format and compression quality @param imageFormat The specify image format @param compressionQuality The quality of the resulting image data. Value between 0.0-1.0. Some coders may not support compression quality. @return The encoded data. If can't encode, return nil */ - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality NS_SWIFT_NAME(sd_imageData(as:compressionQuality:)); /** Encode the current image to data with the specify image format and compression quality, allow specify animate/static control @param imageFormat The specify image format @param compressionQuality The quality of the resulting image data. Value between 0.0-1.0. Some coders may not support compression quality. @param firstFrameOnly Even if the image is animated image, encode the first frame only as static image. @return The encoded data. If can't encode, return nil */ - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality firstFrameOnly:(BOOL)firstFrameOnly NS_SWIFT_NAME(sd_imageData(as:compressionQuality:firstFrameOnly:)); @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+MultiFormat.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIImage+MultiFormat.h" #import "SDImageCodersManager.h" #import "SDAnimatedImageRep.h" #import "UIImage+Metadata.h" @implementation UIImage (MultiFormat) + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data { return [self sd_imageWithData:data scale:1]; } + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data scale:(CGFloat)scale { return [self sd_imageWithData:data scale:scale firstFrameOnly:NO]; } + (nullable UIImage *)sd_imageWithData:(nullable NSData *)data scale:(CGFloat)scale firstFrameOnly:(BOOL)firstFrameOnly { if (!data) { return nil; } SDImageCoderOptions *options = @{SDImageCoderDecodeScaleFactor : @(MAX(scale, 1)), SDImageCoderDecodeFirstFrameOnly : @(firstFrameOnly)}; return [[SDImageCodersManager sharedManager] decodedImageWithData:data options:options]; } - (nullable NSData *)sd_imageData { #if SD_MAC NSRect imageRect = NSMakeRect(0, 0, self.size.width, self.size.height); NSImageRep *imageRep = [self bestRepresentationForRect:imageRect context:nil hints:nil]; // Check weak animated data firstly if ([imageRep isKindOfClass:[SDAnimatedImageRep class]]) { SDAnimatedImageRep *animatedImageRep = (SDAnimatedImageRep *)imageRep; NSData *imageData = [animatedImageRep animatedImageData]; if (imageData) { return imageData; } } #endif return [self sd_imageDataAsFormat:self.sd_imageFormat]; } - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat { return [self sd_imageDataAsFormat:imageFormat compressionQuality:1]; } - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality { return [self sd_imageDataAsFormat:imageFormat compressionQuality:compressionQuality firstFrameOnly:NO]; } - (nullable NSData *)sd_imageDataAsFormat:(SDImageFormat)imageFormat compressionQuality:(double)compressionQuality firstFrameOnly:(BOOL)firstFrameOnly { SDImageCoderOptions *options = @{SDImageCoderEncodeCompressionQuality : @(compressionQuality), SDImageCoderEncodeFirstFrameOnly : @(firstFrameOnly)}; return [[SDImageCodersManager sharedManager] encodedDataWithImage:self format:imageFormat options:options]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" /// The scale mode to apply when image drawing on a container with different sizes. typedef NS_ENUM(NSUInteger, SDImageScaleMode) { /// The option to scale the content to fit the size of itself by changing the aspect ratio of the content if necessary. SDImageScaleModeFill = 0, /// The option to scale the content to fit the size of the view by maintaining the aspect ratio. Any remaining area of the view’s bounds is transparent. SDImageScaleModeAspectFit = 1, /// The option to scale the content to fill the size of the view. Some portion of the content may be clipped to fill the view’s bounds. SDImageScaleModeAspectFill = 2 }; #if SD_UIKIT || SD_WATCH typedef UIRectCorner SDRectCorner; #else typedef NS_OPTIONS(NSUInteger, SDRectCorner) { SDRectCornerTopLeft = 1 << 0, SDRectCornerTopRight = 1 << 1, SDRectCornerBottomLeft = 1 << 2, SDRectCornerBottomRight = 1 << 3, SDRectCornerAllCorners = ~0UL }; #endif /** Provide some common method for `UIImage`. Image process is based on Core Graphics and vImage. */ @interface UIImage (Transform) #pragma mark - Image Geometry /** Returns a new image which is resized from this image. You can specify a larger or smaller size than the image size. The image content will be changed with the scale mode. @param size The new size to be resized, values should be positive. @param scaleMode The scale mode for image content. @return The new image with the given size. */ - (nullable UIImage *)sd_resizedImageWithSize:(CGSize)size scaleMode:(SDImageScaleMode)scaleMode; /** Returns a new image which is cropped from this image. @param rect Image's inner rect. @return The new image with the cropping rect. */ - (nullable UIImage *)sd_croppedImageWithRect:(CGRect)rect; /** Rounds a new image with a given corner radius and corners. @param cornerRadius The radius of each corner oval. Values larger than half the rectangle's width or height are clamped appropriately to half the width or height. @param corners A bitmask value that identifies the corners that you want rounded. You can use this parameter to round only a subset of the corners of the rectangle. @param borderWidth The inset border line width. Values larger than half the rectangle's width or height are clamped appropriately to half the width or height. @param borderColor The border stroke color. nil means clear color. @return The new image with the round corner. */ - (nullable UIImage *)sd_roundedCornerImageWithRadius:(CGFloat)cornerRadius corners:(SDRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(nullable UIColor *)borderColor; /** Returns a new rotated image (relative to the center). @param angle Rotated radians in counterclockwise.⟲ @param fitSize YES: new image's size is extend to fit all content. NO: image's size will not change, content may be clipped. @return The new image with the rotation. */ - (nullable UIImage *)sd_rotatedImageWithAngle:(CGFloat)angle fitSize:(BOOL)fitSize; /** Returns a new horizontally(vertically) flipped image. @param horizontal YES to flip the image horizontally. ⇋ @param vertical YES to flip the image vertically. ⥯ @return The new image with the flipping. */ - (nullable UIImage *)sd_flippedImageWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical; #pragma mark - Image Blending /** Return a tinted image with the given color. This actually use `sourceIn` blend mode. @note Before 5.20, this API actually use `sourceAtop` and cause naming confusing. After 5.20, we match UIKit's behavior using `sourceIn`. @param tintColor The tint color. @return The new image with the tint color. */ - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor; /** Return a tinted image with the given color and blend mode. @note The blend mode treat `self` as background image (destination), treat `tintColor` as input image (source). So mostly you need `source` variant blend mode (use `sourceIn` not `destinationIn`), which is different from UIKit's `+[UIImage imageWithTintColor:]`. @param tintColor The tint color. @param blendMode The blend mode. @return The new image with the tint color. */ - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode; /** Return the pixel color at specify position. The point is from the top-left to the bottom-right and 0-based. The returned the color is always be RGBA format. The image must be CG-based. @note The point's x/y will be converted into integer. @note The point's x/y should not be smaller than 0, or greater than or equal to width/height. @note The overhead of object creation means this method is best suited for infrequent color sampling. For heavy image processing, grab the raw bitmap data and process yourself. @param point The position of pixel @warning This API currently support 8 bits per component only (RGBA8888 etc), not RGBA16U/RGBA10 @return The color for specify pixel, or nil if any error occur */ - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point; /** Return the pixel color array with specify rectangle. The rect is from the top-left to the bottom-right and 0-based. The returned the color is always be RGBA format. The image must be CG-based. @note The rect's origin and size will be converted into integer. @note The rect's width/height should not be smaller than or equal to 0. The minX/minY should not be smaller than 0. The maxX/maxY should not be greater than width/height. Attention this limit is different from `sd_colorAtPoint:` (point: (0, 0) like rect: (0, 0, 1, 1)) @note The overhead of object creation means this method is best suited for infrequent color sampling. For heavy image processing, grab the raw bitmap data and process yourself. @param rect The rectangle of pixels @return The color array for specify pixels, or nil if any error occur */ - (nullable NSArray *)sd_colorsWithRect:(CGRect)rect; #pragma mark - Image Effect /** Return a new image applied a blur effect. @param blurRadius The radius of the blur in points, 0 means no blur effect. @return The new image with blur effect, or nil if an error occurs (e.g. no enough memory). */ - (nullable UIImage *)sd_blurredImageWithRadius:(CGFloat)blurRadius; #if SD_UIKIT || SD_MAC /** Return a new image applied a CIFilter. @param filter The CIFilter to be applied to the image. @return The new image with the CIFilter, or nil if an error occurs (e.g. no enough memory). */ - (nullable UIImage *)sd_filteredImageWithFilter:(nonnull CIFilter *)filter; #endif @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImage+Transform.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIImage+Transform.h" #import "NSImage+Compatibility.h" #import "SDImageGraphics.h" #import "SDGraphicsImageRenderer.h" #import "NSBezierPath+SDRoundedCorners.h" #import "SDInternalMacros.h" #import #if SD_UIKIT || SD_MAC #import #endif static inline CGRect SDCGRectFitWithScaleMode(CGRect rect, CGSize size, SDImageScaleMode scaleMode) { rect = CGRectStandardize(rect); size.width = size.width < 0 ? -size.width : size.width; size.height = size.height < 0 ? -size.height : size.height; CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); switch (scaleMode) { case SDImageScaleModeAspectFit: case SDImageScaleModeAspectFill: { if (rect.size.width < 0.01 || rect.size.height < 0.01 || size.width < 0.01 || size.height < 0.01) { rect.origin = center; rect.size = CGSizeZero; } else { CGFloat scale; if (scaleMode == SDImageScaleModeAspectFit) { if (size.width / size.height < rect.size.width / rect.size.height) { scale = rect.size.height / size.height; } else { scale = rect.size.width / size.width; } } else { if (size.width / size.height < rect.size.width / rect.size.height) { scale = rect.size.width / size.width; } else { scale = rect.size.height / size.height; } } size.width *= scale; size.height *= scale; rect.size = size; rect.origin = CGPointMake(center.x - size.width * 0.5, center.y - size.height * 0.5); } } break; case SDImageScaleModeFill: default: { rect = rect; } } return rect; } static inline UIColor * SDGetColorFromGrayscale(Pixel_88 pixel, CGBitmapInfo bitmapInfo, CGColorSpaceRef cgColorSpace) { // Get alpha info, byteOrder info CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; CGFloat w = 0, a = 1; BOOL byteOrderNormal = NO; switch (byteOrderInfo) { case kCGBitmapByteOrderDefault: { byteOrderNormal = YES; } break; case kCGBitmapByteOrder16Little: case kCGBitmapByteOrder32Little: { } break; case kCGBitmapByteOrder16Big: case kCGBitmapByteOrder32Big: { byteOrderNormal = YES; } break; default: break; } switch (alphaInfo) { case kCGImageAlphaPremultipliedFirst: case kCGImageAlphaFirst: { if (byteOrderNormal) { // AW a = pixel[0] / 255.0; w = pixel[1] / 255.0; } else { // WA w = pixel[0] / 255.0; a = pixel[1] / 255.0; } } break; case kCGImageAlphaPremultipliedLast: case kCGImageAlphaLast: { if (byteOrderNormal) { // WA w = pixel[0] / 255.0; a = pixel[1] / 255.0; } else { // AW a = pixel[0] / 255.0; w = pixel[1] / 255.0; } } break; case kCGImageAlphaNone: { // W w = pixel[0] / 255.0; } break; case kCGImageAlphaNoneSkipLast: { if (byteOrderNormal) { // WX w = pixel[0] / 255.0; } else { // XW a = pixel[1] / 255.0; } } break; case kCGImageAlphaNoneSkipFirst: { if (byteOrderNormal) { // XW a = pixel[1] / 255.0; } else { // WX a = pixel[0] / 255.0; } } break; case kCGImageAlphaOnly: { // A a = pixel[0] / 255.0; } break; default: break; } #if SD_MAC // Mac supports ColorSync, to ensure the same bahvior, we convert color to sRGB NSColorSpace *colorSpace = [[NSColorSpace alloc] initWithCGColorSpace:cgColorSpace]; CGFloat components[2] = {w, a}; NSColor *color = [NSColor colorWithColorSpace:colorSpace components:components count:2]; return [color colorUsingColorSpace:NSColorSpace.genericGamma22GrayColorSpace]; #else return [UIColor colorWithWhite:w alpha:a]; #endif } static inline UIColor * SDGetColorFromRGBA8(Pixel_8888 pixel, CGBitmapInfo bitmapInfo, CGColorSpaceRef cgColorSpace) { // Get alpha info, byteOrder info CGImageAlphaInfo alphaInfo = bitmapInfo & kCGBitmapAlphaInfoMask; CGBitmapInfo byteOrderInfo = bitmapInfo & kCGBitmapByteOrderMask; CGFloat r = 0, g = 0, b = 0, a = 1; BOOL byteOrderNormal = NO; switch (byteOrderInfo) { case kCGBitmapByteOrderDefault: { byteOrderNormal = YES; } break; case kCGBitmapByteOrder16Little: case kCGBitmapByteOrder32Little: { } break; case kCGBitmapByteOrder16Big: case kCGBitmapByteOrder32Big: { byteOrderNormal = YES; } break; default: break; } switch (alphaInfo) { case kCGImageAlphaPremultipliedFirst: { if (byteOrderNormal) { // ARGB8888-premultiplied a = pixel[0] / 255.0; r = pixel[1] / 255.0; g = pixel[2] / 255.0; b = pixel[3] / 255.0; if (a > 0) { r /= a; g /= a; b /= a; } } else { // BGRA8888-premultiplied b = pixel[0] / 255.0; g = pixel[1] / 255.0; r = pixel[2] / 255.0; a = pixel[3] / 255.0; if (a > 0) { r /= a; g /= a; b /= a; } } break; } case kCGImageAlphaFirst: { if (byteOrderNormal) { // ARGB8888 a = pixel[0] / 255.0; r = pixel[1] / 255.0; g = pixel[2] / 255.0; b = pixel[3] / 255.0; } else { // BGRA8888 b = pixel[0] / 255.0; g = pixel[1] / 255.0; r = pixel[2] / 255.0; a = pixel[3] / 255.0; } } break; case kCGImageAlphaPremultipliedLast: { if (byteOrderNormal) { // RGBA8888-premultiplied r = pixel[0] / 255.0; g = pixel[1] / 255.0; b = pixel[2] / 255.0; a = pixel[3] / 255.0; if (a > 0) { r /= a; g /= a; b /= a; } } else { // ABGR8888-premultiplied a = pixel[0] / 255.0; b = pixel[1] / 255.0; g = pixel[2] / 255.0; r = pixel[3] / 255.0; if (a > 0) { r /= a; g /= a; b /= a; } } break; } case kCGImageAlphaLast: { if (byteOrderNormal) { // RGBA8888 r = pixel[0] / 255.0; g = pixel[1] / 255.0; b = pixel[2] / 255.0; a = pixel[3] / 255.0; } else { // ABGR8888 a = pixel[0] / 255.0; b = pixel[1] / 255.0; g = pixel[2] / 255.0; r = pixel[3] / 255.0; } } break; case kCGImageAlphaNone: { if (byteOrderNormal) { // RGB r = pixel[0] / 255.0; g = pixel[1] / 255.0; b = pixel[2] / 255.0; } else { // BGR b = pixel[0] / 255.0; g = pixel[1] / 255.0; r = pixel[2] / 255.0; } } break; case kCGImageAlphaNoneSkipLast: { if (byteOrderNormal) { // RGBX r = pixel[0] / 255.0; g = pixel[1] / 255.0; b = pixel[2] / 255.0; } else { // XBGR b = pixel[1] / 255.0; g = pixel[2] / 255.0; r = pixel[3] / 255.0; } } break; case kCGImageAlphaNoneSkipFirst: { if (byteOrderNormal) { // XRGB r = pixel[1] / 255.0; g = pixel[2] / 255.0; b = pixel[3] / 255.0; } else { // BGRX b = pixel[0] / 255.0; g = pixel[1] / 255.0; r = pixel[2] / 255.0; } } break; case kCGImageAlphaOnly: { // A a = pixel[0] / 255.0; } break; default: break; } #if SD_MAC // Mac supports ColorSync, to ensure the same bahvior, we convert color to sRGB NSColorSpace *colorSpace = [[NSColorSpace alloc] initWithCGColorSpace:cgColorSpace]; CGFloat components[4] = {r, g, b, a}; NSColor *color = [NSColor colorWithColorSpace:colorSpace components:components count:4]; return [color colorUsingColorSpace:NSColorSpace.sRGBColorSpace]; #else return [UIColor colorWithRed:r green:g blue:b alpha:a]; #endif } #if SD_UIKIT || SD_MAC // Create-Rule, caller should call CGImageRelease static inline CGImageRef _Nullable SDCreateCGImageFromCIImage(CIImage * _Nonnull ciImage) { CGImageRef imageRef = NULL; if (@available(iOS 10, macOS 10.12, tvOS 10, *)) { imageRef = ciImage.CGImage; } if (!imageRef) { CIContext *context = [CIContext context]; imageRef = [context createCGImage:ciImage fromRect:ciImage.extent]; } else { CGImageRetain(imageRef); } return imageRef; } #endif @implementation UIImage (Transform) - (void)sd_drawInRect:(CGRect)rect context:(CGContextRef)context scaleMode:(SDImageScaleMode)scaleMode clipsToBounds:(BOOL)clips { CGRect drawRect = SDCGRectFitWithScaleMode(rect, self.size, scaleMode); if (drawRect.size.width == 0 || drawRect.size.height == 0) return; if (clips) { if (context) { CGContextSaveGState(context); CGContextAddRect(context, rect); CGContextClip(context); [self drawInRect:drawRect]; CGContextRestoreGState(context); } } else { [self drawInRect:drawRect]; } } - (nullable UIImage *)sd_resizedImageWithSize:(CGSize)size scaleMode:(SDImageScaleMode)scaleMode { if (size.width <= 0 || size.height <= 0) return nil; SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = self.scale; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { [self sd_drawInRect:CGRectMake(0, 0, size.width, size.height) context:context scaleMode:scaleMode clipsToBounds:NO]; }]; return image; } - (nullable UIImage *)sd_croppedImageWithRect:(CGRect)rect { rect.origin.x *= self.scale; rect.origin.y *= self.scale; rect.size.width *= self.scale; rect.size.height *= self.scale; if (rect.size.width <= 0 || rect.size.height <= 0) return nil; #if SD_UIKIT || SD_MAC // CIImage shortcut if (self.CIImage) { CGRect croppingRect = CGRectMake(rect.origin.x, self.size.height - CGRectGetMaxY(rect), rect.size.width, rect.size.height); CIImage *ciImage = [self.CIImage imageByCroppingToRect:croppingRect]; #if SD_UIKIT UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif return image; } #endif CGImageRef imageRef = self.CGImage; if (!imageRef) { return nil; } CGImageRef croppedImageRef = CGImageCreateWithImageInRect(imageRef, rect); if (!croppedImageRef) { return nil; } #if SD_UIKIT || SD_WATCH UIImage *image = [UIImage imageWithCGImage:croppedImageRef scale:self.scale orientation:self.imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCGImage:croppedImageRef scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif CGImageRelease(croppedImageRef); return image; } - (nullable UIImage *)sd_roundedCornerImageWithRadius:(CGFloat)cornerRadius corners:(SDRectCorner)corners borderWidth:(CGFloat)borderWidth borderColor:(nullable UIColor *)borderColor { SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = self.scale; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:self.size format:format]; UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height); CGFloat minSize = MIN(self.size.width, self.size.height); if (borderWidth < minSize / 2) { #if SD_UIKIT || SD_WATCH UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(cornerRadius, cornerRadius)]; #else NSBezierPath *path = [NSBezierPath sd_bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadius:cornerRadius]; #endif [path closePath]; CGContextSaveGState(context); [path addClip]; [self drawInRect:rect]; CGContextRestoreGState(context); } if (borderColor && borderWidth < minSize / 2 && borderWidth > 0) { CGFloat strokeInset = (floor(borderWidth * self.scale) + 0.5) / self.scale; CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset); CGFloat strokeRadius = cornerRadius > self.scale / 2 ? cornerRadius - self.scale / 2 : 0; #if SD_UIKIT || SD_WATCH UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadii:CGSizeMake(strokeRadius, strokeRadius)]; #else NSBezierPath *path = [NSBezierPath sd_bezierPathWithRoundedRect:strokeRect byRoundingCorners:corners cornerRadius:strokeRadius]; #endif [path closePath]; path.lineWidth = borderWidth; [borderColor setStroke]; [path stroke]; } }]; return image; } - (nullable UIImage *)sd_rotatedImageWithAngle:(CGFloat)angle fitSize:(BOOL)fitSize { size_t width = self.size.width; size_t height = self.size.height; CGRect newRect = CGRectApplyAffineTransform(CGRectMake(0, 0, width, height), fitSize ? CGAffineTransformMakeRotation(angle) : CGAffineTransformIdentity); #if SD_UIKIT || SD_MAC // CIImage shortcut if (self.CIImage) { CIImage *ciImage = self.CIImage; if (fitSize) { CGAffineTransform transform = CGAffineTransformMakeRotation(angle); ciImage = [ciImage imageByApplyingTransform:transform]; } else { CIFilter *filter = [CIFilter filterWithName:@"CIStraightenFilter"]; [filter setValue:ciImage forKey:kCIInputImageKey]; [filter setValue:@(angle) forKey:kCIInputAngleKey]; ciImage = filter.outputImage; } #if SD_UIKIT || SD_WATCH UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif return image; } #endif SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = self.scale; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:newRect.size format:format]; UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { CGContextSetShouldAntialias(context, true); CGContextSetAllowsAntialiasing(context, true); CGContextSetInterpolationQuality(context, kCGInterpolationHigh); CGContextTranslateCTM(context, +(newRect.size.width * 0.5), +(newRect.size.height * 0.5)); #if SD_UIKIT || SD_WATCH // Use UIKit coordinate system counterclockwise (⟲) CGContextRotateCTM(context, -angle); #else CGContextRotateCTM(context, angle); #endif [self drawInRect:CGRectMake(-(width * 0.5), -(height * 0.5), width, height)]; }]; return image; } - (nullable UIImage *)sd_flippedImageWithHorizontal:(BOOL)horizontal vertical:(BOOL)vertical { size_t width = self.size.width; size_t height = self.size.height; #if SD_UIKIT || SD_MAC // CIImage shortcut if (self.CIImage) { CGAffineTransform transform = CGAffineTransformIdentity; // Use UIKit coordinate system if (horizontal) { CGAffineTransform flipHorizontal = CGAffineTransformMake(-1, 0, 0, 1, width, 0); transform = CGAffineTransformConcat(transform, flipHorizontal); } if (vertical) { CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, height); transform = CGAffineTransformConcat(transform, flipVertical); } CIImage *ciImage = [self.CIImage imageByApplyingTransform:transform]; #if SD_UIKIT UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif return image; } #endif SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = self.scale; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:self.size format:format]; UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { // Use UIKit coordinate system if (horizontal) { CGAffineTransform flipHorizontal = CGAffineTransformMake(-1, 0, 0, 1, width, 0); CGContextConcatCTM(context, flipHorizontal); } if (vertical) { CGAffineTransform flipVertical = CGAffineTransformMake(1, 0, 0, -1, 0, height); CGContextConcatCTM(context, flipVertical); } [self drawInRect:CGRectMake(0, 0, width, height)]; }]; return image; } #pragma mark - Image Blending #if SD_UIKIT || SD_MAC static NSString * _Nullable SDGetCIFilterNameFromBlendMode(CGBlendMode blendMode) { // CGBlendMode: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Conceptual/drawingwithquartz2d/dq_images/dq_images.html#//apple_ref/doc/uid/TP30001066-CH212-CJBIJEFG // CIFilter: https://developer.apple.com/library/archive/documentation/GraphicsImaging/Reference/CoreImageFilterReference/index.html#//apple_ref/doc/uid/TP30000136-SW71 NSString *filterName; switch (blendMode) { case kCGBlendModeMultiply: filterName = @"CIMultiplyBlendMode"; break; case kCGBlendModeScreen: filterName = @"CIScreenBlendMode"; break; case kCGBlendModeOverlay: filterName = @"CIOverlayBlendMode"; break; case kCGBlendModeDarken: filterName = @"CIDarkenBlendMode"; break; case kCGBlendModeLighten: filterName = @"CILightenBlendMode"; break; case kCGBlendModeColorDodge: filterName = @"CIColorDodgeBlendMode"; break; case kCGBlendModeColorBurn: filterName = @"CIColorBurnBlendMode"; break; case kCGBlendModeSoftLight: filterName = @"CISoftLightBlendMode"; break; case kCGBlendModeHardLight: filterName = @"CIHardLightBlendMode"; break; case kCGBlendModeDifference: filterName = @"CIDifferenceBlendMode"; break; case kCGBlendModeExclusion: filterName = @"CIExclusionBlendMode"; break; case kCGBlendModeHue: filterName = @"CIHueBlendMode"; break; case kCGBlendModeSaturation: filterName = @"CISaturationBlendMode"; break; case kCGBlendModeColor: // Color blend mode uses the luminance values of the background with the hue and saturation values of the source image. filterName = @"CIColorBlendMode"; break; case kCGBlendModeLuminosity: filterName = @"CILuminosityBlendMode"; break; // macOS 10.5+ case kCGBlendModeSourceAtop: case kCGBlendModeDestinationAtop: filterName = @"CISourceAtopCompositing"; break; case kCGBlendModeSourceIn: case kCGBlendModeDestinationIn: filterName = @"CISourceInCompositing"; break; case kCGBlendModeSourceOut: case kCGBlendModeDestinationOut: filterName = @"CISourceOutCompositing"; break; case kCGBlendModeNormal: // SourceOver case kCGBlendModeDestinationOver: filterName = @"CISourceOverCompositing"; break; // need special handling case kCGBlendModeClear: // use clear color instead break; case kCGBlendModeCopy: // use input color instead break; case kCGBlendModeXOR: // unsupported break; case kCGBlendModePlusDarker: // chain filters break; case kCGBlendModePlusLighter: // chain filters break; } return filterName; } #endif - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor { return [self sd_tintedImageWithColor:tintColor blendMode:kCGBlendModeSourceIn]; } - (nullable UIImage *)sd_tintedImageWithColor:(nonnull UIColor *)tintColor blendMode:(CGBlendMode)blendMode { BOOL hasTint = CGColorGetAlpha(tintColor.CGColor) > __FLT_EPSILON__; if (!hasTint) { return self; } // blend mode, see https://en.wikipedia.org/wiki/Alpha_compositing #if SD_UIKIT || SD_MAC // CIImage shortcut CIImage *ciImage = self.CIImage; if (ciImage) { CIImage *colorImage = [CIImage imageWithColor:[[CIColor alloc] initWithColor:tintColor]]; colorImage = [colorImage imageByCroppingToRect:ciImage.extent]; NSString *filterName = SDGetCIFilterNameFromBlendMode(blendMode); // Some blend mode is not nativelly supported if (filterName) { CIFilter *filter = [CIFilter filterWithName:filterName]; [filter setValue:colorImage forKey:kCIInputImageKey]; [filter setValue:ciImage forKey:kCIInputBackgroundImageKey]; ciImage = filter.outputImage; } else { if (blendMode == kCGBlendModeClear) { // R = 0 CIColor *clearColor; if (@available(iOS 10.0, macOS 10.12, tvOS 10.0, *)) { clearColor = CIColor.clearColor; } else { clearColor = [[CIColor alloc] initWithColor:UIColor.clearColor]; } colorImage = [CIImage imageWithColor:clearColor]; colorImage = [colorImage imageByCroppingToRect:ciImage.extent]; ciImage = colorImage; } else if (blendMode == kCGBlendModeCopy) { // R = S ciImage = colorImage; } else if (blendMode == kCGBlendModePlusLighter) { // R = MIN(1, S + D) // S + D CIFilter *filter = [CIFilter filterWithName:@"CIAdditionCompositing"]; [filter setValue:colorImage forKey:kCIInputImageKey]; [filter setValue:ciImage forKey:kCIInputBackgroundImageKey]; ciImage = filter.outputImage; // MIN ciImage = [ciImage imageByApplyingFilter:@"CIColorClamp" withInputParameters:nil]; } else if (blendMode == kCGBlendModePlusDarker) { // R = MAX(0, (1 - D) + (1 - S)) // (1 - D) CIFilter *filter1 = [CIFilter filterWithName:@"CIColorControls"]; [filter1 setValue:ciImage forKey:kCIInputImageKey]; [filter1 setValue:@(-0.5) forKey:kCIInputBrightnessKey]; ciImage = filter1.outputImage; // (1 - S) CIFilter *filter2 = [CIFilter filterWithName:@"CIColorControls"]; [filter2 setValue:colorImage forKey:kCIInputImageKey]; [filter2 setValue:@(-0.5) forKey:kCIInputBrightnessKey]; colorImage = filter2.outputImage; // + CIFilter *filter = [CIFilter filterWithName:@"CIAdditionCompositing"]; [filter setValue:colorImage forKey:kCIInputImageKey]; [filter setValue:ciImage forKey:kCIInputBackgroundImageKey]; ciImage = filter.outputImage; // MAX ciImage = [ciImage imageByApplyingFilter:@"CIColorClamp" withInputParameters:nil]; } else { SD_LOG("UIImage+Transform error: Unsupported blend mode: %d", blendMode); ciImage = nil; } } if (ciImage) { #if SD_UIKIT UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif return image; } } #endif CGSize size = self.size; CGRect rect = { CGPointZero, size }; CGFloat scale = self.scale; SDGraphicsImageRendererFormat *format = [[SDGraphicsImageRendererFormat alloc] init]; format.scale = scale; SDGraphicsImageRenderer *renderer = [[SDGraphicsImageRenderer alloc] initWithSize:size format:format]; UIImage *image = [renderer imageWithActions:^(CGContextRef _Nonnull context) { [self drawInRect:rect]; CGContextSetBlendMode(context, blendMode); CGContextSetFillColorWithColor(context, tintColor.CGColor); CGContextFillRect(context, rect); }]; return image; } - (nullable UIColor *)sd_colorAtPoint:(CGPoint)point { CGImageRef imageRef = NULL; // CIImage compatible #if SD_UIKIT || SD_MAC if (self.CIImage) { imageRef = SDCreateCGImageFromCIImage(self.CIImage); } #endif if (!imageRef) { imageRef = self.CGImage; CGImageRetain(imageRef); } if (!imageRef) { return nil; } // Check point size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); size_t x = point.x; size_t y = point.y; if (x < 0 || y < 0 || x >= width || y >= height) { CGImageRelease(imageRef); return nil; } // Check pixel format CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef); if (@available(iOS 12.0, tvOS 12.0, macOS 10.14, watchOS 5.0, *)) { CGImagePixelFormatInfo pixelFormat = (bitmapInfo & kCGImagePixelFormatMask); if (pixelFormat != kCGImagePixelFormatPacked || bitsPerComponent > 8) { // like RGBA1010102, need bitwise to extract pixel from single uint32_t, we don't support currently SD_LOG("Unsupported pixel format: %u, bpc: %zu for CGImage: %@", pixelFormat, bitsPerComponent, imageRef); CGImageRelease(imageRef); return nil; } } // Get pixels CGDataProviderRef provider = CGImageGetDataProvider(imageRef); if (!provider) { CGImageRelease(imageRef); return nil; } CFDataRef data = CGDataProviderCopyData(provider); if (!data) { CGImageRelease(imageRef); return nil; } // Get pixel at point size_t bytesPerRow = CGImageGetBytesPerRow(imageRef); size_t components = CGImageGetBitsPerPixel(imageRef) / bitsPerComponent; CFRange range = CFRangeMake(bytesPerRow * y + components * x, components); if (CFDataGetLength(data) < range.location + range.length) { CFRelease(data); CGImageRelease(imageRef); return nil; } // Get color space for transform CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef); // greyscale if (components == 2) { Pixel_88 pixel = {0}; CFDataGetBytes(data, range, pixel); CFRelease(data); CGImageRelease(imageRef); // Convert to color return SDGetColorFromGrayscale(pixel, bitmapInfo, colorSpace); } else if (components == 3 || components == 4) { // RGB/RGBA Pixel_8888 pixel = {0}; CFDataGetBytes(data, range, pixel); CFRelease(data); CGImageRelease(imageRef); // Convert to color return SDGetColorFromRGBA8(pixel, bitmapInfo, colorSpace); } else { SD_LOG("Unsupported components: %zu for CGImage: %@", components, imageRef); CFRelease(data); CGImageRelease(imageRef); return nil; } } - (nullable NSArray *)sd_colorsWithRect:(CGRect)rect { CGImageRef imageRef = NULL; // CIImage compatible #if SD_UIKIT || SD_MAC if (self.CIImage) { imageRef = SDCreateCGImageFromCIImage(self.CIImage); } #endif if (!imageRef) { imageRef = self.CGImage; CGImageRetain(imageRef); } if (!imageRef) { return nil; } // Check rect size_t width = CGImageGetWidth(imageRef); size_t height = CGImageGetHeight(imageRef); if (CGRectGetWidth(rect) <= 0 || CGRectGetHeight(rect) <= 0 || CGRectGetMinX(rect) < 0 || CGRectGetMinY(rect) < 0 || CGRectGetMaxX(rect) > width || CGRectGetMaxY(rect) > height) { CGImageRelease(imageRef); return nil; } // Check pixel format CGBitmapInfo bitmapInfo = CGImageGetBitmapInfo(imageRef); size_t bitsPerComponent = CGImageGetBitsPerComponent(imageRef); if (@available(iOS 12.0, tvOS 12.0, macOS 10.14, watchOS 5.0, *)) { CGImagePixelFormatInfo pixelFormat = (bitmapInfo & kCGImagePixelFormatMask); if (pixelFormat != kCGImagePixelFormatPacked || bitsPerComponent > 8) { // like RGBA1010102, need bitwise to extract pixel from single uint32_t, we don't support currently SD_LOG("Unsupported pixel format: %u, bpc: %zu for CGImage: %@", pixelFormat, bitsPerComponent, imageRef); CGImageRelease(imageRef); return nil; } } // Get pixels CGDataProviderRef provider = CGImageGetDataProvider(imageRef); if (!provider) { CGImageRelease(imageRef); return nil; } CFDataRef data = CGDataProviderCopyData(provider); if (!data) { CGImageRelease(imageRef); return nil; } // Get pixels with rect size_t bytesPerRow = CGImageGetBytesPerRow(imageRef); size_t components = CGImageGetBitsPerPixel(imageRef) / bitsPerComponent; size_t start = bytesPerRow * CGRectGetMinY(rect) + components * CGRectGetMinX(rect); size_t end = bytesPerRow * (CGRectGetMaxY(rect) - 1) + components * CGRectGetMaxX(rect); if (CFDataGetLength(data) < (CFIndex)end) { CFRelease(data); CGImageRelease(imageRef); return nil; } const UInt8 *pixels = CFDataGetBytePtr(data); size_t row = CGRectGetMinY(rect); size_t col = CGRectGetMaxX(rect); // Convert to color NSMutableArray *colors = [NSMutableArray arrayWithCapacity:CGRectGetWidth(rect) * CGRectGetHeight(rect)]; // ColorSpace CGColorSpaceRef colorSpace = CGImageGetColorSpace(imageRef); for (size_t index = start; index < end; index += components) { if (index >= row * bytesPerRow + col * components) { // Index beyond the end of current row, go next row row++; index = row * bytesPerRow + CGRectGetMinX(rect) * components; index -= components; continue; } UIColor *color; if (components == 2) { Pixel_88 pixel = {pixels[index], pixel[index+1]}; color = SDGetColorFromGrayscale(pixel, bitmapInfo, colorSpace); } else { if (components == 3) { Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], 0}; color = SDGetColorFromRGBA8(pixel, bitmapInfo, colorSpace); } else if (components == 4) { Pixel_8888 pixel = {pixels[index], pixels[index+1], pixels[index+2], pixels[index+3]}; color = SDGetColorFromRGBA8(pixel, bitmapInfo, colorSpace); } else { SD_LOG("Unsupported components: %zu for CGImage: %@", components, imageRef); break; } } if (color) { [colors addObject:color]; } } CFRelease(data); CGImageRelease(imageRef); return [colors copy]; } #pragma mark - Image Effect // We use vImage to do box convolve for performance and support for watchOS. However, you can just use `CIFilter.CIGaussianBlur`. For other blur effect, use any filter in `CICategoryBlur` - (nullable UIImage *)sd_blurredImageWithRadius:(CGFloat)blurRadius { if (self.size.width < 1 || self.size.height < 1) { return nil; } BOOL hasBlur = blurRadius > __FLT_EPSILON__; if (!hasBlur) { return self; } CGFloat scale = self.scale; CGFloat inputRadius = blurRadius * scale; #if SD_UIKIT || SD_MAC if (self.CIImage) { CIFilter *filter = [CIFilter filterWithName:@"CIGaussianBlur"]; [filter setValue:self.CIImage forKey:kCIInputImageKey]; [filter setValue:@(inputRadius) forKey:kCIInputRadiusKey]; CIImage *ciImage = filter.outputImage; ciImage = [ciImage imageByCroppingToRect:CGRectMake(0, 0, self.size.width, self.size.height)]; #if SD_UIKIT UIImage *image = [UIImage imageWithCIImage:ciImage scale:self.scale orientation:self.imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCIImage:ciImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif return image; } #endif CGImageRef imageRef = self.CGImage; if (!imageRef) { return nil; } vImage_Buffer effect = {}, scratch = {}; vImage_Buffer *input = NULL, *output = NULL; vImage_CGImageFormat format = { .bitsPerComponent = 8, .bitsPerPixel = 32, .colorSpace = NULL, .bitmapInfo = kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Host, //requests a BGRA buffer. .version = 0, .decode = NULL, .renderingIntent = CGImageGetRenderingIntent(imageRef) }; vImage_Error err; err = vImageBuffer_InitWithCGImage(&effect, &format, NULL, imageRef, kvImageNoFlags); // vImage will convert to format we requests, no need `vImageConvert` if (err != kvImageNoError) { SD_LOG("UIImage+Transform error: vImageBuffer_InitWithCGImage returned error code %zi for inputImage: %@", err, self); return nil; } err = vImageBuffer_Init(&scratch, effect.height, effect.width, format.bitsPerPixel, kvImageNoFlags); if (err != kvImageNoError) { SD_LOG("UIImage+Transform error: vImageBuffer_Init returned error code %zi for inputImage: %@", err, self); return nil; } input = &effect; output = &scratch; // See: https://developer.apple.com/library/archive/samplecode/UIImageEffects/Introduction/Intro.html if (hasBlur) { // A description of how to compute the box kernel width from the Gaussian // radius (aka standard deviation) appears in the SVG spec: // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement // // For larger values of 's' (s >= 2.0), an approximation can be used: Three // successive box-blurs build a piece-wise quadratic convolution kernel, which // approximates the Gaussian kernel to within roughly 3%. // // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) // // ... if d is odd, use three box-blurs of size 'd', centered on the output pixel. // if (inputRadius - 2.0 < __FLT_EPSILON__) inputRadius = 2.0; uint32_t radius = floor(inputRadius * 3.0 * sqrt(2 * M_PI) / 4 + 0.5); radius |= 1; // force radius to be odd so that the three box-blur methodology works. NSInteger tempSize = vImageBoxConvolve_ARGB8888(input, output, NULL, 0, 0, radius, radius, NULL, kvImageGetTempBufferSize | kvImageEdgeExtend); void *temp = malloc(tempSize); vImageBoxConvolve_ARGB8888(input, output, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend); vImageBoxConvolve_ARGB8888(output, input, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend); vImageBoxConvolve_ARGB8888(input, output, temp, 0, 0, radius, radius, NULL, kvImageEdgeExtend); free(temp); vImage_Buffer *tmp = input; input = output; output = tmp; } CGImageRef effectCGImage = NULL; effectCGImage = vImageCreateCGImageFromBuffer(input, &format, NULL, NULL, kvImageNoAllocate, NULL); if (effectCGImage == NULL) { effectCGImage = vImageCreateCGImageFromBuffer(input, &format, NULL, NULL, kvImageNoFlags, NULL); free(input->data); } free(output->data); #if SD_UIKIT || SD_WATCH UIImage *outputImage = [UIImage imageWithCGImage:effectCGImage scale:self.scale orientation:self.imageOrientation]; #else UIImage *outputImage = [[UIImage alloc] initWithCGImage:effectCGImage scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif CGImageRelease(effectCGImage); return outputImage; } #if SD_UIKIT || SD_MAC - (nullable UIImage *)sd_filteredImageWithFilter:(nonnull CIFilter *)filter { CIImage *inputImage; if (self.CIImage) { inputImage = self.CIImage; } else { CGImageRef imageRef = self.CGImage; if (!imageRef) { return nil; } inputImage = [CIImage imageWithCGImage:imageRef]; } if (!inputImage) return nil; CIContext *context = [CIContext context]; [filter setValue:inputImage forKey:kCIInputImageKey]; CIImage *outputImage = filter.outputImage; if (!outputImage) return nil; CGImageRef imageRef = [context createCGImage:outputImage fromRect:outputImage.extent]; if (!imageRef) return nil; #if SD_UIKIT UIImage *image = [UIImage imageWithCGImage:imageRef scale:self.scale orientation:self.imageOrientation]; #else UIImage *image = [[UIImage alloc] initWithCGImage:imageRef scale:self.scale orientation:kCGImagePropertyOrientationUp]; #endif CGImageRelease(imageRef); return image; } #endif @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImageView+HighlightedWebCache.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_UIKIT #import "SDWebImageManager.h" /** * Integrates SDWebImage async downloading and caching of remote images with UIImageView for highlighted state. */ @interface UIImageView (HighlightedWebCache) #pragma mark - Highlighted Image /** * Get the current highlighted image URL. */ @property (nonatomic, strong, readonly, nullable) NSURL *sd_currentHighlightedImageURL; /** * Set the imageView `highlightedImage` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. */ - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT; /** * Set the imageView `highlightedImage` with an `url` and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. */ - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT; /** * Set the imageView `highlightedImage` with an `url`, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. */ - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; /** * Set the imageView `highlightedImage` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT; /** * Set the imageView `highlightedImage` with an `url` and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the imageView `highlightedImage` with an `url` and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the imageView `highlightedImage` with an `url`, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Cancel the current highlighted image load (for `UIImageView.highlighted`) * @note For normal image, use `sd_cancelCurrentImageLoad` */ - (void)sd_cancelCurrentHighlightedImageLoad; @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImageView+HighlightedWebCache.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIImageView+HighlightedWebCache.h" #if SD_UIKIT #import "UIView+WebCacheOperation.h" #import "UIView+WebCacheState.h" #import "UIView+WebCache.h" #import "SDInternalMacros.h" @implementation UIImageView (HighlightedWebCache) - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url { [self sd_setHighlightedImageWithURL:url options:0 progress:nil completed:nil]; } - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options { [self sd_setHighlightedImageWithURL:url options:options progress:nil completed:nil]; } - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { [self sd_setHighlightedImageWithURL:url options:options context:context progress:nil completed:nil]; } - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setHighlightedImageWithURL:url options:0 progress:nil completed:completedBlock]; } - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setHighlightedImageWithURL:url options:options progress:nil completed:completedBlock]; } - (void)sd_setHighlightedImageWithURL:(NSURL *)url options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setHighlightedImageWithURL:url options:options context:nil progress:progressBlock completed:completedBlock]; } - (void)sd_setHighlightedImageWithURL:(nullable NSURL *)url options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { @weakify(self); SDWebImageMutableContext *mutableContext; if (context) { mutableContext = [context mutableCopy]; } else { mutableContext = [NSMutableDictionary dictionary]; } mutableContext[SDWebImageContextSetImageOperationKey] = @keypath(self, highlightedImage); [self sd_internalSetImageWithURL:url placeholderImage:nil options:options context:mutableContext setImageBlock:^(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL) { @strongify(self); self.highlightedImage = image; } progress:progressBlock completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { if (completedBlock) { completedBlock(image, error, cacheType, imageURL); } }]; } #pragma mark - Highlighted State - (NSURL *)sd_currentHighlightedImageURL { return [self sd_imageLoadStateForKey:@keypath(self, highlightedImage)].url; } - (void)sd_cancelCurrentHighlightedImageLoad { return [self sd_cancelImageLoadOperationWithKey:@keypath(self, highlightedImage)]; } @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImageView+WebCache.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import "SDWebImageManager.h" /** * Usage with a UITableViewCell sub-class: * * @code #import ... - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *MyIdentifier = @"MyIdentifier"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:MyIdentifier]; if (cell == nil) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:MyIdentifier]; } // Here we use the provided sd_setImageWithURL:placeholderImage: method to load the web image // Ensure you use a placeholder image otherwise cells will be initialized with no image [cell.imageView sd_setImageWithURL:[NSURL URLWithString:@"http://example.com/image.jpg"] placeholderImage:[UIImage imageNamed:@"placeholder"]]; cell.textLabel.text = @"My Text"; return cell; } * @endcode */ /** * Integrates SDWebImage async downloading and caching of remote images with UIImageView. */ @interface UIImageView (WebCache) #pragma mark - Image State /** * Get the current image URL. */ @property (nonatomic, strong, readonly, nullable) NSURL *sd_currentImageURL; #pragma mark - Image Loading /** * Set the imageView `image` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. */ - (void)sd_setImageWithURL:(nullable NSURL *)url NS_REFINED_FOR_SWIFT; /** * Set the imageView `image` with an `url` and a placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @see sd_setImageWithURL:placeholderImage:options: */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder NS_REFINED_FOR_SWIFT; /** * Set the imageView `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options NS_REFINED_FOR_SWIFT; /** * Set the imageView `image` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context; /** * Set the imageView `image` with an `url`. * * The download is asynchronous and cached. * * @param url The url for the image. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the imageView `image` with an `url`, placeholder. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock NS_REFINED_FOR_SWIFT; /** * Set the imageView `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the imageView `image` with an `url`, placeholder and custom options. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Set the imageView `image` with an `url`, placeholder, custom options and context. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. This block has no return value * and takes the requested UIImage as first parameter. In case of error the image parameter * is nil and the second parameter may contain an NSError. The third parameter is a Boolean * indicating if the image was retrieved from the local cache or from the network. * The fourth parameter is the original image url. */ - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock; /** * Cancel the current normal image load (for `UIImageView.image`) * @note For highlighted image, use `sd_cancelCurrentHighlightedImageLoad` */ - (void)sd_cancelCurrentImageLoad; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIImageView+WebCache.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIImageView+WebCache.h" #import "objc/runtime.h" #import "UIView+WebCacheOperation.h" #import "UIView+WebCacheState.h" #import "UIView+WebCache.h" @implementation UIImageView (WebCache) - (void)sd_setImageWithURL:(nullable NSURL *)url { [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options { [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context { [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:context progress:nil completed:nil]; } - (void)sd_setImageWithURL:(nullable NSURL *)url completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:nil options:0 progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:0 progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:options progress:nil completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_setImageWithURL:url placeholderImage:placeholder options:options context:nil progress:progressBlock completed:completedBlock]; } - (void)sd_setImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDExternalCompletionBlock)completedBlock { [self sd_internalSetImageWithURL:url placeholderImage:placeholder options:options context:context setImageBlock:nil progress:progressBlock completed:^(UIImage * _Nullable image, NSData * _Nullable data, NSError * _Nullable error, SDImageCacheType cacheType, BOOL finished, NSURL * _Nullable imageURL) { if (completedBlock) { completedBlock(image, error, cacheType, imageURL); } }]; } #pragma mark - State - (NSURL *)sd_currentImageURL { return [self sd_imageLoadStateForKey:nil].url; } - (void)sd_cancelCurrentImageLoad { return [self sd_cancelImageLoadOperationWithKey:nil]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import "SDWebImageDefine.h" #import "SDWebImageManager.h" #import "SDWebImageTransition.h" #import "SDWebImageIndicator.h" #import "UIView+WebCacheOperation.h" #import "UIView+WebCacheState.h" /** The value specify that the image progress unit count cannot be determined because the progressBlock is not been called. */ FOUNDATION_EXPORT const int64_t SDWebImageProgressUnitCountUnknown; /* 1LL */ typedef void(^SDSetImageBlock)(UIImage * _Nullable image, NSData * _Nullable imageData, SDImageCacheType cacheType, NSURL * _Nullable imageURL); /** Integrates SDWebImage async downloading and caching of remote images with UIView subclass. */ @interface UIView (WebCache) /** * Get the current image operation key. Operation key is used to identify the different queries for one view instance (like UIButton). * See more about this in `SDWebImageContextSetImageOperationKey`. * * @note You can use method `UIView+WebCacheOperation` to investigate different queries' operation. * @note For the history version compatible, when current UIView has property exactly called `image`, the operation key will use `NSStringFromClass(self.class)`. Include `UIImageView.image/NSImageView.image/NSButton.image` (without `UIButton`) * @warning This property should be only used for single state view, like `UIImageView` without highlighted state. For stateful view like `UIBUtton` (one view can have multiple images loading), check their header to call correct API, like `-[UIButton sd_imageOperationKeyForState:]` */ @property (nonatomic, strong, readonly, nullable) NSString *sd_latestOperationKey; #pragma mark - State /** * Get the current image URL. * This simply translate to `[self sd_imageLoadStateForKey:self.sd_latestOperationKey].url` from v5.18.0 * * @note Note that because of the limitations of categories this property can get out of sync if you use setImage: directly. * @warning This property should be only used for single state view, like `UIImageView` without highlighted state. For stateful view like `UIBUtton` (one view can have multiple images loading), use `sd_imageLoadStateForKey:` instead. See `UIView+WebCacheState.h` for more information. */ @property (nonatomic, strong, readonly, nullable) NSURL *sd_imageURL; /** * The current image loading progress associated to the view. The unit count is the received size and excepted size of download. * The `totalUnitCount` and `completedUnitCount` will be reset to 0 after a new image loading start (change from current queue). And they will be set to `SDWebImageProgressUnitCountUnknown` if the progressBlock not been called but the image loading success to mark the progress finished (change from main queue). * @note You can use Key-Value Observing on the progress, but you should take care that the change to progress is from a background queue during download(the same as progressBlock). If you want to using KVO and update the UI, make sure to dispatch on the main queue. And it's recommend to use some KVO libs like KVOController because it's more safe and easy to use. * @note The getter will create a progress instance if the value is nil. But by default, we don't create one. If you need to use Key-Value Observing, you must trigger the getter or set a custom progress instance before the loading start. The default value is nil. * @note Note that because of the limitations of categories this property can get out of sync if you update the progress directly. * @warning This property should be only used for single state view, like `UIImageView` without highlighted state. For stateful view like `UIBUtton` (one view can have multiple images loading), use `sd_imageLoadStateForKey:` instead. See `UIView+WebCacheState.h` for more information. */ @property (nonatomic, strong, null_resettable) NSProgress *sd_imageProgress; /** * Set the imageView `image` with an `url` and optionally a placeholder image. * * The download is asynchronous and cached. * * @param url The url for the image. * @param placeholder The image to be set initially, until the image request finishes. * @param options The options to use when downloading the image. @see SDWebImageOptions for the possible values. * @param context A context contains different options to perform specify changes or processes, see `SDWebImageContextOption`. This hold the extra objects which `options` enum can not hold. * @param setImageBlock Block used for custom set image code. If not provide, use the built-in set image code (supports `UIImageView/NSImageView` and `UIButton/NSButton` currently) * @param progressBlock A block called while image is downloading * @note the progress block is executed on a background queue * @param completedBlock A block called when operation has been completed. * This block has no return value and takes the requested UIImage as first parameter and the NSData representation as second parameter. * In case of error the image parameter is nil and the third parameter may contain an NSError. * * The forth parameter is an `SDImageCacheType` enum indicating if the image was retrieved from the local cache * or from the memory cache or from the network. * * The fifth parameter normally is always YES. However, if you provide SDWebImageAvoidAutoSetImage with SDWebImageProgressiveLoad options to enable progressive downloading and set the image yourself. This block is thus called repeatedly with a partial image. When image is fully downloaded, the * block is called a last time with the full image and the last parameter set to YES. * * The last parameter is the original image URL * @return The returned operation for cancelling cache and download operation, typically type is `SDWebImageCombinedOperation` */ - (nullable id)sd_internalSetImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock; /** * Cancel the latest image load, using the `sd_latestOperationKey` as operation key * This simply translate to `[self sd_cancelImageLoadOperationWithKey:self.sd_latestOperationKey]` */ - (void)sd_cancelLatestImageLoad; /** * Cancel the current image load, for single state view. * This actually does not cancel current loading, because stateful view can load multiple images at the same time (like UIButton, each state can load different images). Just behave the same as `sd_cancelLatestImageLoad` * * @warning This method should be only used for single state view, like `UIImageView` without highlighted state. For stateful view like `UIBUtton` (one view can have multiple images loading), use `sd_cancelImageLoadOperationWithKey:` instead. See `UIView+WebCacheOperation.h` for more information. * @deprecated Use `sd_cancelLatestImageLoad` instead. Which don't cause overload method misunderstanding (`UIImageView+WebCache` provide the same API as this one, but does not do the same thing). This API will be totally removed in v6.0 due to this. */ - (void)sd_cancelCurrentImageLoad API_DEPRECATED_WITH_REPLACEMENT("sd_cancelLatestImageLoad", macos(10.10, 10.10), ios(8.0, 8.0), tvos(9.0, 9.0), watchos(2.0, 2.0)); #if SD_UIKIT || SD_MAC #pragma mark - Image Transition /** The image transition when image load finished. See `SDWebImageTransition`. If you specify nil, do not do transition. Defaults to nil. @warning This property should be only used for single state view, like `UIImageView` without highlighted state. For stateful view like `UIBUtton` (one view can have multiple images loading), write your own implementation in `setImageBlock:`, and check current stateful view's state to render the UI. */ @property (nonatomic, strong, nullable) SDWebImageTransition *sd_imageTransition; #pragma mark - Image Indicator /** The image indicator during the image loading. If you do not need indicator, specify nil. Defaults to nil The setter will remove the old indicator view and add new indicator view to current view's subview. @note Because this is UI related, you should access only from the main queue. @warning This property should be only used for single state view, like `UIImageView` without highlighted state. For stateful view like `UIBUtton` (one view can have multiple images loading), write your own implementation in `setImageBlock:`, and check current stateful view's state to render the UI. */ @property (nonatomic, strong, nullable) id sd_imageIndicator; #endif @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIView+WebCache.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIView+WebCache.h" #import "objc/runtime.h" #import "UIView+WebCacheOperation.h" #import "SDWebImageError.h" #import "SDInternalMacros.h" #import "SDWebImageTransitionInternal.h" #import "SDImageCache.h" #import "SDCallbackQueue.h" const int64_t SDWebImageProgressUnitCountUnknown = 1LL; @implementation UIView (WebCache) - (nullable NSString *)sd_latestOperationKey { return objc_getAssociatedObject(self, @selector(sd_latestOperationKey)); } - (void)setSd_latestOperationKey:(NSString * _Nullable)sd_latestOperationKey { objc_setAssociatedObject(self, @selector(sd_latestOperationKey), sd_latestOperationKey, OBJC_ASSOCIATION_COPY_NONATOMIC); } #pragma mark - State - (NSURL *)sd_imageURL { return [self sd_imageLoadStateForKey:self.sd_latestOperationKey].url; } - (NSProgress *)sd_imageProgress { SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:self.sd_latestOperationKey]; NSProgress *progress = loadState.progress; if (!progress) { progress = [[NSProgress alloc] initWithParent:nil userInfo:nil]; self.sd_imageProgress = progress; } return progress; } - (void)setSd_imageProgress:(NSProgress *)sd_imageProgress { if (!sd_imageProgress) { return; } SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:self.sd_latestOperationKey]; if (!loadState) { loadState = [SDWebImageLoadState new]; } loadState.progress = sd_imageProgress; [self sd_setImageLoadState:loadState forKey:self.sd_latestOperationKey]; } - (nullable id)sd_internalSetImageWithURL:(nullable NSURL *)url placeholderImage:(nullable UIImage *)placeholder options:(SDWebImageOptions)options context:(nullable SDWebImageContext *)context setImageBlock:(nullable SDSetImageBlock)setImageBlock progress:(nullable SDImageLoaderProgressBlock)progressBlock completed:(nullable SDInternalCompletionBlock)completedBlock { // Very common mistake is to send the URL using NSString object instead of NSURL. For some strange reason, Xcode won't // throw any warning for this type mismatch. Here we failsafe this error by allowing URLs to be passed as NSString. // if url is NSString and shouldUseWeakMemoryCache is true, [cacheKeyForURL:context] will crash. just for a global protect. if ([url isKindOfClass:NSString.class]) { url = [NSURL URLWithString:(NSString *)url]; } // Prevents app crashing on argument type error like sending NSNull instead of NSURL if (![url isKindOfClass:NSURL.class]) { url = nil; } if (context) { // copy to avoid mutable object context = [context copy]; } else { context = [NSDictionary dictionary]; } NSString *validOperationKey = context[SDWebImageContextSetImageOperationKey]; if (!validOperationKey) { // pass through the operation key to downstream, which can used for tracing operation or image view class validOperationKey = NSStringFromClass([self class]); SDWebImageMutableContext *mutableContext = [context mutableCopy]; mutableContext[SDWebImageContextSetImageOperationKey] = validOperationKey; context = [mutableContext copy]; } self.sd_latestOperationKey = validOperationKey; if (!(SD_OPTIONS_CONTAINS(options, SDWebImageAvoidAutoCancelImage))) { // cancel previous loading for the same set-image operation key by default [self sd_cancelImageLoadOperationWithKey:validOperationKey]; } SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:validOperationKey]; if (!loadState) { loadState = [SDWebImageLoadState new]; } loadState.url = url; [self sd_setImageLoadState:loadState forKey:validOperationKey]; SDWebImageManager *manager = context[SDWebImageContextCustomManager]; if (!manager) { manager = [SDWebImageManager sharedManager]; } else { // remove this manager to avoid retain cycle (manger -> loader -> operation -> context -> manager) SDWebImageMutableContext *mutableContext = [context mutableCopy]; mutableContext[SDWebImageContextCustomManager] = nil; context = [mutableContext copy]; } SDCallbackQueue *queue = context[SDWebImageContextCallbackQueue]; BOOL shouldUseWeakCache = NO; if ([manager.imageCache isKindOfClass:SDImageCache.class]) { shouldUseWeakCache = ((SDImageCache *)manager.imageCache).config.shouldUseWeakMemoryCache; } if (!(options & SDWebImageDelayPlaceholder)) { if (shouldUseWeakCache) { NSString *key = [manager cacheKeyForURL:url context:context]; // call memory cache to trigger weak cache sync logic, ignore the return value and go on normal query // this unfortunately will cause twice memory cache query, but it's fast enough // in the future the weak cache feature may be re-design or removed [((SDImageCache *)manager.imageCache) imageFromMemoryCacheForKey:key]; } [(queue ?: SDCallbackQueue.mainQueue) async:^{ [self sd_setImage:placeholder imageData:nil basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:SDImageCacheTypeNone imageURL:url]; }]; } id operation = nil; if (url) { // reset the progress NSProgress *imageProgress = loadState.progress; if (imageProgress) { imageProgress.totalUnitCount = 0; imageProgress.completedUnitCount = 0; } #if SD_UIKIT || SD_MAC // check and start image indicator [self sd_startImageIndicatorWithQueue:queue]; id imageIndicator = self.sd_imageIndicator; #endif SDImageLoaderProgressBlock combinedProgressBlock = ^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) { if (imageProgress) { imageProgress.totalUnitCount = expectedSize; imageProgress.completedUnitCount = receivedSize; } #if SD_UIKIT || SD_MAC if ([imageIndicator respondsToSelector:@selector(updateIndicatorProgress:)]) { double progress = 0; if (expectedSize != 0) { progress = (double)receivedSize / expectedSize; } progress = MAX(MIN(progress, 1), 0); // 0.0 - 1.0 dispatch_async(dispatch_get_main_queue(), ^{ [imageIndicator updateIndicatorProgress:progress]; }); } #endif if (progressBlock) { progressBlock(receivedSize, expectedSize, targetURL); } }; @weakify(self); operation = [manager loadImageWithURL:url options:options context:context progress:combinedProgressBlock completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) { @strongify(self); if (!self) { return; } // if the progress not been updated, mark it to complete state if (imageProgress && finished && !error && imageProgress.totalUnitCount == 0 && imageProgress.completedUnitCount == 0) { imageProgress.totalUnitCount = SDWebImageProgressUnitCountUnknown; imageProgress.completedUnitCount = SDWebImageProgressUnitCountUnknown; } #if SD_UIKIT || SD_MAC // check and stop image indicator if (finished) { [self sd_stopImageIndicatorWithQueue:queue]; } #endif BOOL shouldCallCompletedBlock = finished || (options & SDWebImageAvoidAutoSetImage); BOOL shouldNotSetImage = ((image && (options & SDWebImageAvoidAutoSetImage)) || (!image && !(options & SDWebImageDelayPlaceholder))); SDWebImageNoParamsBlock callCompletedBlockClosure = ^{ if (!self) { return; } if (!shouldNotSetImage) { [self sd_setNeedsLayout]; } if (completedBlock && shouldCallCompletedBlock) { completedBlock(image, data, error, cacheType, finished, url); } }; // case 1a: we got an image, but the SDWebImageAvoidAutoSetImage flag is set // OR // case 1b: we got no image and the SDWebImageDelayPlaceholder is not set if (shouldNotSetImage) { [(queue ?: SDCallbackQueue.mainQueue) async:callCompletedBlockClosure]; return; } UIImage *targetImage = nil; NSData *targetData = nil; if (image) { // case 2a: we got an image and the SDWebImageAvoidAutoSetImage is not set targetImage = image; targetData = data; } else if (options & SDWebImageDelayPlaceholder) { // case 2b: we got no image and the SDWebImageDelayPlaceholder flag is set targetImage = placeholder; targetData = nil; } #if SD_UIKIT || SD_MAC // check whether we should use the image transition SDWebImageTransition *transition = nil; BOOL shouldUseTransition = NO; if (options & SDWebImageForceTransition) { // Always shouldUseTransition = YES; } else if (cacheType == SDImageCacheTypeNone) { // From network shouldUseTransition = YES; } else { // From disk (and, user don't use sync query) if (cacheType == SDImageCacheTypeMemory) { shouldUseTransition = NO; } else if (cacheType == SDImageCacheTypeDisk) { if (options & SDWebImageQueryMemoryDataSync || options & SDWebImageQueryDiskDataSync) { shouldUseTransition = NO; } else { shouldUseTransition = YES; } } else { // Not valid cache type, fallback shouldUseTransition = NO; } } if (finished && shouldUseTransition) { transition = self.sd_imageTransition; } #endif [(queue ?: SDCallbackQueue.mainQueue) async:^{ #if SD_UIKIT || SD_MAC [self sd_setImage:targetImage imageData:targetData options:options basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:transition cacheType:cacheType imageURL:imageURL callback:callCompletedBlockClosure]; #else [self sd_setImage:targetImage imageData:targetData basedOnClassOrViaCustomSetImageBlock:setImageBlock cacheType:cacheType imageURL:imageURL]; callCompletedBlockClosure(); #endif }]; }]; [self sd_setImageLoadOperation:operation forKey:validOperationKey]; } else { #if SD_UIKIT || SD_MAC [self sd_stopImageIndicatorWithQueue:queue]; #endif if (completedBlock) { [(queue ?: SDCallbackQueue.mainQueue) async:^{ NSError *error = [NSError errorWithDomain:SDWebImageErrorDomain code:SDWebImageErrorInvalidURL userInfo:@{NSLocalizedDescriptionKey : @"Image url is nil"}]; completedBlock(nil, nil, error, SDImageCacheTypeNone, YES, url); }]; } } return operation; } - (void)sd_cancelLatestImageLoad { [self sd_cancelImageLoadOperationWithKey:self.sd_latestOperationKey]; } - (void)sd_cancelCurrentImageLoad { [self sd_cancelImageLoadOperationWithKey:self.sd_latestOperationKey]; } // Set image logic without transition (like placeholder and watchOS) - (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL { #if SD_UIKIT || SD_MAC [self sd_setImage:image imageData:imageData options:0 basedOnClassOrViaCustomSetImageBlock:setImageBlock transition:nil cacheType:cacheType imageURL:imageURL callback:nil]; #else // watchOS does not support view transition. Simplify the logic if (setImageBlock) { setImageBlock(image, imageData, cacheType, imageURL); } else if ([self isKindOfClass:[UIImageView class]]) { UIImageView *imageView = (UIImageView *)self; [imageView setImage:image]; } #endif } // Set image logic with transition #if SD_UIKIT || SD_MAC - (void)sd_setImage:(UIImage *)image imageData:(NSData *)imageData options:(SDWebImageOptions)options basedOnClassOrViaCustomSetImageBlock:(SDSetImageBlock)setImageBlock transition:(SDWebImageTransition *)transition cacheType:(SDImageCacheType)cacheType imageURL:(NSURL *)imageURL callback:(SDWebImageNoParamsBlock)callback { UIView *view = self; SDSetImageBlock finalSetImageBlock; if (setImageBlock) { finalSetImageBlock = setImageBlock; } else if ([view isKindOfClass:[UIImageView class]]) { UIImageView *imageView = (UIImageView *)view; finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) { imageView.image = setImage; }; } #if SD_UIKIT else if ([view isKindOfClass:[UIButton class]]) { UIButton *button = (UIButton *)view; finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) { [button setImage:setImage forState:UIControlStateNormal]; }; } #endif #if SD_MAC else if ([view isKindOfClass:[NSButton class]]) { NSButton *button = (NSButton *)view; finalSetImageBlock = ^(UIImage *setImage, NSData *setImageData, SDImageCacheType setCacheType, NSURL *setImageURL) { button.image = setImage; }; } #endif BOOL waitTransition = SD_OPTIONS_CONTAINS(options, SDWebImageWaitTransition); if (transition) { NSString *originalOperationKey = view.sd_latestOperationKey; #if SD_UIKIT [UIView transitionWithView:view duration:0 options:0 animations:^{ if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { return; } // 0 duration to let UIKit render placeholder and prepares block if (transition.prepares) { transition.prepares(view, image, imageData, cacheType, imageURL); } } completion:^(BOOL tempFinished) { [UIView transitionWithView:view duration:transition.duration options:transition.animationOptions animations:^{ if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { return; } if (finalSetImageBlock && !transition.avoidAutoSetImage) { finalSetImageBlock(image, imageData, cacheType, imageURL); } if (transition.animations) { transition.animations(view, image); } } completion:^(BOOL finished) { if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { return; } if (transition.completion) { transition.completion(finished); } if (waitTransition) { if (callback) { callback(); } } }]; }]; #elif SD_MAC [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull prepareContext) { if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { return; } // 0 duration to let AppKit render placeholder and prepares block prepareContext.duration = 0; if (transition.prepares) { transition.prepares(view, image, imageData, cacheType, imageURL); } } completionHandler:^{ [NSAnimationContext runAnimationGroup:^(NSAnimationContext * _Nonnull context) { if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { return; } context.duration = transition.duration; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-declarations" CAMediaTimingFunction *timingFunction = transition.timingFunction; #pragma clang diagnostic pop if (!timingFunction) { timingFunction = SDTimingFunctionFromAnimationOptions(transition.animationOptions); } context.timingFunction = timingFunction; context.allowsImplicitAnimation = SD_OPTIONS_CONTAINS(transition.animationOptions, SDWebImageAnimationOptionAllowsImplicitAnimation); if (finalSetImageBlock && !transition.avoidAutoSetImage) { finalSetImageBlock(image, imageData, cacheType, imageURL); } CATransition *trans = SDTransitionFromAnimationOptions(transition.animationOptions); if (trans) { [view.layer addAnimation:trans forKey:kCATransition]; } if (transition.animations) { transition.animations(view, image); } } completionHandler:^{ if (!view.sd_latestOperationKey || ![originalOperationKey isEqualToString:view.sd_latestOperationKey]) { return; } if (transition.completion) { transition.completion(YES); } if (waitTransition) { if (callback) { callback(); } } }]; }]; #endif if (!waitTransition) { if (callback) { callback(); } } } else { if (finalSetImageBlock) { finalSetImageBlock(image, imageData, cacheType, imageURL); // TODO, in 6.0 // for `waitTransition`, the `setImageBlock` will provide a extra `completionHandler` params // Execute `callback` only after that completionHandler is called if (waitTransition) { if (callback) { callback(); } } } if (!waitTransition) { if (callback) { callback(); } } } } #endif - (void)sd_setNeedsLayout { #if SD_UIKIT [self setNeedsLayout]; #elif SD_MAC [self setNeedsLayout:YES]; #elif SD_WATCH // Do nothing because WatchKit automatically layout the view after property change #endif } #if SD_UIKIT || SD_MAC #pragma mark - Image Transition - (SDWebImageTransition *)sd_imageTransition { return objc_getAssociatedObject(self, @selector(sd_imageTransition)); } - (void)setSd_imageTransition:(SDWebImageTransition *)sd_imageTransition { objc_setAssociatedObject(self, @selector(sd_imageTransition), sd_imageTransition, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } #pragma mark - Indicator - (id)sd_imageIndicator { return objc_getAssociatedObject(self, @selector(sd_imageIndicator)); } - (void)setSd_imageIndicator:(id)sd_imageIndicator { // Remove the old indicator view id previousIndicator = self.sd_imageIndicator; [previousIndicator.indicatorView removeFromSuperview]; objc_setAssociatedObject(self, @selector(sd_imageIndicator), sd_imageIndicator, OBJC_ASSOCIATION_RETAIN_NONATOMIC); // Add the new indicator view UIView *view = sd_imageIndicator.indicatorView; if (CGRectEqualToRect(view.frame, CGRectZero)) { view.frame = self.bounds; } // Center the indicator view #if SD_MAC [view setFrameOrigin:CGPointMake(round((NSWidth(self.bounds) - NSWidth(view.frame)) / 2), round((NSHeight(self.bounds) - NSHeight(view.frame)) / 2))]; #else view.center = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); #endif view.hidden = NO; [self addSubview:view]; } - (void)sd_startImageIndicatorWithQueue:(SDCallbackQueue *)queue { id imageIndicator = self.sd_imageIndicator; if (!imageIndicator) { return; } [(queue ?: SDCallbackQueue.mainQueue) async:^{ [imageIndicator startAnimatingIndicator]; }]; } - (void)sd_stopImageIndicatorWithQueue:(SDCallbackQueue *)queue { id imageIndicator = self.sd_imageIndicator; if (!imageIndicator) { return; } [(queue ?: SDCallbackQueue.mainQueue) async:^{ [imageIndicator stopAnimatingIndicator]; }]; } #endif @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheOperation.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #import "SDWebImageOperation.h" /** These methods are used to support canceling for UIView image loading, it's designed to be used internal but not external. All the stored operations are weak, so it will be dealloced after image loading finished. If you need to store operations, use your own class to keep a strong reference for them. */ @interface UIView (WebCacheOperation) /** * Get the image load operation for key * * @param key key for identifying the operations * @return the image load operation * @note If key is nil, means using the NSStringFromClass(self.class) instead, match the behavior of `operation key` */ - (nullable id)sd_imageLoadOperationForKey:(nullable NSString *)key; /** * Set the image load operation (storage in a UIView based weak map table) * * @param operation the operation, should not be nil or no-op will perform * @param key key for storing the operation * @note If key is nil, means using the NSStringFromClass(self.class) instead, match the behavior of `operation key` */ - (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key; /** * Cancel the operation for the current UIView and key * * @param key key for identifying the operations * @note If key is nil, means using the NSStringFromClass(self.class) instead, match the behavior of `operation key` */ - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key; /** * Just remove the operation corresponding to the current UIView and key without cancelling them * * @param key key for identifying the operations. * @note If key is nil, means using the NSStringFromClass(self.class) instead, match the behavior of `operation key` */ - (void)sd_removeImageLoadOperationWithKey:(nullable NSString *)key; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheOperation.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIView+WebCacheOperation.h" #import "objc/runtime.h" // key is strong, value is weak because operation instance is retained by SDWebImageManager's runningOperations property // we should use lock to keep thread-safe because these method may not be accessed from main queue typedef NSMapTable> SDOperationsDictionary; @implementation UIView (WebCacheOperation) - (SDOperationsDictionary *)sd_operationDictionary { @synchronized(self) { SDOperationsDictionary *operations = objc_getAssociatedObject(self, @selector(sd_operationDictionary)); if (operations) { return operations; } operations = [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsStrongMemory valueOptions:NSPointerFunctionsWeakMemory capacity:0]; objc_setAssociatedObject(self, @selector(sd_operationDictionary), operations, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return operations; } } - (nullable id)sd_imageLoadOperationForKey:(nullable NSString *)key { id operation; if (!key) { key = NSStringFromClass(self.class); } SDOperationsDictionary *operationDictionary = [self sd_operationDictionary]; @synchronized (self) { operation = [operationDictionary objectForKey:key]; } return operation; } - (void)sd_setImageLoadOperation:(nullable id)operation forKey:(nullable NSString *)key { if (!key) { key = NSStringFromClass(self.class); } if (operation) { SDOperationsDictionary *operationDictionary = [self sd_operationDictionary]; @synchronized (self) { [operationDictionary setObject:operation forKey:key]; } } } - (void)sd_cancelImageLoadOperationWithKey:(nullable NSString *)key { if (!key) { key = NSStringFromClass(self.class); } // Cancel in progress downloader from queue SDOperationsDictionary *operationDictionary = [self sd_operationDictionary]; id operation; @synchronized (self) { operation = [operationDictionary objectForKey:key]; } if (operation) { if ([operation respondsToSelector:@selector(cancel)]) { [operation cancel]; } @synchronized (self) { [operationDictionary removeObjectForKey:key]; } } } - (void)sd_removeImageLoadOperationWithKey:(nullable NSString *)key { if (!key) { key = NSStringFromClass(self.class); } SDOperationsDictionary *operationDictionary = [self sd_operationDictionary]; @synchronized (self) { [operationDictionary removeObjectForKey:key]; } } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheState.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /** A loading state to manage View Category which contains multiple states. Like UIImgeView.image && UIImageView.highlightedImage * @code SDWebImageLoadState *loadState = [self sd_imageLoadStateForKey:@keypath(self, highlitedImage)]; NSProgress *highlitedImageProgress = loadState.progress; * @endcode */ @interface SDWebImageLoadState : NSObject /** Image loading URL */ @property (nonatomic, strong, nullable) NSURL *url; /** Image loading progress. The unit count is the received size and excepted size of download. */ @property (nonatomic, strong, nullable) NSProgress *progress; @end /** These methods are used for WebCache view which have multiple states for image loading, for example, `UIButton` or `UIImageView.highlightedImage` It maitain the state container for per-operation, make it possible for control and check each image loading operation's state. @note For developer who want to add SDWebImage View Category support for their own stateful class, learn more on Wiki. */ @interface UIView (WebCacheState) /** Get the image loading state container for specify operation key @param key key for identifying the operations @return The image loading state container */ - (nullable SDWebImageLoadState *)sd_imageLoadStateForKey:(nullable NSString *)key; /** Set the image loading state container for specify operation key @param state The image loading state container @param key key for identifying the operations */ - (void)sd_setImageLoadState:(nullable SDWebImageLoadState *)state forKey:(nullable NSString *)key; /** Rmove the image loading state container for specify operation key @param key key for identifying the operations */ - (void)sd_removeImageLoadStateForKey:(nullable NSString *)key; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Core/UIView+WebCacheState.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIView+WebCacheState.h" #import "objc/runtime.h" typedef NSMutableDictionary SDStatesDictionary; @implementation SDWebImageLoadState @end @implementation UIView (WebCacheState) - (SDStatesDictionary *)sd_imageLoadStateDictionary { SDStatesDictionary *states = objc_getAssociatedObject(self, @selector(sd_imageLoadStateDictionary)); if (!states) { states = [NSMutableDictionary dictionary]; objc_setAssociatedObject(self, @selector(sd_imageLoadStateDictionary), states, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return states; } - (SDWebImageLoadState *)sd_imageLoadStateForKey:(NSString *)key { if (!key) { key = NSStringFromClass(self.class); } @synchronized(self) { return [self.sd_imageLoadStateDictionary objectForKey:key]; } } - (void)sd_setImageLoadState:(SDWebImageLoadState *)state forKey:(NSString *)key { if (!key) { key = NSStringFromClass(self.class); } @synchronized(self) { self.sd_imageLoadStateDictionary[key] = state; } } - (void)sd_removeImageLoadStateForKey:(NSString *)key { if (!key) { key = NSStringFromClass(self.class); } @synchronized(self) { self.sd_imageLoadStateDictionary[key] = nil; } } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/NSBezierPath+SDRoundedCorners.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_MAC #import "UIImage+Transform.h" @interface NSBezierPath (SDRoundedCorners) /** Convenience way to create a bezier path with the specify rounding corners on macOS. Same as the one on `UIBezierPath`. */ + (nonnull instancetype)sd_bezierPathWithRoundedRect:(NSRect)rect byRoundingCorners:(SDRectCorner)corners cornerRadius:(CGFloat)cornerRadius; @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/NSBezierPath+SDRoundedCorners.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "NSBezierPath+SDRoundedCorners.h" #if SD_MAC @implementation NSBezierPath (SDRoundedCorners) + (instancetype)sd_bezierPathWithRoundedRect:(NSRect)rect byRoundingCorners:(SDRectCorner)corners cornerRadius:(CGFloat)cornerRadius { NSBezierPath *path = [NSBezierPath bezierPath]; CGFloat maxCorner = MIN(NSWidth(rect), NSHeight(rect)) / 2; CGFloat topLeftRadius = MIN(maxCorner, (corners & SDRectCornerTopLeft) ? cornerRadius : 0); CGFloat topRightRadius = MIN(maxCorner, (corners & SDRectCornerTopRight) ? cornerRadius : 0); CGFloat bottomLeftRadius = MIN(maxCorner, (corners & SDRectCornerBottomLeft) ? cornerRadius : 0); CGFloat bottomRightRadius = MIN(maxCorner, (corners & SDRectCornerBottomRight) ? cornerRadius : 0); NSPoint topLeft = NSMakePoint(NSMinX(rect), NSMaxY(rect)); NSPoint topRight = NSMakePoint(NSMaxX(rect), NSMaxY(rect)); NSPoint bottomLeft = NSMakePoint(NSMinX(rect), NSMinY(rect)); NSPoint bottomRight = NSMakePoint(NSMaxX(rect), NSMinY(rect)); [path moveToPoint:NSMakePoint(NSMidX(rect), NSMaxY(rect))]; [path appendBezierPathWithArcFromPoint:topLeft toPoint:bottomLeft radius:topLeftRadius]; [path appendBezierPathWithArcFromPoint:bottomLeft toPoint:bottomRight radius:bottomLeftRadius]; [path appendBezierPathWithArcFromPoint:bottomRight toPoint:topRight radius:bottomRightRadius]; [path appendBezierPathWithArcFromPoint:topRight toPoint:topLeft radius:topRightRadius]; [path closePath]; return path; } @end #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDAssociatedObject.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" /// Copy the associated object from source image to target image. The associated object including all the category read/write properties. /// @param source source /// @param target target FOUNDATION_EXPORT void SDImageCopyAssociatedObject(UIImage * _Nullable source, UIImage * _Nullable target); ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDAssociatedObject.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDAssociatedObject.h" #import "UIImage+Metadata.h" #import "UIImage+ExtendedCacheData.h" #import "UIImage+MemoryCacheCost.h" #import "UIImage+ForceDecode.h" void SDImageCopyAssociatedObject(UIImage * _Nullable source, UIImage * _Nullable target) { if (!source || !target) { return; } // Image Metadata target.sd_isIncremental = source.sd_isIncremental; target.sd_isTransformed = source.sd_isTransformed; target.sd_decodeOptions = source.sd_decodeOptions; target.sd_imageLoopCount = source.sd_imageLoopCount; target.sd_imageFormat = source.sd_imageFormat; // Force Decode target.sd_isDecoded = source.sd_isDecoded; // Extended Cache Data target.sd_extendedObject = source.sd_extendedObject; } ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDAsyncBlockOperation.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" @class SDAsyncBlockOperation; typedef void (^SDAsyncBlock)(SDAsyncBlockOperation * __nonnull asyncOperation); /// A async block operation, success after you call `completer` (not like `NSBlockOperation` which is for sync block, success on return) @interface SDAsyncBlockOperation : NSOperation - (nonnull instancetype)initWithBlock:(nonnull SDAsyncBlock)block; + (nonnull instancetype)blockOperationWithBlock:(nonnull SDAsyncBlock)block; - (void)complete; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDAsyncBlockOperation.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDAsyncBlockOperation.h" #import "SDInternalMacros.h" @interface SDAsyncBlockOperation () @property (nonatomic, copy, nonnull) SDAsyncBlock executionBlock; @end @implementation SDAsyncBlockOperation @synthesize executing = _executing; @synthesize finished = _finished; - (nonnull instancetype)initWithBlock:(nonnull SDAsyncBlock)block { self = [super init]; if (self) { self.executionBlock = block; } return self; } + (nonnull instancetype)blockOperationWithBlock:(nonnull SDAsyncBlock)block { SDAsyncBlockOperation *operation = [[SDAsyncBlockOperation alloc] initWithBlock:block]; return operation; } - (void)start { @synchronized (self) { if (self.isCancelled) { self.finished = YES; return; } self.finished = NO; self.executing = YES; } SDAsyncBlock executionBlock = self.executionBlock; if (executionBlock) { @weakify(self); dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ @strongify(self); if (!self) return; executionBlock(self); }); } } - (void)cancel { @synchronized (self) { [super cancel]; if (self.isExecuting) { self.executing = NO; self.finished = YES; } } } - (void)complete { @synchronized (self) { if (self.isExecuting) { self.finished = YES; self.executing = NO; } } } - (void)setFinished:(BOOL)finished { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; } - (void)setExecuting:(BOOL)executing { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } - (BOOL)isAsynchronous { return YES; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDDeviceHelper.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /// Device information helper methods @interface SDDeviceHelper : NSObject #pragma mark - RAM + (NSUInteger)totalMemory; + (NSUInteger)freeMemory; #pragma mark - Screen + (double)screenScale; + (double)screenEDR; + (double)screenMaxEDR; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDDeviceHelper.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDDeviceHelper.h" #import #import @implementation SDDeviceHelper + (NSUInteger)totalMemory { return (NSUInteger)[[NSProcessInfo processInfo] physicalMemory]; } + (NSUInteger)freeMemory { mach_port_t host_port = mach_host_self(); mach_msg_type_number_t host_size = sizeof(vm_statistics_data_t) / sizeof(integer_t); vm_size_t page_size; vm_statistics_data_t vm_stat; kern_return_t kern; kern = host_page_size(host_port, &page_size); if (kern != KERN_SUCCESS) return 0; kern = host_statistics(host_port, HOST_VM_INFO, (host_info_t)&vm_stat, &host_size); if (kern != KERN_SUCCESS) return 0; return vm_stat.free_count * page_size; } + (double)screenScale { #if SD_VISION CGFloat screenScale = UITraitCollection.currentTraitCollection.displayScale; #elif SD_WATCH CGFloat screenScale = [WKInterfaceDevice currentDevice].screenScale; #elif SD_UIKIT CGFloat screenScale = [UIScreen mainScreen].scale; #elif SD_MAC NSScreen *mainScreen = nil; if (@available(macOS 10.12, *)) { mainScreen = [NSScreen mainScreen]; } else { mainScreen = [NSScreen screens].firstObject; } CGFloat screenScale = mainScreen.backingScaleFactor ?: 1.0f; #endif return screenScale; } + (double)screenEDR { #if SD_VISION // no API to query, but it's HDR ready, from the testing, the value is 200 nits CGFloat EDR = 2.0; #elif SD_WATCH // currently no HDR support, fallback to SDR CGFloat EDR = 1.0; #elif SD_UIKIT CGFloat EDR = 1.0; if (@available(iOS 16.0, tvOS 16.0, *)) { UIScreen *mainScreen = [UIScreen mainScreen]; EDR = mainScreen.currentEDRHeadroom; } #elif SD_MAC CGFloat EDR = 1.0; if (@available(macOS 10.15, *)) { NSScreen *mainScreen = [NSScreen mainScreen]; EDR = mainScreen.maximumExtendedDynamicRangeColorComponentValue; } #endif return EDR; } + (double)screenMaxEDR { #if SD_VISION // no API to query, but it's HDR ready, from the testing, the value is 200 nits CGFloat maxEDR = 2.0; #elif SD_WATCH // currently no HDR support, fallback to SDR CGFloat maxEDR = 1.0; #elif SD_UIKIT CGFloat maxEDR = 1.0; if (@available(iOS 16.0, tvOS 16.0, *)) { UIScreen *mainScreen = [UIScreen mainScreen]; maxEDR = mainScreen.potentialEDRHeadroom; } #elif SD_MAC CGFloat maxEDR = 1.0; if (@available(macOS 10.15, *)) { NSScreen *mainScreen = [NSScreen mainScreen]; maxEDR = mainScreen.maximumPotentialExtendedDynamicRangeColorComponentValue; } #endif return maxEDR; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDDisplayLink.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /// Cross-platform display link wrapper. Do not retain the target /// Use `CADisplayLink` on iOS/tvOS, `CVDisplayLink` on macOS, `NSTimer` on watchOS @interface SDDisplayLink : NSObject @property (readonly, nonatomic, weak, nullable) id target; @property (readonly, nonatomic, assign, nonnull) SEL selector; @property (readonly, nonatomic) NSTimeInterval duration; // elapsed time in seconds of previous callback. (or it's first callback, use the time between `start` and callback). Always zero when display link not running @property (readonly, nonatomic) BOOL isRunning; + (nonnull instancetype)displayLinkWithTarget:(nonnull id)target selector:(nonnull SEL)sel; - (void)addToRunLoop:(nonnull NSRunLoop *)runloop forMode:(nonnull NSRunLoopMode)mode; - (void)removeFromRunLoop:(nonnull NSRunLoop *)runloop forMode:(nonnull NSRunLoopMode)mode; - (void)start; - (void)stop; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDDisplayLink.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDDisplayLink.h" #import "SDWeakProxy.h" #if SD_MAC #import #elif SD_UIKIT #import #endif #include #if SD_MAC static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext); #endif #if SD_UIKIT static BOOL kSDDisplayLinkUseTargetTimestamp = NO; // Use `next` fire time, or `previous` fire time (only for CADisplayLink) #endif #define kSDDisplayLinkInterval 1.0 / 60 @interface SDDisplayLink () @property (nonatomic, assign) NSTimeInterval previousFireTime; @property (nonatomic, assign) NSTimeInterval nextFireTime; #if SD_MAC @property (nonatomic, assign) CVDisplayLinkRef displayLink; @property (nonatomic, assign) CVTimeStamp outputTime; @property (nonatomic, copy) NSRunLoopMode runloopMode; #elif SD_UIKIT @property (nonatomic, strong) CADisplayLink *displayLink; #else @property (nonatomic, strong) NSTimer *displayLink; @property (nonatomic, strong) NSRunLoop *runloop; @property (nonatomic, copy) NSRunLoopMode runloopMode; #endif @end @implementation SDDisplayLink - (void)dealloc { #if SD_MAC if (_displayLink) { CVDisplayLinkStop(_displayLink); CVDisplayLinkRelease(_displayLink); _displayLink = NULL; } #elif SD_UIKIT [_displayLink invalidate]; _displayLink = nil; #else [_displayLink invalidate]; _displayLink = nil; #endif } - (instancetype)initWithTarget:(id)target selector:(SEL)sel { self = [super init]; if (self) { _target = target; _selector = sel; // CA/CV/NSTimer will retain to the target, we need to break this using weak proxy SDWeakProxy *weakProxy = [SDWeakProxy proxyWithTarget:self]; #if SD_UIKIT if (@available(iOS 10.0, tvOS 10.0, *)) { // Use static bool, which is a little faster than runtime OS version check kSDDisplayLinkUseTargetTimestamp = YES; } #endif #if SD_MAC CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink); // Simulate retain for target, the target is weak proxy to self CVDisplayLinkSetOutputCallback(_displayLink, DisplayLinkCallback, (__bridge_retained void *)weakProxy); #elif SD_UIKIT _displayLink = [CADisplayLink displayLinkWithTarget:weakProxy selector:@selector(displayLinkDidRefresh:)]; #else _displayLink = [NSTimer timerWithTimeInterval:kSDDisplayLinkInterval target:weakProxy selector:@selector(displayLinkDidRefresh:) userInfo:nil repeats:YES]; #endif } return self; } + (instancetype)displayLinkWithTarget:(id)target selector:(SEL)sel { SDDisplayLink *displayLink = [[SDDisplayLink alloc] initWithTarget:target selector:sel]; return displayLink; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" - (NSTimeInterval)duration { NSTimeInterval duration = 0; #if SD_MAC CVTimeStamp outputTime = self.outputTime; double periodPerSecond = (double)outputTime.videoTimeScale * outputTime.rateScalar; if (periodPerSecond > 0) { duration = (double)outputTime.videoRefreshPeriod / periodPerSecond; } #elif SD_UIKIT // iOS 10+ use current `targetTimestamp` - previous `targetTimestamp` // See: WWDC Session 10147 - Optimize for variable refresh rate displays if (kSDDisplayLinkUseTargetTimestamp) { NSTimeInterval nextFireTime = self.nextFireTime; if (nextFireTime != 0) { duration = self.displayLink.targetTimestamp - nextFireTime; } else { // Invalid, fallback `duration` duration = self.displayLink.duration; } } else { // iOS 9 use current `timestamp` - previous `timestamp` NSTimeInterval previousFireTime = self.previousFireTime; if (previousFireTime != 0) { duration = self.displayLink.timestamp - previousFireTime; } else { // Invalid, fallback `duration` duration = self.displayLink.duration; } } #else NSTimeInterval nextFireTime = self.nextFireTime; if (nextFireTime != 0) { // `CFRunLoopTimerGetNextFireDate`: This time could be a date in the past if a run loop has not been able to process the timer since the firing time arrived. // Don't rely on this, always calculate based on elapsed time duration = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink) - nextFireTime; } #endif // When system sleep, the targetTimestamp will mass up, fallback refresh rate if (duration < 0) { #if SD_MAC // Supports Pro display 120Hz CGDirectDisplayID display = CVDisplayLinkGetCurrentCGDisplay(_displayLink); CGDisplayModeRef mode = CGDisplayCopyDisplayMode(display); if (mode) { double refreshRate = CGDisplayModeGetRefreshRate(mode); if (refreshRate > 0) { duration = 1.0 / refreshRate; } else { duration = kSDDisplayLinkInterval; } CGDisplayModeRelease(mode); } else { duration = kSDDisplayLinkInterval; } #elif SD_UIKIT // Fallback duration = self.displayLink.duration; #else // Watch always 60Hz duration = kSDDisplayLinkInterval; #endif } return duration; } #pragma clang diagnostic pop - (BOOL)isRunning { #if SD_MAC return CVDisplayLinkIsRunning(self.displayLink); #elif SD_UIKIT return !self.displayLink.isPaused; #else return self.displayLink.isValid; #endif } - (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode { if (!runloop || !mode) { return; } #if SD_MAC self.runloopMode = mode; #elif SD_UIKIT [self.displayLink addToRunLoop:runloop forMode:mode]; #else self.runloop = runloop; self.runloopMode = mode; CFRunLoopMode cfMode; if ([mode isEqualToString:NSDefaultRunLoopMode]) { cfMode = kCFRunLoopDefaultMode; } else if ([mode isEqualToString:NSRunLoopCommonModes]) { cfMode = kCFRunLoopCommonModes; } else { cfMode = (__bridge CFStringRef)mode; } CFRunLoopAddTimer(runloop.getCFRunLoop, (__bridge CFRunLoopTimerRef)self.displayLink, cfMode); #endif } - (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode { if (!runloop || !mode) { return; } #if SD_MAC self.runloopMode = nil; #elif SD_UIKIT [self.displayLink removeFromRunLoop:runloop forMode:mode]; #else self.runloop = nil; self.runloopMode = nil; CFRunLoopMode cfMode; if ([mode isEqualToString:NSDefaultRunLoopMode]) { cfMode = kCFRunLoopDefaultMode; } else if ([mode isEqualToString:NSRunLoopCommonModes]) { cfMode = kCFRunLoopCommonModes; } else { cfMode = (__bridge CFStringRef)mode; } CFRunLoopRemoveTimer(runloop.getCFRunLoop, (__bridge CFRunLoopTimerRef)self.displayLink, cfMode); #endif } - (void)start { #if SD_MAC CVDisplayLinkStart(self.displayLink); #elif SD_UIKIT self.displayLink.paused = NO; #else if (self.displayLink.isValid) { // Do nothing } else { SDWeakProxy *weakProxy = [SDWeakProxy proxyWithTarget:self]; self.displayLink = [NSTimer timerWithTimeInterval:kSDDisplayLinkInterval target:weakProxy selector:@selector(displayLinkDidRefresh:) userInfo:nil repeats:YES]; [self addToRunLoop:self.runloop forMode:self.runloopMode]; } #endif } - (void)stop { #if SD_MAC CVDisplayLinkStop(self.displayLink); #elif SD_UIKIT self.displayLink.paused = YES; #else [self.displayLink invalidate]; #endif self.previousFireTime = 0; self.nextFireTime = 0; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" - (void)displayLinkDidRefresh:(id)displayLink { #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" [_target performSelector:_selector withObject:self]; #pragma clang diagnostic pop #if SD_UIKIT if (kSDDisplayLinkUseTargetTimestamp) { self.nextFireTime = self.displayLink.targetTimestamp; } else { self.previousFireTime = self.displayLink.timestamp; } #endif #if SD_WATCH self.nextFireTime = CFRunLoopTimerGetNextFireDate((__bridge CFRunLoopTimerRef)self.displayLink); #endif } #pragma clang diagnostic pop @end #if SD_MAC static CVReturn DisplayLinkCallback(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext) { @autoreleasepool { // CVDisplayLink callback is not on main queue // Actually `SDWeakProxy` but not `SDDisplayLink` SDDisplayLink *object = (__bridge SDDisplayLink *)displayLinkContext; if (!object) return kCVReturnSuccess; // CVDisplayLink does not use runloop, but we can provide similar behavior for modes // May use `default` runloop to avoid extra callback when in `eventTracking` (mouse drag, scroll) or `modalPanel` (modal panel) NSString *runloopMode = object.runloopMode; if (![runloopMode isEqualToString:NSRunLoopCommonModes] && ![runloopMode isEqualToString:NSRunLoop.mainRunLoop.currentMode]) { return kCVReturnSuccess; } CVTimeStamp outputTime = inOutputTime ? *inOutputTime : *inNow; // `SDWeakProxy` is weak, so it's safe to dispatch to main queue without leak dispatch_async(dispatch_get_main_queue(), ^{ object.outputTime = outputTime; [object displayLinkDidRefresh:(__bridge id)(displayLink)]; }); return kCVReturnSuccess; } } #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDFileAttributeHelper.h ================================================ // // This file is from https://gist.github.com/zydeco/6292773 // // Created by Jesús A. Álvarez on 2008-12-17. // Copyright 2008-2009 namedfork.net. All rights reserved. // #import /// File Extended Attribute (xattr) helper methods @interface SDFileAttributeHelper : NSObject + (nullable NSArray *)extendedAttributeNamesAtPath:(nonnull NSString *)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; + (BOOL)hasExtendedAttribute:(nonnull NSString *)name atPath:(nonnull NSString *)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; + (nullable NSData *)extendedAttribute:(nonnull NSString *)name atPath:(nonnull NSString *)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; + (BOOL)setExtendedAttribute:(nonnull NSString *)name value:(nonnull NSData *)value atPath:(nonnull NSString *)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError * _Nullable * _Nullable)err; + (BOOL)removeExtendedAttribute:(nonnull NSString *)name atPath:(nonnull NSString *)path traverseLink:(BOOL)follow error:(NSError * _Nullable * _Nullable)err; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDFileAttributeHelper.m ================================================ // // This file is from https://gist.github.com/zydeco/6292773 // // Created by Jesús A. Álvarez on 2008-12-17. // Copyright 2008-2009 namedfork.net. All rights reserved. // #import "SDFileAttributeHelper.h" #import @implementation SDFileAttributeHelper + (NSArray*)extendedAttributeNamesAtPath:(NSString *)path traverseLink:(BOOL)follow error:(NSError **)err { int flags = follow? 0 : XATTR_NOFOLLOW; // get size of name list ssize_t nameBuffLen = listxattr([path fileSystemRepresentation], NULL, 0, flags); if (nameBuffLen == -1) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: @{ @"error": [NSString stringWithUTF8String:strerror(errno)], @"function": @"listxattr", @":path": path, @":traverseLink": @(follow) } ]; return nil; } else if (nameBuffLen == 0) return @[]; // get name list NSMutableData *nameBuff = [NSMutableData dataWithLength:nameBuffLen]; listxattr([path fileSystemRepresentation], [nameBuff mutableBytes], nameBuffLen, flags); // convert to array NSMutableArray * names = [NSMutableArray arrayWithCapacity:5]; char *nextName, *endOfNames = [nameBuff mutableBytes] + nameBuffLen; for(nextName = [nameBuff mutableBytes]; nextName < endOfNames; nextName += 1+strlen(nextName)) [names addObject:[NSString stringWithUTF8String:nextName]]; return names.copy; } + (BOOL)hasExtendedAttribute:(NSString *)name atPath:(NSString *)path traverseLink:(BOOL)follow error:(NSError **)err { int flags = follow? 0 : XATTR_NOFOLLOW; // get size of name list ssize_t nameBuffLen = listxattr([path fileSystemRepresentation], NULL, 0, flags); if (nameBuffLen == -1) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: @{ @"error": [NSString stringWithUTF8String:strerror(errno)], @"function": @"listxattr", @":path": path, @":traverseLink": @(follow) } ]; return NO; } else if (nameBuffLen == 0) return NO; // get name list NSMutableData *nameBuff = [NSMutableData dataWithLength:nameBuffLen]; listxattr([path fileSystemRepresentation], [nameBuff mutableBytes], nameBuffLen, flags); // find our name char *nextName, *endOfNames = [nameBuff mutableBytes] + nameBuffLen; for(nextName = [nameBuff mutableBytes]; nextName < endOfNames; nextName += 1+strlen(nextName)) if (strcmp(nextName, [name UTF8String]) == 0) return YES; return NO; } + (NSData *)extendedAttribute:(NSString *)name atPath:(NSString *)path traverseLink:(BOOL)follow error:(NSError **)err { int flags = follow? 0 : XATTR_NOFOLLOW; // get length ssize_t attrLen = getxattr([path fileSystemRepresentation], [name UTF8String], NULL, 0, 0, flags); if (attrLen == -1) { if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: @{ @"error": [NSString stringWithUTF8String:strerror(errno)], @"function": @"getxattr", @":name": name, @":path": path, @":traverseLink": @(follow) } ]; return nil; } // get attribute data NSMutableData *attrData = [NSMutableData dataWithLength:attrLen]; getxattr([path fileSystemRepresentation], [name UTF8String], [attrData mutableBytes], attrLen, 0, flags); return attrData; } + (BOOL)setExtendedAttribute:(NSString *)name value:(NSData *)value atPath:(NSString *)path traverseLink:(BOOL)follow overwrite:(BOOL)overwrite error:(NSError **)err { int flags = (follow? 0 : XATTR_NOFOLLOW) | (overwrite? 0 : XATTR_CREATE); if (0 == setxattr([path fileSystemRepresentation], [name UTF8String], [value bytes], [value length], 0, flags)) return YES; // error if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: @{ @"error": [NSString stringWithUTF8String:strerror(errno)], @"function": @"setxattr", @":name": name, @":value.length": @(value.length), @":path": path, @":traverseLink": @(follow), @":overwrite": @(overwrite) } ]; return NO; } + (BOOL)removeExtendedAttribute:(NSString *)name atPath:(NSString *)path traverseLink:(BOOL)follow error:(NSError **)err { int flags = (follow? 0 : XATTR_NOFOLLOW); if (0 == removexattr([path fileSystemRepresentation], [name UTF8String], flags)) return YES; // error if (err) *err = [NSError errorWithDomain:NSPOSIXErrorDomain code:errno userInfo: @{ @"error": [NSString stringWithUTF8String:strerror(errno)], @"function": @"removexattr", @":name": name, @":path": path, @":traverseLink": @(follow) } ]; return NO; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDImageAssetManager.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /// A Image-Asset manager to work like UIKit/AppKit's image cache behavior /// Apple parse the Asset Catalog compiled file(`Assets.car`) by CoreUI.framework, however it's a private framework and there are no other ways to directly get the data. So we just process the normal bundle files :) @interface SDImageAssetManager : NSObject @property (nonatomic, strong, nonnull) NSMapTable *imageTable; + (nonnull instancetype)sharedAssetManager; - (nullable NSString *)getPathForName:(nonnull NSString *)name bundle:(nonnull NSBundle *)bundle preferredScale:(nonnull CGFloat *)scale; - (nullable UIImage *)imageForName:(nonnull NSString *)name; - (void)storeImage:(nonnull UIImage *)image forName:(nonnull NSString *)name; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDImageAssetManager.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageAssetManager.h" #import "SDInternalMacros.h" #import "SDDeviceHelper.h" static NSArray *SDBundlePreferredScales(void) { static NSArray *scales; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ CGFloat screenScale = SDDeviceHelper.screenScale; if (screenScale <= 1) { scales = @[@1,@2,@3]; } else if (screenScale <= 2) { scales = @[@2,@3,@1]; } else { scales = @[@3,@2,@1]; } }); return scales; } @implementation SDImageAssetManager { SD_LOCK_DECLARE(_lock); } + (instancetype)sharedAssetManager { static dispatch_once_t onceToken; static SDImageAssetManager *assetManager; dispatch_once(&onceToken, ^{ assetManager = [[SDImageAssetManager alloc] init]; }); return assetManager; } - (instancetype)init { self = [super init]; if (self) { NSPointerFunctionsOptions valueOptions; #if SD_MAC // Apple says that NSImage use a weak reference to value valueOptions = NSPointerFunctionsWeakMemory; #else // Apple says that UIImage use a strong reference to value valueOptions = NSPointerFunctionsStrongMemory; #endif _imageTable = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsCopyIn valueOptions:valueOptions]; SD_LOCK_INIT(_lock); #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } return self; } - (void)dealloc { #if SD_UIKIT [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } - (void)didReceiveMemoryWarning:(NSNotification *)notification { SD_LOCK(_lock); [self.imageTable removeAllObjects]; SD_UNLOCK(_lock); } - (NSString *)getPathForName:(NSString *)name bundle:(NSBundle *)bundle preferredScale:(CGFloat *)scale { NSParameterAssert(name); NSParameterAssert(bundle); NSString *path; if (name.length == 0) { return path; } if ([name hasSuffix:@"/"]) { return path; } NSString *extension = name.pathExtension; if (extension.length == 0) { // If no extension, follow Apple's doc, check PNG format extension = @"png"; } name = [name stringByDeletingPathExtension]; CGFloat providedScale = *scale; NSArray *scales = SDBundlePreferredScales(); // Check if file name contains scale for (size_t i = 0; i < scales.count; i++) { NSNumber *scaleValue = scales[i]; if ([name hasSuffix:[NSString stringWithFormat:@"@%@x", scaleValue]]) { path = [bundle pathForResource:name ofType:extension]; if (path) { *scale = scaleValue.doubleValue; // override return path; } } } // Search with provided scale first if (providedScale != 0) { NSString *scaledName = [name stringByAppendingFormat:@"@%@x", @(providedScale)]; path = [bundle pathForResource:scaledName ofType:extension]; if (path) { return path; } } // Search with preferred scale for (size_t i = 0; i < scales.count; i++) { NSNumber *scaleValue = scales[i]; if (scaleValue.doubleValue == providedScale) { // Ignore provided scale continue; } NSString *scaledName = [name stringByAppendingFormat:@"@%@x", scaleValue]; path = [bundle pathForResource:scaledName ofType:extension]; if (path) { *scale = scaleValue.doubleValue; // override return path; } } // Search without scale path = [bundle pathForResource:name ofType:extension]; return path; } - (UIImage *)imageForName:(NSString *)name { NSParameterAssert(name); UIImage *image; SD_LOCK(_lock); image = [self.imageTable objectForKey:name]; SD_UNLOCK(_lock); return image; } - (void)storeImage:(UIImage *)image forName:(NSString *)name { NSParameterAssert(image); NSParameterAssert(name); SD_LOCK(_lock); [self.imageTable setObject:image forKey:name]; SD_UNLOCK(_lock); } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDImageCachesManagerOperation.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /// This is used for operation management, but not for operation queue execute @interface SDImageCachesManagerOperation : NSOperation @property (nonatomic, assign, readonly) NSUInteger pendingCount; - (void)beginWithTotalCount:(NSUInteger)totalCount; - (void)completeOne; - (void)done; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDImageCachesManagerOperation.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageCachesManagerOperation.h" #import "SDInternalMacros.h" @implementation SDImageCachesManagerOperation { SD_LOCK_DECLARE(_pendingCountLock); } @synthesize executing = _executing; @synthesize finished = _finished; @synthesize cancelled = _cancelled; @synthesize pendingCount = _pendingCount; - (instancetype)init { if (self = [super init]) { SD_LOCK_INIT(_pendingCountLock); _pendingCount = 0; } return self; } - (void)beginWithTotalCount:(NSUInteger)totalCount { self.executing = YES; self.finished = NO; _pendingCount = totalCount; } - (NSUInteger)pendingCount { SD_LOCK(_pendingCountLock); NSUInteger pendingCount = _pendingCount; SD_UNLOCK(_pendingCountLock); return pendingCount; } - (void)completeOne { SD_LOCK(_pendingCountLock); _pendingCount = _pendingCount > 0 ? _pendingCount - 1 : 0; SD_UNLOCK(_pendingCountLock); } - (void)cancel { self.cancelled = YES; [self reset]; } - (void)done { self.finished = YES; self.executing = NO; [self reset]; } - (void)reset { SD_LOCK(_pendingCountLock); _pendingCount = 0; SD_UNLOCK(_pendingCountLock); } - (void)setFinished:(BOOL)finished { [self willChangeValueForKey:@"isFinished"]; _finished = finished; [self didChangeValueForKey:@"isFinished"]; } - (void)setExecuting:(BOOL)executing { [self willChangeValueForKey:@"isExecuting"]; _executing = executing; [self didChangeValueForKey:@"isExecuting"]; } - (void)setCancelled:(BOOL)cancelled { [self willChangeValueForKey:@"isCancelled"]; _cancelled = cancelled; [self didChangeValueForKey:@"isCancelled"]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDImageFramePool.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" #import "SDImageCoder.h" NS_ASSUME_NONNULL_BEGIN /// A per-provider (provider means, AnimatedImage object) based frame pool, each player who use the same provider share the same frame buffer @interface SDImageFramePool : NSObject /// Register and return back a frame pool, also increase reference count + (instancetype)registerProvider:(id)provider; /// Unregister a frame pool, also decrease reference count, if zero dealloc the frame pool + (void)unregisterProvider:(id)provider; /// Prefetch the current frame, query using `frameAtIndex:` by caller to check whether finished. - (void)prefetchFrameAtIndex:(NSUInteger)index; /// Control the max buffer count for current frame pool, used for RAM/CPU balance, default unlimited @property (nonatomic, assign) NSUInteger maxBufferCount; /// Control the max concurrent fetch queue operation count, used for CPU balance, default 1 @property (nonatomic, assign) NSUInteger maxConcurrentCount; // Frame Operations @property (nonatomic, readonly) NSUInteger currentFrameCount; - (nullable UIImage *)frameAtIndex:(NSUInteger)index; - (void)setFrame:(nullable UIImage *)frame atIndex:(NSUInteger)index; - (void)removeFrameAtIndex:(NSUInteger)index; - (void)removeAllFrames; NS_ASSUME_NONNULL_END @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDImageFramePool.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDImageFramePool.h" #import "SDInternalMacros.h" #import "objc/runtime.h" @interface SDImageFramePool () @property (class, readonly) NSMapTable *providerFramePoolMap; @property (weak) id provider; @property (atomic) NSUInteger registerCount; @property (nonatomic, strong) NSMutableDictionary *frameBuffer; @property (nonatomic, strong) NSOperationQueue *fetchQueue; @end // Lock to ensure atomic behavior SD_LOCK_DECLARE_STATIC(_providerFramePoolMapLock); @implementation SDImageFramePool + (NSMapTable *)providerFramePoolMap { static NSMapTable *providerFramePoolMap; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // Key use `hash` && `isEqual:` providerFramePoolMap = [NSMapTable mapTableWithKeyOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPersonality valueOptions:NSPointerFunctionsStrongMemory | NSPointerFunctionsObjectPointerPersonality]; }); return providerFramePoolMap; } #pragma mark - Life Cycle - (instancetype)init { self = [super init]; if (self) { _frameBuffer = [NSMutableDictionary dictionary]; _fetchQueue = [[NSOperationQueue alloc] init]; _fetchQueue.maxConcurrentOperationCount = 1; _fetchQueue.name = @"com.hackemist.SDImageFramePool.fetchQueue"; #if SD_UIKIT [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } return self; } - (void)dealloc { #if SD_UIKIT [[NSNotificationCenter defaultCenter] removeObserver:self name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; #endif } - (void)didReceiveMemoryWarning:(NSNotification *)notification { [self removeAllFrames]; } + (void)initialize { // Lock to ensure atomic behavior SD_LOCK_INIT(_providerFramePoolMapLock); } + (instancetype)registerProvider:(id)provider { // Lock to ensure atomic behavior SD_LOCK(_providerFramePoolMapLock); SDImageFramePool *framePool = [self.providerFramePoolMap objectForKey:provider]; if (!framePool) { framePool = [[SDImageFramePool alloc] init]; framePool.provider = provider; [self.providerFramePoolMap setObject:framePool forKey:provider]; } framePool.registerCount += 1; SD_UNLOCK(_providerFramePoolMapLock); return framePool; } + (void)unregisterProvider:(id)provider { // Lock to ensure atomic behavior SD_LOCK(_providerFramePoolMapLock); SDImageFramePool *framePool = [self.providerFramePoolMap objectForKey:provider]; if (!framePool) { SD_UNLOCK(_providerFramePoolMapLock); return; } framePool.registerCount -= 1; if (framePool.registerCount == 0) { [self.providerFramePoolMap removeObjectForKey:provider]; } SD_UNLOCK(_providerFramePoolMapLock); } - (void)prefetchFrameAtIndex:(NSUInteger)index { @synchronized (self) { NSUInteger frameCount = self.frameBuffer.count; if (frameCount > self.maxBufferCount) { // Remove the frame buffer if need // TODO, use LRU or better algorithm to detect which frames to clear self.frameBuffer[@(index - 1)] = nil; self.frameBuffer[@(index + 1)] = nil; } } if (self.fetchQueue.operationCount == 0) { // Prefetch next frame in background queue @weakify(self); NSOperation *operation = [NSBlockOperation blockOperationWithBlock:^{ @strongify(self); if (!self) { return; } id animatedProvider = self.provider; if (!animatedProvider) { return; } UIImage *frame = [animatedProvider animatedImageFrameAtIndex:index]; [self setFrame:frame atIndex:index]; }]; [self.fetchQueue addOperation:operation]; } } - (void)setMaxConcurrentCount:(NSUInteger)maxConcurrentCount { self.fetchQueue.maxConcurrentOperationCount = maxConcurrentCount; } - (NSUInteger)currentFrameCount { NSUInteger frameCount = 0; @synchronized (self) { frameCount = self.frameBuffer.count; } return frameCount; } - (void)setFrame:(UIImage *)frame atIndex:(NSUInteger)index { @synchronized (self) { self.frameBuffer[@(index)] = frame; } } - (UIImage *)frameAtIndex:(NSUInteger)index { UIImage *frame; @synchronized (self) { frame = self.frameBuffer[@(index)]; } return frame; } - (void)removeFrameAtIndex:(NSUInteger)index { @synchronized (self) { self.frameBuffer[@(index)] = nil; } } - (void)removeAllFrames { @synchronized (self) { [self.frameBuffer removeAllObjects]; } } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDImageIOAnimatedCoderInternal.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import #import "SDImageIOAnimatedCoder.h" // Xcode 16 SDK contains HDR encoding API, but we still support Xcode 15 #define SD_IMAGEIO_HDR_ENCODING (__IPHONE_OS_VERSION_MAX_ALLOWED >= 180000) // AVFileTypeHEIC/AVFileTypeHEIF is defined in AVFoundation via iOS 11, we use this without import AVFoundation #define kSDUTTypeHEIC ((__bridge CFStringRef)@"public.heic") #define kSDUTTypeHEIF ((__bridge CFStringRef)@"public.heif") // HEIC Sequence (Animated Image) #define kSDUTTypeHEICS ((__bridge CFStringRef)@"public.heics") // kSDUTTypeWebP seems not defined in public UTI framework, Apple use the hardcode string, we define them :) #define kSDUTTypeWebP ((__bridge CFStringRef)@"org.webmproject.webp") #define kSDUTTypeImage ((__bridge CFStringRef)@"public.image") #define kSDUTTypeJPEG ((__bridge CFStringRef)@"public.jpeg") #define kSDUTTypePNG ((__bridge CFStringRef)@"public.png") #define kSDUTTypeTIFF ((__bridge CFStringRef)@"public.tiff") #define kSDUTTypeSVG ((__bridge CFStringRef)@"public.svg-image") #define kSDUTTypeGIF ((__bridge CFStringRef)@"com.compuserve.gif") #define kSDUTTypePDF ((__bridge CFStringRef)@"com.adobe.pdf") #define kSDUTTypeBMP ((__bridge CFStringRef)@"com.microsoft.bmp") #define kSDUTTypeRAW ((__bridge CFStringRef)@"public.camera-raw-image") @interface SDImageIOAnimatedCoder () + (NSTimeInterval)frameDurationAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source; + (NSUInteger)imageLoopCountWithSource:(nonnull CGImageSourceRef)source; + (nullable UIImage *)createFrameAtIndex:(NSUInteger)index source:(nonnull CGImageSourceRef)source scale:(CGFloat)scale preserveAspectRatio:(BOOL)preserveAspectRatio thumbnailSize:(CGSize)thumbnailSize lazyDecode:(BOOL)lazyDecode animatedImage:(BOOL)animatedImage decodeToHDR:(BOOL)decodeToHDR; + (BOOL)canEncodeToFormat:(SDImageFormat)format; + (BOOL)canDecodeFromFormat:(SDImageFormat)format; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDInternalMacros.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import #import #import #import "SDmetamacros.h" #define SD_USE_OS_UNFAIR_LOCK TARGET_OS_MACCATALYST ||\ (__IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_10_0) ||\ (__MAC_OS_X_VERSION_MIN_REQUIRED >= __MAC_10_12) ||\ (__TV_OS_VERSION_MIN_REQUIRED >= __TVOS_10_0) ||\ (__WATCH_OS_VERSION_MIN_REQUIRED >= __WATCHOS_3_0) #ifndef SD_LOCK_DECLARE #if SD_USE_OS_UNFAIR_LOCK #define SD_LOCK_DECLARE(lock) os_unfair_lock lock #else #define SD_LOCK_DECLARE(lock) os_unfair_lock lock API_AVAILABLE(ios(10.0), tvos(10), watchos(3), macos(10.12)); \ OSSpinLock lock##_deprecated; #endif #endif #ifndef SD_LOCK_DECLARE_STATIC #if SD_USE_OS_UNFAIR_LOCK #define SD_LOCK_DECLARE_STATIC(lock) static os_unfair_lock lock #else #define SD_LOCK_DECLARE_STATIC(lock) static os_unfair_lock lock API_AVAILABLE(ios(10.0), tvos(10), watchos(3), macos(10.12)); \ static OSSpinLock lock##_deprecated; #endif #endif #ifndef SD_LOCK_INIT #if SD_USE_OS_UNFAIR_LOCK #define SD_LOCK_INIT(lock) lock = OS_UNFAIR_LOCK_INIT #else #define SD_LOCK_INIT(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) lock = OS_UNFAIR_LOCK_INIT; \ else lock##_deprecated = OS_SPINLOCK_INIT; #endif #endif #ifndef SD_LOCK #if SD_USE_OS_UNFAIR_LOCK #define SD_LOCK(lock) os_unfair_lock_lock(&lock) #else #define SD_LOCK(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) os_unfair_lock_lock(&lock); \ else OSSpinLockLock(&lock##_deprecated); #endif #endif #ifndef SD_UNLOCK #if SD_USE_OS_UNFAIR_LOCK #define SD_UNLOCK(lock) os_unfair_lock_unlock(&lock) #else #define SD_UNLOCK(lock) if (@available(iOS 10, tvOS 10, watchOS 3, macOS 10.12, *)) os_unfair_lock_unlock(&lock); \ else OSSpinLockUnlock(&lock##_deprecated); #endif #endif #ifndef SD_OPTIONS_CONTAINS #define SD_OPTIONS_CONTAINS(options, value) (((options) & (value)) == (value)) #endif #ifndef SD_CSTRING #define SD_CSTRING(str) #str #endif #ifndef SD_NSSTRING #define SD_NSSTRING(str) @(SD_CSTRING(str)) #endif #ifndef SD_SEL_SPI #define SD_SEL_SPI(name) NSSelectorFromString([NSString stringWithFormat:@"_%@", SD_NSSTRING(name)]) #endif FOUNDATION_EXPORT os_log_t sd_getDefaultLog(void); #ifndef SD_LOG #define SD_LOG(_log, ...) if (@available(iOS 10.0, tvOS 10.0, macOS 10.12, watchOS 3.0, *)) os_log(sd_getDefaultLog(), _log, ##__VA_ARGS__); \ else NSLog(@(_log), ##__VA_ARGS__); #endif #ifndef weakify #define weakify(...) \ sd_keywordify \ metamacro_foreach_cxt(sd_weakify_,, __weak, __VA_ARGS__) #endif #ifndef strongify #define strongify(...) \ sd_keywordify \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Wshadow\"") \ metamacro_foreach(sd_strongify_,, __VA_ARGS__) \ _Pragma("clang diagnostic pop") #endif #define sd_weakify_(INDEX, CONTEXT, VAR) \ CONTEXT __typeof__(VAR) metamacro_concat(VAR, _weak_) = (VAR); #define sd_strongify_(INDEX, VAR) \ __strong __typeof__(VAR) VAR = metamacro_concat(VAR, _weak_); #if DEBUG #define sd_keywordify autoreleasepool {} #else #define sd_keywordify try {} @catch (...) {} #endif #ifndef onExit #define onExit \ sd_keywordify \ __strong sd_cleanupBlock_t metamacro_concat(sd_exitBlock_, __LINE__) __attribute__((cleanup(sd_executeCleanupBlock), unused)) = ^ #endif typedef void (^sd_cleanupBlock_t)(void); #if defined(__cplusplus) extern "C" { #endif void sd_executeCleanupBlock (__strong sd_cleanupBlock_t *block); #if defined(__cplusplus) } #endif /** * \@keypath allows compile-time verification of key paths. Given a real object * receiver and key path: * * @code NSString *UTF8StringPath = @keypath(str.lowercaseString.UTF8String); // => @"lowercaseString.UTF8String" NSString *versionPath = @keypath(NSObject, version); // => @"version" NSString *lowercaseStringPath = @keypath(NSString.new, lowercaseString); // => @"lowercaseString" * @endcode * * ... the macro returns an \c NSString containing all but the first path * component or argument (e.g., @"lowercaseString.UTF8String", @"version"). * * In addition to simply creating a key path, this macro ensures that the key * path is valid at compile-time (causing a syntax error if not), and supports * refactoring, such that changing the name of the property will also update any * uses of \@keypath. */ #define keypath(...) \ _Pragma("clang diagnostic push") \ _Pragma("clang diagnostic ignored \"-Warc-repeated-use-of-weak\"") \ (NO).boolValue ? ((NSString * _Nonnull)nil) : ((NSString * _Nonnull)@(cStringKeypath(__VA_ARGS__))) \ _Pragma("clang diagnostic pop") \ #define cStringKeypath(...) \ metamacro_if_eq(1, metamacro_argcount(__VA_ARGS__))(keypath1(__VA_ARGS__))(keypath2(__VA_ARGS__)) #define keypath1(PATH) \ (((void)(NO && ((void)PATH, NO)), \ ({ char *__extobjckeypath__ = strchr(# PATH, '.'); NSCAssert(__extobjckeypath__, @"Provided key path is invalid."); __extobjckeypath__ + 1; }))) #define keypath2(OBJ, PATH) \ (((void)(NO && ((void)OBJ.PATH, NO)), # PATH)) /** * \@collectionKeypath allows compile-time verification of key paths across collections NSArray/NSSet etc. Given a real object * receiver, collection object receiver and related keypaths: * * @code NSString *employeesFirstNamePath = @collectionKeypath(department.employees, Employee.new, firstName) // => @"employees.firstName" NSString *employeesFirstNamePath = @collectionKeypath(Department.new, employees, Employee.new, firstName) // => @"employees.firstName" * @endcode * */ #define collectionKeypath(...) \ metamacro_if_eq(3, metamacro_argcount(__VA_ARGS__))(collectionKeypath3(__VA_ARGS__))(collectionKeypath4(__VA_ARGS__)) #define collectionKeypath3(PATH, COLLECTION_OBJECT, COLLECTION_PATH) \ (YES).boolValue ? (NSString * _Nonnull)@((const char * _Nonnull)[[NSString stringWithFormat:@"%s.%s", cStringKeypath(PATH), cStringKeypath(COLLECTION_OBJECT, COLLECTION_PATH)] UTF8String]) : (NSString * _Nonnull)nil #define collectionKeypath4(OBJ, PATH, COLLECTION_OBJECT, COLLECTION_PATH) \ (YES).boolValue ? (NSString * _Nonnull)@((const char * _Nonnull)[[NSString stringWithFormat:@"%s.%s", cStringKeypath(OBJ, PATH), cStringKeypath(COLLECTION_OBJECT, COLLECTION_PATH)] UTF8String]) : (NSString * _Nonnull)nil ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDInternalMacros.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDInternalMacros.h" os_log_t sd_getDefaultLog(void) { static dispatch_once_t onceToken; static os_log_t log; dispatch_once(&onceToken, ^{ log = os_log_create("com.hackemist.SDWebImage", "Default"); }); return log; } void sd_executeCleanupBlock (__strong sd_cleanupBlock_t *block) { (*block)(); } ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDWeakProxy.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import #import "SDWebImageCompat.h" /// A weak proxy which forward all the message to the target @interface SDWeakProxy : NSProxy @property (nonatomic, weak, readonly, nullable) id target; - (nonnull instancetype)initWithTarget:(nonnull id)target; + (nonnull instancetype)proxyWithTarget:(nonnull id)target; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDWeakProxy.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWeakProxy.h" @implementation SDWeakProxy - (instancetype)initWithTarget:(id)target { _target = target; return self; } + (instancetype)proxyWithTarget:(id)target { return [[SDWeakProxy alloc] initWithTarget:target]; } - (id)forwardingTargetForSelector:(SEL)selector { return _target; } - (void)forwardInvocation:(NSInvocation *)invocation { void *null = NULL; [invocation setReturnValue:&null]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [NSObject instanceMethodSignatureForSelector:@selector(init)]; } - (BOOL)respondsToSelector:(SEL)aSelector { return [_target respondsToSelector:aSelector]; } - (BOOL)isEqual:(id)object { return [_target isEqual:object]; } - (NSUInteger)hash { return [_target hash]; } - (Class)superclass { return [_target superclass]; } - (Class)class { return [_target class]; } - (BOOL)isKindOfClass:(Class)aClass { return [_target isKindOfClass:aClass]; } - (BOOL)isMemberOfClass:(Class)aClass { return [_target isMemberOfClass:aClass]; } - (BOOL)conformsToProtocol:(Protocol *)aProtocol { return [_target conformsToProtocol:aProtocol]; } - (BOOL)isProxy { return YES; } - (NSString *)description { return [_target description]; } - (NSString *)debugDescription { return [_target debugDescription]; } @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDWebImageTransitionInternal.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" #if SD_MAC #import /// Helper method for Core Animation transition FOUNDATION_EXPORT CAMediaTimingFunction * _Nullable SDTimingFunctionFromAnimationOptions(SDWebImageAnimationOptions options); FOUNDATION_EXPORT CATransition * _Nullable SDTransitionFromAnimationOptions(SDWebImageAnimationOptions options); #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/SDmetamacros.h ================================================ /** * Macros for metaprogramming * ExtendedC * * Copyright (C) 2012 Justin Spahr-Summers * Released under the MIT license */ #ifndef EXTC_METAMACROS_H #define EXTC_METAMACROS_H /** * Executes one or more expressions (which may have a void type, such as a call * to a function that returns no value) and always returns true. */ #define metamacro_exprify(...) \ ((__VA_ARGS__), true) /** * Returns a string representation of VALUE after full macro expansion. */ #define metamacro_stringify(VALUE) \ metamacro_stringify_(VALUE) /** * Returns A and B concatenated after full macro expansion. */ #define metamacro_concat(A, B) \ metamacro_concat_(A, B) /** * Returns the Nth variadic argument (starting from zero). At least * N + 1 variadic arguments must be given. N must be between zero and twenty, * inclusive. */ #define metamacro_at(N, ...) \ metamacro_concat(metamacro_at, N)(__VA_ARGS__) /** * Returns the number of arguments (up to twenty) provided to the macro. At * least one argument must be provided. * * Inspired by P99: http://p99.gforge.inria.fr */ #define metamacro_argcount(...) \ metamacro_at(20, __VA_ARGS__, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) /** * Identical to #metamacro_foreach_cxt, except that no CONTEXT argument is * given. Only the index and current argument will thus be passed to MACRO. */ #define metamacro_foreach(MACRO, SEP, ...) \ metamacro_foreach_cxt(metamacro_foreach_iter, SEP, MACRO, __VA_ARGS__) /** * For each consecutive variadic argument (up to twenty), MACRO is passed the * zero-based index of the current argument, CONTEXT, and then the argument * itself. The results of adjoining invocations of MACRO are then separated by * SEP. * * Inspired by P99: http://p99.gforge.inria.fr */ #define metamacro_foreach_cxt(MACRO, SEP, CONTEXT, ...) \ metamacro_concat(metamacro_foreach_cxt, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__) /** * Identical to #metamacro_foreach_cxt. This can be used when the former would * fail due to recursive macro expansion. */ #define metamacro_foreach_cxt_recursive(MACRO, SEP, CONTEXT, ...) \ metamacro_concat(metamacro_foreach_cxt_recursive, metamacro_argcount(__VA_ARGS__))(MACRO, SEP, CONTEXT, __VA_ARGS__) /** * In consecutive order, appends each variadic argument (up to twenty) onto * BASE. The resulting concatenations are then separated by SEP. * * This is primarily useful to manipulate a list of macro invocations into instead * invoking a different, possibly related macro. */ #define metamacro_foreach_concat(BASE, SEP, ...) \ metamacro_foreach_cxt(metamacro_foreach_concat_iter, SEP, BASE, __VA_ARGS__) /** * Iterates COUNT times, each time invoking MACRO with the current index * (starting at zero) and CONTEXT. The results of adjoining invocations of MACRO * are then separated by SEP. * * COUNT must be an integer between zero and twenty, inclusive. */ #define metamacro_for_cxt(COUNT, MACRO, SEP, CONTEXT) \ metamacro_concat(metamacro_for_cxt, COUNT)(MACRO, SEP, CONTEXT) /** * Returns the first argument given. At least one argument must be provided. * * This is useful when implementing a variadic macro, where you may have only * one variadic argument, but no way to retrieve it (for example, because \c ... * always needs to match at least one argument). * * @code #define varmacro(...) \ metamacro_head(__VA_ARGS__) * @endcode */ #define metamacro_head(...) \ metamacro_head_(__VA_ARGS__, 0) /** * Returns every argument except the first. At least two arguments must be * provided. */ #define metamacro_tail(...) \ metamacro_tail_(__VA_ARGS__) /** * Returns the first N (up to twenty) variadic arguments as a new argument list. * At least N variadic arguments must be provided. */ #define metamacro_take(N, ...) \ metamacro_concat(metamacro_take, N)(__VA_ARGS__) /** * Removes the first N (up to twenty) variadic arguments from the given argument * list. At least N variadic arguments must be provided. */ #define metamacro_drop(N, ...) \ metamacro_concat(metamacro_drop, N)(__VA_ARGS__) /** * Decrements VAL, which must be a number between zero and twenty, inclusive. * * This is primarily useful when dealing with indexes and counts in * metaprogramming. */ #define metamacro_dec(VAL) \ metamacro_at(VAL, -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19) /** * Increments VAL, which must be a number between zero and twenty, inclusive. * * This is primarily useful when dealing with indexes and counts in * metaprogramming. */ #define metamacro_inc(VAL) \ metamacro_at(VAL, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21) /** * If A is equal to B, the next argument list is expanded; otherwise, the * argument list after that is expanded. A and B must be numbers between zero * and twenty, inclusive. Additionally, B must be greater than or equal to A. * * @code // expands to true metamacro_if_eq(0, 0)(true)(false) // expands to false metamacro_if_eq(0, 1)(true)(false) * @endcode * * This is primarily useful when dealing with indexes and counts in * metaprogramming. */ #define metamacro_if_eq(A, B) \ metamacro_concat(metamacro_if_eq, A)(B) /** * Identical to #metamacro_if_eq. This can be used when the former would fail * due to recursive macro expansion. */ #define metamacro_if_eq_recursive(A, B) \ metamacro_concat(metamacro_if_eq_recursive, A)(B) /** * Returns 1 if N is an even number, or 0 otherwise. N must be between zero and * twenty, inclusive. * * For the purposes of this test, zero is considered even. */ #define metamacro_is_even(N) \ metamacro_at(N, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1) /** * Returns the logical NOT of B, which must be the number zero or one. */ #define metamacro_not(B) \ metamacro_at(B, 1, 0) // IMPLEMENTATION DETAILS FOLLOW! // Do not write code that depends on anything below this line. #define metamacro_stringify_(VALUE) # VALUE #define metamacro_concat_(A, B) A ## B #define metamacro_foreach_iter(INDEX, MACRO, ARG) MACRO(INDEX, ARG) #define metamacro_head_(FIRST, ...) FIRST #define metamacro_tail_(FIRST, ...) __VA_ARGS__ #define metamacro_consume_(...) #define metamacro_expand_(...) __VA_ARGS__ // implemented from scratch so that metamacro_concat() doesn't end up nesting #define metamacro_foreach_concat_iter(INDEX, BASE, ARG) metamacro_foreach_concat_iter_(BASE, ARG) #define metamacro_foreach_concat_iter_(BASE, ARG) BASE ## ARG // metamacro_at expansions #define metamacro_at0(...) metamacro_head(__VA_ARGS__) #define metamacro_at1(_0, ...) metamacro_head(__VA_ARGS__) #define metamacro_at2(_0, _1, ...) metamacro_head(__VA_ARGS__) #define metamacro_at3(_0, _1, _2, ...) metamacro_head(__VA_ARGS__) #define metamacro_at4(_0, _1, _2, _3, ...) metamacro_head(__VA_ARGS__) #define metamacro_at5(_0, _1, _2, _3, _4, ...) metamacro_head(__VA_ARGS__) #define metamacro_at6(_0, _1, _2, _3, _4, _5, ...) metamacro_head(__VA_ARGS__) #define metamacro_at7(_0, _1, _2, _3, _4, _5, _6, ...) metamacro_head(__VA_ARGS__) #define metamacro_at8(_0, _1, _2, _3, _4, _5, _6, _7, ...) metamacro_head(__VA_ARGS__) #define metamacro_at9(_0, _1, _2, _3, _4, _5, _6, _7, _8, ...) metamacro_head(__VA_ARGS__) #define metamacro_at10(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, ...) metamacro_head(__VA_ARGS__) #define metamacro_at11(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, ...) metamacro_head(__VA_ARGS__) #define metamacro_at12(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, ...) metamacro_head(__VA_ARGS__) #define metamacro_at13(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, ...) metamacro_head(__VA_ARGS__) #define metamacro_at14(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, ...) metamacro_head(__VA_ARGS__) #define metamacro_at15(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, ...) metamacro_head(__VA_ARGS__) #define metamacro_at16(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, ...) metamacro_head(__VA_ARGS__) #define metamacro_at17(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, ...) metamacro_head(__VA_ARGS__) #define metamacro_at18(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, ...) metamacro_head(__VA_ARGS__) #define metamacro_at19(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, ...) metamacro_head(__VA_ARGS__) #define metamacro_at20(_0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19, ...) metamacro_head(__VA_ARGS__) // metamacro_foreach_cxt expansions #define metamacro_foreach_cxt0(MACRO, SEP, CONTEXT) #define metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0) #define metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \ metamacro_foreach_cxt1(MACRO, SEP, CONTEXT, _0) \ SEP \ MACRO(1, CONTEXT, _1) #define metamacro_foreach_cxt3(MACRO, SEP, CONTEXT, _0, _1, _2) \ metamacro_foreach_cxt2(MACRO, SEP, CONTEXT, _0, _1) \ SEP \ MACRO(2, CONTEXT, _2) #define metamacro_foreach_cxt4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \ metamacro_foreach_cxt3(MACRO, SEP, CONTEXT, _0, _1, _2) \ SEP \ MACRO(3, CONTEXT, _3) #define metamacro_foreach_cxt5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \ metamacro_foreach_cxt4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \ SEP \ MACRO(4, CONTEXT, _4) #define metamacro_foreach_cxt6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \ metamacro_foreach_cxt5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \ SEP \ MACRO(5, CONTEXT, _5) #define metamacro_foreach_cxt7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \ metamacro_foreach_cxt6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \ SEP \ MACRO(6, CONTEXT, _6) #define metamacro_foreach_cxt8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \ metamacro_foreach_cxt7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \ SEP \ MACRO(7, CONTEXT, _7) #define metamacro_foreach_cxt9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \ metamacro_foreach_cxt8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \ SEP \ MACRO(8, CONTEXT, _8) #define metamacro_foreach_cxt10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ metamacro_foreach_cxt9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \ SEP \ MACRO(9, CONTEXT, _9) #define metamacro_foreach_cxt11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \ metamacro_foreach_cxt10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ SEP \ MACRO(10, CONTEXT, _10) #define metamacro_foreach_cxt12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \ metamacro_foreach_cxt11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \ SEP \ MACRO(11, CONTEXT, _11) #define metamacro_foreach_cxt13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \ metamacro_foreach_cxt12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \ SEP \ MACRO(12, CONTEXT, _12) #define metamacro_foreach_cxt14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \ metamacro_foreach_cxt13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \ SEP \ MACRO(13, CONTEXT, _13) #define metamacro_foreach_cxt15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \ metamacro_foreach_cxt14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \ SEP \ MACRO(14, CONTEXT, _14) #define metamacro_foreach_cxt16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \ metamacro_foreach_cxt15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \ SEP \ MACRO(15, CONTEXT, _15) #define metamacro_foreach_cxt17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \ metamacro_foreach_cxt16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \ SEP \ MACRO(16, CONTEXT, _16) #define metamacro_foreach_cxt18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \ metamacro_foreach_cxt17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \ SEP \ MACRO(17, CONTEXT, _17) #define metamacro_foreach_cxt19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \ metamacro_foreach_cxt18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \ SEP \ MACRO(18, CONTEXT, _18) #define metamacro_foreach_cxt20(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19) \ metamacro_foreach_cxt19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \ SEP \ MACRO(19, CONTEXT, _19) // metamacro_foreach_cxt_recursive expansions #define metamacro_foreach_cxt_recursive0(MACRO, SEP, CONTEXT) #define metamacro_foreach_cxt_recursive1(MACRO, SEP, CONTEXT, _0) MACRO(0, CONTEXT, _0) #define metamacro_foreach_cxt_recursive2(MACRO, SEP, CONTEXT, _0, _1) \ metamacro_foreach_cxt_recursive1(MACRO, SEP, CONTEXT, _0) \ SEP \ MACRO(1, CONTEXT, _1) #define metamacro_foreach_cxt_recursive3(MACRO, SEP, CONTEXT, _0, _1, _2) \ metamacro_foreach_cxt_recursive2(MACRO, SEP, CONTEXT, _0, _1) \ SEP \ MACRO(2, CONTEXT, _2) #define metamacro_foreach_cxt_recursive4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \ metamacro_foreach_cxt_recursive3(MACRO, SEP, CONTEXT, _0, _1, _2) \ SEP \ MACRO(3, CONTEXT, _3) #define metamacro_foreach_cxt_recursive5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \ metamacro_foreach_cxt_recursive4(MACRO, SEP, CONTEXT, _0, _1, _2, _3) \ SEP \ MACRO(4, CONTEXT, _4) #define metamacro_foreach_cxt_recursive6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \ metamacro_foreach_cxt_recursive5(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4) \ SEP \ MACRO(5, CONTEXT, _5) #define metamacro_foreach_cxt_recursive7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \ metamacro_foreach_cxt_recursive6(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5) \ SEP \ MACRO(6, CONTEXT, _6) #define metamacro_foreach_cxt_recursive8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \ metamacro_foreach_cxt_recursive7(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6) \ SEP \ MACRO(7, CONTEXT, _7) #define metamacro_foreach_cxt_recursive9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \ metamacro_foreach_cxt_recursive8(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7) \ SEP \ MACRO(8, CONTEXT, _8) #define metamacro_foreach_cxt_recursive10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ metamacro_foreach_cxt_recursive9(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8) \ SEP \ MACRO(9, CONTEXT, _9) #define metamacro_foreach_cxt_recursive11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \ metamacro_foreach_cxt_recursive10(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9) \ SEP \ MACRO(10, CONTEXT, _10) #define metamacro_foreach_cxt_recursive12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \ metamacro_foreach_cxt_recursive11(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10) \ SEP \ MACRO(11, CONTEXT, _11) #define metamacro_foreach_cxt_recursive13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \ metamacro_foreach_cxt_recursive12(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11) \ SEP \ MACRO(12, CONTEXT, _12) #define metamacro_foreach_cxt_recursive14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \ metamacro_foreach_cxt_recursive13(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12) \ SEP \ MACRO(13, CONTEXT, _13) #define metamacro_foreach_cxt_recursive15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \ metamacro_foreach_cxt_recursive14(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13) \ SEP \ MACRO(14, CONTEXT, _14) #define metamacro_foreach_cxt_recursive16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \ metamacro_foreach_cxt_recursive15(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14) \ SEP \ MACRO(15, CONTEXT, _15) #define metamacro_foreach_cxt_recursive17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \ metamacro_foreach_cxt_recursive16(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15) \ SEP \ MACRO(16, CONTEXT, _16) #define metamacro_foreach_cxt_recursive18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \ metamacro_foreach_cxt_recursive17(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16) \ SEP \ MACRO(17, CONTEXT, _17) #define metamacro_foreach_cxt_recursive19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \ metamacro_foreach_cxt_recursive18(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17) \ SEP \ MACRO(18, CONTEXT, _18) #define metamacro_foreach_cxt_recursive20(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18, _19) \ metamacro_foreach_cxt_recursive19(MACRO, SEP, CONTEXT, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13, _14, _15, _16, _17, _18) \ SEP \ MACRO(19, CONTEXT, _19) // metamacro_for_cxt expansions #define metamacro_for_cxt0(MACRO, SEP, CONTEXT) #define metamacro_for_cxt1(MACRO, SEP, CONTEXT) MACRO(0, CONTEXT) #define metamacro_for_cxt2(MACRO, SEP, CONTEXT) \ metamacro_for_cxt1(MACRO, SEP, CONTEXT) \ SEP \ MACRO(1, CONTEXT) #define metamacro_for_cxt3(MACRO, SEP, CONTEXT) \ metamacro_for_cxt2(MACRO, SEP, CONTEXT) \ SEP \ MACRO(2, CONTEXT) #define metamacro_for_cxt4(MACRO, SEP, CONTEXT) \ metamacro_for_cxt3(MACRO, SEP, CONTEXT) \ SEP \ MACRO(3, CONTEXT) #define metamacro_for_cxt5(MACRO, SEP, CONTEXT) \ metamacro_for_cxt4(MACRO, SEP, CONTEXT) \ SEP \ MACRO(4, CONTEXT) #define metamacro_for_cxt6(MACRO, SEP, CONTEXT) \ metamacro_for_cxt5(MACRO, SEP, CONTEXT) \ SEP \ MACRO(5, CONTEXT) #define metamacro_for_cxt7(MACRO, SEP, CONTEXT) \ metamacro_for_cxt6(MACRO, SEP, CONTEXT) \ SEP \ MACRO(6, CONTEXT) #define metamacro_for_cxt8(MACRO, SEP, CONTEXT) \ metamacro_for_cxt7(MACRO, SEP, CONTEXT) \ SEP \ MACRO(7, CONTEXT) #define metamacro_for_cxt9(MACRO, SEP, CONTEXT) \ metamacro_for_cxt8(MACRO, SEP, CONTEXT) \ SEP \ MACRO(8, CONTEXT) #define metamacro_for_cxt10(MACRO, SEP, CONTEXT) \ metamacro_for_cxt9(MACRO, SEP, CONTEXT) \ SEP \ MACRO(9, CONTEXT) #define metamacro_for_cxt11(MACRO, SEP, CONTEXT) \ metamacro_for_cxt10(MACRO, SEP, CONTEXT) \ SEP \ MACRO(10, CONTEXT) #define metamacro_for_cxt12(MACRO, SEP, CONTEXT) \ metamacro_for_cxt11(MACRO, SEP, CONTEXT) \ SEP \ MACRO(11, CONTEXT) #define metamacro_for_cxt13(MACRO, SEP, CONTEXT) \ metamacro_for_cxt12(MACRO, SEP, CONTEXT) \ SEP \ MACRO(12, CONTEXT) #define metamacro_for_cxt14(MACRO, SEP, CONTEXT) \ metamacro_for_cxt13(MACRO, SEP, CONTEXT) \ SEP \ MACRO(13, CONTEXT) #define metamacro_for_cxt15(MACRO, SEP, CONTEXT) \ metamacro_for_cxt14(MACRO, SEP, CONTEXT) \ SEP \ MACRO(14, CONTEXT) #define metamacro_for_cxt16(MACRO, SEP, CONTEXT) \ metamacro_for_cxt15(MACRO, SEP, CONTEXT) \ SEP \ MACRO(15, CONTEXT) #define metamacro_for_cxt17(MACRO, SEP, CONTEXT) \ metamacro_for_cxt16(MACRO, SEP, CONTEXT) \ SEP \ MACRO(16, CONTEXT) #define metamacro_for_cxt18(MACRO, SEP, CONTEXT) \ metamacro_for_cxt17(MACRO, SEP, CONTEXT) \ SEP \ MACRO(17, CONTEXT) #define metamacro_for_cxt19(MACRO, SEP, CONTEXT) \ metamacro_for_cxt18(MACRO, SEP, CONTEXT) \ SEP \ MACRO(18, CONTEXT) #define metamacro_for_cxt20(MACRO, SEP, CONTEXT) \ metamacro_for_cxt19(MACRO, SEP, CONTEXT) \ SEP \ MACRO(19, CONTEXT) // metamacro_if_eq expansions #define metamacro_if_eq0(VALUE) \ metamacro_concat(metamacro_if_eq0_, VALUE) #define metamacro_if_eq0_0(...) __VA_ARGS__ metamacro_consume_ #define metamacro_if_eq0_1(...) metamacro_expand_ #define metamacro_if_eq0_2(...) metamacro_expand_ #define metamacro_if_eq0_3(...) metamacro_expand_ #define metamacro_if_eq0_4(...) metamacro_expand_ #define metamacro_if_eq0_5(...) metamacro_expand_ #define metamacro_if_eq0_6(...) metamacro_expand_ #define metamacro_if_eq0_7(...) metamacro_expand_ #define metamacro_if_eq0_8(...) metamacro_expand_ #define metamacro_if_eq0_9(...) metamacro_expand_ #define metamacro_if_eq0_10(...) metamacro_expand_ #define metamacro_if_eq0_11(...) metamacro_expand_ #define metamacro_if_eq0_12(...) metamacro_expand_ #define metamacro_if_eq0_13(...) metamacro_expand_ #define metamacro_if_eq0_14(...) metamacro_expand_ #define metamacro_if_eq0_15(...) metamacro_expand_ #define metamacro_if_eq0_16(...) metamacro_expand_ #define metamacro_if_eq0_17(...) metamacro_expand_ #define metamacro_if_eq0_18(...) metamacro_expand_ #define metamacro_if_eq0_19(...) metamacro_expand_ #define metamacro_if_eq0_20(...) metamacro_expand_ #define metamacro_if_eq1(VALUE) metamacro_if_eq0(metamacro_dec(VALUE)) #define metamacro_if_eq2(VALUE) metamacro_if_eq1(metamacro_dec(VALUE)) #define metamacro_if_eq3(VALUE) metamacro_if_eq2(metamacro_dec(VALUE)) #define metamacro_if_eq4(VALUE) metamacro_if_eq3(metamacro_dec(VALUE)) #define metamacro_if_eq5(VALUE) metamacro_if_eq4(metamacro_dec(VALUE)) #define metamacro_if_eq6(VALUE) metamacro_if_eq5(metamacro_dec(VALUE)) #define metamacro_if_eq7(VALUE) metamacro_if_eq6(metamacro_dec(VALUE)) #define metamacro_if_eq8(VALUE) metamacro_if_eq7(metamacro_dec(VALUE)) #define metamacro_if_eq9(VALUE) metamacro_if_eq8(metamacro_dec(VALUE)) #define metamacro_if_eq10(VALUE) metamacro_if_eq9(metamacro_dec(VALUE)) #define metamacro_if_eq11(VALUE) metamacro_if_eq10(metamacro_dec(VALUE)) #define metamacro_if_eq12(VALUE) metamacro_if_eq11(metamacro_dec(VALUE)) #define metamacro_if_eq13(VALUE) metamacro_if_eq12(metamacro_dec(VALUE)) #define metamacro_if_eq14(VALUE) metamacro_if_eq13(metamacro_dec(VALUE)) #define metamacro_if_eq15(VALUE) metamacro_if_eq14(metamacro_dec(VALUE)) #define metamacro_if_eq16(VALUE) metamacro_if_eq15(metamacro_dec(VALUE)) #define metamacro_if_eq17(VALUE) metamacro_if_eq16(metamacro_dec(VALUE)) #define metamacro_if_eq18(VALUE) metamacro_if_eq17(metamacro_dec(VALUE)) #define metamacro_if_eq19(VALUE) metamacro_if_eq18(metamacro_dec(VALUE)) #define metamacro_if_eq20(VALUE) metamacro_if_eq19(metamacro_dec(VALUE)) // metamacro_if_eq_recursive expansions #define metamacro_if_eq_recursive0(VALUE) \ metamacro_concat(metamacro_if_eq_recursive0_, VALUE) #define metamacro_if_eq_recursive0_0(...) __VA_ARGS__ metamacro_consume_ #define metamacro_if_eq_recursive0_1(...) metamacro_expand_ #define metamacro_if_eq_recursive0_2(...) metamacro_expand_ #define metamacro_if_eq_recursive0_3(...) metamacro_expand_ #define metamacro_if_eq_recursive0_4(...) metamacro_expand_ #define metamacro_if_eq_recursive0_5(...) metamacro_expand_ #define metamacro_if_eq_recursive0_6(...) metamacro_expand_ #define metamacro_if_eq_recursive0_7(...) metamacro_expand_ #define metamacro_if_eq_recursive0_8(...) metamacro_expand_ #define metamacro_if_eq_recursive0_9(...) metamacro_expand_ #define metamacro_if_eq_recursive0_10(...) metamacro_expand_ #define metamacro_if_eq_recursive0_11(...) metamacro_expand_ #define metamacro_if_eq_recursive0_12(...) metamacro_expand_ #define metamacro_if_eq_recursive0_13(...) metamacro_expand_ #define metamacro_if_eq_recursive0_14(...) metamacro_expand_ #define metamacro_if_eq_recursive0_15(...) metamacro_expand_ #define metamacro_if_eq_recursive0_16(...) metamacro_expand_ #define metamacro_if_eq_recursive0_17(...) metamacro_expand_ #define metamacro_if_eq_recursive0_18(...) metamacro_expand_ #define metamacro_if_eq_recursive0_19(...) metamacro_expand_ #define metamacro_if_eq_recursive0_20(...) metamacro_expand_ #define metamacro_if_eq_recursive1(VALUE) metamacro_if_eq_recursive0(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive2(VALUE) metamacro_if_eq_recursive1(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive3(VALUE) metamacro_if_eq_recursive2(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive4(VALUE) metamacro_if_eq_recursive3(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive5(VALUE) metamacro_if_eq_recursive4(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive6(VALUE) metamacro_if_eq_recursive5(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive7(VALUE) metamacro_if_eq_recursive6(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive8(VALUE) metamacro_if_eq_recursive7(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive9(VALUE) metamacro_if_eq_recursive8(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive10(VALUE) metamacro_if_eq_recursive9(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive11(VALUE) metamacro_if_eq_recursive10(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive12(VALUE) metamacro_if_eq_recursive11(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive13(VALUE) metamacro_if_eq_recursive12(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive14(VALUE) metamacro_if_eq_recursive13(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive15(VALUE) metamacro_if_eq_recursive14(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive16(VALUE) metamacro_if_eq_recursive15(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive17(VALUE) metamacro_if_eq_recursive16(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive18(VALUE) metamacro_if_eq_recursive17(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive19(VALUE) metamacro_if_eq_recursive18(metamacro_dec(VALUE)) #define metamacro_if_eq_recursive20(VALUE) metamacro_if_eq_recursive19(metamacro_dec(VALUE)) // metamacro_take expansions #define metamacro_take0(...) #define metamacro_take1(...) metamacro_head(__VA_ARGS__) #define metamacro_take2(...) metamacro_head(__VA_ARGS__), metamacro_take1(metamacro_tail(__VA_ARGS__)) #define metamacro_take3(...) metamacro_head(__VA_ARGS__), metamacro_take2(metamacro_tail(__VA_ARGS__)) #define metamacro_take4(...) metamacro_head(__VA_ARGS__), metamacro_take3(metamacro_tail(__VA_ARGS__)) #define metamacro_take5(...) metamacro_head(__VA_ARGS__), metamacro_take4(metamacro_tail(__VA_ARGS__)) #define metamacro_take6(...) metamacro_head(__VA_ARGS__), metamacro_take5(metamacro_tail(__VA_ARGS__)) #define metamacro_take7(...) metamacro_head(__VA_ARGS__), metamacro_take6(metamacro_tail(__VA_ARGS__)) #define metamacro_take8(...) metamacro_head(__VA_ARGS__), metamacro_take7(metamacro_tail(__VA_ARGS__)) #define metamacro_take9(...) metamacro_head(__VA_ARGS__), metamacro_take8(metamacro_tail(__VA_ARGS__)) #define metamacro_take10(...) metamacro_head(__VA_ARGS__), metamacro_take9(metamacro_tail(__VA_ARGS__)) #define metamacro_take11(...) metamacro_head(__VA_ARGS__), metamacro_take10(metamacro_tail(__VA_ARGS__)) #define metamacro_take12(...) metamacro_head(__VA_ARGS__), metamacro_take11(metamacro_tail(__VA_ARGS__)) #define metamacro_take13(...) metamacro_head(__VA_ARGS__), metamacro_take12(metamacro_tail(__VA_ARGS__)) #define metamacro_take14(...) metamacro_head(__VA_ARGS__), metamacro_take13(metamacro_tail(__VA_ARGS__)) #define metamacro_take15(...) metamacro_head(__VA_ARGS__), metamacro_take14(metamacro_tail(__VA_ARGS__)) #define metamacro_take16(...) metamacro_head(__VA_ARGS__), metamacro_take15(metamacro_tail(__VA_ARGS__)) #define metamacro_take17(...) metamacro_head(__VA_ARGS__), metamacro_take16(metamacro_tail(__VA_ARGS__)) #define metamacro_take18(...) metamacro_head(__VA_ARGS__), metamacro_take17(metamacro_tail(__VA_ARGS__)) #define metamacro_take19(...) metamacro_head(__VA_ARGS__), metamacro_take18(metamacro_tail(__VA_ARGS__)) #define metamacro_take20(...) metamacro_head(__VA_ARGS__), metamacro_take19(metamacro_tail(__VA_ARGS__)) // metamacro_drop expansions #define metamacro_drop0(...) __VA_ARGS__ #define metamacro_drop1(...) metamacro_tail(__VA_ARGS__) #define metamacro_drop2(...) metamacro_drop1(metamacro_tail(__VA_ARGS__)) #define metamacro_drop3(...) metamacro_drop2(metamacro_tail(__VA_ARGS__)) #define metamacro_drop4(...) metamacro_drop3(metamacro_tail(__VA_ARGS__)) #define metamacro_drop5(...) metamacro_drop4(metamacro_tail(__VA_ARGS__)) #define metamacro_drop6(...) metamacro_drop5(metamacro_tail(__VA_ARGS__)) #define metamacro_drop7(...) metamacro_drop6(metamacro_tail(__VA_ARGS__)) #define metamacro_drop8(...) metamacro_drop7(metamacro_tail(__VA_ARGS__)) #define metamacro_drop9(...) metamacro_drop8(metamacro_tail(__VA_ARGS__)) #define metamacro_drop10(...) metamacro_drop9(metamacro_tail(__VA_ARGS__)) #define metamacro_drop11(...) metamacro_drop10(metamacro_tail(__VA_ARGS__)) #define metamacro_drop12(...) metamacro_drop11(metamacro_tail(__VA_ARGS__)) #define metamacro_drop13(...) metamacro_drop12(metamacro_tail(__VA_ARGS__)) #define metamacro_drop14(...) metamacro_drop13(metamacro_tail(__VA_ARGS__)) #define metamacro_drop15(...) metamacro_drop14(metamacro_tail(__VA_ARGS__)) #define metamacro_drop16(...) metamacro_drop15(metamacro_tail(__VA_ARGS__)) #define metamacro_drop17(...) metamacro_drop16(metamacro_tail(__VA_ARGS__)) #define metamacro_drop18(...) metamacro_drop17(metamacro_tail(__VA_ARGS__)) #define metamacro_drop19(...) metamacro_drop18(metamacro_tail(__VA_ARGS__)) #define metamacro_drop20(...) metamacro_drop19(metamacro_tail(__VA_ARGS__)) #endif ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/UIColor+SDHexString.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "SDWebImageCompat.h" @interface UIColor (SDHexString) /** Convenience way to get hex string from color. The output should always be 32-bit RGBA hex string like `#00000000`. */ @property (nonatomic, copy, readonly, nonnull) NSString *sd_hexString; @end ================================================ FILE: Pods/SDWebImage/SDWebImage/Private/UIColor+SDHexString.m ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import "UIColor+SDHexString.h" @implementation UIColor (SDHexString) - (NSString *)sd_hexString { CGFloat red, green, blue, alpha; #if SD_UIKIT if (![self getRed:&red green:&green blue:&blue alpha:&alpha]) { [self getWhite:&red alpha:&alpha]; green = red; blue = red; } #else @try { [self getRed:&red green:&green blue:&blue alpha:&alpha]; } @catch (NSException *exception) { [self getWhite:&red alpha:&alpha]; green = red; blue = red; } #endif red = roundf(red * 255.f); green = roundf(green * 255.f); blue = roundf(blue * 255.f); alpha = roundf(alpha * 255.f); uint hex = ((uint)alpha << 24) | ((uint)red << 16) | ((uint)green << 8) | ((uint)blue); return [NSString stringWithFormat:@"#%08x", hex]; } @end ================================================ FILE: Pods/SDWebImage/WebImage/PrivacyInfo.xcprivacy ================================================ NSPrivacyTracking NSPrivacyCollectedDataTypes NSPrivacyTrackingDomains NSPrivacyAccessedAPITypes NSPrivacyAccessedAPIType NSPrivacyAccessedAPICategoryFileTimestamp NSPrivacyAccessedAPITypeReasons C617.1 ================================================ FILE: Pods/SDWebImage/WebImage/SDWebImage.h ================================================ /* * This file is part of the SDWebImage package. * (c) Olivier Poitrey * (c) Florent Vilmart * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ #import //! Project version number for SDWebImage. FOUNDATION_EXPORT double SDWebImageVersionNumber; //! Project version string for SDWebImage. FOUNDATION_EXPORT const unsigned char SDWebImageVersionString[]; // In this header, you should import all the public headers of your framework using statements like #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import // Mac #if __has_include() #import #endif #if __has_include() #import #endif #if __has_include() #import #endif // MapKit #if __has_include() #import #endif ================================================ FILE: Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 7.6.5 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-dummy.m ================================================ #import @interface PodsDummy_CocoaAsyncSocket : NSObject @end @implementation PodsDummy_CocoaAsyncSocket @end ================================================ FILE: Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-prefix.pch ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif ================================================ FILE: Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif #import "GCDAsyncSocket.h" #import "GCDAsyncUdpSocket.h" FOUNDATION_EXPORT double CocoaAsyncSocketVersionNumber; FOUNDATION_EXPORT const unsigned char CocoaAsyncSocketVersionString[]; ================================================ FILE: Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" -framework "Security" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/CocoaAsyncSocket PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket.modulemap ================================================ framework module CocoaAsyncSocket { umbrella header "CocoaAsyncSocket-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/CocoaAsyncSocket/CocoaAsyncSocket.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" -framework "Security" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/CocoaAsyncSocket PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 1.8.1 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet-dummy.m ================================================ #import @interface PodsDummy_DZNEmptyDataSet : NSObject @end @implementation PodsDummy_DZNEmptyDataSet @end ================================================ FILE: Pods/Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet-prefix.pch ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif ================================================ FILE: Pods/Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif #import "UIScrollView+EmptyDataSet.h" FOUNDATION_EXPORT double DZNEmptyDataSetVersionNumber; FOUNDATION_EXPORT const unsigned char DZNEmptyDataSetVersionString[]; ================================================ FILE: Pods/Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "UIKit" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/DZNEmptyDataSet PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet.modulemap ================================================ framework module DZNEmptyDataSet { umbrella header "DZNEmptyDataSet-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/DZNEmptyDataSet/DZNEmptyDataSet.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "UIKit" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/DZNEmptyDataSet PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Masonry/Masonry-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 1.1.0 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/Masonry/Masonry-dummy.m ================================================ #import @interface PodsDummy_Masonry : NSObject @end @implementation PodsDummy_Masonry @end ================================================ FILE: Pods/Target Support Files/Masonry/Masonry-prefix.pch ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif ================================================ FILE: Pods/Target Support Files/Masonry/Masonry-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif #import "MASCompositeConstraint.h" #import "MASConstraint+Private.h" #import "MASConstraint.h" #import "MASConstraintMaker.h" #import "MASLayoutConstraint.h" #import "Masonry.h" #import "MASUtilities.h" #import "MASViewAttribute.h" #import "MASViewConstraint.h" #import "NSArray+MASAdditions.h" #import "NSArray+MASShorthandAdditions.h" #import "NSLayoutConstraint+MASDebugAdditions.h" #import "View+MASAdditions.h" #import "View+MASShorthandAdditions.h" #import "ViewController+MASAdditions.h" FOUNDATION_EXPORT double MasonryVersionNumber; FOUNDATION_EXPORT const unsigned char MasonryVersionString[]; ================================================ FILE: Pods/Target Support Files/Masonry/Masonry.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Masonry GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "UIKit" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/Masonry PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Masonry/Masonry.modulemap ================================================ framework module Masonry { umbrella header "Masonry-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/Masonry/Masonry.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Masonry GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "Foundation" -framework "UIKit" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/Masonry PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/OCMock/OCMock-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 3.9.4 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/OCMock/OCMock-dummy.m ================================================ #import @interface PodsDummy_OCMock : NSObject @end @implementation PodsDummy_OCMock @end ================================================ FILE: Pods/Target Support Files/OCMock/OCMock-prefix.pch ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif ================================================ FILE: Pods/Target Support Files/OCMock/OCMock-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif #import "OCMock.h" #import "OCMockObject.h" #import "OCMArg.h" #import "OCMConstraint.h" #import "OCMLocation.h" #import "OCMMacroState.h" #import "OCMRecorder.h" #import "OCMStubRecorder.h" #import "NSNotificationCenter+OCMAdditions.h" #import "OCMFunctions.h" #import "OCMVerifier.h" #import "OCMQuantifier.h" #import "OCMockMacros.h" FOUNDATION_EXPORT double OCMockVersionNumber; FOUNDATION_EXPORT const unsigned char OCMockVersionString[]; ================================================ FILE: Pods/Target Support Files/OCMock/OCMock.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/OCMock ENABLE_BITCODE = NO FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 LIBRARY_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/usr/lib" OTHER_LDFLAGS = $(inherited) -framework "XCTest" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/OCMock PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES SWIFT_INCLUDE_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/usr/lib" SYSTEM_FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/OCMock/OCMock.modulemap ================================================ framework module OCMock { umbrella header "OCMock-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/OCMock/OCMock.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/OCMock ENABLE_BITCODE = NO FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 LIBRARY_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/usr/lib" OTHER_LDFLAGS = $(inherited) -framework "XCTest" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/OCMock PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES SWIFT_INCLUDE_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/usr/lib" SYSTEM_FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 1.0.0 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-acknowledgements.markdown ================================================ # Acknowledgements This application makes use of the following third party libraries: ## CocoaAsyncSocket Public Domain License The CocoaAsyncSocket project is in the public domain. The original TCP version (AsyncSocket) was created by Dustin Voss in January 2003. Updated and maintained by Deusty LLC and the Apple development community. ## DZNEmptyDataSet The MIT License (MIT) Copyright (c) 2016 Ignacio Romero Zurbuchen iromero@dzen.cl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Masonry Copyright (c) 2011-2012 Masonry Team - https://github.com/Masonry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Reachability Copyright (c) 2011, Tony Million. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## ReactiveObjC **Copyright (c) 2012 - 2016, GitHub, Inc.** **All rights reserved.** Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## SDWebImage Copyright (c) 2009-2020 Olivier Poitrey rs@dailymotion.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ## Typhoon Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ## YYModel The MIT License (MIT) Copyright (c) 2015 ibireme Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Generated by CocoaPods - https://cocoapods.org ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-acknowledgements.plist ================================================ PreferenceSpecifiers FooterText This application makes use of the following third party libraries: Title Acknowledgements Type PSGroupSpecifier FooterText Public Domain License The CocoaAsyncSocket project is in the public domain. The original TCP version (AsyncSocket) was created by Dustin Voss in January 2003. Updated and maintained by Deusty LLC and the Apple development community. License public domain Title CocoaAsyncSocket Type PSGroupSpecifier FooterText The MIT License (MIT) Copyright (c) 2016 Ignacio Romero Zurbuchen iromero@dzen.cl Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the 'Software'), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. License MIT Title DZNEmptyDataSet Type PSGroupSpecifier FooterText Copyright (c) 2011-2012 Masonry Team - https://github.com/Masonry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. License MIT Title Masonry Type PSGroupSpecifier FooterText Copyright (c) 2011, Tony Million. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. License BSD Title Reachability Type PSGroupSpecifier FooterText **Copyright (c) 2012 - 2016, GitHub, Inc.** **All rights reserved.** Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. License MIT Title ReactiveObjC Type PSGroupSpecifier FooterText Copyright (c) 2009-2020 Olivier Poitrey rs@dailymotion.com Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. License MIT Title SDWebImage Type PSGroupSpecifier FooterText Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. License Apache2.0 Title Typhoon Type PSGroupSpecifier FooterText The MIT License (MIT) Copyright (c) 2015 ibireme <ibireme@gmail.com> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. License MIT Title YYModel Type PSGroupSpecifier FooterText Generated by CocoaPods - https://cocoapods.org Title Type PSGroupSpecifier StringsTable Acknowledgements Title Acknowledgements ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-dummy.m ================================================ #import @interface PodsDummy_Pods_iOS_Network_Stack_Dive : NSObject @end @implementation PodsDummy_Pods_iOS_Network_Stack_Dive @end ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks-Debug-input-files.xcfilelist ================================================ ${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks.sh ${BUILT_PRODUCTS_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework ${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework ${BUILT_PRODUCTS_DIR}/Masonry/Masonry.framework ${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework ${BUILT_PRODUCTS_DIR}/ReactiveObjC/ReactiveObjC.framework ${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework ${BUILT_PRODUCTS_DIR}/Typhoon/Typhoon.framework ${BUILT_PRODUCTS_DIR}/YYModel/YYModel.framework ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks-Debug-output-files.xcfilelist ================================================ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaAsyncSocket.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Masonry.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactiveObjC.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Typhoon.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YYModel.framework ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks-Release-input-files.xcfilelist ================================================ ${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks.sh ${BUILT_PRODUCTS_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework ${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework ${BUILT_PRODUCTS_DIR}/Masonry/Masonry.framework ${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework ${BUILT_PRODUCTS_DIR}/ReactiveObjC/ReactiveObjC.framework ${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework ${BUILT_PRODUCTS_DIR}/Typhoon/Typhoon.framework ${BUILT_PRODUCTS_DIR}/YYModel/YYModel.framework ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks-Release-output-files.xcfilelist ================================================ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/CocoaAsyncSocket.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/DZNEmptyDataSet.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Masonry.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Reachability.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/ReactiveObjC.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SDWebImage.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Typhoon.framework ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/YYModel.framework ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks.sh ================================================ #!/bin/sh set -e set -u set -o pipefail function on_error { echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" } trap 'on_error $LINENO' ERR if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy # frameworks to, so exit 0 (signalling the script phase was successful). exit 0 fi echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" BCSYMBOLMAP_DIR="BCSymbolMaps" # This protects against multiple targets copying the same framework dependency at the same time. The solution # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") # Copies and strips a vendored framework install_framework() { if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then local source="${BUILT_PRODUCTS_DIR}/$1" elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" elif [ -r "$1" ]; then local source="$1" fi local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" if [ -L "${source}" ]; then echo "Symlinked..." source="$(readlink -f "${source}")" fi if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do echo "Installing $f" install_bcsymbolmap "$f" "$destination" rm "$f" done rmdir "${source}/${BCSYMBOLMAP_DIR}" fi # Use filter instead of exclude so missing patterns don't throw errors. echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" local basename basename="$(basename -s .framework "$1")" binary="${destination}/${basename}.framework/${basename}" if ! [ -r "$binary" ]; then binary="${destination}/${basename}" elif [ -L "${binary}" ]; then echo "Destination binary is symlinked..." dirname="$(dirname "${binary}")" binary="${dirname}/$(readlink "${binary}")" fi # Strip invalid architectures so "fat" simulator / device frameworks work on device if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then strip_invalid_archs "$binary" fi # Resign the code if required by the build settings to avoid unstable apps code_sign_if_enabled "${destination}/$(basename "$1")" # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then local swift_runtime_libs swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) for lib in $swift_runtime_libs; do echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" code_sign_if_enabled "${destination}/${lib}" done fi } # Copies and strips a vendored dSYM install_dsym() { local source="$1" warn_missing_arch=${2:-true} if [ -r "$source" ]; then # Copy the dSYM into the targets temp dir. echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" local basename basename="$(basename -s .dSYM "$source")" binary_name="$(ls "$source/Contents/Resources/DWARF")" binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" # Strip invalid architectures from the dSYM. if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then strip_invalid_archs "$binary" "$warn_missing_arch" fi if [[ $STRIP_BINARY_RETVAL == 0 ]]; then # Move the stripped file into its final destination. echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" else # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. mkdir -p "${DWARF_DSYM_FOLDER_PATH}" touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" fi fi } # Used as a return value for each invocation of `strip_invalid_archs` function. STRIP_BINARY_RETVAL=0 # Strip invalid architectures strip_invalid_archs() { binary="$1" warn_missing_arch=${2:-true} # Get architectures for current target binary binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" # Intersect them with the architectures we are building for intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" # If there are no archs supported by this binary then warn the user if [[ -z "$intersected_archs" ]]; then if [[ "$warn_missing_arch" == "true" ]]; then echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." fi STRIP_BINARY_RETVAL=1 return fi stripped="" for arch in $binary_archs; do if ! [[ "${ARCHS}" == *"$arch"* ]]; then # Strip non-valid architectures in-place lipo -remove "$arch" -output "$binary" "$binary" stripped="$stripped $arch" fi done if [[ "$stripped" ]]; then echo "Stripped $binary of architectures:$stripped" fi STRIP_BINARY_RETVAL=0 } # Copies the bcsymbolmap files of a vendored framework install_bcsymbolmap() { local bcsymbolmap_path="$1" local destination="${BUILT_PRODUCTS_DIR}" echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" } # Signs a framework with the provided identity code_sign_if_enabled() { if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then # Use the current code_sign_identity echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then code_sign_cmd="$code_sign_cmd &" fi echo "$code_sign_cmd" eval "$code_sign_cmd" fi } if [[ "$CONFIGURATION" == "Debug" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework" install_framework "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework" install_framework "${BUILT_PRODUCTS_DIR}/Masonry/Masonry.framework" install_framework "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework" install_framework "${BUILT_PRODUCTS_DIR}/ReactiveObjC/ReactiveObjC.framework" install_framework "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework" install_framework "${BUILT_PRODUCTS_DIR}/Typhoon/Typhoon.framework" install_framework "${BUILT_PRODUCTS_DIR}/YYModel/YYModel.framework" fi if [[ "$CONFIGURATION" == "Release" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework" install_framework "${BUILT_PRODUCTS_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework" install_framework "${BUILT_PRODUCTS_DIR}/Masonry/Masonry.framework" install_framework "${BUILT_PRODUCTS_DIR}/Reachability/Reachability.framework" install_framework "${BUILT_PRODUCTS_DIR}/ReactiveObjC/ReactiveObjC.framework" install_framework "${BUILT_PRODUCTS_DIR}/SDWebImage/SDWebImage.framework" install_framework "${BUILT_PRODUCTS_DIR}/Typhoon/Typhoon.framework" install_framework "${BUILT_PRODUCTS_DIR}/YYModel/YYModel.framework" fi if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then wait fi ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif FOUNDATION_EXPORT double Pods_iOS_Network_Stack_DiveVersionNumber; FOUNDATION_EXPORT const unsigned char Pods_iOS_Network_Stack_DiveVersionString[]; ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket" "${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability" "${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/Typhoon" "${PODS_CONFIGURATION_BUILD_DIR}/YYModel" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry/Masonry.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability/Reachability.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC/ReactiveObjC.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Typhoon/Typhoon.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YYModel/YYModel.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" -framework "CocoaAsyncSocket" -framework "CoreFoundation" -framework "DZNEmptyDataSet" -framework "Foundation" -framework "ImageIO" -framework "Masonry" -framework "Reachability" -framework "ReactiveObjC" -framework "SDWebImage" -framework "Security" -framework "SystemConfiguration" -framework "Typhoon" -framework "UIKit" -framework "YYModel" OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket" "-F${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet" "-F${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "-F${PODS_CONFIGURATION_BUILD_DIR}/Reachability" "-F${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC" "-F${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "-F${PODS_CONFIGURATION_BUILD_DIR}/Typhoon" "-F${PODS_CONFIGURATION_BUILD_DIR}/YYModel" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_PODFILE_DIR_PATH = ${SRCROOT}/. PODS_ROOT = ${SRCROOT}/Pods PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive.modulemap ================================================ framework module Pods_iOS_Network_Stack_Dive { umbrella header "Pods-iOS-Network-Stack-Dive-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket" "${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability" "${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/Typhoon" "${PODS_CONFIGURATION_BUILD_DIR}/YYModel" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry/Masonry.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability/Reachability.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC/ReactiveObjC.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Typhoon/Typhoon.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YYModel/YYModel.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" -framework "CocoaAsyncSocket" -framework "CoreFoundation" -framework "DZNEmptyDataSet" -framework "Foundation" -framework "ImageIO" -framework "Masonry" -framework "Reachability" -framework "ReactiveObjC" -framework "SDWebImage" -framework "Security" -framework "SystemConfiguration" -framework "Typhoon" -framework "UIKit" -framework "YYModel" OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket" "-F${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet" "-F${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "-F${PODS_CONFIGURATION_BUILD_DIR}/Reachability" "-F${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC" "-F${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "-F${PODS_CONFIGURATION_BUILD_DIR}/Typhoon" "-F${PODS_CONFIGURATION_BUILD_DIR}/YYModel" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_PODFILE_DIR_PATH = ${SRCROOT}/. PODS_ROOT = ${SRCROOT}/Pods PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 1.0.0 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-acknowledgements.markdown ================================================ # Acknowledgements This application makes use of the following third party libraries: ## OCMock Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS Generated by CocoaPods - https://cocoapods.org ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-acknowledgements.plist ================================================ PreferenceSpecifiers FooterText This application makes use of the following third party libraries: Title Acknowledgements Type PSGroupSpecifier FooterText Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS License Apache 2.0 Title OCMock Type PSGroupSpecifier FooterText Generated by CocoaPods - https://cocoapods.org Title Type PSGroupSpecifier StringsTable Acknowledgements Title Acknowledgements ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-dummy.m ================================================ #import @interface PodsDummy_Pods_iOS_Network_Stack_DiveTests : NSObject @end @implementation PodsDummy_Pods_iOS_Network_Stack_DiveTests @end ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks-Debug-input-files.xcfilelist ================================================ ${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks.sh ${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks-Debug-output-files.xcfilelist ================================================ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks-Release-input-files.xcfilelist ================================================ ${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks.sh ${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks-Release-output-files.xcfilelist ================================================ ${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks.sh ================================================ #!/bin/sh set -e set -u set -o pipefail function on_error { echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" } trap 'on_error $LINENO' ERR if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy # frameworks to, so exit 0 (signalling the script phase was successful). exit 0 fi echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" SWIFT_STDLIB_PATH="${TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" BCSYMBOLMAP_DIR="BCSymbolMaps" # This protects against multiple targets copying the same framework dependency at the same time. The solution # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") # Copies and strips a vendored framework install_framework() { if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then local source="${BUILT_PRODUCTS_DIR}/$1" elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" elif [ -r "$1" ]; then local source="$1" fi local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" if [ -L "${source}" ]; then echo "Symlinked..." source="$(readlink -f "${source}")" fi if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do echo "Installing $f" install_bcsymbolmap "$f" "$destination" rm "$f" done rmdir "${source}/${BCSYMBOLMAP_DIR}" fi # Use filter instead of exclude so missing patterns don't throw errors. echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" local basename basename="$(basename -s .framework "$1")" binary="${destination}/${basename}.framework/${basename}" if ! [ -r "$binary" ]; then binary="${destination}/${basename}" elif [ -L "${binary}" ]; then echo "Destination binary is symlinked..." dirname="$(dirname "${binary}")" binary="${dirname}/$(readlink "${binary}")" fi # Strip invalid architectures so "fat" simulator / device frameworks work on device if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then strip_invalid_archs "$binary" fi # Resign the code if required by the build settings to avoid unstable apps code_sign_if_enabled "${destination}/$(basename "$1")" # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then local swift_runtime_libs swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) for lib in $swift_runtime_libs; do echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" code_sign_if_enabled "${destination}/${lib}" done fi } # Copies and strips a vendored dSYM install_dsym() { local source="$1" warn_missing_arch=${2:-true} if [ -r "$source" ]; then # Copy the dSYM into the targets temp dir. echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" local basename basename="$(basename -s .dSYM "$source")" binary_name="$(ls "$source/Contents/Resources/DWARF")" binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" # Strip invalid architectures from the dSYM. if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then strip_invalid_archs "$binary" "$warn_missing_arch" fi if [[ $STRIP_BINARY_RETVAL == 0 ]]; then # Move the stripped file into its final destination. echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.dSYM" "${DWARF_DSYM_FOLDER_PATH}" else # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. mkdir -p "${DWARF_DSYM_FOLDER_PATH}" touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" fi fi } # Used as a return value for each invocation of `strip_invalid_archs` function. STRIP_BINARY_RETVAL=0 # Strip invalid architectures strip_invalid_archs() { binary="$1" warn_missing_arch=${2:-true} # Get architectures for current target binary binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" # Intersect them with the architectures we are building for intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" # If there are no archs supported by this binary then warn the user if [[ -z "$intersected_archs" ]]; then if [[ "$warn_missing_arch" == "true" ]]; then echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." fi STRIP_BINARY_RETVAL=1 return fi stripped="" for arch in $binary_archs; do if ! [[ "${ARCHS}" == *"$arch"* ]]; then # Strip non-valid architectures in-place lipo -remove "$arch" -output "$binary" "$binary" stripped="$stripped $arch" fi done if [[ "$stripped" ]]; then echo "Stripped $binary of architectures:$stripped" fi STRIP_BINARY_RETVAL=0 } # Copies the bcsymbolmap files of a vendored framework install_bcsymbolmap() { local bcsymbolmap_path="$1" local destination="${BUILT_PRODUCTS_DIR}" echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" } # Signs a framework with the provided identity code_sign_if_enabled() { if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then # Use the current code_sign_identity echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then code_sign_cmd="$code_sign_cmd &" fi echo "$code_sign_cmd" eval "$code_sign_cmd" fi } if [[ "$CONFIGURATION" == "Debug" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework" fi if [[ "$CONFIGURATION" == "Release" ]]; then install_framework "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework" fi if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then wait fi ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif FOUNDATION_EXPORT double Pods_iOS_Network_Stack_DiveTestsVersionNumber; FOUNDATION_EXPORT const unsigned char Pods_iOS_Network_Stack_DiveTestsVersionString[]; ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket" "${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "${PODS_CONFIGURATION_BUILD_DIR}/OCMock" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability" "${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/Typhoon" "${PODS_CONFIGURATION_BUILD_DIR}/YYModel" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry/Masonry.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/OCMock/OCMock.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability/Reachability.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC/ReactiveObjC.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Typhoon/Typhoon.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YYModel/YYModel.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" -framework "CocoaAsyncSocket" -framework "CoreFoundation" -framework "DZNEmptyDataSet" -framework "Foundation" -framework "ImageIO" -framework "Masonry" -framework "OCMock" -framework "Reachability" -framework "ReactiveObjC" -framework "SDWebImage" -framework "Security" -framework "SystemConfiguration" -framework "Typhoon" -framework "UIKit" -framework "XCTest" -framework "YYModel" OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/OCMock" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_PODFILE_DIR_PATH = ${SRCROOT}/. PODS_ROOT = ${SRCROOT}/Pods PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests.modulemap ================================================ framework module Pods_iOS_Network_Stack_DiveTests { umbrella header "Pods-iOS-Network-Stack-DiveTests-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO FRAMEWORK_SEARCH_PATHS = $(inherited) "$(PLATFORM_DIR)/Developer/Library/Frameworks" "${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket" "${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry" "${PODS_CONFIGURATION_BUILD_DIR}/OCMock" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability" "${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage" "${PODS_CONFIGURATION_BUILD_DIR}/Typhoon" "${PODS_CONFIGURATION_BUILD_DIR}/YYModel" GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/CocoaAsyncSocket/CocoaAsyncSocket.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/DZNEmptyDataSet/DZNEmptyDataSet.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Masonry/Masonry.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/OCMock/OCMock.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Reachability/Reachability.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC/ReactiveObjC.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage/SDWebImage.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Typhoon/Typhoon.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/YYModel/YYModel.framework/Headers" LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' OTHER_LDFLAGS = $(inherited) -framework "CFNetwork" -framework "CocoaAsyncSocket" -framework "CoreFoundation" -framework "DZNEmptyDataSet" -framework "Foundation" -framework "ImageIO" -framework "Masonry" -framework "OCMock" -framework "Reachability" -framework "ReactiveObjC" -framework "SDWebImage" -framework "Security" -framework "SystemConfiguration" -framework "Typhoon" -framework "UIKit" -framework "XCTest" -framework "YYModel" OTHER_MODULE_VERIFIER_FLAGS = $(inherited) "-F${PODS_CONFIGURATION_BUILD_DIR}/OCMock" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_PODFILE_DIR_PATH = ${SRCROOT}/. PODS_ROOT = ${SRCROOT}/Pods PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Reachability/Reachability-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 3.7.5 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/Reachability/Reachability-dummy.m ================================================ #import @interface PodsDummy_Reachability : NSObject @end @implementation PodsDummy_Reachability @end ================================================ FILE: Pods/Target Support Files/Reachability/Reachability-prefix.pch ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif ================================================ FILE: Pods/Target Support Files/Reachability/Reachability-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif #import "Reachability.h" FOUNDATION_EXPORT double ReachabilityVersionNumber; FOUNDATION_EXPORT const unsigned char ReachabilityVersionString[]; ================================================ FILE: Pods/Target Support Files/Reachability/Reachability.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Reachability GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "SystemConfiguration" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/Reachability PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Reachability/Reachability.modulemap ================================================ framework module Reachability { umbrella header "Reachability-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/Reachability/Reachability.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Reachability GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "SystemConfiguration" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/Reachability PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/ReactiveObjC/ReactiveObjC-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 3.1.1 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/ReactiveObjC/ReactiveObjC-dummy.m ================================================ #import @interface PodsDummy_ReactiveObjC : NSObject @end @implementation PodsDummy_ReactiveObjC @end ================================================ FILE: Pods/Target Support Files/ReactiveObjC/ReactiveObjC-prefix.pch ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif ================================================ FILE: Pods/Target Support Files/ReactiveObjC/ReactiveObjC-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif #import "MKAnnotationView+RACSignalSupport.h" #import "NSArray+RACSequenceAdditions.h" #import "NSData+RACSupport.h" #import "NSDictionary+RACSequenceAdditions.h" #import "NSEnumerator+RACSequenceAdditions.h" #import "NSFileHandle+RACSupport.h" #import "NSIndexSet+RACSequenceAdditions.h" #import "NSInvocation+RACTypeParsing.h" #import "NSNotificationCenter+RACSupport.h" #import "NSObject+RACDeallocating.h" #import "NSObject+RACDescription.h" #import "NSObject+RACKVOWrapper.h" #import "NSObject+RACLifting.h" #import "NSObject+RACPropertySubscribing.h" #import "NSObject+RACSelectorSignal.h" #import "NSOrderedSet+RACSequenceAdditions.h" #import "NSSet+RACSequenceAdditions.h" #import "NSString+RACKeyPathUtilities.h" #import "NSString+RACSequenceAdditions.h" #import "NSString+RACSupport.h" #import "NSURLConnection+RACSupport.h" #import "NSUserDefaults+RACSupport.h" #import "RACAnnotations.h" #import "RACArraySequence.h" #import "RACBehaviorSubject.h" #import "RACBlockTrampoline.h" #import "RACChannel.h" #import "RACCommand.h" #import "RACCompoundDisposable.h" #import "RACDelegateProxy.h" #import "RACDisposable.h" #import "RACDynamicSequence.h" #import "RACDynamicSignal.h" #import "RACEagerSequence.h" #import "RACErrorSignal.h" #import "RACEvent.h" #import "RACGroupedSignal.h" #import "RACImmediateScheduler.h" #import "RACIndexSetSequence.h" #import "RACKVOChannel.h" #import "RACKVOProxy.h" #import "RACKVOTrampoline.h" #import "RACMulticastConnection.h" #import "RACPassthroughSubscriber.h" #import "RACQueueScheduler+Subclass.h" #import "RACQueueScheduler.h" #import "RACReplaySubject.h" #import "RACReturnSignal.h" #import "RACScheduler+Subclass.h" #import "RACScheduler.h" #import "RACScopedDisposable.h" #import "RACSequence.h" #import "RACSerialDisposable.h" #import "RACSignal+Operations.h" #import "RACSignal.h" #import "RACSignalSequence.h" #import "RACStream.h" #import "RACStringSequence.h" #import "RACSubject.h" #import "RACSubscriber.h" #import "RACSubscriptingAssignmentTrampoline.h" #import "RACSubscriptionScheduler.h" #import "RACTargetQueueScheduler.h" #import "RACTestScheduler.h" #import "RACTuple.h" #import "RACTupleSequence.h" #import "RACUnarySequence.h" #import "RACUnit.h" #import "RACValueTransformer.h" #import "ReactiveObjC.h" #import "UIActionSheet+RACSignalSupport.h" #import "UIAlertView+RACSignalSupport.h" #import "UIBarButtonItem+RACCommandSupport.h" #import "UIButton+RACCommandSupport.h" #import "UICollectionReusableView+RACSignalSupport.h" #import "UIControl+RACSignalSupport.h" #import "UIDatePicker+RACSignalSupport.h" #import "UIGestureRecognizer+RACSignalSupport.h" #import "UIImagePickerController+RACSignalSupport.h" #import "UIRefreshControl+RACCommandSupport.h" #import "UISegmentedControl+RACSignalSupport.h" #import "UISlider+RACSignalSupport.h" #import "UIStepper+RACSignalSupport.h" #import "UISwitch+RACSignalSupport.h" #import "UITableViewCell+RACSignalSupport.h" #import "UITableViewHeaderFooterView+RACSignalSupport.h" #import "UITextField+RACSignalSupport.h" #import "UITextView+RACSignalSupport.h" #import "RACEXTKeyPathCoding.h" #import "RACEXTScope.h" #import "RACmetamacros.h" FOUNDATION_EXPORT double ReactiveObjCVersionNumber; FOUNDATION_EXPORT const unsigned char ReactiveObjCVersionString[]; ================================================ FILE: Pods/Target Support Files/ReactiveObjC/ReactiveObjC.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "Foundation" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/ReactiveObjC PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/ReactiveObjC/ReactiveObjC.modulemap ================================================ framework module ReactiveObjC { umbrella header "ReactiveObjC-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/ReactiveObjC/ReactiveObjC.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/ReactiveObjC GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "Foundation" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/ReactiveObjC PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/SDWebImage/ResourceBundle-SDWebImage-SDWebImage-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType BNDL CFBundleShortVersionString 5.21.1 CFBundleSignature ???? CFBundleVersion 1 NSPrincipalClass ================================================ FILE: Pods/Target Support Files/SDWebImage/SDWebImage-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 5.21.1 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/SDWebImage/SDWebImage-dummy.m ================================================ #import @interface PodsDummy_SDWebImage : NSObject @end @implementation PodsDummy_SDWebImage @end ================================================ FILE: Pods/Target Support Files/SDWebImage/SDWebImage-prefix.pch ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif ================================================ FILE: Pods/Target Support Files/SDWebImage/SDWebImage-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif #import "NSButton+WebCache.h" #import "NSData+ImageContentType.h" #import "NSImage+Compatibility.h" #import "SDAnimatedImage.h" #import "SDAnimatedImagePlayer.h" #import "SDAnimatedImageRep.h" #import "SDAnimatedImageView+WebCache.h" #import "SDAnimatedImageView.h" #import "SDCallbackQueue.h" #import "SDDiskCache.h" #import "SDGraphicsImageRenderer.h" #import "SDImageAPNGCoder.h" #import "SDImageAWebPCoder.h" #import "SDImageCache.h" #import "SDImageCacheConfig.h" #import "SDImageCacheDefine.h" #import "SDImageCachesManager.h" #import "SDImageCoder.h" #import "SDImageCoderHelper.h" #import "SDImageCodersManager.h" #import "SDImageFrame.h" #import "SDImageGIFCoder.h" #import "SDImageGraphics.h" #import "SDImageHEICCoder.h" #import "SDImageIOAnimatedCoder.h" #import "SDImageIOCoder.h" #import "SDImageLoader.h" #import "SDImageLoadersManager.h" #import "SDImageTransformer.h" #import "SDMemoryCache.h" #import "SDWebImageCacheKeyFilter.h" #import "SDWebImageCacheSerializer.h" #import "SDWebImageCompat.h" #import "SDWebImageDefine.h" #import "SDWebImageDownloader.h" #import "SDWebImageDownloaderConfig.h" #import "SDWebImageDownloaderDecryptor.h" #import "SDWebImageDownloaderOperation.h" #import "SDWebImageDownloaderRequestModifier.h" #import "SDWebImageDownloaderResponseModifier.h" #import "SDWebImageError.h" #import "SDWebImageIndicator.h" #import "SDWebImageManager.h" #import "SDWebImageOperation.h" #import "SDWebImageOptionsProcessor.h" #import "SDWebImagePrefetcher.h" #import "SDWebImageTransition.h" #import "UIButton+WebCache.h" #import "UIImage+ExtendedCacheData.h" #import "UIImage+ForceDecode.h" #import "UIImage+GIF.h" #import "UIImage+MemoryCacheCost.h" #import "UIImage+Metadata.h" #import "UIImage+MultiFormat.h" #import "UIImage+Transform.h" #import "UIImageView+HighlightedWebCache.h" #import "UIImageView+WebCache.h" #import "UIView+WebCache.h" #import "UIView+WebCacheOperation.h" #import "UIView+WebCacheState.h" #import "SDWebImage.h" FOUNDATION_EXPORT double SDWebImageVersionNumber; FOUNDATION_EXPORT const unsigned char SDWebImageVersionString[]; ================================================ FILE: Pods/Target Support Files/SDWebImage/SDWebImage.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = NO GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "ImageIO" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/SDWebImage PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES SUPPORTS_MACCATALYST = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/SDWebImage/SDWebImage.modulemap ================================================ framework module SDWebImage { umbrella header "SDWebImage-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/SDWebImage/SDWebImage.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SDWebImage DERIVE_MACCATALYST_PRODUCT_BUNDLE_IDENTIFIER = NO GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "ImageIO" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/SDWebImage PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES SUPPORTS_MACCATALYST = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Typhoon/Typhoon-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 4.0.9 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/Typhoon/Typhoon-dummy.m ================================================ #import @interface PodsDummy_Typhoon : NSObject @end @implementation PodsDummy_Typhoon @end ================================================ FILE: Pods/Target Support Files/Typhoon/Typhoon-prefix.pch ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif ================================================ FILE: Pods/Target Support Files/Typhoon/Typhoon-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif #import "TyphoonConfigPostProcessor+Internal.h" #import "TyphoonConfigPostProcessor.h" #import "TyphoonConfiguration.h" #import "TyphoonJsonStyleConfiguration.h" #import "TyphoonPlistStyleConfiguration.h" #import "TyphoonPropertyStyleConfiguration.h" #import "TyphoonDefinition+Config.h" #import "TyphoonOptionMatcher+Internal.h" #import "TyphoonOptionMatcher.h" #import "TyphoonDefinition+Option.h" #import "TyphoonGlobalConfigCollector.h" #import "TyphoonBundleResource.h" #import "TyphoonPathResource.h" #import "TyphoonResource.h" #import "TyphoonStartup.h" #import "TyphoonAbstractDetachableComponentFactoryPostProcessor.h" #import "TyphoonDefinitionPostProcessor.h" #import "TyphoonInstancePostProcessor.h" #import "TyphoonOrdered.h" #import "TyphoonAutoInjection.h" #import "TyphoonFactoryAutoInjectionPostProcessor.h" #import "TyphoonInjectedObject.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonInjectionsEnumeration.h" #import "TyphoonAbstractInjection.h" #import "TyphoonInject.h" #import "TyphoonInjection.h" #import "TyphoonInjectionByCollection.h" #import "TyphoonInjectionByComponentFactory.h" #import "TyphoonInjectionByConfig.h" #import "TyphoonInjectionByCurrentRuntimeArguments.h" #import "TyphoonInjectionByDictionary.h" #import "TyphoonInjectionByFactoryReference.h" #import "TyphoonInjectionByObjectFromString.h" #import "TyphoonInjectionByObjectInstance.h" #import "TyphoonInjectionByReference.h" #import "TyphoonInjectionByRuntimeArgument.h" #import "TyphoonInjectionByType.h" #import "TyphoonInjectionContext.h" #import "TyphoonInjections.h" #import "TyphoonParameterInjection.h" #import "TyphoonPropertyInjection.h" #import "Collections+CustomInjection.h" #import "NSDictionary+CustomInjection.h" #import "TyphoonBlockDefinition+InstanceBuilder.h" #import "TyphoonBlockDefinition+Internal.h" #import "TyphoonBlockDefinitionController.h" #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonDefinition+Internal.h" #import "TyphoonFactoryDefinition.h" #import "TyphoonInjectionDefinition.h" #import "TyphoonObjectWithCustomInjection.h" #import "TyphoonReferenceDefinition.h" #import "TyphoonMethod+InstanceBuilder.h" #import "TyphoonMethod.h" #import "TyphoonDefinitionNamespace.h" #import "TyphoonBlockDefinition.h" #import "TyphoonDefinition.h" #import "TyphoonAssembly+TyphoonAssemblyFriend.h" #import "TyphoonAssembly.h" #import "TyphoonAssemblyAccessor.h" #import "NSObject+FactoryHooks.h" #import "NSInvocation+TCFCustomImplementation.h" #import "NSInvocation+TCFUnwrapValues.h" #import "NSInvocation+TCFWrapValues.h" #import "NSMethodSignature+TCFUnwrapValues.h" #import "NSValue+TCFUnwrapValues.h" #import "TyphoonAssemblyAdviser.h" #import "TyphoonAssemblyBuilder+PlistProcessor.h" #import "TyphoonAssemblyBuilder.h" #import "TyphoonAssemblyDefinitionBuilder.h" #import "TyphoonAssemblyPropertyInjectionPostProcessor.h" #import "TyphoonAssemblySelectorAdviser.h" #import "TyphoonBlockComponentFactory.h" #import "TyphoonCallStack.h" #import "TyphoonCircularDependencyTerminator.h" #import "TyphoonCollaboratingAssembliesCollector.h" #import "TyphoonCollaboratingAssemblyPropertyEnumerator.h" #import "TyphoonCollaboratingAssemblyProxy.h" #import "TyphoonComponentFactory+InstanceBuilder.h" #import "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h" #import "TyphoonFactoryPropertyInjectionPostProcessor.h" #import "TyphoonMemoryManagementUtils.h" #import "TyphoonParentReferenceHydratingPostProcessor.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonStackElement.h" #import "TyphoonComponentsPool.h" #import "TyphoonWeakComponentsPool.h" #import "TyphoonComponentFactory.h" #import "TyphoonDefinitionRegisterer.h" #import "TyphoonPreattachedComponentsRegisterer.h" #import "TyphoonViewControllerInjector.h" #import "TyphoonStoryboardDefinition.h" #import "TyphoonStoryboardDefinitionContext.h" #import "TyphoonNibLoader.h" #import "TyphoonViewControllerNibResolver.h" #import "UIView+TyphoonDefinitionKey.h" #import "UIViewController+TyphoonStoryboardIntegration.h" #import "NSLayoutConstraint+TyphoonOutletTransfer.h" #import "TyphoonComponentFactory+Storyboard.h" #import "TyphoonDefinition+Storyboard.h" #import "TyphoonLoadedView.h" #import "TyphoonStoryboard.h" #import "TyphoonStoryboardProvider.h" #import "TyphoonStoryboardResolver.h" #import "TyphoonViewControllerFactory.h" #import "TyphoonViewHelpers.h" #import "UIResponder+TyphoonOutletTransfer.h" #import "UIView+TyphoonOutletTransfer.h" #import "TyphoonBundledImageTypeConverter.h" #import "TyphoonUIColorTypeConverter.h" #import "TyphooniOS.h" #import "TyphoonPatcher.h" #import "TyphoonTestUtils.h" #import "NSNullTypeConverter.h" #import "TyphoonNSNumberTypeConverter.h" #import "TyphoonNSURLTypeConverter.h" #import "TyphoonPassThroughTypeConverter.h" #import "TyphoonPrimitiveTypeConverter.h" #import "TyphoonColorConversionUtils.h" #import "TyphoonTypeConversionUtils.h" #import "TyphoonTypeConverter.h" #import "TyphoonTypeConverterRegistry.h" #import "TyphoonTypeDescriptor.h" #import "Typhoon+Infrastructure.h" #import "Typhoon.h" #import "NSArray+TyphoonManualEnumeration.h" #import "NSObject+DeallocNotification.h" #import "NSObject+PropertyInjection.h" #import "NSObject+TyphoonIntrospectionUtils.h" #import "TyphoonMethodSwizzler.h" #import "TyphoonSwizzlerDefaultImpl.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonLinkerCategoryBugFix.h" #import "TyphoonSelector.h" #import "TyphoonUtils.h" #import "OCLogTemplate.h" #import "NSObject+DeallocNotification.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonUtils.h" #import "NSInvocation+TCFInstanceBuilder.h" FOUNDATION_EXPORT double TyphoonVersionNumber; FOUNDATION_EXPORT const unsigned char TyphoonVersionString[]; ================================================ FILE: Pods/Target Support Files/Typhoon/Typhoon.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Typhoon GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/Typhoon PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/Typhoon/Typhoon.modulemap ================================================ framework module Typhoon { umbrella header "Typhoon-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/Typhoon/Typhoon.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Typhoon GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/Typhoon PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/YYModel/YYModel-Info.plist ================================================ CFBundleDevelopmentRegion ${PODS_DEVELOPMENT_LANGUAGE} CFBundleExecutable ${EXECUTABLE_NAME} CFBundleIdentifier ${PRODUCT_BUNDLE_IDENTIFIER} CFBundleInfoDictionaryVersion 6.0 CFBundleName ${PRODUCT_NAME} CFBundlePackageType FMWK CFBundleShortVersionString 1.0.4 CFBundleSignature ???? CFBundleVersion ${CURRENT_PROJECT_VERSION} NSPrincipalClass ================================================ FILE: Pods/Target Support Files/YYModel/YYModel-dummy.m ================================================ #import @interface PodsDummy_YYModel : NSObject @end @implementation PodsDummy_YYModel @end ================================================ FILE: Pods/Target Support Files/YYModel/YYModel-prefix.pch ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif ================================================ FILE: Pods/Target Support Files/YYModel/YYModel-umbrella.h ================================================ #ifdef __OBJC__ #import #else #ifndef FOUNDATION_EXPORT #if defined(__cplusplus) #define FOUNDATION_EXPORT extern "C" #else #define FOUNDATION_EXPORT extern #endif #endif #endif #import "NSObject+YYModel.h" #import "YYClassInfo.h" #import "YYModel.h" FOUNDATION_EXPORT double YYModelVersionNumber; FOUNDATION_EXPORT const unsigned char YYModelVersionString[]; ================================================ FILE: Pods/Target Support Files/YYModel/YYModel.debug.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/YYModel GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "CoreFoundation" -framework "Foundation" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/YYModel PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Target Support Files/YYModel/YYModel.modulemap ================================================ framework module YYModel { umbrella header "YYModel-umbrella.h" export * module * { export * } } ================================================ FILE: Pods/Target Support Files/YYModel/YYModel.release.xcconfig ================================================ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/YYModel GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 OTHER_LDFLAGS = $(inherited) -framework "CoreFoundation" -framework "Foundation" PODS_BUILD_DIR = ${BUILD_DIR} PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} PODS_ROOT = ${SRCROOT} PODS_TARGET_SRCROOT = ${PODS_ROOT}/YYModel PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} SKIP_INSTALL = YES USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES ================================================ FILE: Pods/Typhoon/LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Pods/Typhoon/README.md ================================================ ![Typhoon](http://appsquickly.github.io/typhoon/typhoon-splash.png) # Typhoon Powerful dependency injection for Cocoa and CocoaTouch. Lightweight, yet full-featured and super-easy to use. ## Not familiar with Dependency Injection? Visit the Typhoon website for an introduction. There's also a nice intro over at Big Nerd Ranch, or here's an article, by John Reid. Quite a few books have been written on the topic, though we're not familiar with one that focuses specifically on Objective-C, Swift or Cocoa yet. ## Is Typhoon the right DI framework for you? Check out the feature list. ### Looking for a pure Swift Solution? Typhoon uses the Objective-C runtime to collect metadata and instantiate objects. It powers thousands of Objective-C applications and is also pretty popular for Swift. Nonetheless there are some advantages to using a pure Swift library. * Weaver is a good compile time DI library for Swift. * Fiery Crucible is also an excellent light-weight (just one file) and very straight-forward DI library for Swift. Both of the above solutions have the 'ObjectGraph' scope (you can read more about it in the docs), which provides a way to assemble a complex object-graph from a blue-print and then retain it as long as needed. This scope was introduced by Typhoon, and is an important consideration for mobile and desktop apps. Moreover, scope management is one of the main advantages to simply applying the DI pattern 'by hand'. Please think carefully before choosing a DI library that forces you to write complex adapters, modify your code or tightly couple it to a library. It shouldn't be more complicated than understanding and applying the pattern without a supporting framework. --------------------------------------- # Usage * Read the ***Quick Start*** for Objective-C or Swift. * Here's the User Guide. * 日本のドキュメンテーション ```swift let assembly = MyAssembly().activated() let viewController = assembly.recommendationController() as! RecommendationController ``` # Open Source Sample Applications * Try the official Swift Sample Application or Objective-C Sample Application. * This sample shows how to set up Typhoon with Storyboards, Core Data and Reactive Cocoa. *Have a Typhoon example app that you'd like to share? Great! Get in touch with us :)* # Installing [![Build Status](https://travis-ci.org/appsquickly/typhoon.svg?branch=master)](https://travis-ci.org/appsquickly/typhoon) [![codecov](https://codecov.io/gh/appsquickly/Typhoon/branch/master/graph/badge.svg)](https://codecov.io/gh/appsquickly/Typhoon) ![CocoaPods Version](https://cocoapod-badges.herokuapp.com/v/Typhoon/badge.png) [![Pod Platform](https://img.shields.io/cocoapods/p/Typhoon.svg?style=flat)](http://appsquickly.github.io/Typhoon/docs/latest/api/modules.html) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) [![Dependency Status](https://www.versioneye.com/objective-c/typhoon/1.1.1/badge.svg?style=flat)](https://www.versioneye.com/objective-c/typhoon) [![Pod License](https://img.shields.io/cocoapods/l/Typhoon.svg?style=flat)](https://github.com/appsquickly/typhoon/blob/master/LICENSE) Typhoon is available through CocoaPods or Carthage, and also builds easily from source. ## With CocoaPods . . . ### Static Library ```ruby # platform *must* be at least 5.0 platform :ios, '5.0' target :MyAppTarget, :exclusive => true do pod 'Typhoon' end ``` ### Dynamic Framework If you're using Swift, you may wish to install dynamic frameworks, which can be done with the Podfile shown below: ```ruby # platform *must* be at least 8.0 platform :ios, '8.0' # flag makes all dependencies build as frameworks use_frameworks! # framework dependencies pod 'Typhoon' ``` Simply import the Typhoon module in any Swift file that uses the framework: ```Swift import Typhoon ``` ## With Carthage ``` github "appsquickly/Typhoon" ``` ## From Source Alternatively, add the source files to your project's target or set up an Xcode workspace. **NB:** *All versions of Typhoon work with iOS5 and up (and OSX 10.7 and up), iOS8 is only required if you wish to use dynamic frameworks.* --------------------------------------- # Feedback ### I'm not sure how to do [xyz] If you can't find what you need in the Quick Start or User Guides above, then Typhoon users and contributors monitor the Typhoon tag on Stack Overflow. Chances are your question can be answered there. ### I've found a bug, or have a feature request Please raise a GitHub issue. ### Interested in contributing? Great! Here's the contribution guide. ### I'm blown away! Typhoon is a non-profit, community driven project. We only ask that if you've found it useful to star us on Github or send a tweet mentioning us (@appsquickly). If you've written a Typhoon related blog or tutorial, or published a new Typhoon powered app, we'd certainly be happy to hear about that too. Typhoon is sponsored and led by AppsQuick.ly with contributions from around the world. --------------------------------------- © 2012 - 2015 Jasper Blues, Aleksey Garbarev and contributors. ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/Internal/TyphoonConfigPostProcessor+Internal.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonConfigPostProcessor.h" @protocol TyphoonInjection; @class TyphoonInjectionContext; @class TyphoonInjectionByConfig; @interface TyphoonConfigPostProcessor () - (BOOL)shouldInjectDefinition:(TyphoonDefinition *)definition; - (id)injectionForConfigInjection:(TyphoonInjectionByConfig *)injection; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonConfigPostProcessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinitionPostProcessor.h" @protocol TyphoonResource; @class TyphoonDefinitionNamespace; /** * @ingroup Configuration */ @interface TyphoonConfigPostProcessor : NSObject + (instancetype)processor; /** * Returns a post processor for the bundle resource with the given name. */ + (instancetype)forResourceNamed:(NSString *)resourceName; /** * Returns a post processor for the bundle resource with the given name and bundle. */ + (instancetype)forResourceNamed:(NSString *)resourceName inBundle:(NSBundle *)bundle; /** * Returns a post processor for the resource at the specified path. */ + (instancetype)forResourceAtPath:(NSString *)path; /** * You can manage TyphoonConfigPostProcessor registry by mapping configuration classes for file extensions * Configuration class instance must conforms TyphoonConfiguration protocol * */ + (void)registerConfigurationClass:(Class)configClass forExtension:(NSString *)typeExtension; /** list of all supported path extensions (configuration types) */ + (NSArray *)availableExtensions; /** Registers a namespace for this config post processor */ - (void)registerNamespace:(TyphoonDefinitionNamespace *)space; /** Append resource found in main bundle by name */ - (void)useResourceWithName:(NSString *)name; /** Append resource found by name and bundle */ - (void)useResourceWithName:(NSString *)name bundle:(NSBundle *)bundle; /** Append resource loaded from file at path */ - (void)useResourceAtPath:(NSString *)path; /** Append TyphoonResource with specified extension (@see availableExtensions method) */ - (void)useResource:(id)resource withExtension:(NSString *)typeExtension; @end id TyphoonConfig(NSString *configKey); ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonConfigPostProcessor.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonConfigPostProcessor.h" #import "TyphoonConfigPostProcessor+Internal.h" #import "TyphoonResource.h" #import "TyphoonDefinition.h" #import "TyphoonInjectionByConfig.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonPropertyStyleConfiguration.h" #import "TyphoonInjections.h" #import "TyphoonJsonStyleConfiguration.h" #import "TyphoonBundleResource.h" #import "TyphoonPlistStyleConfiguration.h" #import "TyphoonInjectionByReference.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonDefinitionNamespace.h" #import "TyphoonInject.h" #import "OCLogTemplate.h" static NSMutableDictionary *propertyPlaceholderRegistry; @implementation TyphoonConfigPostProcessor { NSDictionary *_configs; TyphoonDefinitionNamespace *_space; } //------------------------------------------------------------------------------------------- #pragma mark - Class Methods //------------------------------------------------------------------------------------------- + (TyphoonConfigPostProcessor *)processor { return [[self alloc] init]; } + (TyphoonConfigPostProcessor *)forResourceNamed:(NSString *)resourceName { TyphoonConfigPostProcessor *processor = [[TyphoonConfigPostProcessor alloc] init]; [processor useResourceWithName:resourceName]; return processor; } + (TyphoonConfigPostProcessor *)forResourceNamed:(NSString *)resourceName inBundle:(NSBundle *)bundle { TyphoonConfigPostProcessor *processor = [[TyphoonConfigPostProcessor alloc] init]; [processor useResourceWithName:resourceName bundle:bundle]; return processor; } + (TyphoonConfigPostProcessor *)forResourceAtPath:(NSString *)path { TyphoonConfigPostProcessor *processor = [[TyphoonConfigPostProcessor alloc] init]; [processor useResourceAtPath:path]; return processor; } + (void)registerConfigurationClass:(Class)configClass forExtension:(NSString *)typeExtension { @synchronized (self) { if (!propertyPlaceholderRegistry) { propertyPlaceholderRegistry = [NSMutableDictionary new]; } propertyPlaceholderRegistry[typeExtension] = configClass; } } + (NSArray *)availableExtensions { return [propertyPlaceholderRegistry allKeys]; } //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction //------------------------------------------------------------------------------------------- - (id)init { self = [super init]; if (self) { NSMutableDictionary *mutableConfigs = [[NSMutableDictionary alloc] initWithCapacity:[propertyPlaceholderRegistry count]]; [propertyPlaceholderRegistry enumerateKeysAndObjectsUsingBlock:^(NSString *key, id configClass, BOOL *stop) { mutableConfigs[key] = [configClass new]; }]; _configs = mutableConfigs; // Each ConfigPostProcessor has global namespace unless explicitly set to another namespace. _space = [TyphoonDefinitionNamespace globalNamespace]; } return self; } + (void)initialize { [super initialize]; [self registerConfigurationClass:[TyphoonJsonStyleConfiguration class] forExtension:@"json"]; [self registerConfigurationClass:[TyphoonPropertyStyleConfiguration class] forExtension:@"properties"]; [self registerConfigurationClass:[TyphoonPlistStyleConfiguration class] forExtension:@"plist"]; } //------------------------------------------------------------------------------------------- #pragma mark - Interface Methods //------------------------------------------------------------------------------------------- - (void)registerNamespace:(TyphoonDefinitionNamespace *)space { _space = space; } - (void)useResourceWithName:(NSString *)name { [self useResourceWithName:name bundle:[NSBundle mainBundle]]; } - (void)useResourceWithName:(NSString *)name bundle:(NSBundle *)bundle { [self useResource:[TyphoonBundleResource withName:name inBundle:bundle] withExtension:[name pathExtension]]; } - (void)useResourceAtPath:(NSString *)path { [self useResource:[TyphoonPathResource withPath:path] withExtension:[path pathExtension]]; } - (void)useResource:(id)resource withExtension:(NSString *)typeExtension { id config = _configs[typeExtension]; [config appendResource:resource]; } - (id)configurationValueForKey:(NSString *)key { __block id value = nil; #if DEBUG __block NSString *foundExtension = nil; #endif [_configs enumerateKeysAndObjectsUsingBlock:^(NSString *extension, id config, BOOL *stop) { id object = [config objectForKey:key]; #if !DEBUG if (object) { value = object; *stop = YES; } #else if (object) { if (value) { [NSException raise:NSInternalInconsistencyException format:@"Value for key %@ already exists in %@ config", key, foundExtension]; } else { value = object; foundExtension = extension; } } #endif }]; return value; } //------------------------------------------------------------------------------------------- #pragma mark - Protocol Methods //------------------------------------------------------------------------------------------- - (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory { if ([self shouldInjectDefinition:definition]) { [self configureInjectionsInDefinition:definition]; [self configureInjectionsInRuntimeArgumentsInDefinition:definition]; } } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods //------------------------------------------------------------------------------------------- - (BOOL)shouldInjectDefinition:(TyphoonDefinition *)definition { return [_space isEqual:definition.space] || [_space isGlobalNamespace]; } - (void)configureInjectionsInDefinition:(TyphoonDefinition *)definition { [definition enumerateInjectionsOfKind:[TyphoonInjectionByConfig class] options:TyphoonInjectionsEnumerationOptionAll usingBlock:^(TyphoonInjectionByConfig *injection, id *injectionToReplace, BOOL *stop) { id configuredInjection = [self injectionForConfigInjection:injection]; if (configuredInjection) { injection.configuredInjection = configuredInjection; } }]; } - (void)configureInjectionsInRuntimeArgumentsInDefinition:(TyphoonDefinition *)definition { [definition enumerateInjectionsOfKind:[TyphoonInjectionByReference class] options:TyphoonInjectionsEnumerationOptionAll usingBlock:^(TyphoonInjectionByReference *injection, id *injectionToReplace, BOOL *stop) { [injection.referenceArguments enumerateArgumentsUsingBlock:^(TyphoonInjectionByConfig *argument, NSUInteger index, BOOL *innerStop) { if ([argument isKindOfClass:[TyphoonInjectionByConfig class]]) { id configuredInjection = [self injectionForConfigInjection:argument]; if (configuredInjection) { argument.configuredInjection = configuredInjection; } } }]; }]; } - (id)injectionForConfigInjection:(TyphoonInjectionByConfig *)injection { id value = [self configurationValueForKey:injection.configKey]; id result = nil; if ([value isKindOfClass:[NSString class]]) { result = TyphoonInjectionWithObjectFromString(value); } else if (value) { result = TyphoonInjectionWithObject(value); } return result; } @end id TyphoonConfig(NSString *configKey) { return [TyphoonInject byConfigKey:configKey]; } ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonConfiguration.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// @protocol TyphoonResource; @protocol TyphoonConfiguration - (void)appendResource:(id)resource; - (id)objectForKey:(NSString *)key; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonJsonStyleConfiguration.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonConfiguration.h" @interface TyphoonJsonStyleConfiguration : NSObject @end ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonJsonStyleConfiguration.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonJsonStyleConfiguration.h" #import "TyphoonResource.h" @implementation TyphoonJsonStyleConfiguration { NSMutableDictionary *_properties; } - (id)init { self = [super init]; if (self) { _properties = [NSMutableDictionary new]; } return self; } - (NSString *)stringWithoutCommentsFromString:(NSString *)string { static NSRegularExpression *expression; static dispatch_once_t once; dispatch_once(&once, ^{ expression = [NSRegularExpression regularExpressionWithPattern:@"(([\"'])(?:\\\\\\2|.)*?\\2)|(\\/\\/[^\\n\\r]*(?:[\\n\\r]+|$)|(\\/\\*(?:(?!\\*\\/).|[\\n\\r])*\\*\\/))" options:NSRegularExpressionAnchorsMatchLines error:nil]; }); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wassign-enum" NSArray *matches = [expression matchesInString:string options:0 range:NSMakeRange(0, string.length)]; #pragma clang diagnostic pop if ([matches count] > 0) { NSMutableString *mutableString = [string mutableCopy]; [matches enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(NSTextCheckingResult *match, NSUInteger idx, BOOL *stop) { unichar character = [string characterAtIndex:match.range.location]; if (character != '\'' && character != '\"') { [mutableString replaceCharactersInRange:match.range withString:@""]; } }]; string = mutableString; } return string; } - (void)appendResource:(id)resource { NSString *jsonWithoutComments = [self stringWithoutCommentsFromString:[resource asString]]; NSData *jsonData = [jsonWithoutComments dataUsingEncoding:NSUTF8StringEncoding]; NSError *error = nil; NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingAllowFragments error:&error]; if (!error) { [_properties addEntriesFromDictionary:dictionary]; } else { [NSException raise:NSInvalidArgumentException format:@"Can't parse JSON configuration file: %@", error]; } } - (id)objectForKey:(NSString *)key { return [_properties valueForKeyPath:key]; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonPlistStyleConfiguration.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonConfiguration.h" @interface TyphoonPlistStyleConfiguration : NSObject @end ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonPlistStyleConfiguration.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonPlistStyleConfiguration.h" #import "TyphoonResource.h" @implementation TyphoonPlistStyleConfiguration { NSMutableDictionary *_properties; } - (id)init { self = [super init]; if (self) { _properties = [NSMutableDictionary new]; } return self; } - (void)appendResource:(id)resource { NSString *errorString = nil; NSDictionary *dictionary = nil; // remove deprecated warning when targeting iOS 8 + and OSX 10.6 + #if (TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_8) || (TARGET_OS_MAC && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) NSError * error = nil; dictionary = [NSPropertyListSerialization propertyListWithData:[resource data] options:NSPropertyListImmutable format:NULL error:&error]; if (error != nil) { errorString = [error localizedDescription]; } #else dictionary = [NSPropertyListSerialization propertyListFromData:[resource data] mutabilityOption:NSPropertyListImmutable format:NULL errorDescription:&errorString]; #endif if (![dictionary isKindOfClass:[NSDictionary class]]) { [NSException raise:NSInvalidArgumentException format:@"Root plist object must be a dictionary"]; } if (!errorString) { [_properties addEntriesFromDictionary:dictionary]; } else { [NSException raise:NSInvalidArgumentException format:@"Can't prase plist configuration file: %@", errorString]; } } - (id)objectForKey:(NSString *)key { return [_properties valueForKeyPath:key]; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonPropertyStyleConfiguration.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonConfiguration.h" @interface TyphoonPropertyStyleConfiguration : NSObject @end ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonConfiguration/TyphoonPropertyStyleConfiguration.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonPropertyStyleConfiguration.h" #import "TyphoonResource.h" @implementation TyphoonPropertyStyleConfiguration { NSMutableDictionary *_properties; } - (id)init { self = [super init]; if (self) { _properties = [NSMutableDictionary new]; } return self; } - (void)appendResource:(id)resource { NSArray *lines = [[resource asString] componentsSeparatedByCharactersInSet:[NSCharacterSet newlineCharacterSet]]; for (NSString *line in lines) { if (![line hasPrefix:@"#"]) { NSRange range = [line rangeOfString:@"="]; if (range.location != NSNotFound) { NSString *property = [line substringToIndex:range.location]; NSString *value = [line substringFromIndex:range.location + range.length]; [_properties setObject:value forKey:property]; } } } } - (id)objectForKey:(NSString *)key { return _properties[key]; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonDefinition+Config.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition.h" @interface TyphoonDefinition (Config) /** * Factory method for a TyphoonConfigPostProcessor. Don't use it in test targets! * @param fileName The config filename to load. File should be placed in main bundle * @return a definition. */ + (instancetype)withConfigName:(NSString *)fileName; /** * Factory method for a TyphoonConfigPostProcessor. * @param fileName The config filename to load. * @param fileBundle The bundle, where the config file is placed * @return a definition. */ + (instancetype)withConfigName:(NSString *)fileName bundle:(NSBundle *)fileBundle; /** * Factory method for a TyphoonConfigPostProcessor. * @param filePath The path to config file to load. * @return a definition. */ + (instancetype)withConfigPath:(NSString *)filePath; #pragma mark - Deprecated methods + (instancetype)configDefinitionWithName:(NSString *)fileName DEPRECATED_MSG_ATTRIBUTE("use -withConfigName: instead"); + (instancetype)configDefinitionWithName:(NSString *)fileName bundle:(NSBundle *)fileBundle DEPRECATED_MSG_ATTRIBUTE("use -withConfigName:bundle: instead"); + (instancetype)configDefinitionWithPath:(NSString *)filePath DEPRECATED_MSG_ATTRIBUTE("use -withConfigPath: instead"); @end ================================================ FILE: Pods/Typhoon/Source/Configuration/ConfigPostProcessor/TyphoonDefinition+Config.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition+Config.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonConfigPostProcessor.h" #import "TyphoonResource.h" #import "TyphoonLinkerCategoryBugFix.h" TYPHOON_LINK_CATEGORY(TyphoonDefinition_Config) @implementation TyphoonDefinition (Config) #pragma mark - Class Methods + (instancetype)withConfigName:(NSString *)fileName { return [self withClass:[TyphoonConfigPostProcessor class] configuration:^(TyphoonDefinition *definition) { [definition injectMethod:@selector(useResourceWithName:) parameters:^(TyphoonMethod *method) { [method injectParameterWith:fileName]; }]; definition.key = [NSString stringWithFormat:@"%@-%@", NSStringFromClass(definition.class), fileName]; }]; } + (instancetype)withConfigName:(NSString *)fileName bundle:(NSBundle *)fileBundle { return [self withClass:[TyphoonConfigPostProcessor class] configuration:^(TyphoonDefinition *definition) { [definition injectMethod:@selector(useResourceWithName:bundle:) parameters:^(TyphoonMethod *method) { [method injectParameterWith:fileName]; [method injectParameterWith:fileBundle]; }]; definition.key = [NSString stringWithFormat:@"%@-%@", NSStringFromClass(definition.class), fileName]; }]; } + (instancetype)withConfigPath:(NSString *)filePath { return [self withClass:[TyphoonConfigPostProcessor class] configuration:^(TyphoonDefinition *definition) { [definition injectMethod:@selector(useResourceAtPath:) parameters:^(TyphoonMethod *method) { [method injectParameterWith:filePath]; }]; definition.key = [NSString stringWithFormat:@"%@-%@", NSStringFromClass(definition.class), [filePath lastPathComponent]]; }]; } #pragma mark - Deprecated methods + (instancetype)configDefinitionWithName:(NSString *)fileName { TyphoonDefinition *configDefinition = [self withConfigName:fileName]; [configDefinition applyGlobalNamespace]; return configDefinition; } + (instancetype)configDefinitionWithName:(NSString *)fileName bundle:(NSBundle *)fileBundle { TyphoonDefinition *configDefinition = [self withConfigName:fileName bundle:fileBundle]; [configDefinition applyGlobalNamespace]; return configDefinition; } + (instancetype)configDefinitionWithPath:(NSString *)filePath { TyphoonDefinition *configDefinition = [self withConfigPath:filePath]; [configDefinition applyGlobalNamespace]; return configDefinition; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/DefinitionOptionConfiguration/Matcher/TyphoonOptionMatcher+Internal.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonOptionMatcher.h" #import "TyphoonDefinition+Option.h" @class TyphoonComponentFactory; @protocol TyphoonInjection; @interface TyphoonOptionMatcher (Internal) - (instancetype)initWithBlock:(TyphoonMatcherBlock)block; - (id)injectionMatchedValue:(id)value; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/DefinitionOptionConfiguration/Matcher/TyphoonOptionMatcher.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition.h" @interface TyphoonOptionMatcher : NSObject /** If 'option' equals 'optionValue' then use 'injection' */ - (void)caseEqual:(id)optionValue use:(id)injection; /** If 'option' is kind of class 'optionClass' then use 'injection' */ - (void)caseKindOfClass:(Class)optionClass use:(id)injection; /** If 'option' is member of class 'optionClass' then use 'injection' */ - (void)caseMemberOfClass:(Class)optionClass use:(id)injection; /** If 'option' conforms to protocol 'optionProtocol' then use 'injection' */ - (void)caseConformsToProtocol:(Protocol *)optionProtocol use:(id)injection; /** When matcher can't match injection from optionValue, use 'injection' */ - (void)defaultUse:(id)injection; /** * If this method called, matcher will find definition using 'option' value as key for definition. * @note When matching definition from 'option', 'caseEqual:use:', 'caseKindOfClass:use:' and 'caseMemberOfClass:use:' has higher priority than matching by definition key */ - (void)useDefinitionWithKeyMatchedOptionValue; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/DefinitionOptionConfiguration/Matcher/TyphoonOptionMatcher.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition+Option.h" #import "TyphoonInjections.h" #import "TyphoonInjection.h" ////////////////////// TyphoonOptionMatcherValue ////////////////// @interface TyphoonOptionMatchNilValue : NSObject @end @implementation TyphoonOptionMatchNilValue @end @interface TyphoonOptionMatch : NSObject @property (nonatomic, strong) id value; @property (nonatomic) Class memberClass; @property (nonatomic) Class kindClass; @property (nonatomic) Protocol *conformProtocol; @property (nonatomic, strong) id injection; + (id)matchWithValue:(id)value injection:(id)injection; + (id)matchWithKindOfClass:(Class)clazz injection:(id)injection; + (id)matchWithMemberOfClass:(Class)clazz injection:(id)injection; + (id)matchWithConformsToProtocol:(Protocol *)conformProtocol injection:(id)injection; @end @implementation TyphoonOptionMatch + (id)matchWithValue:(id)value injection:(id)injection { TyphoonOptionMatch *match = [TyphoonOptionMatch new]; if (value) { match.value = value; } else { match.value = [TyphoonOptionMatchNilValue class]; } match.injection = TyphoonMakeInjectionFromObjectIfNeeded(injection); return match; } + (id)matchWithKindOfClass:(Class)clazz injection:(id)injection { TyphoonOptionMatch *match = [TyphoonOptionMatch new]; match.kindClass = clazz; match.injection = TyphoonMakeInjectionFromObjectIfNeeded(injection); return match; } + (id)matchWithMemberOfClass:(Class)clazz injection:(id)injection { TyphoonOptionMatch *match = [TyphoonOptionMatch new]; match.memberClass = clazz; match.injection = TyphoonMakeInjectionFromObjectIfNeeded(injection); return match; } + (id)matchWithConformsToProtocol:(Protocol *)conformProtocol injection:(id)injection { TyphoonOptionMatch *match = [TyphoonOptionMatch new]; match.conformProtocol = conformProtocol; match.injection = TyphoonMakeInjectionFromObjectIfNeeded(injection); return match; } @end //////////////////////////////////////// @implementation TyphoonOptionMatcher { NSMutableArray *_matches; id _defaultInjection; BOOL _useMatchingByName; } - (instancetype)initWithBlock:(TyphoonMatcherBlock)block { self = [super init]; if (self) { _matches = [NSMutableArray new]; _useMatchingByName = NO; if (block) { block(self); } } return self; } - (void)caseEqual:(id)optionValue use:(id)injection { [_matches addObject:[TyphoonOptionMatch matchWithValue:optionValue injection:injection]]; } - (void)caseKindOfClass:(Class)optionClass use:(id)injection { [_matches addObject:[TyphoonOptionMatch matchWithKindOfClass:optionClass injection:injection]]; } - (void)caseMemberOfClass:(Class)optionClass use:(id)injection { [_matches addObject:[TyphoonOptionMatch matchWithMemberOfClass:optionClass injection:injection]]; } - (void)caseConformsToProtocol:(Protocol *)optionProtocol use:(id)injection { [_matches addObject:[TyphoonOptionMatch matchWithConformsToProtocol:optionProtocol injection:injection]]; } - (void)useDefinitionWithKeyMatchedOptionValue { _useMatchingByName = YES; } - (void)defaultUse:(id)injection { _defaultInjection = TyphoonMakeInjectionFromObjectIfNeeded(injection); } - (id)injectionMatchedValue:(id)value { id injection = nil; TyphoonOptionMatch *match = [self matchForValue:value]; if (match) { injection = match.injection; } else if (_useMatchingByName && [value isKindOfClass:[NSString class]]){ injection = TyphoonInjectionWithReference(value); } if (!injection) { injection = _defaultInjection; } if (!injection) { [NSException raise:NSInternalInconsistencyException format:@"Can't find injection to match value %@",value]; } return injection; } - (TyphoonOptionMatch *)matchForValue:(id)value { for (TyphoonOptionMatch *match in _matches) { BOOL isEqual = (match.value && [match.value isEqual:value]) || ([match.value isEqual:[TyphoonOptionMatchNilValue class]] && !value); BOOL isKind = (match.kindClass && [value isKindOfClass:match.kindClass]); BOOL isMember = (match.memberClass && [value isMemberOfClass:match.memberClass]); BOOL doesConform = (match.conformProtocol && [value conformsToProtocol:match.conformProtocol]); if (isEqual || isKind || isMember || doesConform) { return match; } } return nil; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/DefinitionOptionConfiguration/TyphoonDefinition+Option.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition.h" #import "TyphoonOptionMatcher.h" @protocol TyphoonAutoInjectionConfig; typedef void(^TyphoonMatcherBlock)(TyphoonOptionMatcher *matcher); @interface TyphoonDefinition (Option) /** if boolean 'option' value is YES, then return yesInjection, otherwise return noInjection */ + (id)withOption:(id)option yes:(id)yesInjection no:(id)noInjection; /** Returns definition matching 'option', specified in 'matcherBlock' */ + (id)withOption:(id)option matcher:(TyphoonMatcherBlock)matcherBlock; + (id)withOption:(id)option matcher:(TyphoonMatcherBlock)matcherBlock autoInjectionConfig:(void(^)(id config))configBlock; @end @protocol TyphoonAutoInjectionConfig @property (nonatomic, strong) id classOrProtocolForAutoInjection; @property (nonatomic) TyphoonAutoInjectVisibility autoInjectionVisibility; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/DefinitionOptionConfiguration/TyphoonDefinition+Option.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition+Option.h" #import "TyphoonOptionMatcher+Internal.h" #import "TyphoonInjections.h" #import "TyphoonFactoryDefinition.h" #import "TyphoonComponentFactory.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonInjectionContext.h" #import "TyphoonInjection.h" #import "TyphoonDefinition+Internal.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonDefinition+InstanceBuilder.h" @interface TyphoonOptionDefinition : TyphoonFactoryDefinition @property(nonatomic, strong) id optionInjection; @property(nonatomic, strong) TyphoonOptionMatcher* matcher; @end @implementation TyphoonOptionDefinition - (instancetype)initWithOptionValue:(id)value matcher:(TyphoonOptionMatcher*)matcher { self = [super initWithClass:[NSObject class] key:nil]; if (self) { self.optionInjection = TyphoonMakeInjectionFromObjectIfNeeded(value); self.matcher = matcher; self.scope = TyphoonScopePrototype; } return self; } - (id)targetForInitializerWithFactory:(TyphoonComponentFactory*)factory args:(TyphoonRuntimeArguments*)args { TyphoonInjectionContext* context = [[TyphoonInjectionContext alloc] initWithFactory:factory args:args raiseExceptionIfCircular:YES]; context.destinationType = [TyphoonTypeDescriptor descriptorWithEncodedType:@encode(id)]; __block id optionValue = nil; [self.optionInjection valueToInjectWithContext:context completion:^(id value) { optionValue = value; }]; id injection = [self.matcher injectionMatchedValue:optionValue]; __block id result = nil; [injection valueToInjectWithContext:context completion:^(id value) { result = value; }]; return result; } - (TyphoonMethod*)initializer { return nil; } - (void)enumerateInjectionsOfKind:(Class)injectionClass options:(TyphoonInjectionsEnumerationOption)options usingBlock:(TyphoonInjectionsEnumerationBlock)block { if (options & TyphoonInjectionsEnumerationOptionProperties) { if ([self.optionInjection isKindOfClass:injectionClass]) { id injectionToReplace = nil; BOOL stop = NO; block(self.optionInjection, &injectionToReplace, &stop); if (injectionToReplace) { self.optionInjection = injectionToReplace; } if (stop) { return; } } } [super enumerateInjectionsOfKind:injectionClass options:options usingBlock:block]; } @end @implementation TyphoonDefinition (Option) + (id)withOption:(id)option yes:(id)yesDefinition no:(id)noDefinition { return [self withOption:option matcher:^(TyphoonOptionMatcher* matcher) { [matcher caseEqual:@YES use:yesDefinition]; [matcher caseEqual:@"YES" use:yesDefinition]; [matcher caseEqual:@"1" use:yesDefinition]; [matcher caseEqual:@NO use:noDefinition]; [matcher caseEqual:@"NO" use:noDefinition]; [matcher caseEqual:@"0" use:noDefinition]; }]; } + (id)withOption:(id)option matcher:(TyphoonMatcherBlock)matcherBlock { TyphoonOptionMatcher* matcher = [[TyphoonOptionMatcher alloc] initWithBlock:matcherBlock]; return [[TyphoonOptionDefinition alloc] initWithOptionValue:option matcher:matcher]; } + (id)withOption:(id)option matcher:(TyphoonMatcherBlock)matcherBlock autoInjectionConfig:(void (^)(id config))configBlock { TyphoonOptionMatcher* matcher = [[TyphoonOptionMatcher alloc] initWithBlock:matcherBlock]; TyphoonOptionDefinition* definition = [[TyphoonOptionDefinition alloc] initWithOptionValue:option matcher:matcher]; if (configBlock) { configBlock(definition); } return definition; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/GlobalConfigResolver/TyphoonGlobalConfigCollector.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonGlobalConfigCollector : NSObject - (instancetype)initWithAppDelegate:(id)appDelegate; - (NSArray *)obtainGlobalConfigFilenamesFromBundle:(NSBundle *)bundle; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/GlobalConfigResolver/TyphoonGlobalConfigCollector.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonGlobalConfigCollector.h" @interface TyphoonGlobalConfigCollector () @property (strong, nonatomic) id appDelegate; @end @implementation TyphoonGlobalConfigCollector - (instancetype)initWithAppDelegate:(id)appDelegate { self = [super init]; if (self) { _appDelegate = appDelegate; } return self; } - (NSArray *)obtainGlobalConfigFilenamesFromBundle:(NSBundle *)bundle { NSArray *plistFilenames = [self fetchConfigFilenamesFromPlistKeyInBundle:bundle]; NSArray *appDelegateFilenames = [self fetchConfigFilenamesFromAppDelegate]; NSString *bundleIdFilename = [self fetchConfigFilenameFromUniqueBundleIdInBundle:bundle]; NSString *oldStylePlistFilename = [self fetchConfigFilenameFromOldStylePlistKeyInBundle:bundle]; NSMutableSet *resultNames = [NSMutableSet set]; if (plistFilenames) { [resultNames addObjectsFromArray:plistFilenames]; } if (appDelegateFilenames) { [resultNames addObjectsFromArray:appDelegateFilenames]; } if (bundleIdFilename) { [resultNames addObject:bundleIdFilename]; } if (oldStylePlistFilename) { [resultNames addObject:oldStylePlistFilename]; } return [resultNames allObjects]; } - (NSArray *)fetchConfigFilenamesFromPlistKeyInBundle:(NSBundle *)bundle { NSArray *fileNames = [bundle infoDictionary][@"TyphoonGlobalConfigFilenames"]; return fileNames; } - (NSArray *)fetchConfigFilenamesFromAppDelegate { NSArray *fileNames; SEL globalConfigFilenamesSelector = NSSelectorFromString(@"globalConfigFilenames"); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" if ([self.appDelegate respondsToSelector:globalConfigFilenamesSelector]) { fileNames = [self.appDelegate performSelector:globalConfigFilenamesSelector]; } #pragma clang diagnostic pop return fileNames; } - (NSString *)fetchConfigFilenameFromOldStylePlistKeyInBundle:(NSBundle *)bundle { NSString *configFileName = [bundle infoDictionary][@"TyphoonConfigFilename"]; return configFileName; } - (NSString *)fetchConfigFilenameFromUniqueBundleIdInBundle:(NSBundle *)bundle { NSString *fileName = nil; NSString *bundleID = [bundle infoDictionary][@"CFBundleIdentifier"]; NSString *configFilename = [NSString stringWithFormat:@"config_%@.plist", bundleID]; NSString *configPath = [[bundle resourcePath] stringByAppendingPathComponent:configFilename]; if ([[NSFileManager defaultManager] fileExistsAtPath:configPath]) { fileName = configFilename; } return fileName; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/Resource/TyphoonBundleResource.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonResource.h" #import "TyphoonPathResource.h" /** * @ingroup Configuration * * Represents a resource within the application bundle. */ @interface TyphoonBundleResource : TyphoonPathResource + (id )withName:(NSString *)name; + (id )withName:(NSString *)name inBundle:(NSBundle *)bundle; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/Resource/TyphoonBundleResource.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonBundleResource.h" @implementation TyphoonBundleResource //------------------------------------------------------------------------------------------- #pragma MARK: - Class Methods //------------------------------------------------------------------------------------------- + (id )withName:(NSString *)name { return [self withName:name inBundle:[NSBundle bundleForClass:[self class]]]; } + (id )withName:(NSString *)name inBundle:(NSBundle *)bundle { NSString *filePath = [self filePathForName:name inBundle:bundle]; if (filePath == nil) { [NSException raise:NSInvalidArgumentException format:@"Resource named '%@' not in bundle.", name]; } return [[TyphoonBundleResource alloc] initWithContentsOfFile:filePath]; } + (NSString *)filePathForName:(NSString *)name inBundle:(NSBundle *)bundle { return [bundle pathForResource:[name stringByDeletingPathExtension] ofType:[name pathExtension]]; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/Resource/TyphoonPathResource.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonResource.h" @interface TyphoonPathResource : NSObject + (id )withPath:(NSString *)filePath; - (instancetype)initWithContentsOfFile:(NSString *)filePath; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/Resource/TyphoonPathResource.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonPathResource.h" @implementation TyphoonPathResource { NSString *_path; NSData *_data; } + (id)withPath:(NSString *)filePath { return [[TyphoonPathResource alloc] initWithContentsOfFile:filePath]; } - (instancetype)initWithContentsOfFile:(NSString *)filePath { self = [super init]; if (self) { _path = filePath; _data = [[NSData alloc] initWithContentsOfFile:filePath]; } return self; } - (NSString *)asString { return [self asStringWithEncoding:NSUTF8StringEncoding]; } - (NSString *)asStringWithEncoding:(NSStringEncoding)encoding { return [[NSString alloc] initWithData:_data encoding:encoding]; } - (NSData *)data { return _data; } - (NSURL *)url { return [NSURL fileURLWithPath:_path]; } - (NSString *)description { return _path.lastPathComponent; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/Resource/TyphoonResource.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** * @ingroup Configuration */ @protocol TyphoonResource /** * Returns the resource as data. */ @property(nonatomic, readonly) NSData *data; /** * Returns the resource with the given name, as an NSString using NSUTF8String encoding. */ @property(nonatomic, readonly, getter=asString) NSString *string; @property(nonatomic, readonly) NSURL *url; /** * Returns the resource with the given name, using the specified encoding. */ - (NSString *)asStringWithEncoding:(NSStringEncoding)encoding; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/Startup/TyphoonStartup.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonComponentFactory; @interface TyphoonStartup : NSObject + (void)requireInitialFactory; + (TyphoonComponentFactory *)initialFactory; + (void)releaseInitialFactory; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/Startup/TyphoonStartup.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonStartup.h" #import "TyphoonComponentFactory.h" #import "TyphoonAssembly.h" #import "TyphoonBlockComponentFactory.h" #import "TyphoonComponentFactory+InstanceBuilder.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonConfigPostProcessor.h" #import "OCLogTemplate.h" #import "TyphoonAssemblyBuilder+PlistProcessor.h" #import "TyphoonAssemblyBuilder.h" #import "TyphoonGlobalConfigCollector.h" #import #if TARGET_OS_IPHONE || TARGET_OS_TV #define ApplicationClass [UIApplication class] #elif TARGET_OS_MAC #define ApplicationClass [NSApplication class] #endif #if TARGET_OS_IPHONE || TARGET_OS_TV #define ApplicationDidFinishLaunchingNotification UIApplicationDidFinishLaunchingNotification #elif TARGET_OS_MAC #define ApplicationDidFinishLaunchingNotification NSApplicationDidFinishLaunchingNotification #endif @implementation TyphoonStartup + (void)load { [self swizzleSetDelegateMethodOnApplicationClass]; } + (TyphoonComponentFactory *)factoryFromAppDelegate:(id)appDelegate { TyphoonComponentFactory *result = nil; SEL initialFactorySelector = NSSelectorFromString(@"initialFactory"); SEL initialAssembliesSelector = NSSelectorFromString(@"initialAssemblies"); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Warc-performSelector-leaks" if ([appDelegate respondsToSelector:initialFactorySelector]) { result = [appDelegate performSelector:initialFactorySelector]; } if ([appDelegate respondsToSelector:initialAssembliesSelector]) { NSArray *assemblyClasses = [appDelegate performSelector:initialAssembliesSelector]; NSArray *assemblies = [TyphoonAssemblyBuilder buildAssembliesWithClasses:assemblyClasses]; result = [TyphoonBlockComponentFactory factoryForResolvingUIWithAssemblies:assemblies]; } #pragma clang diagnostic pop return result; } #pragma mark - static TyphoonComponentFactory *initialFactory; static NSUInteger initialFactoryRequestCount = 0; static BOOL initialFactoryWasCreated = NO; static id initialAppDelegate = nil; + (void)requireInitialFactory { if (initialFactoryRequestCount == 0 && !initialFactoryWasCreated) { NSArray *assemblies = [TyphoonAssemblyBuilder buildAssembliesFromPlistInBundle:[NSBundle mainBundle]]; if (assemblies.count > 0) { initialFactory = [TyphoonBlockComponentFactory factoryForResolvingUIWithAssemblies:assemblies]; initialFactoryWasCreated = YES; } } initialFactoryRequestCount += 1; } + (TyphoonComponentFactory *)initialFactory { return initialFactory; } + (void)swizzleSetDelegateMethodOnApplicationClass { SEL sel = @selector(setDelegate:); Method method = class_getInstanceMethod(ApplicationClass, sel); void(*originalImp)(id, SEL, id) = (void (*)(id, SEL, id))method_getImplementation(method); IMP adjustedImp = imp_implementationWithBlock(^(id instance, id delegate) { if (!delegate || initialAppDelegate) { originalImp(instance, sel, delegate); return; } //This ensures that Typhoon startup runs only once initialAppDelegate = delegate; [self requireInitialFactory]; id factoryFromDelegate = [self factoryFromAppDelegate:delegate]; if (factoryFromDelegate && initialFactory) { [NSException raise:NSInternalInconsistencyException format:@"The method 'initialFactory' is implemented on %@, also Info.plist" " has 'TyphoonInitialAssemblies' key. Typhoon can't decide which factory to use.", [delegate class]]; } if (factoryFromDelegate) { initialFactory = factoryFromDelegate; } if (initialFactory) { TyphoonGlobalConfigCollector *collector = [[TyphoonGlobalConfigCollector alloc] initWithAppDelegate:delegate]; NSBundle *bundle = [NSBundle bundleForClass:[delegate class]]; NSArray *globalConfigFileNames = [collector obtainGlobalConfigFilenamesFromBundle:bundle]; for (NSString *configName in globalConfigFileNames) { id configProcessor = [TyphoonConfigPostProcessor forResourceNamed:configName inBundle:bundle]; [initialFactory attachDefinitionPostProcessor:configProcessor]; } [self injectInitialFactoryIntoDelegate:delegate]; } [self releaseInitialFactoryWhenApplicationDidFinishLaunching]; originalImp(instance, sel, delegate); }); method_setImplementation(method, adjustedImp); } + (void)releaseInitialFactoryWhenApplicationDidFinishLaunching { __weak __typeof(self) weakSelf = self; [[NSNotificationCenter defaultCenter] addObserverForName:ApplicationDidFinishLaunchingNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) { [weakSelf releaseInitialFactory]; }]; } + (void)releaseInitialFactory { initialFactoryRequestCount -= 1; if (initialFactoryRequestCount == 0) { initialFactory = nil; } } #pragma mark - + (void)injectInitialFactoryIntoDelegate:(id)appDelegate { [initialFactory load]; [initialFactory inject:appDelegate]; } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/TyphoonAbstractDetachableComponentFactoryPostProcessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinitionPostProcessor.h" @interface TyphoonAbstractDetachableComponentFactoryPostProcessor : NSObject { TyphoonComponentFactory* _factory; NSMutableDictionary *_rollbackDefinitions; } /** Restores a component factory back to its initial state after post processing. */ - (void)rollback; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/TyphoonAbstractDetachableComponentFactoryPostProcessor.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractDetachableComponentFactoryPostProcessor.h" #import "TyphoonComponentFactory.h" #import "TyphoonDefinition+Infrastructure.h" @implementation TyphoonComponentFactory (DetachableComponentFactoryPostProcessor) - (void)setRegistry:(NSMutableArray *)registry { _registry = registry; } @end @implementation TyphoonAbstractDetachableComponentFactoryPostProcessor - (instancetype)init { self = [super init]; if (self) { _rollbackDefinitions = [NSMutableDictionary new]; } return self; } - (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory { _factory = factory; [self cacheDefinition:definition]; } - (void)rollback { NSMutableArray *postProcessors = (NSMutableArray *) _factory.definitionPostProcessors; if (![postProcessors.lastObject isEqual:self]) { [NSException raise:@"Only the last TyphoonAbstractDetachableComponentFactoryPostProcessor can be rolled-back" format:@"%@",NSInternalInconsistencyException]; } [postProcessors removeLastObject]; _factory.registry = [[_rollbackDefinitions allValues] mutableCopy]; [_factory unload]; } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods - (void)cacheDefinition:(TyphoonDefinition *)definition { if ([definition key]) { _rollbackDefinitions[[definition key]] = [definition copy]; } } @end ================================================ FILE: Pods/Typhoon/Source/Configuration/TyphoonDefinitionPostProcessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonComponentFactory; @class TyphoonDefinition; /** * @ingroup Assembly * Allows for custom modification of a component factory's definitions. Component factories can auto-detect TyphoonComponentFactoryPostProcessor components in their definitions and apply them before any other components get created. @see TyphoonConfigPostProcessor for an example implementation. @see TyphoonComponentPostProcessor which modifies instances after they've been built, rather than the definitions */ @protocol TyphoonDefinitionPostProcessor /** Post process a definition. Called for each definition in the factory. You able to modify definition and you can return another definition, using definitionToReplace pointer. @param definition The definition. @param definitionToReplace pointer to definition replacement @param factory The component factory. */ - (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/TyphoonInstancePostProcessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** * @ingroup Assembly * Allows for custom modification of a instance after its instantiation. Component factories can auto-detect TyphoonComponentPostProcessor components in their definitions and will apply them to components created by the factory. */ @protocol TyphoonInstancePostProcessor /** Post process a component after its initialization and return the processed component. */ - (id)postProcessInstance:(id)instance; @end ================================================ FILE: Pods/Typhoon/Source/Configuration/TyphoonOrdered.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** * Ordering values. */ typedef NS_ENUM(NSInteger, TyphoonOrder) { TyphoonOrderHighestPriority = INT_MIN, TyphoonOrderLowestPriority = INT_MAX }; /** * @ingroup Assembly * Protocol that defines ordering. Ordering can be used with for instance the TyphoonComponentFactoryPostProcessor and TyphoonComponentPostProcessor to control in which order the post processors are applied. A lower value indicates a higher priority. Default value is TyphoonOrderLowestPriority; */ @protocol TyphoonOrdered /** * Returns the order. */ - (NSInteger)order; @end ================================================ FILE: Pods/Typhoon/Source/Definition/AutoInjection/TyphoonAutoInjection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectedObject.h" //Injects property by definition matched by Protocol #define InjectedProtocol(aProtocol) TyphoonInjectedObject* //Injects property by definition matched by Class #define InjectedClass(aClass) aClass* ================================================ FILE: Pods/Typhoon/Source/Definition/AutoInjection/TyphoonFactoryAutoInjectionPostProcessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinitionPostProcessor.h" @class TyphoonDefinition; @interface TyphoonFactoryAutoInjectionPostProcessor : NSObject - (void)postProcessDefinition:(TyphoonDefinition *)definition; - (NSArray *)autoInjectedPropertiesForClass:(Class)clazz; @end ================================================ FILE: Pods/Typhoon/Source/Definition/AutoInjection/TyphoonFactoryAutoInjectionPostProcessor.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonFactoryAutoInjectionPostProcessor.h" #import "TyphoonComponentFactory.h" #import "TyphoonDefinition.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonInjectedObject.h" #import "TyphoonPropertyInjection.h" #import "TyphoonInjections.h" static BOOL IsTyphoonAutoInjectionType(TyphoonTypeDescriptor *type); static id TypeForInjectionFromType(TyphoonTypeDescriptor *type); @implementation TyphoonFactoryAutoInjectionPostProcessor - (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory { [self postProcessDefinition:definition]; } - (void)postProcessDefinition:(TyphoonDefinition *)definition { Class clazz = definition.type; if (clazz) { NSArray *properties = [self autoInjectedPropertiesForClass:clazz]; for (id propertyInjection in properties) { [definition addInjectedPropertyIfNotExists:propertyInjection]; } } } - (NSArray *)autoInjectedPropertiesForClass:(Class)clazz { NSMutableArray *injections = nil; NSSet *allProperties = [TyphoonIntrospectionUtils propertiesForClass:clazz upToParentClass:[NSObject class]]; for (NSString *propertyName in allProperties) { TyphoonTypeDescriptor *type = [TyphoonIntrospectionUtils typeForPropertyNamed:propertyName inClass:clazz]; if (IsTyphoonAutoInjectionType(type)) { id explicitType = TypeForInjectionFromType(type); if (!explicitType) { [NSException raise:NSInternalInconsistencyException format:@"Can't resolve '%@' property in %@ class. Make sure that specified protocol/class exist and linked.", propertyName, clazz]; } id injection = TyphoonInjectionWithType(explicitType); [injection setPropertyName:propertyName]; if (!injections) { injections = [[NSMutableArray alloc] initWithCapacity:allProperties.count]; } [injections addObject:injection]; } } return injections; } static BOOL IsTyphoonAutoInjectionType(TyphoonTypeDescriptor *type) { return protocol_isEqual(type.protocol, @protocol(TyphoonInjectedProtocol)) || type.typeBeingDescribed == [TyphoonInjectedObject class]; } static id TypeForInjectionFromType(TyphoonTypeDescriptor *type) { if (protocol_isEqual(type.protocol, @protocol(TyphoonInjectedProtocol))) { return type.typeBeingDescribed; } else { return type.protocol; } } @end ================================================ FILE: Pods/Typhoon/Source/Definition/AutoInjection/TyphoonInjectedObject.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonInjectedObject : NSObject @end @protocol TyphoonInjectedProtocol @end ================================================ FILE: Pods/Typhoon/Source/Definition/AutoInjection/TyphoonInjectedObject.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectedObject.h" @implementation TyphoonInjectedObject @end ================================================ FILE: Pods/Typhoon/Source/Definition/Infrastructure/TyphoonDefinition+Infrastructure.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition.h" #import "TyphoonInjectionsEnumeration.h" @protocol TyphoonPropertyInjection; /** * Declares definition methods & properties that may be useful for building infrastructure components. * * Normally you won't need to use these, but they are available for advanced usage via Typhoon+Infrastructure.h. */ @interface TyphoonDefinition (/* Infrastructure */) /** * The key of the component. A key is useful when multiple configuration of the same class or protocol are desired - for example * MasterCardPaymentClient and VisaPaymentClient. * * If using the TyphoonBlockComponentFactory style of assembly, the key is automatically generated based on the selector name of the * component, thus avoiding "magic strings" and providing better integration with IDE refactoring tools. */ @property (nonatomic) NSString *key; /** * Describes the initializer, ie the selector and arguments that will be used to instantiate this component. * * An initializer can be an instance method, a class method, or even a reference to another component's method (see factory property). * * If no explicit initializer has been set, returns a default initializer representing the init method. * * @see factory */ @property (nonatomic, readonly) TyphoonMethod *initializer; /** * Returns true if this is a default initializer generated by Typhoon. A manually specified initializer will return false, even if the * selector is @selector(init). */ @property (nonatomic, getter = isInitializerGenerated) BOOL initializerGenerated; /** * A method injection object configured using performBeforeInjections:parameters:. */ @property (nonatomic, readonly) TyphoonMethod *beforeInjections; /** * Describes injected properties, represented by objects conforming to TyphoonPropertyInjection protocol. */ @property (nonatomic, readonly) NSSet *injectedProperties; /** * Describes injected methods, represented by TyphoonMethod objects. */ @property (nonatomic, readonly) NSOrderedSet *injectedMethods; /** * A method injection object configured using performAfterInjections:parameters:. */ @property (nonatomic, readonly) TyphoonMethod *afterInjections; @property (nonatomic, readonly) SEL afterAllInjections; - (instancetype)initWithClass:(Class)clazz key:(NSString *)key; + (instancetype)withClass:(Class)clazz key:(NSString *)key; - (BOOL)hasRuntimeArgumentInjections; - (BOOL)isCandidateForInjectedClass:(Class)clazz includeSubclasses:(BOOL)includeSubclasses; - (BOOL)isCandidateForInjectedProtocol:(Protocol *)aProtocol; - (void)addInjectedProperty:(id)property; - (void)addInjectedPropertyIfNotExists:(id)property; - (void)enumerateInjectionsOfKind:(Class)injectionClass options:(TyphoonInjectionsEnumerationOption)options usingBlock:(TyphoonInjectionsEnumerationBlock)block; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Infrastructure/TyphoonInjectionsEnumeration.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// typedef void (^TyphoonInjectionsEnumerationBlock)(id injection, id *injectionToReplace, BOOL *stop); typedef NS_OPTIONS(NSInteger, TyphoonInjectionsEnumerationOption) { TyphoonInjectionsEnumerationOptionProperties = 1 << 0, TyphoonInjectionsEnumerationOptionMethods = 1 << 2, TyphoonInjectionsEnumerationOptionAll = TyphoonInjectionsEnumerationOptionProperties | TyphoonInjectionsEnumerationOptionMethods, }; ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonAbstractInjection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonPropertyInjection.h" #import "TyphoonParameterInjection.h" #import "TyphoonInjectionContext.h" typedef NS_ENUM(NSInteger, TyphoonInjectionType) { TyphoonInjectionTypeUndefined, TyphoonInjectionTypeProperty, TyphoonInjectionTypeParameter }; @interface TyphoonAbstractInjection : NSObject @property(nonatomic, readonly) TyphoonInjectionType type; /* TyphoonInjectionTypeProperty properties */ @property(nonatomic, readonly, strong) NSString *propertyName; /* TyphoonInjectionTypeParameter properties */ @property(nonatomic, readonly) NSUInteger parameterIndex; - (BOOL)isEqualToCustom:(id)injection; - (NSUInteger)customHash; - (NSString *)customDescription; @end @interface TyphoonAbstractInjection (Protected) - (void)copyBasePropertiesTo:(TyphoonAbstractInjection *)copiedInjection; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonAbstractInjection.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" #import "TyphoonMethod+InstanceBuilder.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonUtils.h" @implementation TyphoonAbstractInjection - (NSString *)customDescription { return @""; } - (NSString *)description { if (self.type == TyphoonInjectionTypeUndefined) { return [NSString stringWithFormat:@"<%@: %p, %@type=Undefined>", [self class], self, [self customDescription]]; } else if (self.type == TyphoonInjectionTypeParameter) { return [NSString stringWithFormat:@"<%@: %p, %@index=%d>", [self class], self, [self customDescription], (int) self.parameterIndex]; } else { return [NSString stringWithFormat:@"<%@: %p, %@property=%@>", [self class], self, [self customDescription], self.propertyName]; } } - (void)setParameterIndex:(NSUInteger)index { NSAssert(self.type != TyphoonInjectionTypeProperty, @"Trying to redefine injection with type %d", (int) self.type); _parameterIndex = index; _type = TyphoonInjectionTypeParameter; } - (void)setPropertyName:(NSString *)name { NSAssert(self.type != TyphoonInjectionTypeParameter, @"Trying to redefine injection with type %d", (int) self.type); _propertyName = name; _type = TyphoonInjectionTypeProperty; } - (NSUInteger)hash { NSUInteger hash = (NSUInteger) self.type; if (self.type == TyphoonInjectionTypeProperty) { hash = TyphoonHashByAppendingInteger(hash, [self.propertyName hash]); } else { hash = TyphoonHashByAppendingInteger(hash, self.parameterIndex); } return TyphoonHashByAppendingInteger(hash, [self customHash]); } - (NSUInteger)customHash { /** Any constant can be here, nothing magical */ return 25042013; } - (BOOL)isEqual:(id)other { if (other == self) { return YES; } if (!other || ![[other class] isEqual:[self class]]) { return NO; } return [self isEqualToBase:other] && [self isEqualToCustom:other]; } - (BOOL)isEqualToBase:(TyphoonAbstractInjection *)base { if (self == base) { return YES; } if (base == nil) { return NO; } if (self.type != base.type) { return NO; } if (self.type == TyphoonInjectionTypeParameter) { return self.parameterIndex == base.parameterIndex; } else if (self.type == TyphoonInjectionTypeProperty) { return [self.propertyName isEqualToString:base.propertyName]; } else { return NO; } } #pragma mark - Methods to override - (BOOL)isEqualToCustom:(id)injection { [NSException raise:NSInternalInconsistencyException format:@"%@ is abstract", NSStringFromSelector(_cmd)]; return NO; } - (id)copyWithZone:(NSZone *)zone { [NSException raise:NSInternalInconsistencyException format:@"%@ is abstract", NSStringFromSelector(_cmd)]; return nil; } - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { [NSException raise:NSInternalInconsistencyException format:@"%@ is abstract", NSStringFromSelector(_cmd)]; } @end @implementation TyphoonAbstractInjection (Protected) - (void)copyBasePropertiesTo:(TyphoonAbstractInjection *)copiedInjection { if (self.type == TyphoonInjectionTypeProperty) { [copiedInjection setPropertyName:_propertyName]; } else if (self.type == TyphoonInjectionTypeParameter) { [copiedInjection setParameterIndex:_parameterIndex]; } } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInject.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** * These methods are primarily intended to be used within TyphoonBlockDefinition blocks, although they will work * with regular TyphoonDefinitions too. */ @interface TyphoonInject : NSObject + (id)byType:(id)classOrProtocol; + (id)byConfigKey:(NSString *)configKey; + (id)byConfigKey:(NSString *)configKey type:(id)classOrProtocol; @end /** * Class-typed alternatives to TyphoonInject methods. */ @interface NSObject (TyphoonInject) + (instancetype)typhoonInjectByType; + (instancetype)typhoonInjectByConfigKey:(NSString *)configKey; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInject.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInject.h" #import "TyphoonConfigPostProcessor+Internal.h" #import "TyphoonBlockDefinitionController.h" #import "TyphoonBlockDefinition.h" #import "TyphoonComponentFactory.h" #import "TyphoonInjectionContext.h" #import "TyphoonInjection.h" #import "TyphoonInjections.h" @implementation TyphoonInject + (id)byType:(id)classOrProtocol { return [self resultWithInjection:TyphoonInjectionWithType(classOrProtocol) definitionBlock:nil]; } + (id)byConfigKey:(NSString *)configKey { return [self byConfigKey:configKey type:nil]; } + (id)byConfigKey:(NSString *)configKey type:(id)classOrProtocol { id configInjection = TyphoonInjectionWithConfigKey(configKey); return [self resultWithInjection:configInjection definitionBlock:^id(TyphoonBlockDefinition *definition, TyphoonInjectionContext *context) { TyphoonInjectionContext *typedContext = [context copy]; if (classOrProtocol) { typedContext.destinationType = [TyphoonTypeDescriptor descriptorWithType:classOrProtocol]; } TyphoonComponentFactory *factory = context.factory; for (id postProcessor in factory.definitionPostProcessors) { if (![postProcessor isKindOfClass:[TyphoonConfigPostProcessor class]]) { continue; } TyphoonConfigPostProcessor *configPostProcessor = (TyphoonConfigPostProcessor *)postProcessor; if (![configPostProcessor shouldInjectDefinition:definition]) { continue; } id injection = [configPostProcessor injectionForConfigInjection:configInjection]; if (!injection) { continue; } return [self valueToInjectWithInjection:injection context:typedContext]; } [NSException raise:NSInternalInconsistencyException format:@"Value for config key %@ is not configured. Make sure that you applied TyphoonConfigPostProcessor.", configKey]; return nil; }]; } + (id)resultWithInjection:(id)injection definitionBlock:(id (^)(TyphoonBlockDefinition *definition, TyphoonInjectionContext *context))definitionBlock { TyphoonBlockDefinitionController *controller = [TyphoonBlockDefinitionController currentController]; if (controller.buildingInstance) { if (definitionBlock) { return definitionBlock(controller.definition, controller.injectionContext); } else { return [self valueToInjectWithInjection:injection context:controller.injectionContext]; } } else { return injection; } } + (id)valueToInjectWithInjection:(id)injection context:(TyphoonInjectionContext *)context { __block id result = nil; [injection valueToInjectWithContext:context completion:^(id value) { result = value; }]; return result; } @end @implementation NSObject (TyphoonBlockInjection) + (instancetype)typhoonInjectByType { return [TyphoonInject byType:[self class]]; } + (instancetype)typhoonInjectByConfigKey:(NSString *)configKey { return [TyphoonInject byConfigKey:configKey type:[self class]]; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionContext.h" @protocol TyphoonInjection - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByCollection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" /** Protocol which should be confirmed by collection (NSArray, NSSet, NSOrderedSet are conforms) */ @protocol TyphoonCollection - (id)initWithCapacity:(NSUInteger)capacity; - (NSUInteger)count; - (void)addObject:(id)object; @end @interface TyphoonInjectionByCollection : TyphoonAbstractInjection - (instancetype)initWithCollection:(id)collection requiredClass:(Class)collectionClass; + (Class)collectionMutableClassFromClass:(Class)collectionClass; + (BOOL)isCollectionClass:(Class)collectionClass; - (NSUInteger)count; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByCollection.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByCollection.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonObjectWithCustomInjection.h" #import "TyphoonInjections.h" #import "TyphoonPropertyInjection.h" #import "NSArray+TyphoonManualEnumeration.h" #import "TyphoonUtils.h" @interface TyphoonInjectionByCollection () @property(nonatomic, strong) NSMutableArray *injections; @property(nonatomic) Class requiredClass; @end @implementation TyphoonInjectionByCollection + (BOOL)isCollectionClass:(Class)collectionClass { collectionClass = [self collectionMutableClassFromClass:collectionClass]; BOOL success = YES; success &= [collectionClass conformsToProtocol:@protocol(NSFastEnumeration)]; success &= [collectionClass instancesRespondToSelector:@selector(initWithCapacity:)]; success &= [collectionClass instancesRespondToSelector:@selector(count)]; success &= [collectionClass instancesRespondToSelector:@selector(addObject:)]; return success; } + (Class)collectionMutableClassFromClass:(Class)collectionClass { Class result; if ([collectionClass isSubclassOfClass:[NSArray class]]) { result = [NSMutableArray class]; } else if ([collectionClass isSubclassOfClass:[NSSet class]]) { result = [NSMutableSet class]; } else if ([collectionClass isSubclassOfClass:[NSOrderedSet class]]) { result = [NSMutableOrderedSet class]; } else { result = collectionClass; } return result; } - (instancetype)initWithCollection:(id)collection requiredClass:(Class)collectionClass { self.requiredClass = collectionClass; if (self.requiredClass && ![TyphoonInjectionByCollection isCollectionClass:self.requiredClass]) { [NSException raise:NSInvalidArgumentException format:@"Required collection type '%@' is neither an NSSet nor NSArray.", NSStringFromClass(self.requiredClass)]; } return [self initWithCollection:collection]; } - (instancetype)initWithCollection:(id )collection { self = [super init]; if (self) { self.injections = [[NSMutableArray alloc] initWithCapacity:[collection count]]; for (id object in collection) { [self.injections addObject:TyphoonMakeInjectionFromObjectIfNeeded(object)]; } } return self; } - (NSUInteger)count { return [self.injections count]; } #pragma mark - Utils - (void)buildCollectionWithClass:(Class)collectionClass context:(TyphoonInjectionContext *)valuesContext completion:(void(^)(idcollection))completion { Class mutableClass = [TyphoonInjectionByCollection collectionMutableClassFromClass:collectionClass]; id result = [[[mutableClass class] alloc] initWithCapacity:[self.injections count]]; [self.injections typhoon_enumerateObjectsWithManualIteration:^(id object, id iterator) { [object valueToInjectWithContext:valuesContext completion:^(id value) { [result addObject:value]; [iterator next]; }]; } completion:^{ completion(result); }]; } - (Class)collectionClassForContext:(TyphoonInjectionContext *)context { Class collectionClass = self.requiredClass; if (!collectionClass) { collectionClass = context.destinationType.classOrProtocol; } if (![TyphoonInjectionByCollection isCollectionClass:collectionClass]) { [NSException raise:NSInvalidArgumentException format:@"Destination type '%@' is neither an NSSet nor NSArray.", NSStringFromClass(collectionClass)]; } return collectionClass; } #pragma mark - Overrides - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByCollection *copied = [[TyphoonInjectionByCollection alloc] init]; copied.injections = self.injections; copied.requiredClass = self.requiredClass; [self copyBasePropertiesTo:copied]; return copied; } - (BOOL)isEqualToCustom:(TyphoonInjectionByCollection *)injection { return [self.injections isEqualToArray:injection.injections] && self.requiredClass == injection.requiredClass; } - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { Class collectionClass = [self collectionClassForContext:context]; TyphoonInjectionContext *contextForValues = [context copy]; contextForValues.destinationType = [TyphoonTypeDescriptor descriptorWithEncodedType:@encode(id)]; [self buildCollectionWithClass:collectionClass context:contextForValues completion:^(id collection) { result(collection); }]; } - (NSUInteger)customHash { return TyphoonHashByAppendingInteger([self.injections hash], [_requiredClass hash]); } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByComponentFactory.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" @interface TyphoonInjectionByComponentFactory : TyphoonAbstractInjection @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByComponentFactory.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByComponentFactory.h" #import "NSInvocation+TCFUnwrapValues.h" #import "TyphoonIntrospectionUtils.h" @implementation TyphoonInjectionByComponentFactory #pragma mark - Overrides - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { result(context.factory); } - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByComponentFactory *copied = [[TyphoonInjectionByComponentFactory alloc] init]; [self copyBasePropertiesTo:copied]; return copied; } - (BOOL)isEqualToCustom:(TyphoonInjectionByComponentFactory *)injection { return YES; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByConfig.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonAbstractInjection.h" @protocol TyphoonInjection; @interface TyphoonInjectionByConfig : TyphoonAbstractInjection @property (nonatomic, strong) id configuredInjection; @property (nonatomic, strong, readonly) NSString *configKey; - (instancetype)initWithConfigKey:(NSString *)configKey; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByConfig.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByConfig.h" #import "TyphoonInjection.h" #import "TyphoonUtils.h" @implementation TyphoonInjectionByConfig { } - (instancetype)initWithConfigKey:(NSString *)configKey { self = [super init]; if (self) { _configKey = configKey; } return self; } - (void)setConfiguredInjection:(id)configuredInjection { _configuredInjection = configuredInjection; } - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByConfig *copied = [[TyphoonInjectionByConfig alloc] initWithConfigKey:self.configKey]; [self copyBasePropertiesTo:copied]; return copied; } - (BOOL)isEqualToCustom:(TyphoonInjectionByConfig *)injection { return [self.configKey isEqualToString:injection.configKey]; } - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { if (!self.configuredInjection) { [NSException raise:NSInternalInconsistencyException format:@"Value for config key %@ is not configured. Make sure that you applied TyphoonConfigPostProcessor.",self.configKey]; } [self.configuredInjection valueToInjectWithContext:context completion:result]; } - (NSUInteger)customHash { return [self.configKey hash]; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByCurrentRuntimeArguments.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonAbstractInjection.h" @interface TyphoonInjectionByCurrentRuntimeArguments : TyphoonAbstractInjection @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByCurrentRuntimeArguments.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByCurrentRuntimeArguments.h" @implementation TyphoonInjectionByCurrentRuntimeArguments - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { result(context.args); } - (id)copyWithZone:(NSZone *)zone { id copied = [[self class] new]; [self copyBasePropertiesTo:copied]; return copied; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByDictionary.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" @protocol TyphoonDictionary - (id)initWithCapacity:(NSUInteger)capacity; - (NSUInteger)count; - (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block; - (void)setObject:(id)anObject forKey:(id)aKey; @end @interface TyphoonInjectionByDictionary : TyphoonAbstractInjection - (instancetype)initWithDictionary:(id)dictionary requiredClass:(Class)dictionaryClass; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByDictionary.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByDictionary.h" #import "TyphoonInjections.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonTypeDescriptor.h" #import "NSArray+TyphoonManualEnumeration.h" #import "TyphoonUtils.h" @interface TyphoonInjectionByDictionary () @property(nonatomic, strong) NSMutableDictionary *injections; @property(nonatomic) Class requiredClass; @end @implementation TyphoonInjectionByDictionary + (BOOL)isDictionaryClass:(Class)dictionaryClass { dictionaryClass = [self dictionaryMutableClassFromClass:dictionaryClass]; BOOL success = YES; success &= [dictionaryClass instancesRespondToSelector:@selector(initWithCapacity:)]; success &= [dictionaryClass instancesRespondToSelector:@selector(count)]; success &= [dictionaryClass instancesRespondToSelector:@selector(setObject:forKey:)]; success &= [dictionaryClass instancesRespondToSelector:@selector(enumerateKeysAndObjectsUsingBlock:)]; return success; } + (Class)dictionaryMutableClassFromClass:(Class)dictionaryClass { Class result; if ([dictionaryClass isSubclassOfClass:[NSDictionary class]]) { result = [NSMutableDictionary class]; } else { result = dictionaryClass; } return result; } - (instancetype)initWithDictionary:(id)dictionary requiredClass:(Class)dictionaryClass { self.requiredClass = dictionaryClass; if (self.requiredClass && ![TyphoonInjectionByDictionary isDictionaryClass:self.requiredClass]) { [NSException raise:NSInvalidArgumentException format:@"Required dictionary type '%@' is not dictionary.", NSStringFromClass(self.requiredClass)]; } return [self initWithDictionary:dictionary]; } - (instancetype)initWithDictionary:(id )dictionary { self = [super init]; if (self) { self.injections = [[NSMutableDictionary alloc] initWithCapacity:[dictionary count]]; [dictionary enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { id injection = TyphoonMakeInjectionFromObjectIfNeeded(obj); [self.injections setObject:injection forKey:key]; }]; } return self; } - (NSUInteger)count { return [self.injections count]; } #pragma mark - Utils - (void)buildDictionaryWithClass:(Class)dictionaryClass context:(TyphoonInjectionContext *)valuesContext completion:(void(^)(iddictionary))completion { Class mutableClass = [TyphoonInjectionByDictionary dictionaryMutableClassFromClass:dictionaryClass]; id result = [[[mutableClass class] alloc] initWithCapacity:[self.injections count]]; [[self.injections allKeys] typhoon_enumerateObjectsWithManualIteration:^(id key, id iterator) { [self.injections[key] valueToInjectWithContext:valuesContext completion:^(id value) { [result setObject:value forKey:key]; [iterator next]; }]; } completion:^{ completion(result); }]; } - (Class)dictionaryClassForContext:(TyphoonInjectionContext *)context { Class dictionaryClass = self.requiredClass; if (!dictionaryClass) { dictionaryClass = context.destinationType.classOrProtocol; } if (![TyphoonInjectionByDictionary isDictionaryClass:dictionaryClass]) { [NSException raise:NSInvalidArgumentException format:@"Property named '%@' on '%@' is not dictionary.", self.propertyName, NSStringFromClass(dictionaryClass)]; } return dictionaryClass; } #pragma mark - Overrides - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByDictionary *copied = [[TyphoonInjectionByDictionary alloc] init]; copied.injections = self.injections; copied.requiredClass = self.requiredClass; [self copyBasePropertiesTo:copied]; return copied; } - (BOOL)isEqualToCustom:(TyphoonInjectionByDictionary *)injection { return [self.injections isEqualToDictionary:injection.injections] && self.requiredClass == injection.requiredClass; } - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { Class dictionaryClass = [self dictionaryClassForContext:context]; TyphoonInjectionContext *contextForValues = [context copy]; contextForValues.destinationType = [TyphoonTypeDescriptor descriptorWithEncodedType:@encode(id)]; [self buildDictionaryWithClass:dictionaryClass context:contextForValues completion:^(id dictionary) { result(dictionary); }]; } - (NSUInteger)customHash { return TyphoonHashByAppendingInteger([self.injections hash], [_requiredClass hash]); } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByFactoryReference.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" #import "TyphoonInjectionByReference.h" @interface TyphoonInjectionByFactoryReference : TyphoonInjectionByReference @property(nonatomic, readonly) NSString *keyPath; - (instancetype)initWithReference:(NSString *)reference args:(TyphoonRuntimeArguments *)referenceArguments keyPath:(NSString *)keyPath; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByFactoryReference.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByFactoryReference.h" #import "TyphoonComponentFactory+InstanceBuilder.h" #import "TyphoonCallStack.h" #import "TyphoonStackElement.h" #import "NSInvocation+TCFUnwrapValues.h" #import "TyphoonUtils.h" @implementation TyphoonInjectionByFactoryReference - (instancetype)initWithReference:(NSString *)reference args:(TyphoonRuntimeArguments *)referenceArguments keyPath:(NSString *)keyPath { self = [super initWithReference:reference args:referenceArguments]; if (self) { _keyPath = keyPath; } return self; } #pragma mark - Overrides - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByFactoryReference *copied = [[TyphoonInjectionByFactoryReference alloc] initWithReference:self.reference args:self.referenceArguments keyPath:self.keyPath]; [self copyBasePropertiesTo:copied]; return copied; } - (BOOL)isEqualToCustom:(TyphoonInjectionByFactoryReference *)injection { return [super isEqualToCustom:injection] && [self.keyPath isEqualToString:injection.keyPath]; } - (id)resolveReferenceWithContext:(TyphoonInjectionContext *)context { id referenceInstance = [super resolveReferenceWithContext:context]; return [referenceInstance valueForKeyPath:self.keyPath]; } - (NSUInteger)customHash { return TyphoonHashByAppendingInteger([super customHash], [self.keyPath hash]); } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByObjectFromString.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" @interface TyphoonInjectionByObjectFromString : TyphoonAbstractInjection @property(nonatomic, strong) NSString *textValue; - (instancetype)initWithString:(NSString *)string; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByObjectFromString.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByObjectFromString.h" #import "TyphoonComponentFactory.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonPrimitiveTypeConverter.h" #import "TyphoonTypeConverterRegistry.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonDefinition.h" #import "NSInvocation+TCFUnwrapValues.h" @implementation TyphoonInjectionByObjectFromString - (instancetype)initWithString:(NSString *)string { self = [super init]; if (self) { _textValue = string; } return self; } #pragma mark - Overrides - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByObjectFromString *copied = [[TyphoonInjectionByObjectFromString alloc] initWithString:self.textValue]; [self copyBasePropertiesTo:copied]; return copied; } - (BOOL)isEqualToCustom:(TyphoonInjectionByObjectFromString *)injection { return [self.textValue isEqualToString:injection.textValue]; } - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { TyphoonTypeDescriptor *type = context.destinationType; TyphoonComponentFactory *factory = context.factory; id value = [self convertText:self.textValue toType:type withTypeConverterRegistry:factory.typeConverterRegistry]; result(value); } - (id)convertText:(NSString *)text toType:(TyphoonTypeDescriptor *)type withTypeConverterRegistry:(TyphoonTypeConverterRegistry *)typeConverterRegistry { // First, let's see if the text explicitly states the value type. NSString *typeStringFromText = [TyphoonTypeConversionUtils typeFromTextValue:text]; if (typeStringFromText) { id converter = [typeConverterRegistry converterForType:typeStringFromText]; if (converter) { return [converter convert:text]; } } // In case we know the type from the method argument, let's try to use it. if (type) { if (type.isPrimitive) { TyphoonPrimitiveTypeConverter *converter = [typeConverterRegistry primitiveTypeConverter]; return [converter valueFromText:text withType:type]; } else { NSString *typeString; if (type.typeBeingDescribed) { typeString = NSStringFromClass(type.typeBeingDescribed); } else { typeString = [NSString stringWithFormat:@"<%@>", type.declaredProtocol]; } id converter = [typeConverterRegistry converterForType:typeString]; if (converter) { return [converter convert:text]; } } } // Fallback to injecting string. return text; } - (NSUInteger)customHash { return [_textValue hash]; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByObjectInstance.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" @interface TyphoonInjectionByObjectInstance : TyphoonAbstractInjection @property(nonatomic, strong, readonly) id objectInstance; - (instancetype)initWithObjectInstance:(id)objectInstance; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByObjectInstance.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByObjectInstance.h" #import "NSInvocation+TCFUnwrapValues.h" @implementation TyphoonInjectionByObjectInstance - (instancetype)initWithObjectInstance:(id)objectInstance { self = [super init]; if (self) { _objectInstance = objectInstance; } return self; } #pragma mark - Overrides - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByObjectInstance *copied = [[TyphoonInjectionByObjectInstance alloc] initWithObjectInstance:self.objectInstance]; [self copyBasePropertiesTo:copied]; return copied; } - (BOOL)isEqualToCustom:(TyphoonInjectionByObjectInstance *)injection { return [self.objectInstance isEqual:injection.objectInstance]; } - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { result(_objectInstance); } - (NSUInteger)customHash { return [_objectInstance hash]; } - (NSString *)customDescription { return [NSString stringWithFormat:@"instanceClass = %@, ", [self.objectInstance class]]; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByReference.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" @interface TyphoonInjectionByReference : TyphoonAbstractInjection @property(nonatomic, strong, readonly) NSString *reference; @property(nonatomic, strong, readonly) TyphoonRuntimeArguments *referenceArguments; - (instancetype)initWithReference:(NSString *)reference args:(TyphoonRuntimeArguments *)referenceArguments; - (void)resolveCircularDependencyWithContext:(TyphoonInjectionContext *)context block:(dispatch_block_t)block; - (id)resolveReferenceWithContext:(TyphoonInjectionContext *)context; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByReference.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByReference.h" #import "TyphoonComponentFactory+InstanceBuilder.h" #import "TyphoonInjectionByRuntimeArgument.h" #import "TyphoonCallStack.h" #import "TyphoonStackElement.h" #import "NSInvocation+TCFUnwrapValues.h" #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonUtils.h" @implementation TyphoonInjectionByReference - (instancetype)initWithReference:(NSString *)reference args:(TyphoonRuntimeArguments *)referenceArguments { self = [super init]; if (self) { _reference = reference; _referenceArguments = referenceArguments; } return self; } #pragma mark - Overrides - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByReference *copied = [[TyphoonInjectionByReference alloc] initWithReference:_reference args:_referenceArguments]; [self copyBasePropertiesTo:copied]; return copied; } - (BOOL)isEqualToCustom:(TyphoonInjectionByReference *)injection { return [self.reference isEqual:injection.reference] && [self.referenceArguments isEqual:injection.referenceArguments]; } - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { if (context.raiseExceptionIfCircular) { result([self resolveReferenceWithContext:context]); } else { [self resolveCircularDependencyWithContext:context block:^{ result([self resolveReferenceWithContext:context]); }]; } } #pragma mark - Protected - (void)resolveCircularDependencyWithContext:(TyphoonInjectionContext *)context block:(dispatch_block_t)block { TyphoonRuntimeArguments *args = [TyphoonRuntimeArguments argumentsFromRuntimeArguments:context.args appliedToReferenceArguments:_referenceArguments]; [context.factory resolveCircularDependency:self.reference args:args resolvedBlock:^(BOOL isCircular) { block(); }]; } //Raises circular dependencies exception if already initializing. - (id)resolveReferenceWithContext:(TyphoonInjectionContext *)context { TyphoonRuntimeArguments *args = [TyphoonRuntimeArguments argumentsFromRuntimeArguments:context.args appliedToReferenceArguments:_referenceArguments]; id referenceInstance = [[[context.factory stack] peekForKey:self.reference args:args] instance]; if (!referenceInstance) { referenceInstance = [context.factory componentForKey:self.reference args:args]; } return referenceInstance; } - (NSUInteger)customHash { return TyphoonHashByAppendingInteger([_reference hash], [_referenceArguments hash]); } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByRuntimeArgument.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" @interface TyphoonInjectionByRuntimeArgument : TyphoonAbstractInjection @property(nonatomic, readonly) NSUInteger runtimeArgumentIndex; /* index is zero-based */ - (instancetype)initWithArgumentIndex:(NSUInteger)index; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByRuntimeArgument.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByRuntimeArgument.h" #import "TyphoonRuntimeArguments.h" #import "NSInvocation+TCFUnwrapValues.h" @implementation TyphoonInjectionByRuntimeArgument - (NSString *)customDescription { return [NSString stringWithFormat:@"runtimeIndex = %d, ", (int)self.runtimeArgumentIndex]; } - (instancetype)initWithArgumentIndex:(NSUInteger)index { self = [super init]; if (self) { _runtimeArgumentIndex = index; } return self; } - (id)forwardingTargetForSelector:(SEL)aSelector { [NSException raise:NSInvalidArgumentException format:@"You can't call a method on the runtime argument being passed in. It has to be passed in as-is"]; return nil; } #pragma mark - Overrides - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { id injection = [context.args argumentValueAtIndex:self.runtimeArgumentIndex]; [injection valueToInjectWithContext:context completion:result]; } - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByRuntimeArgument *copied = [[TyphoonInjectionByRuntimeArgument alloc] initWithArgumentIndex:self.runtimeArgumentIndex]; [self copyBasePropertiesTo:copied]; return copied; } - (BOOL)isEqualToCustom:(TyphoonInjectionByRuntimeArgument *)injection { return self.runtimeArgumentIndex == injection.runtimeArgumentIndex; } - (NSUInteger)customHash { return _runtimeArgumentIndex; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByType.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAbstractInjection.h" @interface TyphoonInjectionByType : TyphoonAbstractInjection @property (nonatomic, strong) id explicitClassOrProtocol; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionByType.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionByType.h" #import "TyphoonComponentFactory.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonComponentFactory+InstanceBuilder.h" #import "TyphoonDefinition.h" @implementation TyphoonInjectionByType #pragma mark - Overrides - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionByType *copied = [[TyphoonInjectionByType alloc] init]; [self copyBasePropertiesTo:copied]; copied.explicitClassOrProtocol = self.explicitClassOrProtocol; return copied; } - (BOOL)isEqualToCustom:(id)injection { return YES; } - (void)valueToInjectWithContext:(TyphoonInjectionContext *)context completion:(TyphoonInjectionValueBlock)result { id classOrProtocol = self.explicitClassOrProtocol; if (!classOrProtocol) { classOrProtocol = context.destinationType.classOrProtocol; } if (!classOrProtocol) { if (self.type == TyphoonInjectionTypeProperty) { [NSException raise:NSInternalInconsistencyException format:@"Can't recognize type for property '%@' of class '%@'. Make sure that @property exists and has correct type.", self.propertyName, context.classUnderConstruction]; } else { [NSException raise:NSInternalInconsistencyException format:@"Only property injection support InjectionByType"]; } } TyphoonDefinition *definition = [context.factory definitionForType:classOrProtocol]; [context.factory resolveCircularDependency:definition.key args:context.args resolvedBlock:^(BOOL isCircular) { result([context.factory componentForKey:definition.key]); }]; } - (NSUInteger)customHash { return [self.explicitClassOrProtocol hash]; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionContext.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeDescriptor.h" #import "TyphoonComponentFactory.h" @class TyphoonRuntimeArguments; typedef void(^TyphoonInjectionValueBlock)(id value); @interface TyphoonInjectionContext : NSObject @property(nonatomic, strong, readonly) TyphoonComponentFactory* factory; @property(nonatomic, strong, readonly) TyphoonRuntimeArguments* args; @property(nonatomic, readonly) BOOL raiseExceptionIfCircular; /** Class of destination instance, - used only for better exception description */ @property(nonatomic, assign, readwrite) Class classUnderConstruction; @property(nonatomic, strong, readwrite) TyphoonTypeDescriptor* destinationType; - (instancetype)initWithFactory:(TyphoonComponentFactory*)factory args:(TyphoonRuntimeArguments*)args raiseExceptionIfCircular:(BOOL)raiseExceptionIfCircular; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjectionContext.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionContext.h" #import "TyphoonRuntimeArguments.h" @interface TyphoonInjectionContext () @property(nonatomic, strong, readwrite) TyphoonComponentFactory* factory; @property(nonatomic, strong, readwrite) TyphoonRuntimeArguments* args; @property(nonatomic, readwrite) BOOL raiseExceptionIfCircular; @end @implementation TyphoonInjectionContext //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction - (instancetype)initWithFactory:(TyphoonComponentFactory*)factory args:(TyphoonRuntimeArguments*)args raiseExceptionIfCircular:(BOOL)raiseExceptionIfCircular { self = [super init]; if (self) { _factory = factory; _args = args; _raiseExceptionIfCircular = raiseExceptionIfCircular; } return self; } //------------------------------------------------------------------------------------------- - (id)copyWithZone:(NSZone*)zone { TyphoonInjectionContext* copied = [[TyphoonInjectionContext allocWithZone:zone] init]; copied.factory = self.factory; copied.args = self.args; copied.destinationType = self.destinationType; copied.classUnderConstruction = self.classUnderConstruction; copied.raiseExceptionIfCircular = self.raiseExceptionIfCircular; return copied; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjections.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import id TyphoonInjectionMatchedByType(void); id TyphoonInjectionWithType(id classOrProtocol); id TyphoonInjectionWithObjectFromString(NSString *string); id TyphoonInjectionWithCollectionAndType(id collection, Class requiredClass); id TyphoonInjectionWithDictionaryAndType(id dictionary, Class requiredClass); id TyphoonInjectionWithRuntimeArgumentAtIndex(NSUInteger argumentIndex); id TyphoonInjectionWithRuntimeArgumentAtIndexWrappedIntoBlock(NSUInteger argumentIndex); id TyphoonInjectionWithObject(id object); id TyphoonInjectionWithReference(NSString *reference); id TyphoonInjectionWithConfigKey(NSString *configKey); id TyphoonInjectionWithCurrentRuntimeArguments(void); id TyphoonMakeInjectionFromObjectIfNeeded(id objectOrInjection); BOOL IsTyphoonInjection(id objectOrInjection); ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonInjections.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjections.h" #import "TyphoonInjectionByType.h" #import "TyphoonInjectionByReference.h" #import "TyphoonInjectionByObjectInstance.h" #import "TyphoonInjectionByObjectFromString.h" #import "TyphoonInjectionByCollection.h" #import "TyphoonInjectionByRuntimeArgument.h" #import "TyphoonInjectionByDictionary.h" #import "TyphoonInjectionByCurrentRuntimeArguments.h" #import "TyphoonObjectWithCustomInjection.h" #import "TyphoonInjectionByConfig.h" #import "TyphoonIntrospectionUtils.h" #import static const char *typhoonRuntimeArgumentBlockWrapperKey; BOOL IsWrappedIntoTyphoonBlock(id objectOrBlock) ; BOOL HasCustomInjection(id objectOrInjection) ; id TyphoonInjectionMatchedByType(void) { return [[TyphoonInjectionByType alloc] init]; } id TyphoonInjectionWithType(id classOrProtocol) { TyphoonInjectionByType *injection = [TyphoonInjectionByType new]; injection.explicitClassOrProtocol = classOrProtocol; return injection; } id TyphoonInjectionWithObjectFromString(NSString *string) { return [[TyphoonInjectionByObjectFromString alloc] initWithString:string]; } id TyphoonInjectionWithCollectionAndType(id collection, Class requiredClass) { return [[TyphoonInjectionByCollection alloc] initWithCollection:collection requiredClass:requiredClass]; } id TyphoonInjectionWithDictionaryAndType(id dictionary, Class requiredClass) { return [[TyphoonInjectionByDictionary alloc] initWithDictionary:dictionary requiredClass:requiredClass]; } id TyphoonInjectionWithRuntimeArgumentAtIndex(NSUInteger argumentIndex) { return [[TyphoonInjectionByRuntimeArgument alloc] initWithArgumentIndex:argumentIndex]; } id TyphoonInjectionWithRuntimeArgumentAtIndexWrappedIntoBlock(NSUInteger argumentIndex) { id(^block)(void) = ^{return[[TyphoonInjectionByRuntimeArgument alloc] initWithArgumentIndex:argumentIndex];}; objc_setAssociatedObject(block, &typhoonRuntimeArgumentBlockWrapperKey, @YES, OBJC_ASSOCIATION_RETAIN_NONATOMIC); return block; } id TyphoonInjectionWithObject(id object) { return [[TyphoonInjectionByObjectInstance alloc] initWithObjectInstance:object]; } id TyphoonInjectionWithReference(NSString *reference) { return [[TyphoonInjectionByReference alloc] initWithReference:reference args:nil]; } id TyphoonInjectionWithConfigKey(NSString *configKey) { return [[TyphoonInjectionByConfig alloc] initWithConfigKey:configKey]; } id TyphoonInjectionWithCurrentRuntimeArguments(void) { return [TyphoonInjectionByCurrentRuntimeArguments new]; } id TyphoonMakeInjectionFromObjectIfNeeded(id objectOrInjection) { id injection = nil; if (HasCustomInjection(objectOrInjection)) { injection = [objectOrInjection typhoonCustomObjectInjection]; } else if (IsTyphoonInjection(objectOrInjection)) { injection = objectOrInjection; } else if (IsWrappedIntoTyphoonBlock(objectOrInjection)) { injection = ((id(^)(void))objectOrInjection)(); } else { injection = TyphoonInjectionWithObject(objectOrInjection); } return injection; } BOOL HasCustomInjection(id objectOrInjection) { return !IsClass(objectOrInjection) && [objectOrInjection conformsToProtocol:@protocol(TyphoonObjectWithCustomInjection)]; } BOOL IsTyphoonInjection(id objectOrInjection) { return [objectOrInjection conformsToProtocol:@protocol(TyphoonPropertyInjection)] || [objectOrInjection conformsToProtocol:@protocol(TyphoonParameterInjection)]; } BOOL IsWrappedIntoTyphoonBlock(id objectOrBlock) { return [objc_getAssociatedObject(objectOrBlock, &typhoonRuntimeArgumentBlockWrapperKey) isEqualToNumber:@YES]; } ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonParameterInjection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjection.h" @protocol TyphoonParameterInjection - (void)setParameterIndex:(NSUInteger)index; - (NSUInteger)parameterIndex; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Injections/TyphoonPropertyInjection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjection.h" @protocol TyphoonPropertyInjection - (void)setPropertyName:(NSString *)name; - (NSString *)propertyName; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/Collections+CustomInjection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonObjectWithCustomInjection.h" @interface NSArray (TyphoonObjectWithCustomInjection) @end @interface NSSet (TyphoonObjectWithCustomInjection) @end @interface NSOrderedSet (TyphoonObjectWithCustomInjection) @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/Collections+CustomInjection.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "Collections+CustomInjection.h" #import "TyphoonInjectionByCollection.h" #import "TyphoonInjections.h" #import "TyphoonLinkerCategoryBugFix.h" TYPHOON_LINK_CATEGORY(CollectionsCustomInjections) static __inline__ BOOL IsContainsTyphoonObjectInCollection(id collection) { BOOL foundTyphoonObject = NO; for (id object in collection) { if ([object conformsToProtocol:@protocol(TyphoonObjectWithCustomInjection)] || IsTyphoonInjection(object)) { foundTyphoonObject = YES; break; } } return foundTyphoonObject; } static __inline__ id InjectionForCollection(id collection) { if (IsContainsTyphoonObjectInCollection(collection)) { return TyphoonInjectionWithCollectionAndType(collection, [collection class]); } else { return TyphoonInjectionWithObject(collection); } } @implementation NSArray (TyphoonObjectWithCustomInjection) - (id )typhoonCustomObjectInjection { return InjectionForCollection(self); } @end @implementation NSSet (TyphoonObjectWithCustomInjection) - (id )typhoonCustomObjectInjection { return InjectionForCollection(self); } @end @implementation NSOrderedSet (TyphoonObjectWithCustomInjection) - (id )typhoonCustomObjectInjection { return InjectionForCollection(self); } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/NSDictionary+CustomInjection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonObjectWithCustomInjection.h" @interface NSDictionary (TyphoonObjectWithCustomInjection) @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/NSDictionary+CustomInjection.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSDictionary+CustomInjection.h" #import "TyphoonInjectionByDictionary.h" #import "TyphoonInjections.h" #import "TyphoonLinkerCategoryBugFix.h" TYPHOON_LINK_CATEGORY(NSDictionaryCustomInjections) @implementation NSDictionary (TyphoonObjectWithCustomInjection) - (id )typhoonCustomObjectInjection { BOOL containsTyphoonObject = NO; id object = nil; NSEnumerator *objectEnumerator = [self objectEnumerator]; while ((object = [objectEnumerator nextObject])) { if (IsTyphoonInjection(object) || [object conformsToProtocol:@protocol(TyphoonObjectWithCustomInjection)]) { containsTyphoonObject = YES; break; } } if (containsTyphoonObject) { return TyphoonInjectionWithDictionaryAndType(self, [self class]); } else { return TyphoonInjectionWithObject(self); } } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonBlockDefinition+InstanceBuilder.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonBlockDefinition.h" @interface TyphoonBlockDefinition (InstanceBuilder) @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonBlockDefinition+InstanceBuilder.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonBlockDefinition+InstanceBuilder.h" #import "TyphoonBlockDefinition+Internal.h" #import "TyphoonDefinition+Internal.h" #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonComponentFactory.h" #import "TyphoonBlockDefinitionController.h" #import "TyphoonInjection.h" #import "NSInvocation+TCFUnwrapValues.h" #import "TyphoonLinkerCategoryBugFix.h" #import "TyphoonAssembly.h" #import "NSInvocation+TCFCustomImplementation.h" #import "TyphoonAssembly+TyphoonAssemblyFriend.h" TYPHOON_LINK_CATEGORY(TyphoonBlockDefinition_InstanceBuilder) @implementation TyphoonBlockDefinition (InstanceBuilder) #pragma mark - Instance Builder - (id)initializeInstanceWithArgs:(TyphoonRuntimeArguments *)args factory:(TyphoonComponentFactory *)factory { if (self.hasInitializerBlock) { TyphoonInjectionContext *context = [[TyphoonInjectionContext alloc] initWithFactory:factory args:args raiseExceptionIfCircular:YES]; context.classUnderConstruction = self.type; __block id instance; [[TyphoonBlockDefinitionController currentController] useInitializerRouteWithDefinition:self injectionContext:context withinBlock:^{ instance = [self invokeAssemblySelector]; }]; return instance; } else { return [super initializeInstanceWithArgs:args factory:factory]; } } - (void)doInjectionEventsOn:(id)instance withArgs:(TyphoonRuntimeArguments *)args factory:(TyphoonComponentFactory *)factory { if (self.hasInjectionsBlock) { TyphoonInjectionContext *context = [[TyphoonInjectionContext alloc] initWithFactory:factory args:args raiseExceptionIfCircular:NO]; context.classUnderConstruction = self.type; [[TyphoonBlockDefinitionController currentController] useInjectionsRouteWithDefinition:self instance:instance injectionContext:context withinBlock:^{ [self invokeAssemblySelector]; }]; } [super doInjectionEventsOn:instance withArgs:args factory:factory]; } #pragma mark - Invocation - (id)invokeAssemblySelector { TyphoonAssembly *assembly = self.assembly; id target = assembly.accessor; SEL selector = self.assemblySelector; if (!target || !selector) { [NSException raise:NSInternalInconsistencyException format:@"Assembly & assemblySelector should be set on TyphoonBlockDefinition in order to build an instance."]; } NSMethodSignature *signature = [target methodSignatureForSelector:selector]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setTarget:target]; [invocation setSelector:selector]; [invocation retainArguments]; TyphoonInjectionContext *context = [TyphoonBlockDefinitionController currentController].injectionContext; [context.args enumerateArgumentsUsingBlock:^(id argument, NSUInteger index, BOOL *stop) { [argument valueToInjectWithContext:context completion:^(id value) { [invocation typhoon_setArgumentObject:value atIndex:(NSInteger)index + 2]; }]; }]; // Don't use [NSInvocation typhoon_resultOfInvokingOnInstance] here for performance reasons. // We're invoking a definition selector on assembly so there's no need to check for memory management stuff. IMP assemblyMethod = [assembly methodForSelector:selector]; [invocation invokeWithCustomImplementation:assemblyMethod]; void *unsafeResult; [invocation getReturnValue:&unsafeResult]; id result = (__bridge id)unsafeResult; return result; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonBlockDefinition+Internal.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonBlockDefinition.h" @interface TyphoonBlockDefinition () @property (nonatomic, assign) BOOL hasInitializerBlock; @property (nonatomic, assign) BOOL hasInjectionsBlock; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonBlockDefinitionController.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonBlockDefinition; @class TyphoonInjectionContext; typedef NS_ENUM(NSInteger, TyphoonBlockDefinitionRoute) { TyphoonBlockDefinitionRouteInvalid, TyphoonBlockDefinitionRouteConfiguration, TyphoonBlockDefinitionRouteInitializer, TyphoonBlockDefinitionRouteInjections }; @interface TyphoonBlockDefinitionController : NSObject + (instancetype)currentController; @property (nonatomic, assign, readonly) TyphoonBlockDefinitionRoute route; @property (nonatomic, assign, readonly, getter = isBuildingInstance) BOOL buildingInstance; @property (nonatomic, strong, readonly) TyphoonBlockDefinition *definition; @property (nonatomic, strong, readonly) id instance; @property (nonatomic, strong, readonly) TyphoonInjectionContext *injectionContext; - (void)useConfigurationRouteWithinBlock:(void (^)(void))block; - (void)useInitializerRouteWithDefinition:(TyphoonBlockDefinition *)definition injectionContext:(TyphoonInjectionContext *)context withinBlock:(void (^)(void))block; - (void)useInjectionsRouteWithDefinition:(TyphoonBlockDefinition *)definition instance:(id)instance injectionContext:(TyphoonInjectionContext *)context withinBlock:(void (^)(void))block; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonBlockDefinitionController.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonBlockDefinitionController.h" static NSString * const kThreadDictionaryKey = @"org.typhoonframework.blockDefinitionController"; @implementation TyphoonBlockDefinitionController #pragma mark - Thread-local Instance + (instancetype)currentController { NSMutableDictionary *threadDictionary = [NSThread currentThread].threadDictionary; TyphoonBlockDefinitionController *controller = threadDictionary[kThreadDictionaryKey]; if (!controller) { controller = [[TyphoonBlockDefinitionController alloc] init]; threadDictionary[kThreadDictionaryKey] = controller; } return controller; } #pragma mark - Public Methods - (BOOL)isBuildingInstance { return _route == TyphoonBlockDefinitionRouteInitializer || _route == TyphoonBlockDefinitionRouteInjections; } - (void)useConfigurationRouteWithinBlock:(void (^)(void))block { [self useRoute:TyphoonBlockDefinitionRouteConfiguration withinBlock:block]; } - (void)useInitializerRouteWithDefinition:(TyphoonBlockDefinition *)definition injectionContext:(TyphoonInjectionContext *)context withinBlock:(void (^)(void))block { _definition = definition; _injectionContext = context; [self useRoute:TyphoonBlockDefinitionRouteInitializer withinBlock:block]; } - (void)useInjectionsRouteWithDefinition:(TyphoonBlockDefinition *)definition instance:(id)instance injectionContext:(TyphoonInjectionContext *)context withinBlock:(void (^)(void))block { _definition = definition; _instance = instance; _injectionContext = context; [self useRoute:TyphoonBlockDefinitionRouteInjections withinBlock:block]; } #pragma mark - Private Methods - (void)useRoute:(TyphoonBlockDefinitionRoute)route withinBlock:(void (^)(void))block { _route = route; block(); _route = TyphoonBlockDefinitionRouteInvalid; _definition = nil; _instance = nil; _injectionContext = nil; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonDefinition+InstanceBuilder.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition.h" @protocol TyphoonPropertyInjection; @protocol TyphoonInjection; @class TyphoonComponentFactory; @class TyphoonRuntimeArguments; @interface TyphoonDefinition (InstanceBuilder) - (id)initializeInstanceWithArgs:(TyphoonRuntimeArguments *)args factory:(TyphoonComponentFactory *)factory; - (void)doInjectionEventsOn:(id)instance withArgs:(TyphoonRuntimeArguments *)args factory:(TyphoonComponentFactory *)factory; - (void)doAfterAllInjectionsOn:(id)instance; - (id)targetForInitializerWithFactory:(TyphoonComponentFactory *)factory args:(TyphoonRuntimeArguments *)args; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonDefinition+InstanceBuilder.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonLinkerCategoryBugFix.h" TYPHOON_LINK_CATEGORY(TyphoonDefinition_InstanceBuilder) #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonMethod+InstanceBuilder.h" #import "TyphoonInjectionByType.h" #import "TyphoonInjectionByRuntimeArgument.h" #import "TyphoonComponentFactory.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonIntrospectionUtils.h" #import "NSObject+TyphoonIntrospectionUtils.h" #import "NSObject+PropertyInjection.h" #import "NSInvocation+TCFInstanceBuilder.h" @implementation TyphoonDefinition (InstanceBuilder) #pragma mark - Instance Builder - (id)initializeInstanceWithArgs:(TyphoonRuntimeArguments *)args factory:(TyphoonComponentFactory *)factory { __block id instance = [self targetForInitializerWithFactory:factory args:args]; if (self.initializer && instance) { BOOL isClass = IsClass(instance); TyphoonInjectionContext *context = [[TyphoonInjectionContext alloc] initWithFactory:factory args:args raiseExceptionIfCircular:YES]; context.classUnderConstruction = isClass ? (Class)instance : [instance class]; [self.initializer createInvocationWithContext:context completion:^(NSInvocation *invocation) { if (isClass && ![self.initializer isClassMethodOnClass:context.classUnderConstruction]) { instance = [invocation typhoon_resultOfInvokingOnAllocationForClass:context.classUnderConstruction]; } else { instance = [invocation typhoon_resultOfInvokingOnInstance:instance]; } }]; } return instance; } - (void)doInjectionEventsOn:(id)instance withArgs:(TyphoonRuntimeArguments *)args factory:(TyphoonComponentFactory *)factory { if (self.beforeInjections) { [self doMethodInjection:self.beforeInjections onInstance:instance args:args factory:factory]; } for (id property in self.injectedProperties) { [self doPropertyInjectionOn:instance property:property args:args factory:factory]; } for (TyphoonMethod *method in self.injectedMethods) { [self doMethodInjection:method onInstance:instance args:args factory:factory]; } if (self.afterInjections) { [self doMethodInjection:self.afterInjections onInstance:instance args:args factory:factory]; } } - (void)doAfterAllInjectionsOn:(id)instance { if (self.afterAllInjections && [instance respondsToSelector:self.afterAllInjections]) { void(*afterInjectionsMethod)(id, SEL) = (void ( *)(id, SEL)) [instance methodForSelector:self.afterAllInjections]; afterInjectionsMethod(instance, self.afterAllInjections); } } - (id)targetForInitializerWithFactory:(TyphoonComponentFactory *)factory args:(TyphoonRuntimeArguments *)args { return self.type; } #pragma mark - Method Injection - (void)doMethodInjection:(TyphoonMethod *)method onInstance:(id)instance args:(TyphoonRuntimeArguments *)args factory:(TyphoonComponentFactory *)factory { if (instance == nil) { return; } TyphoonInjectionContext *context = [[TyphoonInjectionContext alloc] initWithFactory:factory args:args raiseExceptionIfCircular:NO]; context.classUnderConstruction = [instance class]; [method createInvocationWithContext:context completion:^(NSInvocation *invocation) { [invocation invokeWithTarget:instance]; }]; } #pragma mark - Property Injection - (void)doPropertyInjectionOn:(id)instance property:(id)property args:(TyphoonRuntimeArguments *)args factory:(TyphoonComponentFactory *)factory { TyphoonInjectionContext *context = [[TyphoonInjectionContext alloc] initWithFactory:factory args:args raiseExceptionIfCircular:NO]; context.destinationType = [instance typhoonTypeForPropertyNamed:property.propertyName]; context.classUnderConstruction = [instance class]; [property valueToInjectWithContext:context completion:^(id value) { [instance typhoon_injectValue:value forPropertyName:property.propertyName]; }]; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonDefinition+Internal.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition.h" @class TyphoonMethod; @class TyphoonRuntimeArguments; @class TyphoonAssembly; @interface TyphoonDefinition () @property (nonatomic) TyphoonRuntimeArguments *currentRuntimeArguments; /** * This flag used to distinguish definitions from reference to them. First time, when definition created, processed flag set to NO, * but next time, when this definition returned by reference (shortcut with another runtime args) processed flag will be set to YES. */ @property (nonatomic) BOOL processed; /** * This flag indicated where the scope was changed manually by the user. */ @property (nonatomic, readonly, getter = isScopeSetByUser) BOOL scopeSetByUser; /** * An assembly from which this definition was built. * The property must be weak to prevent a retain cycle between factory, definition and assembly. */ @property (nonatomic, weak) TyphoonAssembly *assembly; /** * An assembly selector this definition has originated from. */ @property (nonatomic, assign) SEL assemblySelector; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonFactoryDefinition.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinition.h" @interface TyphoonFactoryDefinition : TyphoonDefinition @property (nonatomic, strong) id classOrProtocolForAutoInjection; - (id)initWithFactory:(id)factory selector:(SEL)selector parameters:(void(^)(TyphoonMethod *method))params; - (void)useInitializer:(SEL)selector parameters:(void (^)(TyphoonMethod *initializer))parametersBlock __attribute((unavailable("Initializer of TyphoonFactoryDefinition cannot be changed"))); - (void)useInitializer:(SEL)selector __attribute((unavailable("Initializer of TyphoonFactoryDefinition cannot be changed"))); @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonFactoryDefinition.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonFactoryDefinition.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonComponentFactory.h" #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonAbstractInjection.h" #import "TyphoonInjections.h" @implementation TyphoonFactoryDefinition { id _factoryInjection; }; - (id)initWithFactory:(id)factory selector:(SEL)selector parameters:(void (^)(TyphoonMethod *method))params { self = [super initWithClass:[NSObject class] key:nil]; if (self) { _factoryInjection = TyphoonMakeInjectionFromObjectIfNeeded(factory); self.scope = TyphoonScopePrototype; [super useInitializer:selector parameters:params]; } return self; } #pragma mark - Overriden methods - (id)targetForInitializerWithFactory:(TyphoonComponentFactory *)factory args:(TyphoonRuntimeArguments *)args { __block id result = nil; TyphoonInjectionContext *context = [[TyphoonInjectionContext alloc] initWithFactory:factory args:args raiseExceptionIfCircular:YES]; [_factoryInjection valueToInjectWithContext:context completion:^(id value) { result = value; }]; return result; } - (void)useInitializer:(SEL)selector parameters:(void (^)(TyphoonMethod *initializer))parametersBlock { NSAssert(NO, @"You cannot change initializer of TyphoonFactoryDefinition"); } - (void)useInitializer:(SEL)selector { NSAssert(NO, @"You cannot change initializer of TyphoonFactoryDefinition"); } - (BOOL)isCandidateForInjectedClass:(Class)clazz includeSubclasses:(BOOL)includeSubclasses { BOOL result = NO; if (self.autoInjectionVisibility & TyphoonAutoInjectVisibilityByClass) { BOOL isSameClass = self.classOrProtocolForAutoInjection == clazz; BOOL isSubclass = includeSubclasses && [self.classOrProtocolForAutoInjection isSubclassOfClass:clazz]; result = isSameClass || isSubclass; } return result; } - (BOOL)isCandidateForInjectedProtocol:(Protocol *)aProtocol { Class componentClass = IsClass(self.classOrProtocolForAutoInjection) ? self.classOrProtocolForAutoInjection : nil; Protocol *componentProtocol = IsProtocol(self.classOrProtocolForAutoInjection) ? self.classOrProtocolForAutoInjection : nil; BOOL result = NO; if (self.autoInjectionVisibility & TyphoonAutoInjectVisibilityByProtocol) { if (componentClass) { result = [componentClass conformsToProtocol:aProtocol]; } else if (componentProtocol) { result = componentProtocol == aProtocol; } } return result; } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { TyphoonFactoryDefinition *copy = [super copyWithZone:zone]; copy->_classOrProtocolForAutoInjection = _classOrProtocolForAutoInjection; copy->_factoryInjection = [_factoryInjection copyWithZone:zone]; return copy; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonInjectionDefinition.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOONFRAMEWORK.ORG // Copyright 2015 typhoonframework.org Pty Ltd // All Rights Reserved. // // NOTICE: Prepared by AppsQuick.ly on behalf of typhoonframework.org. This software // is proprietary information. Unauthorized use is prohibited. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinition.h" #import "TyphoonInjection.h" @interface TyphoonInjectionDefinition : TyphoonDefinition - (instancetype)initWithInjection:(id)injection; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonInjectionDefinition.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOONFRAMEWORK.ORG // Copyright 2015 typhoonframework.org Pty Ltd // All Rights Reserved. // // NOTICE: Prepared by AppsQuick.ly on behalf of typhoonframework.org. This software // is proprietary information. Unauthorized use is prohibited. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonInjectionDefinition.h" #import "TyphoonDefinition+Infrastructure.h" @implementation TyphoonInjectionDefinition { id _injection; } - (instancetype)initWithInjection:(id)injection { self = [super initWithClass:[NSObject class] key:nil]; if (self) { _injection = injection; } return self; } #pragma mark - Overriden methods - (TyphoonMethod *)initializer { return nil; } - (id)targetForInitializerWithFactory:(TyphoonComponentFactory *)factory args:(TyphoonRuntimeArguments *)args { TyphoonInjectionContext *context = [[TyphoonInjectionContext alloc] initWithFactory:factory args:args raiseExceptionIfCircular:YES]; context.destinationType = [TyphoonTypeDescriptor descriptorWithEncodedType:@encode(id)]; __block id valueToInject = nil; [_injection valueToInjectWithContext:context completion:^(id value) { valueToInject = value; }]; return valueToInject; } - (BOOL)isCandidateForInjectedClass:(Class)clazz includeSubclasses:(BOOL)includeSubclasses { return NO; } - (BOOL)isCandidateForInjectedProtocol:(Protocol *)aProtocol { return NO; } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { TyphoonInjectionDefinition *copy = [super copyWithZone:zone]; copy->_injection = [_injection copyWithZone:zone]; return copy; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonObjectWithCustomInjection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonPropertyInjection.h" #import "TyphoonParameterInjection.h" @protocol TyphoonObjectWithCustomInjection - (id )typhoonCustomObjectInjection; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonReferenceDefinition.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinition.h" @interface TyphoonReferenceDefinition : TyphoonDefinition + (instancetype)definitionReferringToComponent:(NSString *)key; @end @interface TyphoonShortcutDefinition : TyphoonDefinition + (instancetype)definitionWithKey:(NSString *)key referringTo:(TyphoonDefinition *)definition; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Internal/TyphoonReferenceDefinition.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonReferenceDefinition.h" #import "TyphoonDefinition+Internal.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonComponentFactory.h" //TODO: Merge TyphoonReferenceDefinition and TyphoonShortcutDefinition with keeping simple logic @implementation TyphoonReferenceDefinition + (instancetype)definitionReferringToComponent:(NSString *)key { return [[self alloc] initWithClass:[NSObject class] key:key]; } @end @implementation TyphoonShortcutDefinition { TyphoonRuntimeArguments *_referringArgs; NSString *_referringKey; }; + (instancetype)definitionWithKey:(NSString *)key referringTo:(TyphoonDefinition *)definition { TyphoonShortcutDefinition *refDefinition = [[TyphoonShortcutDefinition alloc] initWithClass:definition.type key:key]; refDefinition->_referringArgs = definition.currentRuntimeArguments; refDefinition->_referringKey = definition.key; return refDefinition; } #pragma mark - Overriden methods - (id)targetForInitializerWithFactory:(TyphoonComponentFactory *)factory args:(TyphoonRuntimeArguments *)args { if (_referringKey) { return [factory componentForKey:_referringKey args:[TyphoonRuntimeArguments argumentsFromRuntimeArguments:args appliedToReferenceArguments:_referringArgs]]; } else { return [super targetForInitializerWithFactory:factory args:args]; } } - (TyphoonMethod *)initializer { return nil; } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { TyphoonShortcutDefinition *copy = [super copyWithZone:zone]; copy->_referringArgs = [_referringArgs copy]; copy->_referringKey = _referringKey; return copy; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Method/Internal/TyphoonMethod+InstanceBuilder.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonMethod.h" @class TyphoonInjectionContext; @protocol TyphoonParameterInjection; @interface TyphoonMethod (InstanceBuilder) - (NSArray *)injectedParameters; - (void)createInvocationWithContext:(TyphoonInjectionContext *)context completion:(void(^)(NSInvocation *invocation))result; - (BOOL)isClassMethodOnClass:(Class)clazz; - (void)checkParametersCount; - (void)replaceInjection:(id)injection with:(id)injectionToReplace; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Method/Internal/TyphoonMethod+InstanceBuilder.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonLinkerCategoryBugFix.h" #import "TyphoonMethod+InstanceBuilder.h" #import "TyphoonDefinition.h" #import "TyphoonComponentFactory.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonInjectionByObjectFromString.h" #import "TyphoonInjectionByRuntimeArgument.h" #import "TyphoonTypeDescriptor.h" #import "NSArray+TyphoonManualEnumeration.h" #import "NSInvocation+TCFUnwrapValues.h" #import "TyphoonParameterInjection.h" TYPHOON_LINK_CATEGORY(TyphoonInitializer_InstanceBuilder) @implementation TyphoonMethod (InstanceBuilder) //------------------------------------------------------------------------------------------- #pragma mark - Interface Methods - (void)replaceInjection:(id)injection with:(id)injectionToReplace { [injectionToReplace setParameterIndex:[injection parameterIndex]]; NSUInteger index = [_injectedParameters indexOfObject:injection]; [_injectedParameters replaceObjectAtIndex:index withObject:injectionToReplace]; } - (NSArray *)injectedParameters { return [_injectedParameters copy]; } - (void)createInvocationWithContext:(TyphoonInjectionContext *)context completion:(void(^)(NSInvocation *invocation))result { BOOL isClassMethod = [self isClassMethodOnClass:context.classUnderConstruction]; NSMethodSignature *signature = [self methodSignatureWithTarget:context.classUnderConstruction isClassMethod:isClassMethod]; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation retainArguments]; [invocation setSelector:_selector]; [[self injectedParameters] typhoon_enumerateObjectsWithManualIteration:^(id object, id iterator) { NSUInteger index = [object parameterIndex] + 2; context.destinationType = [TyphoonTypeDescriptor descriptorWithEncodedType:[signature getArgumentTypeAtIndex:index]]; [object valueToInjectWithContext:context completion:^(id value) { [invocation typhoon_setArgumentObject:value atIndex:(NSInteger)index]; [iterator next]; }]; } completion:^{ result(invocation); }]; } - (void)checkParametersCount { //TODO: Why does this method cause a crash on Swift in release mode. (Its not needed in release mode, but *why* ) #if DEBUG NSUInteger numberOfArgumentsInSelector = [TyphoonIntrospectionUtils numberOfArgumentsInSelector:_selector]; if (numberOfArgumentsInSelector != [_injectedParameters count]) { NSString *suggestion = @""; if ([_injectedParameters count] - numberOfArgumentsInSelector == 1 && ![NSStringFromSelector(_selector) hasSuffix:@":"]) { suggestion = [NSString stringWithFormat:@"Do you mean '%@:'?", NSStringFromSelector(_selector)]; } else if (numberOfArgumentsInSelector > [_injectedParameters count]) { suggestion = @"Inject with 'nil' if necessary"; } [NSException raise:NSInternalInconsistencyException format:@"Method '%@' has %d parameters, but %d was injected. %@", NSStringFromSelector(_selector), (int)numberOfArgumentsInSelector, (int)[_injectedParameters count], suggestion]; } #endif } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods - (BOOL)isClassMethodOnClass:(Class)_class { BOOL instanceRespondsToSelector = [_class instancesRespondToSelector:_selector]; BOOL classRespondsToSelector = [_class respondsToSelector:_selector]; if (!instanceRespondsToSelector && !classRespondsToSelector) { [NSException raise:NSInvalidArgumentException format:@"Method '%@' not found on '%@'. Did you include the required ':' characters to signify arguments?", NSStringFromSelector(_selector), NSStringFromClass(_class)]; } return classRespondsToSelector && !instanceRespondsToSelector; } - (NSMethodSignature *)methodSignatureWithTarget:(Class)clazz isClassMethod:(BOOL)isClassMethod { return isClassMethod ? [clazz methodSignatureForSelector:_selector] : [clazz instanceMethodSignatureForSelector:_selector]; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Method/TyphoonMethod.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** * @ingroup Definition * * Represents an method to inject for a component. * Used in initializer and method injections * * ##Initializer style injection has the following advantages: * * - Presents a clear contract to put the instance in the required state before use. * - No custom lifecycle methods (before/after property injection) are required. * * ##Initializer injection has the following drawbacks: * * - Not suitable for classes with a very large number of dependencies - a very large initializer method will create poor readability. * - Auto-injection by type is not supported. * - No type introspection for objects injected with a text representation. * * Its generally recommended to use initializer-style injection, unless the above drawbacks will manifest. * */ @interface TyphoonMethod : NSObject { NSMutableArray *_injectedParameters; SEL _selector; } /** * The selector used to initialize the component. */ @property(nonatomic, readonly) SEL selector; - (id)initWithSelector:(SEL)selector; //------------------------------------------------------------------------------------------- #pragma mark - inject - (void)injectParameterWith:(id)injection; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Method/TyphoonMethod.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonMethod.h" #import "TyphoonParameterInjection.h" #import "TyphoonInjections.h" #import "TyphoonUtils.h" @implementation TyphoonMethod { BOOL _needUpdateHash; NSUInteger _hash; } //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction - (id)initWithSelector:(SEL)selector { self = [super init]; if (self) { _injectedParameters = [[NSMutableArray alloc] init]; _selector = selector; _needUpdateHash = YES; } return self; } - (id)init { return [self initWithSelector:nil]; } #pragma mark - manipulations with _injectedParameters - (void)addParameterInjection:(id )injection { [_injectedParameters addObject:injection]; } - (NSUInteger)indexToAddParameter { return [_injectedParameters count]; } #pragma mark - Injections - (void)injectParameterAtIndex:(NSUInteger)index with:(id)injection { injection = TyphoonMakeInjectionFromObjectIfNeeded(injection); [injection setParameterIndex:index]; [self addParameterInjection:injection]; _needUpdateHash = YES; } - (void)injectParameterWith:(id)injection { [self injectParameterAtIndex:[self indexToAddParameter] with:injection]; } //------------------------------------------------------------------------------------------- #pragma mark - Utility Methods - (id)copyWithZone:(NSZone *)zone { TyphoonMethod *copy = [[TyphoonMethod alloc] initWithSelector:_selector]; for (id parameter in _injectedParameters) { [copy addParameterInjection:[parameter copyWithZone:NSDefaultMallocZone()]]; } return copy; } - (NSUInteger)hash { if (_needUpdateHash) { _hash = [self calculateHash]; _needUpdateHash = NO; } return _hash; } - (NSUInteger)calculateHash { NSUInteger hash = (NSUInteger) sel_getName(_selector); for (id parameter in _injectedParameters) { hash = TyphoonHashByAppendingInteger(hash, [[parameter description] hash]); } return hash; } - (BOOL)isEqual:(id)other { if (other == self) { return YES; } if (!other || ![[other class] isEqual:[self class]]) { return NO; } return [self isEqualToMethod:other]; } - (BOOL)isEqualToMethod:(TyphoonMethod *)method { if (self == method) { return YES; } if (method == nil) { return NO; } return _selector == method.selector && [method->_injectedParameters isEqualToArray:_injectedParameters]; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/Namespacing/TyphoonDefinitionNamespace.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonDefinitionNamespace : NSObject + (instancetype)namespaceWithKey:(NSString *)key; + (instancetype)globalNamespace; @property (strong, nonatomic, readonly) NSString *key; - (BOOL)isGlobalNamespace; @end ================================================ FILE: Pods/Typhoon/Source/Definition/Namespacing/TyphoonDefinitionNamespace.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinitionNamespace.h" static NSString *const TyphoonDefinitionGlobalNamespace = @"TyphoonDefinitionGlobalNamespace"; @interface TyphoonDefinitionNamespace () @property (strong, nonatomic, readwrite) NSString *key; @end @implementation TyphoonDefinitionNamespace + (instancetype)globalNamespace { return [self namespaceWithKey:TyphoonDefinitionGlobalNamespace]; } + (instancetype)namespaceWithKey:(NSString *)key { return [[[self class] alloc] initWithKey:key]; } - (instancetype)initWithKey:(NSString *)key { self = [super init]; if (self) { _key = key; } return self; } - (BOOL)isGlobalNamespace { return [self.key isEqualToString:TyphoonDefinitionGlobalNamespace]; } - (NSUInteger)hash { return [self.key hash]; } - (BOOL)isEqual:(id)other { if (other == self) { return YES; } if (!other || ![[other class] isEqual:[self class]]) { return NO; } return [self.key isEqualToString:((TyphoonDefinitionNamespace *)other).key]; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/TyphoonBlockDefinition.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition.h" typedef id (^TyphoonBlockDefinitionInitializerBlock)(void); typedef void (^TyphoonBlockDefinitionInjectionsBlock)(id instance); @interface TyphoonBlockDefinition : TyphoonDefinition + (id)withBlock:(TyphoonBlockDefinitionInitializerBlock)block; + (id)withBlock:(TyphoonBlockDefinitionInitializerBlock)block configuration:(TyphoonDefinitionBlock)configuration; + (id)withInitializer:(TyphoonBlockDefinitionInitializerBlock)initializer injections:(TyphoonBlockDefinitionInjectionsBlock)injections; + (id)withInitializer:(TyphoonBlockDefinitionInitializerBlock)initializer injections:(TyphoonBlockDefinitionInjectionsBlock)injections configuration:(TyphoonDefinitionBlock)configuration; + (id)withClass:(Class)clazz block:(TyphoonBlockDefinitionInitializerBlock)block; + (id)withClass:(Class)clazz block:(TyphoonBlockDefinitionInitializerBlock)block configuration:(TyphoonDefinitionBlock)configuration; + (id)withClass:(Class)clazz injections:(TyphoonBlockDefinitionInjectionsBlock)injections; + (id)withClass:(Class)clazz injections:(TyphoonBlockDefinitionInjectionsBlock)injections configuration:(TyphoonDefinitionBlock)configuration; + (id)withClass:(Class)clazz initializer:(TyphoonBlockDefinitionInitializerBlock)initializer injections:(TyphoonBlockDefinitionInjectionsBlock)injections configuration:(TyphoonDefinitionBlock)configuration; @end ================================================ FILE: Pods/Typhoon/Source/Definition/TyphoonBlockDefinition.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonBlockDefinition.h" #import "TyphoonBlockDefinition+Internal.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonBlockDefinitionController.h" @implementation TyphoonBlockDefinition #pragma mark - Definitions + (id)withBlock:(TyphoonBlockDefinitionInitializerBlock)block { return [self withInitializer:block injections:nil configuration:nil]; } + (id)withBlock:(TyphoonBlockDefinitionInitializerBlock)block configuration:(TyphoonDefinitionBlock)configuration { return [self withInitializer:block injections:nil configuration:configuration]; } + (id)withInitializer:(TyphoonBlockDefinitionInitializerBlock)initializer injections:(TyphoonBlockDefinitionInjectionsBlock)injections { return [self withInitializer:initializer injections:injections configuration:nil]; } + (id)withInitializer:(TyphoonBlockDefinitionInitializerBlock)initializer injections:(TyphoonBlockDefinitionInjectionsBlock)injections configuration:(TyphoonDefinitionBlock)configuration { return [self withClass:[NSObject class] initializer:initializer injections:injections configuration:configuration]; } + (id)withClass:(Class)clazz block:(TyphoonBlockDefinitionInitializerBlock)block { return [self withClass:clazz initializer:block injections:nil configuration:nil]; } + (id)withClass:(Class)clazz block:(TyphoonBlockDefinitionInitializerBlock)block configuration:(TyphoonDefinitionBlock)configuration { return [self withClass:clazz initializer:block injections:nil configuration:configuration]; } + (id)withClass:(Class)clazz injections:(TyphoonBlockDefinitionInjectionsBlock)injections { return [self withClass:clazz initializer:nil injections:injections configuration:nil]; } + (id)withClass:(Class)clazz injections:(TyphoonBlockDefinitionInjectionsBlock)injections configuration:(TyphoonDefinitionBlock)configuration { return [self withClass:clazz initializer:nil injections:injections configuration:configuration]; } + (id)withClass:(Class)clazz initializer:(TyphoonBlockDefinitionInitializerBlock)initializer injections:(TyphoonBlockDefinitionInjectionsBlock)injections configuration:(TyphoonDefinitionBlock)configuration { TyphoonBlockDefinitionController *controller = [TyphoonBlockDefinitionController currentController]; switch (controller.route) { case TyphoonBlockDefinitionRouteInvalid: { [NSException raise:NSInternalInconsistencyException format:@"TyphoonBlockDefinition cannot be used directly. You should only use it inside TyphoonAssembly methods."]; return nil; } case TyphoonBlockDefinitionRouteConfiguration: { TyphoonBlockDefinition *definition = [[TyphoonBlockDefinition alloc] initWithClass:clazz key:nil]; definition.hasInitializerBlock = initializer != nil; definition.hasInjectionsBlock = injections != nil; if (configuration) { configuration(definition); } return definition; } case TyphoonBlockDefinitionRouteInitializer: { if (!initializer) { [NSException raise:NSInternalInconsistencyException format:@"TyphoonBlockDefinition is supposed to have an initializer block at this point."]; } id instance = initializer(); return instance; } case TyphoonBlockDefinitionRouteInjections: { if (injections && controller.instance) { injections(controller.instance); } return controller.instance; } } } #pragma mark - Overriden properties - (TyphoonMethod *)initializer { return self.hasInitializerBlock ? nil : [super initializer]; } - (BOOL)isInitializerGenerated { return self.hasInitializerBlock ? NO : [super isInitializerGenerated]; } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { TyphoonBlockDefinition *copy = [super copyWithZone:zone]; copy->_hasInitializerBlock = _hasInitializerBlock; return copy; } @end ================================================ FILE: Pods/Typhoon/Source/Definition/TyphoonDefinition.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonMethod.h" #import "TyphoonDefinitionNamespace.h" @class TyphoonDefinition; @class TyphoonRuntimeArguments; @class TyphoonFactoryDefinition; /** * @ingroup Definition * Describes the lifecycle of a Typhoon component. * * TyphoonScopeObjectGraph * (default) This scope is essential (and unique to Typhoon) for mobile and desktop applications. When a component is resolved, any * dependencies with the object-graph will be treated as shared instances during resolution. Once resolution is complete they are not * retained by the TyphoonComponentFactory. This allows instantiating an entire object graph for a use-case (say for a ViewController), and * then discarding it when that use-case has completed, therefore making efficient use of memory. * * TyphoonScopePrototype * Indicates that a new instance should always be created by Typhoon, whenever this component is obtained from an assembly or referenced by * another component. * * TyphoonScopeSingleton * Indicates that Typhoon should retain the instance that exists for as long as the TyphoonComponentFactory exists. * * TyphoonScopeLazySingleton * This scope behaves the same as TyphoonScopeSingleton, but the object is not created unless or until it is needed. * * TyphoonScopeWeakSingleton * Indicates that a shared instance should be created as long as necessary. When your application's classes stop referencing this component, * it will be deallocated until needed again. */ typedef NS_ENUM(NSInteger, TyphoonScope) { TyphoonScopeObjectGraph, TyphoonScopePrototype, TyphoonScopeSingleton, TyphoonScopeLazySingleton, TyphoonScopeWeakSingleton, }; typedef NS_OPTIONS(NSInteger, TyphoonAutoInjectVisibility) { TyphoonAutoInjectVisibilityNone = 0, TyphoonAutoInjectVisibilityByClass = 1 << 0, TyphoonAutoInjectVisibilityByProtocol = 1 << 1, TyphoonAutoInjectVisibilityDefault = TyphoonAutoInjectVisibilityByClass | TyphoonAutoInjectVisibilityByProtocol, }; typedef void(^TyphoonDefinitionBlock)(TyphoonDefinition *definition); /** * @ingroup Definition */ @interface TyphoonDefinition : NSObject @property (nonatomic, readonly) Class type; /** * The scope of the component, default being TyphoonScopeObjectGraph. */ @property (nonatomic) TyphoonScope scope; /** * Specifies visibility for AutoInjection. * * AutoInjection is performed when using method: * - (void)injectProperty:(SEL)withSelector; * or when using: * InjectedClass or InjectedProtocol macro */ @property (nonatomic) TyphoonAutoInjectVisibility autoInjectionVisibility; /** * The namespace of the component. */ @property (nonatomic, readonly) TyphoonDefinitionNamespace *space; - (void)applyGlobalNamespace; - (void)applyConcreteNamespace:(NSString *)key; /** * A parent component. When parent is defined the initializer and/or properties from a definition are inherited, unless overridden. Example: * @code - (id)signUpClient { return [TyphoonDefinition withClass:[SignUpClientDefaultImpl class] configuration:^(TyphoonDefinition* definition) { definition.parent = [self abstractClient]; }]; } - (id)abstractClient { return [TyphoonDefinition withClass:[ClientBase class] configuration:^(TyphoonDefinition* definition) { [definition injectProperty:@selector(serviceUrl) with:TyphoonConfig(@"service.url"]; [definition injectProperty:@selector(networkMonitor) with:[self internetMonitor]]; [definition injectProperty:@selector(allowInvalidSSLCertificates) with:@(YES)]; }]; } @endcode * * @see abstract * */ @property(nonatomic, strong) id parent; /** * If set, designates that a component can not be instantiated directly. * * @see parent */ @property(nonatomic) BOOL abstract; //------------------------------------------------------------------------------------------- #pragma mark Factory methods //------------------------------------------------------------------------------------------- + (id)withClass:(Class)clazz; + (id)withClass:(Class)clazz configuration:(TyphoonDefinitionBlock)injections; /** * Returns a definition that inherits initializer injections, property injections, method injections and scope * from the specified parent definition. Parent definitions can be chained. */ + (id)withParent:(id)parent class:(Class)clazz; /** * Returns a definition that inherits initializer injections, property injections, method injections and scope * from the specified parent definition, adding the specified configuration. Parent definitions can be chained. */ + (id)withParent:(id)parent class:(Class)clazz configuration:(TyphoonDefinitionBlock)injections; //TODO: Rewrite this doc /** * A component that will produce an instance (with or without parameters) of this component. For example: * @code - (id)sqliteManager { return [TyphoonDefinition withClass:[MySqliteManager class] configuration:^(TyphoonDefinition* definition) { [definition useInitializer:@selector(initWithDatabaseName:) parameters:^(TyphoonMethod* initializer) { [initializer injectParameterWith:@"database.sqlite"]; }]; definition.scope = TyphoonScopeSingleton; }]; } - (id)databaseQueue { return [TyphoonDefinition withClass:[FMDatabaseQueue class] configuration:^(TyphoonDefinition* definition) { [definition useInitializer:@selector(queue)]; definition.factory = [self sqliteManager]; }]; } @endcode * * @note If the factory method takes arguments, these are provided in the initializer block, just like a regular initializer method. * * @see injectProperty:withDefinition:selector: An alternative short-hand approach for no-args instances. * @see injectProperty:withDefinition:keyPath: An alternative short-hand approach for no-args instances. * @see TyphoonFactoryProvider - For creating factories where the configuration arguments are not known until runtime. * * */ + (id)withFactory:(id)factory selector:(SEL)selector; + (id)withFactory:(id)factory selector:(SEL)selector parameters:(void (^)(TyphoonMethod *factoryMethod))params; + (id)withFactory:(id)factory selector:(SEL)selector parameters:(void (^)(TyphoonMethod *factoryMethod))params configuration:(void(^)(TyphoonFactoryDefinition *definition))configuration; //------------------------------------------------------------------------------------------- #pragma mark Injection //------------------------------------------------------------------------------------------- /** * Injects property with a component from the container that matches the type (class or protocol) of the property. */ - (void)injectProperty:(SEL)withSelector; /** * Injects property for gives selector with injection, where injection can be * - obtained from Injection* functions * - another definition * - assembly or collaboration assembly reference (TyphoonComponentFactory will be injected) * - object instance */ - (void)injectProperty:(SEL)selector with:(id)injection; /** * Injects method specified by selector with parameters. * @see TyphoonMethod documentation for information about parameters */ - (void)injectMethod:(SEL)selector parameters:(void (^)(TyphoonMethod *method))parametersBlock; /** * Injects initializer specified by selector and parameters. * Initializer allow you to create object with special selector and params. Without this injection, * object will be created by 'alloc-init' calls */ - (void)useInitializer:(SEL)selector parameters:(void (^)(TyphoonMethod *initializer))parametersBlock; /** * Convenience method to use a no-args initializer. * */ - (void)useInitializer:(SEL)selector; /** * A custom callback methods that is invoked before properties and method injection occurs. */ - (void)performBeforeInjections:(SEL)sel; - (void)performBeforeInjections:(SEL)sel parameters:(void (^)(TyphoonMethod *params))parametersBlock; /** * A custom callback methods that is invoked after properties and method injection occurs. */ - (void)performAfterInjections:(SEL)sel; - (void)performAfterInjections:(SEL)sel parameters:(void (^)(TyphoonMethod *params))parameterBlock; /* * Custom callback that is invoked after all injections on built graph occurs. */ - (void)performAfterAllInjections:(SEL)sel; #pragma mark Making injections from definition /** * Returns injection which can be used for example in injectProperty:with: method * This method will injects result of selector invocation * @param selector selector to invoke on resolved definition */ - (id)property:(SEL)selector; /** * Returns injection which can be used for example in injectProperty:with: method * This method will injects valueForKeyPath: with given keyPath * @param keyPath path used as argument while calling valueForKeyPath: on resolved definition */ - (id)keyPath:(NSString *)keyPath; #pragma mark Making definition with injection + (id)with:(id)injection; @end @interface TyphoonDefinition(Unavailable) @property(nonatomic, assign, getter = isLazy) BOOL lazy __attribute((unavailable("Use TyphoonScopeLazySingleton instead"))); + (id)withClass:(Class)clazz factory:(id)definition selector:(SEL)selector __attribute((unavailable("Use withFactory:selector: method instead"))); + (instancetype)configDefinitionWithResource:(id)resource __attribute__((unavailable("Use configDefinitionWithName instead"))); + (instancetype)configDefinitionWithResources:(NSArray *)array __attribute__((unavailable("Use configDefinitionWithName instead"))); @property(nonatomic, strong) id factory __attribute((unavailable("Use one of withFactory: method instead"))); - (void)setBeforeInjections:(SEL)sel __attribute((unavailable("Use performBeforeInjections method. (setBeforeInjections will be unavailable in Typhoon 3.0)"))); - (void)setAfterInjections:(SEL)sel __attribute((unavailable("Use performAterInjections method (setAfterInjections method will be unavailable in Typhoon 3.0)"))); @end ================================================ FILE: Pods/Typhoon/Source/Definition/TyphoonDefinition.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition.h" #import "TyphoonDefinition+Internal.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonMethod.h" #import "TyphoonMethod+InstanceBuilder.h" #import "TyphoonPropertyInjection.h" #import "TyphoonObjectWithCustomInjection.h" #import "TyphoonInjectionByObjectInstance.h" #import "TyphoonInjectionByType.h" #import "TyphoonInjectionByReference.h" #import "TyphoonInjectionByFactoryReference.h" #import "TyphoonInjectionByRuntimeArgument.h" #import "TyphoonInjections.h" #import "TyphoonFactoryDefinition.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonInjectionDefinition.h" #import "OCLogTemplate.h" static NSString *TyphoonScopeToString(TyphoonScope scope) { switch (scope) { case TyphoonScopeObjectGraph: return @"ObjectGraph"; case TyphoonScopePrototype: return @"Prototype"; case TyphoonScopeSingleton: return @"Singleton"; case TyphoonScopeWeakSingleton: return @"WeakSingleton"; default: return @"Unknown"; } } @interface TyphoonDefinition () @end @implementation TyphoonDefinition { TyphoonDefinition *_parent; NSMutableSet *_injectedProperties; NSMutableOrderedSet *_injectedMethods; } //------------------------------------------------------------------------------------------- #pragma mark: - Initialization //------------------------------------------------------------------------------------------- - (instancetype)init { return [self initWithClass:Nil key:nil]; } - (instancetype)initWithClass:(Class)clazz key:(NSString *)key { if (clazz == nil) { [NSException raise:NSInvalidArgumentException format:@"Property 'clazz' is required."]; } self = [super init]; if (self) { _type = clazz; _key = key; _scope = TyphoonScopeObjectGraph; _autoInjectionVisibility = TyphoonAutoInjectVisibilityDefault; _injectedProperties = [[NSMutableSet alloc] init]; _injectedMethods = [[NSMutableOrderedSet alloc] init]; [self validateRequiredParametersAreSet]; } return self; } + (instancetype)withClass:(Class)clazz key:(NSString *)key { return [[self alloc] initWithClass:clazz key:key]; } //------------------------------------------------------------------------------------------- #pragma mark - Scope //------------------------------------------------------------------------------------------- @synthesize scope = _scope; - (TyphoonScope)scope { if (_parent && !_scopeSetByUser) { return _parent.scope; } else { return _scope; } } - (void)setScope:(TyphoonScope)scope { _scope = scope; _scopeSetByUser = YES; [self validateScope]; } //------------------------------------------------------------------------------------------- #pragma mark - Namespacing //------------------------------------------------------------------------------------------- - (void)applyGlobalNamespace { _space = [TyphoonDefinitionNamespace globalNamespace]; } - (void)applyConcreteNamespace:(NSString *)key { _space = [TyphoonDefinitionNamespace namespaceWithKey:key]; } //------------------------------------------------------------------------------------------- #pragma mark - Injections //------------------------------------------------------------------------------------------- @synthesize initializer = _initializer; @synthesize beforeInjections = _beforeInjections; @synthesize afterInjections = _afterInjections; - (TyphoonMethod *)initializer { if (!_initializer) { TyphoonMethod *parentInitializer = _parent.initializer; if (parentInitializer) { return parentInitializer; } else { _initializer = [[TyphoonMethod alloc] initWithSelector:@selector(init)]; _initializerGenerated = YES; } } return _initializer; } - (BOOL)isInitializerGenerated { [self initializer]; // call getter to generate initializer if needed return _initializerGenerated; } - (TyphoonMethod *)beforeInjections { if (!_parent || _beforeInjections) { return _beforeInjections; } else { return [_parent beforeInjections]; } } - (TyphoonMethod *)afterInjections { if (!_parent || _afterInjections) { return _afterInjections; } else { return [_parent afterInjections]; } } - (NSSet *)injectedProperties { if (!_parent) { return [_injectedProperties mutableCopy]; } NSMutableSet *properties = (NSMutableSet *)[_parent injectedProperties]; NSMutableSet *overriddenProperties = [NSMutableSet set]; for (id parentProperty in properties) { for (id childProperty in _injectedProperties) { if ([[childProperty propertyName] isEqualToString:[parentProperty propertyName]]) { [overriddenProperties addObject:parentProperty]; } } } [properties minusSet:overriddenProperties]; [properties unionSet:_injectedProperties]; return properties; } - (NSOrderedSet *)injectedMethods { if (!_parent) { return [_injectedMethods mutableCopy]; } NSMutableOrderedSet *methods = (NSMutableOrderedSet *)[_parent injectedMethods]; NSMutableSet *overriddenMethods = [NSMutableSet set]; for (TyphoonMethod *parentMethod in methods) { for (TyphoonMethod *childMethod in _injectedMethods) { if (parentMethod.selector == childMethod.selector) { [overriddenMethods addObject:parentMethod]; } } } [methods minusSet:overriddenMethods]; [methods unionOrderedSet:_injectedMethods]; return methods; } //------------------------------------------------------------------------------------------- #pragma mark - Parent //------------------------------------------------------------------------------------------- - (void)setParent:(id)parent { _parent = parent; [self validateParent]; } //------------------------------------------------------------------------------------------- #pragma mark: - Class Methods //------------------------------------------------------------------------------------------- //- (id)factory //{ // return nil; //} + (id)withClass:(Class)clazz { return [[self alloc] initWithClass:clazz key:nil]; } + (id)withClass:(Class)clazz configuration:(TyphoonDefinitionBlock)injections { return [self withClass:clazz key:nil injections:injections]; } + (id)withClass:(Class)clazz key:(NSString *)key injections:(TyphoonDefinitionBlock)configuration { TyphoonDefinition *definition = [[self alloc] initWithClass:clazz key:key]; if (configuration) { configuration(definition); } [definition validateScope]; return definition; } + (id)withParent:(id)parent class:(Class)clazz { return [self withParent:parent class:clazz configuration:nil]; } + (id)withParent:(id)parent class:(Class)clazz configuration:(TyphoonDefinitionBlock)injections { TyphoonDefinition *definition = [TyphoonDefinition withClass:clazz configuration:injections]; definition.parent = parent; return definition; } + (id)withFactory:(id)factory selector:(SEL)selector { return [self withFactory:factory selector:selector parameters:nil]; } + (id)withFactory:(id)factory selector:(SEL)selector parameters:(void (^)(TyphoonMethod *method))params { return [self withFactory:factory selector:selector parameters:params configuration:nil]; } + (id)withFactory:(id)factory selector:(SEL)selector parameters:(void (^)(TyphoonMethod *))parametersBlock configuration:(void (^)(TyphoonFactoryDefinition *))configuration { TyphoonFactoryDefinition *definition = [[TyphoonFactoryDefinition alloc] initWithFactory:factory selector:selector parameters:parametersBlock]; if (configuration) { configuration(definition); } if ([self isOldStyleStoryboardDefinitionWithFactory:factory]) { LogInfo(@"*** Warning *** You are using TyphoonFactoryDefinition for ViewController injections. The preferred way are special TyphoonStoryboardDefinitions."); } return definition; } //------------------------------------------------------------------------------------------- #pragma mark - TyphoonObjectWithCustomInjection //------------------------------------------------------------------------------------------- - (id)typhoonCustomObjectInjection { return [[TyphoonInjectionByReference alloc] initWithReference:self.key args:self.currentRuntimeArguments]; } //------------------------------------------------------------------------------------------- #pragma mark: - Interface Methods //------------------------------------------------------------------------------------------- - (void)injectProperty:(SEL)selector { [self injectProperty:selector with:[[TyphoonInjectionByType alloc] init]]; } - (void)injectProperty:(SEL)selector with:(id)injection { injection = TyphoonMakeInjectionFromObjectIfNeeded(injection); NSString *propertyName = NSStringFromSelector(selector); [injection setPropertyName:propertyName]; if ([_injectedProperties containsObject:injection]) { LogInfo(@"*** Warning *** The definition (key: %@, namespace: %@) contains duplicate injections for property '%@'. Is this intentional?", self.key, self.space.key, propertyName); } [_injectedProperties addObject:injection]; } - (void)injectMethod:(SEL)selector parameters:(void (^)(TyphoonMethod *method))parametersBlock { TyphoonMethod *method = [[TyphoonMethod alloc] initWithSelector:selector]; if (parametersBlock) { parametersBlock(method); } #if DEBUG [method checkParametersCount]; #endif [_injectedMethods addObject:method]; } - (void)useInitializer:(SEL)selector parameters:(void (^)(TyphoonMethod *initializer))parametersBlock { TyphoonMethod *initializer = [[TyphoonMethod alloc] initWithSelector:selector]; if (parametersBlock) { parametersBlock(initializer); } #if DEBUG [initializer checkParametersCount]; #endif _initializer = initializer; } - (void)useInitializer:(SEL)selector { [self useInitializer:selector parameters:nil]; } - (void)performBeforeInjections:(SEL)sel { [self performBeforeInjections:sel parameters:nil]; } - (void)performBeforeInjections:(SEL)sel parameters:(void (^)(TyphoonMethod *params))parametersBlock { _beforeInjections = [[TyphoonMethod alloc] initWithSelector:sel]; if (parametersBlock) { parametersBlock(_beforeInjections); } #if DEBUG [_beforeInjections checkParametersCount]; #endif } - (void)performAfterInjections:(SEL)sel { [self performAfterInjections:sel parameters:nil]; } - (void)performAfterInjections:(SEL)sel parameters:(void (^)(TyphoonMethod *params))parameterBlock { _afterInjections = [[TyphoonMethod alloc] initWithSelector:sel]; if (parameterBlock) { parameterBlock(_afterInjections); } #if DEBUG [_afterInjections checkParametersCount]; #endif } - (void)performAfterAllInjections:(SEL)sel { _afterAllInjections = sel; } //------------------------------------------------------------------------------------------- #pragma mark: - TyphoonDefinition+Infrastructure methods //------------------------------------------------------------------------------------------- - (BOOL)hasRuntimeArgumentInjections { __block BOOL hasInjections = NO; [self enumerateInjectionsOfKind:[TyphoonInjectionByRuntimeArgument class] options:TyphoonInjectionsEnumerationOptionAll usingBlock:^(id injection, id *injectionToReplace, BOOL *stop) { hasInjections = YES; *stop = YES; }]; return hasInjections; } - (BOOL)isCandidateForInjectedClass:(Class)clazz includeSubclasses:(BOOL)includeSubclasses { BOOL result = NO; if (self.autoInjectionVisibility & TyphoonAutoInjectVisibilityByClass) { BOOL isSameClass = self.type == clazz; BOOL isSubclass = includeSubclasses && [self.type isSubclassOfClass:clazz]; result = isSameClass || isSubclass; } return result; } - (BOOL)isCandidateForInjectedProtocol:(Protocol *)aProtocol { BOOL result = NO; if (self.autoInjectionVisibility & TyphoonAutoInjectVisibilityByProtocol) { result = [self.type conformsToProtocol:aProtocol]; } return result; } - (void)addInjectedProperty:(id )property { [_injectedProperties addObject:property]; } - (void)addInjectedPropertyIfNotExists:(id )property { for (id injection in self.injectedProperties) { if ([[injection propertyName] isEqualToString:[property propertyName]]) { return; } } [_injectedProperties addObject:property]; } - (void)replacePropertyInjection:(id)injection with:(id)injectionToReplace { if ([_injectedProperties containsObject:injection]) { [injectionToReplace setPropertyName:[injection propertyName]]; [_injectedProperties removeObject:injection]; [_injectedProperties addObject:injectionToReplace]; } else if ([_parent.injectedProperties containsObject:injection]) { // [self addInjectedProperty:injectionToReplace]; [_parent replacePropertyInjection:injection with:injectionToReplace]; } } - (void)enumerateInjectionsOfKind:(Class)injectionClass options:(TyphoonInjectionsEnumerationOption)options usingBlock:(TyphoonInjectionsEnumerationBlock)block { if (options & TyphoonInjectionsEnumerationOptionMethods) { [self enumerateInjectionsOfKind:injectionClass onCollection:[_initializer injectedParameters] withBlock:block replaceBlock:^(id injection, id injectionToReplace) { [self->_initializer replaceInjection:injection with:injectionToReplace]; }]; for (TyphoonMethod *method in self.injectedMethods) { [self enumerateInjectionsOfKind:injectionClass onCollection:[method injectedParameters] withBlock:block replaceBlock:^(id injection, id injectionToReplace) { [method replaceInjection:injection with:injectionToReplace]; }]; } [self enumerateInjectionsOfKind:injectionClass onCollection:[_beforeInjections injectedParameters] withBlock:block replaceBlock:^(id injection, id injectionToReplace) { [self->_beforeInjections replaceInjection:injection with:injectionToReplace]; }]; [self enumerateInjectionsOfKind:injectionClass onCollection:[_afterInjections injectedParameters] withBlock:block replaceBlock:^(id injection, id injectionToReplace) { [self->_afterInjections replaceInjection:injection with:injectionToReplace]; }]; } if (options & TyphoonInjectionsEnumerationOptionProperties) { [self enumerateInjectionsOfKind:injectionClass onCollection:self.injectedProperties withBlock:block replaceBlock:^(id injection, id injectionToReplace) { [self replacePropertyInjection:injection with:injectionToReplace]; }]; } } - (void)enumerateInjectionsOfKind:(Class)injectionClass onCollection:(id)collection withBlock:(TyphoonInjectionsEnumerationBlock)block replaceBlock:(void(^)(id injection, id injectionToReplace))replaceBlock { for (id injection in collection) { if ([injection isKindOfClass:injectionClass]) { id injectionToReplace = nil; BOOL stop = NO; block(injection, &injectionToReplace, &stop); if (injectionToReplace) { replaceBlock(injection, injectionToReplace); } if (stop) { break; } } } } //------------------------------------------------------------------------------------------- #pragma mark - Making injections //------------------------------------------------------------------------------------------- - (id)property:(SEL)factorySelector { return [self keyPath:NSStringFromSelector(factorySelector)]; } - (id)keyPath:(NSString *)keyPath { return [[TyphoonInjectionByFactoryReference alloc] initWithReference:self.key args:self.currentRuntimeArguments keyPath:keyPath]; } + (id)with:(id)injection { return [[TyphoonInjectionDefinition alloc] initWithInjection:TyphoonMakeInjectionFromObjectIfNeeded(injection)]; } //------------------------------------------------------------------------------------------- #pragma mark - Utility Methods //------------------------------------------------------------------------------------------- - (id)copyWithZone:(NSZone *)zone { TyphoonDefinition *copy = [[[self class] allocWithZone:zone] initWithClass:_type key:[_key copy]]; copy->_scope = _scope; copy->_scopeSetByUser = _scopeSetByUser; copy->_autoInjectionVisibility = _autoInjectionVisibility; copy->_space = _space; copy->_processed = _processed; copy->_currentRuntimeArguments = [_currentRuntimeArguments copy]; copy->_initializer = [_initializer copy]; copy->_initializerGenerated = _initializerGenerated; copy->_beforeInjections = [_beforeInjections copy]; copy->_injectedProperties = [_injectedProperties mutableCopy]; copy->_injectedMethods = [_injectedMethods mutableCopy]; copy->_afterInjections = [_afterInjections copy]; copy->_parent = _parent; copy->_abstract = _abstract; copy->_assembly = _assembly; copy->_assemblySelector = _assemblySelector; return copy; } - (NSString *)description { return [NSString stringWithFormat:@"%@: class='%@', key='%@', scope='%@'", NSStringFromClass([self class]), NSStringFromClass(_type), _key, TyphoonScopeToString(_scope)]; } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods //------------------------------------------------------------------------------------------- - (void)validateScope { if (self.scope == TyphoonScopeSingleton && [self hasRuntimeArgumentInjections]) { [NSException raise:NSInvalidArgumentException format:@"The runtime arguments injections are not applicable to singleton scoped definitions, because we don't know initial arguments to instantiate eager singletons. But it set for definition: %@ ", self]; } } - (void)validateParent { if (![_parent isKindOfClass:[TyphoonDefinition class]]) { [NSException raise:NSInvalidArgumentException format:@"Only TyphoonDefinition object can be set as parent. But in method '%@' object of class %@ set as parent", self.key, [_parent class]]; } } - (void)validateRequiredParametersAreSet { BOOL hasAppropriateSuper = [self.type isSubclassOfClass:[NSObject class]] || [self.type isSubclassOfClass:[NSProxy class]]; if (!hasAppropriateSuper) { [NSException raise:NSInvalidArgumentException format:@"Subclass of NSProxy or NSObject is required."]; } } + (BOOL)isOldStyleStoryboardDefinitionWithFactory:(id)factory { #if TARGET_OS_IPHONE if ([factory isKindOfClass:[TyphoonDefinition class]]) { Class factoryClass = ((TyphoonDefinition *)factory).type; return [factoryClass isSubclassOfClass:[UIStoryboard class]]; } if ([factory isKindOfClass:[UIStoryboard class]]) { return YES; } #endif return NO; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Assembly/TyphoonAssembly+TyphoonAssemblyFriend.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAssembly.h" @interface TyphoonAssembly (TyphoonAssemblyFriend) + (BOOL)selectorIsReserved:(SEL)selector; - (void)prepareForUse; - (NSArray *)definitions; - (NSArray *)preattachedInfrastructureComponents; - (Class)assemblyClassForKey:(NSString *)key; - (void)activateWithFactory:(TyphoonComponentFactory *)factory collaborators:(NSSet *)collaborators; @property (readonly) NSSet *definitionSelectors; - (instancetype)accessor; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Assembly/TyphoonAssembly.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonComponentFactory.h" #import "TyphoonDefinition.h" #import "TyphoonDefinition+Option.h" #if TARGET_OS_IPHONE #import "TyphoonDefinition+Storyboard.h" #endif @class TyphoonComponentFactory; /** * @ingroup Assembly * * Provides a concise way to declare and encapsulate the architecture of an application in one or more classes that describe * how components collaborate together. * * Prior to activation the assembly interface returns TyphoonDefinitions. After activation assembly interface poses in * of a TyphoonComponentFactory to return built instances. * * ## Example: * * @code MyAssembly* assembly = [[MyAssembly new] activate]; AnalyticsService* service = [assembly analyticsService]; @endcode * * The TyphoonAssembly provides: * * - a way to easily define multiple components of the same class or protocol * - Avoids the use of "magic strings" for component resolution and wiring * - Allows the use of IDE features like refactoring and code completion. * */ @interface TyphoonAssembly : NSObject + (__nonnull instancetype)assembly; /** * Returns the [TyphoonComponentFactory defaultFactory] posing as a TyphoonAssembly. */ + (__nullable instancetype)defaultAssembly; /** * Activates the assembly. The concrete declared type of any collaborating assemblies will be used. If * collaborating assemblies are backed by a protocol then they must be specified explicitly. * * @see activateWithCollaboratingAssemblies * */ - (__nonnull instancetype)activated; /** * Activates the assembly, attaching the specified config resource name from the application bundle. * * This method is a convenience for: @code TyphoonConfigPostProcessor *processor = [TyphoonConfigPostProcessor processor]; [processor useResourceWithName:@"Config_production.plist"]; [self attachPostProcessor:processor]; [self activate]; @endcode * */ - (__nonnull instancetype)activatedWithConfigResourceName:(NSString * __nonnull)resourceName; /** * Activates the assembly, explicitly setting the types for collaborating assemblies. * * @param assemblies The explicit types to be used for collaborating assemblies. For example if this assembly * references another assembly of type NetworkProvider, specifying a subclass TestNetworkProvider will override * the base type. If collaborating assemblies are backed by a protocol, they must be specified explicitly. */ - (__nonnull instancetype)activatedWithCollaboratingAssemblies:(NSArray * __nullable)assemblies; - (__nonnull instancetype)activatedWithCollaboratingAssemblies:(NSArray *__nullable)assemblies postProcessors:(NSArray * __nullable)postProcessors; @end @interface TyphoonAssembly(Unavailable) - (__nonnull instancetype)activate __attribute__((unavailable("Use `activated` non-mutating version instead.")));; - (__nonnull instancetype)activateWithConfigResourceName:(NSString * __nonnull)resourceName __attribute__((unavailable("Use `activatedWithConfigResourceName:` non-mutating version instead."))); - (__nonnull instancetype)activateWithCollaboratingAssemblies:(NSArray * __nullable)assemblies __attribute__((unavailable("Use `activatedWithCollaboratingAssemblies:` non-mutating version instead.")));; - (__nonnull instancetype)activateWithCollaboratingAssemblies:(NSArray *__nullable)assemblies postProcessors:(NSArray * __nullable)postProcessors __attribute__((unavailable("Use `activatedWithCollaboratingAssemblies:postProcessors:` non-mutating version instead.")));; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Assembly/TyphoonAssembly.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonAssembly.h" #import "TyphoonAssembly+TyphoonAssemblyFriend.h" #import "TyphoonAssemblyAdviser.h" #import "TyphoonAssemblyDefinitionBuilder.h" #import "TyphoonCollaboratingAssemblyPropertyEnumerator.h" #import "TyphoonCollaboratingAssemblyProxy.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonObjectWithCustomInjection.h" #import "TyphoonInjectionByComponentFactory.h" #import "NSObject+TyphoonIntrospectionUtils.h" #import "OCLogTemplate.h" #import "TyphoonBlockComponentFactory.h" #import "TyphoonCollaboratingAssembliesCollector.h" #import "TyphoonConfigPostProcessor.h" #import "TyphoonMemoryManagementUtils.h" #import "TyphoonAssemblyAccessor.h" static NSMutableSet *reservedSelectorsAsStrings; @interface TyphoonAssembly () @property (readwrite) NSSet *definitionSelectors; @property (readwrite) NSArray *preattachedInfrastructureComponents; @property (readwrite) NSDictionary *assemblyClassPerDefinitionKey; @property (readonly) TyphoonAssemblyAdviser *adviser; @property (readonly, unsafe_unretained) TyphoonComponentFactory *factory; @property (readonly) TyphoonCollaboratingAssembliesCollector *collector; @property (nonatomic, strong) TyphoonAssemblyAccessor *accessor; @end @implementation TyphoonAssembly { TyphoonAssemblyDefinitionBuilder *_definitionBuilder; } //------------------------------------------------------------------------------------------- #pragma mark - Class Methods //------------------------------------------------------------------------------------------- + (TyphoonAssembly *)assembly { return [[self alloc] init]; } + (instancetype)defaultAssembly { return (id)[TyphoonComponentFactory defaultFactory]; } + (void)load { [self reserveSelectors]; } + (void)reserveSelectors { reservedSelectorsAsStrings = [[NSMutableSet alloc] init]; [self markSelectorReserved:@selector(init)]; [self markSelectorReserved:@selector(definitions)]; [self markSelectorReserved:@selector(prepareForUse)]; [self markSelectorReservedFromString:@".cxx_destruct"]; [self markSelectorReserved:@selector(defaultAssembly)]; [self markSelectorReserved:@selector(proxyCollaboratingAssembliesPriorToActivation)]; [self markSelectorReserved:@selector(componentForType:)]; [self markSelectorReserved:@selector(allComponentsForType:)]; [self markSelectorReserved:@selector(componentForKey:)]; [self markSelectorReserved:@selector(componentForKey:args:)]; } + (void)markSelectorReserved:(SEL)selector { [self markSelectorReservedFromString:NSStringFromSelector(selector)]; } + (void)markSelectorReservedFromString:(NSString *)stringFromSelector { [reservedSelectorsAsStrings addObject:stringFromSelector]; } + (BOOL)selectorIsReserved:(SEL)selector { NSString *selectorString = NSStringFromSelector(selector); return [reservedSelectorsAsStrings containsObject:selectorString]; } + (BOOL)resolveInstanceMethod:(SEL)sel { return YES; } #pragma mark - Forwarding definition methods - (void)forwardInvocation:(NSInvocation *)anInvocation { @synchronized (self) { if (_factory) { [_factory forwardInvocation:anInvocation]; } else { TyphoonRuntimeArguments *args = [TyphoonRuntimeArguments argumentsFromInvocation:anInvocation]; NSString *key = NSStringFromSelector(anInvocation.selector); TyphoonDefinition *definition = [_definitionBuilder builtDefinitionForKey:key args:args]; [anInvocation retainArguments]; [anInvocation setReturnValue:&definition]; } } } //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction //------------------------------------------------------------------------------------------- - (id)init { self = [super init]; if (self) { _definitionBuilder = [[TyphoonAssemblyDefinitionBuilder alloc] initWithAssembly:self]; _adviser = [[TyphoonAssemblyAdviser alloc] initWithAssembly:self]; _collector = [[TyphoonCollaboratingAssembliesCollector alloc] initWithAssemblyClass:[self class]]; _preattachedInfrastructureComponents = [NSArray array]; _accessor = [TyphoonAssemblyAccessor new]; _accessor.assembly = self; _accessor.definitionBuilder = _definitionBuilder; [self proxyCollaboratingAssembliesPriorToActivation]; } return self; } //------------------------------------------------------------------------------------------- #pragma mark - //------------------------------------------------------------------------------------------- - (id)typhoonCustomObjectInjection { return [[TyphoonInjectionByComponentFactory alloc] init]; } //------------------------------------------------------------------------------------------- #pragma mark - //------------------------------------------------------------------------------------------- - (id)componentForType:(id)classOrProtocol { [NSException raise:NSInternalInconsistencyException format:@"componentForType: requires the assembly to be activated."]; return nil; } - (NSArray *)allComponentsForType:(id)classOrProtocol { [NSException raise:NSInternalInconsistencyException format:@"allComponentsForType: requires the assembly to be activated."]; return nil; } - (id)componentForKey:(NSString *)key { [NSException raise:NSInternalInconsistencyException format:@"componentForKey: requires the assembly to be activated."]; return nil; } - (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args { [NSException raise:NSInternalInconsistencyException format:@"componentForKey:args requires the assembly to be activated."]; return nil; } - (void)inject:(id)instance { [NSException raise:NSInternalInconsistencyException format:@"inject: requires the assembly to be activated."]; } - (void)inject:(id)instance withSelector:(SEL)selector { [NSException raise:NSInternalInconsistencyException format:@"inject:withSelector: requires the assembly to be activated."]; } - (void)makeDefault { [NSException raise:NSInternalInconsistencyException format:@"makeDefault requires the assembly to be activated."]; } - (void)attachDefinitionPostProcessor:(id)postProcessor { if (!_factory) { [self preattachInfrastructureComponent:postProcessor]; } } - (void)attachInstancePostProcessor:(id)postProcessor { if (!_factory) { [self preattachInfrastructureComponent:postProcessor]; } } - (void)attachTypeConverter:(id)typeConverter { if (!_factory) { [self preattachInfrastructureComponent:typeConverter]; } } - (id)objectForKeyedSubscript:(id)key { [NSException raise:NSInternalInconsistencyException format:@"objectForKeyedSubscript: requires the assembly to be activated."]; return nil; } //------------------------------------------------------------------------------------------- #pragma mark - Interface Methods //------------------------------------------------------------------------------------------- - (instancetype)activated { if (self.factory) { return (id)self.accessor; } else { return [self activatedWithCollaboratingAssemblies:nil]; } } - (instancetype)activatedWithConfigResourceName:(NSString *__nonnull)resourceName { TyphoonConfigPostProcessor *processor = [TyphoonConfigPostProcessor processor]; [processor useResourceWithName:resourceName]; return [self activatedWithCollaboratingAssemblies:nil postProcessors:@[processor]]; } - (instancetype)activatedWithCollaboratingAssemblies:(NSArray *__nullable)assemblies { return [self activatedWithCollaboratingAssemblies:assemblies postProcessors:nil]; } - (instancetype)activatedWithCollaboratingAssemblies:(NSArray *__nullable)assemblies postProcessors:(NSArray *__nullable)postProcessors { [self attachProcessors:postProcessors]; NSMutableSet *reconciledAssemblies = [NSMutableSet setWithArray:[@[self] arrayByAddingObjectsFromArray:assemblies]]; NSMutableSet *assembliesToRemove = [[NSMutableSet alloc] init]; NSSet *collaboratingAssemblies = [self.collector collectCollaboratingAssemblies]; for (TyphoonAssembly *collaboratingAssembly in collaboratingAssemblies) { for (TyphoonAssembly *overrideCandidate in assemblies) { if ([collaboratingAssembly class] != [overrideCandidate class] && [[overrideCandidate class] isSubclassOfClass:[collaboratingAssembly class]]) { [assembliesToRemove addObject:collaboratingAssembly]; LogInfo(@"%@ will act in place of assembly with class: %@", [overrideCandidate class], [collaboratingAssembly class]); } } if (![self assemblyWithType:[collaboratingAssembly class] in:reconciledAssemblies]) { [reconciledAssemblies addObject:collaboratingAssembly]; } } for (TyphoonAssembly *assembly in assembliesToRemove) { [reconciledAssemblies removeObject:assembly]; } TyphoonBlockComponentFactory *factory = [TyphoonBlockComponentFactory factoryWithAssemblies: [reconciledAssemblies allObjects]]; [TyphoonMemoryManagementUtils makeAssemblies:reconciledAssemblies retainFactory:factory]; return (id)self.accessor; } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods //------------------------------------------------------------------------------------------- - (void)attachProcessors:(NSArray *)postProcessors { _preattachedInfrastructureComponents = [_preattachedInfrastructureComponents arrayByAddingObjectsFromArray:postProcessors]; } - (void)proxyCollaboratingAssembliesPriorToActivation { TyphoonCollaboratingAssemblyPropertyEnumerator *enumerator = [[TyphoonCollaboratingAssemblyPropertyEnumerator alloc] initWithAssembly:self]; NSMutableDictionary *collaboratingAssemblies = [NSMutableDictionary new]; for (NSString *propertyName in enumerator.collaboratingAssemblyProperties) { TyphoonCollaboratingAssemblyProxy *proxy = [TyphoonCollaboratingAssemblyProxy proxy]; [self setValue:proxy forKey:propertyName]; collaboratingAssemblies[propertyName] = proxy; } _accessor.collaboratingAssemblies = collaboratingAssemblies; } - (void)activateWithFactory:(TyphoonComponentFactory *)factory collaborators:(NSSet *)collaborators { _factory = factory; _accessor.factory = _factory; NSMutableDictionary *collaboratingAssemblies = [NSMutableDictionary new]; for (NSString *propertyName in [self typhoonPropertiesUpToParentClass:[TyphoonAssembly class]]) { TyphoonTypeDescriptor *descriptor = [self typhoonTypeForPropertyNamed:propertyName]; if (descriptor.typeBeingDescribed == [TyphoonAssembly class]) { TyphoonAssembly *collaborator = [self assemblyConformingTo:descriptor.declaredProtocol in:collaborators]; if (!collaborator) { LogInfo(@"*** Warning *** Can't find collaborating assembly that conforms to protocol %@. Is this " "intentional? The property '%@' in class %@ will be left as nil.", descriptor.declaredProtocol, propertyName, NSStringFromClass([self class])); } [self setValue:collaborator forKey:propertyName]; collaboratingAssemblies[propertyName] = collaborator; } else if ([descriptor.typeBeingDescribed isSubclassOfClass:[TyphoonAssembly class]]) { TyphoonAssembly *collaborator = [self assemblyWithType:descriptor.typeBeingDescribed in:collaborators]; if (!collaborator) { LogInfo(@"*** Warning *** Can't find assembly of type %@. Is this intentional? The property '%@' " "in class %@ will be left as nil.", descriptor.typeBeingDescribed, propertyName, NSStringFromClass([self class])); } [self setValue:collaborator forKey:propertyName]; collaboratingAssemblies[propertyName] = collaborator; } } _accessor.collaboratingAssemblies = collaboratingAssemblies; } - (TyphoonAssembly *)assemblyConformingTo:(NSString *)protocolName in:(NSSet *)assemblies { for (TyphoonAssembly *assembly in assemblies) { if ([[assembly class] conformsToProtocol:NSProtocolFromString(protocolName)]) { return assembly; } } return nil; } - (TyphoonAssembly *)assemblyWithType:(Class)type in:(NSSet *)assemblies { for (TyphoonAssembly *assembly in assemblies) { if ([[assembly class] isSubclassOfClass:type]) { return assembly; } } return nil; } - (NSArray *)definitions { return [_definitionBuilder builtDefinitions]; } - (void)prepareForUse { self.definitionSelectors = [self.adviser definitionSelectors]; self.assemblyClassPerDefinitionKey = [self.adviser assemblyClassPerDefinitionKey]; } - (Class)assemblyClassForKey:(NSString *)key { if (self.assemblyClassPerDefinitionKey) { return self.assemblyClassPerDefinitionKey[key]; } else { return [self class]; } } - (void)preattachInfrastructureComponent:(id)component { _preattachedInfrastructureComponents = [_preattachedInfrastructureComponents arrayByAddingObject:component]; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Assembly/TyphoonAssemblyAccessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOONFRAMEWORK.ORG // Copyright 2016 typhoonframework.org Pty Ltd // All Rights Reserved. // // NOTICE: Prepared by AppsQuick.ly on behalf of typhoonframework.org. This software // is proprietary information. Unauthorized use is prohibited. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonComponentFactory.h" @class TyphoonComponentFactory; @class TyphoonAssembly; @class TyphoonAssemblyDefinitionBuilder; @interface TyphoonAssemblyAccessor : NSObject @property (nonatomic, weak) TyphoonComponentFactory *factory; @property (nonatomic, weak) TyphoonAssembly *assembly; @property (nonatomic, weak) TyphoonAssemblyDefinitionBuilder *definitionBuilder; @property (nonatomic, strong) NSDictionary *collaboratingAssemblies; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Assembly/TyphoonAssemblyAccessor.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOONFRAMEWORK.ORG // Copyright 2016 typhoonframework.org Pty Ltd // All Rights Reserved. // // NOTICE: Prepared by AppsQuick.ly on behalf of typhoonframework.org. This software // is proprietary information. Unauthorized use is prohibited. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAssemblyAccessor.h" #import "TyphoonComponentFactory.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonAssembly.h" #import "TyphoonAssemblyDefinitionBuilder.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonAssemblySelectorAdviser.h" #import "TyphoonInjectionByComponentFactory.h" #import "TyphoonAssembly+TyphoonAssemblyFriend.h" @implementation TyphoonAssemblyAccessor + (BOOL)resolveInstanceMethod:(SEL)sel { return YES; } - (void)forwardInvocation:(NSInvocation *)anInvocation { @synchronized (self) { TyphoonComponentFactory *factory = self.factory; NSString *key = [TyphoonAssemblySelectorAdviser keyForSEL:anInvocation.selector]; if ([self.collaboratingAssemblies valueForKey:key]) { TyphoonAssembly *assembly = self.collaboratingAssemblies[key]; id result = assembly.accessor; [anInvocation retainArguments]; [anInvocation setReturnValue:&result]; } else if (factory) { [factory forwardInvocation:anInvocation]; } else { TyphoonRuntimeArguments *args = [TyphoonRuntimeArguments argumentsFromInvocation:anInvocation]; TyphoonDefinition *definition = [self.definitionBuilder builtDefinitionForKey:key args:args]; [anInvocation retainArguments]; [anInvocation setReturnValue:&definition]; } } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { @synchronized (self) { NSString *key = [TyphoonAssemblySelectorAdviser keyForSEL:aSelector]; TyphoonComponentFactory *factory = self.factory; if ([self.collaboratingAssemblies valueForKey:key] || !factory) { return [self.assembly methodSignatureForSelector:aSelector]; } else { return [factory methodSignatureForSelector:aSelector]; } } } //------------------------------------------------------------------------------------------- #pragma mark - //------------------------------------------------------------------------------------------- - (id)typhoonCustomObjectInjection { return [[TyphoonInjectionByComponentFactory alloc] init]; } //------------------------------------------------------------------------------------------- #pragma mark - //------------------------------------------------------------------------------------------- - (id)componentForType:(id)classOrProtocol { return [self.factory componentForType:classOrProtocol]; } - (NSArray *)allComponentsForType:(id)classOrProtocol { return [self.factory allComponentsForType:classOrProtocol]; } - (id)componentForKey:(NSString *)key { return [self.factory componentForKey:key]; } - (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args { return [self.factory componentForKey:key args:args]; } - (void)inject:(id)instance { [self.factory inject:instance]; } - (void)inject:(id)instance withSelector:(SEL)selector { [self.factory inject:instance withSelector:selector]; } - (void)makeDefault { [self.factory makeDefault]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (void)attachPostProcessor:(id )postProcessor { [self attachDefinitionPostProcessor:postProcessor]; } #pragma clang diagnostic pop - (void)attachDefinitionPostProcessor:(id )postProcessor { [self.factory attachDefinitionPostProcessor:postProcessor]; } - (void)attachInstancePostProcessor:(id)postProcessor { [self.factory attachInstancePostProcessor:postProcessor]; } - (void)attachTypeConverter:(id)typeConverter { [self.factory attachTypeConverter:typeConverter]; } - (id)objectForKeyedSubscript:(id)key { return [self.factory objectForKeyedSubscript:key]; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Hooks/NSObject+FactoryHooks.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// @interface NSObject (FactoryHooks) /** * @ingroup Assembly * * Implementation of method typhoonSetFactory indicates that a component wishes to be aware of the TyphoonComponentFactory in order to resolve another component. * Typically we'd want to inject all the dependencies, however there are some cases where its desirable to load a component from the factory * at runtime. One example is view controller transitions where a given view controller has: * * - All of the dependencies for its main use-case injected * - Looks up the view controller for a transition from the container. This allows using prototype scope to load one object-graph at a time, * thus making efficient use of memory. * */ /** * Accepts the TyphoonComponentFactory via setter-injection, allowing the factory to be stored to a property or ivar. Note that this method * contract uses the type id, which if you're using a block-style assembly allows setting the factory to the TyphoonAssembly sub-class * itself without casting. The underlying type is TyphoonComponentFactory. * * ##Examples: @code //Using the TyphoonComponentFactory interface: - (void)typhoonSetFactory:(TyphoonComponentFactory*)factory { _factory = factory; MyAnalyticsService* service = [factory componentForType:[MyAnalyticsService class]; } @endcode @code //Using an Assembly interface - (void)typhoonSetFactory:(MyAssemblyType*)assembly { _assembly = assembly; MyAnalyticsService* service = [assembly analyticsService]; } @endcode * @note Whether the factory is injected as a TyphoonComponentFactory or a TyphoonAssembly sub-class, it can still be casted from one to the * other. */ - (void)typhoonSetFactory:(id)theFactory; /** * @ingroup Assembly * * Typhoon components can implement this methods to participate in property-injection life-cycle events. This gives some of the benefits * of initializer-injection - the ability to provide before / after validation - while still allowing the flexibility of property injection. * * @note If you don't wish to implement these methods on your class, you can also define custom callback selectors on TyphoonDefinition. * * @see TyphoonDefinition.beforeInjections * @see TyphoonDefinition.afterInjections * */ /** * Typhoon calls this method (if implemented) just before property and method injections */ - (void)typhoonWillInject; /** * Typhoon calls this method (if implemented) just after property and method injections */ - (void)typhoonDidInject; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSInvocation+TCFCustomImplementation.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOONFRAMEWORK.ORG // Copyright 2016 typhoonframework.org Pty Ltd // All Rights Reserved. // // NOTICE: Prepared by AppsQuick.ly on behalf of typhoonframework.org. This software // is proprietary information. Unauthorized use is prohibited. // //////////////////////////////////////////////////////////////////////////////// #import @interface NSInvocation (TCFCustomImplementation) /** * Works with `id` arguments and `id` return values only * */ - (void)invokeWithCustomImplementation:(IMP)impl; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSInvocation+TCFCustomImplementation.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOONFRAMEWORK.ORG // Copyright 2016 typhoonframework.org Pty Ltd // All Rights Reserved. // // NOTICE: Prepared by AppsQuick.ly on behalf of typhoonframework.org. This software // is proprietary information. Unauthorized use is prohibited. // //////////////////////////////////////////////////////////////////////////////// #import "NSInvocation+TCFCustomImplementation.h" #import "TyphoonAssemblyDefinitionBuilder.h" @implementation NSInvocation (TCFCustomImplementation) - (id)argumentAtIndex:(NSInteger)index { const char *type = [self.methodSignature getArgumentTypeAtIndex:(NSUInteger)index]; if (IsPrimitiveArgumentType(type)) { return nil; } void *unsafeArgument; [self getArgument:&unsafeArgument atIndex:index]; return (__bridge id)unsafeArgument; } - (void)invokeWithCustomImplementation:(IMP)impl { id result = nil; #define Arg(N) [self argumentAtIndex:N] switch (self.methodSignature.numberOfArguments) { case 2: result = ((id(*)(id, SEL))impl)(self.target, self.selector); break; case 3: result = ((id(*)(id, SEL, id))impl)(self.target, self.selector, Arg(2)); break; case 4: result = ((id(*)(id, SEL, id, id))impl)(self.target, self.selector, Arg(2), Arg(3)); break; case 5: result = ((id(*)(id, SEL, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4)); break; case 6: result = ((id(*)(id, SEL, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5)); break; case 7: result = ((id(*)(id, SEL, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6)); break; case 8: result = ((id(*)(id, SEL, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7)); break; case 9: result = ((id(*)(id, SEL, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8)); break; case 10: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9)); break; case 11: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10)); break; case 12: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11)); break; case 13: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12)); break; case 14: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12), Arg(13)); break; case 15: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12), Arg(13), Arg(14)); break; case 16: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12), Arg(13), Arg(14), Arg(15)); break; case 17: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12), Arg(13), Arg(14), Arg(15), Arg(16)); break; case 18: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12), Arg(13), Arg(14), Arg(15), Arg(16), Arg(17)); break; case 19: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12), Arg(13), Arg(14), Arg(15), Arg(16), Arg(17), Arg(18)); break; case 20: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12), Arg(13), Arg(14), Arg(15), Arg(16), Arg(17), Arg(18), Arg(19)); break; case 21: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12), Arg(13), Arg(14), Arg(15), Arg(16), Arg(17), Arg(18), Arg(19), Arg(20)); break; case 22: result = ((id(*)(id, SEL, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id, id))impl)(self.target, self.selector, Arg(2), Arg(3), Arg(4), Arg(5), Arg(6), Arg(7), Arg(8), Arg(9), Arg(10), Arg(11), Arg(12), Arg(13), Arg(14), Arg(15), Arg(16), Arg(17), Arg(18), Arg(19), Arg(20), Arg(21)); break; default: NSAssert(NO, @"NSInvocation isn't supporting more than 20 arguments"); break; } [self setReturnValue:&result]; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSInvocation+TCFInstanceBuilder.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** TyphoonUtils category used to move NSInvocation invoking logic into non-arc file * The problem was when calling 'init' method through NSInvocation on instance created * by 'alloc' in ARC-managed file. * @code { id instance = [aClass alloc]; //ARC transfer ownership to strong local variable 'instance' (retainCount = 1) [invocation setSelector:@selector(initWith..)]; //in some cases, 'initWith..' method can create new instance and release firstly created 'instance' //If it happens 'instance' will be dealloced (retainCount = 0) [invocation invokeWithTarget:instance]; } //'instance' is out of scope. ARC Releases 'instance' - crash if already dealloced * @endcode * To fix this problem we decided to move 'alloc' and NSInvocation with 'initWith..' into non-arc * file and do all right and manually. * * Objects returned by methods from this category are retained, so you responsable to release, * or ARC will do it for you gracefully */ @interface NSInvocation (TCFInstanceBuilder) /** Return result of invoking self on provided instance. Note that result is retained */ - (id)typhoon_resultOfInvokingOnInstance:(id)instance NS_RETURNS_RETAINED; /** Creates instance by 'alloc' message on given class and returns result of invoking self on created instance. Note that result is retained */ - (id)typhoon_resultOfInvokingOnAllocationForClass:(Class)aClass NS_RETURNS_RETAINED; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSInvocation+TCFInstanceBuilder.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSInvocation+TCFInstanceBuilder.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonUtils.h" #if __has_feature(objc_arc) #error You have to disable ARC for this file #endif @implementation NSInvocation (TCFInstanceBuilder) /** Returns YES if selector returns retained instance (not autoreleased) */ static BOOL typhoon_IsSelectorReturnsRetained(SEL selector) { // According to http://clang.llvm.org/docs/AutomaticReferenceCounting.html#method-families // for a selector to be in a given family, the selector must start with the // family name, ignoring underscore prefixes, and followed by a character // other than a lowercase letter. // Otherwise methods like [MYRhyme initialRhyme] or [Player newbieWithName:] // will match incorrectly. static NSRegularExpression *methodFamily = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSError *error = nil; #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wassign-enum" methodFamily = [[NSRegularExpression alloc] initWithPattern:@"^_*(init|new|copy|mutableCopy)($|[^a-z])" options:0 error:&error]; #pragma clang diagnostic pop if (!methodFamily) { @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:[error localizedDescription] userInfo:[error userInfo]]; } }); NSString *selectorString = NSStringFromSelector(selector); NSUInteger numberOfMatches = [methodFamily numberOfMatchesInString:selectorString options:NSMatchingAnchored range:NSMakeRange(0, selectorString.length)]; return numberOfMatches != 0; } - (id)typhoon_resultOfInvokingOn:(id)instanceOrClass NS_RETURNS_RETAINED { id returnValue = nil; [self invokeWithTarget:instanceOrClass]; /* getReturnValue method call must be called inside this file, because ARC is turned off here. * The reason to turn off ARC, because it doesn't work properly with NSInvocation return values and unknown selector. * ARC calls retain/release, which works bad on class clusters (initial value might be autoreleased or released * inside initializer, then over-released by ARC and that lead crash. It also doesnt work well with primitives, so we need to * solve it all here, in NON-ARC file */ returnValue = [self typhoon_getReturnValue]; #ifndef __clang_analyzer__ if (!typhoon_IsSelectorReturnsRetained([self selector])) { [returnValue retain]; /* Retain to take ownership on autoreleased object */ } #endif return returnValue; } - (id)typhoon_getReturnValue NS_RETURNS_RETAINED { const char *type = [self.methodSignature methodReturnType]; if (CStringEquals(type, "@") || // object CStringEquals(type, "@?") || // block CStringEquals(type, "#")) // metaclass { void *pointer = NULL; [self getReturnValue:&pointer]; id returnValue = (id)pointer; if (IsBlock(type)) { returnValue = [returnValue copy]; // Converting NSStackBlock to NSMallocBlock } return returnValue; } else { NSUInteger returnValueSize; NSGetSizeAndAlignment(type, &returnValueSize, NULL); void *buffer = malloc(returnValueSize); [self getReturnValue:buffer]; id returnValue = [[NSValue alloc] initWithBytes:buffer objCType:type]; free(buffer); return returnValue; } } - (id)typhoon_resultOfInvokingOnInstance:(id)instance { return [self typhoon_resultOfInvokingOn:instance]; } - (id)typhoon_resultOfInvokingOnAllocationForClass:(Class)aClass { /* To static analyser warning: * 'firstlyCreatedInstance' is not leak. There is two cases: * 1) instance is firstlyCreatedInstance (have same pointer) - then we returning this as retained result * 2) instance is not firstlyCreatedInstance (have different pointer) - then 'init...' method responsible * to release 'firstlyCreatedInstance' * But clang analyzer dont know this.. */ #ifndef __clang_analyzer__ id firstlyCreatedInstance = [aClass alloc]; return [self typhoon_resultOfInvokingOn:firstlyCreatedInstance]; #else return nil; #endif } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSInvocation+TCFUnwrapValues.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface NSInvocation (TCFUnwrapValues) /** Set object as argument at index, plus unwrap NSValue and NSNumber if needed */ - (void)typhoon_setArgumentObject:(id)object atIndex:(NSInteger)idx; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSInvocation+TCFUnwrapValues.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSInvocation+TCFUnwrapValues.h" #import "NSValue+TCFUnwrapValues.h" #import "TyphoonUtils.h" #import "NSMethodSignature+TCFUnwrapValues.h" @implementation NSInvocation (TCFUnwrapValues) - (void)typhoon_setArgumentObject:(id)object atIndex:(NSInteger)idx { BOOL isValue = [object isKindOfClass:[NSValue class]]; if (isValue) { [(NSValue *) object typhoon_setAsArgumentForInvocation:self atIndex:(NSUInteger)idx]; } else { [self setArgument:&object atIndex:idx]; } } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSInvocation+TCFWrapValues.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface NSInvocation (TCFWrapValues) /** Get argument at index, wrapping primitive values in NSValue if needed */ - (id)typhoon_getArgumentObjectAtIndex:(NSInteger)idx; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSInvocation+TCFWrapValues.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSInvocation+TCFWrapValues.h" #import "TyphoonUtils.h" #import "TyphoonIntrospectionUtils.h" @implementation NSInvocation (TCFWrapValues) - (id)typhoon_getArgumentObjectAtIndex:(NSInteger)idx { const char *argumentType = [self.methodSignature getArgumentTypeAtIndex:(NSUInteger)idx]; if (CStringEquals(argumentType, "@") || // object CStringEquals(argumentType, "@?") || // block CStringEquals(argumentType, "#")) // metaclass { void *pointer; [self getArgument:&pointer atIndex:idx]; id argument = (__bridge id) pointer; if (IsBlock(argumentType)) { return [argument copy]; // Converting NSStackBlock to NSMallocBlock } return argument; } else { NSUInteger argumentSize; NSGetSizeAndAlignment(argumentType, &argumentSize, NULL); void *buffer = malloc(argumentSize); [self getArgument:buffer atIndex:idx]; id argument = [NSValue valueWithBytes:buffer objCType:argumentType]; free(buffer); return argument; } } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSMethodSignature+TCFUnwrapValues.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface NSMethodSignature (TCFUnwrapValues) - (BOOL)shouldUnwrapValue:(id)value forArgumentAtIndex:(NSUInteger)index; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSMethodSignature+TCFUnwrapValues.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSMethodSignature+TCFUnwrapValues.h" #import "TyphoonUtils.h" @implementation NSMethodSignature (TCFUnwrapValues) - (BOOL)shouldUnwrapValue:(id)value forArgumentAtIndex:(NSUInteger)index { const char *argumentType = [self getArgumentTypeAtIndex:index]; BOOL isPrimitive = !CStringEquals(argumentType, @encode(id)); BOOL isObjectIsWrapper = [value isKindOfClass:[NSValue class]]; return isPrimitive && isObjectIsWrapper; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSValue+TCFUnwrapValues.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface NSValue (TCFUnwrapValues) - (void)typhoon_setAsArgumentForInvocation:(NSInvocation *)invocation atIndex:(NSUInteger)index; @end /* Since NSNumber is subclass of NSValue, this category lives in same file */ @interface NSNumber (TCFUnwrapValues) - (void)typhoon_setAsArgumentForInvocation:(NSInvocation *)invocation atIndex:(NSUInteger)index; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/NSValue+TCFUnwrapValues.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSValue+TCFUnwrapValues.h" #import "TyphoonUtils.h" @implementation NSValue (TCFUnwrapValues) - (void)typhoon_setAsArgumentForInvocation:(NSInvocation *)invocation atIndex:(NSUInteger)index { const char *argumentType = [[invocation methodSignature] getArgumentTypeAtIndex:index]; if (CStringEquals(argumentType, @encode(id))) { id selfRef = self; [invocation setArgument:&selfRef atIndex:(NSInteger)index]; } else { //argument is primitive NSUInteger argumentSize; NSGetSizeAndAlignment(argumentType, &argumentSize, NULL); const char *valueType = [self objCType]; if (!CStringEquals(valueType, argumentType)) { NSUInteger valueSize; NSGetSizeAndAlignment(valueType, &valueSize, NULL); NSAssert(valueSize <= argumentSize, @"Trying to inject NSValue with type of different size ('%s' expected, but '%s' passed)", argumentType, valueType); } void *buffer = alloca(argumentSize); [self getValue:buffer]; [invocation setArgument:buffer atIndex:(NSInteger)index]; } } @end @implementation NSNumber (TCFUnwrapValues) - (void)typhoon_setAsArgumentForInvocation:(NSInvocation *)invocation atIndex:(NSUInteger)index { const char *argumentType = [[invocation methodSignature] getArgumentTypeAtIndex:index]; NSInteger signedIndex = (NSInteger)index; /** Doing this type switching below because when we call NSNumber's methods like 'doubleValue' or 'floatValue', * value will be converted if necessary. (instead of approach when we just copy bytes - see NSValue category above) * That will handle situation when for example argumentType is float, but NSNumber's type is double */ if (CStringEquals(argumentType, @encode(id))) { id converted = self; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(int))) { int converted = [self intValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(unsigned int))) { unsigned int converted = [self unsignedIntValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(char))) { char converted = [self charValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(unsigned char))) { unsigned char converted = [self unsignedCharValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(bool))) { bool converted = [self boolValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(short))) { short converted = [self shortValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(unsigned short))) { unsigned short converted = [self unsignedShortValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(float))) { float converted = [self floatValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(double))) { double converted = [self doubleValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(long))) { long converted = [self longValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(unsigned long))) { unsigned long converted = [self unsignedLongValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(long long))) { long long converted = [self longLongValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else if (CStringEquals(argumentType, @encode(unsigned long long))) { unsigned long long converted = [self unsignedLongLongValue]; [invocation setArgument:&converted atIndex:signedIndex]; } else { [NSException raise:@"InvalidNumberType" format:@"Invalid Number: Type '%s' is not supported.", argumentType]; } } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyAdviser.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonMethodSwizzler.h" @class TyphoonAssembly; @interface TyphoonAssemblyAdviser : NSObject //+ (BOOL)assemblyClassIsAdvised:(Class)klass; - (id)initWithAssembly:(TyphoonAssembly *)assembly; //- (void)adviseAssembly; - (NSSet *)definitionSelectors; - (NSDictionary *)assemblyClassPerDefinitionKey; @property(readonly, weak) TyphoonAssembly *assembly; @property(nonatomic, strong) id swizzler; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyAdviser.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAssemblySelectorAdviser.h" #import "TyphoonAssemblyAdviser.h" #import "TyphoonAssembly.h" #import #import "TyphoonAssembly+TyphoonAssemblyFriend.h" #import "OCLogTemplate.h" #import "TyphoonSelector.h" #import "TyphoonSwizzlerDefaultImpl.h" #import "TyphoonIntrospectionUtils.h" static NSMutableSet *advisedAssemblyClasses; @interface TyphoonAssemblyAdviser () @end @implementation TyphoonAssemblyAdviser + (void)initialize { @synchronized (self) { advisedAssemblyClasses = [NSMutableSet new]; } } - (id)initWithAssembly:(TyphoonAssembly *)assembly { self = [super init]; if (self) { _assembly = assembly; _swizzler = [TyphoonSwizzlerDefaultImpl instance]; } return self; } #pragma mark - Advising //- (void)adviseAssembly //{ // @synchronized (self) { // [self swizzleAssemblyMethods]; // } //} // //- (void)swizzleAssemblyMethods //{ // [self enumerateDefinitionSelectorsUsingBlock:^(NSSet *definitionSelectors, Class assemblyClass) { // if (![[self class] assemblyClassIsAdvised:assemblyClass]) { // [self swizzleDefinitionSelectors:definitionSelectors onAssemblyClass:assemblyClass]; // [[self class] markAsAdvisedAssemblyClass:assemblyClass]; // } // }]; //} //- (void)swizzleDefinitionSelectors:(NSSet *)definitionSelectors onAssemblyClass:(Class)assemblyClass //{ // [definitionSelectors enumerateObjectsUsingBlock:^(TyphoonSelector *selectorObj, BOOL *stop) { // [self swapImplementationOfDefinitionSelectorWithAdvisedImplementation:selectorObj onAssemblyClass:assemblyClass]; // }]; //} // //- (void)swapImplementationOfDefinitionSelectorWithAdvisedImplementation:(TyphoonSelector *)wrappedSEL onAssemblyClass:(Class)assemblyClass //{ // SEL methodSelector = [wrappedSEL selector]; // SEL advisedSelector = [TyphoonAssemblySelectorAdviser advisedSELForSEL:methodSelector class:assemblyClass]; // // NSError *error; // BOOL success = [_swizzler swizzleMethod:methodSelector withMethod:advisedSelector onClass:assemblyClass error:&error]; // if (!success) { // [TyphoonAssemblyAdviser onFailureToSwizzleDefinitionSelector:methodSelector withAdvisedSelector:advisedSelector onAssemblyClass:assemblyClass error:error]; // } //} //+ (void)onFailureToSwizzleDefinitionSelector:(SEL)methodSelector withAdvisedSelector:(SEL)swizzled onAssemblyClass:(Class)assemblyClass error:(NSError *)err //{ // LogError(@"Failed to swizzle method '%@' on class '%@' with method '%@'.", NSStringFromSelector(methodSelector), NSStringFromClass(assemblyClass), NSStringFromSelector(swizzled)); // LogError(@"'%@'", err); // [NSException raise:NSInternalInconsistencyException format:@"Failed to swizzle method, everything is broken!"]; //} #pragma mark - Definition Selector Enumerator /** @return Set of TyphoonSelector, pointing definitions methods */ - (NSSet *)definitionSelectors { NSMutableSet *definitionSelectors = [[NSMutableSet alloc] init]; [self enumerateDefinitionSelectorsUsingBlock:^(NSSet *selectors, Class assemblyClass) { [definitionSelectors unionSet:selectors]; }]; return definitionSelectors; } - (NSDictionary *)assemblyClassPerDefinitionKey { NSMutableDictionary *result = [NSMutableDictionary new]; [self enumerateDefinitionSelectorsUsingBlock:^(NSSet *definitionSelectors, Class assemblyClass) { for (TyphoonSelector *sel in definitionSelectors) { NSString *key = [TyphoonAssemblySelectorAdviser keyForSEL:sel.selector]; if (!result[key]) { result[key] = assemblyClass; } } }]; return result; } - (void)enumerateDefinitionSelectorsUsingBlock:(void(^)(NSSet *definitionSelectors, Class assemblyClass))block { Class currentClass = [self.assembly class]; while ([self classNotRootAssemblyClass:currentClass]) { NSSet *allSelectors = [self definitionSelectorsInAssemblyClass:currentClass]; block(allSelectors, currentClass); currentClass = class_getSuperclass(currentClass); } } - (BOOL)classNotRootAssemblyClass:(Class)class { return class != [TyphoonAssembly class]; } - (NSSet *)definitionSelectorsInAssemblyClass:(Class)pClass { NSMutableSet *definitionSelectors = [[NSMutableSet alloc] init]; NSSet *selectorNames = [TyphoonIntrospectionUtils methodsForClass:pClass upToParentClass:[pClass superclass]]; for (NSString *selectorName in selectorNames) { SEL sel = NSSelectorFromString(selectorName); if ([self isSelector:sel notAdvisedOnClass:pClass] && [self isSelector:sel notReservedOnClass:pClass]) { [definitionSelectors addObject:[TyphoonSelector selectorWithSEL:sel]]; } } [definitionSelectors minusSet:[self propertySelectorsForClass:pClass]]; return definitionSelectors; } - (BOOL)isSelector:(SEL)method notReservedOnClass:(Class)aClass { return ![aClass selectorIsReserved:method]; } - (BOOL)isSelector:(SEL)sel notAdvisedOnClass:(Class)aClass { return YES;//![TyphoonAssemblySelectorAdviser selectorIsAdvised:sel]; } - (NSSet *)propertySelectorsForClass:(Class)clazz { NSMutableSet *propertySelectors = [NSMutableSet new]; NSSet *properties = [TyphoonIntrospectionUtils propertiesForClass:clazz upToParentClass:[clazz superclass]]; for (NSString *propertyName in properties) { SEL propertySetter = [TyphoonIntrospectionUtils setterForPropertyWithName:propertyName inClass:clazz]; if (propertySetter) { [propertySelectors addObject:[TyphoonSelector selectorWithSEL:propertySetter]]; } SEL propertyGetter = [TyphoonIntrospectionUtils getterForPropertyWithName:propertyName inClass:clazz]; if (propertyGetter) { [propertySelectors addObject:[TyphoonSelector selectorWithSEL:propertyGetter]]; } } return propertySelectors; } #pragma mark - Advising Registry //- (BOOL)classIsNotAlreadyAdvised:(Class)class //{ // return ![[self class] assemblyClassIsAdvised:class]; //} // //+ (void)markAsAdvisedAssemblyClass:(Class)assemblyClass //{ // [advisedAssemblyClasses addObject:assemblyClass]; //} // //+ (void)markAsNotAdvisedAssemblyClass:(Class)assemblyClass //{ // [advisedAssemblyClasses removeObject:assemblyClass]; //} // //+ (BOOL)assemblyClassIsAdvised:(Class)assemblyClass //{ // return [advisedAssemblyClasses containsObject:assemblyClass]; //} @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyBuilder+PlistProcessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAssemblyBuilder.h" @interface TyphoonAssemblyBuilder (PlistProcessor) + (id)buildAssembliesFromPlistInBundle:(NSBundle *)bundle; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyBuilder+PlistProcessor.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAssemblyBuilder+PlistProcessor.h" #import "TyphoonIntrospectionUtils.h" @implementation TyphoonAssemblyBuilder (PlistProcessor) + (id)buildAssembliesFromPlistInBundle:(NSBundle *)bundle { NSArray *assemblies = nil; NSArray *assemblyNames = [self plistAssemblyNames:bundle]; NSAssert(!assemblyNames || [assemblyNames isKindOfClass:[NSArray class]], @"Value for 'TyphoonInitialAssemblies' key must be array"); if ([assemblyNames count] > 0) { NSMutableArray *assemblyClasses = [[NSMutableArray alloc] initWithCapacity:[assemblyNames count]]; for (NSString *assemblyName in assemblyNames) { Class assemblyClass = TyphoonClassFromString(assemblyName); if (!assemblyClass) { [NSException raise:NSInvalidArgumentException format:@"Can't resolve assembly for name %@", assemblyName]; } [assemblyClasses addObject:assemblyClass]; } assemblies = [self buildAssembliesWithClasses:assemblyClasses]; } return assemblies; } + (NSArray *)plistAssemblyNames:(NSBundle *)bundle { NSArray *names = nil; NSDictionary *bundleInfoDictionary = [bundle infoDictionary]; #if TARGET_OS_IPHONE if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) { names = bundleInfoDictionary[@"TyphoonInitialAssemblies(iPad)"]; } else { names = bundleInfoDictionary[@"TyphoonInitialAssemblies(iPhone)"]; } #endif if (!names) { names = bundleInfoDictionary[@"TyphoonInitialAssemblies"]; } return names; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyBuilder.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonAssemblyBuilder : NSObject + (id)buildAssemblyWithClass:(Class)assemblyClass; + (NSArray *)buildAssembliesWithClasses:(NSArray *)assemblyClasses; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyBuilder.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAssemblyBuilder.h" #import "TyphoonAssembly.h" @implementation TyphoonAssemblyBuilder + (id)buildAssemblyWithClass:(Class)assemblyClass { return [[self buildAssembliesWithClasses:@[assemblyClass]] firstObject]; } + (id)buildAssembliesWithClasses:(NSArray *)assemblyClasses { if (assemblyClasses.count == 0) { return nil; } NSMutableArray *assemblies = [[NSMutableArray alloc] initWithCapacity:[assemblyClasses count]]; for (Class assemblyClass in assemblyClasses) { if (!assemblyClass) { [NSException raise:NSInvalidArgumentException format:@"Can't resolve assembly for class %@", assemblyClass]; } if (![assemblyClass isSubclassOfClass:[TyphoonAssembly class]]) { [NSException raise:NSInvalidArgumentException format:@"Class %@ is not a subclass of TyphoonAssembly", assemblyClass]; } [assemblies addObject:[assemblyClass assembly]]; } return [assemblies copy]; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyDefinitionBuilder.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonAssembly; @class TyphoonDefinition; @class TyphoonRuntimeArguments; BOOL IsPrimitiveArgumentType(const char *argumentType); @interface TyphoonAssemblyDefinitionBuilder : NSObject - (instancetype)initWithAssembly:(TyphoonAssembly *)assembly; - (NSArray *)builtDefinitions; - (TyphoonDefinition *)builtDefinitionForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args; @property(readonly, unsafe_unretained) TyphoonAssembly *assembly; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyDefinitionBuilder.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAssemblyDefinitionBuilder.h" #import "TyphoonAssembly.h" #import "OCLogTemplate.h" #import "TyphoonDefinition+Internal.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonBlockDefinition.h" #import "TyphoonAssembly+TyphoonAssemblyFriend.h" #import "TyphoonAssemblySelectorAdviser.h" #import "TyphoonCircularDependencyTerminator.h" #import "TyphoonSelector.h" #import "TyphoonInjections.h" #import "TyphoonUtils.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonReferenceDefinition.h" #import "TyphoonBlockDefinitionController.h" #import "NSInvocation+TCFCustomImplementation.h" #import #import static id objc_msgSend_InjectionArguments(id target, id implTarget, SEL selector, NSMethodSignature *signature); static id InjectionForArgumentType(const char *argumentType, NSUInteger index); @implementation TyphoonAssemblyDefinitionBuilder { NSMutableDictionary *_resolveStackForSelector; NSMutableDictionary *_cachedDefinitionsForMethodName; } - (instancetype)initWithAssembly:(TyphoonAssembly *)assembly { self = [super init]; if (self) { _assembly = assembly; _resolveStackForSelector = [[NSMutableDictionary alloc] init]; _cachedDefinitionsForMethodName = [[NSMutableDictionary alloc] init]; } return self; } - (NSArray *)builtDefinitions { @synchronized (self) { [self populateCache]; return [_cachedDefinitionsForMethodName allValues]; } } - (void)populateCache { // Use the configuration route for potential TyphoonBlockDefinitions. [[TyphoonBlockDefinitionController currentController] useConfigurationRouteWithinBlock:^{ [[self.assembly definitionSelectors] enumerateObjectsUsingBlock:^(TyphoonSelector *wrappedSEL, BOOL *stop) { SEL selector = [wrappedSEL selector]; NSString *key = [TyphoonAssemblySelectorAdviser keyForSEL:selector]; [self buildDefinitionForKey:key]; }]; }]; } - (void)buildDefinitionForKey:(NSString *)key { [self builtDefinitionForKey:key args:nil]; } - (TyphoonDefinition *)builtDefinitionForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args { [self markCurrentlyResolvingKey:key]; if ([self keyInvolvedInCircularDependency:key]) { return [self definitionToTerminateCircularDependencyForKey:key args:args]; } id cached = [self populateCacheWithDefinitionForKey:key]; [self markKeyResolved:key]; if ([cached isKindOfClass:[TyphoonDefinition class]]) { /* Set current runtime args to know passed arguments when build definition */ ((TyphoonDefinition *) cached).currentRuntimeArguments = args; BOOL shouldApplyConcreteNamespace = [((TyphoonDefinition *) cached) space] == nil; if (shouldApplyConcreteNamespace) { [((TyphoonDefinition *) cached) applyConcreteNamespace:NSStringFromClass([self.assembly class])]; } } LogTrace(@"Did finish building definition for key: '%@'", key); return cached; } #pragma mark - Circular Dependencies - (NSMutableArray *)resolveStackForKey:(NSString *)key { NSMutableArray *resolveStack = [_resolveStackForSelector objectForKey:key]; if (!resolveStack) { resolveStack = [[NSMutableArray alloc] init]; [_resolveStackForSelector setObject:resolveStack forKey:key]; } return resolveStack; } - (void)markCurrentlyResolvingKey:(NSString *)key { [[self resolveStackForKey:key] addObject:key]; } - (BOOL)keyInvolvedInCircularDependency:(NSString *)key { NSMutableArray *resolveStack = [self resolveStackForKey:key]; if ([resolveStack count] >= 2) { NSString *bottom = [resolveStack objectAtIndex:0]; NSString *top = [resolveStack lastObject]; if ([top isEqualToString:bottom]) { LogTrace(@"Circular dependency detected in definition for key '%@'.", key); return YES; } } return NO; } - (TyphoonDefinition *)definitionToTerminateCircularDependencyForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args { // we return a 'dummy' definition just to terminate the cycle. This dummy definition will be overwritten by the real one in the cache, // which will be set further up the stack and will overwrite this one in 'cachedDefinitionsForMethodName'. TyphoonDefinition *dummy = [[TyphoonDefinition alloc] initWithClass:[TyphoonCircularDependencyTerminator class] key:key]; dummy.currentRuntimeArguments = args; return dummy; } - (void)markKeyResolved:(NSString *)key { NSMutableArray *resolveStack = [self resolveStackForKey:key]; if (resolveStack.count) { [resolveStack removeAllObjects]; } } #pragma mark - Building - (TyphoonDefinition *)populateCacheWithDefinitionForKey:(NSString *)key { id d = [self cachedDefinitionForKey:key]; if (!d) { d = [self definitionForKey:key]; d = [self populateCacheWithDefinition:d forKey:key]; } return d; } - (id)cachedDefinitionForKey:(NSString *)key { return _cachedDefinitionsForMethodName[key]; } - (id)definitionForKey:(NSString *)key { // Call the user's assembly method to get it. SEL sel = [self assemblySelectorForKey:key]; NSMethodSignature *signature = [self.assembly methodSignatureForSelector:sel]; id cached = objc_msgSend_InjectionArguments(self.assembly.accessor, self.assembly, sel, signature); // the advisedSEL will call through to the original, unwrapped implementation because prepareForUse has been called, and all our definition methods have been swizzled. // This method will likely call through to other definition methods on the assembly, which will go through the advising machinery because of this swizzling. // Therefore, the definitions a definition depends on will be fully constructed before they are needed to construct that definition. return cached; } - (SEL)assemblySelectorForKey:(NSString *)key { return [TyphoonAssemblySelectorAdviser SELForKey:key class:[self.assembly assemblyClassForKey:key]]; } static id objc_msgSend_InjectionArguments(id target, id implTarget, SEL selector, NSMethodSignature *signature) { if (signature.numberOfArguments > 2) { void *unsafeResult; IMP method = [((NSObject *)implTarget) methodForSelector:selector]; NSUInteger primitiveArgumentIndex = NSNotFound; NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setSelector:selector]; [invocation setTarget:target]; [invocation retainArguments]; /* Fill invocation arguments with TyphoonInjectionWithRuntimeArgumentAtIndex injections */ for (NSUInteger i = 0; i < signature.numberOfArguments - 2; i++) { const char *argumentType = [signature getArgumentTypeAtIndex:i + 2]; if (!IsPrimitiveArgumentType(argumentType)) { id injection = InjectionForArgumentType(argumentType, i); [invocation setArgument:&injection atIndex:(NSInteger)(i + 2)]; } else { [NSException raise:NSInvalidArgumentException format:@"The method '%@' in assembly '%@', contains a runtime argument of primitive type (BOOL, int, CGFloat, etc) at index %@. Runtime arguments for TyphoonDefinitions can only be objects. Use TyphoonBlockDefinition, or wrappers like NSNumber or NSValue (they will be unwrapped into primitive value during injection) ", [TyphoonAssemblySelectorAdviser keyForSEL:selector], [target class], @(primitiveArgumentIndex)]; } } [invocation invokeWithCustomImplementation:method]; [invocation getReturnValue:&unsafeResult]; id result = (__bridge id) unsafeResult; return result; } else { id(*impl)(id, SEL) = (id(*)(id, SEL))[((NSObject *)implTarget) methodForSelector:selector]; return impl(target, selector); } } static id InjectionForArgumentType(const char *argumentType, NSUInteger index) { /** We are checking here, if argument type is block, then we have to wrap our injection into 'real' block, * otherwise, during assignment runtime will try to call Block_copy and it will crash if object is not 'real' block */ if (CStringEquals(argumentType, "@?")) { return TyphoonInjectionWithRuntimeArgumentAtIndexWrappedIntoBlock(index); } else { return TyphoonInjectionWithRuntimeArgumentAtIndex(index); } } BOOL IsPrimitiveArgumentType(const char *argumentType) { return (!CStringEquals(argumentType, "@") && // object !CStringEquals(argumentType, "@?") && // block !CStringEquals(argumentType, "#")); // metaClass } - (TyphoonDefinition *)populateCacheWithDefinition:(TyphoonDefinition *)definition forKey:(NSString *)key { if (definition && [definition isKindOfClass:[TyphoonDefinition class]]) { definition = [self definitionBySettingKey:key toDefinition:definition]; _cachedDefinitionsForMethodName[key] = definition; } return definition; } - (TyphoonDefinition *)definitionBySettingKey:(NSString *)key toDefinition:(TyphoonDefinition *)definition { TyphoonDefinition *result = definition; if ([result.key length] == 0) { result.key = key; } if (result.processed && ![definition.key isEqualToString:key]) { result = [TyphoonShortcutDefinition definitionWithKey:key referringTo:definition]; } if (!result.processed) { result.assembly = _assembly; result.assemblySelector = [self assemblySelectorForKey:key]; } result.processed = YES; return result; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyPropertyInjectionPostProcessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonFactoryPropertyInjectionPostProcessor.h" /** * Replaces property injections by-type with injectuins by-componentFactory when property type subclass of TyphoonAssembly */ @interface TyphoonAssemblyPropertyInjectionPostProcessor : TyphoonFactoryPropertyInjectionPostProcessor @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblyPropertyInjectionPostProcessor.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAssemblyPropertyInjectionPostProcessor.h" #import "TyphoonComponentFactory.h" #import "TyphoonDefinition.h" #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonAssembly.h" #import "TyphoonInjectionByType.h" @implementation TyphoonAssemblyPropertyInjectionPostProcessor - (BOOL)shouldReplaceInjectionByType:(TyphoonInjectionByType *)propertyInjection withFactoryInjectionInDefinition:(TyphoonDefinition *)definition { BOOL isAssemblyClass = NO; TyphoonTypeDescriptor *type = [TyphoonIntrospectionUtils typeForPropertyNamed:propertyInjection.propertyName inClass:definition.type]; if (type.typeBeingDescribed) { isAssemblyClass = [type.typeBeingDescribed isSubclassOfClass:[TyphoonAssembly class]]; } return isAssemblyClass; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblySelectorAdviser.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonAssemblySelectorAdviser : NSObject + (SEL)SELForKey:(NSString *)key class:(Class)clazz; //+ (SEL)advisedSELForSEL:(SEL)unwrappedSEL class:(Class)clazz; //+ (NSString *)keyForAdvisedSEL:(SEL)selWithAdvicePrefix; + (NSString *)keyForSEL:(SEL)sel; //+ (BOOL)selectorIsAdvised:(SEL)sel; //+ (NSString *)advisedNameForName:(NSString *)string class:(Class)clazz; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonAssemblySelectorAdviser.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonAssemblySelectorAdviser.h" static NSString *const TYPHOON_BEFORE_ADVICE_SUFFIX = @"__typhoonBeforeAdvice__"; /** * Used to apply an aspect to TyphoonAssembly methods. Before invoking the original target we re-route to a cache to check if there is a * value there. This is used to populate a component factory with definitions containing unique keys, where the unique key is the selector * name on the TyphoonAssembly. */ @implementation TyphoonAssemblySelectorAdviser + (NSString *)prefixForClass:(Class)clazz { return [NSStringFromClass(clazz) stringByAppendingString:TYPHOON_BEFORE_ADVICE_SUFFIX]; } + (SEL)advisedSELForKey:(NSString *)key class:(Class)clazz { if ([key rangeOfString:TYPHOON_BEFORE_ADVICE_SUFFIX].location != NSNotFound) { [NSException raise:NSInternalInconsistencyException format:@"Don't pass an advised key into a method expecting an unadvised key."]; } return NSSelectorFromString([[self prefixForClass:clazz] stringByAppendingString:key]); } + (NSString *)keyForAdvisedSEL:(SEL)advisedSEL { NSString *name = NSStringFromSelector(advisedSEL); NSString *key = name; NSRange suffixRange = [name rangeOfString:TYPHOON_BEFORE_ADVICE_SUFFIX]; if (suffixRange.location != NSNotFound) { key = [name substringFromIndex:NSMaxRange(suffixRange)]; } return key; } + (NSString *)keyForSEL:(SEL)sel { return NSStringFromSelector(sel); } + (SEL)SELForKey:(NSString *)key class:(Class)clazz { return NSSelectorFromString(key); } + (BOOL)selectorIsAdvised:(SEL)sel { NSString *name = NSStringFromSelector(sel); return [name rangeOfString:TYPHOON_BEFORE_ADVICE_SUFFIX].location != NSNotFound; } + (SEL)advisedSELForSEL:(SEL)sel class:(Class)clazz { return [self advisedSELForKey:[self keyForSEL:sel] class:clazz]; } + (NSString *)advisedNameForName:(NSString *)string class:(Class)clazz { return NSStringFromSelector([self advisedSELForKey:string class:clazz]); } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonBlockComponentFactory.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonComponentFactory.h" #import "TyphoonRuntimeArguments.h" @class TyphoonAssembly; /** * @ingroup Assembly * */ @interface TyphoonBlockComponentFactory : TyphoonComponentFactory + (id)factoryWithAssembly:(TyphoonAssembly *)assembly; + (id)factoryWithAssemblies:(NSArray *)assemblies; + (id)factoryForResolvingUIWithAssemblies:(NSArray *)assemblies; - (id)initWithAssembly:(TyphoonAssembly *)assembly; - (id)initWithAssemblies:(NSArray *)assemblies; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonBlockComponentFactory.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonMethod+InstanceBuilder.h" #import "TyphoonBlockComponentFactory.h" #import "TyphoonAssembly.h" #import "OCLogTemplate.h" #import "TyphoonAssembly+TyphoonAssemblyFriend.h" #import "TyphoonAssemblyPropertyInjectionPostProcessor.h" #import "TyphoonDefinition+Internal.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonTypeConverterRegistry.h" #import "TyphoonTypeConverter.h" #import "TyphoonInstancePostProcessor.h" #import "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h" #import "TyphoonPreattachedComponentsRegisterer.h" #import "TyphoonMemoryManagementUtils.h" @interface TyphoonComponentFactory (Private) - (TyphoonDefinition *)definitionForKey:(NSString *)key; - (void)loadIfNeeded; @end @implementation TyphoonBlockComponentFactory //------------------------------------------------------------------------------------------- #pragma mark - Class Methods //------------------------------------------------------------------------------------------- + (id)factoryWithAssembly:(TyphoonAssembly *)assembly { return [[self alloc] initWithAssemblies:@[assembly]]; } + (id)factoryWithAssemblies:(NSArray *)assemblies { return [[self alloc] initWithAssemblies:assemblies]; } + (id)factoryForResolvingUIWithAssemblies:(NSArray *)assemblies { TyphoonBlockComponentFactory *factory = [self newFactoryForResolvingUI]; [factory setupWithAssemblies:assemblies]; return factory; } //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction //------------------------------------------------------------------------------------------- - (id)initWithAssembly:(TyphoonAssembly *)assembly { return [self initWithAssemblies:@[assembly]]; } - (id)initWithAssemblies:(NSArray *)assemblies { self = [super init]; if (self) { [self setupWithAssemblies:assemblies]; } return self; } - (void)setupWithAssemblies:(NSArray *)assemblies { [self attachDefinitionPostProcessor:[TyphoonAssemblyPropertyInjectionPostProcessor new]]; TyphoonPreattachedComponentsRegisterer *preattachedComponentsRegisterer = [[TyphoonPreattachedComponentsRegisterer alloc] initWithComponentFactory:self]; for (TyphoonAssembly *assembly in assemblies) { [preattachedComponentsRegisterer doRegistrationForAssembly:assembly]; [self buildAssembly:assembly]; [assembly activateWithFactory:self collaborators:[NSSet setWithArray:assemblies]]; } [TyphoonMemoryManagementUtils makeFactory:self retainAssemblies:[NSSet setWithArray:assemblies]]; } - (void)buildAssembly:(TyphoonAssembly*)assembly { LogTrace(@"Building assembly: %@", NSStringFromClass([assembly class])); [self assertIsAssembly:assembly]; [assembly prepareForUse]; [self registerAllDefinitions:assembly]; } - (void)assertIsAssembly:(TyphoonAssembly *)assembly { if (![assembly isKindOfClass:[TyphoonAssembly class]]) { [NSException raise:NSInvalidArgumentException format:@"Class '%@' is not a sub-class of %@", NSStringFromClass([assembly class]), NSStringFromClass([TyphoonAssembly class])]; } } - (void)registerAllDefinitions:(TyphoonAssembly *)assembly { NSArray *definitions = [assembly definitions]; for (TyphoonDefinition *definition in definitions) { [self registerDefinition:definition]; } } //------------------------------------------------------------------------------------------- #pragma mark - Overridden Methods //------------------------------------------------------------------------------------------- - (void)forwardInvocation:(NSInvocation *)invocation { @synchronized (self) { NSString *componentKey = NSStringFromSelector([invocation selector]); LogTrace(@"Component key: %@", componentKey); TyphoonRuntimeArguments *args = [TyphoonRuntimeArguments argumentsFromInvocation:invocation]; NSInvocation *internalInvocation = [NSInvocation invocationWithMethodSignature:[self methodSignatureForSelector:@selector(componentForKey:args:)]]; [internalInvocation setSelector:@selector(componentForKey:args:)]; [internalInvocation setArgument:&componentKey atIndex:2]; [internalInvocation setArgument:&args atIndex:3]; [internalInvocation invokeWithTarget:self]; void *returnValue; [internalInvocation getReturnValue:&returnValue]; [invocation setReturnValue:&returnValue]; } } - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { @synchronized (self) { if ([super respondsToSelector:aSelector]) { return [[self class] instanceMethodSignatureForSelector:aSelector]; } NSString *componentKey = NSStringFromSelector(aSelector); TyphoonDefinition *definition = [self definitionForKey:componentKey]; TyphoonAssembly *assembly = definition.assembly; if (assembly) { return [assembly methodSignatureForSelector:aSelector]; } // Last resort. return [TyphoonIntrospectionUtils methodSignatureWithArgumentsAndReturnValueAsObjectsFromSelector:aSelector]; } } - (BOOL)respondsToSelector:(SEL)aSelector { if ([super respondsToSelector:aSelector]){ return YES; } NSString *key = NSStringFromSelector(aSelector); TyphoonDefinition *definition = [self definitionForKey:key]; if (definition) { return YES; } return NO; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCallStack.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonStackElement; @class TyphoonRuntimeArguments; @interface TyphoonCallStack : NSObject + (instancetype)stack; - (void)push:(TyphoonStackElement *)stackElement; - (TyphoonStackElement *)pop; - (TyphoonStackElement *)peekForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args; - (BOOL)isResolvingKey:(NSString *)key withArgs:(TyphoonRuntimeArguments *)args; - (BOOL)isEmpty; - (void)notifyOnceWhenStackEmptyUsingBlock:(void(^)(void))onEmpty; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCallStack.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonCallStack.h" #import "TyphoonStackElement.h" #import "TyphoonRuntimeArguments.h" @implementation TyphoonCallStack { NSMutableArray *_storage; NSMutableArray *_emptyNotificationBlocks; } //------------------------------------------------------------------------------------------- #pragma mark - Class Methods + (instancetype)stack { return [[self alloc] init]; } //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction - (id)init { self = [super init]; if (self) { _storage = [NSMutableArray array]; _emptyNotificationBlocks = [NSMutableArray new]; } return self; } //------------------------------------------------------------------------------------------- #pragma mark - Interface Methods - (void)push:(TyphoonStackElement *)stackElement { #if DEBUG if (![stackElement isKindOfClass:[TyphoonStackElement class]]) { [NSException raise:NSInvalidArgumentException format:@"Not a TyphoonStackItem: %@", stackElement]; } #endif [_storage addObject:stackElement]; } - (TyphoonStackElement *)pop { id element = [_storage lastObject]; if (![self isEmpty]) { [_storage removeLastObject]; } if ([self isEmpty]) { [self callNotificationBlocksAndClear]; } return element; } - (TyphoonStackElement *)peekForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args { NSUInteger argsHash = [args hash]; NSInteger depth = 0; for (TyphoonStackElement *item in [_storage reverseObjectEnumerator]) { if ([item.key isEqualToString:key] && argsHash == [item.args hash]) { // Circular reference to prototype objects is supported, but only for one level of depth // i.e. backward reference is suported, but reference through several levels would be considered // as reference to another prototyped instance if (!item.isPrototypeElement || depth <= 1) { return item; } } depth += 1; } return nil; } - (BOOL)isEmpty { return ([_storage count] == 0); } - (BOOL)isResolvingKey:(NSString *)key withArgs:(TyphoonRuntimeArguments *)args { return [self peekForKey:key args:args] != nil; } - (void)notifyOnceWhenStackEmptyUsingBlock:(void(^)(void))onEmpty { [_emptyNotificationBlocks addObject:onEmpty]; } //------------------------------------------------------------------------------------------- #pragma mark - Private - (void)callNotificationBlocksAndClear { for (void(^notifyBlock)(void) in _emptyNotificationBlocks) { notifyBlock(); } [_emptyNotificationBlocks removeAllObjects]; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCircularDependencyTerminator.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonCircularDependencyTerminator : NSObject @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCircularDependencyTerminator.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonCircularDependencyTerminator.h" @implementation TyphoonCircularDependencyTerminator { } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCollaboratingAssembliesCollector.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonCollaboratingAssembliesCollector : NSObject - (instancetype)initWithAssemblyClass:(Class)assemblyClass; - (NSSet *)collectCollaboratingAssemblies; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCollaboratingAssembliesCollector.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonCollaboratingAssembliesCollector.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonAssembly.h" #import "TyphoonTypeDescriptor.h" @interface TyphoonCollaboratingAssembliesCollector () @property (nonatomic, assign) Class assemblyClass; @end @implementation TyphoonCollaboratingAssembliesCollector //------------------------------------------------------------------------------------------- #pragma mark - Initialization //------------------------------------------------------------------------------------------- - (instancetype)initWithAssemblyClass:(Class)assemblyClass { self = [super init]; if (self) { _assemblyClass = assemblyClass; } return self; } //------------------------------------------------------------------------------------------- #pragma mark - Public Methods //------------------------------------------------------------------------------------------- - (NSSet *)collectCollaboratingAssemblies { NSMutableSet *collaboratorsQueue = [NSMutableSet setWithObject:self.assemblyClass]; NSMutableSet *connectedCollaboratorClasses = [NSMutableSet set]; while (collaboratorsQueue.count > 0) { Class currentClass = [[collaboratorsQueue allObjects] firstObject]; NSSet *collaboratorsToCollect = [self collaboratingAssembliesForClass:currentClass withCollaboratorClasses:[connectedCollaboratorClasses copy]]; for (Class collaboratorClass in collaboratorsToCollect) { if (![connectedCollaboratorClasses containsObject:collaboratorClass]) { [connectedCollaboratorClasses addObject:collaboratorClass]; [collaboratorsQueue addObject:collaboratorClass]; } } [collaboratorsQueue removeObject:currentClass]; } NSMutableSet *collaborators = [NSMutableSet set]; for (Class collaboratorClass in connectedCollaboratorClasses) { TyphoonAssembly *assemblyInstance = [collaboratorClass assembly]; [collaborators addObject:assemblyInstance]; } return [collaborators copy]; } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods //------------------------------------------------------------------------------------------- - (NSSet *)collaboratingAssembliesForClass:(Class)inspectedClass withCollaboratorClasses:(NSSet *)collaboratorClasses { NSMutableSet *collaboratorsForInspectedClass = [NSMutableSet set]; NSSet *properties = [TyphoonIntrospectionUtils propertiesForClass:inspectedClass upToParentClass:[TyphoonAssembly class]]; for (NSString *propertyName in properties) { Class propertyClass = [TyphoonIntrospectionUtils typeForPropertyNamed:propertyName inClass:inspectedClass].typeBeingDescribed; BOOL shouldCollect = [self shouldCollectClass:propertyClass backTo:collaboratorClasses]; if (shouldCollect) { [collaboratorsForInspectedClass addObject:propertyClass]; } } return [collaboratorsForInspectedClass copy]; } - (BOOL)shouldCollectClass:(Class)assemblyClass backTo:(NSSet *)classes { BOOL isTyphoonAssembly = assemblyClass == [TyphoonAssembly class]; BOOL isAlreadyCollected = [classes containsObject:assemblyClass]; BOOL isSubclassOfTyphoonAssembly = [assemblyClass isSubclassOfClass:[TyphoonAssembly class]]; return !isTyphoonAssembly && !isAlreadyCollected && isSubclassOfTyphoonAssembly; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCollaboratingAssemblyPropertyEnumerator.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonAssembly; @interface TyphoonCollaboratingAssemblyPropertyEnumerator : NSObject - (id)initWithAssembly:(TyphoonAssembly *)assembly; - (NSSet *)collaboratingAssemblyProperties; @property(readonly) TyphoonAssembly *assembly; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCollaboratingAssemblyPropertyEnumerator.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonCollaboratingAssemblyPropertyEnumerator.h" #import "TyphoonAssembly.h" #import #import "TyphoonTypeDescriptor.h" #import "TyphoonIntrospectionUtils.h" @implementation TyphoonCollaboratingAssemblyPropertyEnumerator { } - (id)initWithAssembly:(TyphoonAssembly *)assembly { self = [super init]; if (self) { _assembly = assembly; } return self; } - (NSSet *)collaboratingAssemblyProperties { NSMutableSet *collaboratingAssemblyProperties = [[NSMutableSet alloc] init]; NSSet *properties = [TyphoonIntrospectionUtils propertiesForClass:[_assembly class] upToParentClass:[TyphoonAssembly class]]; for (NSString *property in properties) { if ([self propertyForName:property isCollaboratingAssemblyPropertyOnClass:[_assembly class]]) { [collaboratingAssemblyProperties addObject:property]; } } return collaboratingAssemblyProperties; } - (BOOL)propertyForName:(NSString *)propertyName isCollaboratingAssemblyPropertyOnClass:(Class)class { TyphoonTypeDescriptor *type = [TyphoonIntrospectionUtils typeForPropertyNamed:propertyName inClass:class]; return [type.typeBeingDescribed isSubclassOfClass:[TyphoonAssembly class]]; } - (BOOL)classNotRootAssemblyClass:(Class)class { return class != [TyphoonAssembly class]; } - (id)propertyNameForProperty:(objc_property_t)aProperty { const char *cPropertyName = property_getName(aProperty); return [NSString stringWithCString:cPropertyName encoding:NSUTF8StringEncoding]; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCollaboratingAssemblyProxy.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonDefinition; /** * @ingroup Assembly * * This class allows using the interface from one assembly within another. This is useful for abstracting out environment * dependent components. For example you could say "This class X needs to be injected with something conforming to the * Foo protocol. This is a RealFoo, this is a TestFoo. When I'm running X in real life, I want it to get a RealFoo, but * when I'm running my integration tests, I want it to get a TestFoo." */ @interface TyphoonCollaboratingAssemblyProxy : NSObject + (id)proxy; - (instancetype)accessor; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonCollaboratingAssemblyProxy.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonCollaboratingAssemblyProxy.h" #import "TyphoonDefinition+Internal.h" #import "TyphoonAssemblySelectorAdviser.h" #import "TyphoonReferenceDefinition.h" #import "TyphoonObjectWithCustomInjection.h" #import "TyphoonInjectionByComponentFactory.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonIntrospectionUtils.h" #import @interface TyphoonCollaboratingAssemblyProxy () @end @implementation TyphoonCollaboratingAssemblyProxy + (id)proxy { static dispatch_once_t onceToken; static TyphoonCollaboratingAssemblyProxy *instance; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } - (instancetype)accessor { return self; } - (void)forwardInvocation:(NSInvocation *)anInvocation { TyphoonRuntimeArguments *args = [TyphoonRuntimeArguments argumentsFromInvocation:anInvocation]; //Since we're resolving a reference to another component, all we need to provide here is the definition's key and runtime args. TyphoonDefinition *definition = [TyphoonReferenceDefinition definitionReferringToComponent:[TyphoonAssemblySelectorAdviser keyForSEL:anInvocation.selector]]; definition.currentRuntimeArguments = args; [anInvocation retainArguments]; [anInvocation setReturnValue:&definition]; } - (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { return [TyphoonIntrospectionUtils methodSignatureWithArgumentsAndReturnValueAsObjectsFromSelector:selector]; } //------------------------------------------------------------------------------------------- #pragma mark - - (id )typhoonCustomObjectInjection { return [[TyphoonInjectionByComponentFactory alloc] init]; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonComponentFactory+InstanceBuilder.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonComponentFactory.h" #import "TyphoonRuntimeArguments.h" @class TyphoonCallStack; @class TyphoonDefinition; /** * Encapsulates the methods related to assembling an instance using the Objective-C runtime. This is an internal category - the methods will * not be required for normal use of Typhoon. */ @interface TyphoonComponentFactory (InstanceBuilder) - (TyphoonCallStack *)stack; - (id)buildInstanceWithDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args; - (id)buildSharedInstanceForDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args; - (void)doInjectionEventsOn:(id)instance withDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args; - (NSArray *)allDefinitionsForType:(id)classOrProtocol; - (NSArray *)allDefinitionsForType:(id)classOrProtocol includeSubclasses:(BOOL)includeSubclasses; - (TyphoonDefinition *)definitionForType:(id)classOrProtocol; - (TyphoonDefinition *)definitionForType:(id)classOrProtocol orNil:(BOOL)returnNilIfNotFound includeSubclasses:(BOOL)includeSubclasses; - (void)injectAssemblyOnInstanceIfTyphoonAware:(id)instance; - (void)resolveCircularDependency:(NSString *)key args:(TyphoonRuntimeArguments *)args resolvedBlock:(void(^)(BOOL isCircular))resolvedBlock; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonComponentFactory+InstanceBuilder.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import #import "TyphoonLinkerCategoryBugFix.h" #import "TyphoonInjectionContext.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonDefinition+InstanceBuilder.h" TYPHOON_LINK_CATEGORY(TyphoonComponentFactory_InstanceBuilder) #import "TyphoonCallStack.h" #import "TyphoonMethod+InstanceBuilder.h" #import "TyphoonStackElement.h" #import "NSObject+PropertyInjection.h" #import "NSInvocation+TCFInstanceBuilder.h" #import "TyphoonPropertyInjection.h" #import "NSObject+TyphoonIntrospectionUtils.h" #import "TyphoonFactoryAutoInjectionPostProcessor.h" @implementation TyphoonComponentFactory (InstanceBuilder) - (TyphoonCallStack *)stack { return _stack; } - (id)buildInstanceWithDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args { BOOL isPrototype = (definition.scope == TyphoonScopePrototype); TyphoonStackElement *stackElement = [TyphoonStackElement elementWithKey:definition.key args:args isPrototype:isPrototype]; [_stack push:stackElement]; id instance = [self initializeInstanceWithDefinition:definition args:args]; [stackElement takeInstance:instance]; [self doInjectionEventsOn:instance withDefinition:definition args:args]; instance = [self postProcessInstance:instance]; [_stack pop]; return instance; } - (id)initializeInstanceWithDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args { return [definition initializeInstanceWithArgs:args factory:self]; } - (void)doInjectionEventsOn:(id)instance withDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args { if ([instance respondsToSelector:@selector(typhoonWillInject)]) { [instance typhoonWillInject]; } [definition doInjectionEventsOn:instance withArgs:args factory:self]; [_stack notifyOnceWhenStackEmptyUsingBlock:^{ [definition doAfterAllInjectionsOn:instance]; [self injectAssemblyOnInstanceIfTyphoonAware:instance]; if ([instance respondsToSelector:@selector(typhoonDidInject)]) { [instance typhoonDidInject]; } }]; } - (id)postProcessInstance:(id)instance { if (![instance conformsToProtocol:@protocol(TyphoonInstancePostProcessor)]) { for (id postProcessor in _instancePostProcessors) { instance = [postProcessor postProcessInstance:instance]; } } return instance; } - (void)injectAssemblyOnInstanceIfTyphoonAware:(id)instance { if ([instance respondsToSelector:@selector(typhoonSetFactory:)]) { [(NSObject *)instance typhoonSetFactory:self]; } } - (id)buildSharedInstanceForDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args { id instance = [_stack peekForKey:definition.key args:args].instance; if (instance) { return instance; } return [self buildInstanceWithDefinition:definition args:args]; } //------------------------------------------------------------------------------------------- #pragma mark - Circular dependencies support //------------------------------------------------------------------------------------------- - (void)resolveCircularDependency:(NSString *)key args:(TyphoonRuntimeArguments *)args resolvedBlock:(void (^)(BOOL isCircular))resolvedBlock { TyphoonStackElement *element = [_stack peekForKey:key args:args]; if (element) { [element addInstanceCompleteBlock:^(id instance) { resolvedBlock(YES); }]; } else { resolvedBlock(NO); } } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods //------------------------------------------------------------------------------------------- - (TyphoonDefinition *)definitionForType:(id)classOrProtocol { return [self definitionForType:classOrProtocol orNil:NO includeSubclasses:YES]; } - (TyphoonDefinition *)definitionForType:(id)classOrProtocol orNil:(BOOL)returnNilIfNotFound includeSubclasses:(BOOL)includeSubclasses { NSArray *candidates = [self allDefinitionsForType:classOrProtocol includeSubclasses:includeSubclasses]; if ([candidates count] == 0) { //Auto registering definition with AutoInjection if (IsClass(classOrProtocol)) { TyphoonDefinition *autoDefinition = [self autoInjectionDefinitionForClass:classOrProtocol]; if (autoDefinition) { [self registerDefinition:autoDefinition]; return [self definitionForType:classOrProtocol orNil:returnNilIfNotFound includeSubclasses:includeSubclasses]; } } if (returnNilIfNotFound) { return nil; } else { [NSException raise:NSInvalidArgumentException format:@"No components defined which satisify type: '%@'", TyphoonTypeStringFor(classOrProtocol)]; } } if ([candidates count] > 1) { [NSException raise:NSInvalidArgumentException format:@"More than one component is defined satisfying type: '%@' : %@", TyphoonTypeStringFor(classOrProtocol), candidates]; } return [candidates firstObject]; } - (NSArray *)allDefinitionsForType:(id)classOrProtocol { return [self allDefinitionsForType:classOrProtocol includeSubclasses:YES]; } - (NSArray *)allDefinitionsForType:(id)classOrProtocol includeSubclasses:(BOOL)includeSubclasses { if (!IsClass(classOrProtocol) && !IsProtocol(classOrProtocol)) { [NSException raise:NSInternalInconsistencyException format:@"%@ is not class or protocol", classOrProtocol]; } NSMutableArray *results = [[NSMutableArray alloc] init]; for (TyphoonDefinition *definition in _registry) { if (IsClass(classOrProtocol) && [definition isCandidateForInjectedClass:classOrProtocol includeSubclasses:includeSubclasses]) { [results addObject:definition]; } else if (IsProtocol(classOrProtocol) && [definition isCandidateForInjectedProtocol:classOrProtocol]) { [results addObject:definition]; } } return results; } - (TyphoonDefinition *)autoInjectionDefinitionForClass:(Class)clazz { TyphoonDefinition *result = nil; TyphoonFactoryAutoInjectionPostProcessor *postProcessor = [self autoInjectionPostProcessor]; NSArray *properties = [postProcessor autoInjectedPropertiesForClass:clazz]; if (properties) { result = [TyphoonDefinition withClass:clazz]; for (id propertyInjection in properties) { [result addInjectedPropertyIfNotExists:propertyInjection]; } [result applyGlobalNamespace]; } return result; } - (TyphoonFactoryAutoInjectionPostProcessor *)autoInjectionPostProcessor { TyphoonFactoryAutoInjectionPostProcessor *postProcessor = nil; for (id item in _definitionPostProcessors) { if ([item isMemberOfClass:[TyphoonFactoryAutoInjectionPostProcessor class]]) { postProcessor = item; break; } } return postProcessor; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonComponentFactory+TyphoonDefinitionRegisterer.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonComponentFactory.h" #import "TyphoonRuntimeArguments.h" @protocol TyphoonInstancePostProcessor; @interface TyphoonComponentFactory (TyphoonDefinitionRegisterer) - (TyphoonDefinition *)definitionForKey:(NSString *)key; - (id)newOrScopeCachedInstanceForDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args; - (void)addDefinitionToRegistry:(TyphoonDefinition *)definition; - (void)addInstancePostProcessor:(id )postProcessor DEPRECATED_MSG_ATTRIBUTE("use attachInstancePostProcessor instead"); @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonFactoryPropertyInjectionPostProcessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinitionPostProcessor.h" #import "TyphoonInjectionByType.h" #import "TyphoonDefinition.h" /** * Replaces property injections by-type with injectuins by-componentFactory when property type subclass of TyphoonComponentFactory */ @interface TyphoonFactoryPropertyInjectionPostProcessor : NSObject /* Method to override in subclasses */ - (BOOL)shouldReplaceInjectionByType:(TyphoonInjectionByType *)propertyInjection withFactoryInjectionInDefinition:(TyphoonDefinition *)definition; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonFactoryPropertyInjectionPostProcessor.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonFactoryPropertyInjectionPostProcessor.h" #import "TyphoonComponentFactory.h" #import "TyphoonDefinition.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonInjectionByComponentFactory.h" #import "TyphoonInjectionByType.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonAssembly.h" @implementation TyphoonFactoryPropertyInjectionPostProcessor //------------------------------------------------------------------------------------------- #pragma mark - Protocol Methods - (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory { [definition enumerateInjectionsOfKind:[TyphoonInjectionByType class] options:TyphoonInjectionsEnumerationOptionProperties usingBlock:^(TyphoonInjectionByType *typeInjection, id *injectionToReplace, BOOL *stop) { if ([self shouldReplaceInjectionByType:typeInjection withFactoryInjectionInDefinition:definition]) { *injectionToReplace = [self factoryInjectionToReplacePropertyInjection:typeInjection]; } }]; } //------------------------------------------------------------------------------------------- #pragma mark - Instance Methods - (BOOL)shouldReplaceInjectionByType:(TyphoonInjectionByType *)propertyInjection withFactoryInjectionInDefinition:(TyphoonDefinition *)definition { BOOL isFactoryClass = NO; TyphoonTypeDescriptor *type = [TyphoonIntrospectionUtils typeForPropertyNamed:propertyInjection.propertyName inClass:definition.type]; if (type.typeBeingDescribed) { isFactoryClass = [type.typeBeingDescribed isSubclassOfClass:[TyphoonComponentFactory class]]; } return isFactoryClass; } - (TyphoonInjectionByComponentFactory *)factoryInjectionToReplacePropertyInjection:(id)propertyInjection { TyphoonInjectionByComponentFactory *newInjection = [[TyphoonInjectionByComponentFactory alloc] init]; [newInjection setPropertyName:[propertyInjection propertyName]]; return newInjection; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonMemoryManagementUtils.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonComponentFactory; @interface TyphoonMemoryManagementUtils : NSObject + (void)makeFactory:(TyphoonComponentFactory *)factory retainAssemblies:(NSSet *)assemblies; + (void)makeAssemblies:(NSSet *)assemblies retainFactory:(TyphoonComponentFactory *)factory; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonMemoryManagementUtils.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonMemoryManagementUtils.h" #import @class TyphoonComponentFactory; @class TyphoonAssembly; static const char *kFactoryAssembliesKey; static const char *kAssemblyFactoryKey; @implementation TyphoonMemoryManagementUtils + (void)makeFactory:(TyphoonComponentFactory *)factory retainAssemblies:(NSSet *)assemblies { objc_setAssociatedObject(factory, &kFactoryAssembliesKey, assemblies, OBJC_ASSOCIATION_RETAIN_NONATOMIC); for (TyphoonAssembly *assembly in assemblies) { objc_setAssociatedObject(assembly, &kAssemblyFactoryKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } + (void)makeAssemblies:(NSSet *)assemblies retainFactory:(TyphoonComponentFactory *)factory { objc_setAssociatedObject(factory, &kFactoryAssembliesKey, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); for (TyphoonAssembly *assembly in assemblies) { objc_setAssociatedObject(assembly, &kAssemblyFactoryKey, factory, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonParentReferenceHydratingPostProcessor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinitionPostProcessor.h" /** * Sets the full-definition for any parent definitions that have been provided by key-only, thus allowing the definition to inherit * the parent (and ancestor) initializer and/or properties. */ @interface TyphoonParentReferenceHydratingPostProcessor : NSObject @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonParentReferenceHydratingPostProcessor.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonParentReferenceHydratingPostProcessor.h" #import "TyphoonComponentFactory.h" #import "TyphoonDefinition.h" #import "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h" #import "TyphoonDefinition+Infrastructure.h" @implementation TyphoonParentReferenceHydratingPostProcessor - (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory { if (definition.parent) { TyphoonDefinition *parentDefinition = [factory definitionForKey:[(TyphoonDefinition *)definition.parent key]]; [definition setParent:parentDefinition]; } } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonRuntimeArguments.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonRuntimeArguments : NSObject + (instancetype)argumentsFromInvocation:(NSInvocation *)invocation; - (id)argumentValueAtIndex:(NSUInteger)index; - (NSUInteger)indexOfArgumentWithKind:(Class)clazz; - (NSUInteger)count; - (void)enumerateArgumentsUsingBlock:(void(^)(id argument, NSUInteger index, BOOL *stop))block; /** RuntimeArguments - arguments passed by user at runtime * ReferenceArguments - arguments specified in the assembly class * * for example we have definition in the assembly * * - (Person *)personWithFirstName:(NSString *)firstName lastName:(NSString *)lastName * { * return [TyphoonDefinition withClass:[Person class] configuration:^(TyphoonDefinition *definition) { * [definition injectProperty:@selector(firstName) with:firstName]; * [definition injectProperty:@selector(lastName) with:lastName]; * }]; * } * Reference argument here is : * 0 - TyphoonInjectionByRuntimeArgument at index 0 * 1 - TyphoonInjectionByRuntimeArgument at index 1 * * Runtime argument is: * 0 - (NSString *) John * 1 - (NSString *) Smith * * This method return ReferenceArguments, but with replaced TyphoonInjectionByRuntimeArgument with runtime values * 0 - TyphoonInjectionByRuntimeArgument at index 0 will be replaced by (NSString *)John * 1 - TyphoonInjectionByRuntimeArgument at index 1 will be replaced by (NSString *)Smith * */ + (TyphoonRuntimeArguments *)argumentsFromRuntimeArguments:(TyphoonRuntimeArguments *)runtimeArguments appliedToReferenceArguments:(TyphoonRuntimeArguments *)referenceArguments; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonRuntimeArguments.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonRuntimeArguments.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonInjectionByRuntimeArgument.h" #import "TyphoonInjections.h" #import "TyphoonInjectionByReference.h" #import "NSInvocation+TCFWrapValues.h" #import "TyphoonUtils.h" @implementation TyphoonRuntimeArguments { NSMutableArray *_arguments; NSUInteger _hash; BOOL _needRehash; } + (instancetype)argumentsFromInvocation:(NSInvocation *)invocation { NSUInteger count = [[invocation methodSignature] numberOfArguments]; if (count <= 2) { return nil; } NSMutableArray *args = [[NSMutableArray alloc] initWithCapacity:count]; for (NSInteger i = 2; i < (NSInteger)count; i++) { id argument = [invocation typhoon_getArgumentObjectAtIndex:i]; idinjection = TyphoonMakeInjectionFromObjectIfNeeded(argument); [self validateRuntimeArgumentWithInjection:injection]; [args addObject:injection]; } return [[self alloc] initWithArguments:args]; } + (void)validateRuntimeArgumentWithInjection:(id)injection { if ([injection isKindOfClass:[TyphoonInjectionByReference class]]) { TyphoonInjectionByReference *referenceInjection = injection; [referenceInjection.referenceArguments enumerateArgumentsUsingBlock:^(id argument, NSUInteger index, BOOL *stop) { if ([argument isKindOfClass:[TyphoonInjectionByRuntimeArgument class]]) { [NSException raise:NSInternalInconsistencyException format:@"Congratulations you've tried to do something über-funky with Typhoon %%). You are the 3rd person EVER to receive this error message. Returning a definition that is the result of a nested runtime argument is not supported. Instead unroll the definition."]; } else { [self validateRuntimeArgumentWithInjection:argument]; } }]; } } - (id)initWithArguments:(NSMutableArray *)array { self = [super init]; if (self) { _arguments = array; _needRehash = YES; } return self; } - (id)argumentValueAtIndex:(NSUInteger)index { return _arguments[index]; } - (void)enumerateArgumentsUsingBlock:(void(^)(id argument, NSUInteger index, BOOL *stop))block { if (!block) { return; } NSUInteger count = [_arguments count]; for (NSUInteger i = 0; i < count; ++i) { id argument = [self argumentValueAtIndex:i]; BOOL stop = NO; block(argument, i, &stop); if (stop) { break; } } } - (void)replaceArgumentAtIndex:(NSUInteger)index withArgument:(id)argument { argument = TyphoonMakeInjectionFromObjectIfNeeded(argument); [_arguments replaceObjectAtIndex:index withObject:argument]; _needRehash = YES; } + (TyphoonRuntimeArguments *)argumentsFromRuntimeArguments:(TyphoonRuntimeArguments *)runtimeArguments appliedToReferenceArguments:(TyphoonRuntimeArguments *)referenceArguments { TyphoonRuntimeArguments *result = referenceArguments; Class runtimeArgInjectionClass = [TyphoonInjectionByRuntimeArgument class]; BOOL hasRuntimeArgumentReferences = [referenceArguments indexOfArgumentWithKind:runtimeArgInjectionClass] != NSNotFound; if (referenceArguments && runtimeArguments && hasRuntimeArgumentReferences) { result = [referenceArguments copy]; NSUInteger indexToReplace; while ((indexToReplace = [result indexOfArgumentWithKind:runtimeArgInjectionClass]) != NSNotFound) { TyphoonInjectionByRuntimeArgument *runtimeArgPlaceholder = [result argumentValueAtIndex:indexToReplace]; id runtimeValue = [runtimeArguments argumentValueAtIndex:runtimeArgPlaceholder.runtimeArgumentIndex]; [result replaceArgumentAtIndex:indexToReplace withArgument:runtimeValue]; } } return result; } - (NSUInteger)indexOfArgumentWithKind:(Class)clazz { return [_arguments indexOfObjectPassingTest:^BOOL(id obj, NSUInteger idx, BOOL *stop) { BOOL found = NO; if ([obj isKindOfClass:clazz]) { *stop = YES; found = YES; } return found; }]; } - (NSUInteger)count { return [_arguments count]; } - (id)copyWithZone:(NSZone *)zone { return [[TyphoonRuntimeArguments alloc] initWithArguments:[_arguments mutableCopy]]; } - (NSUInteger)hash { if (_needRehash) { _hash = [self calculateHash]; _needRehash = NO; } return _hash; } - (NSUInteger)calculateHash { NSUInteger hash = 0; for (id arg in _arguments) { hash = TyphoonHashByAppendingInteger(hash, [arg hash]); } return hash; } - (NSString *)description { NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: ", NSStringFromClass([self class])]; [description appendFormat:@"_arguments=%@", _arguments]; [description appendString:@">"]; return description; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonStackElement.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonDefinition; @class TyphoonRuntimeArguments; typedef void(^TyphoonInstanceCompleteBlock)(id instance); @interface TyphoonStackElement : NSObject @property(nonatomic, strong, readonly) NSString *key; @property(nonatomic, strong, readonly) TyphoonRuntimeArguments *args; @property(nonatomic, strong, readonly) id instance; @property(nonatomic, readonly) BOOL isPrototypeElement; /* Raises a circular init exception if instance in initializing state. */ + (instancetype)elementWithKey:(NSString *)key args:(TyphoonRuntimeArguments *)args isPrototype:(BOOL)isPrototype; - (NSString *)description; - (BOOL)isInitializingInstance; - (void)addInstanceCompleteBlock:(TyphoonInstanceCompleteBlock)completeBlock; - (void)takeInstance:(id)instance; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Internal/TyphoonStackElement.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonStackElement.h" @implementation TyphoonStackElement { NSMutableSet *completeBlocks; id _instance; } + (instancetype)elementWithKey:(NSString *)key args:(TyphoonRuntimeArguments *)args isPrototype:(BOOL)isPrototype { return [[self alloc] initWithKey:key args:args isPrototype:isPrototype]; } - (instancetype)initWithKey:(NSString *)key args:(TyphoonRuntimeArguments *)args isPrototype:(BOOL)isPrototype { self = [super init]; if (self) { _key = key; _args = args; _isPrototypeElement = isPrototype; completeBlocks = [NSMutableSet new]; } return self; } - (BOOL)isInitializingInstance { return _instance == nil; } - (id)instance { if ([self isInitializingInstance]) { [NSException raise:@"CircularInitializerDependence" format:@"The object for key %@ is currently initializing, but was specified as init dependency in another object. " "To inject a circular dependency, use a property setter or method injection instead.", self.key]; } return _instance; } - (void)addInstanceCompleteBlock:(TyphoonInstanceCompleteBlock)completeBlock { NSParameterAssert(completeBlock); if ([self isInitializingInstance]) { [completeBlocks addObject:completeBlock]; } else { completeBlock(_instance); } } - (void)takeInstance:(id)instance { _instance = instance; for (TyphoonInstanceCompleteBlock completeBlock in completeBlocks) { completeBlock(instance); } [completeBlocks removeAllObjects]; } - (NSString *)description { NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: ", NSStringFromClass([self class])]; [description appendFormat:@"self.key=%@", self.key]; [description appendFormat:@", completeBlocksCount=%lu", (unsigned long) [completeBlocks count]]; [description appendString:@">"]; return description; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/Pool/TyphoonComponentsPool.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** Methods from NSMutableDictionary which used in componentsPool. Created to maintain custom TyphoonWeakComponentsPool */ @protocol TyphoonComponentsPool - (void)setObject:(id)object forKey:(id )aKey; - (id)objectForKey:(id )aKey; - (id)objectForKeyedSubscript:(id )aKey; - (void)setObject:(id)object forKeyedSubscript:(id )aKey; - (NSArray *)allValues; - (void)removeAllObjects; @end ================================================ FILE: Pods/Typhoon/Source/Factory/Pool/TyphoonWeakComponentsPool.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonComponentsPool.h" @interface TyphoonWeakComponentsPool : NSObject @end ================================================ FILE: Pods/Typhoon/Source/Factory/Pool/TyphoonWeakComponentsPool.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonWeakComponentsPool.h" #import "NSObject+DeallocNotification.h" @implementation TyphoonWeakComponentsPool { NSMutableDictionary *dictionaryWithNonRetainedObjects; } - (id)init { self = [super init]; if (self) { CFDictionaryValueCallBacks callbacks = {0, NULL, NULL, NULL, NULL}; dictionaryWithNonRetainedObjects = (__bridge_transfer id) CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &callbacks); } return self; } - (void)setObject:(id)object forKey:(id )aKey { __weak __typeof (dictionaryWithNonRetainedObjects) weakDict = dictionaryWithNonRetainedObjects; [object setDeallocNotificationInBlock:^{ [weakDict removeObjectForKey:aKey]; }]; [dictionaryWithNonRetainedObjects setObject:object forKey:aKey]; } - (id)objectForKey:(id )aKey { return [dictionaryWithNonRetainedObjects objectForKey:aKey]; } - (id)objectForKeyedSubscript:(id )aKey { return [self objectForKey:aKey]; } - (void)setObject:(id)object forKeyedSubscript:(id )aKey { return [self setObject:object forKey:aKey]; } - (NSArray *)allValues { return [dictionaryWithNonRetainedObjects allValues]; } - (void)removeAllObjects { [dictionaryWithNonRetainedObjects removeAllObjects]; } @end ================================================ FILE: Pods/Typhoon/Source/Factory/TyphoonComponentFactory.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinitionPostProcessor.h" #import "TyphoonInstancePostProcessor.h" #import "TyphoonTypeConverter.h" #import "TyphoonComponentsPool.h" @class TyphoonDefinition; @class TyphoonCallStack; @class TyphoonRuntimeArguments; @class TyphoonTypeConverterRegistry; /** * * @ingroup Assembly * * Defines a protocol for resolving built instances, injecting a pre-obtained instance using a factory containing * definitions from one or more TyphoonAssembly classes. */ @protocol TyphoonComponentFactory /** * Returns an an instance of the component matching the supplied class or protocol. For example: @code [factory objectForType:[Knight class]]; [factory objectForType:@protocol(Quest)]; @endcode * * @exception NSInvalidArgumentException When no singletons or prototypes match the requested type. * @exception NSInvalidArgumentException When when more than one singleton or prototype matches the requested type. * * @warning componentForType with a protocol argument is not currently supported in Objective-C++. * * @see: allComponentsForType: */ - (id)componentForType:(id)classOrProtocol; /** * Returns an array objects matching the given type. * * @see componentForType */ - (NSArray *)allComponentsForType:(id)classOrProtocol; /** * Returns the component matching the given key. For the block-style, this is the name of the method on the * TyphoonAssembly interface, although, for block-style you'd typically use the assembly interface itself * for component resolution. */ - (id)componentForKey:(NSString *)key; - (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args; /** * You can query the factory by using object subscription syntax: _factory[@"myObject"] or even _factory[[MyObject class]] */ - (id)objectForKeyedSubscript:(id)key; /** * Injects the properties and methods of an object */ - (void)inject:(id)instance; /** * Injects the properties and methods of an object, described in definition */ - (void)inject:(id)instance withSelector:(SEL)selector; - (void)makeDefault; /** Attach a TyphoonDefinitionPostProcessor to this component factory. @param postProcessor The definition post processor to attach. */ - (void)attachDefinitionPostProcessor:(id)postProcessor NS_SWIFT_NAME(attachDefinitionPostProcessor(postProcessor:)); /** Attach a TyphoonInstancePostProcessor to this component factory. @param postProcessor The instance post processor to attach. */ - (void)attachInstancePostProcessor:(id)postProcessor NS_SWIFT_NAME(attachInstancePostProcessor(postProcessor:)); /** Attach a TyphoonTypeConverter to this component factory. @param typeConverter The type converter to attach. */ - (void)attachTypeConverter:(id)typeConverter; @end /** * * @ingroup Assembly * * This is the base class for all component factories. It defines methods for retrieving components from the factory, as well as a low-level * API for assembling components from their constituent parts. This low-level API could be used as-is, however its intended to use a higher * level abstraction such as TyphoonBlockComponentFactory. */ @interface TyphoonComponentFactory : NSObject { NSMutableArray *_registry; id _singletons; id _objectGraphSharedInstances; id _weakSingletons; TyphoonCallStack *_stack; TyphoonTypeConverterRegistry *_typeConverterRegistry; NSMutableArray *_definitionPostProcessors; NSMutableArray *_instancePostProcessors; BOOL _isLoading; } /** * The instantiated singletons. */ @property(nonatomic, strong, readonly) NSArray *singletons; /** * Say if the factory has been loaded. */ @property(nonatomic, assign, getter = isLoaded) BOOL loaded; /** * The attached factory post processors. */ @property(nonatomic, strong, readonly) NSArray *definitionPostProcessors; /** * The attached component post processors. */ @property(nonatomic, strong, readonly) NSArray *instancePostProcessors; /** * Returns the default component factory, if one has been set. @see [TyphoonComponentFactory makeDefault]. This allows resolving components * from the Typhoon another class after the container has been set up. * * A more desirable approach, if possible - especially for a component that is also registered with the container is to use * TyphoonComponentFactoryAware, which injects the component factory as a dependency on the class that needs it. This latter approach * simplifies unit testing, in that no special approach to patching out the classes collaborators is required. * * @see [TyphoonComponentFactory makeDefault]. * @see TyphoonComponentFactoryAware * */ + (id)defaultFactory; + (instancetype)newFactoryForResolvingUI; + (void)setFactoryForResolvingUI:(TyphoonComponentFactory *)factory; /** Factory used to resolve definition for UI. */ + (TyphoonComponentFactory *)factoryForResolvingUI; /** Factory used to resolve definition from TyphoonLoadedView. */ + (TyphoonComponentFactory *)factoryForResolvingFromXibs DEPRECATED_MSG_ATTRIBUTE("use factoryForResolvingUI instead"); /** * Mutate the component definitions and * build the not-lazy singletons. */ - (void)load; /** * Dump all the singletons. */ - (void)unload; /** * Registers a component into the factory. Components can be declared in any order, the container will work out how to resolve them. */ - (void)registerDefinition:(TyphoonDefinition *)definition; - (NSArray *)registry; - (TyphoonTypeConverterRegistry *)typeConverterRegistry; - (void)enumerateDefinitions:(void(^)(TyphoonDefinition *definition, NSUInteger index, TyphoonDefinition **definitionToReplace, BOOL *stop))block; @end ================================================ FILE: Pods/Typhoon/Source/Factory/TyphoonComponentFactory.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonComponentFactory.h" #import "TyphoonDefinition.h" #import "TyphoonComponentFactory+InstanceBuilder.h" #import "OCLogTemplate.h" #import "TyphoonDefinitionRegisterer.h" #import "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h" #import "TyphoonOrdered.h" #import "TyphoonCallStack.h" #import "TyphoonParentReferenceHydratingPostProcessor.h" #import "TyphoonFactoryPropertyInjectionPostProcessor.h" #import "TyphoonInstancePostProcessor.h" #import "TyphoonWeakComponentsPool.h" #import "TyphoonFactoryAutoInjectionPostProcessor.h" #import "TyphoonStackElement.h" #import "TyphoonTypeConverterRegistry.h" @interface TyphoonDefinition (TyphoonComponentFactory) @property (nonatomic, strong) NSString *key; @end @implementation TyphoonComponentFactory static TyphoonComponentFactory *defaultFactory; static TyphoonComponentFactory *uiResolvingFactory = nil; //------------------------------------------------------------------------------------------- #pragma mark - Class Methods //------------------------------------------------------------------------------------------- + (id)defaultFactory { return defaultFactory; } + (instancetype)newFactoryForResolvingUI { return [[self alloc] initFactoryForResolvingUI]; } + (void)setFactoryForResolvingUI:(TyphoonComponentFactory *)factory { uiResolvingFactory = factory; } + (TyphoonComponentFactory *)factoryForResolvingUI { return uiResolvingFactory; } + (TyphoonComponentFactory *)factoryForResolvingFromXibs { return uiResolvingFactory; } //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction //------------------------------------------------------------------------------------------- - (id)initFactoryForResolvingUI { uiResolvingFactory = self; return [self init]; } - (id)init { self = [super init]; if (self) { _registry = [[NSMutableArray alloc] init]; _singletons = (id)[[NSMutableDictionary alloc] init]; _weakSingletons = [TyphoonWeakComponentsPool new]; _objectGraphSharedInstances = (id)[[NSMutableDictionary alloc] init]; _stack = [TyphoonCallStack stack]; _typeConverterRegistry = [[TyphoonTypeConverterRegistry alloc] init]; _definitionPostProcessors = [[NSMutableArray alloc] init]; _instancePostProcessors = [[NSMutableArray alloc] init]; [self attachDefinitionPostProcessor:[TyphoonParentReferenceHydratingPostProcessor new]]; [self attachAutoInjectionPostProcessorIfNeeded]; [self attachDefinitionPostProcessor:[TyphoonFactoryPropertyInjectionPostProcessor new]]; } return self; } - (void)attachAutoInjectionPostProcessorIfNeeded { NSDictionary *bundleInfoDictionary = [[NSBundle mainBundle] infoDictionary]; NSNumber *value = bundleInfoDictionary[@"TyphoonAutoInjectionEnabled"]; if (!value || [value boolValue]) { [self attachDefinitionPostProcessor:[TyphoonFactoryAutoInjectionPostProcessor new]]; } } //------------------------------------------------------------------------------------------- #pragma mark - Interface Methods //------------------------------------------------------------------------------------------- - (NSArray *)singletons { return [[_singletons allValues] copy]; } - (void)load { @synchronized (self) { if (!_isLoading && ![self isLoaded]) { // ensure that the method won't be call recursively. _isLoading = YES; [self _load]; _isLoading = NO; [self setLoaded:YES]; } } } - (void)unload { @synchronized (self) { if ([self isLoaded]) { NSAssert([_stack isEmpty], @"Stack should be empty when unloading factory. Please finish all object creation before factory unloading"); [_singletons removeAllObjects]; [_weakSingletons removeAllObjects]; [_objectGraphSharedInstances removeAllObjects]; [self setLoaded:NO]; } } } - (void)registerDefinition:(TyphoonDefinition *)definition { if (_isLoading || [self isLoaded]) { definition = [self definitionAfterApplyingPostProcessorsToDefinition:definition]; } TyphoonDefinitionRegisterer *registerer = [[TyphoonDefinitionRegisterer alloc] initWithDefinition:definition componentFactory:self]; [registerer doRegistration]; if ([self isLoaded]) { [self instantiateIfEagerSingleton:definition]; } } - (id)objectForKeyedSubscript:(id)key { if ([key isKindOfClass:[NSString class]]) { return [self componentForKey:key]; } return [self componentForType:key]; } - (id)componentForType:(id)classOrProtocol { [self loadIfNeeded]; return [self newOrScopeCachedInstanceForDefinition:[self definitionForType:classOrProtocol] args:nil]; } - (NSArray *)allComponentsForType:(id)classOrProtocol { [self loadIfNeeded]; NSMutableArray *results = [[NSMutableArray alloc] init]; NSArray *definitions = [self allDefinitionsForType:classOrProtocol]; for (TyphoonDefinition *definition in definitions) { [results addObject:[self newOrScopeCachedInstanceForDefinition:definition args:nil]]; } return [results copy]; } - (id)componentForKey:(NSString *)key { return [self componentForKey:key args:nil]; } - (id)componentForKey:(NSString *)key args:(TyphoonRuntimeArguments *)args { if (!key) { return nil; } [self loadIfNeeded]; TyphoonDefinition *definition = [self definitionForKey:key]; if (!definition) { [NSException raise:NSInvalidArgumentException format:@"No component matching id '%@'.", key]; } return [self newOrScopeCachedInstanceForDefinition:definition args:args]; } - (void)loadIfNeeded { if (![self isLoaded]) { [self load]; } } - (void)makeDefault { @synchronized (self) { if (defaultFactory) { LogInfo(@"*** Warning *** overriding current default factory."); } defaultFactory = self; } } - (TyphoonTypeConverterRegistry *)typeConverterRegistry { return _typeConverterRegistry; } - (NSArray *)registry { [self loadIfNeeded]; return [_registry copy]; } - (void)enumerateDefinitions:(void (^)(TyphoonDefinition *definition, NSUInteger index, TyphoonDefinition **definitionToReplace, BOOL *stop))block { [self loadIfNeeded]; for (NSUInteger i = 0; i < [_registry count]; i++) { TyphoonDefinition *definition = _registry[i]; TyphoonDefinition *definitionToReplace = nil; BOOL stop = NO; block(definition, i, &definitionToReplace, &stop); if (definitionToReplace) { _registry[i] = definitionToReplace; } if (stop) { break; } } } - (void)attachDefinitionPostProcessor:(id)postProcessor { LogTrace(@"Attaching definition post processor: %@", postProcessor); [_definitionPostProcessors addObject:postProcessor]; if ([self isLoaded]) { LogDebug(@"Definitions registered, refreshing all singletons."); [self unload]; } } - (void)attachInstancePostProcessor:(id)postProcessor { LogTrace(@"Attaching instance post processor: %@", postProcessor); [_instancePostProcessors addObject:postProcessor]; } - (void)attachTypeConverter:(id)typeConverter { LogTrace(@"Attaching type conveter: %@", typeConverter); [_typeConverterRegistry registerTypeConverter:typeConverter]; } - (void)inject:(id)instance { @synchronized (self) { [self loadIfNeeded]; TyphoonDefinition *definitionForInstance = [self definitionForType:[instance class] orNil:YES includeSubclasses:NO]; if (definitionForInstance) { [self inject:instance withDefinition:definitionForInstance]; } } } - (void)inject:(id)instance withSelector:(SEL)selector { @synchronized (self) { [self loadIfNeeded]; TyphoonDefinition *definition = [self definitionForKey:NSStringFromSelector(selector)]; if (definition) { [self inject:instance withDefinition:definition]; } else { [NSException raise:NSInvalidArgumentException format:@"Can't find definition for specified selector %@", NSStringFromSelector(selector)]; } } } //------------------------------------------------------------------------------------------- #pragma mark - Utility Methods //------------------------------------------------------------------------------------------- - (NSString *)description { NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: ", NSStringFromClass([self class])]; [description appendFormat:@"_registry=%@", _registry]; [description appendString:@">"]; return description; } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods //------------------------------------------------------------------------------------------- - (void)_load { [self preparePostProcessors]; [self applyPostProcessors]; [self instantiateEagerSingletons]; } - (NSArray *)orderedArray:(NSMutableArray *)array { return [array sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) { NSInteger firstObjectOrder = TyphoonOrderLowestPriority; NSInteger secondObjectOrder = TyphoonOrderLowestPriority; if ([obj1 conformsToProtocol:@protocol(TyphoonOrdered)]) { firstObjectOrder = [obj1 order]; } if ([obj2 conformsToProtocol:@protocol(TyphoonOrdered)]) { secondObjectOrder = [obj2 order]; } return [@(firstObjectOrder) compare:@(secondObjectOrder)]; }]; } - (void)preparePostProcessors { _definitionPostProcessors = [[self orderedArray:_definitionPostProcessors] mutableCopy]; _instancePostProcessors = [[self orderedArray:_instancePostProcessors] mutableCopy]; } - (void)applyPostProcessors { [self enumerateDefinitions:^(TyphoonDefinition *definition, NSUInteger index, TyphoonDefinition **definitionToReplace, BOOL *stop) { TyphoonDefinition *result = [self definitionAfterApplyingPostProcessorsToDefinition:definition]; if (definitionToReplace && result != definition) { *definitionToReplace = result; } }]; } - (TyphoonDefinition *)definitionAfterApplyingPostProcessorsToDefinition:(TyphoonDefinition *)definition { TyphoonDefinition *result = definition; for (id postProcessor in _definitionPostProcessors) { TyphoonDefinition *currentReplacement = nil; [postProcessor postProcessDefinition:result replacement:¤tReplacement withFactory:self]; if (currentReplacement) { result = currentReplacement; } } return result; } - (void)instantiateEagerSingletons { [_registry enumerateObjectsUsingBlock:^(TyphoonDefinition *definition, NSUInteger idx, BOOL *stop) { [self instantiateIfEagerSingleton:definition]; }]; } - (void)instantiateIfEagerSingleton:(TyphoonDefinition *)definition { if (definition.scope == TyphoonScopeSingleton) { [self newOrScopeCachedInstanceForDefinition:definition args:nil]; } } - (NSString *)poolKeyForDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args { if (args) { return [NSString stringWithFormat:@"%@-%ld", definition.key, (unsigned long)[args hash]]; } else { return definition.key; } } - (id)poolForDefinition:(TyphoonDefinition *)definition { switch (definition.scope) { case TyphoonScopeSingleton: case TyphoonScopeLazySingleton: return _singletons; case TyphoonScopeWeakSingleton: return _weakSingletons; case TyphoonScopeObjectGraph: return _objectGraphSharedInstances; default: case TyphoonScopePrototype: return nil; } } - (void)inject:(id)instance withDefinition:(TyphoonDefinition *)definition { @synchronized (self) { id pool = [self poolForDefinition:definition]; [pool setObject:instance forKey:definition.key]; BOOL isPrototype = (definition.scope == TyphoonScopePrototype); TyphoonStackElement *element = [TyphoonStackElement elementWithKey:definition.key args:nil isPrototype:isPrototype]; [element takeInstance:instance]; [_stack push:element]; [self doInjectionEventsOn:instance withDefinition:definition args:nil]; [_stack pop]; if ([_stack isEmpty]) { [_objectGraphSharedInstances removeAllObjects]; } } } @end @implementation TyphoonComponentFactory (TyphoonDefinitionRegisterer) - (TyphoonDefinition *)definitionForKey:(NSString *)key { for (TyphoonDefinition *definition in _registry) { if ([definition.key isEqualToString:key]) { return definition; } } return nil; } - (id)newOrScopeCachedInstanceForDefinition:(TyphoonDefinition *)definition args:(TyphoonRuntimeArguments *)args { if (definition.abstract) { [NSException raise:NSInvalidArgumentException format:@"Attempt to instantiate abstract definition: %@", definition]; } @synchronized (self) { id pool = [self poolForDefinition:definition]; id instance = nil; NSString *poolKey = [self poolKeyForDefinition:definition args:args]; instance = [pool objectForKey:poolKey]; if (instance == nil) { instance = [self buildSharedInstanceForDefinition:definition args:args]; if (instance) { [pool setObject:instance forKey:poolKey]; } } if ([_stack isEmpty]) { [_objectGraphSharedInstances removeAllObjects]; } return instance; } } - (void)addDefinitionToRegistry:(TyphoonDefinition *)definition { [_registry addObject:definition]; } #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wdeprecated-implementations" - (void)addInstancePostProcessor:(id)postProcessor { [self attachInstancePostProcessor:postProcessor]; } #pragma clang diagnostic pop @end ================================================ FILE: Pods/Typhoon/Source/Factory/TyphoonDefinitionRegisterer.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonDefinition; @class TyphoonComponentFactory; /** * @ingroup Assembly * */ @interface TyphoonDefinitionRegisterer : NSObject - (id)initWithDefinition:(TyphoonDefinition *)definition componentFactory:(TyphoonComponentFactory *)componentFactory; - (void)doRegistration; @end ================================================ FILE: Pods/Typhoon/Source/Factory/TyphoonDefinitionRegisterer.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinitionRegisterer.h" #import "TyphoonDefinition.h" #import "TyphoonComponentFactory.h" #import "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h" #import "TyphoonTypeConverter.h" #import "TyphoonTypeConverterRegistry.h" #import #import "OCLogTemplate.h" #import "TyphoonInstancePostProcessor.h" #import "TyphoonMethod.h" #import "TyphoonMethod+InstanceBuilder.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonConfigPostProcessor.h" @implementation TyphoonDefinitionRegisterer { TyphoonDefinition *_definition; TyphoonComponentFactory *_componentFactory; } - (id)initWithDefinition:(TyphoonDefinition *)aDefinition componentFactory:(TyphoonComponentFactory *)aComponentFactory { self = [super init]; if (self) { _definition = aDefinition; _componentFactory = aComponentFactory; } return self; } - (void)doRegistration { [self setDefinitionKeyRandomlyIfNeeded]; if ([self definitionAlreadyRegistered]) { [NSException raise:NSInvalidArgumentException format:@"Key '%@' is already registered.", _definition.key]; } [self registerDefinitionWithFactory]; } - (void)setDefinitionKeyRandomlyIfNeeded { if ([_definition.key length] == 0) { NSString *uuidStr = [[NSProcessInfo processInfo] globallyUniqueString]; _definition.key = [NSString stringWithFormat:@"%@_%@", NSStringFromClass(_definition.type), uuidStr]; } } - (BOOL)definitionAlreadyRegistered { return [_componentFactory definitionForKey:_definition.key] != nil; } - (void)registerDefinitionWithFactory { if ([self definitionIsInfrastructureComponent]) { [self registerInfrastructureComponentFromDefinition]; } else { LogTrace(@"Registering: %@ with key: %@", NSStringFromClass(_definition.type), _definition.key); [_componentFactory addDefinitionToRegistry:_definition]; } } - (BOOL)definitionIsInfrastructureComponent { if ([_definition.type conformsToProtocol:@protocol(TyphoonDefinitionPostProcessor)] || [_definition.type conformsToProtocol:@protocol(TyphoonInstancePostProcessor)] || [_definition.type conformsToProtocol:@protocol(TyphoonTypeConverter)]) { return YES; } return NO; } - (void)registerInfrastructureComponentFromDefinition { LogTrace(@"Registering Infrastructure component: %@ with key: %@", NSStringFromClass(_definition.type), _definition.key); id infrastructureComponent = [_componentFactory newOrScopeCachedInstanceForDefinition:_definition args:nil]; if (_definition.type == [TyphoonConfigPostProcessor class]) { [infrastructureComponent registerNamespace:_definition.space]; [_componentFactory attachDefinitionPostProcessor:infrastructureComponent]; } else if ([_definition.type conformsToProtocol:@protocol(TyphoonDefinitionPostProcessor)]) { [_componentFactory attachDefinitionPostProcessor:infrastructureComponent]; } else if ([_definition.type conformsToProtocol:@protocol(TyphoonInstancePostProcessor)]) { [_componentFactory attachInstancePostProcessor:infrastructureComponent]; } else if ([_definition.type conformsToProtocol:@protocol(TyphoonTypeConverter)]) { [_componentFactory attachTypeConverter:infrastructureComponent]; } } @end ================================================ FILE: Pods/Typhoon/Source/Factory/TyphoonPreattachedComponentsRegisterer.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonComponentFactory; @class TyphoonAssembly; @interface TyphoonPreattachedComponentsRegisterer : NSObject - (instancetype)initWithComponentFactory:(TyphoonComponentFactory *)componentFactory; - (void)doRegistrationForAssembly:(TyphoonAssembly *)assembly; @end ================================================ FILE: Pods/Typhoon/Source/Factory/TyphoonPreattachedComponentsRegisterer.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonPreattachedComponentsRegisterer.h" #import "TyphoonComponentFactory.h" #import "TyphoonAssembly+TyphoonAssemblyFriend.h" @interface TyphoonPreattachedComponentsRegisterer () @property (strong, nonatomic) TyphoonComponentFactory *factory; @end @implementation TyphoonPreattachedComponentsRegisterer - (instancetype)initWithComponentFactory:(TyphoonComponentFactory *)componentFactory { self = [super init]; if (self) { _factory = componentFactory; } return self; } - (void)doRegistrationForAssembly:(TyphoonAssembly *)assembly { NSArray *infrastructureComponents = [assembly preattachedInfrastructureComponents]; for (id component in infrastructureComponents) { if ([component conformsToProtocol:@protocol(TyphoonDefinitionPostProcessor)]) { [self.factory attachDefinitionPostProcessor:component]; } else if ([component conformsToProtocol:@protocol(TyphoonInstancePostProcessor)]) { [self.factory attachInstancePostProcessor:component]; } else if ([component conformsToProtocol:@protocol(TyphoonTypeConverter)]) { [self.factory attachTypeConverter:component]; } } } @end ================================================ FILE: Pods/Typhoon/Source/Test/Patcher/TyphoonPatcher.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonDefinitionPostProcessor.h" #import "TyphoonAbstractDetachableComponentFactoryPostProcessor.h" @class TyphoonDefinition; typedef id (^TyphoonPatchObjectCreationBlock)(void); /** * @ingroup Test * * TyphoonPatcher is a TyphoonComponentFactoryPostProcessor that allows patching out one or more definitions with another object. Integration * testing - testing a class along with its collaborators and configuration - can be a very useful practice. However, its is sometimes * difficult put the system in the required state. Patcher allows taking a fully assembled system, changing just the part required for the * given test scenario. */ @interface TyphoonPatcher : TyphoonAbstractDetachableComponentFactoryPostProcessor { NSMutableDictionary *_patches; } - (void)patchDefinitionWithKey:(NSString *)key withObject:(TyphoonPatchObjectCreationBlock)objectCreationBlock; - (void)patchDefinitionWithSelector:(SEL)definitionSelector withObject:(TyphoonPatchObjectCreationBlock)objectCreationBlock; - (void)detach; @end @interface TyphoonPatcher(Deprecated) /** * This method causes confusion, it only works with a non-activated assembly interface. Users could (rightly) expect it to work with both. * Use the alternative methods: patchDefinitionWithKey: and patchDefinitionWithSelector: */ - (void)patchDefinition:(TyphoonDefinition *)definition withObject:(TyphoonPatchObjectCreationBlock)objectCreationBlock; @end ================================================ FILE: Pods/Typhoon/Source/Test/Patcher/TyphoonPatcher.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonPatcher.h" #import "TyphoonDefinition.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonComponentFactory.h" @interface TyphoonPatcherDefinition : TyphoonDefinition @property (nonatomic, strong) TyphoonPatchObjectCreationBlock patchObjectBlock; - (id)initWithOriginalDefinition:(TyphoonDefinition *)definition patchObjectBlock:(TyphoonPatchObjectCreationBlock)patchObjectBlock; @end @implementation TyphoonPatcherDefinition - (id)initWithOriginalDefinition:(TyphoonDefinition *)definition patchObjectBlock:(TyphoonPatchObjectCreationBlock)patchObjectBlock { self = [super initWithClass:definition.type key:definition.key]; if (self) { self.patchObjectBlock = patchObjectBlock; self.scope = definition.scope; self.autoInjectionVisibility = definition.autoInjectionVisibility; } return self; } #pragma mark - Overriden methods - (id)targetForInitializerWithFactory:(TyphoonComponentFactory *)factory args:(TyphoonRuntimeArguments *)args { return self.patchObjectBlock(); } - (id)initializer { return nil; } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { TyphoonPatcherDefinition *copy = [super copyWithZone:zone]; copy->_patchObjectBlock = _patchObjectBlock; return copy; } @end @implementation TyphoonPatcher //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction - (id)init { self = [super init]; if (self) { _patches = [[NSMutableDictionary alloc] init]; } return self; } //------------------------------------------------------------------------------------------- #pragma mark - Interface Methods - (void)patchDefinitionWithKey:(NSString *)key withObject:(TyphoonPatchObjectCreationBlock)objectCreationBlock { [_patches setObject:objectCreationBlock forKey:key]; } - (void)patchDefinitionWithSelector:(SEL)definitionSelector withObject:(TyphoonPatchObjectCreationBlock)objectCreationBlock { [self patchDefinitionWithKey:NSStringFromSelector(definitionSelector) withObject:objectCreationBlock]; } - (void)detach { [self rollback]; } //------------------------------------------------------------------------------------------- #pragma mark - Protocol Methods - (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory { [super postProcessDefinition:definition replacement:definitionToReplace withFactory:factory]; TyphoonPatchObjectCreationBlock patchObjectBlock = _patches[definition.key]; if (patchObjectBlock && definitionToReplace) { *definitionToReplace = [[TyphoonPatcherDefinition alloc] initWithOriginalDefinition:definition patchObjectBlock:patchObjectBlock]; } } @end @implementation TyphoonPatcher(Deprecated) - (void)patchDefinition:(TyphoonDefinition *)definition withObject:(TyphoonPatchObjectCreationBlock)objectCreationBlock { [self patchDefinitionWithKey:definition.key withObject:objectCreationBlock]; } @end ================================================ FILE: Pods/Typhoon/Source/Test/TestUtils/TyphoonTestUtils.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #define typhoon_asynch_condition(expression) return expression; typedef BOOL (^TyphoonAsynchConditionBlock)(void); typedef void (^TyphoonTestAssertionsBlock)(void); /** * @ingroup Test * * Provides a utility for performing asynchronous integration tests with Typhoon. If a method dispatches on a background thread or queue, * and responds via a block or delegate, this class can be used to test the the expected response occurred. * */ @interface TyphoonTestUtils : NSObject /** * Waits for a condition to occur. The default time-out is seven seconds, which is the current usability standard for remote request * round-trips. */ + (void)waitForCondition:(TyphoonAsynchConditionBlock)condition; /** * Waits for a condition to occur, and performs additional tests. */ + (void)waitForCondition:(TyphoonAsynchConditionBlock)condition andPerformTests:(TyphoonTestAssertionsBlock)assertions; /** * Waits for a condition to occur, and performs additional tests, also overriding the default timeout with the supplied value. * * ##Example: * @code __block BusinessDetails* result = nil; [_client requestBusinessDetailsWithSuccess:^(BusinessDetails* businessDetails) { result = businessDetails; }]; [TyphoonTestUtils wait:3 secondsForCondition:^BOOL { typhoon_asynch_condition(result != nil); } andPerformTests:^ { assertThatBool(businessDetails.goldenEgg, equalToBool:YES); }]; @endcode */ + (void)wait:(NSTimeInterval)seconds secondsForCondition:(TyphoonAsynchConditionBlock)condition andPerformTests:(TyphoonTestAssertionsBlock)assertions; @end ================================================ FILE: Pods/Typhoon/Source/Test/TestUtils/TyphoonTestUtils.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonTestUtils.h" @implementation TyphoonTestUtils + (void)waitForCondition:(TyphoonAsynchConditionBlock)condition { [self waitForCondition:condition andPerformTests:^{ //No assertions - wait for condition only. }]; } + (void)waitForCondition:(TyphoonAsynchConditionBlock)condition andPerformTests:(TyphoonTestAssertionsBlock)assertions { [self wait:7 secondsForCondition:condition andPerformTests:assertions]; } + (void)wait:(NSTimeInterval)seconds secondsForCondition:(TyphoonAsynchConditionBlock)condition andPerformTests:(TyphoonTestAssertionsBlock)assertions { __block BOOL conditionMet = NO; for (float i = 0; i < seconds * 4; i = i + 0.1f) { conditionMet = condition(); if (conditionMet) { break; } else { [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; } } if (conditionMet) { if (assertions) { assertions(); } } else { [NSException raise:NSGenericException format:@"Condition didn't happen before timeout: %f", seconds]; } } @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/NSNullTypeConverter.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeConverter.h" /** Converter used for testing. */ @interface NSNullTypeConverter : NSObject @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/NSNullTypeConverter.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSNullTypeConverter.h" @implementation NSNullTypeConverter - (id)supportedType { return @"NSNull"; } - (id)convert:(NSString *)stringValue { return [NSNull null]; } @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/TyphoonNSNumberTypeConverter.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeConverter.h" @interface TyphoonNSNumberTypeConverter : NSObject @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/TyphoonNSNumberTypeConverter.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonNSNumberTypeConverter.h" @implementation TyphoonNSNumberTypeConverter - (id)supportedType { return @"NSNumber"; } - (id)convert:(NSString *)stringValue { stringValue = [TyphoonTypeConversionUtils textWithoutTypeFromTextValue:stringValue]; NSNumberFormatter *f = [[NSNumberFormatter alloc] init]; [f setNumberStyle:NSNumberFormatterDecimalStyle]; __autoreleasing NSNumber *number = [f numberFromString:stringValue]; return number; } @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/TyphoonNSURLTypeConverter.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeConverter.h" @interface TyphoonNSURLTypeConverter : NSObject @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/TyphoonNSURLTypeConverter.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonNSURLTypeConverter.h" @implementation TyphoonNSURLTypeConverter - (id)supportedType { return @"NSURL"; } - (id)convert:(NSString *)stringValue { stringValue = [TyphoonTypeConversionUtils textWithoutTypeFromTextValue:stringValue]; __autoreleasing NSURL *url = [NSURL URLWithString:stringValue]; return url; } @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/TyphoonPassThroughTypeConverter.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeConverter.h" /** * A 'type converter' for NSString and NSMutableString. */ @interface TyphoonPassThroughTypeConverter : NSObject { BOOL _isMutable; } - (id)initWithIsMutable:(BOOL)isMutable; @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/TyphoonPassThroughTypeConverter.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonPassThroughTypeConverter.h" @implementation TyphoonPassThroughTypeConverter //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction - (id)initWithIsMutable:(BOOL)isMutable { self = [super init]; if (self) { _isMutable = isMutable; } return self; } //------------------------------------------------------------------------------------------- #pragma mark - Protocol Methods - (id)supportedType { if (_isMutable) { return @"NSMutableString"; } else { return @"NSString"; } } - (id)convert:(NSString *)stringValue { stringValue = [TyphoonTypeConversionUtils textWithoutTypeFromTextValue:stringValue]; if (_isMutable) { return [NSMutableString stringWithString:stringValue]; } else { return [NSString stringWithString:stringValue]; } } @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/TyphoonPrimitiveTypeConverter.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeConverter.h" @class TyphoonTypeDescriptor; @interface TyphoonPrimitiveTypeConverter : NSObject - (int)convertToInt:(NSString *)stringValue; - (short)convertToShort:(NSString *)stringValue; - (long)convertToLong:(NSString *)stringValue; - (long long)convertToLongLong:(NSString *)stringValue; - (unsigned char)convertToUnsignedChar:(NSString *)stringValue; - (unsigned int)convertToUnsignedInt:(NSString *)stringValue; - (unsigned short)convertToUnsignedShort:(NSString *)stringValue; - (unsigned long)convertToUnsignedLong:(NSString *)stringValue; - (unsigned long long)convertToUnsignedLongLong:(NSString *)stringValue; - (float)convertToFloat:(NSString *)stringValue; - (double)convertToDouble:(NSString *)stringValue; - (BOOL)convertToBoolean:(NSString *)stringValue; - (const char *)convertToCString:(NSString *)stringValue; - (Class)convertToClass:(NSString *)stringValue; - (SEL)convertToSelector:(NSString *)stringValue; /** @return NSNumber of NSValue from textValue */ - (id)valueFromText:(NSString *)textValue withType:(TyphoonTypeDescriptor *)typeDescription; @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Converters/TyphoonPrimitiveTypeConverter.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonPrimitiveTypeConverter.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonUtils.h" #import "TyphoonIntrospectionUtils.h" @implementation TyphoonPrimitiveTypeConverter //------------------------------------------------------------------------------------------- #pragma mark - Interface Methods - (int)convertToInt:(NSString *)stringValue { return [stringValue intValue]; } - (short)convertToShort:(NSString *)stringValue { return (short) [stringValue intValue]; } - (long)convertToLong:(NSString *)stringValue { return (long) [stringValue longLongValue]; } - (long long)convertToLongLong:(NSString *)stringValue { return [stringValue longLongValue]; } - (unsigned char)convertToUnsignedChar:(NSString *)stringValue { return (unsigned char) [stringValue intValue]; } - (unsigned int)convertToUnsignedInt:(NSString *)stringValue { return (unsigned int) [stringValue longLongValue]; } - (unsigned short)convertToUnsignedShort:(NSString *)stringValue { return (unsigned short) [stringValue intValue]; } - (unsigned long)convertToUnsignedLong:(NSString *)stringValue { return (unsigned long) [stringValue longLongValue]; } - (unsigned long long)convertToUnsignedLongLong:(NSString *)stringValue { return strtoull([stringValue UTF8String], NULL, 0); } - (float)convertToFloat:(NSString *)stringValue { return [stringValue floatValue]; } - (double)convertToDouble:(NSString *)stringValue { return [stringValue doubleValue]; } - (BOOL)convertToBoolean:(NSString *)stringValue { return [stringValue boolValue]; } - (const char *)convertToCString:(NSString *)stringValue { return [stringValue cStringUsingEncoding:NSUTF8StringEncoding]; } - (Class)convertToClass:(NSString *)stringValue { return TyphoonClassFromString(stringValue); } - (SEL)convertToSelector:(NSString *)stringValue { return NSSelectorFromString(stringValue); } - (void *)convertToPointer:(NSString *)stringValue { return (void *)[stringValue integerValue]; } //------------------------------------------------------------------------------------------- - (id)valueFromText:(NSString *)textValue withType:(TyphoonTypeDescriptor *)requiredType { id value = nil; /** Used ugly long switch because it on order faster than using NSNumberFormatter */ switch (requiredType.primitiveType) { case TyphoonPrimitiveTypeBoolean: value = [NSNumber numberWithBool:[self convertToBoolean:textValue]]; break; case TyphoonPrimitiveTypeChar: value = [NSNumber numberWithChar:[self convertToBoolean:textValue]]; break; case TyphoonPrimitiveTypeDouble: value = [NSNumber numberWithDouble:[self convertToDouble:textValue]]; break; case TyphoonPrimitiveTypeFloat: value = [NSNumber numberWithFloat:[self convertToFloat:textValue]]; break; case TyphoonPrimitiveTypeInt: value = [NSNumber numberWithInt:[self convertToInt:textValue]]; break; case TyphoonPrimitiveTypeShort: value = [NSNumber numberWithShort:[self convertToShort:textValue]]; break; case TyphoonPrimitiveTypeLong: value = [NSNumber numberWithLong:[self convertToLong:textValue]]; break; case TyphoonPrimitiveTypeLongLong: value = [NSNumber numberWithLongLong:[self convertToLongLong:textValue]]; break; case TyphoonPrimitiveTypeUnsignedChar: value = [NSNumber numberWithUnsignedChar:[self convertToUnsignedChar:textValue]]; break; case TyphoonPrimitiveTypeUnsignedShort: value = [NSNumber numberWithUnsignedShort:[self convertToUnsignedShort:textValue]]; break; case TyphoonPrimitiveTypeBitField: case TyphoonPrimitiveTypeUnsignedInt: value = [NSNumber numberWithUnsignedInt:[self convertToUnsignedInt:textValue]]; break; case TyphoonPrimitiveTypeUnsignedLong: value = [NSNumber numberWithUnsignedLong:[self convertToUnsignedLong:textValue]]; break; case TyphoonPrimitiveTypeUnsignedLongLong: value = [NSNumber numberWithUnsignedLongLong:[self convertToUnsignedLongLong:textValue]]; break; case TyphoonPrimitiveTypeClass: value = [self convertToClass:textValue]; break; case TyphoonPrimitiveTypeSelector: case TyphoonPrimitiveTypeString: value = [NSValue valueWithPointer:[self convertToSelector:textValue]]; break; case TyphoonPrimitiveTypeUnknown: case TyphoonPrimitiveTypeVoid: { /* Inject all pointers to void and unknown pointers just like void pointers */ if (requiredType.isPointer) { void *pointer = [self convertToPointer:textValue]; value = [NSValue valueWithPointer:pointer]; } else { [NSException raise:NSInvalidArgumentException format:@"Type for %@ is not supported.", requiredType]; } break; } } return value; } @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Helpers/TyphoonColorConversionUtils.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import struct RGBA { CGFloat red; CGFloat green; CGFloat blue; CGFloat alpha; }; @interface TyphoonColorConversionUtils : NSObject + (struct RGBA)colorFromHexString:(NSString *)hexString; + (struct RGBA)colorFromCssStyleString:(NSString *)cssString; @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/Helpers/TyphoonColorConversionUtils.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonColorConversionUtils.h" @implementation TyphoonColorConversionUtils + (struct RGBA)colorFromHexString:(NSString *)hexString { hexString = [[hexString stringByReplacingOccurrencesOfString:@"#" withString:@""] stringByReplacingOccurrencesOfString:@"0x" withString:@""]; unsigned int red, green, blue, alpha; if ([hexString length] == 6) { sscanf([hexString UTF8String], "%02X%02X%02X", &red, &green, &blue); alpha = 255; } else if ([hexString length] == 8) { sscanf([hexString UTF8String], "%02X%02X%02X%02X", &alpha, &red, &green, &blue); } else { [NSException raise:NSInvalidArgumentException format:@"%@ requires a six or eight digit hex string.", NSStringFromClass([self class])]; } return [self colorFromRed:red green:green blue:blue alpha:(CGFloat)(alpha / 255.0)]; } + (struct RGBA)colorFromCssStyleString:(NSString *)cssString { NSArray *colorComponents = [cssString componentsSeparatedByString:@","]; unsigned int red, green, blue; float alpha; if ([colorComponents count] == 3) { sscanf([cssString UTF8String], "%d,%d,%d", &red, &green, &blue); alpha = 1.0; } else if ([colorComponents count] == 4) { sscanf([cssString UTF8String], "%d,%d,%d,%f", &red, &green, &blue, &alpha); } else { [NSException raise:NSInvalidArgumentException format:@"%@ requires css style format UIColor(r,g,b) or UIColor(r,g,b,a).", NSStringFromClass([self class])]; } return [self colorFromRed:red green:green blue:blue alpha:alpha]; } + (struct RGBA)colorFromRed:(NSUInteger)red green:(NSUInteger)green blue:(NSUInteger)blue alpha:(CGFloat)alpha { struct RGBA color; color.red = (CGFloat)(red / 255.0); color.green = (CGFloat)(green / 255.0); color.blue = (CGFloat)(blue / 255.0); color.alpha = (CGFloat)alpha; return color; } @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/TyphoonTypeConversionUtils.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** * A collection of helper methods for type conversion purposes */ @interface TyphoonTypeConversionUtils : NSObject /** * Returns the type from the text, e.g. NSURL for NSURL(http://typhoonframework.org) */ + (NSString *)typeFromTextValue:(NSString *)textValue; /** * Returns the type from the text, e.g. http://typhoonframework.org for NSURL(http://typhoonframework.org) */ + (NSString *)textWithoutTypeFromTextValue:(NSString *)textValue; @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/TyphoonTypeConversionUtils.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonTypeConversionUtils.h" @implementation TyphoonTypeConversionUtils + (NSString *)typeFromTextValue:(NSString *)textValue { NSString *type = nil; NSRange openBraceRange = [textValue rangeOfString:@"("]; BOOL hasBraces = [textValue hasSuffix:@")"] && openBraceRange.location != NSNotFound; if (hasBraces) { type = [textValue substringToIndex:openBraceRange.location]; } return type; } + (NSString *)textWithoutTypeFromTextValue:(NSString *)textValue { NSString *result = textValue; NSRange openBraceRange = [textValue rangeOfString:@"("]; BOOL hasBraces = [textValue hasSuffix:@")"] && openBraceRange.location != NSNotFound; if (hasBraces) { NSRange range = NSMakeRange(openBraceRange.location + openBraceRange.length, 0); range.length = [textValue length] - range.location - 1; result = [textValue substringWithRange:range]; } return result; } @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/TyphoonTypeConverter.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeConverterRegistry.h" #import "TyphoonTypeConversionUtils.h" /** * Declares a contract for converting configuration arguments to their required runtime type. */ @protocol TyphoonTypeConverter /** The supported type of the converter. Class or protocol. */ - (NSString *)supportedType; /** Converts the given string into the supported type. */ - (id)convert:(NSString *)stringValue; @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/TyphoonTypeConverterRegistry.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @protocol TyphoonTypeConverter; @class TyphoonPrimitiveTypeConverter; /** * Registry of type converters, with special treatment for primitives. */ @interface TyphoonTypeConverterRegistry : NSObject /** * Returns the type converter for the given type string. Usually type is class of object you want to convert. * For example for NSURL type, you should use next syntax in properties file. * @code * key=NSURL(http://example.com) * @endcode */ - (id )converterForType:(NSString *)type; /** * Returns the type converter for primitives - BOOLS, ints, floats, etc. */ - (TyphoonPrimitiveTypeConverter *)primitiveTypeConverter; /** * Adds a converter to the registry. Raises an exception if the a converter for the same type * already exists. */ - (void)registerTypeConverter:(id )converter; /** * Unregister a type converter. */ - (void)unregisterTypeConverter:(id )converter; @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/TyphoonTypeConverterRegistry.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeConverterRegistry.h" #import "TyphoonTypeConverter.h" #import "TyphoonPrimitiveTypeConverter.h" #import "TyphoonPassThroughTypeConverter.h" #import "TyphoonNSURLTypeConverter.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonNSNumberTypeConverter.h" #if TARGET_OS_IPHONE || TARGET_OS_TV #import "TyphoonUIColorTypeConverter.h" #import "TyphoonBundledImageTypeConverter.h" #else #import "TyphoonNSColorTypeConverter.h" #endif @interface TyphoonTypeConverterRegistry () @property (strong, nonatomic) TyphoonPrimitiveTypeConverter *primitiveTypeConverter; @property (strong, nonatomic) NSMutableDictionary *typeConverters; @end @implementation TyphoonTypeConverterRegistry //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction - (id)init { self = [super init]; if (self) { _typeConverters = [[NSMutableDictionary alloc] init]; _primitiveTypeConverter = [[TyphoonPrimitiveTypeConverter alloc] init]; [self registerSharedConverters]; [self registerPlatformConverters]; } return self; } //------------------------------------------------------------------------------------------- #pragma mark - Interface Methods - (id )converterForType:(NSString *)type { return _typeConverters[type]; } - (TyphoonPrimitiveTypeConverter *)primitiveTypeConverter { return _primitiveTypeConverter; } - (void)registerTypeConverter:(id )converter { NSString *type = [converter supportedType]; if (!(_typeConverters[type])) { _typeConverters[type] = converter; } else { [NSException raise:NSInvalidArgumentException format:@"Converter for '%@' already registered.", type]; } } - (void)unregisterTypeConverter:(id )converter { [_typeConverters removeObjectForKey:[converter supportedType]]; } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods - (void)registerSharedConverters { [self registerTypeConverter:[[TyphoonPassThroughTypeConverter alloc] initWithIsMutable:NO]]; [self registerTypeConverter:[[TyphoonPassThroughTypeConverter alloc] initWithIsMutable:YES]]; [self registerTypeConverter:[[TyphoonNSURLTypeConverter alloc] init]]; [self registerTypeConverter:[[TyphoonNSNumberTypeConverter alloc] init]]; } - (void)registerPlatformConverters { #if TARGET_OS_IPHONE || TARGET_OS_TV { [self registerTypeConverter:[TyphoonClassFromClass([TyphoonUIColorTypeConverter class]) new]]; [self registerTypeConverter:[TyphoonClassFromClass([TyphoonBundledImageTypeConverter class])new]]; } #else { [self registerTypeConverter:[TyphoonClassFromClass([TyphoonNSColorTypeConverter class]) new]]; } #endif } @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/TyphoonTypeDescriptor.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import typedef enum { TyphoonPrimitiveTypeUnknown, TyphoonPrimitiveTypeChar, TyphoonPrimitiveTypeInt, TyphoonPrimitiveTypeShort, TyphoonPrimitiveTypeLong, TyphoonPrimitiveTypeLongLong, TyphoonPrimitiveTypeUnsignedChar, TyphoonPrimitiveTypeUnsignedInt, TyphoonPrimitiveTypeUnsignedShort, TyphoonPrimitiveTypeUnsignedLong, TyphoonPrimitiveTypeUnsignedLongLong, TyphoonPrimitiveTypeFloat, TyphoonPrimitiveTypeDouble, TyphoonPrimitiveTypeBoolean, TyphoonPrimitiveTypeVoid, TyphoonPrimitiveTypeString, TyphoonPrimitiveTypeClass, TyphoonPrimitiveTypeSelector, TyphoonPrimitiveTypeBitField, } TyphoonPrimitiveType; @interface TyphoonTypeDescriptor : NSObject /** * Indicates if the type being described is a primitive. */ @property(nonatomic, readonly) BOOL isPrimitive; /** * The primitive type being described (if the type being described is a primitive). */ @property(nonatomic, readonly) TyphoonPrimitiveType primitiveType; /** * The type being described, or nil if the type is a primitive or protocol. */ @property(nonatomic, readonly) Class typeBeingDescribed; /** * The protocol being declared. */ @property (nonatomic, strong, readonly) NSString *declaredProtocol; /** * The protocol being described. Returns NSProtocolFromString(declaredProtocol). This may return nil if the protocol * has been trimmed out by the compiler in the case that it has no direct references. */ @property(nonatomic, readonly) Protocol *protocol; /** * Indicates a primitive type is an array. */ @property(nonatomic, readonly) BOOL isArray; /** * Array length for primitive type. */ @property(nonatomic, readonly) int arrayLength; /** * Indicates a primitive type is a pointer. */ @property(nonatomic, readonly) BOOL isPointer; /** * Indicates a primitive type is a structure. */ @property(nonatomic, readonly) BOOL isStructure; @property(nonatomic, strong, readonly) NSString *structureTypeName; + (TyphoonTypeDescriptor *)descriptorWithEncodedType:(const char *)encodedType; + (TyphoonTypeDescriptor *)descriptorWithTypeCode:(NSString *)typeCode; + (TyphoonTypeDescriptor *)descriptorWithType:(id)classOrProtocol; - (id)initWithTypeCode:(NSString *)typeCode; /** * Returns the class or protocol. If the type descriptor is for a primitive, returns nil. */ - (id)classOrProtocol; /** Returns encoded type as string */ - (const char *)encodedType; @end ================================================ FILE: Pods/Typhoon/Source/TypeConversion/TyphoonTypeDescriptor.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeDescriptor.h" #import "TyphoonIntrospectionUtils.h" @implementation NSDictionary (TyphoonPrimitiveType) + (NSDictionary *)dictionaryWithTyphoonPrimitiveTypesAsStrings { static dispatch_once_t onceToken; static NSDictionary *_typhoonPrimitiveTypesAsStrings; dispatch_once(&onceToken, ^{ _typhoonPrimitiveTypesAsStrings = @{ @"c" : @(TyphoonPrimitiveTypeChar), @"i" : @(TyphoonPrimitiveTypeInt), @"s" : @(TyphoonPrimitiveTypeShort), @"l" : @(TyphoonPrimitiveTypeLong), @"q" : @(TyphoonPrimitiveTypeLongLong), @"C" : @(TyphoonPrimitiveTypeUnsignedChar), @"I" : @(TyphoonPrimitiveTypeUnsignedInt), @"S" : @(TyphoonPrimitiveTypeUnsignedShort), @"L" : @(TyphoonPrimitiveTypeUnsignedLong), @"Q" : @(TyphoonPrimitiveTypeUnsignedLongLong), @"f" : @(TyphoonPrimitiveTypeFloat), @"d" : @(TyphoonPrimitiveTypeDouble), @"B" : @(TyphoonPrimitiveTypeBoolean), @"v" : @(TyphoonPrimitiveTypeVoid), @"*" : @(TyphoonPrimitiveTypeString), @"#" : @(TyphoonPrimitiveTypeClass), @":" : @(TyphoonPrimitiveTypeSelector), @"?" : @(TyphoonPrimitiveTypeUnknown) }; }); return _typhoonPrimitiveTypesAsStrings; } @end @implementation TyphoonTypeDescriptor { NSString *_typeCode; } //------------------------------------------------------------------------------------------- #pragma mark - Class Methods //------------------------------------------------------------------------------------------- + (TyphoonTypeDescriptor *)descriptorWithEncodedType:(const char *)encodedType { return [[self alloc] initWithTypeCode:[NSString stringWithCString:encodedType encoding:NSUTF8StringEncoding]]; } + (TyphoonTypeDescriptor *)descriptorWithTypeCode:(NSString *)typeCode { return [[self alloc] initWithTypeCode:typeCode]; } + (TyphoonTypeDescriptor *)descriptorWithType:(id)classOrProtocol { return [[self alloc] initWithType:classOrProtocol]; } //------------------------------------------------------------------------------------------- #pragma mark - Initialization & Destruction //------------------------------------------------------------------------------------------- - (id)initWithTypeCode:(NSString *)typeCode { self = [super init]; if (self) { if ([typeCode hasPrefix:@"T@"]) { _isPrimitive = NO; NSRange typeNameRange = NSMakeRange(2, typeCode.length - 2); #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wassign-enum" NSRange quoteRange = [typeCode rangeOfString:@"\"" options:0 range:typeNameRange locale:nil]; if (quoteRange.length > 0) { typeNameRange.location = quoteRange.location + 1; typeNameRange.length = typeCode.length - typeNameRange.location; NSRange range = [typeCode rangeOfString:@"\"" options:0 range:typeNameRange locale:nil]; typeNameRange.length = range.location - typeNameRange.location; } NSRange protocolRange = [typeCode rangeOfString:@"<" options:0 range:typeNameRange locale:nil]; if (protocolRange.length > 0) { NSRange range = [typeCode rangeOfString:@">" options:0 range:typeNameRange locale:nil]; typeNameRange.length = protocolRange.location - typeNameRange.location; protocolRange.location += 1; protocolRange.length = range.location - protocolRange.location; _declaredProtocol = [typeCode substringWithRange:protocolRange]; } #pragma clang diagnostic pop NSString *typeName = [typeCode substringWithRange:typeNameRange]; _typeBeingDescribed = TyphoonClassFromString(typeName); } else { _isPrimitive = YES; typeCode = [typeCode stringByReplacingOccurrencesOfString:@"T" withString:@""]; [self parsePrimitiveType:typeCode]; } _typeCode = typeCode; } return self; } - (id)initWithType:(id)classOrProtocol { self = [super init]; if (self) { _isPrimitive = NO; _typeCode = [NSString stringWithCString:@encode(id) encoding:NSUTF8StringEncoding]; if (IsClass(classOrProtocol)) { _typeBeingDescribed = classOrProtocol; } else if (IsProtocol(classOrProtocol)) { _declaredProtocol = NSStringFromProtocol(classOrProtocol); } else { [NSException raise:NSInternalInconsistencyException format:@"%@ is not class or protocol", classOrProtocol]; } } return self; } //------------------------------------------------------------------------------------------- #pragma mark - Interface Methods //------------------------------------------------------------------------------------------- - (id)classOrProtocol { if (_typeBeingDescribed) { return _typeBeingDescribed; } else { return self.protocol; } } - (const char *)encodedType { return [_typeCode cStringUsingEncoding:NSUTF8StringEncoding]; } //------------------------------------------------------------------------------------------- #pragma mark - Overridden Methods //------------------------------------------------------------------------------------------- - (Protocol *)protocol { return NSProtocolFromString(_declaredProtocol); } - (NSString *)description { if (_isPrimitive) { return [NSString stringWithFormat:@"Type descriptor for primitive: %i", _primitiveType]; } else { Protocol *protocol = [self protocol]; if (protocol && [self typeBeingDescribed]) { return [NSString stringWithFormat:@"Type descriptor: %@<%@>", NSStringFromClass([self typeBeingDescribed]), NSStringFromProtocol(protocol)]; } else if (protocol) { return [NSString stringWithFormat:@"Type descriptor: id<%@>", NSStringFromProtocol(protocol)]; } else { return [NSString stringWithFormat:@"Type descriptor: %@", [self classOrProtocol]]; } } } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods //------------------------------------------------------------------------------------------- - (void)parsePrimitiveType:(NSString *)typeCode { typeCode = [self extractArrayInformation:typeCode]; typeCode = [self extractPointerInformation:typeCode]; typeCode = [self extractStructureInformation:typeCode]; typeCode = [self extractObjectInformation:typeCode]; _primitiveType = [self typeFromTypeCode:typeCode]; } - (NSString *)extractArrayInformation:(NSString *)typeCode { if ([typeCode hasPrefix:@"["] && [typeCode hasSuffix:@"]"]) { _isArray = YES; typeCode = [[typeCode stringByReplacingOccurrencesOfString:@"[" withString:@""] stringByReplacingOccurrencesOfString:@"]" withString:@""]; NSScanner *scanner = [[NSScanner alloc] initWithString:typeCode]; [scanner scanInt:&_arrayLength]; typeCode = [typeCode stringByTrimmingCharactersInSet:[NSCharacterSet decimalDigitCharacterSet]]; } return typeCode; } - (NSString *)extractPointerInformation:(NSString *)typeCode { if ([typeCode hasPrefix:@"^"]) { _isPointer = YES; typeCode = [typeCode stringByReplacingOccurrencesOfString:@"^" withString:@""]; } return typeCode; } - (NSString *)extractStructureInformation:(NSString *)typeCode { if ([typeCode hasPrefix:@"{"] && [typeCode hasSuffix:@"}"]) { _isStructure = YES; typeCode = [[typeCode stringByReplacingOccurrencesOfString:@"{" withString:@""] stringByReplacingOccurrencesOfString:@"}" withString:@""]; _structureTypeName = [typeCode copy]; } return typeCode; } - (NSString *)extractObjectInformation:(NSString *)typeCode { if ([typeCode isEqualToString:@"@"]) { _isPrimitive = NO; return @"?"; } return typeCode; } - (TyphoonPrimitiveType)typeFromTypeCode:(NSString *)typeCode { return (TyphoonPrimitiveType)[[NSDictionary dictionaryWithTyphoonPrimitiveTypesAsStrings][typeCode] intValue]; } @end ================================================ FILE: Pods/Typhoon/Source/Typhoon+Infrastructure.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h" #import "TyphoonRuntimeArguments.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonInjection.h" #import "TyphoonPropertyInjection.h" #import "TyphoonParameterInjection.h" #import "TyphoonAbstractInjection.h" #import "TyphoonInjectionByCollection.h" #import "TyphoonInjectionByComponentFactory.h" #import "TyphoonInjectionByConfig.h" #import "TyphoonInjectionByDictionary.h" #import "TyphoonInjectionByFactoryReference.h" #import "TyphoonInjectionByObjectFromString.h" #import "TyphoonInjectionByObjectInstance.h" #import "TyphoonInjectionByReference.h" #import "TyphoonInjectionByRuntimeArgument.h" #import "TyphoonInjectionByType.h" #import "TyphoonInjections.h" ================================================ FILE: Pods/Typhoon/Source/Typhoon.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #if TARGET_OS_IPHONE || TARGET_OS_TV #import #endif //! Project version number for Typhoon. FOUNDATION_EXPORT double TyphoonVersionNumber; //! Project version string for Typhoon. FOUNDATION_EXPORT const unsigned char TyphoonVersionString[]; #import "TyphoonAssembly.h" #import "TyphoonDefinition.h" #import "TyphoonBlockDefinition.h" #import "TyphoonInject.h" #import "TyphoonMethod.h" #import "TyphoonConfigPostProcessor.h" #import "TyphoonDefinition+Config.h" #import "TyphoonResource.h" #import "TyphoonBundleResource.h" #import "TyphoonComponentFactory.h" #import "TyphoonBlockComponentFactory.h" #import "TyphoonFactoryDefinition.h" #import "TyphoonDefinitionPostProcessor.h" #import "TyphoonInstancePostProcessor.h" #import "TyphoonTypeConverter.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonCollaboratingAssemblyProxy.h" #import "TyphoonTestUtils.h" #import "TyphoonPatcher.h" #import "TyphoonAutoInjection.h" #import "TyphoonDefinitionNamespace.h" #import "NSObject+FactoryHooks.h" #if TARGET_OS_IPHONE || TARGET_OS_TV #import "TyphooniOS.h" #endif ================================================ FILE: Pods/Typhoon/Source/Utils/NSArray+TyphoonManualEnumeration.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @protocol TyphoonIterator - (void)next; @end typedef void (^TyphoonManualIterationBlock)(id object, iditerator); @interface NSArray (TyphoonSignalEnumerator) - (void)typhoon_enumerateObjectsWithManualIteration:(TyphoonManualIterationBlock)block completion:(dispatch_block_t)completion; @end ================================================ FILE: Pods/Typhoon/Source/Utils/NSArray+TyphoonManualEnumeration.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSArray+TyphoonManualEnumeration.h" @interface TyphoonNextSignal : NSObject - (void)next; - (void)setNextBlock:(dispatch_block_t)block; @end @implementation TyphoonNextSignal { dispatch_block_t _block; } - (void)next { if (_block) { _block(); } } - (void)setNextBlock:(dispatch_block_t)block { _block = block; } @end @implementation NSArray (TyphoonSignalEnumerator) - (void)typhoon_enumerateObjectsWithManualIteration:(TyphoonManualIterationBlock)block completion:(dispatch_block_t)completion { TyphoonNextSignal *signal = [[TyphoonNextSignal alloc] init]; NSEnumerator *objectsEnumerator = [self objectEnumerator]; __block __strong id strongSelf = self; __weak __typeof (signal) weakSignal = signal; [signal setNextBlock:^{ BOOL enumeratorExhausted = [strongSelf typhoon_doStepWithEnumerator:objectsEnumerator signal:weakSignal block:block completion:completion]; if (enumeratorExhausted) { strongSelf = nil; } }]; [self typhoon_doStepWithEnumerator:objectsEnumerator signal:signal block:block completion:completion]; } - (BOOL)typhoon_doStepWithEnumerator:(NSEnumerator *)enumerator signal:(id)iterator block:(TyphoonManualIterationBlock)block completion:(dispatch_block_t)completion { id object = [enumerator nextObject]; if (object) { block(object, iterator); return NO; } else { completion(); return YES; } } @end ================================================ FILE: Pods/Typhoon/Source/Utils/NSObject+DeallocNotification.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface NSObject (DeallocNotification) - (void)setDeallocNotificationInBlock:(dispatch_block_t)block; - (void)removeDeallocNotification; - (void)setDeallocNotificationWithKey:(const char *)key andBlock:(dispatch_block_t)block; - (void)removeDeallocNotificationForKey:(const char *)key; @end ================================================ FILE: Pods/Typhoon/Source/Utils/NSObject+DeallocNotification.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSObject+DeallocNotification.h" #import ///////////////////// Object with Dealloc callback /////////////////////////////////////////////////////////////// /** * DeallocCallbackObject notify in deallocCallback block when dealloced */ @interface DeallocCallbackObject : NSObject - (id)initWithDeallocCallback:(dispatch_block_t)deallocCallback; - (void)removeCallback; @end @implementation DeallocCallbackObject { dispatch_block_t callback; } - (id)initWithDeallocCallback:(dispatch_block_t)deallocCallback { NSParameterAssert(deallocCallback); self = [super init]; if (self) { callback = [deallocCallback copy]; } return self; } - (void)removeCallback { callback = nil; } - (void)dealloc { if (callback) { callback(); } } @end /////////////////////////////// DeallocNotifier /////////////////////////////////////////////////////////////// @implementation NSObject (DeallocNotifier) static const char *kTyphoonDefaultDeallocNotifierKey = "kTyphoonDefaultDeallocNotifierKey"; - (void)setDeallocNotificationInBlock:(dispatch_block_t)block { [self setDeallocNotificationWithKey:kTyphoonDefaultDeallocNotifierKey andBlock:block]; } - (void)removeDeallocNotification { [self removeDeallocNotificationForKey:kTyphoonDefaultDeallocNotifierKey]; } - (void)setDeallocNotificationWithKey:(const char *)key andBlock:(dispatch_block_t)block { DeallocCallbackObject *deallocNotifier = [[DeallocCallbackObject alloc] initWithDeallocCallback:block]; objc_setAssociatedObject(self, key, deallocNotifier, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (void)removeDeallocNotificationForKey:(const char *)key { DeallocCallbackObject *deallocNotifier = objc_getAssociatedObject(self, key); [deallocNotifier removeCallback]; objc_setAssociatedObject(self, key, nil, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end ================================================ FILE: Pods/Typhoon/Source/Utils/NSObject+PropertyInjection.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface NSObject (PropertyInjection) - (void)typhoon_injectValue:(id)value forPropertyName:(NSString *)propertyName; @end ================================================ FILE: Pods/Typhoon/Source/Utils/NSObject+PropertyInjection.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSObject+PropertyInjection.h" #import "TyphoonTypeDescriptor.h" #import "TyphoonUtils.h" #import #import "NSInvocation+TCFUnwrapValues.h" #import "NSMethodSignature+TCFUnwrapValues.h" #import "TyphoonIntrospectionUtils.h" @implementation NSObject (PropertyInjection) - (void)typhoon_injectValue:(id)value forPropertyName:(NSString *)propertyName { SEL setterSelector = [TyphoonIntrospectionUtils setterForPropertyWithName:propertyName inClass:[self class]]; if (!setterSelector) { [NSException raise:@"TyphoonPropertySetterNotFoundException" format:@"Can't inject property '%@' for object '%@'. Setter selector not found. Make sure that property exists and writable",propertyName, self]; } NSMethodSignature *signature = [self methodSignatureForSelector:setterSelector]; if ([signature shouldUnwrapValue:value forArgumentAtIndex:2]) { [self typhoon_injectUnwrappedValue:value forPropertySetter:setterSelector signature:signature]; } else { [self typhoon_injectObject:value forPropertySetter:setterSelector]; } } - (void)typhoon_injectUnwrappedValue:(NSValue *)value forPropertySetter:(SEL)setter signature:(NSMethodSignature *)signature { NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; [invocation setSelector:setter]; [invocation typhoon_setArgumentObject:value atIndex:2]; [invocation invokeWithTarget:self]; } - (void)typhoon_injectObject:(id)value forPropertySetter:(SEL)setter { void(*setterMethod)(id, SEL, id) = (void ( *)(id, SEL, id)) [self methodForSelector:setter]; setterMethod(self, setter, value); } @end ================================================ FILE: Pods/Typhoon/Source/Utils/NSObject+TyphoonIntrospectionUtils.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonTypeDescriptor; @interface NSObject (TyphoonIntrospectionUtils) /** * Returns a set of property names up to the parent class. */ - (NSSet*) /* */ typhoonPropertiesUpToParentClass:(Class)clazz; /** * Returns a Class object or `TyphoonTypeDescriptor` in the case of a primitive type. */ - (TyphoonTypeDescriptor *)typhoonTypeForPropertyNamed:(NSString *)propertyName; @end ================================================ FILE: Pods/Typhoon/Source/Utils/NSObject+TyphoonIntrospectionUtils.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonLinkerCategoryBugFix.h" TYPHOON_LINK_CATEGORY(NSObject_TyphoonIntrospectionUtils) #import #import "TyphoonTypeDescriptor.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonUtils.h" @implementation NSObject (TyphoonIntrospectionUtils) - (NSSet*)typhoonPropertiesUpToParentClass:(Class)clazz { return [TyphoonIntrospectionUtils propertiesForClass:[self class] upToParentClass:clazz]; } - (TyphoonTypeDescriptor *)typhoonTypeForPropertyNamed:(NSString *)propertyName { return [TyphoonIntrospectionUtils typeForPropertyNamed:propertyName inClass:[self class]]; } @end ================================================ FILE: Pods/Typhoon/Source/Utils/Swizzle/TyphoonMethodSwizzler.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @protocol TyphoonMethodSwizzler @required - (BOOL)swizzleMethod:(SEL)selA withMethod:(SEL)selB onClass:(Class)pClass error:(NSError **)error; @end ================================================ FILE: Pods/Typhoon/Source/Utils/Swizzle/TyphoonSwizzlerDefaultImpl.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonMethodSwizzler.h" @interface TyphoonSwizzlerDefaultImpl : NSObject + (instancetype)instance; @end ================================================ FILE: Pods/Typhoon/Source/Utils/Swizzle/TyphoonSwizzlerDefaultImpl.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2014, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonSwizzlerDefaultImpl.h" #import @implementation TyphoonSwizzlerDefaultImpl + (instancetype)instance { static TyphoonSwizzlerDefaultImpl *defaultSwizzler; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ defaultSwizzler = [TyphoonSwizzlerDefaultImpl new]; }); return defaultSwizzler; } - (BOOL)swizzleMethod:(SEL)origSel withMethod:(SEL)altSel onClass:(Class)pClass error:(NSError **)error { Method origMethod = class_getInstanceMethod(pClass, origSel); Method altMethod = class_getInstanceMethod(pClass, altSel); if (!origMethod && !altMethod) { if (error) { NSString *description = [NSString stringWithFormat:@"Can't swizzle methods since selector %@ and %@ not implemented in %@", NSStringFromSelector(origSel), NSStringFromSelector(altSel), NSStringFromClass(pClass)]; *error = [NSError errorWithDomain:@"TyphoonMethodSwizzle" code:0 userInfo:@{NSLocalizedDescriptionKey : description }]; } return NO; } const char *typeEncoding = NULL; if (origMethod) { typeEncoding = method_getTypeEncoding(origMethod); } else { typeEncoding = method_getTypeEncoding(altMethod); } class_addMethod(pClass, origSel, class_getMethodImplementation(pClass, origSel), typeEncoding); class_addMethod(pClass, altSel, class_getMethodImplementation(pClass, altSel), typeEncoding); method_exchangeImplementations(class_getInstanceMethod(pClass, origSel), class_getInstanceMethod(pClass, altSel)); return YES; } @end ================================================ FILE: Pods/Typhoon/Source/Utils/TyphoonIntrospectionUtils.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import @class TyphoonTypeDescriptor; NSString *TyphoonTypeStringFor(id classOrProtocol); Class TyphoonClassFromClass(Class clazz); Class TyphoonClassFromString(NSString *className); BOOL IsClass(id classOrProtocol); BOOL IsBlock(const char *objCType); BOOL IsProtocol(id classOrProtocol); @interface TyphoonIntrospectionUtils : NSObject + (TyphoonTypeDescriptor *)typeForPropertyNamed:(NSString *)propertyName inClass:(Class)clazz; + (SEL)setterForPropertyWithName:(NSString *)property inClass:(Class)clazz; + (SEL)getterForPropertyWithName:(NSString *)property inClass:(Class)clazz; + (NSMethodSignature *)methodSignatureWithArgumentsAndReturnValueAsObjectsFromSelector:(SEL)selector; + (NSUInteger)numberOfArgumentsInSelector:(SEL)selector; + (NSSet *)propertiesForClass:(Class)clazz upToParentClass:(Class)parent; + (NSSet *)methodsForClass:(Class)clazz upToParentClass:(Class)parent; @end ================================================ FILE: Pods/Typhoon/Source/Utils/TyphoonIntrospectionUtils.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import #import "TyphoonIntrospectionUtils.h" #import "TyphoonTypeDescriptor.h" @implementation TyphoonIntrospectionUtils BOOL TyphoonIsInvalidClassName(NSString *className); NSString *TyphoonDefaultModuleName(void); Class TyphoonClassFromFrameworkString(NSString *className); + (TyphoonTypeDescriptor *)typeForPropertyNamed:(NSString *)propertyName inClass:(Class)clazz { TyphoonTypeDescriptor *typeDescriptor = nil; objc_property_t propertyReflection = class_getProperty(clazz, [propertyName cStringUsingEncoding:NSASCIIStringEncoding]); if (propertyReflection) { const char *attributes = property_getAttributes(propertyReflection); if (attributes == NULL) { return (NULL); } char buffer[256]; const char *e = strchr(attributes, ','); if (e == NULL) { return (NULL); } NSUInteger len = (NSUInteger) (e - attributes); memcpy( buffer, attributes, len ); buffer[len] = '\0'; NSString *typeCode = [NSString stringWithCString:buffer encoding:NSASCIIStringEncoding]; typeDescriptor = [TyphoonTypeDescriptor descriptorWithTypeCode:typeCode]; } return typeDescriptor; } + (SEL)setterForPropertyWithName:(NSString *)propertyName inClass:(Class)clazz { SEL setterSelector = nil; objc_property_t property = class_getProperty(clazz, [propertyName cStringUsingEncoding:NSASCIIStringEncoding]); if (property) { if (![self isReadonlyProperty:property]) { NSString *selectorString = [self customSetterForProperty:property]; if (!selectorString) { selectorString = [self defaultSetterForPropertyWithName:propertyName]; } setterSelector = NSSelectorFromString(selectorString); } } else if (propertyName.length > 0) { NSString *selectorString = [self defaultSetterForPropertyWithName:propertyName]; SEL aSelector = NSSelectorFromString(selectorString); if (class_getInstanceMethod(clazz, aSelector)) { setterSelector = aSelector; } } return setterSelector; } + (SEL)getterForPropertyWithName:(NSString *)propertyName inClass:(Class)clazz { SEL getterSelector = nil; objc_property_t property = class_getProperty(clazz, [propertyName cStringUsingEncoding:NSASCIIStringEncoding]); if (property) { NSString *getterString = [self customGetterForProperty:property]; if (!getterString) { getterString = [self defaultGetterForPropertyWithName:propertyName]; } getterSelector = NSSelectorFromString(getterString); } return getterSelector; } + (NSMethodSignature *)methodSignatureWithArgumentsAndReturnValueAsObjectsFromSelector:(SEL)selector { NSUInteger argc = [self numberOfArgumentsInSelector:selector]; NSMutableString *signatureString = [[NSMutableString alloc] initWithCapacity:argc + 3]; //one symbol per encoded type [signatureString appendFormat:@"%s%s%s", @encode(id), @encode(id), @encode(SEL)]; for (NSUInteger i = 0; i < argc; i++) { [signatureString appendString:[NSString stringWithCString:@encode(id) encoding:NSASCIIStringEncoding]]; } NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:[signatureString cStringUsingEncoding:NSASCIIStringEncoding]]; return signature; } + (NSUInteger)numberOfArgumentsInSelector:(SEL)selector { NSString *string = NSStringFromSelector(selector); NSUInteger count = 0; for (NSUInteger i = 0; i < string.length; i++) { if ([string characterAtIndex:i] == ':') { count++; } } return count; } + (NSSet *)propertiesForClass:(Class)clazz upToParentClass:(Class)parent { NSMutableSet *propertyNames = [[NSMutableSet alloc] init]; while (clazz && clazz != parent) { unsigned int count = 0; objc_property_t *properties = class_copyPropertyList(clazz, &count); for (unsigned int propertyIndex = 0; propertyIndex < count; propertyIndex++) { objc_property_t aProperty = properties[propertyIndex]; NSString *propertyName = [NSString stringWithCString:property_getName(aProperty) encoding:NSUTF8StringEncoding]; [propertyNames addObject:propertyName]; } clazz = class_getSuperclass(clazz); free(properties); } return propertyNames; } + (NSSet *)methodsForClass:(Class)clazz upToParentClass:(Class)parent { NSMutableSet *methodSelectors = [[NSMutableSet alloc] init]; while (clazz && clazz != parent) { unsigned int methodCount; Method *methodList = class_copyMethodList(clazz, &methodCount); for (unsigned int i = 0; i < methodCount; i++) { Method method = methodList[i]; [methodSelectors addObject:NSStringFromSelector(method_getName(method))]; } free(methodList); clazz = class_getSuperclass(clazz); } return methodSelectors; } #pragma mark - Property Attributes Utils + (NSString *)classNameOfProperty:(objc_property_t)property { NSString *propertyAttributes = [NSString stringWithCString:property_getAttributes(property) encoding:NSUTF8StringEncoding]; NSArray *splitPropertyAttributes = [propertyAttributes componentsSeparatedByString:@"\""]; if (splitPropertyAttributes.count >= 2) { return [splitPropertyAttributes objectAtIndex:1]; } return nil; } + (BOOL)isReadonlyProperty:(objc_property_t)property { char *readonlyFlag = property_copyAttributeValue(property, "R"); BOOL isReadonly = readonlyFlag != NULL; free(readonlyFlag); return isReadonly; } + (NSString *)customSetterForProperty:(objc_property_t)property { NSString *customSetter = nil; char *setterName = property_copyAttributeValue(property, "S"); if (setterName != NULL) { customSetter = [NSString stringWithCString:setterName encoding:NSASCIIStringEncoding];; free(setterName); } return customSetter; } + (NSString *)customGetterForProperty:(objc_property_t)property { NSString *customGetter = nil; char *getterName = property_copyAttributeValue(property, "G"); if (getterName != NULL) { customGetter = [NSString stringWithCString:getterName encoding:NSASCIIStringEncoding];; free(getterName); } return customGetter; } + (NSString *)defaultSetterForPropertyWithName:(NSString *)propertyName { NSString *firstLetterUppercase = [[propertyName substringToIndex:1] uppercaseString]; NSString *propertyPart = [propertyName stringByReplacingCharactersInRange:NSMakeRange(0, 1) withString:firstLetterUppercase]; return [NSString stringWithFormat:@"set%@:", propertyPart]; } + (NSString *)defaultGetterForPropertyWithName:(NSString *)propertyName { return propertyName; } @end NSString *TyphoonTypeStringFor(id classOrProtocol) { if (IsClass(classOrProtocol)) { return NSStringFromClass(classOrProtocol); } else { return [NSString stringWithFormat:@"id<%@>", NSStringFromProtocol(classOrProtocol)]; } } NSString *TyphoonDefaultModuleName(void) { static NSString *defaultModuleName; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ defaultModuleName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleExecutable"]; defaultModuleName = [[defaultModuleName stringByReplacingOccurrencesOfString:@" " withString:@"_"] stringByReplacingOccurrencesOfString:@"-" withString:@"_"]; }); return defaultModuleName; } BOOL TyphoonIsInvalidClassName(NSString *className) { if (className.length == 0) { return YES; } if ([className isEqualToString:@"?"]) { return YES; } return NO; } Class TyphoonClassFromClass(Class clazz) { return TyphoonClassFromString(NSStringFromClass(clazz)); } Class TyphoonClassFromString(NSString *className) { if (TyphoonIsInvalidClassName(className)) { return Nil; } Class clazz = NSClassFromString(className); if (!clazz) { clazz = NSClassFromString([TyphoonDefaultModuleName() stringByAppendingFormat:@".%@", className]); } if (!clazz) { /** Swift issue: When calling property_getAttributes() for a Swift class that belongs in a different module, the module name is *NOT* returned, therefore attempting to resolve the Swift class via name wont work: 1. Foo.BarAssembly in Foo.framework 2. App.app includes Foo.framework and references Foo.BarAssembly in AppAssembly import Foo class AppAssembly: TyphonAssembly { var barAssembly: Foo.BarAssembly? } 3. property_getAttributes() called on `barAssembly` returns @T"BarAssembly" but should return @T"Foo.BarAssembly" 4. NSClassforString("BarAssembly") returns nil because the App.app does not define this class Poor workaround, not the best, but no other way around it unless Apple fixes the objc reflection of Swift classes there is no other way: If we are unable to find the class in the top level project iterate all dynamic frameworks and attempt to resolve the class name by appending the module name This is somewhat brittle as we loose the namespacing modules give us (for example, if 2 modules define the same class then which ever appears first in the list will be used which might not be expected). In order for this to work, developers are required to: - have unique names of assemblies across modules - frameworks bundle ids much match the following format: *.{Name} where Name is the name of the module */ //Those class names will never be resolved, so no reason to try if(!className || [className isEqual:@"?"] || [className isEqual:@""]) { return nil; } clazz = TyphoonClassFromFrameworkString(className); } return clazz; } #if TARGET_OS_IOS Class TyphoonClassFromFrameworkString(NSString *className) { Class clazz = nil; // on iOS, we can safely precache frameworks // because they'll never change during app lifetime static NSArray * frameworkNames = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSMutableArray * temporaryNames = [NSMutableArray array]; NSArray *frameworks = [NSBundle allFrameworks]; [frameworks enumerateObjectsUsingBlock:^(NSBundle* _Nonnull framework, NSUInteger idx, BOOL * _Nonnull stop) { NSString *bundleIdentifier = [framework bundleIdentifier]; // ignore apple frameworks and resource bundles if (bundleIdentifier && ![bundleIdentifier hasPrefix:@"com.apple"]) { [temporaryNames addObject:bundleIdentifier]; } }]; frameworkNames = [temporaryNames copy]; }); for (uint i = 0; i < frameworkNames.count && clazz == nil; ++i) { NSString *bundleIdentifier = frameworkNames[i]; NSRange range = [bundleIdentifier rangeOfString:@"." options:NSBackwardsSearch]; if (range.location != NSNotFound) { NSString *frameworkName = [bundleIdentifier substringFromIndex:range.location + 1]; if (frameworkName != nil) { clazz = NSClassFromString([frameworkName stringByAppendingFormat:@".%@", className]); } } } return clazz; } #else Class TyphoonClassFromFrameworkString(NSString *className) { // For OSX we still need to iterate, since platform allows us to use downloaded frameworks // and they can change during app lifetime Class clazz = nil; NSArray *frameworks = [NSBundle allFrameworks]; for (uint i = 0; i < frameworks.count && clazz == nil; ++i) { NSBundle *framework = [frameworks objectAtIndex:i]; NSString *bundleIdentifier = [framework bundleIdentifier]; // ignore apple frameworks if (![bundleIdentifier hasPrefix:@"com.apple"]) { NSRange range = [bundleIdentifier rangeOfString:@"." options:NSBackwardsSearch]; if (range.location != NSNotFound) { NSString *frameworkName = [bundleIdentifier substringFromIndex:range.location + 1]; if (frameworkName != nil) { clazz = NSClassFromString([frameworkName stringByAppendingFormat:@".%@", className]); } } } } return clazz; } #endif BOOL IsClass(id classOrProtocol) { return class_isMetaClass(object_getClass(classOrProtocol)); } BOOL IsBlock(const char *objCType) { return strcmp(objCType, "@?") == 0; } BOOL IsProtocol(id classOrProtocol) { return object_getClass(classOrProtocol) == object_getClass(@protocol(NSObject)); } ================================================ FILE: Pods/Typhoon/Source/Utils/TyphoonLinkerCategoryBugFix.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** Add this macro before each category implementation, so we don't have to use -all_load or -force_load to load object files from static libraries that only contain categories and no classes. See http://developer.apple.com/library/mac/#qa/qa2006/qa1490.html for more info. Shamelessly borrowed from Three20 and RestKit */ #define TYPHOON_LINK_CATEGORY(name) \ @interface TYPHOON_LINK_CATEGORY_##name : NSObject \ @end \ @implementation TYPHOON_LINK_CATEGORY_##name \ @end ================================================ FILE: Pods/Typhoon/Source/Utils/TyphoonSelector.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonSelector : NSObject + (TyphoonSelector *)selectorWithName:(NSString *)string; + (TyphoonSelector *)selectorWithSEL:(SEL)pSelector; - (id)initWithName:(NSString *)string; - (id)initWithSEL:(SEL)pSelector; @property(readonly) SEL selector; @end ================================================ FILE: Pods/Typhoon/Source/Utils/TyphoonSelector.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonSelector.h" #import @interface TyphoonSelector () @end @implementation TyphoonSelector { } + (TyphoonSelector *)selectorWithName:(NSString *)aName { return [[self alloc] initWithName:aName]; } + (TyphoonSelector *)selectorWithSEL:(SEL)aSelector { return [[self alloc] initWithSEL:aSelector]; } - (id)initWithName:(NSString *)aName { return [self initWithSEL:NSSelectorFromString(aName)]; } - (id)initWithSEL:(SEL)aSelector { self = [super init]; if (self) { _selector = aSelector; } return self; } - (NSString *)description { // NSMutableString *description = [NSMutableString stringWithFormat:@"<%@: %p SEL named: '%@'>", NSStringFromClass([self class]), self, NSStringFromSelector(self.selector)]; return description; } - (NSUInteger)hash { return (NSUInteger) sel_getName(self.selector); } - (BOOL)isEqual:(id)other { if (other == self) { return YES; } if (!other || [other class] != [self class]) { return NO; } return [self isEqualToSelector:(TyphoonSelector *) other]; } - (BOOL)isEqualToSelector:(TyphoonSelector *)wrappedSelector { return sel_isEqual(self.selector, wrappedSelector.selector); } @end ================================================ FILE: Pods/Typhoon/Source/Utils/TyphoonUtils.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #define CStringEquals(stringA, stringB) (stringA == stringB || strcmp(stringA, stringB) == 0) #define TyphoonHashByAppendingInteger(hash, integer) ((hash << 5) - hash + integer) ================================================ FILE: Pods/Typhoon/Source/Vendor/OCLogTemplate/OCLogTemplate.h ================================================ /** * For Objective-C code, this library adds flexible, non-intrusive logging capabilities * that can be efficiently enabled or disabled via compile switches. * * There are four levels of logging: Trace, Info, Error and Debug, and each can be enabled * independently via the LOGGING_LEVEL_TRACE, LOGGING_LEVEL_INFO, LOGGING_LEVEL_ERROR and * LOGGING_LEVEL_DEBUG switches, respectively. * * In addition, ALL logging can be enabled or disabled via the LOGGING_ENABLED switch. * * Logging functions are implemented here via macros. Disabling logging, either entirely, or * at a specific level, completely removes the corresponding log invocations from the compiled * code, thus eliminating both the memory and CPU overhead that the logging calls would add. * You might choose, for example, to completely remove all logging from production release code, * by setting LOGGING_ENABLED off in your production builds settings. Or, as another example, * you might choose to include Error logging in your production builds by turning only * LOGGING_ENABLED and LOGGING_LEVEL_ERROR on, and turning the others off. * * To perform logging, use any of the following function calls in your code: * * LogTrace(fmt, ...) - recommended for detailed tracing of program flow * - will print if LOGGING_LEVEL_TRACE is set on. * * LogInfo(fmt, ...) - recommended for general, infrequent, information messages * - will print if LOGGING_LEVEL_INFO is set on. * * LogError(fmt, ...) - recommended for use only when there is an error to be logged * - will print if LOGGING_LEVEL_ERROR is set on. * * LogDebug(fmt, ...) - recommended for temporary use during debugging * - will print if LOGGING_LEVEL_DEBUG is set on. * * In each case, the functions follow the general NSLog/printf template, where the first argument * "fmt" is an NSString that optionally includes embedded Format Specifiers, and subsequent optional * arguments indicate data to be formatted and inserted into the string. As with NSLog, the number * of optional arguments must match the number of embedded Format Specifiers. For more info, see the * core documentation for NSLog and String Format Specifiers. * * You can choose to have each logging entry automatically include class, method and line information * by enabling the LOGGING_INCLUDE_CODE_LOCATION switch. * * Although you can directly edit this file to turn on or off the switches below, the preferred * technique is to set these switches via the compiler build setting GCC_PREPROCESSOR_DEFINITIONS * in your build configuration. */ /** * Set this switch to enable or disable logging capabilities. * This can be set either here or via the compiler build setting GCC_PREPROCESSOR_DEFINITIONS * in your build configuration. Using the compiler build setting is preferred for this to * ensure that logging is not accidentally left enabled by accident in release builds. */ #ifndef LOGGING_ENABLED # define LOGGING_ENABLED 1 #endif /** * Set any or all of these switches to enable or disable logging at specific levels. * These can be set either here or as a compiler build settings. * For these settings to be effective, LOGGING_ENABLED must also be defined and non-zero. */ #ifndef LOGGING_LEVEL_TRACE # define LOGGING_LEVEL_TRACE 0 #endif #ifndef LOGGING_LEVEL_INFO # define LOGGING_LEVEL_INFO 1 #endif #ifndef LOGGING_LEVEL_ERROR # define LOGGING_LEVEL_ERROR 1 #endif #ifndef LOGGING_LEVEL_DEBUG # define LOGGING_LEVEL_DEBUG 1 #endif /** * Set this switch to indicate whether or not to include class, method and line information * in the log entries. This can be set either here or as a compiler build setting. */ #ifndef LOGGING_INCLUDE_CODE_LOCATION #define LOGGING_INCLUDE_CODE_LOCATION 1 #endif // *********** END OF USER SETTINGS - Do not change anything below this line *********** #if !(defined(LOGGING_ENABLED) && LOGGING_ENABLED) #undef LOGGING_LEVEL_TRACE #undef LOGGING_LEVEL_INFO #undef LOGGING_LEVEL_ERROR #undef LOGGING_LEVEL_DEBUG #endif #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wgnu-zero-variadic-macro-arguments" // Logging format #define LOG_FORMAT_NO_LOCATION(fmt, lvl, ...) NSLog((@"[%@] " fmt), lvl, ##__VA_ARGS__) #define LOG_FORMAT_WITH_LOCATION(fmt, lvl, ...) NSLog((@"%s[Line %d] [%@] " fmt), __PRETTY_FUNCTION__, __LINE__, lvl, ##__VA_ARGS__) #if defined(LOGGING_INCLUDE_CODE_LOCATION) && LOGGING_INCLUDE_CODE_LOCATION #define LOG_FORMAT(fmt, lvl, ...) LOG_FORMAT_WITH_LOCATION(fmt, lvl, ##__VA_ARGS__) #else #define LOG_FORMAT(fmt, lvl, ...) LOG_FORMAT_NO_LOCATION(fmt, lvl, ##__VA_ARGS__) #endif // Trace logging - for detailed tracing #if defined(LOGGING_LEVEL_TRACE) && LOGGING_LEVEL_TRACE #define LogTrace(fmt, ...) LOG_FORMAT(fmt, @"trace", ##__VA_ARGS__) #else #define LogTrace(...) #endif // Info logging - for general, non-performance affecting information messages #if defined(LOGGING_LEVEL_INFO) && LOGGING_LEVEL_INFO #define LogInfo(fmt, ...) LOG_FORMAT(fmt, @"info", ##__VA_ARGS__) #else #define LogInfo(...) #endif // Error logging - only when there is an error to be logged #if defined(LOGGING_LEVEL_ERROR) && LOGGING_LEVEL_ERROR #define LogError(fmt, ...) LOG_FORMAT(fmt, @"***ERROR***", ##__VA_ARGS__) #else #define LogError(...) #endif // Debug logging - use only temporarily for highlighting and tracking down problems #if defined(LOGGING_LEVEL_DEBUG) && LOGGING_LEVEL_DEBUG #define LogDebug(fmt, ...) LOG_FORMAT(fmt, @"DEBUG", ##__VA_ARGS__) #else #define LogDebug(...) #endif #pragma clang diagnostic pop ================================================ FILE: Pods/Typhoon/Source/ios/Configuration/TyphoonViewControllerInjector.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonComponentFactory.h" @interface TyphoonViewControllerInjector : NSObject /** * Inject properties. * @param viewController View controller. @param factory Typhoon factory. */ - (void)injectPropertiesForViewController:(UIViewController *)viewController withFactory:(id)factory; /** * * Inject properties and check view controller's storyboard is equal to param storyboard. * @param viewController View controller. @param factory Typhoon factory. @param storyboard Storyboard to compare with. */ - (void)injectPropertiesForViewController:(UIViewController *)viewController withFactory:(id)factory storyboard:(UIStoryboard *)storyboard; @end ================================================ FILE: Pods/Typhoon/Source/ios/Configuration/TyphoonViewControllerInjector.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonViewControllerInjector.h" #import "UIViewController+TyphoonStoryboardIntegration.h" #import "UIView+TyphoonDefinitionKey.h" #import "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h" #import //------------------------------------------------------------------------------------------- #pragma mark - TyphoonViewControllerInjector @implementation TyphoonViewControllerInjector + (void)load { [UIViewController swizzleViewDidLoadMethod]; } - (void)injectPropertiesForViewController:(UIViewController *)viewController withFactory:(id)factory { [self injectPropertiesForViewController:viewController withFactory:factory storyboard:nil]; } - (void)injectPropertiesForViewController:(UIViewController *)viewController withFactory:(id)factory storyboard:(UIStoryboard *)storyboard { if (storyboard && viewController.storyboard && ![viewController.storyboard isEqual:storyboard]) { return; } else if (viewController.typhoonKey.length > 0) { [factory inject:viewController withSelector:NSSelectorFromString(viewController.typhoonKey)]; } else { [factory inject:viewController]; } NSArray<__kindof UIViewController *> *childViewControllers = [viewController isKindOfClass:UITabBarController.class] ? ((UITabBarController *)viewController).viewControllers : viewController.childViewControllers; for (UIViewController *controller in childViewControllers) { if (storyboard && controller.storyboard && ![controller.storyboard isEqual:storyboard]) { continue; } [self injectPropertiesForViewController:controller withFactory:factory storyboard:storyboard]; } __weak __typeof (viewController) weakViewController = viewController; [viewController setViewDidLoadNotificationBlock:^{ [self injectPropertiesInView:weakViewController.view withFactory:factory]; }]; } - (void)injectPropertiesInView:(UIView *)view withFactory:(id)factory { if (view.typhoonKey.length > 0) { [factory inject:view withSelector:NSSelectorFromString(view.typhoonKey)]; } if ([view.subviews count] == 0) { return; } for (UIView *subview in view.subviews) { [self injectPropertiesInView:subview withFactory:factory]; } } @end ================================================ FILE: Pods/Typhoon/Source/ios/Definition/TyphoonStoryboardDefinition.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonStoryboardDefinition : TyphoonFactoryDefinition - (id)initWithStoryboardName:(id)storyboardName viewControllerId:(id)viewControllerId; - (id)initWithStoryboard:(UIStoryboard *)storyboard viewControllerId:(id)viewControllerId; @end ================================================ FILE: Pods/Typhoon/Source/ios/Definition/TyphoonStoryboardDefinition.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonStoryboardDefinition.h" #import "TyphoonViewControllerFactory.h" #import "TyphoonStoryboardDefinitionContext.h" #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonAbstractInjection.h" #import "TyphoonInjections.h" #import "TyphoonDefinition+Infrastructure.h" @interface TyphoonStoryboardDefinition () @property (strong, nonatomic) TyphoonStoryboardDefinitionContext *context; @end @implementation TyphoonStoryboardDefinition @synthesize scope = _scope; - (id)targetForInitializerWithFactory:(TyphoonComponentFactory *)factory args:(TyphoonRuntimeArguments *)args { if (self.context.type == TyphoonStoryboardDefinitionByFactoryType) { return [super targetForInitializerWithFactory:factory args:args]; } TyphoonInjectionContext *injectionContext = [[TyphoonInjectionContext alloc] initWithFactory:factory args:args raiseExceptionIfCircular:YES]; UIViewController *result = [TyphoonViewControllerFactory viewControllerWithStoryboardContext:self.context injectionContext:injectionContext factory:factory]; return result; } - (id)initWithStoryboardName:(id)storyboardName viewControllerId:(id)viewControllerId { if (!storyboardName) { [NSException raise:NSInvalidArgumentException format:@"Tried to instantiate ViewController with identifier %@ from the storyboard with unspecified name. This property cannot be nil.", viewControllerId]; } self = [super initWithClass:[NSObject class] key:nil]; if (self) { _context = [TyphoonStoryboardDefinitionContext contextWithStoryboardName:TyphoonMakeInjectionFromObjectIfNeeded(storyboardName) viewControllerId:TyphoonMakeInjectionFromObjectIfNeeded(viewControllerId)]; _scope = TyphoonScopePrototype; } return self; } - (id)initWithStoryboard:(UIStoryboard *)storyboard viewControllerId:(id)viewControllerId { self = [super initWithFactory:storyboard selector:@selector(instantiateViewControllerWithIdentifier:) parameters:^(TyphoonMethod *method) { [method injectParameterWith:TyphoonMakeInjectionFromObjectIfNeeded(viewControllerId)]; }]; if (self) { _context = [TyphoonStoryboardDefinitionContext contextForPreconfiguredStoryboardWithViewControllerId:TyphoonMakeInjectionFromObjectIfNeeded(viewControllerId)]; } return self; } - (TyphoonMethod *)initializer { if (self.context.type == TyphoonStoryboardDefinitionByFactoryType) { return [super initializer]; } return nil; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Definition/TyphoonStoryboardDefinitionContext.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import typedef NS_ENUM(NSUInteger, TyphoonStoryboardDefinitionType) { TyphoonStoryboardDefinitionByNameType = 0, TyphoonStoryboardDefinitionByFactoryType }; @interface TyphoonStoryboardDefinitionContext : NSObject @property (assign, nonatomic, readonly) TyphoonStoryboardDefinitionType type; @property (strong, nonatomic, readonly) id storyboardName; @property (strong, nonatomic, readonly) id viewControllerId; + (instancetype)contextForPreconfiguredStoryboardWithViewControllerId:(id)viewControllerId; + (instancetype)contextWithStoryboardName:(id)storyboardName viewControllerId:(id)viewControllerId; @end ================================================ FILE: Pods/Typhoon/Source/ios/Definition/TyphoonStoryboardDefinitionContext.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonStoryboardDefinitionContext.h" @interface TyphoonStoryboardDefinitionContext () @property (assign, nonatomic, readwrite) TyphoonStoryboardDefinitionType type; @property (strong, nonatomic, readwrite) id storyboardName; @property (strong, nonatomic, readwrite) id viewControllerId; @end @implementation TyphoonStoryboardDefinitionContext - (instancetype)initWithType:(TyphoonStoryboardDefinitionType)type viewControllerId:(id)viewControllerId storyboardName:(id)storyboardName { self = [super init]; if (self) { _type = type; _viewControllerId = viewControllerId; _storyboardName = storyboardName; } return self; } + (instancetype)contextWithStoryboardName:(id)storyboardName viewControllerId:(id)viewControllerId { return [[[self class] alloc] initWithType:TyphoonStoryboardDefinitionByNameType viewControllerId:viewControllerId storyboardName:storyboardName]; } + (instancetype)contextForPreconfiguredStoryboardWithViewControllerId:(id)viewControllerId { return [[[self class] alloc] initWithType:TyphoonStoryboardDefinitionByFactoryType viewControllerId:viewControllerId storyboardName:nil]; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Nib/TyphoonNibLoader.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #ifdef __IPHONE_2_0 #import #import #import "TyphoonComponentFactory.h" /** * TyphoonNibLoader will inject properties for view controller instantiated with nib. * * TyphoonNibLoader injection performed by view controller's type. */ @interface TyphoonNibLoader : NSObject @property (nonatomic, strong) id factory; @property (nonatomic, strong) NSBundle *bundle; + (TyphoonNibLoader *)nibLoaderWithBundle:(NSBundle *)bundleOrNil; + (TyphoonNibLoader *)nibLoaderWithFactory:(id)factory bundle:(NSBundle *)bundleOrNil; /** Instantiates and returns the view controller with the specified nib name. View controller class will be the same as the class by NSClassFromString(nibName). Default class is UIViewController if NSClassFromString(nibName) is nil. @param nibName The view controlelr nib name. @return The view controller created by initWithNibName:bundle:. */ - (__kindof UIViewController *)instantiateViewControllerWithIdentifier:(NSString *)nibName; @end #endif ================================================ FILE: Pods/Typhoon/Source/ios/Nib/TyphoonNibLoader.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonNibLoader.h" #import "TyphoonViewControllerInjector.h" #import "OCLogTemplate.h" @interface TyphoonNibLoader () @property (nonatomic, strong) TyphoonViewControllerInjector *injector; @end @implementation TyphoonNibLoader + (TyphoonNibLoader *)nibLoaderWithBundle:(NSBundle *)bundleOrNil { LogInfo(@"*** Warning *** The TyphoonNibLoader doesn't have a TyphoonComponentFactory inside. Is this " "intentional? You won't be able to inject anything in its ViewControllers"); return [self nibLoaderWithFactory:nil bundle:bundleOrNil]; } + (TyphoonNibLoader *)nibLoaderWithFactory:(id)factory bundle:(NSBundle *)bundleOrNil { TyphoonNibLoader *nibLoader = [[TyphoonNibLoader alloc] init]; nibLoader.factory = factory; nibLoader.bundle = bundleOrNil; nibLoader.injector = [[TyphoonViewControllerInjector alloc] init]; return nibLoader; } - (id)instantiateViewControllerWithIdentifier:(NSString *)nibName { NSAssert(self.factory, @"TyphoonNibLoader's factory property can't be nil!"); Class viewControllerClass = NSClassFromString(nibName); if (!viewControllerClass) { viewControllerClass = [UIViewController class]; LogInfo(@"*** Warning *** NSClassFromString(%@) is nil. UIViewController class will be used instead. " "This can lead to 'this class is not key value coding-compliant for the key' exception.", nibName); } id viewController = [[viewControllerClass alloc] initWithNibName:nibName bundle:self.bundle]; [self.injector injectPropertiesForViewController:viewController withFactory:self.factory]; return viewController; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Nib/TyphoonViewControllerNibResolver.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #ifdef TARGET_OS_IPHONE #import #endif #import "TyphoonDefinitionPostProcessor.h" /** * @ingroup Configuration * Post-Processor that completes the initializer of definitions with type UIViewController. If the definition already has a TyphoonInitializer set, the processor will ignore the component. */ @interface TyphoonViewControllerNibResolver : NSObject /** Resolves the nib name for a view controller class. Defaults to the same name as the class by NSStringFromClass. @param viewControllerClass The class. @return The nib name. */ - (NSString *)resolveNibNameForClass:(Class)viewControllerClass; @end ================================================ FILE: Pods/Typhoon/Source/ios/Nib/TyphoonViewControllerNibResolver.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonViewControllerNibResolver.h" #import "TyphoonDefinition.h" #import "TyphoonComponentFactory.h" #import "TyphoonMethod.h" #import "TyphoonDefinition+Infrastructure.h" @implementation TyphoonViewControllerNibResolver #pragma mark - Protocol methods - (void)postProcessDefinition:(TyphoonDefinition *)definition replacement:(TyphoonDefinition **)definitionToReplace withFactory:(TyphoonComponentFactory *)factory { if ([self shouldProcessDefinition:definition]) { [self processViewControllerDefinition:definition]; } } #pragma mark - Interface methods - (NSString *)resolveNibNameForClass:(Class)viewControllerClass { return NSStringFromClass(viewControllerClass); } //------------------------------------------------------------------------------------------- #pragma mark - Private Methods - (void)processViewControllerDefinition:(TyphoonDefinition *)definition { [definition useInitializer:@selector(initWithNibName:bundle:) parameters:^(TyphoonMethod *initializer) { [initializer injectParameterWith:[self resolveNibNameForClass:definition.type]]; [initializer injectParameterWith:[NSBundle mainBundle]]; }]; } - (BOOL)shouldProcessDefinition:(TyphoonDefinition *)definition { return [definition.type isSubclassOfClass:[UIViewController class]] && definition.initializerGenerated; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/Internal/UIView+TyphoonDefinitionKey.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface UIView (TyphoonDefinitionKey) @property(nonatomic, strong) NSString *typhoonKey; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/Internal/UIView+TyphoonDefinitionKey.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "UIView+TyphoonDefinitionKey.h" #import @implementation UIView (TyphoonDefinitionKey) static const char *kTyphoonKey; - (void)setTyphoonKey:(NSString *)typhoonKey { objc_setAssociatedObject(self, &kTyphoonKey, typhoonKey, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSString *)typhoonKey { return objc_getAssociatedObject(self, &kTyphoonKey); } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/Internal/UIViewController+TyphoonStoryboardIntegration.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface UIViewController (TyphoonStoryboardIntegration) @property(nonatomic, strong) NSString *typhoonKey; - (void)setViewDidLoadNotificationBlock:(void(^)(void))viewDidLoadBlock; + (void)swizzleViewDidLoadMethod; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/Internal/UIViewController+TyphoonStoryboardIntegration.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "UIViewController+TyphoonStoryboardIntegration.h" #import @implementation UIViewController (TyphoonStoryboardIntegration) static const char *kTyphoonKey; static const char *kTyphoonViewDidLoadBlock; - (void)setTyphoonKey:(NSString *)typhoonKey { objc_setAssociatedObject(self, &kTyphoonKey, typhoonKey, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSString *)typhoonKey { return objc_getAssociatedObject(self, &kTyphoonKey); } - (void)setViewDidLoadNotificationBlock:(void(^)(void))viewDidLoadBlock { objc_setAssociatedObject(self, &kTyphoonViewDidLoadBlock, viewDidLoadBlock, OBJC_ASSOCIATION_COPY_NONATOMIC); } + (void)swizzleViewDidLoadMethod { SEL sel = @selector(viewDidLoad); Method method = class_getInstanceMethod([UIViewController class], sel); void(*originalImp)(id, SEL) = (void (*)(id, SEL)) method_getImplementation(method); IMP adjustedImp = imp_implementationWithBlock(^void(id instance) { void(^didLoadBlock)(void) = objc_getAssociatedObject(instance, &kTyphoonViewDidLoadBlock); if (didLoadBlock) { didLoadBlock(); objc_setAssociatedObject(instance, &kTyphoonViewDidLoadBlock, nil, OBJC_ASSOCIATION_COPY_NONATOMIC); } originalImp(instance, sel); }); method_setImplementation(method, adjustedImp); } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/NSLayoutConstraint+TyphoonOutletTransfer.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface NSLayoutConstraint (TyphoonOutletTransfer) // Identifier for compare constraints for transfer @property (nonatomic, strong) NSString *typhoonTransferIdentifier; // Retain constraint from TyphoonLoadedView @property (nonatomic, strong) NSLayoutConstraint *typhoonTransferConstraint; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/NSLayoutConstraint+TyphoonOutletTransfer.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "NSLayoutConstraint+TyphoonOutletTransfer.h" #import @implementation NSLayoutConstraint (TyphoonOutletTransfer) - (void)setTyphoonTransferIdentifier:(NSString *)identifier { objc_setAssociatedObject(self, @selector(typhoonTransferIdentifier), identifier, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSString *)typhoonTransferIdentifier { return objc_getAssociatedObject(self, @selector(typhoonTransferIdentifier)); } - (void)setTyphoonTransferConstraint:(NSLayoutConstraint *)typhoonTransferConstraint { objc_setAssociatedObject(self, @selector(typhoonTransferConstraint), typhoonTransferConstraint, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSLayoutConstraint *)typhoonTransferConstraint { return objc_getAssociatedObject(self, @selector(typhoonTransferConstraint)); } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonComponentFactory+Storyboard.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonComponentFactory (Storyboard) @property (strong, nonatomic, readonly) id storyboardPool; - (id)scopeCachedViewControllerForInstance:(UIViewController *)instance typhoonKey:(NSString *)typhoonKey; - (id)scopeCachedViewControllerForClass:(Class)viewControllerClass typhoonKey:(NSString *)typhoonKey; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonComponentFactory+Storyboard.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonComponentFactory+Storyboard.h" #import "TyphoonComponentFactory+InstanceBuilder.h" #import "TyphoonDefinition.h" #import "TyphoonDefinition+Infrastructure.h" #import "TyphoonStoryboard.h" #import "TyphoonWeakComponentsPool.h" @interface TyphoonComponentFactory (Private) @property(nonatomic, strong) NSString *typhoonKey; - (TyphoonDefinition *)definitionForKey:(NSString *)key; - (void)loadIfNeeded; - (id)poolForDefinition:(TyphoonDefinition *)definition; @end @implementation TyphoonComponentFactory (Storyboard) static const char *kStoryboardPool; - (void)setStoryboardPool:(id)storyboardsPool { objc_setAssociatedObject(self, &kStoryboardPool, storyboardsPool, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (id)storyboardPool { id pool = objc_getAssociatedObject(self, &kStoryboardPool); if (pool == nil) { @synchronized (self) { pool = [TyphoonWeakComponentsPool new]; [self setStoryboardPool:pool]; } } return pool; } - (id)scopeCachedViewControllerForInstance:(UIViewController *)instance typhoonKey:(NSString *)typhoonKey { @synchronized (self) { [self loadIfNeeded]; TyphoonDefinition *definition = [self definitionForInstance:instance typhoonKey:typhoonKey]; return [self cachedInstanceForDefinition:definition]; } } - (id)scopeCachedViewControllerForClass:(Class)viewControllerClass typhoonKey:(NSString *)typhoonKey { @synchronized (self) { [self loadIfNeeded]; id cachedInstance = nil; TyphoonDefinition *keyDefinition = [self definitionForKey:typhoonKey]; if (keyDefinition) { return [self cachedInstanceForDefinition:keyDefinition]; } TyphoonDefinition *classDefinition = [self definitionForClass:viewControllerClass]; if (classDefinition) { return [self cachedInstanceForDefinition:classDefinition]; } return cachedInstance; } } - (TyphoonDefinition *)definitionForClass:(Class)viewControllerClass { if (!viewControllerClass) { return nil; } TyphoonDefinition *definition = [self definitionForType:viewControllerClass orNil:YES includeSubclasses:NO]; return definition; } - (TyphoonDefinition *)definitionForInstance:(UIViewController *)instance typhoonKey:(NSString *)typhoonKey { TyphoonDefinition *definition; if (typhoonKey.length) { definition = [self definitionForKey:typhoonKey]; } else { definition = [self definitionForType:[instance class] orNil:YES includeSubclasses:NO]; } return definition; } - (id)cachedInstanceForDefinition:(TyphoonDefinition *)definition { if (!definition) { return nil; } id pool = [self poolForDefinition:definition]; id cachedInstance = [pool objectForKey:definition.key]; return cachedInstance; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonDefinition+Storyboard.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonDefinition (Storyboard) /** Produces TyphoonDefinition for UIViewController instantiated via storyboard name. Result definition can use the same scopes as usual ones. @param storyboardName The name of the storyboard in the main bundle @param viewControllerId The target ViewController storyboard identifier @return TyphoonDefinition for UIViewController @see withStoryboardName:viewControllerId:configuration: */ + (id)withStoryboardName:(NSString *)storyboardName viewControllerId:(NSString *)viewControllerId; /** Produces TyphoonDefinition for UIViewController instantiated via storyboard name. Result definition can use the same scopes as usual ones. @param storyboardName The name of the storyboard in the main bundle @param viewControllerId The target ViewController storyboard identifier @param injections The definition configuration block @return TyphoonDefinition for UIViewController */ + (id)withStoryboardName:(NSString *)storyboardName viewControllerId:(NSString *)viewControllerId configuration:(TyphoonDefinitionBlock)injections; /** Produces TyphoonDefinition for UIViewController instantiated via storyboard definition. Result definition can use the same scopes as usual ones. @param storyboard The TyphoonDefinition of the storyboard @param viewControllerId The target ViewController storyboard identifier @return TyphoonDefinition for UIViewController @see withStoryboard:viewControllerId:configuration: */ + (id)withStoryboard:(id)storyboard viewControllerId:(NSString *)viewControllerId; /** Produces TyphoonDefinition for UIViewController instantiated via storyboard definition. Result definition can use the same scopes as usual ones. @param storyboard The TyphoonDefinition of the storyboard @param viewControllerId The target ViewController storyboard identifier @param injections The definition configuration block @return TyphoonDefinition for UIViewController */ + (id)withStoryboard:(id)storyboard viewControllerId:(NSString *)viewControllerId configuration:(TyphoonDefinitionBlock)injections; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonDefinition+Storyboard.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonDefinition+Storyboard.h" #import "TyphoonStoryboardDefinition.h" @implementation TyphoonDefinition (Storyboard) + (id)withStoryboardName:(NSString *)storyboardName viewControllerId:(NSString *)viewControllerId { return [self withStoryboardName:storyboardName viewControllerId:viewControllerId configuration:nil]; } + (id)withStoryboardName:(NSString *)storyboardName viewControllerId:(NSString *)viewControllerId configuration:(TyphoonDefinitionBlock)injections { TyphoonStoryboardDefinition *definition = [[TyphoonStoryboardDefinition alloc] initWithStoryboardName:storyboardName viewControllerId:viewControllerId]; if (injections) { injections(definition); } return definition; } + (id)withStoryboard:(id)storyboard viewControllerId:(NSString *)viewControllerId { return [self withStoryboard:storyboard viewControllerId:viewControllerId configuration:nil]; } + (id)withStoryboard:(id)storyboard viewControllerId:(NSString *)viewControllerId configuration:(TyphoonDefinitionBlock)injections { TyphoonStoryboardDefinition *definition = [[TyphoonStoryboardDefinition alloc] initWithStoryboard:storyboard viewControllerId:viewControllerId]; if (injections) { injections(definition); } return definition; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonLoadedView.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import /** * Just drop this view into your Xib and specify definition key as restorationIdentifier * This view will be dynamically replaced with view from definition at runtime. * TyphoonLoadedView's frame, autoresizing mask and constraints would be transferred into view from definition * */ @interface TyphoonLoadedView : UIView @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonLoadedView.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonLoadedView.h" #import "TyphoonViewHelpers.h" #import "UIView+TyphoonOutletTransfer.h" #import @implementation TyphoonLoadedView - (NSString *)typhoonKey { return [self restorationIdentifier]; } - (id)awakeAfterUsingCoder:(NSCoder *)aDecoder { UIView *replacement = [TyphoonViewHelpers viewFromDefinition:[self typhoonKey] originalView:self]; if (replacement != self) { /** * Coupling view loaded from Xib with replacement loaded from Typhoon * to retain view loaded from Xib, to avoid UIKit bug that cause crash. * Reason: Sometimes UIKit sends 'isDescendantOfView:' message during AutoLayout solving * to initially loaded view after it's dealloc. It's strange because this view hasn't superview. * */ objc_setAssociatedObject(replacement, "TyphoonXibPrototype", self, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } replacement.typhoonNeedTransferOutlets = YES; return replacement; } - (void)drawRect:(CGRect)rect { CGContextRef context = UIGraphicsGetCurrentContext(); [[UIColor colorWithWhite:0.93f alpha:1] setFill]; CGContextFillRect(context, self.bounds); UILabel *label = [[UILabel alloc] initWithFrame:self.bounds]; label.lineBreakMode = NSLineBreakByWordWrapping; label.numberOfLines = 0; label.textAlignment = NSTextAlignmentCenter; label.textColor = [UIColor colorWithWhite:0.78f alpha:1]; UIFont *baseFont = [UIFont fontWithName:@"HelveticaNeue-CondensedBold" size:33]; UIFont *subtitleFont = [baseFont fontWithSize:24]; NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString:@"Typhoon Definition\n" attributes:@{NSFontAttributeName : baseFont}]; if ([self typhoonKey]) { [string appendAttributedString:[[NSAttributedString alloc] initWithString:[self typhoonKey] attributes:@{ NSFontAttributeName : subtitleFont }]]; } else { [string appendAttributedString:[[NSAttributedString alloc] initWithString:@"key is missing" attributes:@{ NSFontAttributeName : subtitleFont, NSForegroundColorAttributeName : [UIColor colorWithRed:0.74f green:0.18f blue:0.18f alpha:1.0f] }]]; } label.attributedText = string; [label drawRect:self.bounds]; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonStoryboard.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #ifdef __IPHONE_5_0 #import #import "TyphoonComponentFactory.h" /** * TyphoonStoryboard will inject properties for each view controller created by storyboard. * * Normally, TyphoonStoryboard injection performed by view controller's type. But if you want to specify definition * for view controller injection - use view controller's 'typhoonKey'runtime property. * * To specify 'typhoonKey' in storyboard IB, select your view controller, navigate to 'identity inspector'(cmd+option+3) tab, * section 'User Defined Runtime Attributes'. Add new row with columns: * @code * Key Path : typhoonKey * Type : String * Value : #set your definition selector string here# * @endcode */ @interface TyphoonStoryboard : UIStoryboard @property(nonatomic, strong) id factory; @property(nonatomic, strong) NSString *storyboardName; + (TyphoonStoryboard *)storyboardWithName:(NSString *)name bundle:(NSBundle *)storyboardBundleOrNil; + (TyphoonStoryboard *)storyboardWithName:(NSString *)name factory:(id)factory bundle:(NSBundle *)bundleOrNil; - (UIViewController *)instantiatePrototypeViewControllerWithIdentifier:(NSString *)identifier; @end #endif ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonStoryboard.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonStoryboard.h" #import "OCLogTemplate.h" #import "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h" #import "TyphoonComponentFactory+Storyboard.h" #import "TyphoonViewControllerFactory.h" #import "OCLogTemplate.h" #import "UIViewController+TyphoonStoryboardIntegration.h" @implementation TyphoonStoryboard + (TyphoonStoryboard *)storyboardWithName:(NSString *)name bundle:(NSBundle *)storyboardBundleOrNil { LogInfo(@"*** Warning *** The TyphoonStoryboard with name %@ doesn't have a TyphoonComponentFactory inside. Is this " "intentional? You won't be able to inject anything in its ViewControllers", name); return [self storyboardWithName:name factory:nil bundle:storyboardBundleOrNil]; } + (TyphoonStoryboard *)storyboardWithName:(NSString *)name factory:(id)factory bundle:(NSBundle *)bundleOrNil { TyphoonStoryboard *storyboard = (id) [super storyboardWithName:name bundle:bundleOrNil]; storyboard.factory = factory; storyboard.storyboardName = name; return storyboard; } - (UIViewController *)instantiatePrototypeViewControllerWithIdentifier:(NSString *)identifier { return [super instantiateViewControllerWithIdentifier:identifier]; } - (id)instantiateViewControllerWithIdentifier:(NSString *)identifier { NSAssert(self.factory, @"TyphoonStoryboard's factory property can't be nil!"); UIViewController *cachedInstance = [TyphoonViewControllerFactory cachedViewControllerWithIdentifier:identifier storyboardName:self.storyboardName factory:self.factory]; if (cachedInstance) { return cachedInstance; } UIViewController *result = [TyphoonViewControllerFactory viewControllerWithIdentifier:identifier storyboard:self]; return result; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonStoryboardProvider.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonStoryboardProvider : NSObject - (NSArray *)collectStoryboardsFromBundle:(NSBundle *)bundle; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonStoryboardProvider.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonStoryboardProvider.h" #import "OCLogTemplate.h" @implementation TyphoonStoryboardProvider - (NSArray *)collectStoryboardsFromBundle:(NSBundle *)bundle { NSArray *storyboardPaths = [bundle pathsForResourcesOfType:@"storyboardc" inDirectory:@""]; NSMutableSet *mutableStoryboardNames = [NSMutableSet new]; for (NSString *storyboardPath in storyboardPaths) { NSString *storyboardName = [[storyboardPath lastPathComponent] stringByDeletingPathExtension]; [mutableStoryboardNames addObject:storyboardName]; } NSSet *storyboardNames = [mutableStoryboardNames copy]; storyboardNames = [self filterStoryboards:storyboardNames withBlackListInBundle:bundle]; return [storyboardNames allObjects]; } - (NSSet *)filterStoryboards:(NSSet *)storyboardNames withBlackListInBundle:(NSBundle *)bundle { NSDictionary *bundleInfoDictionary = [bundle infoDictionary]; NSSet *blackListedNames = [NSSet setWithArray:bundleInfoDictionary[@"TyphoonCleanStoryboards"]]; for (NSString *blackListedName in blackListedNames) { if (![storyboardNames containsObject:blackListedName]) { LogInfo(@"*** Warning *** Can't find black-listed storyboard with name %@. Is this intentional?", blackListedName); } } NSMutableSet *filteredStoryboardNames = [storyboardNames mutableCopy]; [filteredStoryboardNames minusSet:blackListedNames]; return [filteredStoryboardNames copy]; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonStoryboardResolver.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonStoryboardResolver : NSObject @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonStoryboardResolver.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonStoryboardResolver.h" #import "TyphoonStartup.h" #import "TyphoonStoryboard.h" #import "TyphoonStoryboardProvider.h" #import "TyphoonComponentFactory+Storyboard.h" #import "TyphoonComponentsPool.h" #import @implementation TyphoonStoryboardResolver + (void)load { NSBundle *bundle = [NSBundle mainBundle]; TyphoonStoryboardProvider *provider = [TyphoonStoryboardProvider new]; NSArray *resolvingStoryboardNames = [provider collectStoryboardsFromBundle:bundle]; if (resolvingStoryboardNames.count > 0) { [self swizzleUIStoryboardWithNames:resolvingStoryboardNames]; } } + (void)swizzleUIStoryboardWithNames:(NSArray *)storyboardNames { SEL sel = @selector(storyboardWithName:bundle:); Method method = class_getClassMethod([UIStoryboard class], sel); id(*originalImp)(id, SEL, id, id) = (id (*)(id, SEL, id, id)) method_getImplementation(method); IMP adjustedImp = imp_implementationWithBlock(^id(id instance, NSString *name, NSBundle *bundle) { id componentFactory = [TyphoonComponentFactory factoryForResolvingUI]; if ([instance class] == [UIStoryboard class] && componentFactory && [storyboardNames containsObject:name]) { TyphoonStoryboard *storyboard = [TyphoonStoryboard storyboardWithName:name factory:componentFactory bundle:bundle]; @synchronized(self) { id storyboardPool = [componentFactory storyboardPool]; [storyboardPool setObject:storyboardPool forKey:name]; } return storyboard; } else { return originalImp(instance, sel, name, bundle); } }); method_setImplementation(method, adjustedImp); } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonViewControllerFactory.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @class TyphoonStoryboardDefinitionContext; @protocol TyphoonComponentFactory; @class TyphoonStoryboard; @class TyphoonInjectionContext; @interface TyphoonViewControllerFactory : NSObject + (UIViewController *)viewControllerWithStoryboardContext:(TyphoonStoryboardDefinitionContext *)context injectionContext:(TyphoonInjectionContext *)injectionContext factory:(id)factory; + (UIViewController *)viewControllerWithIdentifier:(NSString *)identifier storyboard:(TyphoonStoryboard *)storyboard; + (UIViewController *)cachedViewControllerWithIdentifier:(NSString *)identifier storyboardName:(NSString *)storyboardName factory:(id)factory; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonViewControllerFactory.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonViewControllerFactory.h" #import "TyphoonStoryboardDefinitionContext.h" #import "TyphoonComponentFactory+Storyboard.h" #import "TyphoonComponentFactory+TyphoonDefinitionRegisterer.h" #import "UIViewController+TyphoonStoryboardIntegration.h" #import "UIView+TyphoonDefinitionKey.h" #import "TyphoonDefinition+InstanceBuilder.h" #import "TyphoonInjectionContext.h" #import "TyphoonAbstractInjection.h" #import "TyphoonViewControllerInjector.h" #import "TyphoonAssemblyAccessor.h" static NSDictionary *viewControllerClassMap; static NSDictionary *viewControllerTyphoonKeyMap; @implementation TyphoonViewControllerFactory + (NSDictionary *)viewControllerClassMap { if (!viewControllerClassMap) { viewControllerClassMap = @{}; } return viewControllerClassMap; } + (NSDictionary *)viewControllerTyphoonKeyMap { if (!viewControllerTyphoonKeyMap) { viewControllerTyphoonKeyMap = @{}; } return viewControllerTyphoonKeyMap; } + (void)cacheControllerClass:(Class)controllerClass forKey:(NSString *)key { NSMutableDictionary *map = [[self viewControllerClassMap] mutableCopy]; map[key] = controllerClass; viewControllerClassMap = [map copy]; } + (void)cacheTyphoonKey:(NSString *)typhoonKey forKey:(NSString *)key { NSMutableDictionary *map = [[self viewControllerTyphoonKeyMap] mutableCopy]; map[key] = typhoonKey; viewControllerTyphoonKeyMap = [map copy]; } + (TyphoonComponentFactory *)factoryFromFactoryCompatable:(id)factoryCompatible { if ([factoryCompatible isKindOfClass:[TyphoonComponentFactory class]]) { return (id)factoryCompatible; } else if ([factoryCompatible respondsToSelector:@selector(factory)]) { id factory = [(TyphoonAssemblyAccessor *)factoryCompatible factory]; if ([factory isKindOfClass:[TyphoonComponentFactory class]]) { return factory; } } [NSException raise:NSInternalInconsistencyException format:@"Can't TyphoonComponentFactory from %@ instance", factoryCompatible]; return nil; } + (UIViewController *)viewControllerWithStoryboardContext:(TyphoonStoryboardDefinitionContext *)context injectionContext:(TyphoonInjectionContext *)injectionContext factory:(id)factoryCompatible { TyphoonComponentFactory *factory = [self factoryFromFactoryCompatable:factoryCompatible]; id storyboardPool = [factory storyboardPool]; __block NSString *storyboardName = nil; [context.storyboardName valueToInjectWithContext:injectionContext completion:^(id value) { storyboardName = value; }]; UIStoryboard *storyboard = [storyboardPool objectForKey:storyboardName]; if (!storyboard) { storyboard = [TyphoonStoryboard storyboardWithName:storyboardName factory:factory bundle:[NSBundle bundleForClass:[self class]]]; @synchronized(self) { [storyboardPool setObject:storyboard forKey:storyboardName]; } } __block NSString *viewControllerId = nil; [context.viewControllerId valueToInjectWithContext:injectionContext completion:^(id value) { viewControllerId = value; }]; UIViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:viewControllerId]; NSString *key = [self viewControllerMapKeyWithIdentifier:viewControllerId storyboardName:storyboardName]; [self cacheControllerClass:[viewController class] forKey:key]; if (viewController.typhoonKey) { [self cacheTyphoonKey:viewController.typhoonKey forKey:key]; } return viewController; } + (UIViewController *)viewControllerWithIdentifier:(NSString *)identifier storyboard:(TyphoonStoryboard *)storyboard { UIViewController *prototype = [storyboard instantiatePrototypeViewControllerWithIdentifier:identifier]; UIViewController *result = [self configureOrObtainFromPoolViewControllerForInstance:prototype withFactory:storyboard.factory storyboard:storyboard]; NSString *key = [self viewControllerMapKeyWithIdentifier:identifier storyboardName:storyboard.storyboardName]; [self cacheControllerClass:[result class] forKey:key]; if (result.typhoonKey) { [self cacheTyphoonKey:result.typhoonKey forKey:key]; } return result; } + (UIViewController *)cachedViewControllerWithIdentifier:(NSString *)identifier storyboardName:(NSString *)storyboardName factory:(id)factoryCompatible { TyphoonComponentFactory *factory = [self factoryFromFactoryCompatable:factoryCompatible]; NSDictionary *classMap = [self viewControllerClassMap]; NSDictionary *typhoonKeyMap = [self viewControllerTyphoonKeyMap]; NSString *key = [self viewControllerMapKeyWithIdentifier:identifier storyboardName:storyboardName]; Class viewControllerClass = classMap[key]; NSString *typhoonKey = typhoonKeyMap[key]; return [factory scopeCachedViewControllerForClass:viewControllerClass typhoonKey:typhoonKey]; } + (id)configureOrObtainFromPoolViewControllerForInstance:(UIViewController *)instance withFactory:(id)factoryCompatible storyboard:(TyphoonStoryboard *)storyboard { TyphoonComponentFactory *factory = [self factoryFromFactoryCompatable:factoryCompatible]; UIViewController *cachedInstance = [factory scopeCachedViewControllerForInstance:instance typhoonKey:instance.typhoonKey]; if (cachedInstance) { return cachedInstance; } TyphoonViewControllerInjector *injector = [TyphoonViewControllerInjector new]; [injector injectPropertiesForViewController:instance withFactory:factory storyboard:storyboard]; return instance; } + (NSString *)viewControllerMapKeyWithIdentifier:(NSString *)identifier storyboardName:(NSString *)storyboardName { return [NSString stringWithFormat:@"%@-%@", storyboardName, identifier]; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonViewHelpers.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface TyphoonViewHelpers : NSObject + (id)viewFromDefinition:(NSString *)definitionKey originalView:(UIView *)original; + (void)transferPropertiesFromView:(UIView *)src toView:(UIView *)dst; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/TyphoonViewHelpers.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2015, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonViewHelpers.h" #import "TyphoonStoryboard.h" #import "NSLayoutConstraint+TyphoonOutletTransfer.h" #import "OCLogTemplate.h" @implementation TyphoonViewHelpers + (id)viewFromDefinition:(NSString *)definitionKey originalView:(UIView *)original { if ([[original subviews] count] > 0) { LogInfo(@"Warning: placeholder view contains (%d) subviews. They will be replaced by typhoon definition '%@'", (int)[[original subviews] count], definitionKey); } TyphoonComponentFactory *currentFactory = [TyphoonComponentFactory factoryForResolvingUI]; if (!currentFactory) { [NSException raise:NSInternalInconsistencyException format:@"Can't find Typhoon factory to resolve definition from xib. Check [TyphoonComponentFactory setFactoryForResolvingUI:] method."]; } id result = [currentFactory componentForKey:definitionKey]; if (![result isKindOfClass:[UIView class]]) { [NSException raise:NSInternalInconsistencyException format:@"Error: definition for key '%@' is not kind of UIView but %@", definitionKey, result]; } [self transferPropertiesFromView:original toView:result]; return result; } + (void)transferPropertiesFromView:(UIView *)src toView:(UIView *)dst { //Transferring autolayout dst.translatesAutoresizingMaskIntoConstraints = src.translatesAutoresizingMaskIntoConstraints; for (NSLayoutConstraint *constraint in src.constraints) { BOOL replaceFirstItem = [constraint firstItem] == src; BOOL replaceSecondItem = [constraint secondItem] == src; id firstItem = replaceFirstItem ? dst : constraint.firstItem; id secondItem = replaceSecondItem ? dst : constraint.secondItem; NSLayoutConstraint *newConstraint = [NSLayoutConstraint constraintWithItem:firstItem attribute:constraint.firstAttribute relatedBy:constraint.relation toItem:secondItem attribute:constraint.secondAttribute multiplier:constraint.multiplier constant:constraint.constant]; newConstraint.priority = constraint.priority; NSString *typhoonTransferIdentifier = [[NSUUID UUID] UUIDString]; constraint.typhoonTransferIdentifier = typhoonTransferIdentifier; newConstraint.typhoonTransferIdentifier = typhoonTransferIdentifier; newConstraint.typhoonTransferConstraint = constraint; [dst addConstraint:newConstraint]; } dst.frame = src.frame; dst.autoresizesSubviews = src.autoresizesSubviews; dst.autoresizingMask = src.autoresizingMask; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/UIResponder+TyphoonOutletTransfer.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface UIResponder (TyphoonOutletTransfer) - (void)transferConstraintsFromView:(UIView *)view; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/UIResponder+TyphoonOutletTransfer.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "UIResponder+TyphoonOutletTransfer.h" #import "NSLayoutConstraint+TyphoonOutletTransfer.h" #import "TyphoonIntrospectionUtils.h" #import "TyphoonTypeDescriptor.h" @implementation UIResponder (TyphoonOutletTransfer) - (void)transferConstraintsFromView:(UIView *)view { Class currentClass = [self class]; NSSet *properties = [TyphoonIntrospectionUtils propertiesForClass:currentClass upToParentClass:[NSObject class]]; for (NSString *propertyName in properties) { TyphoonTypeDescriptor *type = [TyphoonIntrospectionUtils typeForPropertyNamed:propertyName inClass:currentClass]; SEL setter = [TyphoonIntrospectionUtils setterForPropertyWithName:propertyName inClass:currentClass]; if (setter) { // IBOutlet if (type.typeBeingDescribed == [NSLayoutConstraint class]) { [self transferConstraintOutletForKey:propertyName fromView:view]; } // IBOutlet​Collection if ([type.typeBeingDescribed isSubclassOfClass:[NSArray class]]) { [self transferConstraintOutletsForKey:propertyName fromView:view]; } } } } - (void)transferConstraintOutletForKey:(NSString *)propertyName fromView:(UIView *)view { NSLayoutConstraint *constraint = [self valueForKey:propertyName]; if (constraint.typhoonTransferIdentifier) { for (NSLayoutConstraint *transferConstraint in view.constraints) { BOOL equalObjects = constraint == transferConstraint; BOOL equalIdentifier = [constraint.typhoonTransferIdentifier isEqualToString:transferConstraint.typhoonTransferIdentifier]; if (!equalObjects && equalIdentifier) { [self setValue:transferConstraint forKey:propertyName]; } } } } - (void)transferConstraintOutletsForKey:(NSString *)propertyName fromView:(UIView *)view { NSArray *constraints = [self valueForKey:propertyName]; if ([self isOutletCollection:constraints]) { BOOL needChange = NO; NSMutableArray *newOutlets = [NSMutableArray new]; for (NSLayoutConstraint *constraint in constraints) { NSLayoutConstraint *transferConstraint = [self transferConstraint:constraint fromView:view]; if (transferConstraint) { needChange = YES; } transferConstraint = transferConstraint ? transferConstraint : constraint; [newOutlets addObject:transferConstraint]; } if (needChange) { [self setValue:newOutlets forKey:propertyName]; } } } - (NSLayoutConstraint *)transferConstraint:(NSLayoutConstraint *)transferConstraint fromView:(UIView *)view { if (!transferConstraint.typhoonTransferIdentifier) { return nil; } for (NSLayoutConstraint *constraint in view.constraints) { BOOL equalObjects = constraint == transferConstraint; BOOL equalIdentifier = [constraint.typhoonTransferIdentifier isEqualToString:transferConstraint.typhoonTransferIdentifier]; if (!equalObjects && equalIdentifier) { return constraint; } } return nil; } - (BOOL)isOutletCollection:(NSArray *)array { NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self isKindOfClass: %@", [NSLayoutConstraint class]]; NSArray *filtered = [array filteredArrayUsingPredicate:predicate]; return filtered.count == array.count; } @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/UIView+TyphoonOutletTransfer.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import @interface UIView (TyphoonOutletTransfer) // Flag to check whether the outlets constraint transportation needs @property (nonatomic, assign) BOOL typhoonNeedTransferOutlets; @end ================================================ FILE: Pods/Typhoon/Source/ios/Storyboard/UIView+TyphoonOutletTransfer.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2016, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "UIView+TyphoonOutletTransfer.h" #import "UIResponder+TyphoonOutletTransfer.h" #import @implementation UIView (TyphoonOutletTransfer) - (void)setTyphoonNeedTransferOutlets:(BOOL)typhoonNeedTransferOutlets { objc_setAssociatedObject(self, @selector(typhoonNeedTransferOutlets), @(typhoonNeedTransferOutlets), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)typhoonNeedTransferOutlets { return [objc_getAssociatedObject(self, @selector(typhoonNeedTransferOutlets)) boolValue]; } // Swizzle awakeFromNib // After the [super awakeFromNib] all the outlets on view will be set + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(awakeFromNib); SEL swizzledSelector = @selector(typhoon_awakeFromNib); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } }); } - (void)typhoon_awakeFromNib { [self typhoon_awakeFromNib]; // When view have superview transfer outlets if needed if (self.typhoonNeedTransferOutlets) { // recursive search for root view (superview without superview) UIView *rootView = [self findRootView:self]; // Change UIViewController outlets properties UIResponder *nextRexponder = [rootView nextResponder]; if ([nextRexponder isKindOfClass:[UIViewController class]]) { [nextRexponder transferConstraintsFromView:self]; } // recursive check and change super outlets properties [self transferOutlets:rootView transferView:self]; // Mark that the transportation of finished self.typhoonNeedTransferOutlets = NO; } } - (void)transferOutlets:(UIView *)view transferView:(UIView *)transferView { [view transferConstraintsFromView:transferView]; // Optimization. The outlet from view to the subview of TyphoonLoadedView is invalid. if (view == transferView) { return; } for (UIView *subview in view.subviews) { [subview transferOutlets:subview transferView:transferView]; } } - (UIView *)findRootView:(UIView *)view { NSArray *expulsionViewClasses = [self expulsionViewClasses]; // Optimization. The outlet from view to the UICollectionViewCell is invalid. // Outlets cannot be connected to repeating content. for (Class expulsionClass in expulsionViewClasses) { if ([view isKindOfClass:expulsionClass]) { return view; } } UIResponder *nextRexponder = [view nextResponder]; if ([nextRexponder isKindOfClass:[UIViewController class]]) { return view; } if (view.superview) { return [view.superview findRootView:view.superview]; } return view; } - (NSArray *)expulsionViewClasses { return @[[UITableViewCell class], [UICollectionViewCell class]]; } @end ================================================ FILE: Pods/Typhoon/Source/ios/TypeConversion/Converters/TyphoonBundledImageTypeConverter.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import #import "TyphoonTypeConverter.h" @interface TyphoonBundledImageTypeConverter : NSObject @end ================================================ FILE: Pods/Typhoon/Source/ios/TypeConversion/Converters/TyphoonBundledImageTypeConverter.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonBundledImageTypeConverter.h" #import @implementation TyphoonBundledImageTypeConverter - (id)supportedType { return @"UIImage"; } - (id)convert:(NSString *)stringValue { stringValue = [TyphoonTypeConversionUtils textWithoutTypeFromTextValue:stringValue]; __autoreleasing UIImage *image = [UIImage imageNamed:stringValue]; return image; } @end ================================================ FILE: Pods/Typhoon/Source/ios/TypeConversion/Converters/TyphoonUIColorTypeConverter.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonTypeConverter.h" /** * A type converter for UIColor. * * The formats supported are: * Hexadecimal, #RRGGBB or #AARRGGBB * Css-style, rgb(r,g,b) or rgba(r,g,b,a) * */ @interface TyphoonUIColorTypeConverter : NSObject @end ================================================ FILE: Pods/Typhoon/Source/ios/TypeConversion/Converters/TyphoonUIColorTypeConverter.m ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonUIColorTypeConverter.h" #import "TyphoonColorConversionUtils.h" #import @implementation TyphoonUIColorTypeConverter - (id)supportedType { return @"UIColor"; } - (id)convert:(NSString *)stringValue { stringValue = [TyphoonTypeConversionUtils textWithoutTypeFromTextValue:stringValue]; struct RGBA color; if ([stringValue hasPrefix:@"#"] || [stringValue hasPrefix:@"0x"]) { color = [TyphoonColorConversionUtils colorFromHexString:stringValue]; } else { color = [TyphoonColorConversionUtils colorFromCssStyleString:stringValue]; } return [self colorFromRGBA:color]; } - (UIColor *)colorFromRGBA:(struct RGBA)rgba { return [UIColor colorWithRed:rgba.red green:rgba.green blue:rgba.blue alpha:rgba.alpha]; } @end ================================================ FILE: Pods/Typhoon/Source/ios/TyphooniOS.h ================================================ //////////////////////////////////////////////////////////////////////////////// // // TYPHOON FRAMEWORK // Copyright 2013, Typhoon Framework Contributors // All Rights Reserved. // // NOTICE: The authors permit you to use, modify, and distribute this file // in accordance with the terms of the license agreement accompanying it. // //////////////////////////////////////////////////////////////////////////////// #import "TyphoonStoryboard.h" #import "TyphoonNibLoader.h" #import "TyphoonBundledImageTypeConverter.h" #import "TyphoonLoadedView.h" ================================================ FILE: Pods/YYModel/LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 ibireme Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Pods/YYModel/README.md ================================================ YYModel ============== [![License MIT](https://img.shields.io/badge/license-MIT-green.svg?style=flat)](https://raw.githubusercontent.com/ibireme/YYModel/master/LICENSE)  [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)  [![CocoaPods](http://img.shields.io/cocoapods/v/YYModel.svg?style=flat)](http://cocoapods.org/?q= YYModel)  [![CocoaPods](http://img.shields.io/cocoapods/p/YYModel.svg?style=flat)](http://cocoapods.org/?q= YYModel)  [![Build Status](https://travis-ci.org/ibireme/YYModel.svg?branch=master)](https://travis-ci.org/ibireme/YYModel)  [![codecov.io](https://codecov.io/github/ibireme/YYModel/coverage.svg?branch=master)](https://codecov.io/github/ibireme/YYModel?branch=master) High performance model framework for iOS/OSX.
(It's a component of [YYKit](https://github.com/ibireme/YYKit)) Performance ============== Time cost (process GithubUser 10000 times on iPhone 6): ![Benchmark result](https://raw.github.com/ibireme/YYModel/master/Benchmark/Result.png ) See `Benchmark/ModelBenchmark.xcodeproj` for more benchmark case. Features ============== - **High performance**: The conversion performance is close to handwriting code. - **Automatic type conversion**: The object types can be automatically converted. - **Type Safe**: All data types will be verified to ensure type-safe during the conversion process. - **Non-intrusive**: There is no need to make the model class inherit from other base class. - **Lightwight**: This library contains only 5 files. - **Docs and unit testing**: 100% docs coverage, 99.6% code coverage. Usage ============== ###Simple model json convert // JSON: { "uid":123456, "name":"Harry", "created":"1965-07-31T00:00:00+0000" } // Model: @interface User : NSObject @property UInt64 uid; @property NSString *name; @property NSDate *created; @end @implementation User @end // Convert json to model: User *user = [User yy_modelWithJSON:json]; // Convert model to json: NSDictionary *json = [user yy_modelToJSONObject]; If the type of an object in JSON/Dictionary cannot be matched to the property of the model, the following automatic conversion is performed. If the automatic conversion failed, the value will be ignored.
JSON/Dictionary Model
NSString NSNumber,NSURL,SEL,Class
NSNumber NSString
NSString/NSNumber C number (BOOL,int,float,NSUInteger,UInt64,...)
NaN and Inf will be ignored
NSString NSDate parsed with these formats:
yyyy-MM-dd
yyyy-MM-dd HH:mm:ss
yyyy-MM-dd'T'HH:mm:ss
yyyy-MM-dd'T'HH:mm:ssZ
EEE MMM dd HH:mm:ss Z yyyy
NSDate NSString formatted with ISO8601:
"YYYY-MM-dd'T'HH:mm:ssZ"
NSValue struct (CGRect,CGSize,...)
NSNull nil,0
"no","false",... @(NO),0
"yes","true",... @(YES),1
###Match model property to different JSON key // JSON: { "n":"Harry Pottery", "p": 256, "ext" : { "desc" : "A book written by J.K.Rowing." }, "ID" : 100010 } // Model: @interface Book : NSObject @property NSString *name; @property NSInteger page; @property NSString *desc; @property NSString *bookID; @end @implementation Book + (NSDictionary *)modelCustomPropertyMapper { return @{@"name" : @"n", @"page" : @"p", @"desc" : @"ext.desc", @"bookID" : @[@"id",@"ID",@"book_id"]}; } @end You can map a json key (key path) or an array of json key (key path) to one or multiple property name. If there's no mapper for a property, it will use the property's name as default. ###Nested model // JSON { "author":{ "name":"J.K.Rowling", "birthday":"1965-07-31T00:00:00+0000" }, "name":"Harry Potter", "pages":256 } // Model: (no need to do anything) @interface Author : NSObject @property NSString *name; @property NSDate *birthday; @end @implementation Author @end @interface Book : NSObject @property NSString *name; @property NSUInteger pages; @property Author *author; @end @implementation Book @end ### Container property @class Shadow, Border, Attachment; @interface Attributes @property NSString *name; @property NSArray *shadows; //Array @property NSSet *borders; //Set @property NSMutableDictionary *attachments; //Dict @end @implementation Attributes + (NSDictionary *)modelContainerPropertyGenericClass { // value should be Class or Class name. return @{@"shadows" : [Shadow class], @"borders" : Border.class, @"attachments" : @"Attachment" }; } @end ### Whitelist and blacklist @interface User @property NSString *name; @property NSUInteger age; @end @implementation Attributes + (NSArray *)modelPropertyBlacklist { return @[@"test1", @"test2"]; } + (NSArray *)modelPropertyWhitelist { return @[@"name"]; } @end ###Data validate and custom transform // JSON: { "name":"Harry", "timestamp" : 1445534567 } // Model: @interface User @property NSString *name; @property NSDate *createdAt; @end @implementation User - (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic { NSNumber *timestamp = dic[@"timestamp"]; if (![timestamp isKindOfClass:[NSNumber class]]) return NO; _createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue]; return YES; } - (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic { if (!_createdAt) return NO; dic[@"timestamp"] = @(n.timeIntervalSince1970); return YES; } @end ###Coding/Copying/hash/equal/description @interface YYShadow :NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) CGSize size; @end @implementation YYShadow - (void)encodeWithCoder:(NSCoder *)aCoder { [self yy_modelEncodeWithCoder:aCoder]; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; return [self yy_modelInitWithCoder:aDecoder]; } - (id)copyWithZone:(NSZone *)zone { return [self yy_modelCopy]; } - (NSUInteger)hash { return [self yy_modelHash]; } - (BOOL)isEqual:(id)object { return [self yy_modelIsEqual:object]; } - (NSString *)description { return [self yy_modelDescription]; } @end Installation ============== ### CocoaPods 1. Add `pod 'YYModel'` to your Podfile. 2. Run `pod install` or `pod update`. 3. Import \. ### Carthage 1. Add `github "ibireme/YYModel"` to your Cartfile. 2. Run `carthage update --platform ios` and add the framework to your project. 3. Import \. ### Manually 1. Download all the files in the YYModel subdirectory. 2. Add the source files to your Xcode project. 3. Import `YYModel.h`. Documentation ============== Full API documentation is available on [CocoaDocs](http://cocoadocs.org/docsets/YYModel/).
You can also install documentation locally using [appledoc](https://github.com/tomaz/appledoc). Requirements ============== This library requires `iOS 6.0+` and `Xcode 7.0+`. License ============== YYModel is provided under the MIT license. See LICENSE file for details.

--- 中文介绍 ============== 高性能 iOS/OSX 模型转换框架。
(该项目是 [YYKit](https://github.com/ibireme/YYKit) 组件之一) 性能 ============== 处理 GithubUser 数据 10000 次耗时统计 (iPhone 6): ![Benchmark result](https://raw.github.com/ibireme/YYModel/master/Benchmark/Result.png ) 更多测试代码和用例见 `Benchmark/ModelBenchmark.xcodeproj`。 特性 ============== - **高性能**: 模型转换性能接近手写解析代码。 - **自动类型转换**: 对象类型可以自动转换,详情见下方表格。 - **类型安全**: 转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。 - **无侵入性**: 模型无需继承自其他基类。 - **轻量**: 该框架只有 5 个文件 (包括.h文件)。 - **文档和单元测试**: 文档覆盖率100%, 代码覆盖率99.6%。 使用方法 ============== ###简单的 Model 与 JSON 相互转换 // JSON: { "uid":123456, "name":"Harry", "created":"1965-07-31T00:00:00+0000" } // Model: @interface User : NSObject @property UInt64 uid; @property NSString *name; @property NSDate *created; @end @implementation User @end // 将 JSON (NSData,NSString,NSDictionary) 转换为 Model: User *user = [User yy_modelWithJSON:json]; // 将 Model 转换为 JSON 对象: NSDictionary *json = [user yy_modelToJSONObject]; 当 JSON/Dictionary 中的对象类型与 Model 属性不一致时,YYModel 将会进行如下自动转换。自动转换不支持的值将会被忽略,以避免各种潜在的崩溃问题。
JSON/Dictionary Model
NSString NSNumber,NSURL,SEL,Class
NSNumber NSString
NSString/NSNumber 基础类型 (BOOL,int,float,NSUInteger,UInt64,...)
NaN 和 Inf 会被忽略
NSString NSDate 以下列格式解析:
yyyy-MM-dd
yyyy-MM-dd HH:mm:ss
yyyy-MM-dd'T'HH:mm:ss
yyyy-MM-dd'T'HH:mm:ssZ
EEE MMM dd HH:mm:ss Z yyyy
NSDate NSString 格式化为 ISO8601:
"YYYY-MM-dd'T'HH:mm:ssZ"
NSValue struct (CGRect,CGSize,...)
NSNull nil,0
"no","false",... @(NO),0
"yes","true",... @(YES),1
###Model 属性名和 JSON 中的 Key 不相同 // JSON: { "n":"Harry Pottery", "p": 256, "ext" : { "desc" : "A book written by J.K.Rowing." }, "ID" : 100010 } // Model: @interface Book : NSObject @property NSString *name; @property NSInteger page; @property NSString *desc; @property NSString *bookID; @end @implementation Book //返回一个 Dict,将 Model 属性名对映射到 JSON 的 Key。 + (NSDictionary *)modelCustomPropertyMapper { return @{@"name" : @"n", @"page" : @"p", @"desc" : @"ext.desc", @"bookID" : @[@"id",@"ID",@"book_id"]}; } @end 你可以把一个或一组 json key (key path) 映射到一个或多个属性。如果一个属性没有映射关系,那默认会使用相同属性名作为映射。 在 json->model 的过程中:如果一个属性对应了多个 json key,那么转换过程会按顺序查找,并使用第一个不为空的值。 在 model->json 的过程中:如果一个属性对应了多个 json key (key path),那么转换过程仅会处理第一个 json key (key path);如果多个属性对应了同一个 json key,则转换过过程会使用其中任意一个不为空的值。 ###Model 包含其他 Model // JSON { "author":{ "name":"J.K.Rowling", "birthday":"1965-07-31T00:00:00+0000" }, "name":"Harry Potter", "pages":256 } // Model: 什么都不用做,转换会自动完成 @interface Author : NSObject @property NSString *name; @property NSDate *birthday; @end @implementation Author @end @interface Book : NSObject @property NSString *name; @property NSUInteger pages; @property Author *author; //Book 包含 Author 属性 @end @implementation Book @end ###容器类属性 @class Shadow, Border, Attachment; @interface Attributes @property NSString *name; @property NSArray *shadows; //Array @property NSSet *borders; //Set @property NSMutableDictionary *attachments; //Dict @end @implementation Attributes // 返回容器类中的所需要存放的数据类型 (以 Class 或 Class Name 的形式)。 + (NSDictionary *)modelContainerPropertyGenericClass { return @{@"shadows" : [Shadow class], @"borders" : Border.class, @"attachments" : @"Attachment" }; } @end ###黑名单与白名单 @interface User @property NSString *name; @property NSUInteger age; @end @implementation Attributes // 如果实现了该方法,则处理过程中会忽略该列表内的所有属性 + (NSArray *)modelPropertyBlacklist { return @[@"test1", @"test2"]; } // 如果实现了该方法,则处理过程中不会处理该列表外的属性。 + (NSArray *)modelPropertyWhitelist { return @[@"name"]; } @end ###数据校验与自定义转换 // JSON: { "name":"Harry", "timestamp" : 1445534567 } // Model: @interface User @property NSString *name; @property NSDate *createdAt; @end @implementation User // 当 JSON 转为 Model 完成后,该方法会被调用。 // 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。 // 你也可以在这里做一些自动转换不能完成的工作。 - (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic { NSNumber *timestamp = dic[@"timestamp"]; if (![timestamp isKindOfClass:[NSNumber class]]) return NO; _createdAt = [NSDate dateWithTimeIntervalSince1970:timestamp.floatValue]; return YES; } // 当 Model 转为 JSON 完成后,该方法会被调用。 // 你可以在这里对数据进行校验,如果校验不通过,可以返回 NO,则该 Model 会被忽略。 // 你也可以在这里做一些自动转换不能完成的工作。 - (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic { if (!_createdAt) return NO; dic[@"timestamp"] = @(n.timeIntervalSince1970); return YES; } @end ###Coding/Copying/hash/equal/description @interface YYShadow :NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) CGSize size; @end @implementation YYShadow // 直接添加以下代码即可自动完成 - (void)encodeWithCoder:(NSCoder *)aCoder { [self yy_modelEncodeWithCoder:aCoder]; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; return [self yy_modelInitWithCoder:aDecoder]; } - (id)copyWithZone:(NSZone *)zone { return [self yy_modelCopy]; } - (NSUInteger)hash { return [self yy_modelHash]; } - (BOOL)isEqual:(id)object { return [self yy_modelIsEqual:object]; } - (NSString *)description { return [self yy_modelDescription]; } @end 安装 ============== ### CocoaPods 1. 在 Podfile 中添加 `pod 'YYModel'`。 2. 执行 `pod install` 或 `pod update`。 3. 导入 \。 ### Carthage 1. 在 Cartfile 中添加 `github "ibireme/YYModel"`。 2. 执行 `carthage update --platform ios` 并将生成的 framework 添加到你的工程。 3. 导入 \。 ### 手动安装 1. 下载 YYModel 文件夹内的所有内容。 2. 将 YYModel 内的源文件添加(拖放)到你的工程。 3. 导入 `YYModel.h`。 文档 ============== 你可以在 [CocoaDocs](http://cocoadocs.org/docsets/YYModel/) 查看在线 API 文档,也可以用 [appledoc](https://github.com/tomaz/appledoc) 本地生成文档。 系统要求 ============== 该项目最低支持 `iOS 6.0` 和 `Xcode 7.0`。 许可证 ============== YYModel 使用 MIT 许可证,详情见 LICENSE 文件。 相关链接 ============== [iOS JSON 模型转换库评测](http://blog.ibireme.com/2015/10/23/ios_model_framework_benchmark/) ================================================ FILE: Pods/YYModel/YYModel/NSObject+YYModel.h ================================================ // // NSObject+YYModel.h // YYModel // // Created by ibireme on 15/5/10. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import NS_ASSUME_NONNULL_BEGIN /** Provide some data-model method: * Convert json to any object, or convert any object to json. * Set object properties with a key-value dictionary (like KVC). * Implementations of `NSCoding`, `NSCopying`, `-hash` and `-isEqual:`. See `YYModel` protocol for custom methods. Sample Code: ********************** json convertor ********************* @interface YYAuthor : NSObject @property (nonatomic, strong) NSString *name; @property (nonatomic, assign) NSDate *birthday; @end @implementation YYAuthor @end @interface YYBook : NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) NSUInteger pages; @property (nonatomic, strong) YYAuthor *author; @end @implementation YYBook @end int main() { // create model from json YYBook *book = [YYBook yy_modelWithJSON:@"{\"name\": \"Harry Potter\", \"pages\": 256, \"author\": {\"name\": \"J.K.Rowling\", \"birthday\": \"1965-07-31\" }}"]; // convert model to json NSString *json = [book yy_modelToJSONString]; // {"author":{"name":"J.K.Rowling","birthday":"1965-07-31T00:00:00+0000"},"name":"Harry Potter","pages":256} } ********************** Coding/Copying/hash/equal ********************* @interface YYShadow :NSObject @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) CGSize size; @end @implementation YYShadow - (void)encodeWithCoder:(NSCoder *)aCoder { [self yy_modelEncodeWithCoder:aCoder]; } - (id)initWithCoder:(NSCoder *)aDecoder { self = [super init]; return [self yy_modelInitWithCoder:aDecoder]; } - (id)copyWithZone:(NSZone *)zone { return [self yy_modelCopy]; } - (NSUInteger)hash { return [self yy_modelHash]; } - (BOOL)isEqual:(id)object { return [self yy_modelIsEqual:object]; } @end */ @interface NSObject (YYModel) /** Creates and returns a new instance of the receiver from a json. This method is thread-safe. @param json A json object in `NSDictionary`, `NSString` or `NSData`. @return A new instance created from the json, or nil if an error occurs. */ + (nullable instancetype)yy_modelWithJSON:(id)json; /** Creates and returns a new instance of the receiver from a key-value dictionary. This method is thread-safe. @param dictionary A key-value dictionary mapped to the instance's properties. Any invalid key-value pair in dictionary will be ignored. @return A new instance created from the dictionary, or nil if an error occurs. @discussion The key in `dictionary` will mapped to the reciever's property name, and the value will set to the property. If the value's type does not match the property, this method will try to convert the value based on these rules: `NSString` or `NSNumber` -> c number, such as BOOL, int, long, float, NSUInteger... `NSString` -> NSDate, parsed with format "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd HH:mm:ss" or "yyyy-MM-dd". `NSString` -> NSURL. `NSValue` -> struct or union, such as CGRect, CGSize, ... `NSString` -> SEL, Class. */ + (nullable instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary; /** Set the receiver's properties with a json object. @discussion Any invalid data in json will be ignored. @param json A json object of `NSDictionary`, `NSString` or `NSData`, mapped to the receiver's properties. @return Whether succeed. */ - (BOOL)yy_modelSetWithJSON:(id)json; /** Set the receiver's properties with a key-value dictionary. @param dic A key-value dictionary mapped to the receiver's properties. Any invalid key-value pair in dictionary will be ignored. @discussion The key in `dictionary` will mapped to the reciever's property name, and the value will set to the property. If the value's type doesn't match the property, this method will try to convert the value based on these rules: `NSString`, `NSNumber` -> c number, such as BOOL, int, long, float, NSUInteger... `NSString` -> NSDate, parsed with format "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd HH:mm:ss" or "yyyy-MM-dd". `NSString` -> NSURL. `NSValue` -> struct or union, such as CGRect, CGSize, ... `NSString` -> SEL, Class. @return Whether succeed. */ - (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic; /** Generate a json object from the receiver's properties. @return A json object in `NSDictionary` or `NSArray`, or nil if an error occurs. See [NSJSONSerialization isValidJSONObject] for more information. @discussion Any of the invalid property is ignored. If the reciver is `NSArray`, `NSDictionary` or `NSSet`, it just convert the inner object to json object. */ - (nullable id)yy_modelToJSONObject; /** Generate a json string's data from the receiver's properties. @return A json string's data, or nil if an error occurs. @discussion Any of the invalid property is ignored. If the reciver is `NSArray`, `NSDictionary` or `NSSet`, it will also convert the inner object to json string. */ - (nullable NSData *)yy_modelToJSONData; /** Generate a json string from the receiver's properties. @return A json string, or nil if an error occurs. @discussion Any of the invalid property is ignored. If the reciver is `NSArray`, `NSDictionary` or `NSSet`, it will also convert the inner object to json string. */ - (nullable NSString *)yy_modelToJSONString; /** Copy a instance with the receiver's properties. @return A copied instance, or nil if an error occurs. */ - (nullable id)yy_modelCopy; /** Encode the receiver's properties to a coder. @param aCoder An archiver object. */ - (void)yy_modelEncodeWithCoder:(NSCoder *)aCoder; /** Decode the receiver's properties from a decoder. @param aDecoder An archiver object. @return self */ - (id)yy_modelInitWithCoder:(NSCoder *)aDecoder; /** Get a hash code with the receiver's properties. @return Hash code. */ - (NSUInteger)yy_modelHash; /** Compares the receiver with another object for equality, based on properties. @param model Another object. @return `YES` if the reciever is equal to the object, otherwise `NO`. */ - (BOOL)yy_modelIsEqual:(id)model; /** Description method for debugging purposes based on properties. @return A string that describes the contents of the receiver. */ - (NSString *)yy_modelDescription; @end /** Provide some data-model method for NSArray. */ @interface NSArray (YYModel) /** Creates and returns an array from a json-array. This method is thread-safe. @param cls The instance's class in array. @param json A json array of `NSArray`, `NSString` or `NSData`. Example: [{"name","Mary"},{name:"Joe"}] @return A array, or nil if an error occurs. */ + (nullable NSArray *)yy_modelArrayWithClass:(Class)cls json:(id)json; @end /** Provide some data-model method for NSDictionary. */ @interface NSDictionary (YYModel) /** Creates and returns a dictionary from a json. This method is thread-safe. @param cls The value instance's class in dictionary. @param json A json dictionary of `NSDictionary`, `NSString` or `NSData`. Example: {"user1":{"name","Mary"}, "user2": {name:"Joe"}} @return A dictionary, or nil if an error occurs. */ + (nullable NSDictionary *)yy_modelDictionaryWithClass:(Class)cls json:(id)json; @end /** If the default model transform does not fit to your model class, implement one or more method in this protocol to change the default key-value transform process. There's no need to add '' to your class header. */ @protocol YYModel @optional /** Custom property mapper. @discussion If the key in JSON/Dictionary does not match to the model's property name, implements this method and returns the additional mapper. Example: json: { "n":"Harry Pottery", "p": 256, "ext" : { "desc" : "A book written by J.K.Rowling." }, "ID" : 100010 } model: @interface YYBook : NSObject @property NSString *name; @property NSInteger page; @property NSString *desc; @property NSString *bookID; @end @implementation YYBook + (NSDictionary *)modelCustomPropertyMapper { return @{@"name" : @"n", @"page" : @"p", @"desc" : @"ext.desc", @"bookID": @[@"id", @"ID", @"book_id"]}; } @end @return A custom mapper for properties. */ + (nullable NSDictionary *)modelCustomPropertyMapper; /** The generic class mapper for container properties. @discussion If the property is a container object, such as NSArray/NSSet/NSDictionary, implements this method and returns a property->class mapper, tells which kind of object will be add to the array/set/dictionary. Example: @class YYShadow, YYBorder, YYAttachment; @interface YYAttributes @property NSString *name; @property NSArray *shadows; @property NSSet *borders; @property NSDictionary *attachments; @end @implementation YYAttributes + (NSDictionary *)modelContainerPropertyGenericClass { return @{@"shadows" : [YYShadow class], @"borders" : YYBorder.class, @"attachments" : @"YYAttachment" }; } @end @return A class mapper. */ + (nullable NSDictionary *)modelContainerPropertyGenericClass; /** If you need to create instances of different classes during json->object transform, use the method to choose custom class based on dictionary data. @discussion If the model implements this method, it will be called to determine resulting class during `+modelWithJSON:`, `+modelWithDictionary:`, conveting object of properties of parent objects (both singular and containers via `+modelContainerPropertyGenericClass`). Example: @class YYCircle, YYRectangle, YYLine; @implementation YYShape + (Class)modelCustomClassForDictionary:(NSDictionary*)dictionary { if (dictionary[@"radius"] != nil) { return [YYCircle class]; } else if (dictionary[@"width"] != nil) { return [YYRectangle class]; } else if (dictionary[@"y2"] != nil) { return [YYLine class]; } else { return [self class]; } } @end @param dictionary The json/kv dictionary. @return Class to create from this dictionary, `nil` to use current class. */ + (nullable Class)modelCustomClassForDictionary:(NSDictionary *)dictionary; /** All the properties in blacklist will be ignored in model transform process. Returns nil to ignore this feature. @return An array of property's name. */ + (nullable NSArray *)modelPropertyBlacklist; /** If a property is not in the whitelist, it will be ignored in model transform process. Returns nil to ignore this feature. @return An array of property's name. */ + (nullable NSArray *)modelPropertyWhitelist; /** This method's behavior is similar to `- (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic;`, but be called before the model transform. @discussion If the model implements this method, it will be called before `+modelWithJSON:`, `+modelWithDictionary:`, `-modelSetWithJSON:` and `-modelSetWithDictionary:`. If this method returns nil, the transform process will ignore this model. @param dic The json/kv dictionary. @return Returns the modified dictionary, or nil to ignore this model. */ - (NSDictionary *)modelCustomWillTransformFromDictionary:(NSDictionary *)dic; /** If the default json-to-model transform does not fit to your model object, implement this method to do additional process. You can also use this method to validate the model's properties. @discussion If the model implements this method, it will be called at the end of `+modelWithJSON:`, `+modelWithDictionary:`, `-modelSetWithJSON:` and `-modelSetWithDictionary:`. If this method returns NO, the transform process will ignore this model. @param dic The json/kv dictionary. @return Returns YES if the model is valid, or NO to ignore this model. */ - (BOOL)modelCustomTransformFromDictionary:(NSDictionary *)dic; /** If the default model-to-json transform does not fit to your model class, implement this method to do additional process. You can also use this method to validate the json dictionary. @discussion If the model implements this method, it will be called at the end of `-modelToJSONObject` and `-modelToJSONString`. If this method returns NO, the transform process will ignore this json dictionary. @param dic The json dictionary. @return Returns YES if the model is valid, or NO to ignore this model. */ - (BOOL)modelCustomTransformToDictionary:(NSMutableDictionary *)dic; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/YYModel/YYModel/NSObject+YYModel.m ================================================ // // NSObject+YYModel.m // YYModel // // Created by ibireme on 15/5/10. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import "NSObject+YYModel.h" #import "YYClassInfo.h" #import #define force_inline __inline__ __attribute__((always_inline)) /// Foundation Class Type typedef NS_ENUM (NSUInteger, YYEncodingNSType) { YYEncodingTypeNSUnknown = 0, YYEncodingTypeNSString, YYEncodingTypeNSMutableString, YYEncodingTypeNSValue, YYEncodingTypeNSNumber, YYEncodingTypeNSDecimalNumber, YYEncodingTypeNSData, YYEncodingTypeNSMutableData, YYEncodingTypeNSDate, YYEncodingTypeNSURL, YYEncodingTypeNSArray, YYEncodingTypeNSMutableArray, YYEncodingTypeNSDictionary, YYEncodingTypeNSMutableDictionary, YYEncodingTypeNSSet, YYEncodingTypeNSMutableSet, }; /// Get the Foundation class type from property info. static force_inline YYEncodingNSType YYClassGetNSType(Class cls) { if (!cls) return YYEncodingTypeNSUnknown; if ([cls isSubclassOfClass:[NSMutableString class]]) return YYEncodingTypeNSMutableString; if ([cls isSubclassOfClass:[NSString class]]) return YYEncodingTypeNSString; if ([cls isSubclassOfClass:[NSDecimalNumber class]]) return YYEncodingTypeNSDecimalNumber; if ([cls isSubclassOfClass:[NSNumber class]]) return YYEncodingTypeNSNumber; if ([cls isSubclassOfClass:[NSValue class]]) return YYEncodingTypeNSValue; if ([cls isSubclassOfClass:[NSMutableData class]]) return YYEncodingTypeNSMutableData; if ([cls isSubclassOfClass:[NSData class]]) return YYEncodingTypeNSData; if ([cls isSubclassOfClass:[NSDate class]]) return YYEncodingTypeNSDate; if ([cls isSubclassOfClass:[NSURL class]]) return YYEncodingTypeNSURL; if ([cls isSubclassOfClass:[NSMutableArray class]]) return YYEncodingTypeNSMutableArray; if ([cls isSubclassOfClass:[NSArray class]]) return YYEncodingTypeNSArray; if ([cls isSubclassOfClass:[NSMutableDictionary class]]) return YYEncodingTypeNSMutableDictionary; if ([cls isSubclassOfClass:[NSDictionary class]]) return YYEncodingTypeNSDictionary; if ([cls isSubclassOfClass:[NSMutableSet class]]) return YYEncodingTypeNSMutableSet; if ([cls isSubclassOfClass:[NSSet class]]) return YYEncodingTypeNSSet; return YYEncodingTypeNSUnknown; } /// Whether the type is c number. static force_inline BOOL YYEncodingTypeIsCNumber(YYEncodingType type) { switch (type & YYEncodingTypeMask) { case YYEncodingTypeBool: case YYEncodingTypeInt8: case YYEncodingTypeUInt8: case YYEncodingTypeInt16: case YYEncodingTypeUInt16: case YYEncodingTypeInt32: case YYEncodingTypeUInt32: case YYEncodingTypeInt64: case YYEncodingTypeUInt64: case YYEncodingTypeFloat: case YYEncodingTypeDouble: case YYEncodingTypeLongDouble: return YES; default: return NO; } } /// Parse a number value from 'id'. static force_inline NSNumber *YYNSNumberCreateFromID(__unsafe_unretained id value) { static NSCharacterSet *dot; static NSDictionary *dic; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ dot = [NSCharacterSet characterSetWithRange:NSMakeRange('.', 1)]; dic = @{@"TRUE" : @(YES), @"True" : @(YES), @"true" : @(YES), @"FALSE" : @(NO), @"False" : @(NO), @"false" : @(NO), @"YES" : @(YES), @"Yes" : @(YES), @"yes" : @(YES), @"NO" : @(NO), @"No" : @(NO), @"no" : @(NO), @"NIL" : (id)kCFNull, @"Nil" : (id)kCFNull, @"nil" : (id)kCFNull, @"NULL" : (id)kCFNull, @"Null" : (id)kCFNull, @"null" : (id)kCFNull, @"(NULL)" : (id)kCFNull, @"(Null)" : (id)kCFNull, @"(null)" : (id)kCFNull, @"" : (id)kCFNull, @"" : (id)kCFNull, @"" : (id)kCFNull}; }); if (!value || value == (id)kCFNull) return nil; if ([value isKindOfClass:[NSNumber class]]) return value; if ([value isKindOfClass:[NSString class]]) { NSNumber *num = dic[value]; if (num) { if (num == (id)kCFNull) return nil; return num; } if ([(NSString *)value rangeOfCharacterFromSet:dot].location != NSNotFound) { const char *cstring = ((NSString *)value).UTF8String; if (!cstring) return nil; double num = atof(cstring); if (isnan(num) || isinf(num)) return nil; return @(num); } else { const char *cstring = ((NSString *)value).UTF8String; if (!cstring) return nil; return @(atoll(cstring)); } } return nil; } /// Parse string to date. static force_inline NSDate *YYNSDateFromString(__unsafe_unretained NSString *string) { typedef NSDate* (^YYNSDateParseBlock)(NSString *string); #define kParserNum 34 static YYNSDateParseBlock blocks[kParserNum + 1] = {0}; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ { /* 2014-01-20 // Google */ NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; formatter.dateFormat = @"yyyy-MM-dd"; blocks[10] = ^(NSString *string) { return [formatter dateFromString:string]; }; } { /* 2014-01-20 12:24:48 2014-01-20T12:24:48 // Google 2014-01-20 12:24:48.000 2014-01-20T12:24:48.000 */ NSDateFormatter *formatter1 = [[NSDateFormatter alloc] init]; formatter1.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter1.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; formatter1.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss"; NSDateFormatter *formatter2 = [[NSDateFormatter alloc] init]; formatter2.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter2.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; formatter2.dateFormat = @"yyyy-MM-dd HH:mm:ss"; NSDateFormatter *formatter3 = [[NSDateFormatter alloc] init]; formatter3.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter3.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; formatter3.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSS"; NSDateFormatter *formatter4 = [[NSDateFormatter alloc] init]; formatter4.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter4.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0]; formatter4.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS"; blocks[19] = ^(NSString *string) { if ([string characterAtIndex:10] == 'T') { return [formatter1 dateFromString:string]; } else { return [formatter2 dateFromString:string]; } }; blocks[23] = ^(NSString *string) { if ([string characterAtIndex:10] == 'T') { return [formatter3 dateFromString:string]; } else { return [formatter4 dateFromString:string]; } }; } { /* 2014-01-20T12:24:48Z // Github, Apple 2014-01-20T12:24:48+0800 // Facebook 2014-01-20T12:24:48+12:00 // Google 2014-01-20T12:24:48.000Z 2014-01-20T12:24:48.000+0800 2014-01-20T12:24:48.000+12:00 */ NSDateFormatter *formatter = [NSDateFormatter new]; formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZ"; NSDateFormatter *formatter2 = [NSDateFormatter new]; formatter2.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter2.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss.SSSZ"; blocks[20] = ^(NSString *string) { return [formatter dateFromString:string]; }; blocks[24] = ^(NSString *string) { return [formatter dateFromString:string]?: [formatter2 dateFromString:string]; }; blocks[25] = ^(NSString *string) { return [formatter dateFromString:string]; }; blocks[28] = ^(NSString *string) { return [formatter2 dateFromString:string]; }; blocks[29] = ^(NSString *string) { return [formatter2 dateFromString:string]; }; } { /* Fri Sep 04 00:12:21 +0800 2015 // Weibo, Twitter Fri Sep 04 00:12:21.000 +0800 2015 */ NSDateFormatter *formatter = [NSDateFormatter new]; formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter.dateFormat = @"EEE MMM dd HH:mm:ss Z yyyy"; NSDateFormatter *formatter2 = [NSDateFormatter new]; formatter2.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter2.dateFormat = @"EEE MMM dd HH:mm:ss.SSS Z yyyy"; blocks[30] = ^(NSString *string) { return [formatter dateFromString:string]; }; blocks[34] = ^(NSString *string) { return [formatter2 dateFromString:string]; }; } }); if (!string) return nil; if (string.length > kParserNum) return nil; YYNSDateParseBlock parser = blocks[string.length]; if (!parser) return nil; return parser(string); #undef kParserNum } /// Get the 'NSBlock' class. static force_inline Class YYNSBlockClass() { static Class cls; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ void (^block)(void) = ^{}; cls = ((NSObject *)block).class; while (class_getSuperclass(cls) != [NSObject class]) { cls = class_getSuperclass(cls); } }); return cls; // current is "NSBlock" } /** Get the ISO date formatter. ISO8601 format example: 2010-07-09T16:13:30+12:00 2011-01-11T11:11:11+0000 2011-01-26T19:06:43Z length: 20/24/25 */ static force_inline NSDateFormatter *YYISODateFormatter() { static NSDateFormatter *formatter = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ formatter = [[NSDateFormatter alloc] init]; formatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"]; formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ssZ"; }); return formatter; } /// Get the value with key paths from dictionary /// The dic should be NSDictionary, and the keyPath should not be nil. static force_inline id YYValueForKeyPath(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *keyPaths) { id value = nil; for (NSUInteger i = 0, max = keyPaths.count; i < max; i++) { value = dic[keyPaths[i]]; if (i + 1 < max) { if ([value isKindOfClass:[NSDictionary class]]) { dic = value; } else { return nil; } } } return value; } /// Get the value with multi key (or key path) from dictionary /// The dic should be NSDictionary static force_inline id YYValueForMultiKeys(__unsafe_unretained NSDictionary *dic, __unsafe_unretained NSArray *multiKeys) { id value = nil; for (NSString *key in multiKeys) { if ([key isKindOfClass:[NSString class]]) { value = dic[key]; if (value) break; } else { value = YYValueForKeyPath(dic, (NSArray *)key); if (value) break; } } return value; } /// A property info in object model. @interface _YYModelPropertyMeta : NSObject { @package NSString *_name; ///< property's name YYEncodingType _type; ///< property's type YYEncodingNSType _nsType; ///< property's Foundation type BOOL _isCNumber; ///< is c number type Class _cls; ///< property's class, or nil Class _genericCls; ///< container's generic class, or nil if threr's no generic class SEL _getter; ///< getter, or nil if the instances cannot respond SEL _setter; ///< setter, or nil if the instances cannot respond BOOL _isKVCCompatible; ///< YES if it can access with key-value coding BOOL _isStructAvailableForKeyedArchiver; ///< YES if the struct can encoded with keyed archiver/unarchiver BOOL _hasCustomClassFromDictionary; ///< class/generic class implements +modelCustomClassForDictionary: /* property->key: _mappedToKey:key _mappedToKeyPath:nil _mappedToKeyArray:nil property->keyPath: _mappedToKey:keyPath _mappedToKeyPath:keyPath(array) _mappedToKeyArray:nil property->keys: _mappedToKey:keys[0] _mappedToKeyPath:nil/keyPath _mappedToKeyArray:keys(array) */ NSString *_mappedToKey; ///< the key mapped to NSArray *_mappedToKeyPath; ///< the key path mapped to (nil if the name is not key path) NSArray *_mappedToKeyArray; ///< the key(NSString) or keyPath(NSArray) array (nil if not mapped to multiple keys) YYClassPropertyInfo *_info; ///< property's info _YYModelPropertyMeta *_next; ///< next meta if there are multiple properties mapped to the same key. } @end @implementation _YYModelPropertyMeta + (instancetype)metaWithClassInfo:(YYClassInfo *)classInfo propertyInfo:(YYClassPropertyInfo *)propertyInfo generic:(Class)generic { // support pseudo generic class with protocol name if (!generic && propertyInfo.protocols) { for (NSString *protocol in propertyInfo.protocols) { Class cls = objc_getClass(protocol.UTF8String); if (cls) { generic = cls; break; } } } _YYModelPropertyMeta *meta = [self new]; meta->_name = propertyInfo.name; meta->_type = propertyInfo.type; meta->_info = propertyInfo; meta->_genericCls = generic; if ((meta->_type & YYEncodingTypeMask) == YYEncodingTypeObject) { meta->_nsType = YYClassGetNSType(propertyInfo.cls); } else { meta->_isCNumber = YYEncodingTypeIsCNumber(meta->_type); } if ((meta->_type & YYEncodingTypeMask) == YYEncodingTypeStruct) { /* It seems that NSKeyedUnarchiver cannot decode NSValue except these structs: */ static NSSet *types = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSMutableSet *set = [NSMutableSet new]; // 32 bit [set addObject:@"{CGSize=ff}"]; [set addObject:@"{CGPoint=ff}"]; [set addObject:@"{CGRect={CGPoint=ff}{CGSize=ff}}"]; [set addObject:@"{CGAffineTransform=ffffff}"]; [set addObject:@"{UIEdgeInsets=ffff}"]; [set addObject:@"{UIOffset=ff}"]; // 64 bit [set addObject:@"{CGSize=dd}"]; [set addObject:@"{CGPoint=dd}"]; [set addObject:@"{CGRect={CGPoint=dd}{CGSize=dd}}"]; [set addObject:@"{CGAffineTransform=dddddd}"]; [set addObject:@"{UIEdgeInsets=dddd}"]; [set addObject:@"{UIOffset=dd}"]; types = set; }); if ([types containsObject:propertyInfo.typeEncoding]) { meta->_isStructAvailableForKeyedArchiver = YES; } } meta->_cls = propertyInfo.cls; if (generic) { meta->_hasCustomClassFromDictionary = [generic respondsToSelector:@selector(modelCustomClassForDictionary:)]; } else if (meta->_cls && meta->_nsType == YYEncodingTypeNSUnknown) { meta->_hasCustomClassFromDictionary = [meta->_cls respondsToSelector:@selector(modelCustomClassForDictionary:)]; } if (propertyInfo.getter) { if ([classInfo.cls instancesRespondToSelector:propertyInfo.getter]) { meta->_getter = propertyInfo.getter; } } if (propertyInfo.setter) { if ([classInfo.cls instancesRespondToSelector:propertyInfo.setter]) { meta->_setter = propertyInfo.setter; } } if (meta->_getter && meta->_setter) { /* KVC invalid type: long double pointer (such as SEL/CoreFoundation object) */ switch (meta->_type & YYEncodingTypeMask) { case YYEncodingTypeBool: case YYEncodingTypeInt8: case YYEncodingTypeUInt8: case YYEncodingTypeInt16: case YYEncodingTypeUInt16: case YYEncodingTypeInt32: case YYEncodingTypeUInt32: case YYEncodingTypeInt64: case YYEncodingTypeUInt64: case YYEncodingTypeFloat: case YYEncodingTypeDouble: case YYEncodingTypeObject: case YYEncodingTypeClass: case YYEncodingTypeBlock: case YYEncodingTypeStruct: case YYEncodingTypeUnion: { meta->_isKVCCompatible = YES; } break; default: break; } } return meta; } @end /// A class info in object model. @interface _YYModelMeta : NSObject { @package YYClassInfo *_classInfo; /// Key:mapped key and key path, Value:_YYModelPropertyMeta. NSDictionary *_mapper; /// Array<_YYModelPropertyMeta>, all property meta of this model. NSArray *_allPropertyMetas; /// Array<_YYModelPropertyMeta>, property meta which is mapped to a key path. NSArray *_keyPathPropertyMetas; /// Array<_YYModelPropertyMeta>, property meta which is mapped to multi keys. NSArray *_multiKeysPropertyMetas; /// The number of mapped key (and key path), same to _mapper.count. NSUInteger _keyMappedCount; /// Model class type. YYEncodingNSType _nsType; BOOL _hasCustomWillTransformFromDictionary; BOOL _hasCustomTransformFromDictionary; BOOL _hasCustomTransformToDictionary; BOOL _hasCustomClassFromDictionary; } @end @implementation _YYModelMeta - (instancetype)initWithClass:(Class)cls { YYClassInfo *classInfo = [YYClassInfo classInfoWithClass:cls]; if (!classInfo) return nil; self = [super init]; // Get black list NSSet *blacklist = nil; if ([cls respondsToSelector:@selector(modelPropertyBlacklist)]) { NSArray *properties = [(id)cls modelPropertyBlacklist]; if (properties) { blacklist = [NSSet setWithArray:properties]; } } // Get white list NSSet *whitelist = nil; if ([cls respondsToSelector:@selector(modelPropertyWhitelist)]) { NSArray *properties = [(id)cls modelPropertyWhitelist]; if (properties) { whitelist = [NSSet setWithArray:properties]; } } // Get container property's generic class NSDictionary *genericMapper = nil; if ([cls respondsToSelector:@selector(modelContainerPropertyGenericClass)]) { genericMapper = [(id)cls modelContainerPropertyGenericClass]; if (genericMapper) { NSMutableDictionary *tmp = [NSMutableDictionary new]; [genericMapper enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) { if (![key isKindOfClass:[NSString class]]) return; Class meta = object_getClass(obj); if (!meta) return; if (class_isMetaClass(meta)) { tmp[key] = obj; } else if ([obj isKindOfClass:[NSString class]]) { Class cls = NSClassFromString(obj); if (cls) { tmp[key] = cls; } } }]; genericMapper = tmp; } } // Create all property metas. NSMutableDictionary *allPropertyMetas = [NSMutableDictionary new]; YYClassInfo *curClassInfo = classInfo; while (curClassInfo && curClassInfo.superCls != nil) { // recursive parse super class, but ignore root class (NSObject/NSProxy) for (YYClassPropertyInfo *propertyInfo in curClassInfo.propertyInfos.allValues) { if (!propertyInfo.name) continue; if (blacklist && [blacklist containsObject:propertyInfo.name]) continue; if (whitelist && ![whitelist containsObject:propertyInfo.name]) continue; _YYModelPropertyMeta *meta = [_YYModelPropertyMeta metaWithClassInfo:classInfo propertyInfo:propertyInfo generic:genericMapper[propertyInfo.name]]; if (!meta || !meta->_name) continue; if (!meta->_getter || !meta->_setter) continue; if (allPropertyMetas[meta->_name]) continue; allPropertyMetas[meta->_name] = meta; } curClassInfo = curClassInfo.superClassInfo; } if (allPropertyMetas.count) _allPropertyMetas = allPropertyMetas.allValues.copy; // create mapper NSMutableDictionary *mapper = [NSMutableDictionary new]; NSMutableArray *keyPathPropertyMetas = [NSMutableArray new]; NSMutableArray *multiKeysPropertyMetas = [NSMutableArray new]; if ([cls respondsToSelector:@selector(modelCustomPropertyMapper)]) { NSDictionary *customMapper = [(id )cls modelCustomPropertyMapper]; [customMapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyName, NSString *mappedToKey, BOOL *stop) { _YYModelPropertyMeta *propertyMeta = allPropertyMetas[propertyName]; if (!propertyMeta) return; [allPropertyMetas removeObjectForKey:propertyName]; if ([mappedToKey isKindOfClass:[NSString class]]) { if (mappedToKey.length == 0) return; propertyMeta->_mappedToKey = mappedToKey; NSArray *keyPath = [mappedToKey componentsSeparatedByString:@"."]; for (NSString *onePath in keyPath) { if (onePath.length == 0) { NSMutableArray *tmp = keyPath.mutableCopy; [tmp removeObject:@""]; keyPath = tmp; break; } } if (keyPath.count > 1) { propertyMeta->_mappedToKeyPath = keyPath; [keyPathPropertyMetas addObject:propertyMeta]; } propertyMeta->_next = mapper[mappedToKey] ?: nil; mapper[mappedToKey] = propertyMeta; } else if ([mappedToKey isKindOfClass:[NSArray class]]) { NSMutableArray *mappedToKeyArray = [NSMutableArray new]; for (NSString *oneKey in ((NSArray *)mappedToKey)) { if (![oneKey isKindOfClass:[NSString class]]) continue; if (oneKey.length == 0) continue; NSArray *keyPath = [oneKey componentsSeparatedByString:@"."]; if (keyPath.count > 1) { [mappedToKeyArray addObject:keyPath]; } else { [mappedToKeyArray addObject:oneKey]; } if (!propertyMeta->_mappedToKey) { propertyMeta->_mappedToKey = oneKey; propertyMeta->_mappedToKeyPath = keyPath.count > 1 ? keyPath : nil; } } if (!propertyMeta->_mappedToKey) return; propertyMeta->_mappedToKeyArray = mappedToKeyArray; [multiKeysPropertyMetas addObject:propertyMeta]; propertyMeta->_next = mapper[mappedToKey] ?: nil; mapper[mappedToKey] = propertyMeta; } }]; } [allPropertyMetas enumerateKeysAndObjectsUsingBlock:^(NSString *name, _YYModelPropertyMeta *propertyMeta, BOOL *stop) { propertyMeta->_mappedToKey = name; propertyMeta->_next = mapper[name] ?: nil; mapper[name] = propertyMeta; }]; if (mapper.count) _mapper = mapper; if (keyPathPropertyMetas) _keyPathPropertyMetas = keyPathPropertyMetas; if (multiKeysPropertyMetas) _multiKeysPropertyMetas = multiKeysPropertyMetas; _classInfo = classInfo; _keyMappedCount = _allPropertyMetas.count; _nsType = YYClassGetNSType(cls); _hasCustomWillTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomWillTransformFromDictionary:)]); _hasCustomTransformFromDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformFromDictionary:)]); _hasCustomTransformToDictionary = ([cls instancesRespondToSelector:@selector(modelCustomTransformToDictionary:)]); _hasCustomClassFromDictionary = ([cls respondsToSelector:@selector(modelCustomClassForDictionary:)]); return self; } /// Returns the cached model class meta + (instancetype)metaWithClass:(Class)cls { if (!cls) return nil; static CFMutableDictionaryRef cache; static dispatch_once_t onceToken; static dispatch_semaphore_t lock; dispatch_once(&onceToken, ^{ cache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); lock = dispatch_semaphore_create(1); }); dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); _YYModelMeta *meta = CFDictionaryGetValue(cache, (__bridge const void *)(cls)); dispatch_semaphore_signal(lock); if (!meta || meta->_classInfo.needUpdate) { meta = [[_YYModelMeta alloc] initWithClass:cls]; if (meta) { dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); CFDictionarySetValue(cache, (__bridge const void *)(cls), (__bridge const void *)(meta)); dispatch_semaphore_signal(lock); } } return meta; } @end /** Get number from property. @discussion Caller should hold strong reference to the parameters before this function returns. @param model Should not be nil. @param meta Should not be nil, meta.isCNumber should be YES, meta.getter should not be nil. @return A number object, or nil if failed. */ static force_inline NSNumber *ModelCreateNumberFromProperty(__unsafe_unretained id model, __unsafe_unretained _YYModelPropertyMeta *meta) { switch (meta->_type & YYEncodingTypeMask) { case YYEncodingTypeBool: { return @(((bool (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter)); } case YYEncodingTypeInt8: { return @(((int8_t (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter)); } case YYEncodingTypeUInt8: { return @(((uint8_t (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter)); } case YYEncodingTypeInt16: { return @(((int16_t (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter)); } case YYEncodingTypeUInt16: { return @(((uint16_t (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter)); } case YYEncodingTypeInt32: { return @(((int32_t (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter)); } case YYEncodingTypeUInt32: { return @(((uint32_t (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter)); } case YYEncodingTypeInt64: { return @(((int64_t (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter)); } case YYEncodingTypeUInt64: { return @(((uint64_t (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter)); } case YYEncodingTypeFloat: { float num = ((float (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter); if (isnan(num) || isinf(num)) return nil; return @(num); } case YYEncodingTypeDouble: { double num = ((double (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter); if (isnan(num) || isinf(num)) return nil; return @(num); } case YYEncodingTypeLongDouble: { double num = ((long double (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter); if (isnan(num) || isinf(num)) return nil; return @(num); } default: return nil; } } /** Set number to property. @discussion Caller should hold strong reference to the parameters before this function returns. @param model Should not be nil. @param num Can be nil. @param meta Should not be nil, meta.isCNumber should be YES, meta.setter should not be nil. */ static force_inline void ModelSetNumberToProperty(__unsafe_unretained id model, __unsafe_unretained NSNumber *num, __unsafe_unretained _YYModelPropertyMeta *meta) { switch (meta->_type & YYEncodingTypeMask) { case YYEncodingTypeBool: { ((void (*)(id, SEL, bool))(void *) objc_msgSend)((id)model, meta->_setter, num.boolValue); } break; case YYEncodingTypeInt8: { ((void (*)(id, SEL, int8_t))(void *) objc_msgSend)((id)model, meta->_setter, (int8_t)num.charValue); } break; case YYEncodingTypeUInt8: { ((void (*)(id, SEL, uint8_t))(void *) objc_msgSend)((id)model, meta->_setter, (uint8_t)num.unsignedCharValue); } break; case YYEncodingTypeInt16: { ((void (*)(id, SEL, int16_t))(void *) objc_msgSend)((id)model, meta->_setter, (int16_t)num.shortValue); } break; case YYEncodingTypeUInt16: { ((void (*)(id, SEL, uint16_t))(void *) objc_msgSend)((id)model, meta->_setter, (uint16_t)num.unsignedShortValue); } break; case YYEncodingTypeInt32: { ((void (*)(id, SEL, int32_t))(void *) objc_msgSend)((id)model, meta->_setter, (int32_t)num.intValue); } case YYEncodingTypeUInt32: { ((void (*)(id, SEL, uint32_t))(void *) objc_msgSend)((id)model, meta->_setter, (uint32_t)num.unsignedIntValue); } break; case YYEncodingTypeInt64: { if ([num isKindOfClass:[NSDecimalNumber class]]) { ((void (*)(id, SEL, int64_t))(void *) objc_msgSend)((id)model, meta->_setter, (int64_t)num.stringValue.longLongValue); } else { ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)((id)model, meta->_setter, (uint64_t)num.longLongValue); } } break; case YYEncodingTypeUInt64: { if ([num isKindOfClass:[NSDecimalNumber class]]) { ((void (*)(id, SEL, int64_t))(void *) objc_msgSend)((id)model, meta->_setter, (int64_t)num.stringValue.longLongValue); } else { ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)((id)model, meta->_setter, (uint64_t)num.unsignedLongLongValue); } } break; case YYEncodingTypeFloat: { float f = num.floatValue; if (isnan(f) || isinf(f)) f = 0; ((void (*)(id, SEL, float))(void *) objc_msgSend)((id)model, meta->_setter, f); } break; case YYEncodingTypeDouble: { double d = num.doubleValue; if (isnan(d) || isinf(d)) d = 0; ((void (*)(id, SEL, double))(void *) objc_msgSend)((id)model, meta->_setter, d); } break; case YYEncodingTypeLongDouble: { long double d = num.doubleValue; if (isnan(d) || isinf(d)) d = 0; ((void (*)(id, SEL, long double))(void *) objc_msgSend)((id)model, meta->_setter, (long double)d); } // break; commented for code coverage in next line default: break; } } /** Set value to model with a property meta. @discussion Caller should hold strong reference to the parameters before this function returns. @param model Should not be nil. @param value Should not be nil, but can be NSNull. @param meta Should not be nil, and meta->_setter should not be nil. */ static void ModelSetValueForProperty(__unsafe_unretained id model, __unsafe_unretained id value, __unsafe_unretained _YYModelPropertyMeta *meta) { if (meta->_isCNumber) { NSNumber *num = YYNSNumberCreateFromID(value); ModelSetNumberToProperty(model, num, meta); if (num) [num class]; // hold the number } else if (meta->_nsType) { if (value == (id)kCFNull) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil); } else { switch (meta->_nsType) { case YYEncodingTypeNSString: case YYEncodingTypeNSMutableString: { if ([value isKindOfClass:[NSString class]]) { if (meta->_nsType == YYEncodingTypeNSString) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); } else { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSString *)value).mutableCopy); } } else if ([value isKindOfClass:[NSNumber class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (meta->_nsType == YYEncodingTypeNSString) ? ((NSNumber *)value).stringValue : ((NSNumber *)value).stringValue.mutableCopy); } else if ([value isKindOfClass:[NSData class]]) { NSMutableString *string = [[NSMutableString alloc] initWithData:value encoding:NSUTF8StringEncoding]; ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, string); } else if ([value isKindOfClass:[NSURL class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (meta->_nsType == YYEncodingTypeNSString) ? ((NSURL *)value).absoluteString : ((NSURL *)value).absoluteString.mutableCopy); } else if ([value isKindOfClass:[NSAttributedString class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (meta->_nsType == YYEncodingTypeNSString) ? ((NSAttributedString *)value).string : ((NSAttributedString *)value).string.mutableCopy); } } break; case YYEncodingTypeNSValue: case YYEncodingTypeNSNumber: case YYEncodingTypeNSDecimalNumber: { if (meta->_nsType == YYEncodingTypeNSNumber) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, YYNSNumberCreateFromID(value)); } else if (meta->_nsType == YYEncodingTypeNSDecimalNumber) { if ([value isKindOfClass:[NSDecimalNumber class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); } else if ([value isKindOfClass:[NSNumber class]]) { NSDecimalNumber *decNum = [NSDecimalNumber decimalNumberWithDecimal:[((NSNumber *)value) decimalValue]]; ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, decNum); } else if ([value isKindOfClass:[NSString class]]) { NSDecimalNumber *decNum = [NSDecimalNumber decimalNumberWithString:value]; NSDecimal dec = decNum.decimalValue; if (dec._length == 0 && dec._isNegative) { decNum = nil; // NaN } ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, decNum); } } else { // YYEncodingTypeNSValue if ([value isKindOfClass:[NSValue class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); } } } break; case YYEncodingTypeNSData: case YYEncodingTypeNSMutableData: { if ([value isKindOfClass:[NSData class]]) { if (meta->_nsType == YYEncodingTypeNSData) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); } else { NSMutableData *data = ((NSData *)value).mutableCopy; ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, data); } } else if ([value isKindOfClass:[NSString class]]) { NSData *data = [(NSString *)value dataUsingEncoding:NSUTF8StringEncoding]; if (meta->_nsType == YYEncodingTypeNSMutableData) { data = ((NSData *)data).mutableCopy; } ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, data); } } break; case YYEncodingTypeNSDate: { if ([value isKindOfClass:[NSDate class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); } else if ([value isKindOfClass:[NSString class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, YYNSDateFromString(value)); } } break; case YYEncodingTypeNSURL: { if ([value isKindOfClass:[NSURL class]]) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); } else if ([value isKindOfClass:[NSString class]]) { NSCharacterSet *set = [NSCharacterSet whitespaceAndNewlineCharacterSet]; NSString *str = [value stringByTrimmingCharactersInSet:set]; if (str.length == 0) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, nil); } else { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, [[NSURL alloc] initWithString:str]); } } } break; case YYEncodingTypeNSArray: case YYEncodingTypeNSMutableArray: { if (meta->_genericCls) { NSArray *valueArr = nil; if ([value isKindOfClass:[NSArray class]]) valueArr = value; else if ([value isKindOfClass:[NSSet class]]) valueArr = ((NSSet *)value).allObjects; if (valueArr) { NSMutableArray *objectArr = [NSMutableArray new]; for (id one in valueArr) { if ([one isKindOfClass:meta->_genericCls]) { [objectArr addObject:one]; } else if ([one isKindOfClass:[NSDictionary class]]) { Class cls = meta->_genericCls; if (meta->_hasCustomClassFromDictionary) { cls = [cls modelCustomClassForDictionary:one]; if (!cls) cls = meta->_genericCls; // for xcode code coverage } NSObject *newOne = [cls new]; [newOne yy_modelSetWithDictionary:one]; if (newOne) [objectArr addObject:newOne]; } } ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, objectArr); } } else { if ([value isKindOfClass:[NSArray class]]) { if (meta->_nsType == YYEncodingTypeNSArray) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); } else { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSArray *)value).mutableCopy); } } else if ([value isKindOfClass:[NSSet class]]) { if (meta->_nsType == YYEncodingTypeNSArray) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSSet *)value).allObjects); } else { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSSet *)value).allObjects.mutableCopy); } } } } break; case YYEncodingTypeNSDictionary: case YYEncodingTypeNSMutableDictionary: { if ([value isKindOfClass:[NSDictionary class]]) { if (meta->_genericCls) { NSMutableDictionary *dic = [NSMutableDictionary new]; [((NSDictionary *)value) enumerateKeysAndObjectsUsingBlock:^(NSString *oneKey, id oneValue, BOOL *stop) { if ([oneValue isKindOfClass:[NSDictionary class]]) { Class cls = meta->_genericCls; if (meta->_hasCustomClassFromDictionary) { cls = [cls modelCustomClassForDictionary:oneValue]; if (!cls) cls = meta->_genericCls; // for xcode code coverage } NSObject *newOne = [cls new]; [newOne yy_modelSetWithDictionary:(id)oneValue]; if (newOne) dic[oneKey] = newOne; } }]; ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, dic); } else { if (meta->_nsType == YYEncodingTypeNSDictionary) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, value); } else { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSDictionary *)value).mutableCopy); } } } } break; case YYEncodingTypeNSSet: case YYEncodingTypeNSMutableSet: { NSSet *valueSet = nil; if ([value isKindOfClass:[NSArray class]]) valueSet = [NSMutableSet setWithArray:value]; else if ([value isKindOfClass:[NSSet class]]) valueSet = ((NSSet *)value); if (meta->_genericCls) { NSMutableSet *set = [NSMutableSet new]; for (id one in valueSet) { if ([one isKindOfClass:meta->_genericCls]) { [set addObject:one]; } else if ([one isKindOfClass:[NSDictionary class]]) { Class cls = meta->_genericCls; if (meta->_hasCustomClassFromDictionary) { cls = [cls modelCustomClassForDictionary:one]; if (!cls) cls = meta->_genericCls; // for xcode code coverage } NSObject *newOne = [cls new]; [newOne yy_modelSetWithDictionary:one]; if (newOne) [set addObject:newOne]; } } ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, set); } else { if (meta->_nsType == YYEncodingTypeNSSet) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, valueSet); } else { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, ((NSSet *)valueSet).mutableCopy); } } } // break; commented for code coverage in next line default: break; } } } else { BOOL isNull = (value == (id)kCFNull); switch (meta->_type & YYEncodingTypeMask) { case YYEncodingTypeObject: { if (isNull) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)nil); } else if ([value isKindOfClass:meta->_cls] || !meta->_cls) { ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)value); } else if ([value isKindOfClass:[NSDictionary class]]) { NSObject *one = nil; if (meta->_getter) { one = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, meta->_getter); } if (one) { [one yy_modelSetWithDictionary:value]; } else { Class cls = meta->_cls; if (meta->_hasCustomClassFromDictionary) { cls = [cls modelCustomClassForDictionary:value]; if (!cls) cls = meta->_genericCls; // for xcode code coverage } one = [cls new]; [one yy_modelSetWithDictionary:value]; ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)model, meta->_setter, (id)one); } } } break; case YYEncodingTypeClass: { if (isNull) { ((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)NULL); } else { Class cls = nil; if ([value isKindOfClass:[NSString class]]) { cls = NSClassFromString(value); if (cls) { ((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)cls); } } else { cls = object_getClass(value); if (cls) { if (class_isMetaClass(cls)) { ((void (*)(id, SEL, Class))(void *) objc_msgSend)((id)model, meta->_setter, (Class)value); } } } } } break; case YYEncodingTypeSEL: { if (isNull) { ((void (*)(id, SEL, SEL))(void *) objc_msgSend)((id)model, meta->_setter, (SEL)NULL); } else if ([value isKindOfClass:[NSString class]]) { SEL sel = NSSelectorFromString(value); if (sel) ((void (*)(id, SEL, SEL))(void *) objc_msgSend)((id)model, meta->_setter, (SEL)sel); } } break; case YYEncodingTypeBlock: { if (isNull) { ((void (*)(id, SEL, void (^)()))(void *) objc_msgSend)((id)model, meta->_setter, (void (^)())NULL); } else if ([value isKindOfClass:YYNSBlockClass()]) { ((void (*)(id, SEL, void (^)()))(void *) objc_msgSend)((id)model, meta->_setter, (void (^)())value); } } break; case YYEncodingTypeStruct: case YYEncodingTypeUnion: case YYEncodingTypeCArray: { if ([value isKindOfClass:[NSValue class]]) { const char *valueType = ((NSValue *)value).objCType; const char *metaType = meta->_info.typeEncoding.UTF8String; if (valueType && metaType && strcmp(valueType, metaType) == 0) { [model setValue:value forKey:meta->_name]; } } } break; case YYEncodingTypePointer: case YYEncodingTypeCString: { if (isNull) { ((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, (void *)NULL); } else if ([value isKindOfClass:[NSValue class]]) { NSValue *nsValue = value; if (nsValue.objCType && strcmp(nsValue.objCType, "^v") == 0) { ((void (*)(id, SEL, void *))(void *) objc_msgSend)((id)model, meta->_setter, nsValue.pointerValue); } } } // break; commented for code coverage in next line default: break; } } } typedef struct { void *modelMeta; ///< _YYModelMeta void *model; ///< id (self) void *dictionary; ///< NSDictionary (json) } ModelSetContext; /** Apply function for dictionary, to set the key-value pair to model. @param _key should not be nil, NSString. @param _value should not be nil. @param _context _context.modelMeta and _context.model should not be nil. */ static void ModelSetWithDictionaryFunction(const void *_key, const void *_value, void *_context) { ModelSetContext *context = _context; __unsafe_unretained _YYModelMeta *meta = (__bridge _YYModelMeta *)(context->modelMeta); __unsafe_unretained _YYModelPropertyMeta *propertyMeta = [meta->_mapper objectForKey:(__bridge id)(_key)]; __unsafe_unretained id model = (__bridge id)(context->model); while (propertyMeta) { if (propertyMeta->_setter) { ModelSetValueForProperty(model, (__bridge __unsafe_unretained id)_value, propertyMeta); } propertyMeta = propertyMeta->_next; }; } /** Apply function for model property meta, to set dictionary to model. @param _propertyMeta should not be nil, _YYModelPropertyMeta. @param _context _context.model and _context.dictionary should not be nil. */ static void ModelSetWithPropertyMetaArrayFunction(const void *_propertyMeta, void *_context) { ModelSetContext *context = _context; __unsafe_unretained NSDictionary *dictionary = (__bridge NSDictionary *)(context->dictionary); __unsafe_unretained _YYModelPropertyMeta *propertyMeta = (__bridge _YYModelPropertyMeta *)(_propertyMeta); if (!propertyMeta->_setter) return; id value = nil; if (propertyMeta->_mappedToKeyArray) { value = YYValueForMultiKeys(dictionary, propertyMeta->_mappedToKeyArray); } else if (propertyMeta->_mappedToKeyPath) { value = YYValueForKeyPath(dictionary, propertyMeta->_mappedToKeyPath); } else { value = [dictionary objectForKey:propertyMeta->_mappedToKey]; } if (value) { __unsafe_unretained id model = (__bridge id)(context->model); ModelSetValueForProperty(model, value, propertyMeta); } } /** Returns a valid JSON object (NSArray/NSDictionary/NSString/NSNumber/NSNull), or nil if an error occurs. @param model Model, can be nil. @return JSON object, nil if an error occurs. */ static id ModelToJSONObjectRecursive(NSObject *model) { if (!model || model == (id)kCFNull) return model; if ([model isKindOfClass:[NSString class]]) return model; if ([model isKindOfClass:[NSNumber class]]) return model; if ([model isKindOfClass:[NSDictionary class]]) { if ([NSJSONSerialization isValidJSONObject:model]) return model; NSMutableDictionary *newDic = [NSMutableDictionary new]; [((NSDictionary *)model) enumerateKeysAndObjectsUsingBlock:^(NSString *key, id obj, BOOL *stop) { NSString *stringKey = [key isKindOfClass:[NSString class]] ? key : key.description; if (!stringKey) return; id jsonObj = ModelToJSONObjectRecursive(obj); if (!jsonObj) jsonObj = (id)kCFNull; newDic[stringKey] = jsonObj; }]; return newDic; } if ([model isKindOfClass:[NSSet class]]) { NSArray *array = ((NSSet *)model).allObjects; if ([NSJSONSerialization isValidJSONObject:array]) return array; NSMutableArray *newArray = [NSMutableArray new]; for (id obj in array) { if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) { [newArray addObject:obj]; } else { id jsonObj = ModelToJSONObjectRecursive(obj); if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj]; } } return newArray; } if ([model isKindOfClass:[NSArray class]]) { if ([NSJSONSerialization isValidJSONObject:model]) return model; NSMutableArray *newArray = [NSMutableArray new]; for (id obj in (NSArray *)model) { if ([obj isKindOfClass:[NSString class]] || [obj isKindOfClass:[NSNumber class]]) { [newArray addObject:obj]; } else { id jsonObj = ModelToJSONObjectRecursive(obj); if (jsonObj && jsonObj != (id)kCFNull) [newArray addObject:jsonObj]; } } return newArray; } if ([model isKindOfClass:[NSURL class]]) return ((NSURL *)model).absoluteString; if ([model isKindOfClass:[NSAttributedString class]]) return ((NSAttributedString *)model).string; if ([model isKindOfClass:[NSDate class]]) return [YYISODateFormatter() stringFromDate:(id)model]; if ([model isKindOfClass:[NSData class]]) return nil; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:[model class]]; if (!modelMeta || modelMeta->_keyMappedCount == 0) return nil; NSMutableDictionary *result = [[NSMutableDictionary alloc] initWithCapacity:64]; __unsafe_unretained NSMutableDictionary *dic = result; // avoid retain and release in block [modelMeta->_mapper enumerateKeysAndObjectsUsingBlock:^(NSString *propertyMappedKey, _YYModelPropertyMeta *propertyMeta, BOOL *stop) { if (!propertyMeta->_getter) return; id value = nil; if (propertyMeta->_isCNumber) { value = ModelCreateNumberFromProperty(model, propertyMeta); } else if (propertyMeta->_nsType) { id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter); value = ModelToJSONObjectRecursive(v); } else { switch (propertyMeta->_type & YYEncodingTypeMask) { case YYEncodingTypeObject: { id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter); value = ModelToJSONObjectRecursive(v); if (value == (id)kCFNull) value = nil; } break; case YYEncodingTypeClass: { Class v = ((Class (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter); value = v ? NSStringFromClass(v) : nil; } break; case YYEncodingTypeSEL: { SEL v = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, propertyMeta->_getter); value = v ? NSStringFromSelector(v) : nil; } break; default: break; } } if (!value) return; if (propertyMeta->_mappedToKeyPath) { NSMutableDictionary *superDic = dic; NSMutableDictionary *subDic = nil; for (NSUInteger i = 0, max = propertyMeta->_mappedToKeyPath.count; i < max; i++) { NSString *key = propertyMeta->_mappedToKeyPath[i]; if (i + 1 == max) { // end if (!superDic[key]) superDic[key] = value; break; } subDic = superDic[key]; if (subDic) { if ([subDic isKindOfClass:[NSDictionary class]]) { subDic = subDic.mutableCopy; superDic[key] = subDic; } else { break; } } else { subDic = [NSMutableDictionary new]; superDic[key] = subDic; } superDic = subDic; subDic = nil; } } else { if (!dic[propertyMeta->_mappedToKey]) { dic[propertyMeta->_mappedToKey] = value; } } }]; if (modelMeta->_hasCustomTransformToDictionary) { BOOL suc = [((id)model) modelCustomTransformToDictionary:dic]; if (!suc) return nil; } return result; } /// Add indent to string (exclude first line) static NSMutableString *ModelDescriptionAddIndent(NSMutableString *desc, NSUInteger indent) { for (NSUInteger i = 0, max = desc.length; i < max; i++) { unichar c = [desc characterAtIndex:i]; if (c == '\n') { for (NSUInteger j = 0; j < indent; j++) { [desc insertString:@" " atIndex:i + 1]; } i += indent * 4; max += indent * 4; } } return desc; } /// Generaate a description string static NSString *ModelDescription(NSObject *model) { static const int kDescMaxLength = 100; if (!model) return @""; if (model == (id)kCFNull) return @""; if (![model isKindOfClass:[NSObject class]]) return [NSString stringWithFormat:@"%@",model]; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:model.class]; switch (modelMeta->_nsType) { case YYEncodingTypeNSString: case YYEncodingTypeNSMutableString: { return [NSString stringWithFormat:@"\"%@\"",model]; } case YYEncodingTypeNSValue: case YYEncodingTypeNSData: case YYEncodingTypeNSMutableData: { NSString *tmp = model.description; if (tmp.length > kDescMaxLength) { tmp = [tmp substringToIndex:kDescMaxLength]; tmp = [tmp stringByAppendingString:@"..."]; } return tmp; } case YYEncodingTypeNSNumber: case YYEncodingTypeNSDecimalNumber: case YYEncodingTypeNSDate: case YYEncodingTypeNSURL: { return [NSString stringWithFormat:@"%@",model]; } case YYEncodingTypeNSSet: case YYEncodingTypeNSMutableSet: { model = ((NSSet *)model).allObjects; } // no break case YYEncodingTypeNSArray: case YYEncodingTypeNSMutableArray: { NSArray *array = (id)model; NSMutableString *desc = [NSMutableString new]; if (array.count == 0) { return [desc stringByAppendingString:@"[]"]; } else { [desc appendFormat:@"[\n"]; for (NSUInteger i = 0, max = array.count; i < max; i++) { NSObject *obj = array[i]; [desc appendString:@" "]; [desc appendString:ModelDescriptionAddIndent(ModelDescription(obj).mutableCopy, 1)]; [desc appendString:(i + 1 == max) ? @"\n" : @";\n"]; } [desc appendString:@"]"]; return desc; } } case YYEncodingTypeNSDictionary: case YYEncodingTypeNSMutableDictionary: { NSDictionary *dic = (id)model; NSMutableString *desc = [NSMutableString new]; if (dic.count == 0) { return [desc stringByAppendingString:@"{}"]; } else { NSArray *keys = dic.allKeys; [desc appendFormat:@"{\n"]; for (NSUInteger i = 0, max = keys.count; i < max; i++) { NSString *key = keys[i]; NSObject *value = dic[key]; [desc appendString:@" "]; [desc appendFormat:@"%@ = %@",key, ModelDescriptionAddIndent(ModelDescription(value).mutableCopy, 1)]; [desc appendString:(i + 1 == max) ? @"\n" : @";\n"]; } [desc appendString:@"}"]; } return desc; } default: { NSMutableString *desc = [NSMutableString new]; [desc appendFormat:@"<%@: %p>", model.class, model]; if (modelMeta->_allPropertyMetas.count == 0) return desc; // sort property names NSArray *properties = [modelMeta->_allPropertyMetas sortedArrayUsingComparator:^NSComparisonResult(_YYModelPropertyMeta *p1, _YYModelPropertyMeta *p2) { return [p1->_name compare:p2->_name]; }]; [desc appendFormat:@" {\n"]; for (NSUInteger i = 0, max = properties.count; i < max; i++) { _YYModelPropertyMeta *property = properties[i]; NSString *propertyDesc; if (property->_isCNumber) { NSNumber *num = ModelCreateNumberFromProperty(model, property); propertyDesc = num.stringValue; } else { switch (property->_type & YYEncodingTypeMask) { case YYEncodingTypeObject: { id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, property->_getter); propertyDesc = ModelDescription(v); if (!propertyDesc) propertyDesc = @""; } break; case YYEncodingTypeClass: { id v = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, property->_getter); propertyDesc = ((NSObject *)v).description; if (!propertyDesc) propertyDesc = @""; } break; case YYEncodingTypeSEL: { SEL sel = ((SEL (*)(id, SEL))(void *) objc_msgSend)((id)model, property->_getter); if (sel) propertyDesc = NSStringFromSelector(sel); else propertyDesc = @""; } break; case YYEncodingTypeBlock: { id block = ((id (*)(id, SEL))(void *) objc_msgSend)((id)model, property->_getter); propertyDesc = block ? ((NSObject *)block).description : @""; } break; case YYEncodingTypeCArray: case YYEncodingTypeCString: case YYEncodingTypePointer: { void *pointer = ((void* (*)(id, SEL))(void *) objc_msgSend)((id)model, property->_getter); propertyDesc = [NSString stringWithFormat:@"%p",pointer]; } break; case YYEncodingTypeStruct: case YYEncodingTypeUnion: { NSValue *value = [model valueForKey:property->_name]; propertyDesc = value ? value.description : @"{unknown}"; } break; default: propertyDesc = @""; } } propertyDesc = ModelDescriptionAddIndent(propertyDesc.mutableCopy, 1); [desc appendFormat:@" %@ = %@",property->_name, propertyDesc]; [desc appendString:(i + 1 == max) ? @"\n" : @";\n"]; } [desc appendFormat:@"}"]; return desc; } } } @implementation NSObject (YYModel) + (NSDictionary *)_yy_dictionaryWithJSON:(id)json { if (!json || json == (id)kCFNull) return nil; NSDictionary *dic = nil; NSData *jsonData = nil; if ([json isKindOfClass:[NSDictionary class]]) { dic = json; } else if ([json isKindOfClass:[NSString class]]) { jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding]; } else if ([json isKindOfClass:[NSData class]]) { jsonData = json; } if (jsonData) { dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL]; if (![dic isKindOfClass:[NSDictionary class]]) dic = nil; } return dic; } + (instancetype)yy_modelWithJSON:(id)json { NSDictionary *dic = [self _yy_dictionaryWithJSON:json]; return [self yy_modelWithDictionary:dic]; } + (instancetype)yy_modelWithDictionary:(NSDictionary *)dictionary { if (!dictionary || dictionary == (id)kCFNull) return nil; if (![dictionary isKindOfClass:[NSDictionary class]]) return nil; Class cls = [self class]; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:cls]; if (modelMeta->_hasCustomClassFromDictionary) { cls = [cls modelCustomClassForDictionary:dictionary] ?: cls; } NSObject *one = [cls new]; if ([one yy_modelSetWithDictionary:dictionary]) return one; return nil; } - (BOOL)yy_modelSetWithJSON:(id)json { NSDictionary *dic = [NSObject _yy_dictionaryWithJSON:json]; return [self yy_modelSetWithDictionary:dic]; } - (BOOL)yy_modelSetWithDictionary:(NSDictionary *)dic { if (!dic || dic == (id)kCFNull) return NO; if (![dic isKindOfClass:[NSDictionary class]]) return NO; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:object_getClass(self)]; if (modelMeta->_keyMappedCount == 0) return NO; if (modelMeta->_hasCustomWillTransformFromDictionary) { dic = [((id)self) modelCustomWillTransformFromDictionary:dic]; if (![dic isKindOfClass:[NSDictionary class]]) return NO; } ModelSetContext context = {0}; context.modelMeta = (__bridge void *)(modelMeta); context.model = (__bridge void *)(self); context.dictionary = (__bridge void *)(dic); if (modelMeta->_keyMappedCount >= CFDictionaryGetCount((CFDictionaryRef)dic)) { CFDictionaryApplyFunction((CFDictionaryRef)dic, ModelSetWithDictionaryFunction, &context); if (modelMeta->_keyPathPropertyMetas) { CFArrayApplyFunction((CFArrayRef)modelMeta->_keyPathPropertyMetas, CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_keyPathPropertyMetas)), ModelSetWithPropertyMetaArrayFunction, &context); } if (modelMeta->_multiKeysPropertyMetas) { CFArrayApplyFunction((CFArrayRef)modelMeta->_multiKeysPropertyMetas, CFRangeMake(0, CFArrayGetCount((CFArrayRef)modelMeta->_multiKeysPropertyMetas)), ModelSetWithPropertyMetaArrayFunction, &context); } } else { CFArrayApplyFunction((CFArrayRef)modelMeta->_allPropertyMetas, CFRangeMake(0, modelMeta->_keyMappedCount), ModelSetWithPropertyMetaArrayFunction, &context); } if (modelMeta->_hasCustomTransformFromDictionary) { return [((id)self) modelCustomTransformFromDictionary:dic]; } return YES; } - (id)yy_modelToJSONObject { /* Apple said: The top level object is an NSArray or NSDictionary. All objects are instances of NSString, NSNumber, NSArray, NSDictionary, or NSNull. All dictionary keys are instances of NSString. Numbers are not NaN or infinity. */ id jsonObject = ModelToJSONObjectRecursive(self); if ([jsonObject isKindOfClass:[NSArray class]]) return jsonObject; if ([jsonObject isKindOfClass:[NSDictionary class]]) return jsonObject; return nil; } - (NSData *)yy_modelToJSONData { id jsonObject = [self yy_modelToJSONObject]; if (!jsonObject) return nil; return [NSJSONSerialization dataWithJSONObject:jsonObject options:0 error:NULL]; } - (NSString *)yy_modelToJSONString { NSData *jsonData = [self yy_modelToJSONData]; if (jsonData.length == 0) return nil; return [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; } - (id)yy_modelCopy{ if (self == (id)kCFNull) return self; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:self.class]; if (modelMeta->_nsType) return [self copy]; NSObject *one = [self.class new]; for (_YYModelPropertyMeta *propertyMeta in modelMeta->_allPropertyMetas) { if (!propertyMeta->_getter || !propertyMeta->_setter) continue; if (propertyMeta->_isCNumber) { switch (propertyMeta->_type & YYEncodingTypeMask) { case YYEncodingTypeBool: { bool num = ((bool (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, bool))(void *) objc_msgSend)((id)one, propertyMeta->_setter, num); } break; case YYEncodingTypeInt8: case YYEncodingTypeUInt8: { uint8_t num = ((bool (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, uint8_t))(void *) objc_msgSend)((id)one, propertyMeta->_setter, num); } break; case YYEncodingTypeInt16: case YYEncodingTypeUInt16: { uint16_t num = ((uint16_t (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, uint16_t))(void *) objc_msgSend)((id)one, propertyMeta->_setter, num); } break; case YYEncodingTypeInt32: case YYEncodingTypeUInt32: { uint32_t num = ((uint32_t (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, uint32_t))(void *) objc_msgSend)((id)one, propertyMeta->_setter, num); } break; case YYEncodingTypeInt64: case YYEncodingTypeUInt64: { uint64_t num = ((uint64_t (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, uint64_t))(void *) objc_msgSend)((id)one, propertyMeta->_setter, num); } break; case YYEncodingTypeFloat: { float num = ((float (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, float))(void *) objc_msgSend)((id)one, propertyMeta->_setter, num); } break; case YYEncodingTypeDouble: { double num = ((double (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, double))(void *) objc_msgSend)((id)one, propertyMeta->_setter, num); } break; case YYEncodingTypeLongDouble: { long double num = ((long double (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, long double))(void *) objc_msgSend)((id)one, propertyMeta->_setter, num); } // break; commented for code coverage in next line default: break; } } else { switch (propertyMeta->_type & YYEncodingTypeMask) { case YYEncodingTypeObject: case YYEncodingTypeClass: case YYEncodingTypeBlock: { id value = ((id (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)one, propertyMeta->_setter, value); } break; case YYEncodingTypeSEL: case YYEncodingTypePointer: case YYEncodingTypeCString: { size_t value = ((size_t (*)(id, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_getter); ((void (*)(id, SEL, size_t))(void *) objc_msgSend)((id)one, propertyMeta->_setter, value); } break; case YYEncodingTypeStruct: case YYEncodingTypeUnion: { @try { NSValue *value = [self valueForKey:NSStringFromSelector(propertyMeta->_getter)]; if (value) { [one setValue:value forKey:propertyMeta->_name]; } } @catch (NSException *exception) {} } // break; commented for code coverage in next line default: break; } } } return one; } - (void)yy_modelEncodeWithCoder:(NSCoder *)aCoder { if (!aCoder) return; if (self == (id)kCFNull) { [((id)self)encodeWithCoder:aCoder]; return; } _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:self.class]; if (modelMeta->_nsType) { [((id)self)encodeWithCoder:aCoder]; return; } for (_YYModelPropertyMeta *propertyMeta in modelMeta->_allPropertyMetas) { if (!propertyMeta->_getter) return; if (propertyMeta->_isCNumber) { NSNumber *value = ModelCreateNumberFromProperty(self, propertyMeta); if (value) [aCoder encodeObject:value forKey:propertyMeta->_name]; } else { switch (propertyMeta->_type & YYEncodingTypeMask) { case YYEncodingTypeObject: { id value = ((id (*)(id, SEL))(void *)objc_msgSend)((id)self, propertyMeta->_getter); if (value && (propertyMeta->_nsType || [value respondsToSelector:@selector(encodeWithCoder:)])) { if ([value isKindOfClass:[NSValue class]]) { if ([value isKindOfClass:[NSNumber class]]) { [aCoder encodeObject:value forKey:propertyMeta->_name]; } } else { [aCoder encodeObject:value forKey:propertyMeta->_name]; } } } break; case YYEncodingTypeSEL: { SEL value = ((SEL (*)(id, SEL))(void *)objc_msgSend)((id)self, propertyMeta->_getter); if (value) { NSString *str = NSStringFromSelector(value); [aCoder encodeObject:str forKey:propertyMeta->_name]; } } break; case YYEncodingTypeStruct: case YYEncodingTypeUnion: { if (propertyMeta->_isKVCCompatible && propertyMeta->_isStructAvailableForKeyedArchiver) { @try { NSValue *value = [self valueForKey:NSStringFromSelector(propertyMeta->_getter)]; [aCoder encodeObject:value forKey:propertyMeta->_name]; } @catch (NSException *exception) {} } } break; default: break; } } } } - (id)yy_modelInitWithCoder:(NSCoder *)aDecoder { if (!aDecoder) return self; if (self == (id)kCFNull) return self; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:self.class]; if (modelMeta->_nsType) return self; for (_YYModelPropertyMeta *propertyMeta in modelMeta->_allPropertyMetas) { if (!propertyMeta->_setter) continue; if (propertyMeta->_isCNumber) { NSNumber *value = [aDecoder decodeObjectForKey:propertyMeta->_name]; if ([value isKindOfClass:[NSNumber class]]) { ModelSetNumberToProperty(self, value, propertyMeta); [value class]; } } else { YYEncodingType type = propertyMeta->_type & YYEncodingTypeMask; switch (type) { case YYEncodingTypeObject: { id value = [aDecoder decodeObjectForKey:propertyMeta->_name]; ((void (*)(id, SEL, id))(void *) objc_msgSend)((id)self, propertyMeta->_setter, value); } break; case YYEncodingTypeSEL: { NSString *str = [aDecoder decodeObjectForKey:propertyMeta->_name]; if ([str isKindOfClass:[NSString class]]) { SEL sel = NSSelectorFromString(str); ((void (*)(id, SEL, SEL))(void *) objc_msgSend)((id)self, propertyMeta->_setter, sel); } } break; case YYEncodingTypeStruct: case YYEncodingTypeUnion: { if (propertyMeta->_isKVCCompatible) { @try { NSValue *value = [aDecoder decodeObjectForKey:propertyMeta->_name]; if (value) [self setValue:value forKey:propertyMeta->_name]; } @catch (NSException *exception) {} } } break; default: break; } } } return self; } - (NSUInteger)yy_modelHash { if (self == (id)kCFNull) return [self hash]; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:self.class]; if (modelMeta->_nsType) return [self hash]; NSUInteger value = 0; NSUInteger count = 0; for (_YYModelPropertyMeta *propertyMeta in modelMeta->_allPropertyMetas) { if (!propertyMeta->_isKVCCompatible) continue; value ^= [[self valueForKey:NSStringFromSelector(propertyMeta->_getter)] hash]; count++; } if (count == 0) value = (long)((__bridge void *)self); return value; } - (BOOL)yy_modelIsEqual:(id)model { if (self == model) return YES; if (![model isMemberOfClass:self.class]) return NO; _YYModelMeta *modelMeta = [_YYModelMeta metaWithClass:self.class]; if (modelMeta->_nsType) return [self isEqual:model]; if ([self hash] != [model hash]) return NO; for (_YYModelPropertyMeta *propertyMeta in modelMeta->_allPropertyMetas) { if (!propertyMeta->_isKVCCompatible) continue; id this = [self valueForKey:NSStringFromSelector(propertyMeta->_getter)]; id that = [model valueForKey:NSStringFromSelector(propertyMeta->_getter)]; if (this == that) continue; if (this == nil || that == nil) return NO; if (![this isEqual:that]) return NO; } return YES; } - (NSString *)yy_modelDescription { return ModelDescription(self); } @end @implementation NSArray (YYModel) + (NSArray *)yy_modelArrayWithClass:(Class)cls json:(id)json { if (!json) return nil; NSArray *arr = nil; NSData *jsonData = nil; if ([json isKindOfClass:[NSArray class]]) { arr = json; } else if ([json isKindOfClass:[NSString class]]) { jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding]; } else if ([json isKindOfClass:[NSData class]]) { jsonData = json; } if (jsonData) { arr = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL]; if (![arr isKindOfClass:[NSArray class]]) arr = nil; } return [self yy_modelArrayWithClass:cls array:arr]; } + (NSArray *)yy_modelArrayWithClass:(Class)cls array:(NSArray *)arr { if (!cls || !arr) return nil; NSMutableArray *result = [NSMutableArray new]; for (NSDictionary *dic in arr) { if (![dic isKindOfClass:[NSDictionary class]]) continue; NSObject *obj = [cls yy_modelWithDictionary:dic]; if (obj) [result addObject:obj]; } return result; } @end @implementation NSDictionary (YYModel) + (NSDictionary *)yy_modelDictionaryWithClass:(Class)cls json:(id)json { if (!json) return nil; NSDictionary *dic = nil; NSData *jsonData = nil; if ([json isKindOfClass:[NSDictionary class]]) { dic = json; } else if ([json isKindOfClass:[NSString class]]) { jsonData = [(NSString *)json dataUsingEncoding : NSUTF8StringEncoding]; } else if ([json isKindOfClass:[NSData class]]) { jsonData = json; } if (jsonData) { dic = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:NULL]; if (![dic isKindOfClass:[NSDictionary class]]) dic = nil; } return [self yy_modelDictionaryWithClass:cls dictionary:dic]; } + (NSDictionary *)yy_modelDictionaryWithClass:(Class)cls dictionary:(NSDictionary *)dic { if (!cls || !dic) return nil; NSMutableDictionary *result = [NSMutableDictionary new]; for (NSString *key in dic.allKeys) { if (![key isKindOfClass:[NSString class]]) continue; NSObject *obj = [cls yy_modelWithDictionary:dic[key]]; if (obj) result[key] = obj; } return result; } @end ================================================ FILE: Pods/YYModel/YYModel/YYClassInfo.h ================================================ // // YYClassInfo.h // YYModel // // Created by ibireme on 15/5/9. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import #import NS_ASSUME_NONNULL_BEGIN /** Type encoding's type. */ typedef NS_OPTIONS(NSUInteger, YYEncodingType) { YYEncodingTypeMask = 0xFF, ///< mask of type value YYEncodingTypeUnknown = 0, ///< unknown YYEncodingTypeVoid = 1, ///< void YYEncodingTypeBool = 2, ///< bool YYEncodingTypeInt8 = 3, ///< char / BOOL YYEncodingTypeUInt8 = 4, ///< unsigned char YYEncodingTypeInt16 = 5, ///< short YYEncodingTypeUInt16 = 6, ///< unsigned short YYEncodingTypeInt32 = 7, ///< int YYEncodingTypeUInt32 = 8, ///< unsigned int YYEncodingTypeInt64 = 9, ///< long long YYEncodingTypeUInt64 = 10, ///< unsigned long long YYEncodingTypeFloat = 11, ///< float YYEncodingTypeDouble = 12, ///< double YYEncodingTypeLongDouble = 13, ///< long double YYEncodingTypeObject = 14, ///< id YYEncodingTypeClass = 15, ///< Class YYEncodingTypeSEL = 16, ///< SEL YYEncodingTypeBlock = 17, ///< block YYEncodingTypePointer = 18, ///< void* YYEncodingTypeStruct = 19, ///< struct YYEncodingTypeUnion = 20, ///< union YYEncodingTypeCString = 21, ///< char* YYEncodingTypeCArray = 22, ///< char[10] (for example) YYEncodingTypeQualifierMask = 0xFF00, ///< mask of qualifier YYEncodingTypeQualifierConst = 1 << 8, ///< const YYEncodingTypeQualifierIn = 1 << 9, ///< in YYEncodingTypeQualifierInout = 1 << 10, ///< inout YYEncodingTypeQualifierOut = 1 << 11, ///< out YYEncodingTypeQualifierBycopy = 1 << 12, ///< bycopy YYEncodingTypeQualifierByref = 1 << 13, ///< byref YYEncodingTypeQualifierOneway = 1 << 14, ///< oneway YYEncodingTypePropertyMask = 0xFF0000, ///< mask of property YYEncodingTypePropertyReadonly = 1 << 16, ///< readonly YYEncodingTypePropertyCopy = 1 << 17, ///< copy YYEncodingTypePropertyRetain = 1 << 18, ///< retain YYEncodingTypePropertyNonatomic = 1 << 19, ///< nonatomic YYEncodingTypePropertyWeak = 1 << 20, ///< weak YYEncodingTypePropertyCustomGetter = 1 << 21, ///< getter= YYEncodingTypePropertyCustomSetter = 1 << 22, ///< setter= YYEncodingTypePropertyDynamic = 1 << 23, ///< @dynamic }; /** Get the type from a Type-Encoding string. @discussion See also: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html @param typeEncoding A Type-Encoding string. @return The encoding type. */ YYEncodingType YYEncodingGetType(const char *typeEncoding); /** Instance variable information. */ @interface YYClassIvarInfo : NSObject @property (nonatomic, assign, readonly) Ivar ivar; ///< ivar opaque struct @property (nonatomic, strong, readonly) NSString *name; ///< Ivar's name @property (nonatomic, assign, readonly) ptrdiff_t offset; ///< Ivar's offset @property (nonatomic, strong, readonly) NSString *typeEncoding; ///< Ivar's type encoding @property (nonatomic, assign, readonly) YYEncodingType type; ///< Ivar's type /** Creates and returns an ivar info object. @param ivar ivar opaque struct @return A new object, or nil if an error occurs. */ - (instancetype)initWithIvar:(Ivar)ivar; @end /** Method information. */ @interface YYClassMethodInfo : NSObject @property (nonatomic, assign, readonly) Method method; ///< method opaque struct @property (nonatomic, strong, readonly) NSString *name; ///< method name @property (nonatomic, assign, readonly) SEL sel; ///< method's selector @property (nonatomic, assign, readonly) IMP imp; ///< method's implementation @property (nonatomic, strong, readonly) NSString *typeEncoding; ///< method's parameter and return types @property (nonatomic, strong, readonly) NSString *returnTypeEncoding; ///< return value's type @property (nullable, nonatomic, strong, readonly) NSArray *argumentTypeEncodings; ///< array of arguments' type /** Creates and returns a method info object. @param method method opaque struct @return A new object, or nil if an error occurs. */ - (instancetype)initWithMethod:(Method)method; @end /** Property information. */ @interface YYClassPropertyInfo : NSObject @property (nonatomic, assign, readonly) objc_property_t property; ///< property's opaque struct @property (nonatomic, strong, readonly) NSString *name; ///< property's name @property (nonatomic, assign, readonly) YYEncodingType type; ///< property's type @property (nonatomic, strong, readonly) NSString *typeEncoding; ///< property's encoding value @property (nonatomic, strong, readonly) NSString *ivarName; ///< property's ivar name @property (nullable, nonatomic, assign, readonly) Class cls; ///< may be nil @property (nullable, nonatomic, strong, readonly) NSArray *protocols; ///< may nil @property (nonatomic, assign, readonly) SEL getter; ///< getter (nonnull) @property (nonatomic, assign, readonly) SEL setter; ///< setter (nonnull) /** Creates and returns a property info object. @param property property opaque struct @return A new object, or nil if an error occurs. */ - (instancetype)initWithProperty:(objc_property_t)property; @end /** Class information for a class. */ @interface YYClassInfo : NSObject @property (nonatomic, assign, readonly) Class cls; ///< class object @property (nullable, nonatomic, assign, readonly) Class superCls; ///< super class object @property (nullable, nonatomic, assign, readonly) Class metaCls; ///< class's meta class object @property (nonatomic, readonly) BOOL isMeta; ///< whether this class is meta class @property (nonatomic, strong, readonly) NSString *name; ///< class name @property (nullable, nonatomic, strong, readonly) YYClassInfo *superClassInfo; ///< super class's class info @property (nullable, nonatomic, strong, readonly) NSDictionary *ivarInfos; ///< ivars @property (nullable, nonatomic, strong, readonly) NSDictionary *methodInfos; ///< methods @property (nullable, nonatomic, strong, readonly) NSDictionary *propertyInfos; ///< properties /** If the class is changed (for example: you add a method to this class with 'class_addMethod()'), you should call this method to refresh the class info cache. After called this method, `needUpdate` will returns `YES`, and you should call 'classInfoWithClass' or 'classInfoWithClassName' to get the updated class info. */ - (void)setNeedUpdate; /** If this method returns `YES`, you should stop using this instance and call `classInfoWithClass` or `classInfoWithClassName` to get the updated class info. @return Whether this class info need update. */ - (BOOL)needUpdate; /** Get the class info of a specified Class. @discussion This method will cache the class info and super-class info at the first access to the Class. This method is thread-safe. @param cls A class. @return A class info, or nil if an error occurs. */ + (nullable instancetype)classInfoWithClass:(Class)cls; /** Get the class info of a specified Class. @discussion This method will cache the class info and super-class info at the first access to the Class. This method is thread-safe. @param className A class name. @return A class info, or nil if an error occurs. */ + (nullable instancetype)classInfoWithClassName:(NSString *)className; @end NS_ASSUME_NONNULL_END ================================================ FILE: Pods/YYModel/YYModel/YYClassInfo.m ================================================ // // YYClassInfo.m // YYModel // // Created by ibireme on 15/5/9. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import "YYClassInfo.h" #import YYEncodingType YYEncodingGetType(const char *typeEncoding) { char *type = (char *)typeEncoding; if (!type) return YYEncodingTypeUnknown; size_t len = strlen(type); if (len == 0) return YYEncodingTypeUnknown; YYEncodingType qualifier = 0; bool prefix = true; while (prefix) { switch (*type) { case 'r': { qualifier |= YYEncodingTypeQualifierConst; type++; } break; case 'n': { qualifier |= YYEncodingTypeQualifierIn; type++; } break; case 'N': { qualifier |= YYEncodingTypeQualifierInout; type++; } break; case 'o': { qualifier |= YYEncodingTypeQualifierOut; type++; } break; case 'O': { qualifier |= YYEncodingTypeQualifierBycopy; type++; } break; case 'R': { qualifier |= YYEncodingTypeQualifierByref; type++; } break; case 'V': { qualifier |= YYEncodingTypeQualifierOneway; type++; } break; default: { prefix = false; } break; } } len = strlen(type); if (len == 0) return YYEncodingTypeUnknown | qualifier; switch (*type) { case 'v': return YYEncodingTypeVoid | qualifier; case 'B': return YYEncodingTypeBool | qualifier; case 'c': return YYEncodingTypeInt8 | qualifier; case 'C': return YYEncodingTypeUInt8 | qualifier; case 's': return YYEncodingTypeInt16 | qualifier; case 'S': return YYEncodingTypeUInt16 | qualifier; case 'i': return YYEncodingTypeInt32 | qualifier; case 'I': return YYEncodingTypeUInt32 | qualifier; case 'l': return YYEncodingTypeInt32 | qualifier; case 'L': return YYEncodingTypeUInt32 | qualifier; case 'q': return YYEncodingTypeInt64 | qualifier; case 'Q': return YYEncodingTypeUInt64 | qualifier; case 'f': return YYEncodingTypeFloat | qualifier; case 'd': return YYEncodingTypeDouble | qualifier; case 'D': return YYEncodingTypeLongDouble | qualifier; case '#': return YYEncodingTypeClass | qualifier; case ':': return YYEncodingTypeSEL | qualifier; case '*': return YYEncodingTypeCString | qualifier; case '^': return YYEncodingTypePointer | qualifier; case '[': return YYEncodingTypeCArray | qualifier; case '(': return YYEncodingTypeUnion | qualifier; case '{': return YYEncodingTypeStruct | qualifier; case '@': { if (len == 2 && *(type + 1) == '?') return YYEncodingTypeBlock | qualifier; else return YYEncodingTypeObject | qualifier; } default: return YYEncodingTypeUnknown | qualifier; } } @implementation YYClassIvarInfo - (instancetype)initWithIvar:(Ivar)ivar { if (!ivar) return nil; self = [super init]; _ivar = ivar; const char *name = ivar_getName(ivar); if (name) { _name = [NSString stringWithUTF8String:name]; } _offset = ivar_getOffset(ivar); const char *typeEncoding = ivar_getTypeEncoding(ivar); if (typeEncoding) { _typeEncoding = [NSString stringWithUTF8String:typeEncoding]; _type = YYEncodingGetType(typeEncoding); } return self; } @end @implementation YYClassMethodInfo - (instancetype)initWithMethod:(Method)method { if (!method) return nil; self = [super init]; _method = method; _sel = method_getName(method); _imp = method_getImplementation(method); const char *name = sel_getName(_sel); if (name) { _name = [NSString stringWithUTF8String:name]; } const char *typeEncoding = method_getTypeEncoding(method); if (typeEncoding) { _typeEncoding = [NSString stringWithUTF8String:typeEncoding]; } char *returnType = method_copyReturnType(method); if (returnType) { _returnTypeEncoding = [NSString stringWithUTF8String:returnType]; free(returnType); } unsigned int argumentCount = method_getNumberOfArguments(method); if (argumentCount > 0) { NSMutableArray *argumentTypes = [NSMutableArray new]; for (unsigned int i = 0; i < argumentCount; i++) { char *argumentType = method_copyArgumentType(method, i); NSString *type = argumentType ? [NSString stringWithUTF8String:argumentType] : nil; [argumentTypes addObject:type ? type : @""]; if (argumentType) free(argumentType); } _argumentTypeEncodings = argumentTypes; } return self; } @end @implementation YYClassPropertyInfo - (instancetype)initWithProperty:(objc_property_t)property { if (!property) return nil; self = [super init]; _property = property; const char *name = property_getName(property); if (name) { _name = [NSString stringWithUTF8String:name]; } YYEncodingType type = 0; unsigned int attrCount; objc_property_attribute_t *attrs = property_copyAttributeList(property, &attrCount); for (unsigned int i = 0; i < attrCount; i++) { switch (attrs[i].name[0]) { case 'T': { // Type encoding if (attrs[i].value) { _typeEncoding = [NSString stringWithUTF8String:attrs[i].value]; type = YYEncodingGetType(attrs[i].value); if ((type & YYEncodingTypeMask) == YYEncodingTypeObject && _typeEncoding.length) { NSScanner *scanner = [NSScanner scannerWithString:_typeEncoding]; if (![scanner scanString:@"@\"" intoString:NULL]) continue; NSString *clsName = nil; if ([scanner scanUpToCharactersFromSet: [NSCharacterSet characterSetWithCharactersInString:@"\"<"] intoString:&clsName]) { if (clsName.length) _cls = objc_getClass(clsName.UTF8String); } NSMutableArray *protocols = nil; while ([scanner scanString:@"<" intoString:NULL]) { NSString* protocol = nil; if ([scanner scanUpToString:@">" intoString: &protocol]) { if (protocol.length) { if (!protocols) protocols = [NSMutableArray new]; [protocols addObject:protocol]; } } [scanner scanString:@">" intoString:NULL]; } _protocols = protocols; } } } break; case 'V': { // Instance variable if (attrs[i].value) { _ivarName = [NSString stringWithUTF8String:attrs[i].value]; } } break; case 'R': { type |= YYEncodingTypePropertyReadonly; } break; case 'C': { type |= YYEncodingTypePropertyCopy; } break; case '&': { type |= YYEncodingTypePropertyRetain; } break; case 'N': { type |= YYEncodingTypePropertyNonatomic; } break; case 'D': { type |= YYEncodingTypePropertyDynamic; } break; case 'W': { type |= YYEncodingTypePropertyWeak; } break; case 'G': { type |= YYEncodingTypePropertyCustomGetter; if (attrs[i].value) { _getter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); } } break; case 'S': { type |= YYEncodingTypePropertyCustomSetter; if (attrs[i].value) { _setter = NSSelectorFromString([NSString stringWithUTF8String:attrs[i].value]); } } // break; commented for code coverage in next line default: break; } } if (attrs) { free(attrs); attrs = NULL; } _type = type; if (_name.length) { if (!_getter) { _getter = NSSelectorFromString(_name); } if (!_setter) { _setter = NSSelectorFromString([NSString stringWithFormat:@"set%@%@:", [_name substringToIndex:1].uppercaseString, [_name substringFromIndex:1]]); } } return self; } @end @implementation YYClassInfo { BOOL _needUpdate; } - (instancetype)initWithClass:(Class)cls { if (!cls) return nil; self = [super init]; _cls = cls; _superCls = class_getSuperclass(cls); _isMeta = class_isMetaClass(cls); if (!_isMeta) { _metaCls = objc_getMetaClass(class_getName(cls)); } _name = NSStringFromClass(cls); [self _update]; _superClassInfo = [self.class classInfoWithClass:_superCls]; return self; } - (void)_update { _ivarInfos = nil; _methodInfos = nil; _propertyInfos = nil; Class cls = self.cls; unsigned int methodCount = 0; Method *methods = class_copyMethodList(cls, &methodCount); if (methods) { NSMutableDictionary *methodInfos = [NSMutableDictionary new]; _methodInfos = methodInfos; for (unsigned int i = 0; i < methodCount; i++) { YYClassMethodInfo *info = [[YYClassMethodInfo alloc] initWithMethod:methods[i]]; if (info.name) methodInfos[info.name] = info; } free(methods); } unsigned int propertyCount = 0; objc_property_t *properties = class_copyPropertyList(cls, &propertyCount); if (properties) { NSMutableDictionary *propertyInfos = [NSMutableDictionary new]; _propertyInfos = propertyInfos; for (unsigned int i = 0; i < propertyCount; i++) { YYClassPropertyInfo *info = [[YYClassPropertyInfo alloc] initWithProperty:properties[i]]; if (info.name) propertyInfos[info.name] = info; } free(properties); } unsigned int ivarCount = 0; Ivar *ivars = class_copyIvarList(cls, &ivarCount); if (ivars) { NSMutableDictionary *ivarInfos = [NSMutableDictionary new]; _ivarInfos = ivarInfos; for (unsigned int i = 0; i < ivarCount; i++) { YYClassIvarInfo *info = [[YYClassIvarInfo alloc] initWithIvar:ivars[i]]; if (info.name) ivarInfos[info.name] = info; } free(ivars); } if (!_ivarInfos) _ivarInfos = @{}; if (!_methodInfos) _methodInfos = @{}; if (!_propertyInfos) _propertyInfos = @{}; _needUpdate = NO; } - (void)setNeedUpdate { _needUpdate = YES; } - (BOOL)needUpdate { return _needUpdate; } + (instancetype)classInfoWithClass:(Class)cls { if (!cls) return nil; static CFMutableDictionaryRef classCache; static CFMutableDictionaryRef metaCache; static dispatch_once_t onceToken; static dispatch_semaphore_t lock; dispatch_once(&onceToken, ^{ classCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); metaCache = CFDictionaryCreateMutable(CFAllocatorGetDefault(), 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks); lock = dispatch_semaphore_create(1); }); dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); YYClassInfo *info = CFDictionaryGetValue(class_isMetaClass(cls) ? metaCache : classCache, (__bridge const void *)(cls)); if (info && info->_needUpdate) { [info _update]; } dispatch_semaphore_signal(lock); if (!info) { info = [[YYClassInfo alloc] initWithClass:cls]; if (info) { dispatch_semaphore_wait(lock, DISPATCH_TIME_FOREVER); CFDictionarySetValue(info.isMeta ? metaCache : classCache, (__bridge const void *)(cls), (__bridge const void *)(info)); dispatch_semaphore_signal(lock); } } return info; } + (instancetype)classInfoWithClassName:(NSString *)className { Class cls = NSClassFromString(className); return [self classInfoWithClass:cls]; } @end ================================================ FILE: Pods/YYModel/YYModel/YYModel.h ================================================ // // YYModel.h // YYModel // // Created by ibireme on 15/5/10. // Copyright (c) 2015 ibireme. // // This source code is licensed under the MIT-style license found in the // LICENSE file in the root directory of this source tree. // #import #if __has_include() FOUNDATION_EXPORT double YYModelVersionNumber; FOUNDATION_EXPORT const unsigned char YYModelVersionString[]; #import #import #else #import "NSObject+YYModel.h" #import "YYClassInfo.h" #endif ================================================ FILE: README.en.md ================================================ # ios-async-socket-explorer (EN) A production-ready TCP communication framework for iOS, designed for high-concurrency, weak network environments, and modular architecture in enterprise-level applications. ## ✨ Overview `ios-async-socket-explorer` is an industrial-grade TCP socket framework built on CocoaAsyncSocket, abstracted from real-world enterprise IM systems. - Supports **3000+ concurrent connections**, handling **10,000+ messages daily** - Implements **TLV binary protocol**, **CRC32 checksum**, and **ACK-based reliability mechanism** - Equipped with **RTT-adaptive heartbeat** and **exponential backoff reconnection**, optimized for complex weak network conditions - Features **enterprise-level VIPER architecture**, with **unit test coverage over 85%** - Provides **comprehensive monitoring metrics and full-link tracing** to ensure system observability ## 🔧 Why not just use WebSocket? | Comparison | Advantage of CocoaAsyncSocket | |------------|-------------------------------| | Custom protocol support | Enables binary framing, versioning, compression | | Fine-grained control | Better handling of sessions, retries, and heartbeats | | Enterprise security | Supports TLS, keepalive, session isolation | | Flexibility | Objective-C base, easy Swift integration | ## 🚀 Quick Start **Objective-C Example** ```Objc // 0. Add this in AppDelegate [TJPMessageFactory load]; // 1. Initialize the client TJPIMClient *client = [TJPIMClient shared]; // It's recommended to keep 'client' as a member variable to avoid premature release // 2. Establish different session connections [client connectToHost:@"media.example.com" port:8080 forType:TJPSessionTypeChat]; [client connectToHost:@"media.example.com" port:8081 forType:TJPSessionTypeMedia]; // 3. Create different types of messages TJPTextMessage *textMsg = [[TJPTextMessage alloc] initWithText:@"Hello World!!!!!"]; // 4.1 Send message - specify session manually [client sendMessage:textMsg throughType:TJPSessionTypeChat]; // 4.2 Send message - auto route TJPMediaMessage *mediaMsg = [[TJPMediaMessage alloc] initWithMediaId:@"12345"]; [client sendMessageWithAutoRoute:mediaMsg]; // Automatically routed to media session ``` **Swift Example** ```Swift // 0. Add this in AppDelegate TJPMessageFactory.load() // 1. Initialize the client let client = TJPIMClient.shared // It's recommended to keep 'client' as a property to avoid premature deallocation // 2. Establish different session connections client.connect(toHost: "media.example.com", port: 8080, for: .chat) client.connect(toHost: "media.example.com", port: 8081, for: .media) // 3. Create different types of messages let textMsg = TJPTextMessage(text: "Hello World!!!!!") // 4.1 Send message - specify session manually client.sendMessage(textMsg, through: .chat) // 4.2 Send message - auto route let mediaMsg = TJPMediaMessage(mediaId: "12345") client.sendMessageWithAutoRoute(mediaMsg) // Automatically routed to media session ``` ## License & Disclaimer This project is released under the **MIT License** and intended for personal study and research purposes only. Please be aware of the following before using: 1. You are free to modify and distribute the code, but it's **not recommended** to use it directly in production applications. 2. Ensure your usage **complies with relevant data privacy regulations**. 3. Given the complexity and variability of network environments, you should **fully test the framework before integrating it**. 4. The author **is not responsible** for any issues that may arise from the use of this project. For full license details, see the [LICENSE](./LICENSE) file. ================================================ FILE: README.md ================================================ # ios-async-socket-explorer > [English Version (简要英文版入口) → Click here](./README.en.md) ![GitHub stars](https://img.shields.io/github/stars/CodeAcmen/ios-async-socket-explorer?style=social) ![Platform](https://img.shields.io/badge/platform-iOS-blue.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) > 高性能 iOS TCP 通信治理工程实践 —— 解决复杂异步环境下的协议确定性与稳定性。 ## 🛠️ 技术栈 ![Objective-C](https://img.shields.io/badge/Objective--C-orange?style=flat-square&logo=objective-c) ![TCP/IP](https://img.shields.io/badge/TCP%2FIP-blue?style=flat-square&logo=internetexplorer) ![CocoaAsyncSocket](https://img.shields.io/badge/CocoaAsyncSocket-lightgrey?style=flat-square) ![SSL/TLS](https://img.shields.io/badge/SSL%2FTLS-green?style=flat-square&logo=openssl) ![Typhoon](https://img.shields.io/badge/Typhoon-red?style=flat-square) ![GCD](https://img.shields.io/badge/GCD-purple?style=flat-square&logo=apple) ## 概述 `ios-async-socket-explorer` 是一套基于 CocoaAsyncSocket 封装的生产级通信框架,源自真实企业级 IM 项目实践,致力于提升 iOS 在弱网、高并发场景下的 TCP 通信稳定性、可维护性和扩展能力。 **主要特性:** - 支持 **单连接支持 8000+ PPS(每秒包数) 的极速解析**,**日均处理万级消息** - 实现 **TLV二进制协议 + CRC32校验 + ACK确认机制** - 搭载 **RTT自适应心跳**、**指数退避重连**,支持复杂弱网环境 - 企业级 **VIPER分层架构设计**,单元测试覆盖率>85% - 丰富的**监控指标和全链路追踪**,确保系统可观测性 > 已被用于 B2B IM 服务、物联网通信、弱网移动端场景评估 ## 为什么选型 CocoaAsyncSocket? 尽管 iOS 生态已有多种通信方案(如 Starscream、NSURLSession WebSocket 等),但: | 选型因素 | 原因 | |----------|------| | 高并发 + 底层控制 | CocoaAsyncSocket 支持底层 socket 原生封装,适合定制协议 | | 企业部署场景 | 兼容 TLS、Socket KeepAlive、链路监控等安全与连接策略 | | 可控性强 | 相比 WebSocket,更灵活地实现连接复用、消息确认、重传策略 | | 跨项目适配 | Objective-C 封装 + Swift 调用,适配多技术栈客户端项目 | --- ## 🚀 快速开始 **Objective-C 接入示例** ```Objc // 0. 在AppDelegate中添加 [TJPMessageFactory load]; // 1. 初始化客户端 TJPIMClient *client = [TJPIMClient shared]; //可以进行相关client设置 client最好为成员变量 防止提前释放问题 // 2. 建立不同类型的连接 [client connectToHost:@"media.example.com" port:8080 forType:TJPSessionTypeChat]; [client connectToHost:@"media.example.com" port:8081 forType:TJPSessionTypeMedia] // 3. 创建不同类型消息 TJPTextMessage *textMsg = [[TJPTextMessage alloc] initWithText:@"Hello World!!!!!"]; // 4.1 发送消息 - 手动指定会话 [client sendMessage:textMsg throughType:TJPSessionTypeChat]; // 4.2 发送消息 - 自动路由 TJPMediaMessage *mediaMsg = [[TJPMediaMessage alloc] initWithMediaId:@"12345"]; [client sendMessageWithAutoRoute:mediaMsg]; // 自动路由到媒体会话 ``` **Swift 接入示例** ```Swift // 0. 在AppDelegate中添加 TJPMessageFactory.load // 1. 初始化客户端 let client = TJPIMClient.shared // 可以进行相关client设置 client最好为成员变量 防止提前释放问题 // 2. 建立不同类型的连接 client.connect(toHost: "media.example.com", port: 8080, for: .chat) client.connect(toHost: "media.example.com", port: 8081, for: .media) // 3. 创建不同类型消息 let textMsg = TJPTextMessage(text: "Hello World!!!!!") // 4.1 发送消息 - 手动指定会话 client.sendMessage(textMsg, through: .chat) // 4.2 发送消息 - 自动路由 let mediaMsg = TJPMediaMessage(mediaId: "12345") client.sendMessageWithAutoRoute(mediaMsg) // 自动路由到媒体会话 ``` ## 核心功能 **按需使用**,快速定位需求 | 功能类别 | 核心特性 | 应用场景 | | -------------|----------------------- | --------------------| | **网络通信核心** | 内置心跳保活、断线重连、ACK确认机制 | 即时通讯、IoT设备管理 | | **二进制协议设计** | 自定义TLV结构协议、CRC32校验、高效压缩 | 高吞吐、低延迟场景 | | **高并发优化** | 多路复用连接池、GCD优化、零拷贝传输 | 实时数据同步 | | **现代企业级架构** | VIPER分层架构、注入式解耦框架(Typhoon)、IM防腐层设计 | 大型项目长期维护 | | **弱网优化** | ACK确认机制、指数退避重传、自适应动态心跳 | 移动网络环境通信 | ## 性能指标 - **高并发能力**: 支持峰值3000+并发连接,内存占用1.6GB (约320KB/连接) - **消息吞吐量**: 单连接峰值8,000 pps (约6.4 Mbps),基准测试环境(iPhone 14 Pro) - **线程效率**: 多线程切换耗时占比 < 3%,GCD优化调度 - **弱网表现**: 30%丢包环境下消息可达率>92%,平均延迟<800ms - **响应速度**: 网络恢复后连接重建平均耗时<2秒 - **资源占用**: 相比NSURLSession方案,内存占用减少35%,CPU使用降低28% - **生产验证**: 日均处理万+消息,真实服务于企业客户 ## 🔥 技术亮点
查看技术实现细节 ### 高性能通信会话 - **连接复用能力**:TLS支持 + 单会话支持复用30+次 - **协议解析**:定长头部 + TLV协议 + CRC32校验 - **健壮机制**:指数退避重连 + 心跳保活(15s间隔/30s超时) - **线程安全**:串行队列 + 无锁数据结构,支持高并发访问 - **智能重传机制**:ACK确认重试 + 随机抖动防惊群 ### 企业级多路复用架构 **轻量级会话池设计** - 资源利用率提升80%+ - **智能调度**:基于负载的会话分配 + 故障自动隔离 - **动态扩容**:按需创建会话 + 智能清理(30s周期) - **全局监控**:实时统计命中率、连接数、网络质量 - **容错设计**:网络抖动处理 + 优雅降级机制 ### 完整TCP状态机实现 通过实现完整的TCP状态机,掌握TCP协议的核心机制: - **三次握手**:建立可靠连接 - **快速重传**:提高数据传输效率 - **流量控制**:滑动窗口机制,接收方控制发送速率 - **超时重传机制**:确保数据传输可靠性
## 通信架构设计 通信系统架构 ``` +-------------------------------------------------------+ | 应用层 | | 使用统一API进行网络通信管理 | +-------------------------------------------------------+ | v +-------------------------------------------------------+ | TJPIMClient | | (门面模式: 高级API + 路由管理 + 多通道协调) | | • 消息路由 (文本→聊天会话, 媒体→媒体会话) | | • 通道管理 (支持多种会话类型) | +-------------------------------------------------------+ | v +-------------------------------------------------------+ | TJPNetworkCoordinator | | (网络协调器: 全局网络状态 + 会话生命周期管理) | | • 会话代理管理 | | • 重连策略协调 | +-------------------------------------------------------+ | v +-------------------------------------------------------+ | TJPLightweightSessionPool | | (会话池: 对象复用 + 生命周期管理 + 性能优化) | | • 健康检查 (自动清理无效会话) | | • 预热机制 (提前创建常用会话) | +-------------------------------------------------------+ | v +-------------------------------------------------------+ | TJPConcreteSession | | (具体会话: 单连接管理 + 协议处理 + 状态机) | | • 协议版本协商、智能重传 | | • 心跳检测 (TJPDynamicHeartbeat) | | • 消息确认机制 (可靠性保证) | | • 数据解析 (TJPMessageParser) | +-------------------------------------------------------+ | v +-------------------------------------------------------+ | GCDAsyncSocket | | (底层套接字: 异步IO + 数据传输) | | • 异步数据收发 | | • 连接建立/断开 | | • 底层网络通信 | +-------------------------------------------------------+ ``` - **门面模式**: 统一API入口,简化调用 - **分层设计**: 连接管理与消息处理分离 - **状态机**: 完整实现TCP连接生命周期管理 - **内存安全**: 严格的资源生命周期管理,自动回收避免泄漏 - **并发控制**: 读写分离与串行队列设计,确保线程安全与数据一致性 - **自适应策略**: 基于当前网络质量动态调整传输参数和重试策略 - **开闭原则**: 基于协议设计的可插拔架构,支持业务定制与扩展 ### TLV数据协议设计 二进制高效通信协议,支持协议平滑升级和嵌套结构:
Tag (2字节) Length (4字节) Value (N字节)
业务标识
0x1001=文本消息
0x1002=图片消息
Value部分长度
(不含T和L字段)
原始数据或嵌套TLV
(保留Tag 0xFFFF标记)
**示例数据包**: 文本消息 "Hello" 的TLV编码: [10 01] [00 00 00 05] [48 65 6C 6C 6F] Tag Length Value("Hello") - 采用**大端字节序**,兼容不同硬件平台 - 支持**协议版本协商**,实现向前兼容 - 内置**校验机制**,确保数据完整性 ## 生产级VIPER架构 在iOS项目中,采用VIPER架构模式进行分层设计,提高系统的可维护性和扩展性: - **分层架构**:将网络层、业务逻辑层和UI层解耦,提升代码可维护性。 - **Typhoon注入式框架**:使用Typhoon框架进行依赖注入,减少类之间的耦合,便于单元测试和功能扩展。 ``` +-----------------+ +------------------+ +-----------------+ | | | | | | | View | <--> | Presenter | <--> | Interactor | | (UI Components) | | (Coordinator) | | (Business Logic)| | | | | | | +-----------------+ +------------------+ +-----------------+ ^ | | | +----------------+ +---------------------+ | Entity | | Router | | (Data Models) | | (Navigation Logic) | +----------------+ +---------------------+ ``` ### VIPER架构在IM场景中的应用 基于VIPER架构的消息处理系统特别适合IM场景,提供: - **消息状态管理**: 完整支持发送中、已发送、已读等状态流转 - **多渠道路由**: 支持文本、图片等不同消息类型的专用处理流程 - **UI渲染优化**: 分离数据处理与界面渲染,提升复杂聊天界面性能 - **测试友好**: 业务逻辑完全独立,单元测试覆盖率可达90%以上 ## 项目结构 ``` iOS-Network-Stack-Dive # 项目结构构思(后续会根据实际情况调整) iOS-Network-Stack-Dive/ ├── Docs/ # 文档 │ ├── ArchitectureExtensions/ │ │ └── AspectLoggerDesign.md │ ├── CoreNetworkStackDoc/ │ │ ├── 轻量级多路复用架构设计.md │ │ ├── 协议流程解析图.jpg │ │ ├── TCP链路流转图.jpg │ │ ├── 单元测试用例文档 │ │ ├── ProtocolParseDesign.md │ │ ├── TJPNetworkManagerV2Design.md │ │ └── TJPNetworkV3FinalDesign.md │ ├── VIPER-Integration/ │ │ ├── VIPER-Design.md │ │ └── VIPER-RouterGuide.md │ └── RFC/ # 协议标准文档 │ ├── RFC793-TCP.pdf │ └── RFC768-UDP.pdf ├── Labs/ │ ├── NetworkFundamentals/ │ │ ├── Lab1-Socket-API/ # Socket实践 │ │ └── Lab2-NSStream-Analysis/ # 流解析实验 ├── ArchitectureExtensions/ │ ├── VIPER-Integration/ # 生产级VIPER架构 │ │ ├── NetworkService/ # 网络服务层 │ │ │ ├── ConnectionManager/ # 连接池管理 │ │ │ └── ProtocolAdapter/ # 协议适配器 │ │ ├── VIPER-Demo/ # VIPER架构演示 │ │ └── DI Container/ # 依赖注入实现 │ └── AOP/ │ │ └── LoggingAspect/ # 日志追踪切面 │ └── NetworkMonitor/ # 网络监控 ├── CoreNetworkStack/ │ ├── V1_BasicFunction/ # 最初演示版本(演示TCP问题,并解决问题) │ ├── V2_Concurrency/ # 多并发版本(单链接,此项目中更多用于演示作用) │ ├── TJPIMCore/ # IM核心,多路复用通讯框架 │ └── TransportLayer/ # 传输层Mock └── ProductionBridge/ └── VIPER-Sample/ # 真实项目代码片段 └── MessageModule/ # 消息模块实现 ``` ## 版本历史
📋 版本历史 - **v1.0.0**:网络框架基础核心功能基本完成、生产级VIPER架构演示完成 - **v1.0.1**:修复了因libffi编译导致无法在模拟器运行的问题 - **v1.1.0**:新增全链路追踪、关键指标采集(网络质量/成功率/延迟)并添加演示Demo,引入序列号分区机制,整体逻辑优化 - **v1.2.0**:协议改造为TLV结构,支持协议无缝升级,整体逻辑重构,消息构造和解析逻辑发生本质变化,详见Doc - **v1.2.1**:完善了消息错误机制,遵循单一职责拆分了数据包解析、组装,抽象了连接管理类,优化了握手交换协议版本信息逻辑 - **v1.3.0**:升级动态心跳机制,结合App状态+网络状态,使用更成熟稳定的方案动态调整心跳频率;埋点功能优化,提供更全面的埋点维度 - **v1.3.1**:新增缓冲区配置策略,改造环形缓冲区和传统缓冲区并行,新增轻量级智能切换机制保证运行时无缝切换,增强缓冲区监控调试 - **v1.4.0**:新增会话连接池,整体架构加入池化层,在保持API兼容的前提下,大幅提高性能。支持会话复用、按类型分池存储、负载均衡策略,状态监控升级 - **v1.5.0**:新增消息已读机制,增加聊天实战Demo,当前支持文字消息发送并整体演示消息状态流转。后续聊天页面迭代至VIPER架构
## 后续迭代计划 - **运营商网络适配**: NAT超时处理、运营商防拦截 - **极端环境支持**: 智能升降级策略、极端弱网优化、多级故障恢复 - **高性能传输**: 大文件传输、QoS流量控制 - **IM组件库**: 防腐层设计、聊天UI组件、VIPER架构示例 ## 贡献 欢迎**任何开发者贡献代码、改进文档、提出意见和建议!** 如果你有关于iOS网络栈的实践经验或心得,欢迎提交PR来丰富本项目。 ## License 本项目遵循 **MIT 许可证**,详细信息请查看 [LICENSE](./LICENSE) 文件。 如果这个项目对你有帮助,请点个 ⭐ Star 支持!
推荐学习资源 ### 书籍与文档 - 《计算机网络:自顶向下方法》- 经典教程 - 《TCP/IP详解》- 协议深度解析 - [Apple iOS Network Programming Guide](https://developer.apple.com/documentation/foundation/networking) ### 工具与开源项目 - [CocoaAsyncSocket](https://github.com/robbiehanson/CocoaAsyncSocket) - iOS Socket编程库 - [Wireshark](https://www.wireshark.org/) - 网络协议分析工具 - [Typhoon](https://github.com/appsquickly/Typhoon) - 依赖注入框架
## 许可与免责声明 本项目采用MIT许可证,供个人学习和研究使用。使用时请注意: 1. 允许修改和分发,但建议不要直接用于商业产品 2. 使用本项目时请确保符合数据隐私法规 3. 由于网络环境复杂多变,使用前请充分测试 4. 作者不对因使用本项目可能导致的任何问题负责 详情请查看[LICENSE](./LICENSE)文件。 ## 特别致谢 感谢提出质疑的开发者!您的监督促使我更好地践行开源精神。任何疑问或建议,欢迎通过 issue 或 discussion 提出。 —— 项目维护者 *更新于 2025.06.07* ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/LoggingAspect/TJPAspectCore.h ================================================ // // TJPAspectCore.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // 核心类 #import #import "TJPLogAspectInterface.h" @class TJPLogModel; NS_ASSUME_NONNULL_BEGIN @interface TJPAspectCore : NSObject /// 注册日志切面 + (void)registerLogWithConfig:(TJPLogConfig)config trigger:(TJPLogTriggerPoint)trigger handler:(void(^)(TJPLogModel *log))handler; /// 移除日志切面 + (void)removeLogForClass:(Class)cls; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/LoggingAspect/TJPAspectCore.m ================================================ // // TJPAspectCore.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // #import "TJPAspectCore.h" #import "TJPLogModel.h" #import #import #import //#import /* 结构: { "ClassName1" : { "method1": IMP_1, "method2": IMP_2, }, "ClassName2" : { "method1": IMP_1, "method2": IMPV_2, } } 工作流程设计: 1.获取目标类和方法。 2.保存原始方法的实现。 3.创建一个新的方法实现(newIMP),这个方法实现会包含你自定义的逻辑。 4.替换目标类的原始方法实现为 newIMP。 5.在 newIMP 中,调用原始方法并在方法执行的前后插入日志记录。 */ static NSMutableDictionary *> *_originIMPMap; static os_unfair_lock aspect_lock = OS_UNFAIR_LOCK_INIT; @interface TJPAspectCore () @end @implementation TJPAspectCore + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ _originIMPMap = [NSMutableDictionary dictionary]; }); } + (void)registerLogWithConfig:(TJPLogConfig)config trigger:(TJPLogTriggerPoint)trigger handler:(void (^)(TJPLogModel * _Nonnull))handler { // NSLog(@"[DEBUG] 注册日志方法开始执行"); // //获取目标类 // Class cls = config.targetClass; // SEL originSEL = config.targetSelector; // if (!cls || !originSEL) return; // // //避免出现并发问题 // os_unfair_lock_lock(&aspect_lock); // NSString *clsKey = NSStringFromClass(cls); // NSString *selKey = NSStringFromSelector(originSEL); // NSLog(@"[DEBUG] 原始类 %@ - 原始方法实现 %@", clsKey, selKey); // // //避免重复交换 // if ([_originIMPMap[clsKey] objectForKey:selKey]) { // NSLog(@"[DEBUG] 已经存在钩子: %@ %@", clsKey, selKey); // os_unfair_lock_unlock(&aspect_lock); // return; // }; // // //获取原始方法 // Method originMethod = class_getInstanceMethod(cls, originSEL); // if (!originMethod) return; // // //获取原始方法实现 // IMP originIMP = method_getImplementation(originMethod); // const char *typeEncoding = method_getTypeEncoding(originMethod); // // // //动态生成新的方法实现 核心点! // IMP newIMP = imp_implementationWithBlock(^(id self, SEL _cmd, ...) { // NSLog(@"[DEBUG] ========== Swizzled IMP 被调用: %@ %@ ==========", clsKey, selKey); // @autoreleasepool { // //构造日志模型 // TJPLogModel *logModel = [TJPLogModel new]; // logModel.clsName = clsKey; // logModel.methodName = selKey; // // //方法执行前的切点 // if (trigger & TJPLogTriggerBeforeMethod) { // NSLog(@"[DEBUG] 触发 BEFORE 钩子"); // handler(logModel); // } // // //动态调用原始IMP // void *returnValue = NULL; // //获取原始方法签名 // NSMethodSignature *originSig = [self methodSignatureForSelector:originSEL]; // NSLog(@"[DEBUG] 方法签名: %@", originSig); // NSUInteger numArgs = [originSig numberOfArguments]; // // //使用libffi // ffi_cif cif; // ffi_type *returnType = [TJPAspectCore _ffiTypeForTypeEncoding:originSig.methodReturnType]; // ffi_type **argTypes = malloc(numArgs * sizeof(ffi_type *)); // void **argValues = malloc(numArgs * sizeof(void *)); // // va_list args; // va_start(args, _cmd); // // // 提取参数类型和值 // NSLog(@"[DEBUG] 调用 ffi_call: 准备参数"); // for (NSUInteger i = 0; i < numArgs; i++) { // const char *type = [originSig getArgumentTypeAtIndex:i]; // NSLog(@"[DEBUG] 参数 %lu 类型: %s", (unsigned long)i, type); // // argTypes[i] = [TJPAspectCore _ffiTypeForTypeEncoding:type]; // // if (i == 0) { // self // argValues[i] = &self; // NSLog(@"[DEBUG] 参数 self: %@", self); // } else if (i == 1) { // _cmd // argValues[i] = (void *)&originSEL; // NSLog(@"[DEBUG] 参数 _cmd: %@", NSStringFromSelector(originSEL)); // } else { // 其他参数 // [TJPAspectCore _extractValue:&argValues[i] fromArgs:args type:type]; // NSLog(@"[DEBUG] 参数 %lu 值: %@", (unsigned long)i, argValues[i]); // } // } // va_end(args); // // // 准备ffi调用 // ffi_status status = ffi_prep_cif(&cif, FFI_DEFAULT_ABI, (unsigned int) numArgs, returnType, argTypes); // NSLog(@"[DEBUG] ffi_prep_cif 状态: %d", status); // // if (status != FFI_OK) { // NSLog(@"[ERROR] ffi_prep_cif 失败: %d", status); // os_unfair_lock_unlock(&aspect_lock); // return; // } // // // 分配返回值内存 // if (originSig.methodReturnLength > 0) { // returnValue = malloc(originSig.methodReturnLength); // } // // //使用CACurrentMediaTime为了不受系统时间影响 // NSTimeInterval start = CACurrentMediaTime(); // // // 执行原始IMP // @try { // NSLog(@"[DEBUG] 准备调用原始 IMP: %p", originIMP); // // /*此处不可以使用 [invocation invoke]; // 因为先替换了原方法实现 newIMP -> originIMP // 此时动态生成新的方法实现newIMP方法中, [invocation invoke]会再次调用newIMP, // newIMP <-> newIMP 会无限死循环 // */ // // // 调用原始方法实现(使用 originIMP),确保不会递归调用新方法实现 // // NSLog(@"[DEBUG] 准备调用原始 IMP: %p", originIMP); // ffi_call(&cif, originIMP, returnValue, argValues); // NSLog(@"[DEBUG] 原始 IMP 调用完成, 返回值: %@", returnValue ? [NSValue valueWithPointer:returnValue] : @"无返回值"); // } @catch (NSException *ex) { // NSLog(@"[ERROR] 调用原始方法时发生异常: %@", ex); // if (trigger & TJPLogTriggerOnException) { // logModel.exception = ex; // handler(logModel); // } // @throw; // } @finally { // // 触发切点:调用后 // NSLog(@"[DEBUG] 触发 AFTER 钩子 - 耗时: %f", logModel.executeTime); // if (trigger & TJPLogTriggerAfterMethod) { // logModel.executeTime = CACurrentMediaTime() - start; // handler(logModel); // } // // // 处理返回值 // if (returnValue) { // [TJPAspectCore _processReturnValue:returnValue forSignature:originSig]; // } // // // 释放内存 // free(argTypes); // free(argValues); // if (returnValue) free(returnValue); // } // } // }); // // //替换方法实现 // class_replaceMethod(cls, originSEL, newIMP, typeEncoding); // // // 存储原始IMP // NSMutableDictionary *selDict = _originIMPMap[clsKey] ?: [NSMutableDictionary new]; // [selDict setObject:[NSValue valueWithPointer:originIMP] forKey:selKey]; // [_originIMPMap setObject:selDict forKey:clsKey]; // // os_unfair_lock_unlock(&aspect_lock); // NSLog(@"[DEBUG] 注册日志方法执行结束 ---- "); } + (void)removeLogForClass:(Class)cls { if (!cls) return; //防止并发问题 os_unfair_lock_lock(&aspect_lock); //获取key NSString *clsKey = NSStringFromClass(cls); //取出对应的内层字典 NSDictionary *selDict = _originIMPMap[clsKey]; //遍历取出原方法和方法实现 [selDict enumerateKeysAndObjectsUsingBlock:^(NSString *selKey, NSValue *impValue, BOOL * _Nonnull stop) { SEL originalSEL = NSSelectorFromString(selKey); IMP originalIMP = [impValue pointerValue]; Method currentMethod = class_getInstanceMethod(cls, originalSEL); if (currentMethod) { //恢复 method_setImplementation(currentMethod, originalIMP); NSLog(@"[DEBUG] removeLogForClass success class:%@", cls); } }]; [_originIMPMap removeObjectForKey:clsKey]; os_unfair_lock_unlock(&aspect_lock); } //#pragma mark - Private Helpers //// 类型编码 -> ffi_type 映射 //+ (ffi_type *)_ffiTypeForTypeEncoding:(const char *)encoding { // switch (encoding[0]) { // case 'v': return &ffi_type_void; // case 'c': return &ffi_type_schar; // case 'i': return &ffi_type_sint; // case 's': return &ffi_type_sshort; // case 'l': return &ffi_type_slong; // case 'q': return &ffi_type_sint64; // case 'C': return &ffi_type_uchar; // case 'I': return &ffi_type_uint; // case 'S': return &ffi_type_ushort; // case 'L': return &ffi_type_ulong; // case 'Q': return &ffi_type_uint64; // case 'f': return &ffi_type_float; // case 'd': return &ffi_type_double; // case 'B': return &ffi_type_uint8; // case '@': return &ffi_type_pointer; // case '#': return &ffi_type_pointer; // case ':': return &ffi_type_pointer; // case '{': return [TJPAspectCore _ffiStructTypeForEncoding:encoding]; // default: return &ffi_type_void; // } //} // //// 处理结构体(以CGRect为例) //+ (ffi_type *)_ffiStructTypeForEncoding:(const char *)encoding { // if (strcmp(encoding, @encode(CGRect)) == 0) { // static ffi_type *rectType = NULL; // if (!rectType) { // rectType = malloc(sizeof(ffi_type)); // rectType->type = FFI_TYPE_STRUCT; // rectType->elements = malloc(5 * sizeof(ffi_type *)); // rectType->elements[0] = &ffi_type_float; // x // rectType->elements[1] = &ffi_type_float; // y // rectType->elements[2] = &ffi_type_float; // width // rectType->elements[3] = &ffi_type_float; // height // rectType->elements[4] = NULL; // } // return rectType; // } // return &ffi_type_void; //} // 处理返回值 + (void)_processReturnValue:(void *)returnValue forSignature:(NSMethodSignature *)sig { const char *returnType = sig.methodReturnType; if (strcmp(returnType, @encode(id)) == 0) { id obj = (__bridge id)(*(void **)returnValue); NSLog(@"[RETURN] 对象: %@", obj); } else if (strcmp(returnType, @encode(int)) == 0) { int val = *(int *)returnValue; NSLog(@"[RETURN] 整数: %d", val); } // 扩展其他类型... } + (void)_extractValue:(void **)valuePtr fromArgs:(va_list)args type:(const char *)type { NSLog(@"[DEBUG] va_list 地址: %p", args); NSLog(@"[DEBUG] 当前参数类型: %s", type); if (strcmp(type, @encode(id)) == 0) { // 处理对象类型(id) id obj = va_arg(args, id); if (obj) { NSLog(@"[DEBUG] 提取到对象参数: %@", obj); } else { NSLog(@"[ERROR] 提取到空对象参数"); } *valuePtr = (__bridge void *)obj; }else if (strcmp(type, @encode(SEL)) == 0) { // 处理 SEL 类型 SEL sel = va_arg(args, SEL); NSLog(@"[DEBUG] 提取到 SEL 参数: %@", NSStringFromSelector(sel)); *valuePtr = (void *)sel; // 直接传递 SEL,无需转换字符串 } else if (strcmp(type, @encode(int)) == 0) { // 处理 int 类型 int val = va_arg(args, int); int *storage = malloc(sizeof(int)); *storage = val; *valuePtr = storage; NSLog(@"[DEBUG] 提取到 int 参数: %d", val); } else if (strcmp(type, @encode(float)) == 0) { // 处理 float 类型(注意:va_arg 需用 double 提取) double temp = va_arg(args, double); float val = (float)temp; float *storage = malloc(sizeof(float)); *storage = val; *valuePtr = storage; NSLog(@"[DEBUG] 提取到 float 参数: %f", val); } else if (strcmp(type, @encode(BOOL)) == 0) { // 处理 BOOL 类型(注意:BOOL 实际是 signed char) BOOL val = va_arg(args, int); // BOOL 被提升为 int BOOL *storage = malloc(sizeof(BOOL)); *storage = val; *valuePtr = storage; NSLog(@"[DEBUG] 提取到 BOOL 参数: %d", val); } else if (strcmp(type, @encode(CGRect)) == 0) { // 处理结构体(如 CGRect) CGRect rect = va_arg(args, CGRect); CGRect *storage = malloc(sizeof(CGRect)); *storage = rect; *valuePtr = storage; NSLog(@"[DEBUG] 提取到 CGRect 参数"); } else { // 其他类型处理(可扩展) NSLog(@"[ERROR] 不支持的参数类型: %s", type); *valuePtr = NULL; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/LoggingAspect/TJPLogAspectInterface.h ================================================ // // TJPLogAspectInterface.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // 日志切面接口 #import NS_ASSUME_NONNULL_BEGIN @protocol TJPLogAspectInterface //日志触发点 typedef NS_ENUM(NSUInteger, TJPLogTriggerPoint) { TJPLogTriggerBeforeMethod, //方法执行前 TJPLogTriggerAfterMethod, //方法执行后 TJPLogTriggerOnException //发生异常时 }; //日志配置 方法过滤 typedef struct TJPLogConfig { Class targetClass; //目标类 SEL targetSelector; //目标方法 Protocol *targetProtocol; //目标协议 }TJPLogConfig; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/LoggingAspect/TJPLogModel.h ================================================ // // TJPLogModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // 日志内容模型 #import NS_ASSUME_NONNULL_BEGIN @interface TJPLogModel : NSObject /// 类名 @property (nonatomic, copy) NSString *clsName; /// 方法名 @property (nonatomic, copy) NSString *methodName; /// 参数 @property (nonatomic, strong) NSArray *arguments; /// 执行时间 @property (nonatomic, assign) NSTimeInterval executeTime; /// 异常 @property (nonatomic, strong) NSException *exception; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/LoggingAspect/TJPLogModel.m ================================================ // // TJPLogModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // #import "TJPLogModel.h" @implementation TJPLogModel @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/LoggingAspect/TJPLogger.h ================================================ // // TJPLogger.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // #import NS_ASSUME_NONNULL_BEGIN @class TJPLogModel; @interface TJPLogger : NSObject /// 全局id用于链路追踪 @property (nonatomic, copy, readonly) NSString *traceId; + (instancetype)shared; - (void)log:(TJPLogModel *)log; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/LoggingAspect/TJPLogger.m ================================================ // // TJPLogger.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // #import "TJPLogger.h" #import "TJPLogModel.h" #import "TJPNetworkDefine.h" @interface TJPLogger () @end @implementation TJPLogger { dispatch_queue_t _logQueue; } + (instancetype)shared { static TJPLogger *instace = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instace = [[self alloc] init]; }); return instace; } - (instancetype)init { if (self = [super init]) { _logQueue = dispatch_queue_create("com.tjp.logger.logQuee", DISPATCH_QUEUE_SERIAL); _traceId = [[NSUUID UUID] UUIDString]; } return self; } - (void)log:(TJPLogModel *)log { dispatch_async(self->_logQueue, ^{ NSString *logStr = [NSString stringWithFormat:@"日志记录 [TraceID: %@] - %@.%@ 耗时:%.2fms 参数:%@", self.traceId, log.clsName, log.methodName, log.executeTime * 1000, log.arguments]; TJPLOG_INFO(@"- %@", logStr); }); } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/LoggingAspect/TJPLoggerManager.h ================================================ // // TJPLoggerManager.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // #import #import "TJPLogAspectInterface.h" NS_ASSUME_NONNULL_BEGIN typedef NS_OPTIONS(NSUInteger, TJPLogOutputOption) { TJPLogOutputOptionNone = 0, TJPLogOutputOptionConsole = 1 << 0, // 控制台日志 TJPLogOutputOptionFile = 1 << 1, // 文件日志 TJPLogOutputOptionServer = 1 << 2 // 上传服务器 }; @interface TJPLoggerManager : NSObject /// 注册日志切面的方法 + (void)registerLogForTargetClass:(Class)targetClass targetSelector:(SEL)targetSelector triggers:(TJPLogTriggerPoint)triggers outputs:(TJPLogOutputOption)outputOption; /// 移除切面日志 + (void)removeLogForTargetClass:(Class)targetClass; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/LoggingAspect/TJPLoggerManager.m ================================================ // // TJPLoggerManager.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // #import "TJPLoggerManager.h" #import "TJPLogger.h" #import "TJPLogModel.h" #import "TJPAspectCore.h" @implementation TJPLoggerManager // 注册日志的方法,封装日志配置和注册操作 + (void)registerLogForTargetClass:(Class)targetClass targetSelector:(SEL)targetSelector triggers:(TJPLogTriggerPoint)triggers outputs:(TJPLogOutputOption)outputOption { TJPLogConfig config; config.targetClass = targetClass; config.targetSelector = targetSelector; [TJPAspectCore registerLogWithConfig:config trigger:triggers handler:^(TJPLogModel *log) { if (outputOption & TJPLogOutputOptionConsole) { [[TJPLogger shared] log:log]; } if (outputOption & TJPLogOutputOptionFile) { [self saveLogToFile:log]; } if (outputOption & TJPLogOutputOptionServer) { [self sendLogToServer:log]; } }]; } + (void)removeLogForTargetClass:(Class)targetClass { [TJPAspectCore removeLogForClass:targetClass]; } // 保存日志到文件 + (void)saveLogToFile:(TJPLogModel *)log { // 获取沙盒 Documents 目录 NSString *documentsPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents"]; // 日志文件路径 NSString *logFilePath = [documentsPath stringByAppendingPathComponent:@"logs.txt"]; // 创建文件管理器 NSFileManager *fileManager = [NSFileManager defaultManager]; // 检查文件是否存在,不存在则创建文件 if (![fileManager fileExistsAtPath:logFilePath]) { [fileManager createFileAtPath:logFilePath contents:nil attributes:nil]; } // 生成日志内容 NSString *logContent = [NSString stringWithFormat:@"[%@] %@: %f\n", log.clsName, log.methodName, log.executeTime]; // 将日志追加到文件 NSFileHandle *fileHandle = [NSFileHandle fileHandleForWritingAtPath:logFilePath]; [fileHandle seekToEndOfFile]; [fileHandle writeData:[logContent dataUsingEncoding:NSUTF8StringEncoding]]; [fileHandle closeFile]; } // 发送日志到服务器 + (void)sendLogToServer:(TJPLogModel *)log { // 假设服务器的日志上传接口为 POST 请求 NSURL *url = [NSURL URLWithString:@"https://your-server.com/api/log"]; NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; [request setHTTPMethod:@"POST"]; // 构造日志数据(可以根据实际需要选择合适的日志字段) NSDictionary *logDict = @{ @"clsName" : log.clsName, @"methodName" : log.methodName, @"executeTime" : @(log.executeTime), @"traceID" : [TJPLogger shared].traceId ? [TJPLogger shared].traceId : @"" }; NSError *error; NSData *bodyData = [NSJSONSerialization dataWithJSONObject:logDict options:0 error:&error]; if (error) { NSLog(@"Error serializing log data: %@", error); return; } // 设置请求体 [request setHTTPBody:bodyData]; // 创建网络请求 NSURLSession *session = [NSURLSession sharedSession]; NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (error) { NSLog(@"Failed to send log to server: %@", error); } else { NSLog(@"Successfully sent log to server"); } }]; [dataTask resume]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/TJPLoggerViewController.h ================================================ // // TJPLoggerViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPLoggerViewController : UIViewController - (NSString *)processData:(NSData *)data count:(int)count; - (void)testMethod; - (NSString *)greeting:(NSString *)name; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/AOP/TJPLoggerViewController.m ================================================ // // TJPLoggerViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/26. // #import "TJPLoggerViewController.h" #import "TJPLoggerManager.h" @interface TJPLoggerViewController () @end @implementation TJPLoggerViewController - (NSString *)greeting:(NSString *)name { return name; } - (void)viewDidLoad { [super viewDidLoad]; self.title = @"轻量级切面日志演示"; self.view.backgroundColor = [UIColor whiteColor]; // 注册日志切面 [self registerLoggingForMethods]; // 调用需要日志记录的方法 [self testNoParams]; // [self testOneParam:@"小明"]; // [self testTwoParams:@"小红" age:18]; } - (void)registerLoggingForMethods { [TJPLoggerManager registerLogForTargetClass:[self class] targetSelector:@selector(testNoParams) triggers:(TJPLogTriggerBeforeMethod | TJPLogTriggerAfterMethod) outputs:(TJPLogOutputOptionConsole)]; [TJPLoggerManager registerLogForTargetClass:[self class] targetSelector:@selector(testOneParam:) triggers:(TJPLogTriggerBeforeMethod | TJPLogTriggerAfterMethod) outputs:(TJPLogOutputOptionConsole)]; } - (void)testNoParams { NSLog(@"someMethodThatLogs started"); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSLog(@"after 1.5s someMethodThatLogs..."); }); NSLog(@"someMethodThatLogs ended"); } - (void)testOneParam:(NSString *)name { NSLog(@"testOneParam called with name = %@", name); } - (void)testTwoParams:(NSString *)name age:(NSInteger)age { NSLog(@"testTwoParams called - name: %@, age: %ld", name, (long)age); } - (void)testThreeParams:(NSString *)name age:(NSInteger)age city:(NSString *)city { NSLog(@"testThreeParams - name: %@, age: %ld, city: %@", name, (long)age, city); } - (NSString *)testReturnString { NSLog(@"testReturnString called"); return @"Hello, Log!"; } - (NSInteger)testAdd:(NSInteger)a b:(NSInteger)b { NSLog(@"testAdd called with a = %ld, b = %ld", (long)a, (long)b); return a + b; } - (NSString *)processData:(NSData *)data count:(int)count { NSLog(@"processData data - count: %@, age: %ld", data, (long)count); return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Collector/TJPMetricsCollector.h ================================================ // // TJPMetricsCollector.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/9. // 指标收集器 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN // 以下定义渐进式迁移至 TJPMetricsKeys中统一管理 // 连接相关指标 //extern NSString * const TJPMetricsKeyConnectionAttempts; //extern NSString * const TJPMetricsKeyConnectionSuccess; // 心跳相关指标 //extern NSString * const TJPMetricsKeyHeartbeatSend; //extern NSString * const TJPMetricsKeyHeartbeatLoss; //extern NSString * const TJPMetricsKeyHeartbeatRTT; //extern NSString * const TJPMetricsKeyHeartbeatInterval; //extern NSString * const TJPMetricsKeyHeartbeatTimeoutInterval; // 网络性能指标 //extern NSString * const TJPMetricsKeyRTT; // 流量统计指标 extern NSString * const TJPMetricsKeyBytesSend; extern NSString * const TJPMetricsKeyBytesReceived; // 数据包解析指标 extern NSString * const TJPMetricsKeyParsedPackets; extern NSString * const TJPMetricsKeyParsedPacketsTime; extern NSString * const TJPMetricsKeyParsedBufferSize; extern NSString * const TJPMetricsKeyParseErrors; extern NSString * const TJPMetricsKeyParsedErrorsTime; // 负载统计指标 extern NSString * const TJPMetricsKeyPayloadBytes; extern NSString * const TJPMetricsKeyParserResets; // 消息统计指标 extern NSString * const TJPMetricsKeyMessageSend; // 消息发送总数 extern NSString * const TJPMetricsKeyMessageAcked; // 消息确认总数 extern NSString * const TJPMetricsKeyMessageTimeout; // 消息超时总数 // 消息类型统计指标 extern NSString * const TJPMetricsKeyControlMessageSend; // 控制消息发送数 extern NSString * const TJPMetricsKeyNormalMessageSend; // 普通消息发送数 extern NSString * const TJPMetricsKeyMessageRetried; // 消息重传总数 // 会话错误数和状态指标 extern NSString * const TJPMetricsKeyErrorCount; // 错误总数 extern NSString * const TJPMetricsKeySessionReconnects; // 会话重连次数 extern NSString * const TJPMetricsKeySessionDisconnects; // 会话断开次数 @interface TJPMetricsCollector : NSObject //流量统计 @property (nonatomic, readonly) NSUInteger byteSend; @property (nonatomic, readonly) NSUInteger byteReceived; + (instancetype)sharedInstance; /// 计数器操作 - (void)incrementCounter:(NSString *)key; /// 带增量的计数器 - (void)incrementCounter:(NSString *)key by:(NSUInteger)value; /// 获取计数器 - (NSUInteger)counterValue:(NSString *)key; - (void)addValue:(NSUInteger)value forKey:(NSString *)key; /// 时间样本记录 (秒级单位) - (void)addTimeSample:(NSTimeInterval)duration forKey:(NSString *)key; - (NSTimeInterval)averageDuration:(NSString *)key; /// 连接成功率 - (float)connectSuccessRate; /// 平均往返时间 - (NSTimeInterval)averageRTT; /// 丢包率 - (float)packetLossRate; /// 指定状态平均处理时间 - (NSTimeInterval)averageStateDuration:(TJPConnectState)state; /// 指定事件平均处理时间 - (NSTimeInterval)averageEventDuration:(TJPConnectEvent)event; /// 错误记录 - (void)recordError:(NSError *)error forKey:(NSString *)key; /// 重连错误 - (NSArray *)recentErrors; /// 记录事件 - (void)recordEvent:(NSString *)eventName withParameters:(NSDictionary *)params; - (NSArray *)recentEventsForName:(NSString *)eventName limit:(NSUInteger)limit; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Collector/TJPMetricsCollector.m ================================================ // // TJPMetricsCollector.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/9. // #import "TJPMetricsCollector.h" #import "TJPMetricsKeys.h" #import //NSString * const TJPMetricsKeyConnectionAttempts = @"connection_attempts"; //NSString * const TJPMetricsKeyConnectionSuccess = @"connection_success"; //NSString * const TJPMetricsKeyHeartbeatSend = @"heartbeat_send"; //NSString * const TJPMetricsKeyHeartbeatLoss = @"heartbeat_loss"; //NSString * const TJPMetricsKeyHeartbeatRTT = @"heartbeat_rtt"; //NSString * const TJPMetricsKeyHeartbeatInterval = @"heartbeat_interval"; //NSString * const TJPMetricsKeyHeartbeatTimeoutInterval = @"heartbeat_timeout_interval"; //NSString * const TJPMetricsKeyRTT = @"rtt"; NSString * const TJPMetricsKeyBytesSend = @"bytes_send"; NSString * const TJPMetricsKeyBytesReceived = @"bytes_received"; NSString * const TJPMetricsKeyParsedPackets = @"parsed_packets_total"; NSString * const TJPMetricsKeyParsedPacketsTime = @"parse_packets_time"; NSString * const TJPMetricsKeyParsedBufferSize = @"parser_buffer_size"; NSString * const TJPMetricsKeyParseErrors = @"parse_errors_total"; NSString * const TJPMetricsKeyParsedErrorsTime = @"parse_error_time"; NSString * const TJPMetricsKeyPayloadBytes = @"payload_bytes_total"; NSString * const TJPMetricsKeyParserResets = @"parser_forced_resets"; NSString * const TJPMetricsKeyMessageSend = @"message_send_total"; NSString * const TJPMetricsKeyMessageAcked = @"message_acked_total"; NSString * const TJPMetricsKeyMessageTimeout = @"message_timeout_total"; // 消息类型统计指标 NSString * const TJPMetricsKeyControlMessageSend = @"control_message_send"; NSString * const TJPMetricsKeyNormalMessageSend = @"normal_message_send"; NSString * const TJPMetricsKeyMessageRetried = @"message_retried_total"; NSString * const TJPMetricsKeyErrorCount = @"error_count"; NSString * const TJPMetricsKeySessionReconnects = @"session_reconnects"; NSString * const TJPMetricsKeySessionDisconnects = @"session_disconnects"; @interface TJPMetricsCollector () { os_unfair_lock _lock; NSUInteger _bytesSend; NSUInteger _bytesReceived; } //指标数量监控 @property (nonatomic, strong) NSMutableDictionary *counts; //时间数据存储 @property (nonatomic, strong) NSMutableDictionary *> *timeSeries; //错误存储 @property (nonatomic, strong) NSMutableArray *errors; //事件 @property (nonatomic, strong) NSMutableDictionary *> *events; @end @implementation TJPMetricsCollector + (instancetype)sharedInstance { static TJPMetricsCollector *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[TJPMetricsCollector alloc] init]; }); return instance; } - (instancetype)init { if (self = [super init]) { _lock = OS_UNFAIR_LOCK_INIT; // 初始化计数器 _counts = [NSMutableDictionary dictionaryWithDictionary:@{ // 连接相关 TJPMetricsKeyConnectionAttempts: @0, TJPMetricsKeyConnectionSuccess: @0, // 心跳相关 TJPMetricsKeyHeartbeatSend: @0, TJPMetricsKeyHeartbeatLoss: @0, // 流量统计 TJPMetricsKeyBytesSend: @0, TJPMetricsKeyBytesReceived: @0, // 数据包解析 TJPMetricsKeyParsedPackets: @0, TJPMetricsKeyParsedPacketsTime: @0, TJPMetricsKeyParsedBufferSize: @0, TJPMetricsKeyParseErrors: @0, TJPMetricsKeyParsedErrorsTime: @0, TJPMetricsKeyPayloadBytes: @0, TJPMetricsKeyParserResets: @0, // 消息统计 TJPMetricsKeyMessageSend: @0, TJPMetricsKeyMessageAcked: @0, TJPMetricsKeyMessageTimeout: @0, // 错误和会话状态 TJPMetricsKeyErrorCount: @0, TJPMetricsKeySessionReconnects: @0, TJPMetricsKeySessionDisconnects: @0 }]; // 初始化时间序列 _timeSeries = [NSMutableDictionary dictionary]; // 初始化字节计数器 _bytesSend = 0; _bytesReceived = 0; _events = [NSMutableDictionary dictionary]; } return self; } #pragma mark - 计数器操作 - (void)incrementCounter:(NSString *)key { [self incrementCounter:key by:1]; } - (void)incrementCounter:(NSString *)key by:(NSUInteger)value { if (!key) return; [self performLocked:^{ // 特殊处理字节计数器 if ([key isEqualToString:TJPMetricsKeyBytesSend]) { self->_bytesSend += value; } else if ([key isEqualToString:TJPMetricsKeyBytesReceived]) { self->_bytesReceived += value; }else { NSNumber *currCount = self.counts[key] ?: @0; self.counts[key] = @(currCount.unsignedIntegerValue + value); } }]; } - (NSUInteger)counterValue:(NSString *)key { __block NSUInteger value; [self performLocked:^{ if ([key isEqualToString:TJPMetricsKeyBytesSend]) { value = self->_bytesSend; } else if ([key isEqualToString:TJPMetricsKeyBytesReceived]) { value = self->_bytesReceived; } else { // 如果键不存在,添加一个默认值0 if (!self.counts[key]) { self.counts[key] = @0; } value = [self.counts[key] unsignedIntegerValue]; } }]; return value; } - (void)addValue:(NSUInteger)value forKey:(NSString *)key { [self performLocked:^{ // 如果字典中已存在该key,累加该值 NSNumber *currentValue = self.counts[key]; if (currentValue) { self.counts[key] = @(currentValue.unsignedIntegerValue + value); } else { // 如果字典中不存在该key,初始化该key的值 self.counts[key] = @(value); } }]; } #pragma mark - 时间序列记录 - (void)addTimeSample:(NSTimeInterval)duration forKey:(NSString *)key { [self performLocked:^{ NSMutableArray *samples = self.timeSeries[key]; if (!samples) { samples = [NSMutableArray array]; self.timeSeries[key] = samples; } [samples addObject:@(duration)]; //保留最近1000个样本数据 if (samples.count > 1000) { [samples removeObjectsInRange:NSMakeRange(0, samples.count - 1000)]; } }]; } - (NSTimeInterval)averageDuration:(NSString *)key { __block NSTimeInterval total = 0; __block NSUInteger count = 0; [self performLocked:^{ NSArray *samples = self.timeSeries[key]; count = samples.count; for (NSNumber *num in samples) { total += num.doubleValue; } }]; return count > 0 ? total / count : 0; } #pragma mark - 指标相关 - (float)connectSuccessRate { NSUInteger attempts = [self counterValue:TJPMetricsKeyConnectionAttempts]; NSUInteger success = [self counterValue:TJPMetricsKeyConnectionSuccess]; float ratio = (attempts > 0) ? (float)success / (float)attempts : 0; // 防止除以0 return success > 0 ? ratio : 0; } - (NSTimeInterval)averageRTT { return [self averageDuration:TJPMetricsKeyRTT]; } - (float)packetLossRate { NSUInteger send = [self counterValue:TJPMetricsKeyHeartbeatSend]; NSUInteger loss = [self counterValue:TJPMetricsKeyHeartbeatLoss]; float ratio = (send > 0) ? (float)loss / (float)send : 0; // 防止除以0 return loss > 0 ? ratio : 0; } - (NSTimeInterval)averageStateDuration:(TJPConnectState)state { return [self averageDuration:[NSString stringWithFormat:@"state_%@", state]]; } - (NSTimeInterval)averageEventDuration:(TJPConnectEvent)event { return [self averageDuration:[NSString stringWithFormat:@"event_%@", event]]; } #pragma mark - 错误记录 - (void)recordError:(NSError *)error forKey:(NSString *)key { [self performLocked:^{ if (!self.errors) { self.errors = [NSMutableArray array]; } [self.errors addObject:@{ @"time": [NSDate date], @"key": key ?: @"unknown", @"code": @(error.code), @"message": error.localizedDescription ?: @"No description" }]; // 只保留最近的30条错误 if (self.errors.count > 30) { [self.errors removeObjectsInRange:NSMakeRange(0, self.errors.count - 30)]; } }]; // 增加错误计数 [self incrementCounter:TJPMetricsKeyErrorCount]; } - (NSArray *)recentErrors { __block NSArray *result; [self performLocked:^{ result = [self.errors copy]; }]; return result; } - (void)recordEvent:(NSString *)eventName withParameters:(NSDictionary *)params { [self performLocked:^{ // 确保事件数组存在 NSMutableArray *eventArray = self.events[eventName]; if (!eventArray) { eventArray = [NSMutableArray array]; self.events[eventName] = eventArray; } // 创建事件记录 NSMutableDictionary *eventRecord = [NSMutableDictionary dictionaryWithDictionary:params ?: @{}]; eventRecord[@"timestamp"] = [NSDate date]; // 添加事件 [eventArray addObject:eventRecord]; // 限制每种事件最多存储100条记录 if (eventArray.count > 100) { [eventArray removeObjectsInRange:NSMakeRange(0, eventArray.count - 100)]; } }]; } - (NSArray *)recentEventsForName:(NSString *)eventName limit:(NSUInteger)limit { __block NSArray *result; [self performLocked:^{ NSArray *events = self.events[eventName] ?: @[]; NSUInteger count = MIN(limit, events.count); if (count == 0) { result = @[]; return; } result = [events subarrayWithRange:NSMakeRange(events.count - count, count)]; }]; return result; } #pragma mark - 线程安全操作 - (void)performLocked:(void (^)(void))block { // NSLog(@"准备获取锁 - 线程: %@", [NSThread currentThread]); os_unfair_lock_lock(&_lock); // NSLog(@"已获取锁 - 线程: %@", [NSThread currentThread]); block(); // NSLog(@"准备释放锁 - 线程: %@", [NSThread currentThread]); os_unfair_lock_unlock(&_lock); // NSLog(@"已释放锁 - 线程: %@", [NSThread currentThread]); } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPConcreteSession+TJPMetrics.h ================================================ // // TJPConcreteSession+TJPMetrics.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/16. // 消息发送监控 #import "TJPConcreteSession.h" NS_ASSUME_NONNULL_BEGIN @interface TJPConcreteSession (TJPMetrics) @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPConcreteSession+TJPMetrics.m ================================================ // // TJPConcreteSession+TJPMetrics.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/16. // #import "TJPConcreteSession+TJPMetrics.h" #import #import #import "TJPMetricsCollector.h" #import "TJPSessionProtocol.h" @implementation TJPConcreteSession (TJPMetrics) + (void)initialize { [self enableMessageMetricsMonitoring]; } + (void)enableMessageMetricsMonitoring { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 消息发送 [self swizzleMethod:@selector(sendData:) withMethod:@selector(metrics_sendData:)]; // ack确认 [self swizzleMethod:@selector(handleACKForSequence:) withMethod:@selector(metrics_handleACKForSequence:)]; // 收到消息 [self swizzleMethod:@selector(socket:didReadData:withTag:) withMethod:@selector(metrics_socket:didReadData:withTag:)]; // 监控断开连接 [self swizzleMethod:@selector(disconnectWithReason:) withMethod:@selector(metrics_disconnectWithReason:)]; // 监控重连 [self swizzleMethod:@selector(forceReconnect) withMethod:@selector(metrics_forceReconnect)]; // 监控错误 [self swizzleMethod:@selector(connection:didDisconnectWithError:reason:) withMethod:@selector(metrics_connection:didDisconnectWithError:reason:)]; // 监控重传 [self swizzleMethod:@selector(handleRetransmissionForSequence:) withMethod:@selector(metrics_handleRetransmissionForSequence:)]; // 监控版本协商 [self swizzleMethod:@selector(performVersionHandshake) withMethod:@selector(metrics_performVersionHandshake)]; }); } + (void)swizzleMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector { Method originalMethod = class_getInstanceMethod(self, originalSelector); Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); BOOL didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } // 监控消息发送 - (void)metrics_sendData:(NSData *)data { // 记录消息发送 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyMessageSend]; //记录发送数据量 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyBytesSend by:data.length]; //发送普通消息 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyNormalMessageSend]; // 调用原始方法 [self metrics_sendData:data]; } // 监控消息确认 - (void)metrics_handleACKForSequence:(uint32_t)sequence { // 记录消息确认 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyMessageAcked]; // 调用原始方法 [self metrics_handleACKForSequence:sequence]; } // 埋点接收消息方法 - (void)metrics_socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyBytesReceived by:data.length]; [self metrics_socket:sock didReadData:data withTag:tag]; } // 实现新方法 - (void)metrics_disconnectWithReason:(TJPDisconnectReason)reason { // 记录会话断开 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeySessionDisconnects]; // 调用原始方法 [self metrics_disconnectWithReason:reason]; } - (void)metrics_forceReconnect { // 记录会话重连 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeySessionReconnects]; // 调用原始方法 [self metrics_forceReconnect]; } - (void)metrics_connection:(id)connection didDisconnectWithError:(NSError *)error reason:(TJPDisconnectReason)reason { // 记录错误 if (error) { [[TJPMetricsCollector sharedInstance] recordError:error forKey:@"disconnect"]; } // 调用原始方法 [self metrics_connection:connection didDisconnectWithError:error reason:reason]; } - (void)metrics_handleRetransmissionForSequence:(uint32_t)sequence { // 记录消息重传 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyMessageRetried]; // 调用原始方法 [self metrics_handleRetransmissionForSequence:sequence]; } - (void)metrics_performVersionHandshake { // 记录控制消息 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyControlMessageSend]; // 调用原始方法 [self metrics_performVersionHandshake]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPConnectStateMachine+TJPMetrics.h ================================================ // // TJPConnectStateMachine+TJPMetrics.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/9. // 状态机增强埋点 #import "TJPConnectStateMachine.h" NS_ASSUME_NONNULL_BEGIN @interface TJPConnectStateMachine (TJPMetrics) /// 增强版状态进入时间 @property (nonatomic, assign) NSTimeInterval metrics_stateEnterTime; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPConnectStateMachine+TJPMetrics.m ================================================ // // TJPConnectStateMachine+TJPMetrics.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/9. // #import "TJPConnectStateMachine+TJPMetrics.h" #import #import #import "TJPMetricsCollector.h" @implementation TJPConnectStateMachine (TJPMetrics) + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleSendEvent]; [self swizzleStateSetter]; }); } + (void)swizzleSendEvent { Class class = [self class]; //Hook sendEvent方法 SEL originSEL = @selector(sendEvent:); SEL swizzleSEL = @selector(metrics_sendEvent:); Method originMethod = class_getInstanceMethod(class, originSEL); Method swizzleMethod = class_getInstanceMethod(class, swizzleSEL); // 检查原始方法是否存在 if (!originMethod || !swizzleMethod) { NSLog(@"Method not found!"); return; } method_exchangeImplementations(originMethod, swizzleMethod); } + (void)swizzleStateSetter { //Hook 系统隐式生成的setter方法 SEL setterSEL = NSSelectorFromString(@"setCurrentState:"); if (!setterSEL) return; Class class = [self class]; if (!class_respondsToSelector(class, setterSEL)) { // 关键检查 NSLog(@"setCurrentState: not found!"); return; } SEL originSEL = setterSEL; SEL swizzleSEL = @selector(metrics_setCurrentState:); Method originMethod = class_getInstanceMethod(class, originSEL); Method swizzleMethod = class_getInstanceMethod(class, swizzleSEL); method_exchangeImplementations(originMethod, swizzleMethod); } #pragma mark - Swizzled Method - (void)metrics_sendEvent:(TJPConnectEvent)event { //记录开始时间 NSTimeInterval startTime = CACurrentMediaTime(); //调用原始实现 [self metrics_sendEvent:event]; //记录事件处理耗时 NSTimeInterval duration = CACurrentMediaTime() - startTime; [[TJPMetricsCollector sharedInstance] addTimeSample:duration forKey:[NSString stringWithFormat:@"event_%@", event]]; } - (void)metrics_setCurrentState:(TJPConnectState)newState { if (self.isInitializing) { NSLog(@"[METRICS] 初始化期间,直接设置状态: %@", newState); // 直接调用原始方法,不进行指标收集 [self metrics_setCurrentState:newState]; return; } if (self.metrics_stateEnterTime == 0) { NSLog(@"[METRICS] 首次启动指标收集,状态: %@", newState); self.metrics_stateEnterTime = CACurrentMediaTime(); [self metrics_setCurrentState:newState]; return; } // 递归保护 static NSMutableSet *processingInstances = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ processingInstances = [NSMutableSet set]; }); @synchronized(processingInstances) { NSString *instanceKey = [NSString stringWithFormat:@"%p", self]; if ([processingInstances containsObject:instanceKey]) { NSLog(@"[METRICS] 检测到递归调用,跳过指标收集"); [self metrics_setCurrentState:newState]; return; } [processingInstances addObject:instanceKey]; } @try { // 记录旧状态的进入时间 NSTimeInterval previousEnterTime = self.metrics_stateEnterTime; // 立即更新进入时间到当前时间(新状态的开始时间) self.metrics_stateEnterTime = CACurrentMediaTime(); // 获取旧状态并计算持续时间 TJPConnectState oldState = self.currentState; NSTimeInterval duration = self.metrics_stateEnterTime - previousEnterTime; // 首次设置状态时,oldState 为 nil,不记录 if (oldState && duration > 0 && ![oldState isEqualToString:newState]) { NSString *key = [NSString stringWithFormat:@"state_%@", oldState]; NSLog(@"[METRICS] 状态变化: %@ -> %@, 持续时间: %.3f秒", oldState, newState, duration); [[TJPMetricsCollector sharedInstance] addTimeSample:duration forKey:key]; } // 调用原始实现(更新 currentState) [self metrics_setCurrentState:newState]; } @finally { // 清理递归标记 @synchronized(processingInstances) { NSString *instanceKey = [NSString stringWithFormat:@"%p", self]; [processingInstances removeObject:instanceKey]; } } } #pragma mark - Associated Properties - (NSTimeInterval)metrics_stateEnterTime { return [objc_getAssociatedObject(self, _cmd) doubleValue]; } - (void)setMetrics_stateEnterTime:(NSTimeInterval)metrics_stateEnterTime { objc_setAssociatedObject(self, @selector(metrics_stateEnterTime), @(metrics_stateEnterTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPConnectionManager+TJPMetrics.h ================================================ // // TJPConnectionManager+TJPMetrics.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/15. // 连接埋点 #import "TJPConnectionManager.h" NS_ASSUME_NONNULL_BEGIN @interface TJPConnectionManager (TJPMetrics) @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPConnectionManager+TJPMetrics.m ================================================ // // TJPConnectionManager+TJPMetrics.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/15. // #import "TJPConnectionManager+TJPMetrics.h" #import #import #import "TJPMetricsKeys.h" #import "TJPMetricsCollector.h" @implementation TJPConnectionManager (TJPMetrics) + (void)initialize { [self enableMetricsMonitoring]; } + (void)enableMetricsMonitoring { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleMethod:@selector(connectToHost:port:) withMethod:@selector(metrics_connectToHost:port:)]; [self swizzleMethod:@selector(socket:didConnectToHost:port:) withMethod:@selector(metrics_socket:didConnectToHost:port:)]; }); } + (void)swizzleMethod:(SEL)originalSelector withMethod:(SEL)swizzledSelector { Method originalMethod = class_getInstanceMethod(self, originalSelector); Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); BOOL didAddMethod = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); } } // 埋点连接方法 - (void)metrics_connectToHost:(NSString *)host port:(uint16_t)port{ [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyConnectionAttempts]; [self metrics_connectToHost:host port:port]; } // 连接成功方法 - (void)metrics_socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyConnectionSuccess]; [self metrics_socket:sock didConnectToHost:host port:port]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPDynamicHeartbeat+TJPMetrics.h ================================================ // // TJPDynamicHeartbeat+TJPMetrics.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/11. // #import "TJPDynamicHeartbeat.h" #import "TJPMetricsKeys.h" NS_ASSUME_NONNULL_BEGIN @interface TJPDynamicHeartbeat (TJPMetrics) // 心跳质量指标 @property (nonatomic, readonly) float heartbeatLossRate; // 实时丢包率 @property (nonatomic, readonly) NSTimeInterval avgRTT; // 动态平均往返时延 @property (nonatomic, readonly) NSTimeInterval currentInterval; // 当前心跳间隔 // 事件记录方法(使用字符串键) - (void)recordHeartbeatEvent:(NSString *)eventType withParameters:(nullable NSDictionary *)params; // 获取心跳诊断数据 - (NSDictionary *)getHeartbeatDiagnostics; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPDynamicHeartbeat+TJPMetrics.m ================================================ // // TJPDynamicHeartbeat+TJPMetrics.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/11. // #import "TJPDynamicHeartbeat+TJPMetrics.h" #import #import #import "TJPMetricsCollector.h" @implementation TJPDynamicHeartbeat (TJPMetrics) + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleHeartbeatMethods]; }); } #pragma mark - Method Swizzling + (void)swizzleHeartbeatMethods { // 发送心跳埋点 [self swizzleSelector:@selector(sendHeartbeat) withSelector:@selector(metrics_sendHeartbeat)]; // 心跳发送失败买点 [self swizzleSelector:@selector(sendHeartbeatFailed) withSelector:@selector(metrics_sendHeartbeatFailed)]; // 心跳ACK处理埋点 [self swizzleSelector:@selector(heartbeatACKNowledgedForSequence:) withSelector:@selector(metrics_heartbeatACKNowledgedForSequence:)]; // 心跳超时处理埋点 [self swizzleSelector:@selector(handleHeaderbeatTimeoutForSequence:) withSelector:@selector(metrics_handleHeaderbeatTimeoutForSequence:)]; // 心跳模式改变埋点 [self swizzleSelector:@selector(changeToHeartbeatMode:) withSelector:@selector(metrics_changeToHeartbeatMode:)]; // 开始监控埋点 [self swizzleSelector:@selector(startMonitoring) withSelector:@selector(metrics_startMonitoring)]; // 关闭监控埋点 [self swizzleSelector:@selector(stopMonitoring) withSelector:@selector(metrics_stopMonitoring)]; } + (void)swizzleSelector:(SEL)original withSelector:(SEL)swizzled { Class cls = [self class]; Method originalMethod = class_getInstanceMethod(cls, original); Method swizzledMethod = class_getInstanceMethod(cls, swizzled); method_exchangeImplementations(originalMethod, swizzledMethod); } #pragma mark - Swizzled Methods - (void)metrics_sendHeartbeat { // 埋点记录 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyHeartbeatSend]; [[TJPMetricsCollector sharedInstance] addValue:self.currentInterval forKey:TJPMetricsKeyHeartbeatInterval]; //新增事件记录 [self recordHeartbeatEvent:TJPHeartbeatEventSend withParameters:nil]; // 调用原始方法 [self metrics_sendHeartbeat]; } - (void)metrics_sendHeartbeatFailed { // 埋点记录 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyHeartbeatLoss]; [[TJPMetricsCollector sharedInstance] addValue:self.currentInterval forKey:TJPMetricsKeyHeartbeatTimeoutInterval]; // 新增事件记录,使用统一常量 [self recordHeartbeatEvent:TJPHeartbeatEventFailed withParameters:nil]; // 调用原始方法 [self metrics_sendHeartbeatFailed]; } - (void)metrics_heartbeatACKNowledgedForSequence:(uint32_t)sequence { NSDate *sendTime = self.pendingHeartbeats[@(sequence)]; if (sendTime) { // 计算RTT(毫秒级精度) NSTimeInterval rtt = [[NSDate date] timeIntervalSinceDate:sendTime] * 1000; // 记录关键指标 [[TJPMetricsCollector sharedInstance] addTimeSample:rtt/1000.0 forKey:TJPMetricsKeyRTT]; [[TJPMetricsCollector sharedInstance] addValue:rtt forKey:TJPMetricsKeyHeartbeatRTT]; // 新增事件记录,使用统一常量 [self recordHeartbeatEvent:TJPHeartbeatEventACK withParameters:@{ TJPHeartbeatParamSequence: @(sequence), TJPHeartbeatParamRTT: @(rtt) }]; } // 调用原始方法 [self metrics_heartbeatACKNowledgedForSequence:sequence]; } - (void)metrics_handleHeaderbeatTimeoutForSequence:(uint32_t)sequence { // 记录丢包事件 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyHeartbeatLoss]; [[TJPMetricsCollector sharedInstance] addValue:self.currentInterval forKey:TJPMetricsKeyHeartbeatTimeoutInterval]; // 新增事件记录 [self recordHeartbeatEvent:TJPHeartbeatEventTimeout withParameters:@{ TJPHeartbeatParamSequence: @(sequence) }]; // 调用原始方法 [self metrics_handleHeaderbeatTimeoutForSequence:sequence]; } - (void)metrics_changeToHeartbeatMode:(TJPHeartbeatMode)mode { // 记录模式变更前的状态 TJPHeartbeatMode oldMode = 0; if ([self respondsToSelector:@selector(heartbeatMode)]) { // 此处要通过KVC拿属性名 NSNumber *modeNumber = [self valueForKey:@"heartbeatMode"]; if (modeNumber) { oldMode = [modeNumber unsignedIntegerValue]; } } // 调用原始方法 [self metrics_changeToHeartbeatMode:mode]; // 记录事件 [self recordHeartbeatEvent:TJPHeartbeatEventModeChanged withParameters:@{ TJPHeartbeatParamOldMode: @(oldMode), TJPHeartbeatParamNewMode: @(mode) }]; } - (void)metrics_startMonitoring { // 调用原始方法 [self metrics_startMonitoring]; // 记录事件,使用统一常量 [self recordHeartbeatEvent:TJPHeartbeatEventStarted withParameters:nil]; } - (void)metrics_stopMonitoring { // 记录事件,使用统一常量 [self recordHeartbeatEvent:TJPHeartbeatEventStopped withParameters:nil]; // 调用原始方法 [self metrics_stopMonitoring]; } #pragma mark - Computed Properties - (float)heartbeatLossRate { NSUInteger sent = [[TJPMetricsCollector sharedInstance] counterValue:TJPMetricsKeyHeartbeatSend]; NSUInteger lost = [[TJPMetricsCollector sharedInstance] counterValue:TJPMetricsKeyHeartbeatLoss]; return (sent > 0) ? (float)lost/(float)sent : 0; } - (NSTimeInterval)avgRTT { return [[TJPMetricsCollector sharedInstance] averageDuration:TJPMetricsKeyRTT]; } - (NSDictionary *)getHeartbeatDiagnostics { TJPMetricsCollector *collector = [TJPMetricsCollector sharedInstance]; NSMutableDictionary *diagnostics = [NSMutableDictionary dictionary]; // 基本计数器指标 diagnostics[TJPHeartbeatDiagnosticSendCount] = @([collector counterValue:TJPMetricsKeyHeartbeatSend]); diagnostics[TJPHeartbeatDiagnosticLossCount] = @([collector counterValue:TJPMetricsKeyHeartbeatLoss]); diagnostics[TJPHeartbeatDiagnosticLossRate] = @(self.heartbeatLossRate); // RTT指标 diagnostics[TJPHeartbeatDiagnosticAverageRTT] = @(self.avgRTT); diagnostics[TJPHeartbeatDiagnosticCurrentInterval] = @(self.currentInterval); // 最近事件 if ([collector respondsToSelector:@selector(recentEventsForName:limit:)]) { diagnostics[TJPHeartbeatDiagnosticRecentTimeouts] = [collector recentEventsForName:TJPHeartbeatEventTimeout limit:5]; diagnostics[TJPHeartbeatDiagnosticRecentModeChanges] = [collector recentEventsForName:TJPHeartbeatEventModeChanged limit:3]; diagnostics[TJPHeartbeatDiagnosticRecentSends] = [collector recentEventsForName:TJPHeartbeatEventSend limit:5]; diagnostics[TJPHeartbeatDiagnosticRecentACKs] = [collector recentEventsForName:TJPHeartbeatEventACK limit:5]; } return diagnostics; } #pragma mark - Event Recording - (void)recordHeartbeatEvent:(NSString *)eventType withParameters:(nullable NSDictionary *)params { // 获取公共参数 NSMutableDictionary *eventParams = [NSMutableDictionary dictionaryWithDictionary:params ?: @{}]; // 添加心跳状态作为通用参数 eventParams[TJPHeartbeatParamInterval] = @(self.currentInterval); // 添加心跳模式 if ([self respondsToSelector:@selector(heartbeatMode)]) { NSNumber *heartbeatMode = [self valueForKey:@"heartbeatMode"]; if (heartbeatMode) { eventParams[TJPHeartbeatParamMode] = heartbeatMode; } } // 添加网络状态(如果可用) if ([self respondsToSelector:@selector(networkCondition)]) { id networkCondition = [self valueForKey:@"networkCondition"]; if (networkCondition && [networkCondition respondsToSelector:NSSelectorFromString(@"qualityLevel")]) { NSNumber *qualityLevel = [networkCondition valueForKey:@"qualityLevel"]; if (qualityLevel) { eventParams[TJPHeartbeatParamNetworkQuality] = qualityLevel; } } } // 记录事件 if ([[TJPMetricsCollector sharedInstance] respondsToSelector:@selector(recordEvent:withParameters:)]) { [[TJPMetricsCollector sharedInstance] recordEvent:eventType withParameters:eventParams]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPMessageParser+TJPMetrics.h ================================================ // // TJPMessageParser+TJPMetrics.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/10. // 数据解析增强埋点 #import "TJPMessageParser.h" NS_ASSUME_NONNULL_BEGIN @interface TJPMessageParser (TJPMetrics) @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Metrics/TJPMessageParser+TJPMetrics.m ================================================ // // TJPMessageParser+TJPMetrics.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/10. // #import "TJPMessageParser+TJPMetrics.h" #import #import #import "TJPMetricsCollector.h" #import "TJPParsedPacket.h" @implementation TJPMessageParser (TJPMetrics) + (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self swizzleFeedData]; [self swizzleNextPacket]; [self swizzleReset]; }); } + (void)swizzleFeedData { Class class = [self class]; SEL originSEL = @selector(feedData:); SEL swizzledSEL = @selector(metrics_feedData:); Method originMethod = class_getInstanceMethod(class, originSEL); Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL); method_exchangeImplementations(originMethod, swizzledMethod); } + (void)swizzleNextPacket { Class class = [self class]; SEL originSEL = @selector(nextPacket); SEL swizzledSEL = @selector(metrics_nextPacket); Method originMethod = class_getInstanceMethod(class, originSEL); Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL); method_exchangeImplementations(originMethod, swizzledMethod); } + (void)swizzleReset { Class class = [self class]; SEL originSEL = @selector(reset); SEL swizzledSEL = @selector(metrics_reset); Method originMethod = class_getInstanceMethod(class, originSEL); Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL); method_exchangeImplementations(originMethod, swizzledMethod); } #pragma mark - Swizzled Methods - (void)metrics_feedData:(NSData *)data { // 记录输入流量 [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyBytesReceived by:data.length]; [self metrics_feedData:data]; } - (TJPParsedPacket *)metrics_nextPacket { TJPMetricsCollector *metrics = [TJPMetricsCollector sharedInstance]; // 记录缓冲区状态 [metrics addValue:self.buffer.length forKey:TJPMetricsKeyParsedBufferSize]; NSTimeInterval start = CACurrentMediaTime(); TJPParsedPacket *packet = [self metrics_nextPacket]; NSTimeInterval duration = CACurrentMediaTime() - start; if (packet) { // 成功解析埋点 NSString *typeKey = [NSString stringWithFormat:@"packet_type_%d", packet.header.msgType]; [metrics incrementCounter:typeKey]; [metrics addTimeSample:duration forKey:TJPMetricsKeyParsedPacketsTime]; [metrics incrementCounter:TJPMetricsKeyParsedPackets]; // 有效载荷大小统计 if (packet.payload) { [metrics incrementCounter:TJPMetricsKeyPayloadBytes by:packet.payload.length]; } } else { // 解析失败埋点 [metrics incrementCounter:TJPMetricsKeyParseErrors]; [metrics addTimeSample:duration forKey:TJPMetricsKeyParsedErrorsTime]; } return packet; } - (void)metrics_reset { // 记录异常重置事件 if (self.buffer.length > 0) { [[TJPMetricsCollector sharedInstance] incrementCounter:TJPMetricsKeyParserResets]; } [self metrics_reset]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/MetricsHeader/TJPMetricsKeys.h ================================================ // // TJPMetricsKeys.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/18. // #import #pragma mark - 基础指标键 extern NSString * const TJPMetricsKeyConnectionAttempts; extern NSString * const TJPMetricsKeyConnectionSuccess; #pragma mark - 网络相关指标 extern NSString * const TJPMetricsKeyRTT; // 通用RTT指标(s) #pragma mark - 心跳指标相关 // 基本计数指标 extern NSString * const TJPMetricsKeyHeartbeatSend; // 心跳发送次数 extern NSString * const TJPMetricsKeyHeartbeatLoss; // 心跳丢失次数 extern NSString * const TJPMetricsKeyHeartbeatRTT; // 心跳RTT值(ms) extern NSString * const TJPMetricsKeyHeartbeatInterval; // 心跳间隔(s) extern NSString * const TJPMetricsKeyHeartbeatTimeoutInterval; // 心跳超时间隔(s) // 心跳事件类型键 extern NSString * const TJPHeartbeatEventSend; // 心跳发送事件 extern NSString * const TJPHeartbeatEventACK; // 心跳确认事件 extern NSString * const TJPHeartbeatEventTimeout; // 心跳超时事件 extern NSString * const TJPHeartbeatEventFailed; // 心跳发送失败事件 extern NSString * const TJPHeartbeatEventModeChanged; // 心跳模式变更事件 extern NSString * const TJPHeartbeatEventIntervalChanged; // 心跳间隔变更事件 extern NSString * const TJPHeartbeatEventStarted; // 心跳监控启动事件 extern NSString * const TJPHeartbeatEventStopped; // 心跳监控停止事件 // 心跳状态参数键 用于事件参数中的字段名 extern NSString * const TJPHeartbeatParamSequence; // 序列号字段 extern NSString * const TJPHeartbeatParamRTT; // RTT字段 extern NSString * const TJPHeartbeatParamInterval; // 当前间隔字段 extern NSString * const TJPHeartbeatParamMode; // 心跳模式字段 extern NSString * const TJPHeartbeatParamOldMode; // 旧心跳模式字段 extern NSString * const TJPHeartbeatParamNewMode; // 新心跳模式字段 extern NSString * const TJPHeartbeatParamNetworkQuality; // 网络质量字段 extern NSString * const TJPHeartbeatParamState; // 状态字段 extern NSString * const TJPHeartbeatParamReason; // 原因字段 // 心跳诊断键 extern NSString * const TJPHeartbeatDiagnosticSendCount; extern NSString * const TJPHeartbeatDiagnosticLossCount; extern NSString * const TJPHeartbeatDiagnosticLossRate; // 时间指标键 extern NSString * const TJPHeartbeatDiagnosticAverageRTT; extern NSString * const TJPHeartbeatDiagnosticCurrentInterval; // 事件历史键 extern NSString * const TJPHeartbeatDiagnosticRecentTimeouts; extern NSString * const TJPHeartbeatDiagnosticRecentModeChanges; extern NSString * const TJPHeartbeatDiagnosticRecentSends; extern NSString * const TJPHeartbeatDiagnosticRecentACKs; ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/MetricsHeader/TJPMetricsKeys.m ================================================ // // TJPMetricsKeys.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/18. // #import "TJPMetricsKeys.h" #pragma mark - 基础指标键 NSString * const TJPMetricsKeyConnectionAttempts = @"connection_attempts"; NSString * const TJPMetricsKeyConnectionSuccess = @"connection_success"; #pragma mark - 网络相关指标 NSString * const TJPMetricsKeyRTT = @"rtt"; #pragma mark - 心跳相关指标 NSString * const TJPMetricsKeyHeartbeatSend = @"heartbeat_send"; NSString * const TJPMetricsKeyHeartbeatLoss = @"heartbeat_loss"; NSString * const TJPMetricsKeyHeartbeatRTT = @"heartbeat_rtt"; NSString * const TJPMetricsKeyHeartbeatInterval = @"heartbeat_interval"; NSString * const TJPMetricsKeyHeartbeatTimeoutInterval = @"heartbeat_timeout_interval"; // 心跳事件类型 NSString * const TJPHeartbeatEventSend = @"heartbeat_event_send"; NSString * const TJPHeartbeatEventACK = @"heartbeat_event_ack"; NSString * const TJPHeartbeatEventTimeout = @"heartbeat_event_timeout"; NSString * const TJPHeartbeatEventFailed = @"heartbeat_event_failed"; NSString * const TJPHeartbeatEventModeChanged = @"heartbeat_event_mode_changed"; NSString * const TJPHeartbeatEventIntervalChanged = @"heartbeat_eventinterval_changed"; NSString * const TJPHeartbeatEventStarted = @"heartbeat_event_started"; NSString * const TJPHeartbeatEventStopped = @"heartbeat_event_stopped"; // 心跳状态类型 NSString * const TJPHeartbeatParamSequence = @"heartbeat_param_sequence"; NSString * const TJPHeartbeatParamRTT = @"heartbeat_param_rtt"; NSString * const TJPHeartbeatParamInterval = @"heartbeat_param_interval"; NSString * const TJPHeartbeatParamMode = @"heartbeat_param_mode"; NSString * const TJPHeartbeatParamOldMode = @"heartbeat_param_old_mode"; NSString * const TJPHeartbeatParamNewMode = @"heartbeat_param_new_mode"; NSString * const TJPHeartbeatParamNetworkQuality = @"heartbeat_param_network_quality"; NSString * const TJPHeartbeatParamState = @"heartbeat_param_state"; NSString * const TJPHeartbeatParamReason = @"heartbeat_param_reason"; #pragma mark - 心跳诊断键 // 基本计数指标键 NSString * const TJPHeartbeatDiagnosticSendCount = @"heartbeat_send_count"; NSString * const TJPHeartbeatDiagnosticLossCount = @"heartbeat_loss_count"; NSString * const TJPHeartbeatDiagnosticLossRate = @"heartbeat_loss_rate"; // 时间指标键 NSString * const TJPHeartbeatDiagnosticAverageRTT = @"average_rtt"; NSString * const TJPHeartbeatDiagnosticCurrentInterval = @"current_interval"; // 事件历史键 NSString * const TJPHeartbeatDiagnosticRecentTimeouts = @"recent_timeouts"; NSString * const TJPHeartbeatDiagnosticRecentModeChanges = @"recent_mode_changes"; NSString * const TJPHeartbeatDiagnosticRecentSends = @"recent_sends"; NSString * const TJPHeartbeatDiagnosticRecentACKs = @"recent_acks"; ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Reporter/TJPMetricsConsoleReporter.h ================================================ // // TJPMetricsConsoleReporter.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/9. // 控制台输出 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @class TJPNetworkConfig; @interface TJPMetricsConsoleReporter : NSObject /** * 报告回调,用于自定义报告处理 */ @property (nonatomic, copy, nullable) void (^reportCallback)(NSString *report); /** * 是否正在运行 */ @property (nonatomic, class, readonly) BOOL isRunning; /** * 当前指标收集级别 */ @property (nonatomic, class, readonly) TJPMetricsLevel currentLevel; /** * 获取单例实例 */ + (instancetype)sharedInstance; /** * 启动控制台输出 默认15s间隔,标准级别 */ + (void)start; /** * 自定义时间间隔启动控制台输出,标准级别 * @param interval 报告输出间隔(秒) */ + (void)startWithInterval:(NSTimeInterval)interval; /** * 使用指定级别启动指标收集 * @param level 指标收集级别 */ + (void)startWithLevel:(TJPMetricsLevel)level; /** * 使用配置启动指标收集 * @param config 网络配置 */ + (void)startWithConfig:(TJPNetworkConfig *)config; /** * 使用完整配置启动指标收集 * @param level 指标收集级别 * @param consoleEnabled 是否在控制台输出 * @param interval 报告间隔(秒) */ + (void)startWithLevel:(TJPMetricsLevel)level consoleEnabled:(BOOL)consoleEnabled interval:(NSTimeInterval)interval; /** * 停止指标收集 */ + (void)stop; /** * 立即触发一次报告生成和输出 */ + (void)flush; /** * 生成当前指标报告 * @return 指标报告字符串 */ + (NSString *)generateReport; /** * 将指标级别转为可读字符串 * @param level 指标级别 * @return 级别对应的字符串描述 */ + (NSString *)metricsLevelToString:(TJPMetricsLevel)level; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/Reporter/TJPMetricsConsoleReporter.m ================================================ // // TJPMetricsConsoleReporter.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/9. // #import "TJPMetricsConsoleReporter.h" #import #import "TJPMetricsCollector.h" #import "TJPNetworkConfig.h" #import "TJPNetworkDefine.h" #import "TJPMetricsKeys.h" // 静态变量 static os_unfair_lock _reportLock = OS_UNFAIR_LOCK_INIT; static dispatch_source_t _timer; static BOOL _isRunning = NO; static TJPMetricsLevel _currentLevel = TJPMetricsLevelStandard; static BOOL _consoleEnabled = YES; static NSTimeInterval _reportInterval = 15.0; @implementation TJPMetricsConsoleReporter #pragma mark - Init + (instancetype)sharedInstance { static TJPMetricsConsoleReporter *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[TJPMetricsConsoleReporter alloc] init]; }); return instance; } - (void)dealloc { TJPLogDealloc(); } #pragma mark - Private Method + (BOOL)isRunning { return _isRunning; } + (TJPMetricsLevel)currentLevel { return _currentLevel; } #pragma markdown - Public Method + (void)start { [self startWithLevel:TJPMetricsLevelStandard consoleEnabled:YES interval:15.0]; } + (void)startWithInterval:(NSTimeInterval)interval { [self startWithLevel:TJPMetricsLevelStandard consoleEnabled:YES interval:interval]; } + (void)startWithLevel:(TJPMetricsLevel)level { [self startWithLevel:level consoleEnabled:YES interval:15.0]; } + (void)startWithConfig:(TJPNetworkConfig *)config { [self startWithLevel:config.metricsLevel consoleEnabled:config.metricsConsoleEnabled interval:config.metricsReportInterval]; } + (void)startWithLevel:(TJPMetricsLevel)level consoleEnabled:(BOOL)consoleEnabled interval:(NSTimeInterval)interval { if (_isRunning) { // 停止已有的定时器 [self stop]; }; // 如果level为None级别,直接返回 if (level == TJPMetricsLevelNone) return; // 更新配置 _currentLevel = level; _consoleEnabled = consoleEnabled; _reportInterval = interval; _isRunning = YES; // 创建定时器 dispatch_queue_t queue = dispatch_queue_create("com.tjp.network.monitor.queue", DISPATCH_QUEUE_SERIAL); _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_timer(_timer, DISPATCH_TIME_NOW, interval * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC); dispatch_source_set_event_handler(_timer, ^{ [self printMetrics]; }); // 启动定时器 dispatch_resume(_timer); TJPLOG_INFO(@"开始执行指标打印 - 级别: %@", [self metricsLevelToString:_currentLevel]); } + (void)stop { if (!_isRunning) { return; } if (_timer) { dispatch_source_cancel(_timer); _timer = nil; } _isRunning = NO; TJPLOG_INFO(@"指标打印停止"); } + (void)flush { [self printMetrics]; } #pragma mark - Core logic + (void)printMetrics { // 生成报告 NSString *report = [self generateReport]; // 是否开启控制台打印 if (_consoleEnabled) { printf("%s\n", report.UTF8String); } if ([TJPMetricsConsoleReporter sharedInstance].reportCallback) { [TJPMetricsConsoleReporter sharedInstance].reportCallback([report copy]); } } + (NSString *)generateReport { os_unfair_lock_lock(&_reportLock); TJPMetricsCollector *collector = [TJPMetricsCollector sharedInstance]; // 时间戳 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.dateFormat = @"HH:mm:ss.SSS"; NSString *timestamp = [formatter stringFromDate:[NSDate date]]; // 构建中文报告 NSMutableString *report = [NSMutableString stringWithFormat:@"\n网络指标报告 时间 - %@\n", timestamp]; // 连接指标 所有级别 [report appendFormat:@"\n[连接状态]\n 尝试次数: %lu\n 成功次数: %lu (成功率: %.1f%%)\n", [collector counterValue:TJPMetricsKeyConnectionAttempts], [collector counterValue:TJPMetricsKeyConnectionSuccess], [collector connectSuccessRate] * 100]; // 流量统计 标准级别以上 if (_currentLevel >= TJPMetricsLevelStandard) { [report appendFormat:@"\n[流量统计]\n 发送数据: %.2f KB\n 接收数据: %.2f KB\n", [collector counterValue:TJPMetricsKeyBytesSend] / 1024.0, [collector counterValue:TJPMetricsKeyBytesReceived] / 1024.0]; } // 心跳数据 标准级别以上 if (_currentLevel >= TJPMetricsLevelStandard) { [report appendFormat:@"\n[心跳检测]\n 发送心跳: %lu 次\n 丢失心跳: %lu 次 (丢包率: %.1f%%)\n 平均RTT: %.1fms\n", [collector counterValue:TJPMetricsKeyHeartbeatSend], [collector counterValue:TJPMetricsKeyHeartbeatLoss], [collector packetLossRate] * 100, [collector averageRTT] * 1000]; // 添加最近事件摘要 NSArray *modeChanges = [collector recentEventsForName:TJPHeartbeatEventModeChanged limit:1]; if (modeChanges.count > 0) { NSDictionary *lastChange = modeChanges.firstObject; NSNumber *oldMode = lastChange[TJPHeartbeatParamOldMode]; NSNumber *newMode = lastChange[TJPHeartbeatParamNewMode]; [report appendFormat:@"\n[最近模式变更]: %@ -> %@\n", [self heartbeatModeString:[oldMode integerValue]], [self heartbeatModeString:[newMode integerValue]]]; } } // 网络质量 所有级别 [report appendFormat:@"\n[网络质量]\n 平均往返时间: %.1fms\n", [collector averageRTT] * 1000]; // 消息统计 详细级别以上 if (_currentLevel >= TJPMetricsLevelStandard) { [report appendFormat:@"\n[消息统计]\n 发送消息: %lu 条\n 确认消息: %lu 条\n", (unsigned long)[collector counterValue:TJPMetricsKeyMessageSend], (unsigned long)[collector counterValue:TJPMetricsKeyMessageAcked]]; } // 调试信息 调试级别 if (_currentLevel >= TJPMetricsLevelDebug) { [report appendString:@"\n[调试信息]\n"]; // 会话状态 [report appendFormat:@" 断开次数: %lu 次\n 重连次数: %lu 次\n", (unsigned long)[collector counterValue:TJPMetricsKeySessionDisconnects], (unsigned long)[collector counterValue:TJPMetricsKeySessionReconnects]]; // 错误统计 [report appendFormat:@" 错误总数: %lu 个\n", (unsigned long)[collector counterValue:TJPMetricsKeyErrorCount]]; // 最近错误 NSArray *recentErrors = [collector recentErrors]; if (recentErrors.count > 0) { [report appendString:@" 最近错误:\n"]; // 最多显示5条最新错误 NSInteger startIndex = MAX(0, recentErrors.count - 5); for (NSInteger i = startIndex; i < recentErrors.count; i++) { NSDictionary *error = recentErrors[i]; NSDate *time = error[@"time"]; NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.dateFormat = @"HH:mm:ss"; [report appendFormat:@" %@: [%@] %@ (代码: %@)\n", [formatter stringFromDate:time], error[@"key"], error[@"message"], error[@"code"]]; } } } os_unfair_lock_unlock(&_reportLock); return [report copy]; } + (NSString *)metricsLevelToString:(TJPMetricsLevel)level { switch (level) { case TJPMetricsLevelNone: return @"无"; case TJPMetricsLevelBasic: return @"基本"; case TJPMetricsLevelStandard: return @"标准"; case TJPMetricsLevelDetailed: return @"详细"; case TJPMetricsLevelDebug: return @"调试"; default: return @"未知"; } } + (NSString *)heartbeatModeString:(NSInteger)mode { switch (mode) { case 0: return @"前台"; case 1: return @"后台"; case 2: return @"暂停"; case 3: return @"省电"; default: return @"未知"; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/TJPNetworkMonitorViewController.h ================================================ // // TJPNetworkMonitorViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/14. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPNetworkMonitorViewController : UIViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/TJPNetworkMonitorViewController.m ================================================ // // TJPNetworkMonitorViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/14. // #import "TJPNetworkMonitorViewController.h" #import "TJPNetworkConfig.h" #import "TJPConcreteSession.h" #import "TJPNetworkCoordinator.h" #import "TJPMockFinalVersionTCPServer.h" #import "TJPMetricsConsoleReporter.h" #import "TJPIMClient.h" #import "TJPTextMessage.h" #import "TJPNetworkDefine.h" @interface TJPNetworkMonitorViewController () @property (nonatomic, strong) TJPMockFinalVersionTCPServer *mockServer; @property (nonatomic, strong) TJPConcreteSession *session; @property (nonatomic, strong) UIButton *sendMessageButton; @property (nonatomic, strong) UITextView *logTextView; @property (nonatomic, strong) TJPIMClient *client; // 新增:状态显示标签 @property (nonatomic, strong) UILabel *connectionStatusLabel; @property (nonatomic, strong) UIButton *connectButton; @property (nonatomic, strong) UIButton *disconnectButton; @property (nonatomic, strong) UIButton *sendMediaButton; // 新增:定时器用于更新状态显示 @property (nonatomic, strong) NSTimer *statusUpdateTimer; @end @implementation TJPNetworkMonitorViewController - (void)dealloc { TJPLogDealloc(); // 清理定时器 [self.statusUpdateTimer invalidate]; self.statusUpdateTimer = nil; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.title = @"数据监控演示"; [self setupUI]; [self setupNetwork]; [self startStatusMonitoring]; // 设置指标报告回调 [[TJPMetricsConsoleReporter sharedInstance] setReportCallback:^(NSString * _Nonnull report) { [self logMessage:report]; }]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.mockServer stop]; [self.client disconnectAll]; [self.statusUpdateTimer invalidate]; } #pragma mark - UI Setup - (void)setupUI { CGFloat currentY = 100; // 连接状态标签 [self setupConnectionStatusLabelWithY:currentY]; currentY += 50; // 控制按钮 [self setupControlButtonsWithY:currentY]; currentY += 120; // 日志文本视图 [self setupLogTextViewWithY:currentY]; currentY = CGRectGetMaxY(self.logTextView.frame) + 20; // 消息发送按钮 [self setupMessageButtonsWithY:currentY]; } - (void)setupConnectionStatusLabelWithY:(CGFloat)y { self.connectionStatusLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, y, self.view.frame.size.width - 20, 30)]; self.connectionStatusLabel.textAlignment = NSTextAlignmentCenter; self.connectionStatusLabel.backgroundColor = [UIColor lightGrayColor]; self.connectionStatusLabel.text = @"连接状态: 未连接"; self.connectionStatusLabel.font = [UIFont boldSystemFontOfSize:16]; [self.view addSubview:self.connectionStatusLabel]; } - (void)setupControlButtonsWithY:(CGFloat)y { // 连接按钮 self.connectButton = [UIButton buttonWithType:UIButtonTypeSystem]; self.connectButton.frame = CGRectMake(20, y, 100, 40); [self.connectButton setTitle:@"连接" forState:UIControlStateNormal]; [self.connectButton setBackgroundColor:[UIColor systemBlueColor]]; [self.connectButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [self.connectButton addTarget:self action:@selector(connectButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.connectButton]; // 断开按钮 self.disconnectButton = [UIButton buttonWithType:UIButtonTypeSystem]; self.disconnectButton.frame = CGRectMake(140, y, 100, 40); [self.disconnectButton setTitle:@"断开" forState:UIControlStateNormal]; [self.disconnectButton setBackgroundColor:[UIColor systemRedColor]]; [self.disconnectButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [self.disconnectButton addTarget:self action:@selector(disconnectButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.disconnectButton]; // 状态查询按钮 UIButton *statusButton = [UIButton buttonWithType:UIButtonTypeSystem]; statusButton.frame = CGRectMake(260, y, 100, 40); [statusButton setTitle:@"查询状态" forState:UIControlStateNormal]; [statusButton setBackgroundColor:[UIColor systemGreenColor]]; [statusButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [statusButton addTarget:self action:@selector(queryStatusButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:statusButton]; } - (void)setupLogTextViewWithY:(CGFloat)y { self.logTextView = [[UITextView alloc] initWithFrame:CGRectMake(10, y, self.view.frame.size.width - 20, 350)]; self.logTextView.editable = NO; self.logTextView.backgroundColor = [UIColor lightGrayColor]; self.logTextView.font = [UIFont systemFontOfSize:12]; [self.view addSubview:self.logTextView]; } - (void)setupMessageButtonsWithY:(CGFloat)y { // 发送文本消息按钮 self.sendMessageButton = [UIButton buttonWithType:UIButtonTypeSystem]; self.sendMessageButton.frame = CGRectMake(20, y, 150, 40); [self.sendMessageButton setTitle:@"发送文本消息" forState:UIControlStateNormal]; [self.sendMessageButton setBackgroundColor:[UIColor systemOrangeColor]]; [self.sendMessageButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [self.sendMessageButton addTarget:self action:@selector(sendTextMessageButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.sendMessageButton]; // 发送媒体消息按钮 self.sendMediaButton = [UIButton buttonWithType:UIButtonTypeSystem]; self.sendMediaButton.frame = CGRectMake(190, y, 150, 40); [self.sendMediaButton setTitle:@"发送媒体消息" forState:UIControlStateNormal]; [self.sendMediaButton setBackgroundColor:[UIColor systemPurpleColor]]; [self.sendMediaButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [self.sendMediaButton addTarget:self action:@selector(sendMediaMessageButtonTapped) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.sendMediaButton]; } #pragma mark - Network Setup - (void)setupNetwork { // 1. 初始化模拟服务器 self.mockServer = [[TJPMockFinalVersionTCPServer alloc] init]; [self.mockServer startWithPort:12345]; [self logMessage:@"📡 模拟服务器已启动,端口: 12345"]; // 2. 获取TJPIMClient实例 self.client = [TJPIMClient shared]; [self logMessage:@"🔧 TJPIMClient 初始化完成"]; // 3. 配置自定义路由(可选) [self configureCustomRouting]; [self logMessage:@"🚀 网络组件初始化完成,准备连接"]; } - (void)configureCustomRouting { // 示例:配置自定义消息路由 // [self.client configureRouting:TJPContentTypeCustom toSessionType:TJPSessionTypeDefault]; [self logMessage:@"⚙️ 消息路由配置完成"]; } #pragma mark - Button Actions - (void)connectButtonTapped { NSString *host = @"127.0.0.1"; uint16_t port = 12345; [self logMessage:@"🔗 开始建立连接..."]; // 建立聊天连接 [self.client connectToHost:host port:port forType:TJPSessionTypeChat]; [self logMessage:[NSString stringWithFormat:@"📞 正在连接聊天服务器: %@:%u", host, port]]; // 如果需要,也可以建立媒体连接 // [self.client connectToHost:host port:port forType:TJPSessionTypeMedia]; // [self logMessage:[NSString stringWithFormat:@"📺 正在连接媒体服务器: %@:%u", host, port]]; } - (void)disconnectButtonTapped { [self logMessage:@"⚠️ 开始断开连接..."]; // 断开所有连接 [self.client disconnectAll]; [self logMessage:@"🔌 已断开所有连接"]; } - (void)queryStatusButtonTapped { [self logMessage:@"📊 查询当前连接状态:"]; // 查询所有连接状态 NSDictionary *allStates = [self.client getAllConnectionStates]; if (allStates.count == 0) { [self logMessage:@" 无活跃连接"]; return; } for (NSNumber *typeKey in allStates.allKeys) { TJPSessionType type = [typeKey unsignedIntegerValue]; TJPConnectState state = allStates[typeKey]; NSString *typeName = [self sessionTypeToString:type]; [self logMessage:[NSString stringWithFormat:@" %@: %@", typeName, state]]; } } #pragma mark - Message Sending - (void)sendTextMessageButtonTapped { // 检查聊天连接状态 if (![self.client isConnectedForType:TJPSessionTypeChat]) { [self logMessage:@"❌ 聊天连接未建立,无法发送文本消息"]; return; } // 创建文本消息 static int messageCounter = 0; messageCounter++; NSString *messageText = [NSString stringWithFormat:@"Hello World! 消息编号: %d", messageCounter]; TJPTextMessage *textMsg = [[TJPTextMessage alloc] initWithText:messageText]; [self logMessage:[NSString stringWithFormat:@"📝 发送文本消息: %@", messageText]]; // 方式1: 手动指定会话类型发送 [self.client sendMessage:textMsg throughType:TJPSessionTypeChat]; // 方式2: 自动路由发送(根据消息内容类型自动选择会话) // [self.client sendMessageWithAutoRoute:textMsg]; } - (void)sendMediaMessageButtonTapped { // // 检查是否有可用的媒体连接(如果建立了的话) // if (![self.client isConnectedForType:TJPSessionTypeMedia]) { // // 如果没有媒体连接,使用聊天连接发送 // if (![self.client isConnectedForType:TJPSessionTypeChat]) { // [self logMessage:@"❌ 无可用连接,无法发送媒体消息"]; // return; // } // [self logMessage:@"ℹ️ 媒体连接未建立,使用聊天连接发送媒体消息"]; // } // // // 创建媒体消息 // static int mediaCounter = 0; // mediaCounter++; // // NSString *mediaId = [NSString stringWithFormat:@"media_%d", mediaCounter]; // TJPMediaMessage *mediaMsg = [[TJPMediaMessage alloc] initWithMediaId:mediaId]; // // [self logMessage:[NSString stringWithFormat:@"🎬 发送媒体消息: %@", mediaId]]; // // // 使用自动路由发送(会根据消息类型自动选择合适的会话) // [self.client sendMessageWithAutoRoute:mediaMsg]; } #pragma mark - Status Monitoring - (void)startStatusMonitoring { // 每2秒更新一次状态显示 self.statusUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(updateConnectionStatus) userInfo:nil repeats:YES]; } - (void)updateConnectionStatus { NSDictionary *allStates = [self.client getAllConnectionStates]; if (allStates.count == 0) { self.connectionStatusLabel.text = @"连接状态: 未连接"; self.connectionStatusLabel.backgroundColor = [UIColor lightGrayColor]; return; } NSMutableString *statusText = [NSMutableString stringWithString:@"连接状态: "]; BOOL hasConnected = NO; BOOL hasConnecting = NO; for (NSNumber *typeKey in allStates.allKeys) { TJPSessionType type = [typeKey unsignedIntegerValue]; TJPConnectState state = allStates[typeKey]; if ([self.client isStateConnected:state]) { hasConnected = YES; } else if ([self.client isStateConnecting:state]) { hasConnecting = YES; } NSString *typeName = [self sessionTypeToString:type]; [statusText appendFormat:@"%@:%@ ", typeName, [self shortStateString:state]]; } self.connectionStatusLabel.text = statusText; // 根据连接状态设置背景色 if (hasConnected) { self.connectionStatusLabel.backgroundColor = [UIColor systemGreenColor]; } else if (hasConnecting) { self.connectionStatusLabel.backgroundColor = [UIColor systemYellowColor]; } else { self.connectionStatusLabel.backgroundColor = [UIColor systemRedColor]; } } #pragma mark - Helper Methods - (NSString *)sessionTypeToString:(TJPSessionType)type { switch (type) { case TJPSessionTypeDefault: return @"默认"; case TJPSessionTypeChat: return @"聊天"; case TJPSessionTypeMedia: return @"媒体"; default: return [NSString stringWithFormat:@"类型%lu", (unsigned long)type]; } } - (NSString *)shortStateString:(TJPConnectState)state { if ([state isEqualToString:TJPConnectStateConnected]) { return @"已连接"; } else if ([state isEqualToString:TJPConnectStateConnecting]) { return @"连接中"; } else if ([state isEqualToString:TJPConnectStateDisconnected]) { return @"已断开"; } else if ([state isEqualToString:TJPConnectStateDisconnecting]) { return @"断开中"; } else { return @"未知"; } } - (void)logMessage:(NSString *)message { dispatch_async(dispatch_get_main_queue(), ^{ // 添加时间戳 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.dateFormat = @"HH:mm:ss"; NSString *timestamp = [formatter stringFromDate:[NSDate date]]; // 获取当前UITextView内容,并追加新的日志消息 NSString *currentLog = self.logTextView.text; NSString *newLog = [currentLog stringByAppendingFormat:@"[%@] %@\n", timestamp, message]; // 更新UITextView的内容 self.logTextView.text = newLog; // 滚动到最新日志 NSRange range = NSMakeRange(self.logTextView.text.length, 0); [self.logTextView scrollRangeToVisible:range]; // 限制日志长度,避免内存问题 if (newLog.length > 10000) { // 保留最后8000个字符 NSString *trimmedLog = [newLog substringFromIndex:newLog.length - 8000]; self.logTextView.text = [@"...(日志已截断)\n" stringByAppendingString:trimmedLog]; } }); } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/TJPSessionPacketMonitor.h ================================================ // // TJPSessionPacketMonitor.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/21. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPSessionPacketMonitor : NSObject /// 记录新数据包大小(线程安全) - (void)recordPacketSize:(NSUInteger)size; /// 获取最近N个包的平均大小(线程安全) - (CGFloat)averageSizeForLastPackets:(NSUInteger)count; /// 重置监控数据 - (void)reset; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/NetworkMonitor/TJPSessionPacketMonitor.m ================================================ // // TJPSessionPacketMonitor.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/21. // #import "TJPSessionPacketMonitor.h" @interface TJPSessionPacketMonitor () { dispatch_queue_t _monitorQueue; NSMutableArray *_packetSizes; } @end @implementation TJPSessionPacketMonitor - (instancetype)init { if (self = [super init]) { _monitorQueue = dispatch_queue_create("com.session.monitor", DISPATCH_QUEUE_SERIAL); _packetSizes = [NSMutableArray arrayWithCapacity:10]; } return self; } - (void)recordPacketSize:(NSUInteger)size { dispatch_async(_monitorQueue, ^{ // 保留最近10个包 if (self->_packetSizes.count >= 10) { [self->_packetSizes removeObjectAtIndex:0]; } [self->_packetSizes addObject:@(size)]; }); } - (CGFloat)averageSizeForLastPackets:(NSUInteger)count { __block CGFloat avg = 0; dispatch_sync(_monitorQueue, ^{ NSUInteger actualCount = MIN(count, _packetSizes.count); if (actualCount == 0) { avg = 0; return; } NSUInteger total = 0; NSArray *targetPackets = [_packetSizes subarrayWithRange: NSMakeRange(_packetSizes.count - actualCount, actualCount)]; for (NSNumber *num in targetPackets) { total += num.unsignedIntegerValue; } avg = (CGFloat)total / actualCount; }); return avg; } - (void)reset { dispatch_async(_monitorQueue, ^{ [self->_packetSizes removeAllObjects]; }); } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/Utility/Category/UIImage+TJPImageOrientation.h ================================================ // // UIImage+TJPImageOrientation.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/14. // #import NS_ASSUME_NONNULL_BEGIN @interface UIImage (TJPImageOrientation) /// 修正图片方向(解决拍照图片旋转问题) - (UIImage *)fixOrientation; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/Utility/Category/UIImage+TJPImageOrientation.m ================================================ // // UIImage+TJPImageOrientation.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/14. // #import "UIImage+TJPImageOrientation.h" @implementation UIImage (TJPImageOrientation) - (UIImage *)fixOrientation { // 方向正常的图片直接返回 if (self.imageOrientation == UIImageOrientationUp) { return self; } // 计算变换矩阵 CGAffineTransform transform = CGAffineTransformIdentity; switch (self.imageOrientation) { case UIImageOrientationDown: case UIImageOrientationDownMirrored: transform = CGAffineTransformTranslate(transform, self.size.width, self.size.height); transform = CGAffineTransformRotate(transform, M_PI); break; case UIImageOrientationLeft: case UIImageOrientationLeftMirrored: transform = CGAffineTransformTranslate(transform, self.size.width, 0); transform = CGAffineTransformRotate(transform, M_PI_2); break; case UIImageOrientationRight: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, 0, self.size.height); transform = CGAffineTransformRotate(transform, -M_PI_2); break; default: break; } // 处理镜像情况 switch (self.imageOrientation) { case UIImageOrientationUpMirrored: case UIImageOrientationDownMirrored: transform = CGAffineTransformTranslate(transform, self.size.width, 0); transform = CGAffineTransformScale(transform, -1, 1); break; case UIImageOrientationLeftMirrored: case UIImageOrientationRightMirrored: transform = CGAffineTransformTranslate(transform, self.size.height, 0); transform = CGAffineTransformScale(transform, -1, 1); break; default: break; } // 应用变换 CGContextRef ctx = CGBitmapContextCreate( NULL, self.size.width, self.size.height, CGImageGetBitsPerComponent(self.CGImage), 0, CGImageGetColorSpace(self.CGImage), CGImageGetBitmapInfo(self.CGImage) ); CGContextConcatCTM(ctx, transform); switch (self.imageOrientation) { case UIImageOrientationLeft: case UIImageOrientationLeftMirrored: case UIImageOrientationRight: case UIImageOrientationRightMirrored: CGContextDrawImage(ctx, CGRectMake(0, 0, self.size.height, self.size.width), self.CGImage); break; default: CGContextDrawImage(ctx, CGRectMake(0, 0, self.size.width, self.size.height), self.CGImage); break; } CGImageRef cgimg = CGBitmapContextCreateImage(ctx); UIImage *result = [UIImage imageWithCGImage:cgimg]; CGContextRelease(ctx); CGImageRelease(cgimg); return result ?: self; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/Utility/TJPUtility/TJPFPSLabel.h ================================================ // // TJPFPSLabel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPFPSLabel : UILabel @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/Utility/TJPUtility/TJPFPSLabel.m ================================================ // // TJPFPSLabel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPFPSLabel.h" #import @implementation TJPFPSLabel { CADisplayLink *_link; NSUInteger _count; NSTimeInterval _lastTime; UIFont *_font; UIFont *_subFont; UIColor *_normalColor; UIColor *_warnColor; } - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (!self) return nil; self.layer.cornerRadius = 5; self.clipsToBounds = YES; self.textAlignment = NSTextAlignmentCenter; self.userInteractionEnabled = NO; self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700]; _font = [UIFont fontWithName:@"Menlo" size:14]; if (_font) { _subFont = [UIFont fontWithName:@"Menlo" size:4]; } else { _font = [UIFont systemFontOfSize:14]; _subFont = [UIFont systemFontOfSize:4]; } _normalColor = [UIColor colorWithRed:0.0 green:1.0 blue:0.0 alpha:1.0]; // Green _warnColor = [UIColor colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0]; // Red _link = [CADisplayLink displayLinkWithTarget:self selector:@selector(tick:)]; [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes]; return self; } - (void)dealloc { [_link invalidate]; } - (void)tick:(CADisplayLink *)link { if (_lastTime == 0) { _lastTime = link.timestamp; return; } _count++; NSTimeInterval delta = link.timestamp - _lastTime; if (delta < 1) return; _lastTime = link.timestamp; float fps = _count / delta; _count = 0; // 更新文本 NSString *text = [NSString stringWithFormat:@"%d FPS", (int)round(fps)]; UIColor *color = fps >= 55 ? _normalColor : _warnColor; NSMutableAttributedString *attrText = [[NSMutableAttributedString alloc] initWithString:text]; [attrText addAttribute:NSForegroundColorAttributeName value:color range:NSMakeRange(0, text.length - 3)]; [attrText addAttribute:NSFontAttributeName value:_font range:NSMakeRange(0, text.length)]; self.attributedText = attrText; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPCustomTableViewDemoViewController.h ================================================ // // TJPCustomTableViewDemoViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPCustomTableViewDemoViewController : UIViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPCustomTableViewDemoViewController.m ================================================ // // TJPCustomTableViewDemoViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // #import "TJPCustomTableViewDemoViewController.h" #import #import "TJPBaseTableView.h" #import "TJPBaseSectionModel.h" #import "TJPBaseCellModel.h" #import "TJPBaseTableViewCell.h" @interface TJPViperDemoCellModel : TJPBaseCellModel @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *subTitle; @end @implementation TJPViperDemoCellModel - (NSString *)cellName { return @"TJPViperDemoTableViewCell"; } @end @interface TJPViperDemoTableViewCell : TJPBaseTableViewCell @property (nonnull, strong) UILabel *titleLabel; @property (nonatomic, weak) TJPViperDemoCellModel *cellModel; @end @implementation TJPViperDemoTableViewCell @synthesize cellModel = _cellModel; - (void)initializationChildUI { self.titleLabel = [UILabel new]; [self.contentView addSubview:self.titleLabel]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.mas_equalTo(self.contentView); make.left.mas_equalTo(15); }]; } - (void)configureWithModel:(id)cellModel { [super configureWithModel:cellModel]; self.titleLabel.text = self.cellModel.title; } @end @interface TJPCustomTableViewDemoViewController () @property (nonatomic, strong) TJPBaseTableView *tableView; @property (nonatomic, strong) NSArray *demoData; @property (nonatomic, strong) RACCommand *selectCommand; @end @implementation TJPCustomTableViewDemoViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.title = @"模块化TableView 实践"; self.demoData = @[ @{@"title": @"Item 1", @"subtitle": @"Description 1"}, @{@"title": @"Item 2", @"subtitle": @"Description 2"}, @{@"title": @"Item 3", @"subtitle": @"Description 3"}, @{@"title": @"Item 4", @"subtitle": @"Description 4"}, @{@"title": @"Item 5", @"subtitle": @"Description 5"} ]; self.tableView = [[TJPBaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; self.tableView.tjpBaseTableViewDelegate = self; id section = [[TJPBaseSectionModel alloc] initWithCellModels:[self createCellModelsFromData:self.demoData]]; self.tableView.sectionModels = @[section]; // self.tableView.cellModels = [[self createCellModelsFromData:self.demoData] mutableCopy]; [self.view addSubview:self.tableView]; [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.top.equalTo(self.mas_topLayoutGuideBottom); make.bottom.equalTo(self.view); }]; [self.tableView configurePullDownRefreshControlWithTarget:self pullDownAction:@selector(handlePullDownRefresh)]; [self.tableView configurePullUpRefreshControlWithTarget:self pullUpAction:@selector(handlePullUpRefresh)]; } // 将 demoData 转换为 cell 模型数组 - (NSArray *)createCellModelsFromData:(NSArray *)data { NSMutableArray *models = [NSMutableArray array]; for (NSDictionary *item in data) { TJPViperDemoCellModel *model = [[TJPViperDemoCellModel alloc] init]; model.title = item[@"title"]; model.subTitle = item[@"subtitle"]; model.selectedCommand = self.selectCommand; // model.cellHeight = 60; [models addObject:model]; } return models; } // 下拉刷新处理方法 - (void)handlePullDownRefresh { // 模拟刷新操作 NSLog(@"Pull down to refresh..."); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.tableView endRefreshing]; }); } // 上拉加载更多处理方法 - (void)handlePullUpRefresh { // 模拟加载更多操作 NSLog(@"Pull up to load more..."); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.tableView endRefreshing]; }); } - (RACCommand *)selectCommand { if (nil == _selectCommand) { _selectCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(TJPViperDemoCellModel * _Nullable input) { NSLog(@"当前点击了cell subtitle: %@", input.subTitle); return [RACSignal empty]; }]; } return _selectCommand; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPSectionTableViewDemoViewController.h ================================================ // // TJPSectionTableViewDemoViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/24. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPSectionTableViewDemoViewController : UIViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPSectionTableViewDemoViewController.m ================================================ // // TJPSectionTableViewDemoViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/24. // #import "TJPSectionTableViewDemoViewController.h" #import #import "TJPBaseTableView.h" #import "TJPBaseSectionModel.h" #import "TJPBaseCellModel.h" #import "TJPBaseTableViewCell.h" @interface TJPSectionDemoCellModel : TJPBaseCellModel @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *subTitle; @end @implementation TJPSectionDemoCellModel - (NSString *)cellName { return @"TJPSectionDemoTableViewCell"; } @end @interface TJPSectionDemoTableViewCell : TJPBaseTableViewCell @property (nonnull, strong) UILabel *titleLabel; @property (nonatomic, weak) TJPSectionDemoCellModel *cellModel; @end @implementation TJPSectionDemoTableViewCell @synthesize cellModel = _cellModel; - (void)initializationChildUI { self.titleLabel = [UILabel new]; [self.contentView addSubview:self.titleLabel]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.centerY.mas_equalTo(self.contentView); make.left.mas_equalTo(15); }]; } - (void)configureWithModel:(id)cellModel { [super configureWithModel:cellModel]; self.titleLabel.text = self.cellModel.title; } @end @interface TJPSectionTableViewDemoViewController () @property (nonatomic, strong) TJPBaseTableView *tableView; @property (nonatomic, strong) NSArray *sectionedData; @property (nonatomic, strong) RACCommand *selectCommand; @end @implementation TJPSectionTableViewDemoViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.title = @"VIPER 多Section列表"; self.sectionedData = @[ @{ @"title": @"常规内容", @"items": @[ @{@"title": @"常规-1", @"subtitle": @"描述-A"}, @{@"title": @"常规-2", @"subtitle": @"描述-B"} ] }, @{ @"title": @"推荐内容", @"items": @[ @{@"title": @"推荐-1", @"subtitle": @"推荐内容-A"}, @{@"title": @"推荐-2", @"subtitle": @"推荐内容-B"}, @{@"title": @"推荐-3", @"subtitle": @"推荐内容-C"} ] } ]; self.tableView = [[TJPBaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; self.tableView.tjpBaseTableViewDelegate = self; [self.view addSubview:self.tableView]; [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.edges.equalTo(self.view); }]; self.tableView.sectionModels = [self buildSectionModels]; } - (NSArray> *)buildSectionModels { NSMutableArray *sections = [NSMutableArray array]; for (NSDictionary *sectionDict in self.sectionedData) { NSMutableArray *cellModels = [NSMutableArray array]; for (NSDictionary *item in sectionDict[@"items"]) { TJPSectionDemoCellModel *model = [[TJPSectionDemoCellModel alloc] init]; model.title = item[@"title"]; model.subTitle = item[@"subtitle"]; [cellModels addObject:model]; } TJPBaseSectionModel *sectionModel = [[TJPBaseSectionModel alloc] initWithCellModels:cellModels]; sectionModel.sectionTitle = sectionDict[@"title"]; sectionModel.sectionHeaderHeight = 40.0; [sections addObject:sectionModel]; } return sections; } // 下拉刷新处理方法 - (void)handlePullDownRefresh { // 模拟刷新操作 NSLog(@"Pull down to refresh..."); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.tableView endRefreshing]; }); } // 上拉加载更多处理方法 - (void)handlePullUpRefresh { // 模拟加载更多操作 NSLog(@"Pull up to load more..."); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.tableView endRefreshing]; }); } - (RACCommand *)selectCommand { if (nil == _selectCommand) { _selectCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(TJPSectionDemoCellModel * _Nullable input) { NSLog(@"当前点击了cell subtitle: %@", input.subTitle); return [RACSignal empty]; }]; } return _selectCommand; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Cell/TJPBaseTableViewCell.h ================================================ // // TJPBaseTableViewCell.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // #import #import "TJPBaseCellModelProtocol.h" #import "TJPBaseTableViewCellProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPBaseTableViewCell : UITableViewCell @property (nonatomic, weak) id cellModel; @property (nonatomic, strong) UIView *tjp_bottomLineView; - (void)initializationChildUI; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Cell/TJPBaseTableViewCell.m ================================================ // // TJPBaseTableViewCell.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // #import "TJPBaseTableViewCell.h" #import "TJPBaseCellModel.h" #import #import "UIColor+TJPColor.h" #pragma mark - #pragma mark Constants #pragma mark - //********************************************************************************************************** // // Constants // //********************************************************************************************************** #pragma mark - #pragma mark Private Interface #pragma mark - //********************************************************************************************************** // // Private Interface @interface TJPBaseTableViewCell () @end // //********************************************************************************************************** @implementation TJPBaseTableViewCell //@synthesize cellModel = _cellModel; #pragma mark - #pragma mark Object Constructors //************************************************** // Constructors - (instancetype)init { self = [super init]; if (self) { [self initializationChildUI]; } return self; } - (instancetype)initWithCoder:(NSCoder *)aDecoder { self = [super initWithCoder:aDecoder]; if (self) { [self initializationChildUI]; } return self; } - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if(self) { [self initializationChildUI]; } return self; } //************************************************** #pragma mark - #pragma mark ViewLifeCycle //************************************************** // ViewLifeCycle Methods //************************************************** - (void)awakeFromNib { [super awakeFromNib]; } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; } - (void)configureWithModel:(id)cellModel { if (self.cellModel == cellModel) { return; } self.cellModel = cellModel; if ([self.cellModel isKindOfClass:[TJPBaseCellModel class]]) { TJPBaseCellModel *cellViewModel = self.cellModel; self.tjp_bottomLineView.hidden = !cellViewModel.tjp_showBottomLine; } } - (void)cellWillDisplay:(id)cellModel { if (self.cellModel == cellModel) { return; } self.cellModel = cellModel; } #pragma mark - #pragma mark Private Methods //************************************************** // Private Methods //************************************************** - (void)initializationChildUI { // 子类可以覆盖这个方法进行UI初始化 self.backgroundColor = [UIColor whiteColor]; } #pragma mark - #pragma mark Self Public Methods //************************************************** // Self Public Methods //************************************************** #pragma mark - #pragma mark HitTest //************************************************** // HitTest Methods //************************************************** #pragma mark - #pragma mark UserAction //************************************************** // UserAction Methods //************************************************** #pragma mark - #pragma mark Properties Getter & Setter //************************************************** // Properties - (UIView *)tjp_bottomLineView { if (!_tjp_bottomLineView) { _tjp_bottomLineView = [[UIView alloc] init]; _tjp_bottomLineView.backgroundColor = [UIColor tjp_backgroundGrayColor]; [self.contentView addSubview:_tjp_bottomLineView]; [_tjp_bottomLineView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.mas_equalTo(15); make.right.mas_equalTo(-15); make.bottom.equalTo(self); make.height.mas_equalTo(0.5); }]; } return _tjp_bottomLineView; } //************************************************** @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Loading/TJPDefaultLoadingAnimation.h ================================================ // // TJPDefaultLoadingAnimation.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/24. // #import #import "TJPBaseTableViewLoadingProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPDefaultLoadingAnimation : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Loading/TJPDefaultLoadingAnimation.m ================================================ // // TJPDefaultLoadingAnimation.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/24. // #import "TJPDefaultLoadingAnimation.h" @implementation TJPDefaultLoadingAnimation - (UIView *)customLoadingView { UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, 50)]; container.backgroundColor = [UIColor clearColor]; UIImageView *loadingImage = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"img_default_loading"]]; loadingImage.frame = CGRectMake(0, 0, 40, 40); loadingImage.center = CGPointMake(CGRectGetMidX(container.bounds), CGRectGetMidY(container.bounds)); CABasicAnimation *rotation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"]; rotation.toValue = @(M_PI * 2); rotation.duration = 1.0; rotation.repeatCount = MAXFLOAT; rotation.removedOnCompletion = NO; [loadingImage.layer addAnimation:rotation forKey:@"rotate"]; [container addSubview:loadingImage]; return container; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Model/TJPBaseCellModel.h ================================================ // // TJPBaseCellModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // #import #import "TJPBaseCellModelProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPBaseCellModel : NSObject /// 选中Cell信号 @property (nonatomic, strong) RACCommand* selectedCommand; /// 是否显示底部线条 @property (nonatomic, assign) BOOL tjp_showBottomLine; /// Cell名称 - (NSString *)cellName; /// Cell高度 - (CGFloat)cellHeight; /// 子类实现的计算Cell高度方法 - (CGFloat)calculateCellHeight; /// 刷新缓存 - (void)invalidateCellHeightCache; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Model/TJPBaseCellModel.m ================================================ // // TJPBaseCellModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // #import "TJPBaseCellModel.h" #import "TJPNavigationModel.h" @interface TJPBaseCellModel () // 用于缓存 cell 高度 @property (nonatomic, assign) CGFloat cachedHeight; @end @implementation TJPBaseCellModel #pragma mark - #pragma mark Object Constructors //************************************************** // Constructors - (instancetype)init { self = [super init]; if (self) { self.tjp_showBottomLine = YES; // 缓存高度为 -1,表示未计算过 self.cachedHeight = -1; } return self; } //************************************************** - (NSString *)cellName { return @"TJPBaseTableViewCell"; } - (CGFloat)cellHeight { if (self.cachedHeight < 0) { // 如果缓存的高度没有被计算,进行计算 self.cachedHeight = [self calculateCellHeight]; } return self.cachedHeight; } - (CGFloat)calculateCellHeight { return 44; } - (void)invalidateCellHeightCache { self.cachedHeight = -1; } #pragma mark - #pragma mark Private Methods //************************************************** // Private Methods //************************************************** @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Model/TJPBaseSectionModel.h ================================================ // // TJPBaseSectionModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/24. // #import #import "TJPBaseSectionModelProtocol.h" NS_ASSUME_NONNULL_BEGIN @protocol TJPBaseCellModelProtocol; @interface TJPBaseSectionModel : NSObject - (instancetype)initWithCellModels:(NSArray >*)cellModels; @property (nonatomic, copy) NSString *sectionTitle; @property (nonatomic, assign) CGFloat sectionHeaderHeight; @property (nonatomic, assign) CGFloat sectionFooterHeight; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Model/TJPBaseSectionModel.m ================================================ // // TJPBaseSectionModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/24. // #import "TJPBaseSectionModel.h" @implementation TJPBaseSectionModel @synthesize cellModels; - (instancetype)initWithCellModels:(NSArray >*)cellModels { if (self = [super init]) { self.cellModels = cellModels; } return self; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Protocol/TJPBaseCellModelProtocol.h ================================================ // // TJPBaseCellModelProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // #import #import #import "TJPNavigationDefines.h" NS_ASSUME_NONNULL_BEGIN //@class TJPNavigationModel; @protocol TJPBaseCellModelProtocol @property (nonatomic, strong) RACCommand, NSObject*>* selectedCommand; //// 返回要跳转的类型 (已废弃) //- (TJPNavigationType)navigationTypeForModel __deprecated_msg("请使用navigationModelForCell方法"); - (NSString *)cellName; - (CGFloat)cellHeight; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Protocol/TJPBaseSectionModelProtocol.h ================================================ // // TJPBaseSectionModelProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/24. // #import #import NS_ASSUME_NONNULL_BEGIN @protocol TJPBaseCellModelProtocol; @protocol TJPBaseSectionModelProtocol @required @property (nonatomic, copy) NSArray> *cellModels; @optional @property (nonatomic, strong) RACCommand, NSObject*>* selectedSectionCommand; @property (nonatomic, copy) NSString *sectionTitle; @property (nonatomic, assign) CGFloat sectionHeaderHeight; @property (nonatomic, assign) CGFloat sectionFooterHeight; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Protocol/TJPBaseTableViewCellProtocol.h ================================================ // // TJPBaseTableViewCellProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // #import NS_ASSUME_NONNULL_BEGIN @protocol TJPBaseCellModelProtocol; @protocol TJPBaseTableViewCellProtocol @property (nonatomic, weak) id cellModel; /// 配置cell数据 /// - Parameter cellModel: cellModel - (void)configureWithModel:(id)cellModel; @optional /// cell即将展示 /// - Parameter cellModel: cellModel - (void)cellWillDisplay:(id) cellModel; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/Protocol/TJPBaseTableViewLoadingProtocol.h ================================================ // // TJPBaseTableViewLoadingProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/24. // 加载动画协议,可注入 Lottie 动画、骨架屏、ProgressHUD 等 #import NS_ASSUME_NONNULL_BEGIN @protocol TJPBaseTableViewLoadingProtocol /// 返回一个 loading 状态的视图(供空态展示使用) - (UIView *)customLoadingView; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/View/TJPBaseTableView.h ================================================ // // TJPBaseTableView.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // 适用于MVVM/VIPER的基础UI组件 #import NS_ASSUME_NONNULL_BEGIN @protocol TJPBaseCellModelProtocol, TJPBaseSectionModelProtocol, TJPBaseTableViewLoadingProtocol; @protocol TJPBaseTableViewDelegate @optional - (void)tjpEmptyViewDidTapped:(UIView *)view; - (void)tjpTableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath; - (void)tjpTableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath; @end @interface TJPBaseTableView : UITableView @property (nonatomic, weak) id tjpBaseTableViewDelegate; /// Section 模型数组,每个 Section 包含 cellModels @property (nonatomic, strong) NSArray> *sectionModels; /// 动画对象(默认提供旋转动画) @property (nonatomic, strong) id loadingAnimation; /** * 刷新TableView数据 * @param sections 装载Section模型的数据 cell模型的数组cellModels为sections中的属性 */ - (void)reloadDataWithSectionModels:(NSArray> *)sections; /** * 刷新TableView数据 - 单section模式(兼容性方法) * @param cellModels cell模型数组,内部会自动包装为单个section */ - (void)reloadDataWithCellModels:(NSArray> *)cellModels; /** * 局部刷新TableView数据 * @param indexPaths 需要刷新的行的索引路径数组 * @param animation 刷新时的动画效果 */ - (void)tableReloadRowsWithIndexPaths:(NSArray *)indexPaths animation:(UITableViewRowAnimation)animation; /// 刷新某个 section - (void)reloadSection:(NSInteger)section withAnimation:(UITableViewRowAnimation)animation; /// 配置下拉刷新 - (void)configurePullDownRefreshControlWithTarget:(id)target pullDownAction:(SEL)pullDownAction; /// 配置上拉加载更多 - (void)configurePullUpRefreshControlWithTarget:(id)target pullUpAction:(SEL)pullUpAction; /// 结束刷新 - (void)endRefreshing; /// 没有更多数据 - (void)noMoreData; /// 重置没有更多数据 - (void)resetNoMoreData; /// 空白样式 允许重写 - (UIView *)customViewForEmptyDataSet:(UIScrollView *)scrollView; /// 展示空白数据 - (void)showEmptyData; /// 隐藏空白数据 - (void)hideEmptyData; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/CustomTableViewDemo/TJPViperBaseTableView/View/TJPBaseTableView.m ================================================ // // TJPBaseTableView.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/29. // #import "TJPBaseTableView.h" #import #import "TJPBaseCellModelProtocol.h" #import "TJPBaseTableViewCellProtocol.h" #import "TJPBaseSectionModelProtocol.h" #import "TJPBaseTableViewLoadingProtocol.h" #import "TJPDefaultLoadingAnimation.h" #import "MJRefresh.h" #import "UIColor+TJPColor.h" #import "TJPNetworkDefine.h" #import "TJPBaseSectionModel.h" #pragma mark - #pragma mark Constants #pragma mark - //********************************************************************************************************** // // Constants // //********************************************************************************************************** #pragma mark - #pragma mark Private Interface #pragma mark - //********************************************************************************************************** // // Private Interface @interface TJPBaseTableView () /// 内部数据源 - 统一的section数据 所有数据都使用它 @property (nonatomic, strong) NSArray> *internalSections; // 使用一个集合来存储已注册的单元格标识符,避免重复注册 @property (nonatomic, strong) NSMutableSet *registeredIdentifiers; // 是否显示空视图标记 @property (nonatomic, assign) BOOL isShowEmptyData; @end // //********************************************************************************************************** #pragma mark - #pragma mark Object Constructors //************************************************** // Constructors @implementation TJPBaseTableView // 初始化方法,设置数据源和代理 - (void)commonInit { self.delegate = self; self.dataSource = self; self.internalSections = [NSMutableArray array]; self.registeredIdentifiers = [NSMutableSet set]; self.emptyDataSetSource = self; self.emptyDataSetDelegate = self; self.loadingAnimation = [[TJPDefaultLoadingAnimation alloc] init]; self.isShowEmptyData = NO; } - (instancetype)init { if (self = [super init]) { [self commonInit]; } return self; } - (instancetype)initWithFrame:(CGRect)frame style:(UITableViewStyle)style { if (self = [super initWithFrame:frame style:style]) { [self commonInit]; } return self; } - (void)dealloc { TJPLogDealloc(); // 确保数据源被释放 self.internalSections = nil; // 释放注册的 cell 标识符 self.registeredIdentifiers = nil; } //************************************************** #pragma mark - #pragma mark ViewLifeCycle //************************************************** // ViewLifeCycle Methods //************************************************** //************************************************** #pragma mark - #pragma mark Private Methods //************************************************** // Private Methods - (void)setSectionModels:(NSArray> *)sectionModels { if (_internalSections != sectionModels) { _internalSections = sectionModels ?: @[]; [self registerCellsForSections:sectionModels]; } } - (NSArray> *)sectionModels { return self.internalSections; } - (void)registerCellsForSections:(NSArray> *)sections { for (id section in sections) { for (id model in section.cellModels) { NSString *cellName = [model cellName]; Class cellClass = NSClassFromString(cellName); NSString *cellIdentifier = NSStringFromClass(cellClass); //如果该类型已经注册过则跳过注册 if ([self.registeredIdentifiers containsObject:cellIdentifier]) { continue; } NSBundle *bundle = [NSBundle bundleForClass:cellClass]; if ([bundle pathForResource:cellIdentifier ofType:@"nib"] != nil) { // 如果有 nib 文件,注册 nib [self registerNib:[UINib nibWithNibName:cellIdentifier bundle:bundle] forCellReuseIdentifier:cellIdentifier]; TJPLOG_INFO(@"Registered nib for cell: %@", cellIdentifier); } else { // 如果没有 nib 文件,注册 class [self registerClass:cellClass forCellReuseIdentifier:cellIdentifier]; TJPLOG_INFO(@"Registered class for cell: %@", cellIdentifier); } [self.registeredIdentifiers addObject:cellIdentifier]; } } } //************************************************** #pragma mark - #pragma mark Self Public Methods //************************************************** // Self Public Methods //************************************************** - (void)reloadDataWithSectionModels:(NSArray> *)sections { if (sections == nil || sections.count == 0) { TJPLOG_WARN(@"[TJPBaseTableView] sectionModels 为空,请检查!!当前sectionModels已赋值为@[]"); sections = @[]; } // 赋值数据并注册cell [self setSectionModels:sections]; [self reloadData]; // TODO 增加差异化局部刷新机制 } - (void)reloadDataWithCellModels:(NSArray> *)cellModels { // 兼容性方法:将cellModels包装为单个section TJPBaseSectionModel *defaultSection = [[TJPBaseSectionModel alloc] initWithCellModels:cellModels ?: @[]]; // 转发到统一的section方法 [self reloadDataWithSectionModels:@[defaultSection]]; } - (void)reloadSection:(NSInteger)section withAnimation:(UITableViewRowAnimation)animation { if (section < self.internalSections.count) { [self reloadSections:[NSIndexSet indexSetWithIndex:section] withRowAnimation:animation]; } } - (void)tableReloadRowsWithIndexPaths:(NSArray *)indexPaths animation:(UITableViewRowAnimation)animation { if (!indexPaths.count) { return; } [self reloadRowsAtIndexPaths:indexPaths withRowAnimation:animation]; } - (void)showEmptyData { self.isShowEmptyData = YES; // 刷新空白页显示 [self reloadEmptyDataSet]; } - (void)hideEmptyData { self.isShowEmptyData = NO; // 刷新空白页显示 [self reloadEmptyDataSet]; } - (void)configurePullDownRefreshControlWithTarget:(id)target pullDownAction:(SEL)pullDownAction { MJRefreshNormalHeader *header = [MJRefreshNormalHeader headerWithRefreshingTarget:target refreshingAction:pullDownAction]; header.stateLabel.textColor= [UIColor tjp_lightTextColor]; header.lastUpdatedTimeLabel.hidden = YES; self.mj_header = header; TJPLOG_INFO(@"配置下拉刷新控件"); } - (void)configurePullUpRefreshControlWithTarget:(id)target pullUpAction:(SEL)pullUpAction { self.mj_footer = [MJRefreshBackNormalFooter footerWithRefreshingTarget:target refreshingAction:pullUpAction]; TJPLOG_INFO(@"配置上拉加载更多控件"); } - (void)endRefreshing { [self.mj_header endRefreshing]; [self.mj_footer endRefreshing]; } - (void)resetNoMoreData { [self.mj_footer resetNoMoreData]; } - (void)noMoreData { [self.mj_footer endRefreshingWithNoMoreData]; } //************************************************** #pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.internalSections.count > 0 ? self.internalSections.count : 1; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { if (section < self.internalSections.count) { id sectionModel = self.internalSections[section]; return sectionModel.cellModels.count; } return 0; } - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { id section = self.internalSections[indexPath.section]; id model = section.cellModels[indexPath.row]; return model.cellHeight; } - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { if (section < self.internalSections.count) { id sectionModel = self.internalSections[section]; return [sectionModel respondsToSelector:@selector(sectionTitle)] ? sectionModel.sectionTitle : @""; } return @""; } #pragma mark - UITableViewDelegate - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { if (indexPath.section >= self.internalSections.count) { TJPLOG_WARN(@"[TJPBaseTableView] section越界: %ld", indexPath.section); return [self defaultErrorCell:@"section 越界"]; } id sectionModel = self.internalSections[indexPath.section]; if (indexPath.row >= sectionModel.cellModels.count) { TJPLOG_WARN(@"[TJPBaseTableView] row越界: %ld", indexPath.row); return [self defaultErrorCell:@"row 越界"]; } id model = sectionModel.cellModels[indexPath.row]; NSString *cellIdentifier = [model cellName]; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier]; if (!cell) { TJPLOG_WARN(@"[TJPBaseTableView] 找不到注册的cell: %@", cellIdentifier); return [self defaultErrorCell:@"未注册cell"]; } if ([cell respondsToSelector:@selector(configureWithModel:)]) { [(id)cell configureWithModel:model]; } return cell; } - (UITableViewCell *)defaultErrorCell:(NSString *)msg { UITableViewCell *cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil]; cell.textLabel.text = [NSString stringWithFormat:@"⚠️ %@", msg]; cell.textLabel.textColor = [UIColor redColor]; return cell; } - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { id section = self.internalSections[indexPath.section]; id model = section.cellModels[indexPath.row]; TJPLOG_INFO(@"第 %ld 行被选中,模型: %@", (long)indexPath.row, model); if (model.selectedCommand) { [model.selectedCommand execute:model]; } if (self.tjpBaseTableViewDelegate && [self.tjpBaseTableViewDelegate respondsToSelector:@selector(tjpTableView:didSelectRowAtIndexPath:)]) { [self.tjpBaseTableViewDelegate tjpTableView:tableView didSelectRowAtIndexPath:indexPath]; } } - (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { id section = self.internalSections[indexPath.section]; id model = section.cellModels[indexPath.row]; UITableViewCell *viperCell = (UITableViewCell *)cell; [viperCell cellWillDisplay:model]; if (self.tjpBaseTableViewDelegate && [self.tjpBaseTableViewDelegate respondsToSelector:@selector(tjpTableView:willDisplayCell:forRowAtIndexPath:)]) { [self.tjpBaseTableViewDelegate tjpTableView:tableView willDisplayCell:cell forRowAtIndexPath:indexPath]; } } #pragma mark - DZNEmptyDataSetSource && DZNEmptyDataSetDelegate - (NSAttributedString *)titleForEmptyDataSet:(UIScrollView *)scrollView { return [[NSAttributedString alloc] initWithString:@"暂无相关数据" attributes:@{NSFontAttributeName:[UIFont systemFontOfSize:15], NSForegroundColorAttributeName:[UIColor tjp_lightTextColor]}]; } - (UIImage *)imageForEmptyDataSet:(UIScrollView *)scrollView { if (!self.isShowEmptyData) { return nil; } return [UIImage imageNamed:@"img_data_empty"]; } - (CGFloat)verticalOffsetForEmptyDataSet:(UIScrollView *)scrollView { return -40; } - (UIColor *)backgroundColorForEmptyDataSet:(UIScrollView *)scrollView { return [UIColor whiteColor]; } - (void)emptyDataSet:(UIScrollView *)scrollView didTapView:(UIView *)view { TJPLOG_INFO(@"点击了空数据视图"); [self hideEmptyData]; if (self.tjpBaseTableViewDelegate && [self.tjpBaseTableViewDelegate respondsToSelector:@selector(tjpEmptyViewDidTapped:)]) { [self.tjpBaseTableViewDelegate tjpEmptyViewDidTapped:view]; } } - (UIView *)customViewForEmptyDataSet:(UIScrollView *)scrollView { if (self.isShowEmptyData) { return nil; } return [self.loadingAnimation customLoadingView]; } #pragma mark - #pragma mark HitTest //************************************************** // HitTest Methods //************************************************** #pragma mark - #pragma mark UserAction //************************************************** // UserAction Methods //************************************************** #pragma mark - #pragma mark Properties Getter & Setter //************************************************** // Properties //************************************************** @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/DIContainer/TJPViperModuleAssembly.h ================================================ // // TJPViperModuleAssembly.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import NS_ASSUME_NONNULL_BEGIN @protocol TJPViperModuleProvider; @interface TJPViperModuleAssembly : TyphoonAssembly @property (nonatomic, strong, readonly) TyphoonAssembly *tjpViperModuleProvider; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/DIContainer/TJPViperModuleAssembly.m ================================================ // // TJPViperModuleAssembly.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import "TJPViperModuleAssembly.h" #import "TJPViperModuleProvider.h" #import "AppDelegate.h" #import "HomeViewController.h" #import "TJPViperBasePresenterImpl.h" #import "TJPViperBaseInteractorImpl.h" #import "TJPViperBaseRouterImpl.h" #import "TJPVIPERDemoRouter.h" #import "TJPVIPERDemoPresenter.h" #import "TJPVIPERDemoViewController.h" #import "TJPVIPERDemoInteractorImpl.h" #import "TJPVIPERDemoDetailViewController.h" #import "TJPNewsDetailTableViewController.h" @interface TJPViperModuleAssembly () @end @implementation TJPViperModuleAssembly - (UIResponder *)appDelegate { return [TyphoonDefinition withClass:[AppDelegate class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector(homeViewController) with:[self tjpHomeNavViewController]]; // [definition injectProperty:@selector(jzIMService) with:self.jzViperBaseModuleProvider.jzIMService]; }]; } - (UINavigationController *)tjpHomeNavViewController { return [TyphoonDefinition withClass:[UINavigationController class] configuration:^(TyphoonDefinition *definition) { [definition useInitializer:@selector(initWithRootViewController:) parameters:^(TyphoonMethod *initializer) { [initializer injectParameterWith:[self tjpHomeViewController]]; }]; }]; } - (UIViewController *)tjpHomeViewController { return [TyphoonDefinition withClass:[HomeViewController class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector(hidesBottomBarWhenPushed) with:@(NO)]; [definition injectProperty:@selector(tjpViperModuleProvider) with:self]; }]; } - (UIViewController *)viperDemoViewController { return [TyphoonDefinition withClass:[TJPVIPERDemoViewController class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector(basePresenter) with:self.viperDemoPresenter]; }]; } - (id)viperDemoRouter { return [TyphoonDefinition withClass:[TJPVIPERDemoRouter class] configuration:^(TyphoonDefinition *definition) { //路由一般注入Typhoon不同模块定义类的接口 如TJPViperModuleProvider [definition injectProperty:@selector(viperModuleProvider) with:self]; }]; } - (id)viperDemoPresenter { return [TyphoonDefinition withClass:[TJPVIPERDemoPresenter class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector(baseInteractor) with:self.viperDemoInteractor]; [definition injectProperty:@selector(baseRouter) with:self.viperDemoRouter]; }]; } - (id)viperDemoInteractor { return [TyphoonDefinition withClass:[TJPVIPERDemoInteractorImpl class] configuration:^(TyphoonDefinition *definition) { //interactor层一般都是注入网络框架 缓存框架等工具框架 }]; } - (UIViewController *)viperDemoDetailViewController { return [TyphoonDefinition withClass:[TJPVIPERDemoDetailViewController class] configuration:^(TyphoonDefinition *definition) { }]; } - (UIViewController *)viperNewsDetailViewControllerWithTitle:(id)title { return [TyphoonDefinition withClass:[TJPNewsDetailTableViewController class] configuration:^(TyphoonDefinition *definition) { [definition injectProperty:@selector(titleStr) with:title]; }]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/DIContainer/TJPViperModuleProvider.h ================================================ // // TJPViperModuleProvider.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import NS_ASSUME_NONNULL_BEGIN @protocol TJPViperModuleProvider /// VIPER示例VC - (UIViewController *)viperDemoViewController; - (UIViewController *)viperDemoDetailViewController; - (UIViewController *)viperNewsDetailViewControllerWithTitle:(id)title; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Entity/TJPNavigationModel.h ================================================ // // TJPNavigationModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import #import "TJPNavigationDefines.h" NS_ASSUME_NONNULL_BEGIN @interface TJPNavigationModel : NSObject /// 路由id @property (nonatomic, copy) NSString *routeId; /// 跳转参数 @property (nonatomic, strong) NSDictionary *parameters; /// 时间戳 @property (nonatomic, assign) NSTimeInterval timestamp; /// 跳转类型 @property (nonatomic, assign) TJPNavigationRouteType routeType; /// 是否显示动画 @property (nonatomic, assign) BOOL animated; @property (nonatomic, strong) UIViewController *targetVC; + (instancetype)modelWithRouteId:(NSString *)routeId parameters:(NSDictionary *)params routeType:(TJPNavigationRouteType)routeType; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Entity/TJPNavigationModel.m ================================================ // // TJPNavigationModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import "TJPNavigationModel.h" @implementation TJPNavigationModel + (instancetype)modelWithRouteId:(NSString *)routeId parameters:(NSDictionary *)params routeType:(TJPNavigationRouteType)routeType { TJPNavigationModel *model = [[TJPNavigationModel alloc] init]; model.routeId = routeId; model.parameters = params ?: @{}; model.routeType = routeType; model.timestamp = [[NSDate date] timeIntervalSince1970]; return model; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Error/TJPViperDefaultErrorHandler.h ================================================ // // TJPViperDefaultErrorHandler.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/25. // 默认VIPER错误处理器 #import #import "TJPViperErrorHandlerProtocol.h" #import "TJPViperErrorHandlerDelegate.h" NS_ASSUME_NONNULL_BEGIN @interface TJPViperDefaultErrorHandler : NSObject /// 委托对象 @property (nonatomic, weak) id delegate; /// 重试计数映射表 @property (nonatomic, strong) NSMutableDictionary *retryCountMap; /// 是否显示Debug信息 @property (nonatomic, assign) BOOL showDebugInfo; /// 单例 + (instancetype)sharedHandler; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Error/TJPViperDefaultErrorHandler.m ================================================ // // TJPViperDefaultErrorHandler.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/25. // #import "TJPViperDefaultErrorHandler.h" #import "TJPViperErrorHandlingStrategy.h" @implementation TJPViperDefaultErrorHandler + (instancetype)sharedHandler { static TJPViperDefaultErrorHandler *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[TJPViperDefaultErrorHandler alloc] init]; }); return instance; } - (instancetype)init { if (self = [super init]) { _retryCountMap = [NSMutableDictionary dictionary]; _showDebugInfo = NO; #ifdef DEBUG _showDebugInfo = YES; #endif } return self; } - (void)handleError:(NSError *)error inContext:(UIViewController *)context completion:(void(^)(BOOL shouldRetry))completion { TJPViperErrorHandlingStrategy *strategy; // 判断错误来源,选择不同的处理策略 if ([error.domain isEqualToString:TJPViperErrorDomain]) { // 应用层错误 - 直接处理 strategy = [TJPViperErrorHandlingStrategy strategyForViperError:(TJPViperError)error.code]; [self processErrorWithStrategy:strategy error:error inContext:context completion:completion]; } else { // 非应用层错误 - 委托给对应的错误处理器或回调上层决定 if ([self.delegate respondsToSelector:@selector(viperErrorHandler:shouldHandleExternalError:inContext:completion:)]) { [self.delegate viperErrorHandler:self shouldHandleExternalError:error inContext:context completion:completion]; } else { // 默认处理:转换为未知应用层错误 NSError *viperError = [self createViperErrorWithCode:TJPViperErrorUnknown description:@"外部错误"]; strategy = [TJPViperErrorHandlingStrategy strategyForViperError:TJPViperErrorUnknown]; [self processErrorWithStrategy:strategy error:viperError inContext:context completion:completion]; } } } // 提取错误处理核心逻辑 - (void)processErrorWithStrategy:(TJPViperErrorHandlingStrategy *)strategy error:(NSError *)error inContext:(UIViewController *)context completion:(void(^)(BOOL shouldRetry))completion { // 生成重试键(基于错误域和错误码) NSString *retryKey = [NSString stringWithFormat:@"%@_%ld", error.domain, (long)error.code]; NSInteger currentRetryCount = [self.retryCountMap[retryKey] integerValue]; // 判断是否可以重试 if (strategy.shouldRetry && currentRetryCount < strategy.maxRetryCount) { [self showRetryAlertWithStrategy:strategy error:error inContext:context completion:^(BOOL userWantsRetry) { if (userWantsRetry) { self.retryCountMap[retryKey] = @(currentRetryCount + 1); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(strategy.retryDelay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ completion(YES); }); } else { [self.retryCountMap removeObjectForKey:retryKey]; completion(NO); } }]; } else { // 不可重试或达到最大重试次数 [self.retryCountMap removeObjectForKey:retryKey]; [self showErrorAlertWithStrategy:strategy error:error inContext:context]; completion(NO); } } - (NSError *)createViperErrorWithCode:(TJPViperError)errorCode description:(nullable NSString *)description { NSDictionary *userInfo = @{ NSLocalizedDescriptionKey: description ?: @"应用错误" }; return [NSError errorWithDomain:TJPViperErrorDomain code:errorCode userInfo:userInfo]; } - (void)resetErrorState { [self.retryCountMap removeAllObjects]; } #pragma mark - Private Methods - (void)showRetryAlertWithStrategy:(TJPViperErrorHandlingStrategy *)strategy error:(NSError *)error inContext:(UIViewController *)context completion:(void(^)(BOOL userWantsRetry))completion { NSString *message = [self buildAlertMessage:strategy error:error]; UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) { completion(NO); }]; UIAlertAction *retryAction = [UIAlertAction actionWithTitle:strategy.actionTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { completion(YES); }]; [alert addAction:cancelAction]; [alert addAction:retryAction]; [context presentViewController:alert animated:YES completion:nil]; } - (void)showErrorAlertWithStrategy:(TJPViperErrorHandlingStrategy *)strategy error:(NSError *)error inContext:(UIViewController *)context { NSString *message = [self buildAlertMessage:strategy error:error]; UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提示" message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *okAction = [UIAlertAction actionWithTitle:strategy.actionTitle style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) { if (strategy.needsSpecialHandling) { [self handleSpecialAction:strategy inContext:context]; } }]; [alert addAction:okAction]; [context presentViewController:alert animated:YES completion:nil]; } - (NSString *)buildAlertMessage:(TJPViperErrorHandlingStrategy *)strategy error:(NSError *)error { NSMutableString *message = [NSMutableString stringWithString:strategy.userMessage]; if (strategy.recoverySuggestion) { [message appendFormat:@"\n\n%@", strategy.recoverySuggestion]; } if (self.showDebugInfo) { [message appendFormat:@"\n\n[Debug] %@ (Domain: %@, Code: %ld)", error.localizedDescription, error.domain, (long)error.code]; } return message; } - (void)handleSpecialAction:(TJPViperErrorHandlingStrategy *)strategy inContext:(UIViewController *)context { if ([strategy.actionTitle isEqualToString:@"重新登录"] || [strategy.actionTitle isEqualToString:@"去登录"]) { [[NSNotificationCenter defaultCenter] postNotificationName:@"TJPViperShouldReloginNotification" object:nil]; } else if ([strategy.actionTitle isEqualToString:@"去更新"]) { // 跳转应用商店 NSURL *appStoreURL = [NSURL URLWithString:@"itms-apps://itunes.apple.com/app/idXXXXXXXX"]; [[UIApplication sharedApplication] openURL:appStoreURL options:@{} completionHandler:nil]; } else if ([strategy.actionTitle isEqualToString:@"联系客服"]) { [[NSNotificationCenter defaultCenter] postNotificationName:@"TJPViperShouldContactSupportNotification" object:nil]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Error/TJPViperErrorDefine.h ================================================ // // TJPViperErrorDefine.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/10. // #ifndef TJPErrorUtil_h #define TJPErrorUtil_h #import /** * VIPER应用层错误代码枚举 * 错误域: "com.tjp.viper.error" * * 注意:这是应用层错误,与底层网络框架的TJPNetworkError分离 */ typedef NS_ENUM(NSInteger, TJPViperError) { // 基础错误 (0-99) TJPViperErrorNone = 0, // 无错误 TJPViperErrorUnknown = 1, // 未知错误 TJPViperErrorCancelled = 2, // 用户取消操作 TJPViperErrorTimeout = 3, // 应用层操作超时 // 数据相关错误 (100-199) TJPViperErrorDataEmpty = 100, // 数据为空 TJPViperErrorDataInvalid = 101, // 数据格式无效 TJPViperErrorDataProcessFailed = 102, // 数据处理失败 TJPViperErrorDataCacheCorrupted = 103, // 缓存数据损坏 // 业务逻辑错误 (200-299) TJPViperErrorBusinessLogicFailed = 200, // 业务逻辑失败 TJPViperErrorPermissionDenied = 201, // 权限不足 TJPViperErrorUserNotLogin = 202, // 用户未登录 TJPViperErrorUserBlocked = 203, // 用户被封禁 TJPViperErrorOperationNotSupported = 204, // 操作不支持 // UI交互错误 (300-399) TJPViperErrorViewNotReady = 300, // 视图未准备好 TJPViperErrorNavigationFailed = 301, // 页面导航失败 TJPViperErrorPresenterNotBound = 302, // Presenter未绑定 // 系统资源错误 (400-499) TJPViperErrorMemoryLow = 400, // 内存不足 TJPViperErrorStorageFull = 401, // 存储空间不足 TJPViperErrorDeviceNotSupported = 402, // 设备不支持 }; /** * 错误严重程度 */ typedef NS_ENUM(NSUInteger, TJPViperErrorSeverity) { TJPViperErrorSeverityInfo = 0, // 信息提示 TJPViperErrorSeverityWarning, // 警告 TJPViperErrorSeverityError, // 错误 TJPViperErrorSeverityCritical // 严重错误 }; // VIPER错误域常量 FOUNDATION_EXPORT NSString * const TJPViperErrorDomain; // 错误信息键 FOUNDATION_EXPORT NSString * const TJPViperErrorUserInfoKeyRetryable; FOUNDATION_EXPORT NSString * const TJPViperErrorUserInfoKeyRecoverySuggestion; #endif /* TJPErrorUtil_h */ ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Error/TJPViperErrorHandlingStrategy.h ================================================ // // TJPViperErrorHandlingStrategy.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/25. // VIPER应用层错误处理策略 #import #import "TJPViperErrorDefine.h" NS_ASSUME_NONNULL_BEGIN @interface TJPViperErrorHandlingStrategy : NSObject /// 是否可以重试 @property (nonatomic, assign) BOOL shouldRetry; /// 最大重试次数 @property (nonatomic, assign) NSInteger maxRetryCount; /// 重试延迟时间(秒) @property (nonatomic, assign) NSTimeInterval retryDelay; /// 用户友好的错误描述 @property (nonatomic, copy) NSString *userMessage; /// 操作按钮标题 @property (nonatomic, copy) NSString *actionTitle; /// 错误严重程度 @property (nonatomic, assign) TJPViperErrorSeverity severity; /// 恢复建议 @property (nonatomic, copy, nullable) NSString *recoverySuggestion; /// 是否需要特殊处理 @property (nonatomic, assign) BOOL needsSpecialHandling; /** * 根据TJPViperError创建处理策略 */ + (instancetype)strategyForViperError:(TJPViperError)errorCode; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Error/TJPViperErrorHandlingStrategy.m ================================================ // // TJPViperErrorHandlingStrategy.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/25. // #import "TJPViperErrorHandlingStrategy.h" NSString * const TJPViperErrorDomain = @"com.tjp.viper.error"; NSString * const TJPViperErrorUserInfoKeyRetryable = @"TJPViperErrorRetryable"; NSString * const TJPViperErrorUserInfoKeyRecoverySuggestion = @"TJPViperErrorRecoverySuggestion"; @implementation TJPViperErrorHandlingStrategy + (instancetype)strategyForViperError:(TJPViperError)errorCode { TJPViperErrorHandlingStrategy *strategy = [[TJPViperErrorHandlingStrategy alloc] init]; // 默认值 strategy.shouldRetry = NO; strategy.maxRetryCount = 0; strategy.retryDelay = 1.0; strategy.severity = TJPViperErrorSeverityError; strategy.needsSpecialHandling = NO; switch (errorCode) { case TJPViperErrorNone: strategy.severity = TJPViperErrorSeverityInfo; strategy.userMessage = @"操作成功"; strategy.actionTitle = @"确定"; break; case TJPViperErrorCancelled: strategy.severity = TJPViperErrorSeverityInfo; strategy.userMessage = @"操作已取消"; strategy.actionTitle = @"确定"; break; case TJPViperErrorTimeout: strategy.shouldRetry = YES; strategy.maxRetryCount = 2; strategy.retryDelay = 2.0; strategy.userMessage = @"操作超时,请重试"; strategy.actionTitle = @"重试"; break; case TJPViperErrorDataEmpty: strategy.severity = TJPViperErrorSeverityWarning; strategy.userMessage = @"暂无数据"; strategy.actionTitle = @"刷新"; strategy.shouldRetry = YES; strategy.maxRetryCount = 1; break; case TJPViperErrorDataInvalid: case TJPViperErrorDataProcessFailed: strategy.shouldRetry = YES; strategy.maxRetryCount = 1; strategy.userMessage = @"数据处理失败,请重试"; strategy.actionTitle = @"重试"; break; case TJPViperErrorDataCacheCorrupted: strategy.userMessage = @"缓存数据损坏,将重新加载"; strategy.actionTitle = @"确定"; strategy.shouldRetry = YES; strategy.maxRetryCount = 1; break; case TJPViperErrorUserNotLogin: strategy.severity = TJPViperErrorSeverityCritical; strategy.userMessage = @"请先登录"; strategy.actionTitle = @"去登录"; strategy.needsSpecialHandling = YES; break; case TJPViperErrorUserBlocked: strategy.severity = TJPViperErrorSeverityCritical; strategy.userMessage = @"账号已被封禁,请联系客服"; strategy.actionTitle = @"联系客服"; strategy.needsSpecialHandling = YES; break; case TJPViperErrorPermissionDenied: strategy.userMessage = @"没有权限执行此操作"; strategy.actionTitle = @"确定"; break; case TJPViperErrorNavigationFailed: strategy.shouldRetry = YES; strategy.maxRetryCount = 1; strategy.userMessage = @"页面跳转失败"; strategy.actionTitle = @"重试"; break; case TJPViperErrorMemoryLow: strategy.severity = TJPViperErrorSeverityWarning; strategy.userMessage = @"设备内存不足,请清理后台应用"; strategy.actionTitle = @"确定"; break; case TJPViperErrorStorageFull: strategy.severity = TJPViperErrorSeverityWarning; strategy.userMessage = @"存储空间不足,请清理设备存储"; strategy.actionTitle = @"确定"; break; default: strategy.userMessage = @"操作失败,请重试"; strategy.actionTitle = @"重试"; strategy.shouldRetry = YES; strategy.maxRetryCount = 1; break; } return strategy; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Handler/TJPViperErrorHandlerDelegate.h ================================================ // // TJPViperErrorHandlerDelegate.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/25. // #import NS_ASSUME_NONNULL_BEGIN @protocol TJPViperErrorHandlerProtocol; @protocol TJPViperErrorHandlerDelegate @optional /** * 询问是否应该处理外部错误 * @param errorHandler 错误处理器 * @param error 外部错误(如网络层错误) * @param context 上下文 * @param completion 完成回调 * @return YES表示已处理,NO表示使用默认处理 */ - (BOOL)viperErrorHandler:(id)errorHandler shouldHandleExternalError:(NSError *)error inContext:(UIViewController *)context completion:(void(^)(BOOL shouldRetry))completion; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Handler/TJPViperErrorHandlerProtocol.h ================================================ // // TJPViperErrorHandlerProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/25. // 应用层错误处理器协议 #import #import "TJPViperErrorDefine.h" NS_ASSUME_NONNULL_BEGIN @protocol TJPViperErrorHandlerDelegate; @protocol TJPViperErrorHandlerProtocol /// 委托对象,用于处理外部错误 @property (nonatomic, weak) id delegate; /** * 处理错误(仅处理应用层错误,外部错误委托给delegate) * @param error 错误对象 * @param context 上下文视图控制器 * @param completion 完成回调 */ - (void)handleError:(NSError *)error inContext:(UIViewController *)context completion:(void(^)(BOOL shouldRetry))completion; /** * 创建应用层错误 */ - (NSError *)createViperErrorWithCode:(TJPViperError)errorCode description:(nullable NSString *)description; /** * 重置错误状态 */ - (void)resetErrorState; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Interactor/TJPViperBaseInteractorImpl.h ================================================ // // TJPViperBaseInteractorImpl.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import #import "TJPViperBaseInteractorProtocol.h" #import "TJPViperErrorDefine.h" #import "TJPCacheManager.h" #import "TJPViperDefaultErrorHandler.h" NS_ASSUME_NONNULL_BEGIN @class TJPPaginationInfo; @interface TJPViperBaseInteractorImpl : NSObject // 基础控件 @property (nonatomic, strong, readonly) TJPCacheManager *cacheManager; @property (nonatomic, strong, readonly) TJPViperDefaultErrorHandler *errorHandler; // 网络配置 @property (nonatomic, assign) NSTimeInterval requestTimeout; @property (nonatomic, assign) NSInteger maxRetryCount; // 状态管理 @property (nonatomic, assign, readonly) BOOL isInitialized; // 分页管理 @property (nonatomic, strong, readonly) TJPPaginationInfo *currentPagination; @property (nonatomic, assign, readonly) NSInteger currentPage; @property (nonatomic, assign, readonly) BOOL hasMoreData; @property (nonatomic, assign) NSInteger defaultPageSize; // 快速定义错误 - (NSError *)createErrorWithCode:(TJPViperError)errorCode description:(NSString *)description; /// 子类需要实现的抽象方法 - (void)performDataRequestForPage:(NSInteger)page completion:(void (^)(NSArray * _Nullable data, NSInteger totalPage, NSError * _Nullable error))completion; /// 子类实现带分页信息的抽象方法(推荐) - (void)performDataRequestForPage:(NSInteger)page withPagination:(void (^)(NSArray * _Nullable data, TJPPaginationInfo * _Nullable pagination, NSError * _Nullable error))completion; // 子类可重写的方法 - (NSString *)baseURLString; - (NSDictionary *)commonParameters; - (NSDictionary *)parametersForPage:(NSInteger)page; - (NSArray *)processRawResponseData:(id)rawData; - (NSError * _Nullable)validateResponseData:(id)rawData; - (TJPPaginationInfo * _Nullable)extractPaginationFromResponse:(id)rawData; // 分页相关方法 - (void)resetPagination; - (void)updatePaginationInfo:(TJPPaginationInfo *)paginationInfo; - (BOOL)canLoadNextPage; - (NSInteger)getNextPageNumber; // 工具方法 - (NSString *)cacheKeyForPage:(NSInteger)page; - (BOOL)shouldCacheDataForPage:(NSInteger)page; // 分页缓存相关 - (NSString *)paginationCacheKeyForPage:(NSInteger)page; - (void)cachePaginationInfo:(TJPPaginationInfo *)pagination forPage:(NSInteger)page; - (TJPPaginationInfo * _Nullable)loadCachedPaginationForPage:(NSInteger)page; // 子类可重写的扩展方法 - (void)setupInteractor; - (void)teardownInteractor; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Interactor/TJPViperBaseInteractorImpl.m ================================================ // // TJPViperBaseInteractorImpl.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import "TJPViperBaseInteractorImpl.h" #import "TJPNetworkDefine.h" #import "TJPViperDefaultErrorHandler.h" #import "TJPMemoryCache.h" #import "TJPPaginationInfo.h" @interface TJPViperBaseInteractorImpl () @property (nonatomic, strong) TJPCacheManager *cacheManager; @property (nonatomic, strong) TJPViperDefaultErrorHandler *errorHandler; @property (nonatomic, assign) BOOL isInitialized; @property (nonatomic, strong) NSMutableDictionary *uploadProgressMap; @property (nonatomic, strong) NSMutableSet *subscribedTopics; // 分页相关属性 @property (nonatomic, strong) TJPPaginationInfo *currentPagination; @property (nonatomic, assign) NSInteger currentPage; @property (nonatomic, assign) BOOL hasMoreData; @end @implementation TJPViperBaseInteractorImpl @synthesize navigateToPageSubject = _navigateToPageSubject, dataListUpdatedSignal = _dataListUpdatedSignal; #pragma mark - Lifecycle - (instancetype)init { self = [super init]; if (self) { // 缓存组件 _cacheManager = [[TJPCacheManager alloc] initWithCacheStrategy:[[TJPMemoryCache alloc] init] defaultStrategy:TJPCacheStrategyStaleWhileRevalidate]; // 错误处理组件 _errorHandler = [TJPViperDefaultErrorHandler sharedHandler]; _requestTimeout = 30.0; _maxRetryCount = 3; _isInitialized = NO; _uploadProgressMap = [NSMutableDictionary dictionary]; _subscribedTopics = [NSMutableSet set]; // 分页相关初始化 _defaultPageSize = 10; _currentPage = 0; _hasMoreData = YES; _currentPagination = nil; [self setupInteractor]; _isInitialized = YES; TJPLOG_INFO(@"[%@] Interactor 初始化完成", NSStringFromClass([self class])); } return self; } - (void)dealloc { TJPLogDealloc(); [self teardownInteractor]; } #pragma mark - Subject - (RACSubject *)navigateToPageSubject { if (!_navigateToPageSubject) { _navigateToPageSubject = [RACSubject subject]; } return _navigateToPageSubject; } - (RACSubject *)dataListUpdatedSignal { if (!_dataListUpdatedSignal) { _dataListUpdatedSignal = [RACSubject subject]; } return _dataListUpdatedSignal; } #pragma mark - Load Data - (void)fetchDataForPageWithCompletion:(NSInteger)page success:(void (^)(NSArray * _Nullable, NSInteger))success failure:(void (^)(NSError * _Nullable))failure { // 调用新方法并适配旧接口 [self fetchDataForPageWithPagination:page success:^(NSArray * _Nullable data, TJPPaginationInfo * _Nullable pagination) { if (success) { NSInteger totalPage = pagination.paginationType == TJPPaginationTypePageBased ? pagination.totalPages : 0; success(data, totalPage); } } failure:failure]; } - (void)fetchDataForPageWithPagination:(NSInteger)page success:(void (^)(NSArray * _Nullable, TJPPaginationInfo * _Nullable))success failure:(void (^)(NSError * _Nullable))failure { TJPLOG_INFO(@"[%@] 开始请求第 %ld 页数据(带分页信息)", NSStringFromClass([self class]), (long)page); // 第一页请求时重置分页状态 if (page == 1) { [self resetPagination]; } // 参数验证 if (page <= 0) { NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorBusinessLogicFailed userInfo:@{NSLocalizedDescriptionKey: @"页码必须大于0"}]; if (failure) failure(error); return; } // 检查是否可以加载下一页 if (page > 1 && _currentPagination && !_currentPagination.canLoadNextPage) { NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorBusinessLogicFailed userInfo:@{NSLocalizedDescriptionKey: @"没有更多数据"}]; if (failure) failure(error); return; } // 检查数据缓存 NSString *cacheKey = [self cacheKeyForPage:page]; NSArray *cachedData = [self.cacheManager loadCacheForKey:cacheKey]; // 检查分页信息缓存 TJPPaginationInfo *cachedPagination = [self loadCachedPaginationForPage:page]; if (cachedData && cachedPagination) { TJPLOG_INFO(@"[%@] 返回第 %ld 页缓存数据", NSStringFromClass([self class]), (long)page); [self updatePaginationInfo:cachedPagination]; if (success) success(cachedData, cachedPagination); return; } // 如果是基类被直接调用,抛出错误 if ([self isMemberOfClass:[TJPViperBaseInteractorImpl class]]) { NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorBusinessLogicFailed userInfo:@{NSLocalizedDescriptionKey: @"子类必须重写此方法"}]; if (failure) failure(error); return; } // 执行具体的数据请求(由子类实现) [self performDataRequestForPage:page withPagination:^(NSArray *data, TJPPaginationInfo *pagination, NSError *error) { if (error) { TJPLOG_ERROR(@"数据请求失败: %@", error.localizedDescription); if (failure) failure(error); } else { TJPLOG_INFO(@"数据请求成功: %lu 条数据", (unsigned long)data.count); // 更新分页信息 if (pagination) { [self updatePaginationInfo:pagination]; } // 缓存数据和分页信息 if ([self shouldCacheDataForPage:page] && data.count > 0) { TJPLOG_INFO(@"准备缓存数据:%@", data); [self.cacheManager saveCacheWithData:data forKey:cacheKey expireTime:TJPCacheExpireTimeMedium]; if (pagination) { [self cachePaginationInfo:pagination forPage:page]; } } if (success) success(data, pagination); } }]; } #pragma mark - Manage Data - (void)createData:(NSDictionary *)data completion:(void (^)(id _Nullable, NSError * _Nullable))completion { TJPLOG_INFO(@"[%@] 创建数据: %@", NSStringFromClass([self class]), data); // 业务规则验证 NSError *validationError = [self validateBusinessRules:data]; if (validationError) { if (completion) completion(nil, validationError); return; } // 基类提供默认实现,子类可重写 // 这里可以是模拟实现或者调用通用的创建API dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSDictionary *result = @{ @"id": [[NSUUID UUID] UUIDString], @"status": @"created", @"timestamp": @([[NSDate date] timeIntervalSince1970]) }; if (completion) completion(result, nil); // 发送数据更新信号 [self.dataListUpdatedSignal sendNext:@{@"action": @"create", @"data": result}]; }); } - (void)updateDataWithId:(NSString *)dataId updateData:(NSDictionary *)updateData completion:(void (^)(id _Nullable, NSError * _Nullable))completion { TJPLOG_INFO(@"[%@] 更新数据,ID: %@", NSStringFromClass([self class]), dataId); if (!dataId || dataId.length == 0) { NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataInvalid userInfo:@{NSLocalizedDescriptionKey: @"数据ID不能为空"}]; if (completion) completion(nil, error); return; } // 基类提供默认实现,子类可重写 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSDictionary *result = @{ @"id": dataId, @"status": @"updated", @"timestamp": @([[NSDate date] timeIntervalSince1970]) }; if (completion) completion(result, nil); // 发送数据更新信号 [self.dataListUpdatedSignal sendNext:@{@"action": @"update", @"id": dataId, @"data": result}]; }); } - (void)deleteDataWithId:(NSString *)dataId completion:(void (^)(BOOL, NSError * _Nullable))completion { TJPLOG_INFO(@"[%@] 删除数据,ID: %@", NSStringFromClass([self class]), dataId); if (!dataId || dataId.length == 0) { NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataInvalid userInfo:@{NSLocalizedDescriptionKey: @"数据ID不能为空"}]; if (completion) completion(NO, error); return; } // 基类提供默认实现,子类可重写 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (completion) completion(YES, nil); // 发送数据更新信号 [self.dataListUpdatedSignal sendNext:@{@"action": @"delete", @"id": dataId}]; }); } - (void)searchDataWithKeyword:(NSString *)keyword filters:(NSDictionary *)filters completion:(void (^)(NSArray * _Nullable, NSError * _Nullable))completion { TJPLOG_INFO(@"[%@] 搜索数据,关键词: %@, 筛选条件: %@", NSStringFromClass([self class]), keyword, filters); // 基类提供默认实现,子类可重写 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.8 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 模拟搜索结果 NSMutableArray *results = [NSMutableArray array]; if (keyword && keyword.length > 0) { [results addObject:@{ @"id": @"search_1", @"title": [NSString stringWithFormat:@"搜索结果: %@", keyword], @"type": @"search_result" }]; } if (completion) completion([results copy], nil); }); } - (TJPPaginationInfo * _Nullable)extractPaginationFromResponse:(id)rawData { // 基类提供默认实现,子类可重写 if ([rawData isKindOfClass:[NSDictionary class]]) { NSDictionary *dict = (NSDictionary *)rawData; NSDictionary *paginationDict = dict[@"pagination"] ?: dict[@"page_info"] ?: dict[@"paging"]; if (paginationDict) { return [TJPPaginationInfo paginationWithDict:paginationDict]; } } return nil; } #pragma mark - Manage Cache - (NSString *)cacheKeyForPage:(NSInteger)page { // 数据key包含更多上下文信息 NSString *userId = [self getCurrentUserId] ?: @"AaronTang"; NSString *apiVersion = [self getAPIVersion] ?: @"v1"; return [NSString stringWithFormat:@"%@_%@_%@_page_%ld", NSStringFromClass([self class]), userId, apiVersion, (long)page]; } - (NSString *)paginationCacheKeyForPage:(NSInteger)page { // 分页信息key return [NSString stringWithFormat:@"%@_pagination", [self cacheKeyForPage:page]]; } - (NSString *)getCurrentUserId { // return @"mock_user_id_10086"; } - (NSString *)getAPIVersion { return @"v1"; } - (NSString *)cacheKeyWithParams:(NSDictionary *)params { NSMutableString *key = [NSMutableString stringWithFormat:@"%@", NSStringFromClass([self class])]; // 按键排序确保一致性 NSArray *sortedKeys = [[params allKeys] sortedArrayUsingSelector:@selector(compare:)]; for (NSString *paramKey in sortedKeys) { [key appendFormat:@"_%@_%@", paramKey, params[paramKey]]; } return [key copy]; } - (void)cachePaginationInfo:(TJPPaginationInfo *)pagination forPage:(NSInteger)page { NSString *cacheKey = [self paginationCacheKeyForPage:page]; NSDictionary *paginationDict = [pagination toDictionary]; [self.cacheManager saveCacheWithData:paginationDict forKey:cacheKey expireTime:TJPCacheExpireTimeMedium]; TJPLOG_INFO(@"[%@] 缓存第 %ld 页分页信息", NSStringFromClass([self class]), (long)page); } - (TJPPaginationInfo * _Nullable)loadCachedPaginationForPage:(NSInteger)page { NSString *cacheKey = [self paginationCacheKeyForPage:page]; NSDictionary *cachedDict = [self.cacheManager loadCacheForKey:cacheKey]; if (cachedDict && [cachedDict isKindOfClass:[NSDictionary class]]) { TJPLOG_INFO(@"[%@] 加载第 %ld 页缓存分页信息", NSStringFromClass([self class]), (long)page); return [TJPPaginationInfo paginationWithDict:cachedDict]; } return nil; } - (void)clearCache:(NSString *)cacheKey { TJPLOG_INFO(@"[%@] 清理缓存,缓存键: %@", NSStringFromClass([self class]), cacheKey); [self.cacheManager removeCacheForKey:cacheKey]; // 同时清理对应的分页缓存 if ([cacheKey containsString:@"_page_"]) { NSString *paginationCacheKey = [cacheKey stringByReplacingOccurrencesOfString:@"_page_" withString:@"_pagination_page_"]; [self.cacheManager removeCacheForKey:paginationCacheKey]; } } - (void)clearAllCache { TJPLOG_INFO(@"[%@] 清理所有缓存", NSStringFromClass([self class])); [self.cacheManager clearAllCache]; [self resetPagination]; // 同时重置分页信息 } - (NSUInteger)getCacheSize { // 实际项目中可以实现更精确的计算 return 1024 * 1024 * 3; // 3MB } #pragma mark - Manage State - (void)syncDataToServer:(void (^)(BOOL, NSError * _Nullable))completion { TJPLOG_INFO(@"[%@] 正在同步数据到服务器", NSStringFromClass([self class])); // 基类提供默认实现,子类可重写 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (completion) completion(YES, nil); // 发送同步完成信号 [self.dataListUpdatedSignal sendNext:@{@"action": @"sync_to_server", @"status": @"completed"}]; }); } - (void)syncDataFromServer:(void (^)(BOOL, NSError * _Nullable))completion { TJPLOG_INFO(@"[%@] 正在同步数据从服务器", NSStringFromClass([self class])); // 基类提供默认实现,子类可重写 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (completion) completion(YES, nil); // 发送同步完成信号 [self.dataListUpdatedSignal sendNext:@{@"action": @"sync_from_server", @"status": @"completed"}]; }); } #pragma mark - Option Method - (void)subscribeToRealTimeData:(NSString *)topic completion:(void (^)(BOOL, NSError * _Nullable))completion { TJPLOG_INFO(@"[%@] 订阅实时数据: %@", NSStringFromClass([self class]), topic); if (!topic || topic.length == 0) { NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataInvalid userInfo:@{NSLocalizedDescriptionKey: @"订阅主题不能为空"}]; if (completion) completion(NO, error); return; } [self.subscribedTopics addObject:topic]; // 模拟订阅成功 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (completion) completion(YES, nil); // 模拟实时数据推送 [self simulateRealTimeDataForTopic:topic]; }); } - (void)unsubscribeFromRealTimeData:(NSString *)topic { TJPLOG_INFO(@"[%@] 取消订阅实时数据: %@", NSStringFromClass([self class]), topic); [self.subscribedTopics removeObject:topic]; } - (void)uploadFile:(NSData *)fileData fileName:(NSString *)fileName progress:(void (^)(CGFloat))progress completion:(void (^)(NSString * _Nullable, NSError * _Nullable))completion { TJPLOG_INFO(@"[%@] 正在上传文件: %@, 大小: %lu 字节", NSStringFromClass([self class]), fileName, (unsigned long)fileData.length); if (!fileData || !fileName) { NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataInvalid userInfo:@{NSLocalizedDescriptionKey: @"文件数据或文件名不能为空"}]; if (completion) completion(nil, error); return; } // 模拟文件上传进度 调用实际项目中的网络框架上传 __block CGFloat currentProgress = 0.0; NSString *uploadId = [[NSUUID UUID] UUIDString]; self.uploadProgressMap[uploadId] = @(currentProgress); NSTimer *progressTimer = [NSTimer scheduledTimerWithTimeInterval:0.1 repeats:YES block:^(NSTimer *timer) { currentProgress += 0.1; self.uploadProgressMap[uploadId] = @(currentProgress); if (progress) progress(currentProgress); if (currentProgress >= 1.0) { [timer invalidate]; [self.uploadProgressMap removeObjectForKey:uploadId]; // 模拟上传完成 NSString *fileUrl = [NSString stringWithFormat:@"https://cdn.example.com/files/%@", fileName]; if (completion) completion(fileUrl, nil); // 发送上传完成信号 [self.dataListUpdatedSignal sendNext:@{ @"action": @"file_uploaded", @"fileName": fileName, @"fileUrl": fileUrl }]; } }]; } - (id _Nullable)getConfigValue:(NSString *)configKey { // 模拟配置获取,子类可重写 static NSDictionary *configs = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ configs = @{ @"api_base_url": @"https://api.example.com", @"max_retry_count": @3, @"request_timeout": @30.0, @"cache_expire_time": @(TJPCacheExpireTimeMedium), @"enable_debug": @YES, @"max_upload_size": @(10 * 1024 * 1024) // 10MB }; }); return configs[configKey]; } - (NSError * _Nullable)validateBusinessRules:(NSDictionary *)data { // 基础业务规则验证,子类可重写 if (!data || ![data isKindOfClass:[NSDictionary class]]) { return [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataInvalid userInfo:@{NSLocalizedDescriptionKey: @"数据格式错误"}]; } return nil; // 验证通过 } #pragma mark - Abstract Methods - (void)performDataRequestForPage:(NSInteger)page completion:(void (^)(NSArray * _Nullable, NSInteger, NSError * _Nullable))completion { // 调用新的带分页信息的方法 [self performDataRequestForPage:page withPagination:^(NSArray * _Nullable data, TJPPaginationInfo * _Nullable pagination, NSError * _Nullable error) { if (completion) { NSInteger totalPage = pagination && pagination.paginationType == TJPPaginationTypePageBased ? pagination.totalPages : 0; completion(data, totalPage, error); } }]; } - (void)performDataRequestForPage:(NSInteger)page withPagination:(void (^)(NSArray * _Nullable, TJPPaginationInfo * _Nullable, NSError * _Nullable))completion { // 这是新的抽象方法,子类必须实现 NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorBusinessLogicFailed userInfo:@{NSLocalizedDescriptionKey: @"子类必须实现performDataRequestForPage:withPagination:方法"}]; if (completion) completion(nil, nil, error); } #pragma mark - Methods for Subclass Override - (NSString *)baseURLString { return @"https://api.example.com"; } - (NSDictionary *)commonParameters { return @{ @"timestamp": @([[NSDate date] timeIntervalSince1970]), @"version": @"1.0", @"platform": @"ios" }; } - (NSDictionary *)parametersForPage:(NSInteger)page { NSMutableDictionary *params = [[self commonParameters] mutableCopy]; params[@"page"] = @(page); params[@"pageSize"] = @(20); return [params copy]; } - (NSArray *)processRawResponseData:(id)rawData { if ([rawData isKindOfClass:[NSArray class]]) { return (NSArray *)rawData; } else if ([rawData isKindOfClass:[NSDictionary class]]) { NSDictionary *dict = (NSDictionary *)rawData; return dict[@"data"] ?: dict[@"list"] ?: dict[@"items"] ?: @[]; } return @[]; } - (NSError * _Nullable)validateResponseData:(id)rawData { if (!rawData) { return [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataEmpty userInfo:@{NSLocalizedDescriptionKey: @"服务器返回空数据"}]; } return nil; } #pragma mark - Utility Methods - (BOOL)shouldCacheDataForPage:(NSInteger)page { return page <= 10; // 默认对前10页进行缓存 } - (void)setupInteractor { // 子类可重写此方法进行初始化设置 } - (void)teardownInteractor { // 清理订阅 [self.subscribedTopics removeAllObjects]; // 清理上传进度 [self.uploadProgressMap removeAllObjects]; // 清理分页信息 [self resetPagination]; // 子类可重写此方法进行清理工作 } #pragma mark - Pagination Management - (void)resetPagination { _currentPage = 0; _hasMoreData = YES; _currentPagination = nil; TJPLOG_INFO(@"[%@] 分页信息已重置", NSStringFromClass([self class])); } - (void)updatePaginationInfo:(TJPPaginationInfo *)paginationInfo { _currentPagination = [paginationInfo copy]; if (paginationInfo.paginationType == TJPPaginationTypePageBased) { _currentPage = paginationInfo.currentPage; _hasMoreData = paginationInfo.hasMore; } else { // 游标分页时,currentPage 用于记录已加载的页面数 _currentPage = _currentPage > 0 ? _currentPage : 1; _hasMoreData = paginationInfo.hasMore; } TJPLOG_INFO(@"[%@] 分页信息已更新: %@", NSStringFromClass([self class]), paginationInfo.debugDescription); } - (BOOL)canLoadNextPage { if (!_hasMoreData) return NO; if (_currentPagination) { return _currentPagination.canLoadNextPage; } // 如果没有分页信息但有更多数据,允许加载 return YES; } - (NSInteger)getNextPageNumber { if (!_currentPagination) { return _currentPage + 1; } if (_currentPagination.paginationType == TJPPaginationTypePageBased) { return _currentPagination.getNextPageNumber; } else { return _currentPage + 1; } } - (NSInteger)getCurrentPage { return _currentPage; } - (NSInteger)getTotalPage { if (!_currentPagination) return 0; if (_currentPagination.paginationType == TJPPaginationTypePageBased) { return _currentPagination.totalPages; } else { // 游标分页无总页数概念 return NSIntegerMax; } } - (BOOL)hasMoreData { return _hasMoreData; } #pragma mark - Private Methods - (void)simulateRealTimeDataForTopic:(NSString *)topic { // 模拟实时数据推送 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if ([self.subscribedTopics containsObject:topic]) { NSDictionary *realTimeData = @{ @"topic": topic, @"data": @{ @"message": [NSString stringWithFormat:@"实时数据更新: %@", topic], @"timestamp": @([[NSDate date] timeIntervalSince1970]) }, @"type": @"real_time_update" }; [self.dataListUpdatedSignal sendNext:realTimeData]; } }); } - (NSError *)createErrorWithCode:(TJPViperError)errorCode description:(NSString *)description { return [NSError errorWithDomain:TJPViperErrorDomain code:errorCode userInfo:@{NSLocalizedDescriptionKey: description ?: @"未知错误"}]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Interactor/TJPViperBaseInteractorProtocol.h ================================================ // // TJPViperBaseInteractorProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // VIPER Interactor基础协议 定义了Interactor层的标准接口和职责 #import #import NS_ASSUME_NONNULL_BEGIN @class TJPPaginationInfo; @protocol TJPViperBaseInteractorProtocol #pragma mark - 响应式信号 /// 透传跳转需求 @property (nonatomic, strong) RACSubject *navigateToPageSubject; /// 数据源更新需求 @property (nonatomic, strong) RACSubject *dataListUpdatedSignal; #pragma mark - 获取数据 /** * 获取分页数据 * @param page 页码 * @param success 成功回调 (数据数组, 总页数) * @param failure 失败回调 */ - (void)fetchDataForPageWithCompletion:(NSInteger)page success:(void (^)(NSArray * _Nullable data, NSInteger totalPage))success failure:(void (^)(NSError * _Nullable error))failure; // 带分页信息的数据加载(推荐使用) - (void)fetchDataForPageWithPagination:(NSInteger)page success:(void (^)(NSArray * _Nullable data, TJPPaginationInfo * _Nullable pagination))success failure:(void (^)(NSError * _Nullable error))failure; #pragma mark - 数据操作 /** * 创建数据 * @param data 要创建的数据 * @param completion 完成回调 */ - (void)createData:(NSDictionary *)data completion:(void (^)(id _Nullable result, NSError * _Nullable error))completion; /** * 更新数据 * @param dataId 数据ID * @param updateData 更新的数据 * @param completion 完成回调 */ - (void)updateDataWithId:(NSString *)dataId updateData:(NSDictionary *)updateData completion:(void (^)(id _Nullable result, NSError * _Nullable error))completion; /** * 删除数据 * @param dataId 数据ID * @param completion 完成回调 */ - (void)deleteDataWithId:(NSString *)dataId completion:(void (^)(BOOL success, NSError * _Nullable error))completion; /** * 搜索数据 * @param keyword 搜索关键词 * @param filters 筛选条件 * @param completion 完成回调 */ - (void)searchDataWithKeyword:(NSString *)keyword filters:(NSDictionary *)filters completion:(void (^)(NSArray * _Nullable results, NSError * _Nullable error))completion; #pragma mark - 缓存管理 /** * 清除指定缓存 * @param cacheKey 缓存键 */ - (void)clearCache:(NSString *)cacheKey; /** * 清除所有缓存 */ - (void)clearAllCache; /** * 获取缓存大小 * @return 缓存大小(字节) */ - (NSUInteger)getCacheSize; #pragma mark - 状态同步 /** * 同步数据到服务器 * @param completion 完成回调 */ - (void)syncDataToServer:(void (^)(BOOL success, NSError * _Nullable error))completion; /** * 从服务器同步数据 * @param completion 完成回调 */ - (void)syncDataFromServer:(void (^)(BOOL success, NSError * _Nullable error))completion; /** * 实时数据订阅 * @param topic 订阅主题 * @param completion 完成回调 */ - (void)subscribeToRealTimeData:(NSString *)topic completion:(void (^)(BOOL success, NSError * _Nullable error))completion; /** * 取消实时数据订阅 * @param topic 订阅主题 */ - (void)unsubscribeFromRealTimeData:(NSString *)topic; /** * 文件上传 * @param fileData 文件数据 * @param fileName 文件名 * @param progress 进度回调 * @param completion 完成回调 */ - (void)uploadFile:(NSData *)fileData fileName:(NSString *)fileName progress:(void (^)(CGFloat progress))progress completion:(void (^)(NSString * _Nullable fileUrl, NSError * _Nullable error))completion; /** * 获取配置信息 * @param configKey 配置键 * @return 配置值 */ - (id _Nullable)getConfigValue:(NSString *)configKey; /** * 业务规则验证 * @param data 待验证数据 * @return 验证错误,nil表示验证通过 */ - (NSError * _Nullable)validateBusinessRules:(NSDictionary *)data; #pragma mark - 分页管理 - (void)resetPagination; - (BOOL)canLoadNextPage; - (NSInteger)getNextPageNumber; - (NSInteger)getCurrentPage; - (NSInteger)getTotalPage; - (BOOL)hasMoreData; @optional @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Presenter/TJPViperBasePresenterImpl.h ================================================ // // TJPViperBasePresenterImpl.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import #import "TJPViperBasePresenterProtocol.h" NS_ASSUME_NONNULL_BEGIN @protocol TJPViperBaseInteractorProtocol, TJPViperBaseRouterHandlerProtocol, TJPBaseCellModelProtocol, TJPViperBaseViewControllerProtocol; @class TJPViperDefaultErrorHandler, TJPNavigationModel; @interface TJPViperBasePresenterImpl : NSObject // 核心组件 //presenter->强引用Interactor和router 防止提前释放 @property (nonatomic, strong) id baseInteractor; @property (nonatomic, strong) id baseRouter; // 此处用weak (避免循环引用) @property (nonatomic, weak) id contextProvider; // 错误处理 @property (nonatomic, strong, readonly) TJPViperDefaultErrorHandler *errorHandler; // 状态管理 @property (nonatomic, assign, readonly) BOOL isProcessingRequest; @property (nonatomic, strong, readonly) NSError *lastError; @property (nonatomic, strong, readonly) NSMutableDictionary *businessStateStorage; /** * 抽象方法:子类必须实现 * 根据CellModel构建NavigationModel * * @param cellModel 被点击的CellModel * @return 构建的NavigationModel,如果返回nil表示不处理该类型的Cell */ - (TJPNavigationModel *)buildNavigationModelFromCellModel:(id)cellModel; // 子类可重写的业务逻辑方法 - (void)preprocessRequestForPage:(NSInteger)page; - (void)postprocessResponseData:(NSArray *)data; - (BOOL)validateResponseData:(NSArray *)data; - (void)handleBusinessError:(NSError *)error; // 子类可重写的生命周期扩展方法 - (void)setupPresenter; - (void)teardownPresenter; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Presenter/TJPViperBasePresenterImpl.m ================================================ // // TJPViperBasePresenterImpl.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import "TJPViperBasePresenterImpl.h" #import "TJPViperBaseViewControllerProtocol.h" #import "TJPNetworkDefine.h" #import "TJPViperDefaultErrorHandler.h" #import "TJPViperBaseInteractorProtocol.h" #import "TJPViperBaseRouterHandlerProtocol.h" #import "TJPBaseCellModelProtocol.h" #import "TJPNavigationModel.h" #import "TJPPaginationInfo.h" @interface TJPViperBasePresenterImpl () @property (nonatomic, assign) BOOL isProcessingRequest; @property (nonatomic, strong) NSError *lastError; @property (nonatomic, strong) NSMutableSet *activeRequests; @property (nonatomic, strong) TJPViperDefaultErrorHandler *errorHandler; @property (nonatomic, strong) NSMutableDictionary *businessStateStorage; @property (nonatomic, assign) BOOL isInitialized; @end @implementation TJPViperBasePresenterImpl @synthesize viewUpdatedDataSignal = _viewUpdatedDataSignal; #pragma mark - Life Cycle - (instancetype)init { self = [super init]; if (self) { _activeRequests = [NSMutableSet set]; _isProcessingRequest = NO; _errorHandler = [TJPViperDefaultErrorHandler sharedHandler]; _businessStateStorage = [NSMutableDictionary dictionary]; _isInitialized = NO; [self setupPresenter]; } return self; } - (void)dealloc { TJPLogDealloc(); [self teardownPresenter]; } #pragma mark - Signal bind - (RACSubject *)viewUpdatedDataSignal { if (!_viewUpdatedDataSignal) { _viewUpdatedDataSignal = [RACSubject subject]; } return _viewUpdatedDataSignal; } - (void)bindInteractorToPageSubjectWithContextProvider:(id)contextProvider { self.contextProvider = contextProvider; if (!contextProvider || !self.baseInteractor) { TJPLOG_ERROR(@"无法绑定信号: contextProvider=%@, interactor=%@", contextProvider, self.baseInteractor); return; } @weakify(self) [[[self.baseInteractor.navigateToPageSubject takeUntil:self.rac_willDeallocSignal] deliverOnMainThread] subscribeNext:^(id cellModel) { @strongify(self) if (!self.contextProvider) { TJPLOG_ERROR(@"contextProvider在跳转期间已被释放"); return; } TJPLOG_INFO(@"接收到Cell模型信号: %@", NSStringFromClass([cellModel class])); // ===== 从CellModel构建TJPNavigationModel 模板方法:基类定义流程,子类实现具体逻辑 ===== TJPNavigationModel *navigationModel = [self buildNavigationModelFromCellModel:cellModel]; if (!navigationModel) { TJPLOG_ERROR(@"无法从CellModel构建NavigationModel: %@", NSStringFromClass([cellModel class])); return; } UIViewController *fromVC = [self.contextProvider currentViewController]; if (!fromVC) { TJPLOG_ERROR(@"无法获取当前上下文 VC,跳转终止"); return; } // 信号订阅成功 交给路由管理 BOOL navigationSuccess = [self.baseRouter navigateToRouteWithNavigationModel:navigationModel fromContext:fromVC animated:YES]; if (!navigationSuccess) { TJPLOG_ERROR(@"模型导航失败: %@", cellModel); // 使用错误处理器处理导航失败 NSError *navError = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorNavigationFailed userInfo:@{NSLocalizedDescriptionKey: @"页面跳转失败"}]; [self.errorHandler handleError:navError inContext:fromVC completion:^(BOOL shouldRetry) { }]; } }]; } - (TJPNavigationModel *)buildNavigationModelFromCellModel:(id)cellModel { // 基类提供默认实现,但建议子类重写 TJPLOG_WARN(@"[%@] 子类应该重写 buildNavigationModelFromCellModel: 方法", NSStringFromClass([self class])); // 基类提供最基础的默认实现 return [self defaultNavigationModelFromCellModel:cellModel]; } - (void)bindInteractorDataUpdateSubject { if (!self.baseInteractor) { TJPLOG_ERROR(@"无法绑定数据更新信号: interactor 为空"); return; } @weakify(self) [[self.baseInteractor.dataListUpdatedSignal takeUntil:self.rac_willDeallocSignal] subscribeNext:^(NSDictionary * _Nullable x) { @strongify(self) TJPLOG_INFO(@"[%@] 接收到来自Interactor的数据更新信号", NSStringFromClass([self class])); [self.viewUpdatedDataSignal sendNext:x]; }]; } #pragma mark - Refresh - (NSInteger)getCurrentPage { if (!self.baseInteractor) { TJPLOG_ERROR(@"baseInteractor为空,无法获取当前页码"); return 0; } return [self.baseInteractor getCurrentPage]; } - (NSInteger)getTotalPage { if (!self.baseInteractor) { TJPLOG_ERROR(@"baseInteractor为空,无法获取总页数"); return 0; } return [self.baseInteractor getTotalPage]; } - (BOOL)hasMoreData { if (!self.baseInteractor) { TJPLOG_ERROR(@"baseInteractor为空,无法判断是否有更多数据"); return NO; } return [self.baseInteractor hasMoreData]; } - (BOOL)canLoadNextPage { if (!self.baseInteractor) { TJPLOG_ERROR(@"baseInteractor为空,无法判断是否可以加载下一页"); return NO; } return [self.baseInteractor canLoadNextPage]; } - (NSInteger)getNextPageNumber { if (!self.baseInteractor) { TJPLOG_ERROR(@"baseInteractor为空,无法获取下一页页码"); return 1; } return [self.baseInteractor getNextPageNumber]; } #pragma mark - Load Data - (void)fetchInteractorDataForPage:(NSInteger)page success:(void (^)(NSArray * _Nonnull, NSInteger))success failure:(void (^)(NSError * _Nonnull))failure { // 向后兼容的方法 [self fetchInteractorDataForPageWithPagination:page success:^(NSArray *data, TJPPaginationInfo *pagination) { if (success) { NSInteger totalPage = pagination && pagination.paginationType == TJPPaginationTypePageBased ? pagination.totalPages : 0; success(data, totalPage); } } failure:failure]; } - (void)fetchInteractorDataForPageWithPagination:(NSInteger)page success:(void (^)(NSArray * _Nonnull, TJPPaginationInfo * _Nullable))success failure:(void (^)(NSError * _Nonnull))failure { // 参数验证 if (page <= 0) { NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataInvalid userInfo:@{NSLocalizedDescriptionKey: @"页码必须大于0"}]; if (failure) failure(error); return; } // 检查是否有相同页码的请求正在进行 NSNumber *pageKey = @(page); if ([self.activeRequests containsObject:pageKey]) { TJPLOG_INFO(@"第 %ld 页的请求已经在进行中", (long)page); return; } // 预处理请求 [self preprocessRequestForPage:page]; // 标记请求开始 [self.activeRequests addObject:pageKey]; self.isProcessingRequest = YES; @weakify(self) // 优先使用新的分页方法 if ([self.baseInteractor respondsToSelector:@selector(fetchDataForPageWithPagination:success:failure:)]) { [self.baseInteractor fetchDataForPageWithPagination:page success:^(NSArray *data, TJPPaginationInfo *pagination) { @strongify(self) [self handleDataLoadSuccess:data pagination:pagination pageKey:pageKey success:success]; } failure:^(NSError *error) { @strongify(self) [self handleDataLoadFailure:error pageKey:pageKey failure:failure]; }]; } else { // 回退到旧方法 [self.baseInteractor fetchDataForPageWithCompletion:page success:^(NSArray *data, NSInteger totalPage) { @strongify(self) // 从旧方法构造分页信息 TJPPaginationInfo *pagination = nil; if (totalPage > 0) { pagination = [TJPPaginationInfo pageBasedPaginationWithPage:page pageSize:data.count totalCount:totalPage * data.count]; } [self handleDataLoadSuccess:data pagination:pagination pageKey:pageKey success:success]; } failure:^(NSError *error) { @strongify(self) [self handleDataLoadFailure:error pageKey:pageKey failure:failure]; }]; } } #pragma mark - Data Load Response Handling - 数据加载响应处理 - (void)handleDataLoadSuccess:(NSArray *)data pagination:(TJPPaginationInfo *)pagination pageKey:(NSNumber *)pageKey success:(void (^)(NSArray *, TJPPaginationInfo *))success { // 清理请求状态 [self.activeRequests removeObject:pageKey]; self.isProcessingRequest = self.activeRequests.count > 0; self.lastError = nil; // 验证响应数据 if (![self validateResponseData:data]) { NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataInvalid userInfo:@{NSLocalizedDescriptionKey: @"服务器返回数据格式错误"}]; [self handleBusinessError:error]; return; } // 后处理响应数据 [self postprocessResponseData:data]; TJPLOG_INFO(@"[%@] 第 %@ 页数据请求成功,分页信息: %@", NSStringFromClass([self class]), pageKey, pagination.debugDescription); if (success) success(data, pagination); } - (void)handleDataLoadFailure:(NSError *)error pageKey:(NSNumber *)pageKey failure:(void (^)(NSError *))failure { // 清理请求状态 [self.activeRequests removeObject:pageKey]; self.isProcessingRequest = self.activeRequests.count > 0; self.lastError = error; // 处理业务错误 [self handleBusinessError:error]; TJPLOG_ERROR(@"[%@] 第 %@ 页数据请求失败: %@", NSStringFromClass([self class]), pageKey, error.localizedDescription); if (failure) failure(error); } #pragma mark - Manage Status - (void)presenterDidInitialize { if (self.isInitialized) { TJPLOG_WARN(@"Presenter 已经初始化过"); return; } self.isInitialized = YES; TJPLOG_INFO(@"[%@] Presenter 初始化完成", NSStringFromClass([self class])); // 初始化业务状态 [self resetBusinessState]; } - (void)viewWillAppear { TJPLOG_INFO(@"[%@] viewWillAppear", NSStringFromClass([self class])); // 子类可重写此方法进行特定处理 } - (void)viewDidAppear { TJPLOG_INFO(@"[%@] viewDidAppear", NSStringFromClass([self class])); // 子类可重写此方法进行特定处理 } - (void)viewWillDisappear { TJPLOG_INFO(@"[%@] viewWillDisappear", NSStringFromClass([self class])); // 子类可重写此方法进行特定处理 } - (void)viewDidDisappear { TJPLOG_INFO(@"[%@] viewDidDisappear", NSStringFromClass([self class])); // 子类可重写此方法进行特定处理 } #pragma mark - Manage State - (NSDictionary *)currentBusinessState { NSMutableDictionary *state = [self.businessStateStorage mutableCopy]; // 添加基础状态信息 state[@"isProcessingRequest"] = @(self.isProcessingRequest); state[@"hasError"] = @(self.lastError != nil); state[@"isInitialized"] = @(self.isInitialized); state[@"timestamp"] = @([[NSDate date] timeIntervalSince1970]); if (self.lastError) { state[@"lastError"] = @{ @"domain": self.lastError.domain, @"code": @(self.lastError.code), @"message": self.lastError.localizedDescription }; } return [state copy]; } - (void)resetBusinessState { [self.businessStateStorage removeAllObjects]; self.lastError = nil; // 设置默认状态 self.businessStateStorage[@"initialized"] = @YES; TJPLOG_INFO(@"[%@] 业务状态已重置", NSStringFromClass([self class])); } #pragma mark - User Interactor - (void)handleUserInteraction:(NSString *)event withData:(nullable id)data { TJPLOG_INFO(@"[%@] 处理用户交互事件: %@,数据: %@", NSStringFromClass([self class]), event, data); // 基类提供默认实现,子类可重写 // 这里可以添加通用的用户交互处理逻辑 } - (NSError * _Nullable)validateUserInput:(NSDictionary *)input { // 基础验证 if (!input || ![input isKindOfClass:[NSDictionary class]]) { return [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataInvalid userInfo:@{NSLocalizedDescriptionKey: @"输入数据格式错误"}]; } // 子类可重写此方法进行特定验证 return nil; // 验证通过 } - (BOOL)handleDeepLink:(NSURL *)url parameters:(NSDictionary *)parameters { TJPLOG_INFO(@"[%@] 处理深度链接: %@,参数: %@", NSStringFromClass([self class]), url, parameters); // 基类提供默认实现,子类可重写 return NO; // 默认不处理 } - (void)handlePushNotification:(NSDictionary *)notification { TJPLOG_INFO(@"[%@] 处理推送通知: %@", NSStringFromClass([self class]), notification); // 基类提供默认实现,子类可重写 } - (void)preloadData { TJPLOG_INFO(@"[%@] 预加载数据", NSStringFromClass([self class])); // 基类提供默认实现,子类可重写 } - (void)cleanup { TJPLOG_INFO(@"[%@] 清理资源", NSStringFromClass([self class])); // 清理业务状态 [self.businessStateStorage removeAllObjects]; // 清理请求状态 [self.activeRequests removeAllObjects]; self.isProcessingRequest = NO; self.lastError = nil; // 子类可重写此方法进行特定清理 } #pragma mark - 子类可重写的业务逻辑方法 - (void)preprocessRequestForPage:(NSInteger)page { TJPLOG_INFO(@"[%@] 正在预处理第 %ld 页的请求", NSStringFromClass([self class]), (long)page); // 子类可重写 } - (void)postprocessResponseData:(NSArray *)data { TJPLOG_INFO(@"[%@] 正在后处理 %lu 条响应数据", NSStringFromClass([self class]), (unsigned long)data.count); // 子类可重写 } - (BOOL)validateResponseData:(NSArray *)data { if (!data) { return NO; } return YES; // 子类可重写进行更详细的验证 } - (void)handleBusinessError:(NSError *)error { TJPLOG_ERROR(@"[%@] 发生业务错误: %@", NSStringFromClass([self class]), error.localizedDescription); // 子类可重写 } #pragma mark - 子类可重写的扩展方法 - (void)setupPresenter { // 子类可重写此方法进行初始化设置 } - (void)teardownPresenter { // 子类可重写此方法进行清理工作 } #pragma mark - Private Method - (TJPNavigationModel *)defaultNavigationModelFromCellModel:(id)cellModel { if (!cellModel) { return nil; } // 基于类名约定生成routeId NSString *className = NSStringFromClass([cellModel class]); NSString *routeId = nil; if ([className hasSuffix:@"CellModel"]) { NSString *prefix = [className substringToIndex:className.length - 9]; // 去掉"CellModel" if ([prefix hasPrefix:@"TJP"]) { prefix = [prefix substringFromIndex:3]; // 去掉"TJP"前缀 } routeId = [prefix lowercaseString]; } else { routeId = @"unknown"; } // 构建基础参数 NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; parameters[@"cellType"] = className; parameters[@"timestamp"] = @([[NSDate date] timeIntervalSince1970]); // 尝试提取一些通用属性 if ([cellModel respondsToSelector:@selector(title)]) { id title = [(NSObject *)cellModel valueForKey:@"title"]; if (title) parameters[@"title"] = title; } TJPNavigationModel *model = [TJPNavigationModel modelWithRouteId:routeId parameters:[parameters copy] routeType:TJPNavigationRouteTypeViewPush]; model.animated = YES; TJPLOG_INFO(@"[%@] 使用默认实现构建NavigationModel: %@ -> %@", NSStringFromClass([self class]), className, routeId); return model; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Presenter/TJPViperBasePresenterProtocol.h ================================================ // // TJPViperBasePresenterProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // VIPER Presenter基础协议 定义了Presenter层的标准接口和职责 #import #import NS_ASSUME_NONNULL_BEGIN @protocol TJPViperBaseViewControllerProtocol; @protocol TJPViperBasePresenterProtocol #pragma mark - 获取数据 /** * 获取分页数据 * @param page 页码 * @param success 成功回调 * @param failure 失败回调 */ - (void)fetchInteractorDataForPage:(NSInteger)page success:(void (^)(NSArray * _Nonnull data, NSInteger totalPage))success failure:(void (^)(NSError * _Nonnull error))failure; #pragma mark - 信号绑定 /** * 绑定Interactor的页面跳转信号 * @param contextProvider controller上下文 */ - (void)bindInteractorToPageSubjectWithContextProvider:(id)contextProvider; /** * 绑定Interactor的数据更新信号 */ - (void)bindInteractorDataUpdateSubject; /** * presenter层透传刷新信号 */ @property (nonatomic, strong) RACSubject *viewUpdatedDataSignal; #pragma mark - 分页状态 - (NSInteger)getCurrentPage; - (NSInteger)getTotalPage; - (BOOL)hasMoreData; - (BOOL)canLoadNextPage; - (NSInteger)getNextPageNumber; #pragma mark - 生命周期管理 /** * Presenter初始化完成 */ - (void)presenterDidInitialize; /** * View即将出现 */ - (void)viewWillAppear; /** * View已经出现 */ - (void)viewDidAppear; /** * View即将消失 */ - (void)viewWillDisappear; /** * View已经消失 */ - (void)viewDidDisappear; /** * 获取当前业务状态 */ - (NSDictionary *)currentBusinessState; /** * 重置业务状态 */ - (void)resetBusinessState; /** * 处理用户交互事件 * @param event 事件名称 * @param data 事件数据 */ - (void)handleUserInteraction:(NSString *)event withData:(nullable id)data; /** * 处理用户输入验证 * @param input 用户输入 * @return 验证结果 */ - (NSError * _Nullable)validateUserInput:(NSDictionary *)input; @optional /** * 处理深度链接 * @param url 深度链接URL * @param parameters 参数 */ - (BOOL)handleDeepLink:(NSURL *)url parameters:(NSDictionary *)parameters; /** * 处理推送消息 * @param notification 推送消息 */ - (void)handlePushNotification:(NSDictionary *)notification; /** * 数据预加载 */ - (void)preloadData; /** * 清理资源 */ - (void)cleanup; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Response/TJPBaseResponse.h ================================================ // // TJPBaseResponse.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // 通用API响应基类 支持泛型,可以指定data字段的具体类型 #import NS_ASSUME_NONNULL_BEGIN @class TJPPaginationInfo; @interface TJPBaseResponse<__covariant DataType> : NSObject // 通用响应字段 @property (nonatomic, assign) NSInteger code; @property (nonatomic, copy) NSString *message; @property (nonatomic, strong, nullable) DataType data; @property (nonatomic, assign) NSInteger totalCount; // 分页信息(可选) @property (nonatomic, strong, nullable) TJPPaginationInfo *pagination; // 原始响应数据(用于调试和扩展) @property (nonatomic, strong, nullable) NSDictionary *rawResponseDict; // 便利构造方法 + (instancetype)responseWithDict:(NSDictionary *)dict; + (instancetype)responseWithDict:(NSDictionary *)dict dataClass:(Class)dataClass; // 子类可重写的解析方法 - (DataType _Nullable)parseDataFromDict:(NSDictionary *)dict; - (TJPPaginationInfo * _Nullable)parsePaginationFromDict:(NSDictionary *)dict; // 响应状态判断 - (BOOL)isSuccess; - (BOOL)hasData; - (BOOL)hasPagination; // 调试信息 - (NSString *)debugDescription; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Response/TJPBaseResponse.m ================================================ // // TJPBaseResponse.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseResponse.h" #import "TJPPaginationInfo.h" @implementation TJPBaseResponse + (instancetype)responseWithDict:(NSDictionary *)dict { return [self responseWithDict:dict dataClass:nil]; } + (instancetype)responseWithDict:(NSDictionary *)dict dataClass:(Class)dataClass { if (!dict || ![dict isKindOfClass:[NSDictionary class]]) { return nil; } TJPBaseResponse *response = [[self alloc] init]; response.rawResponseDict = dict; // 解析基础字段 response.code = [dict[@"code"] integerValue]; response.message = dict[@"message"] ?: @""; // 解析数据字段 response.data = [response parseDataFromDict:dict]; // 解析分页信息 response.pagination = [response parsePaginationFromDict:dict]; return response; } #pragma mark - 数据解析方法(子类可重写) - (id)parseDataFromDict:(NSDictionary *)dict { // 基类提供默认实现 id dataValue = dict[@"data"]; if (!dataValue) { // 尝试其他可能的字段名 dataValue = dict[@"result"] ?: dict[@"content"]; } return dataValue; } - (TJPPaginationInfo *)parsePaginationFromDict:(NSDictionary *)dict { // 从多个可能的位置查找分页信息 NSDictionary *paginationDict = nil; // 1. 尝试从data字段中获取 if ([dict[@"data"] isKindOfClass:[NSDictionary class]]) { NSDictionary *dataDict = dict[@"data"]; paginationDict = dataDict[@"pagination"] ?: dataDict[@"page_info"] ?: dataDict[@"paging"]; } // 2. 尝试从根级别获取 if (!paginationDict) { paginationDict = dict[@"pagination"] ?: dict[@"page_info"] ?: dict[@"paging"]; } if (paginationDict && [paginationDict isKindOfClass:[NSDictionary class]]) { return [TJPPaginationInfo paginationWithDict:paginationDict]; } return nil; } #pragma mark - 状态判断方法 - (BOOL)isSuccess { return self.code == 200 || self.code == 0; // 支持不同的成功码规范 } - (BOOL)hasData { return self.data != nil; } - (BOOL)hasPagination { return self.pagination != nil; } #pragma mark - 调试信息 - (NSString *)debugDescription { NSMutableString *debug = [NSMutableString string]; [debug appendFormat:@"<%@: %p>\n", NSStringFromClass([self class]), self]; [debug appendFormat:@" code: %ld\n", (long)self.code]; [debug appendFormat:@" message: %@\n", self.message]; [debug appendFormat:@" hasData: %@\n", @(self.hasData)]; [debug appendFormat:@" hasPagination: %@\n", @(self.hasPagination)]; if (self.pagination) { [debug appendFormat:@" pagination: %@\n", self.pagination.debugDescription]; } return [debug copy]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPNavigationCoordinator.h ================================================ // // TJPNavigationCoordinator.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // 路由中枢类 #import #import "TJPNavigationDefines.h" NS_ASSUME_NONNULL_BEGIN @protocol TJPViperBaseRouterHandlerProtocol; @class TJPNavigationModel; @interface TJPNavigationCoordinator : NSObject @property (nonatomic, strong) dispatch_queue_t syncQueue; //Coordinator弱引用持有handler @property (nonatomic, strong) NSMapTable *handlers; /// 单例方法 + (instancetype)sharedInstance; /// 注册处理器 建议应用启动时注册 - (void)registerHandler:(id)handler forRouteType:(TJPNavigationRouteType)routeType; /// 取消注册 - (void)unregisterHandlerForRouteType:(TJPNavigationRouteType)routeType; - (id)handlerForRouteType:(TJPNavigationRouteType)routeType; /// 协调器进行跳转分发 - (BOOL)dispatchRequestWithModel:(TJPNavigationModel *)model inContext:(UIViewController *)context; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPNavigationCoordinator.m ================================================ // // TJPNavigationCoordinator.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import "TJPNavigationCoordinator.h" #import "TJPViperBaseRouterHandlerProtocol.h" #import "TJPNavigationModel.h" #import "TJPNetworkDefine.h" @interface TJPNavigationCoordinator () @end @implementation TJPNavigationCoordinator + (instancetype)sharedInstance { static TJPNavigationCoordinator *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[TJPNavigationCoordinator alloc] init]; }); return instance; } - (instancetype)init { if (self = [super init]) { _handlers = [NSMapTable strongToWeakObjectsMapTable]; _syncQueue = dispatch_queue_create("com.tjp.navigationCoordinator.syncQyeye", DISPATCH_QUEUE_SERIAL); } return self; } - (void)registerHandler:(id)handler forRouteType:(TJPNavigationRouteType)routeType { if (!handler || routeType == TJPNavigationRouteTypeUnknown) { TJPLOG_ERROR(@"当前路由类型未知,请检查 routeType: %lu", (unsigned long)routeType); return; } dispatch_async(self.syncQueue, ^{ [self.handlers setObject:handler forKey:@(routeType)]; TJPLOG_INFO(@"添加类型处理器: %@ 类型:%lu", handler, (unsigned long)routeType); }); } - (void)unregisterHandlerForRouteType:(TJPNavigationRouteType)routeType { dispatch_async(self.syncQueue, ^{ [self.handlers removeObjectForKey:@(routeType)]; }); } - (id)handlerForRouteType:(TJPNavigationRouteType)routeType { __block id handler; dispatch_sync(self.syncQueue, ^{ handler = [self.handlers objectForKey:@(routeType)]; }); return handler; } - (BOOL)dispatchRequestWithModel:(TJPNavigationModel *)model inContext:(UIViewController *)context{ if (!model || model.routeId.length == 0) { NSLog(@"[NavigationCoordinator] 无效的 NavigationModel"); return NO; } __block id handler = nil; dispatch_sync(self.syncQueue, ^{ handler = [self.handlers objectForKey:@(model.routeType)]; }); if (!handler) { NSLog(@"[NavigationCoordinator] 未找到对应的 Handler:routeType = %ld", (long)model.routeType); // if ([self.delegate respondsToSelector:@selector(coordinator:didFailWithUnregisteredRouteType:)]) { // [self.delegate coordinator:self didFailWithUnregisteredRouteType:model.routeType]; // } return NO; } //策略模式分发 return [handler handleRequestWithModel:model context:context]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPNavigationDefines.h ================================================ // // TJPNavigationDefines.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #ifndef TJPNavigationDefines_h #define TJPNavigationDefines_h typedef NS_ENUM(NSUInteger, TJPNavigationRouteType) { TJPNavigationRouteTypeUnknown = 0, TJPNavigationRouteTypeViewPush, // Push跳转 TJPNavigationRouteTypeViewPresent, // 弹出跳转 TJPNavigationRouteTypeViewCustom, // 自定义跳转 TJPNavigationRouteTypeAction, // 跳转信号,但不执行动作 TJPNavigationRouteTypeHybrid }; typedef NS_ENUM(NSUInteger, TJPNavigationTransitionStyle) { TJPNavigationTransitionStyleDefault, TJPNavigationTransitionStyleFade, TJPNavigationTransitionStyleSlide }; //typedef NS_OPTIONS(NSUInteger, TJPNavigationType) { // TJPNavigationTypePush, // 普通Push跳转 // TJPNavigationTypePresent, // 弹出跳转 // TJPNavigationTypeModal, // Modal跳转 // TJPNavigationTypeCustom // 自定义跳转 //}; /** * 路由创建策略 */ typedef NS_ENUM(NSInteger, TJPRouterCreationStrategy) { TJPRouterCreationStrategyHardcode, // 硬编码创建 TJPRouterCreationStrategyDI, // 依赖注入创建 TJPRouterCreationStrategyFactory // 工厂模式创建 }; #endif /* TJPNavigationDefines_h */ ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPViewPresentHandler.h ================================================ // // TJPViewPresentHandler.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/1. // #import #import "TJPViperBaseRouterHandlerProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPViewPresentHandler : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPViewPresentHandler.m ================================================ // // TJPViewPresentHandler.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/1. // #import "TJPViewPresentHandler.h" #import "TJPNavigationModel.h" #import "TJPNetworkDefine.h" @implementation TJPViewPresentHandler - (BOOL)handleRequestWithModel:(TJPNavigationModel *)model context:(UIViewController *)context { UIViewController *targetVC = model.targetVC; if (!targetVC) { NSLog(@"[ViewPresentHandler] Router 未提供 targetVC,跳转失败:%@", model.routeId); return NO; } if (context) { [context presentViewController:targetVC animated:YES completion:nil]; return YES; } else { NSLog(@"[ViewPresentHandler] 当前上下文无 NavigationController"); return NO; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPViewPushHandler.h ================================================ // // TJPViewPushHandler.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import #import "TJPViperBaseRouterHandlerProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPViewPushHandler : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPViewPushHandler.m ================================================ // // TJPViewPushHandler.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import "TJPViewPushHandler.h" #import "TJPNavigationModel.h" #import "TJPNetworkDefine.h" @implementation TJPViewPushHandler - (void)dealloc { NSLog(@"TJPViewPushHandler dealloc: %p", self); } - (BOOL)handleRequestWithModel:(TJPNavigationModel *)model context:(UIViewController *)context { UIViewController *targetVC = model.targetVC; if (!targetVC) { NSLog(@"[ViewPushHandler] Router 未提供 targetVC,跳转失败:%@", model.routeId); return NO; } if (context.navigationController) { [context.navigationController pushViewController:targetVC animated:model.animated]; return YES; } else { NSLog(@"[ViewPushHandler] 当前上下文无 NavigationController"); return NO; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPViperBaseRouterHandlerProtocol.h ================================================ // // TJPViperBaseRouterHandlerProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // 定义了Router层的标准接口和职责 #import #import "TJPNavigationDefines.h" NS_ASSUME_NONNULL_BEGIN @class TJPNavigationModel; @protocol TJPViperModuleProvider; @protocol TJPViperBaseRouterHandlerProtocol @optional /** * 处理导航逻辑 * @param model 导航模型 * @param context 当前上下文 * @return 是否导航成功 */ - (BOOL)handleNavigationLogicWithModel:(TJPNavigationModel *)model context:(UIViewController *)context; /** * 处理导航请求 * @param model 导航模型 * @param context 当前上下文 * @return 是否处理成功 */ - (BOOL)handleRequestWithModel:(TJPNavigationModel *)model context:(UIViewController *)context; /** * 完整的路由跳转方法 * @param model 导航模型 * @param context 当前上下文ViewController * @param animated 是否显示动画 * @return 是否处理成功 */ - (BOOL)navigateToRouteWithNavigationModel:(TJPNavigationModel *)model fromContext:(UIViewController *)context animated:(BOOL)animated; // 根据路由标识创建对应的ViewController - (UIViewController *)createViewControllerForRoute:(NSString *)routeId parameters:(NSDictionary *)parameters; // 获取指定路由的创建策略 - (TJPRouterCreationStrategy)creationStrategyForRoute:(NSString *)routeId; // 验证路由参数是否有效 - (BOOL)validateRoute:(NSString *)routeId parameters:(NSDictionary *)parameters; // 即将开始导航时调用 - (void)willNavigateToRoute:(NSString *)routeId parameters:(NSDictionary *)parameters; // 导航完成后调用 - (void)didNavigateToRoute:(NSString *)routeId success:(BOOL)success; // 预处理路由参数 - (NSDictionary *)processParametersForRoute:(NSString *)routeId parameters:(NSDictionary *)parameters; // 配置ViewController - (void)configureViewController:(UIViewController *)viewController forRoute:(NSString *)routeId parameters:(NSDictionary *)parameters; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPViperBaseRouterImpl.h ================================================ // // TJPViperBaseRouterImpl.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // 提供通用的路由功能和扩展点,子类通过重写相关方法实现具体的路由逻辑 #import #import "TJPViperBaseRouterHandlerProtocol.h" NS_ASSUME_NONNULL_BEGIN @class TJPViperDefaultErrorHandler; /** * - Base类:定义通用的跳转规则、参数处理、生命周期管理等 * - 子类:只需要关心具体ViewController的创建逻辑 * - 协调器模式:通过TJPNavigationCoordinator统一分发路由请求 */ @interface TJPViperBaseRouterImpl : NSObject // 创建策略 @property (nonatomic, assign) TJPRouterCreationStrategy creationStrategy; // 错误处理 @property (nonatomic, strong, readonly) TJPViperDefaultErrorHandler *errorHandler; // 是否启用导航日志 @property (nonatomic, assign) BOOL enableNavigationLog; // 是否启用导航动画 @property (nonatomic, assign) BOOL enableNavigationAnimation; //泛型,通用的Provider存储,子类可以转换为具体类型 @property (nonatomic, strong) id moduleProvider; // 初始化方法 - (instancetype)initWithModuleProvider:(id)moduleProvider; - (BOOL)navigateToRouteWithNavigationModel:(TJPNavigationModel *)model fromContext:(UIViewController *)context animated:(BOOL)animated; /// 子类必须实现:根据路由标识创建ViewController - (UIViewController *)createViewControllerForRoute:(NSString *)routeId parameters:(NSDictionary *)parameters; // 路由注册 - (void)registerRoute:(NSString *)routeIdentifier selectorName:(NSString *)selectorName creationStrategy:(TJPRouterCreationStrategy)strategy; // 批量注册路由 - (void)registerRoutesFromConfig:(NSDictionary *)config; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/Router/TJPViperBaseRouterImpl.m ================================================ // // TJPViperBaseRouterImpl.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import "TJPViperBaseRouterImpl.h" #import "TJPNetworkDefine.h" #import "TJPNavigationModel.h" #import "TJPNavigationDefines.h" #import "TJPViperDefaultErrorHandler.h" #import "TJPNavigationCoordinator.h" @interface TJPViperBaseRouterImpl () @end @implementation TJPViperBaseRouterImpl #pragma mark - LifeCycle - (instancetype)initWithModuleProvider:(id)moduleProvider { if (self = [super init]) { _moduleProvider = moduleProvider; _enableNavigationAnimation = YES; _enableNavigationLog = YES; _creationStrategy = TJPRouterCreationStrategyDI; // 默认DI方式 } return self; } - (void)dealloc { TJPLogDealloc(); } #pragma mark - TJPViperBaseRouterHandlerProtocol - (BOOL)navigateToRouteWithNavigationModel:(TJPNavigationModel *)model fromContext:(UIViewController *)context animated:(BOOL)animated { // 生命周期:准备导航 [self willNavigateToRoute:model.routeId parameters:model.parameters]; if (self.enableNavigationLog) { NSLog(@"[%@] 开始导航: %@ -> %@", NSStringFromClass([self class]), NSStringFromClass([context class]), model.routeId); } // 参数验证 if (![self validateRoute:model.routeId parameters:model.parameters]) { NSLog(@"[%@] 路由验证失败: %@", NSStringFromClass([self class]), model.routeId); [self didNavigateToRoute:model.routeId success:NO]; return NO; } // 参数预处理 NSDictionary *processedParams = [self processParametersForRoute:model.routeId parameters:model.parameters]; model.parameters = processedParams; model.animated = animated; // ===== 定义模板方法 子类重写并创建ViewController 支持硬编码及DI注入====== UIViewController *targetVC = [self createViewControllerForRoute:model.routeId parameters:processedParams]; if (!targetVC) { NSLog(@"[%@] 创建ViewController失败: %@", NSStringFromClass([self class]), model.routeId); [self didNavigateToRoute:model.routeId success:NO]; return NO; } // 配置ViewController [self configureViewController:targetVC forRoute:model.routeId parameters:processedParams]; model.targetVC = targetVC; // 使用协调器模式分发路由请求 确保Handler系统正常工作 BOOL success = [self dispatchNavigationWithModel:model fromContext:context]; // 生命周期:导航完成 [self didNavigateToRoute:model.routeId success:success]; if (self.enableNavigationLog) { NSLog(@"[%@] 导航结果: %@ %@", NSStringFromClass([self class]), model.routeId, success ? @"成功" : @"失败"); } return success; } - (BOOL)dispatchNavigationWithModel:(TJPNavigationModel *)model fromContext:(UIViewController *)context { return [[TJPNavigationCoordinator sharedInstance] dispatchRequestWithModel:model inContext:context]; } #pragma mark - 协议方法的默认实现(子类可重写) - (TJPRouterCreationStrategy)creationStrategyForRoute:(NSString *)routeId { // 默认使用全局策略,子类可以为不同路由指定不同策略 return self.creationStrategy; } - (BOOL)validateRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { // 默认只检查routeId非空,子类可以添加更复杂的验证逻辑 return routeId.length > 0; } - (void)willNavigateToRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { // 默认空实现,子类可重写添加统计埋点等逻辑 } - (void)didNavigateToRoute:(NSString *)routeId success:(BOOL)success { // 默认空实现,子类可重写添加统计分析等逻辑 } - (NSDictionary *)processParametersForRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { // 默认不处理,直接返回原参数,子类可重写添加通用参数等 return parameters; } - (void)configureViewController:(UIViewController *)viewController forRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { // 默认的参数注入逻辑:通过KVC设置属性 if (parameters) { for (NSString *key in parameters) { if ([viewController respondsToSelector:NSSelectorFromString(key)]) { @try { [viewController setValue:parameters[key] forKey:key]; } @catch (NSException *exception) { NSLog(@"[%@] 参数注入失败: %@ - %@", NSStringFromClass([self class]), key, exception.reason); } } } } } - (UIViewController *)createViewControllerForRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { NSAssert(NO, @"子类必须实现 createViewControllerForRoute:parameters: 方法"); return nil; } - (BOOL)handleNavigationLogicWithModel:(TJPNavigationModel *)model context:(UIViewController *)context { // 保持原有的协调器模式调用 return [[TJPNavigationCoordinator sharedInstance] dispatchRequestWithModel:model inContext:context]; } - (void)registerRoute:(NSString *)routeIdentifier selectorName:(NSString *)selectorName creationStrategy:(TJPRouterCreationStrategy)strategy { // 保持兼容,但新设计中推荐使用createViewControllerForRoute方法 NSLog(@"[%@] registerRoute方法已废弃,请使用createViewControllerForRoute方法", NSStringFromClass([self class])); } - (void)registerRoutesFromConfig:(NSDictionary *)config { // 保持兼容,但新设计中推荐直接在子类中实现路由逻辑 NSLog(@"[%@] registerRoutesFromConfig方法已废弃,请在子类中直接实现路由逻辑", NSStringFromClass([self class])); } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/StateMachine/TJPViewControllerStateMachine.h ================================================ // // TJPViewControllerStateMachine.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/31. // view状态机 管理状态转换逻辑,不处理UI和业务逻辑 #import NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, TJPViewControllerState) { TJPViewControllerStateIdle, // 空闲状态 TJPViewControllerStateInitialLoading, // 初始加载 TJPViewControllerStateContent, // 内容显示 TJPViewControllerStateRefreshing, // 刷新中 TJPViewControllerStateLoadingMore, // 加载更多 TJPViewControllerStateEmpty, // 空数据 TJPViewControllerStateError // 错误状态 }; @protocol TJPViewControllerStateMachineDelegate @optional /// 状态转换前的回调 - 可以阻止状态转换 - (BOOL)stateMachine:(id)stateMachine shouldTransitionFromState:(TJPViewControllerState)fromState toState:(TJPViewControllerState)toState; /// 状态转换后的回调 - (void)stateMachine:(id)stateMachine didTransitionFromState:(TJPViewControllerState)fromState toState:(TJPViewControllerState)toState; /// 状态转换失败的回调 - (void)stateMachine:(id)stateMachine failedTransitionFromState:(TJPViewControllerState)fromState toState:(TJPViewControllerState)toState; @end @interface TJPViewControllerStateMachine : NSObject @property (nonatomic, assign, readonly) TJPViewControllerState currentState; @property (nonatomic, weak) id delegate; /// 初始化状态机 - (instancetype)initWithInitialState:(TJPViewControllerState)initialState; /// 尝试转换状态 - (BOOL)transitionToState:(TJPViewControllerState)newState; /// 重置到空闲状态 - (void)resetToIdleState; /// 检查状态转换是否合法 - (BOOL)canTransitionToState:(TJPViewControllerState)newState; /// 获取状态描述 - (NSString *)stateDescription:(TJPViewControllerState)state; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/StateMachine/TJPViewControllerStateMachine.m ================================================ // // TJPViewControllerStateMachine.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/31. // #import "TJPViewControllerStateMachine.h" #import "TJPNetworkDefine.h" @interface TJPViewControllerStateMachine () @property (nonatomic, assign, readwrite) TJPViewControllerState currentState; @property (nonatomic, strong) NSDictionary *> *stateTransitionRules; @end @implementation TJPViewControllerStateMachine #pragma mark - Life Cycle - (instancetype)initWithInitialState:(TJPViewControllerState)initialState { if (self = [super init]) { _currentState = initialState; [self setupStateTransitionRules]; } return self; } - (instancetype)init { return [self initWithInitialState:TJPViewControllerStateIdle]; } #pragma mark - Public method - (BOOL)transitionToState:(TJPViewControllerState)newState { // 检查是否为相同状态 if (self.currentState == newState) { return YES; } // 检查状态转换是否合法 if (![self canTransitionToState:newState]) { NSLog(@"[StateMachine] 无效的状态转换: %@ -> %@", [self stateDescription:self.currentState], [self stateDescription:newState]); if ([self.delegate respondsToSelector:@selector(stateMachine:failedTransitionFromState:toState:)]) { [self.delegate stateMachine:self failedTransitionFromState:self.currentState toState:newState]; } return NO; } // 询问代理是否允许状态转换 if ([self.delegate respondsToSelector:@selector(stateMachine:shouldTransitionFromState:toState:)]) { BOOL shouldTransition = [self.delegate stateMachine:self shouldTransitionFromState:self.currentState toState:newState]; if (!shouldTransition) { return NO; } } TJPViewControllerState oldState = self.currentState; self.currentState = newState; NSLog(@"[StateMachine] 状态转换: %@ -> %@", [self stateDescription:oldState], [self stateDescription:newState]); // 通知delegate状态已转换 if ([self.delegate respondsToSelector:@selector(stateMachine:didTransitionFromState:toState:)]) { [self.delegate stateMachine:self didTransitionFromState:oldState toState:newState]; } return YES; } /// 重置到空闲状态 - (void)resetToIdleState { [self transitionToState:TJPViewControllerStateIdle]; } /// 检查状态转换是否合法 - (BOOL)canTransitionToState:(TJPViewControllerState)newState { NSArray *allowedStates = self.stateTransitionRules[@(self.currentState)]; return [allowedStates containsObject:@(newState)]; } /// 获取状态描述 - (NSString *)stateDescription:(TJPViewControllerState)state { switch (state) { case TJPViewControllerStateIdle: return @"Idle"; case TJPViewControllerStateInitialLoading: return @"InitialLoading"; case TJPViewControllerStateContent: return @"Content"; case TJPViewControllerStateRefreshing: return @"Refreshing"; case TJPViewControllerStateLoadingMore: return @"LoadingMore"; case TJPViewControllerStateEmpty: return @"Empty"; case TJPViewControllerStateError: return @"Error"; default: return @"Unknown"; } } #pragma mark - Private Method - (void)setupStateTransitionRules { self.stateTransitionRules = @{ @(TJPViewControllerStateIdle): @[ @(TJPViewControllerStateInitialLoading), @(TJPViewControllerStateError) ], @(TJPViewControllerStateInitialLoading): @[ @(TJPViewControllerStateContent), @(TJPViewControllerStateEmpty), @(TJPViewControllerStateError), @(TJPViewControllerStateIdle) ], @(TJPViewControllerStateContent): @[ @(TJPViewControllerStateRefreshing), @(TJPViewControllerStateLoadingMore), @(TJPViewControllerStateError), @(TJPViewControllerStateEmpty) ], @(TJPViewControllerStateRefreshing): @[ @(TJPViewControllerStateContent), @(TJPViewControllerStateEmpty), @(TJPViewControllerStateError) ], @(TJPViewControllerStateLoadingMore): @[ @(TJPViewControllerStateContent), @(TJPViewControllerStateError) ], @(TJPViewControllerStateEmpty): @[ @(TJPViewControllerStateInitialLoading), @(TJPViewControllerStateRefreshing), @(TJPViewControllerStateContent), @(TJPViewControllerStateError) ], @(TJPViewControllerStateError): @[ @(TJPViewControllerStateInitialLoading), @(TJPViewControllerStateRefreshing), @(TJPViewControllerStateContent), @(TJPViewControllerStateEmpty), @(TJPViewControllerStateIdle) ] }; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/View/TJPViperBaseTableViewController.h ================================================ // // TJPViperBaseTableViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // /** * 基础TableView视图控制器 * * 职责: * - UI管理和视图生命周期 * - 数据协调和分页控制 * - 缓存管理 * - 错误处理 * * 抽离的组件: * - 状态机:TJPViewControllerStateMachine * - 可插拔组件:缓存、错误处理、刷新控件 */ #import #import "TJPViperBaseViewControllerProtocol.h" #import "TJPBaseTableView.h" NS_ASSUME_NONNULL_BEGIN @protocol TJPViperBasePresenterProtocol; @class TJPViewControllerStateMachine; @interface TJPViperBaseTableViewController : UIViewController // 核心组件 @property (nonatomic, strong) TJPBaseTableView *tableView; //vc->强引用presenter @property (nonatomic, strong) id basePresenter; // 状态管理 @property (nonatomic, strong, readonly) TJPViewControllerStateMachine *stateMachine; /// 是否启用下拉刷新 @property (nonatomic, assign) BOOL shouldEnablePullDownRefresh; /// 是否启用上拉加载更多 @property (nonatomic, assign) BOOL shouldEnablePullUpRefresh; /// 重复请求管理 @property (nonatomic, assign) BOOL shouldPreventDuplicateRequests; // 子类可重写的方法 - (void)setupTableViewStyle; - (void)configureInitialState; - (NSString *)cacheKeyForPage:(NSInteger)page; - (NSString *)requestKeyForPage:(NSInteger)page; /// 额外操作TableViewUI方法 - (void)updateTableViewUIForExtensionOperate; /// 配置刷新控件 - (void)configureRefreshControls; // 数据操作方法 - (void)reloadData; - (void)loadDataForPage:(NSInteger)page; - (void)refreshData; - (void)loadMoreData; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/View/TJPViperBaseTableViewController.m ================================================ // // TJPViperBaseTableViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import "TJPViperBaseTableViewController.h" #import #import "TJPToast.h" #import "TJPViewControllerStateMachine.h" #import "TJPViperBasePresenterProtocol.h" #import "TJPNetworkDefine.h" #import "TJPViperDefaultErrorHandler.h" @interface TJPViperBaseTableViewController () // 状态管理 @property (nonatomic, strong) TJPViewControllerStateMachine *stateMachine; // 错误处理器 @property (nonatomic, strong) id errorHandler; // 数据管理 @property (nonatomic, strong) NSMutableArray *dataArray; // 请求管理 @property (nonatomic, strong) NSMutableSet *activeRequests; // 生命周期标记 @property (nonatomic, assign) BOOL hasAppeared; @property (nonatomic, assign) BOOL isInitialized; @end @implementation TJPViperBaseTableViewController #pragma mark - #pragma mark Object Constructors //************************************************** // Constructors - (instancetype)init { self = [super init]; if (self) { [self commonInit]; } return self; } - (void)dealloc { TJPLogDealloc(); } //************************************************** #pragma mark - #pragma mark ViewLifeCycle //************************************************** // ViewLifeCycle Methods //************************************************** - (void)viewDidLoad { [super viewDidLoad]; if (@available(iOS 11.0, *)) { self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever; } // 配置初始状态 [self configureInitialState]; // 初始化UI [self initializationUI]; // 初始化数据 [self triggerInitialDataLoad]; self.isInitialized = YES; TJPLOG_INFO(@"[TJPViperBaseTableViewController] viewDidLoad 方法执行完成"); } - (void)viewDidAppear:(BOOL)animated { [super viewDidAppear:animated]; [self configureRefreshControls]; } - (void)viewDidDisappear:(BOOL)animated { [super viewDidDisappear:animated]; // 取消当前页面的所有请求 [self.activeRequests removeAllObjects]; } //************************************************** #pragma mark - #pragma mark Private Methods //************************************************** // Private Methods - (void)commonInit { // 初始化状态机 _stateMachine = [[TJPViewControllerStateMachine alloc] initWithInitialState:TJPViewControllerStateIdle]; _stateMachine.delegate = self; _dataArray = [NSMutableArray array]; _activeRequests = [NSMutableSet set]; // 默认配置 _shouldEnablePullDownRefresh = YES; _shouldEnablePullUpRefresh = YES; _shouldPreventDuplicateRequests = YES; // 初始化错误处理 _errorHandler = [TJPViperDefaultErrorHandler sharedHandler]; // _errorHandler.delegate = self; } - (void)configureInitialState { self.view.backgroundColor = [UIColor whiteColor]; // 子类可重写此方法进行特定配置 } - (void)initializationUI { self.tableView = [[TJPBaseTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain]; self.tableView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone; self.tableView.tjpBaseTableViewDelegate = self; [self.view addSubview:self.tableView]; [self setupTableViewStyle]; [self layOutTableView]; } - (void)setupTableViewStyle { // 子类可重写此方法自定义TableView样式 } - (void)layOutTableView { UIEdgeInsets insets = UIEdgeInsetsMake(0, 0, 0, 0); [self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.view); make.top.equalTo(self.mas_topLayoutGuideBottom).offset(insets.top); make.bottom.equalTo(self.view).offset(insets.bottom); }]; } - (void)triggerInitialDataLoad { if (!self.basePresenter) { TJPLOG_ERROR(@"basePresenter 为空,无法加载数据!请检查!"); return; } // 绑定Interactor透传的跳转信号 [self bindInteractorSignals]; // 下拉刷新 [self pullDownRefresh]; // 请求第一页数据 [self loadDataForPage:1]; } - (void)bindInteractorSignals { [self.basePresenter bindInteractorToPageSubjectWithContextProvider:self]; // 绑定数据更新信号 [self.basePresenter bindInteractorDataUpdateSubject]; // throttle防抖动处理 @weakify(self) [[[[[self.basePresenter viewUpdatedDataSignal] takeUntil:self.rac_willDeallocSignal] throttle:0.3] deliverOnMainThread] subscribeNext:^(NSDictionary * _Nullable updateDict) { TJPLOG_INFO(@"[TJPViperBaseTableViewController] VC层收到Interactor透传过来的数据源更新信号"); @strongify(self) if (updateDict && self.isInitialized) { dispatch_async(dispatch_get_main_queue(), ^{ [self childVCUpdateDatasource:updateDict]; }); } }]; } #pragma mark - TJPViperBaseViewControllerProtocol - (UIViewController *)currentViewController { return self; } #pragma mark - State Management - (TJPViewControllerState)currentState { return self.stateMachine.currentState; } - (BOOL)transitionToState:(TJPViewControllerState)newState { return [self.stateMachine transitionToState:newState]; } - (void)resetToIdleState { [self.stateMachine resetToIdleState]; } #pragma mark - TJPViewControllerStateMachineDelegate - (void)stateMachine:(TJPViewControllerStateMachine *)stateMachine didTransitionFromState:(TJPViewControllerState)fromState toState:(TJPViewControllerState)toState { // 处理状态转换逻辑 [self handleStateTransition:fromState toState:toState]; // 更新UI [self updateUIForState:toState withData:self.dataArray]; } - (BOOL)stateMachine:(TJPViewControllerStateMachine *)stateMachine shouldTransitionFromState:(TJPViewControllerState)fromState toState:(TJPViewControllerState)toState { // 可以在这里添加状态转换的前置条件检查 return YES; } - (void)stateMachine:(TJPViewControllerStateMachine *)stateMachine failedTransitionFromState:(TJPViewControllerState)fromState toState:(TJPViewControllerState)toState { TJPLOG_WARN(@"状态转换失败: %@ -> %@", [stateMachine stateDescription:fromState], [stateMachine stateDescription:toState]); } - (void)handleStateTransition:(TJPViewControllerState)fromState toState:(TJPViewControllerState)toState { // 子类可重写此方法处理特定的状态转换逻辑 } - (void)updateUIForState:(TJPViewControllerState)state withData:(nullable id)data { dispatch_async(dispatch_get_main_queue(), ^{ switch (state) { case TJPViewControllerStateInitialLoading: [self showInitialLoadingState]; break; case TJPViewControllerStateContent: [self showContentState:data]; break; case TJPViewControllerStateRefreshing: // 刷新状态下不需要额外UI更新,刷新控件会自动显示 break; case TJPViewControllerStateLoadingMore: // 加载更多状态下不需要额外UI更新 break; case TJPViewControllerStateEmpty: [self showEmptyState]; break; case TJPViewControllerStateError: [self showErrorState:data]; break; default: break; } }); } #pragma mark - Data Management - (void)reloadData { [self resetToIdleState]; [self loadDataForPage:1]; } - (void)loadDataForPage:(NSInteger)page { // 检查是否应该阻止重复请求 NSNumber *pageKey = @(page); if (self.shouldPreventDuplicateRequests && [self.activeRequests containsObject:pageKey]) { TJPLOG_INFO(@"第 %ld 页的请求已经在进行中", (long)page); return; } // 更新状态 if (page == 1) { if (self.currentState == TJPViewControllerStateContent) { [self.stateMachine transitionToState:TJPViewControllerStateRefreshing]; } else { [self.stateMachine transitionToState:TJPViewControllerStateInitialLoading]; } } else { [self.stateMachine transitionToState:TJPViewControllerStateLoadingMore]; } // 标记请求开始 [self.activeRequests addObject:pageKey]; // 执行数据请求 [self fetchDataForPage:page]; } - (void)refreshData { [self loadDataForPage:1]; } - (void)loadMoreData { // 通过Presenter查询分页状态 if (![self.basePresenter canLoadNextPage]) { [self.tableView endRefreshing]; [self.tableView noMoreData]; return; } // 让Presenter决定加载哪一页 NSInteger nextPage = [self.basePresenter getNextPageNumber]; [self loadDataForPage:nextPage]; } - (void)fetchDataForPage:(NSInteger)page { NSDate *startTime = [NSDate date]; NSNumber *pageKey = @(page); TJPLOG_INFO(@"正在请求第 %ld 页的数据", (long)page); @weakify(self) [self.basePresenter fetchInteractorDataForPage:page success:^(NSArray *data, NSInteger totalPage) { @strongify(self) NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:startTime]; TJPLOG_INFO(@"第 %ld 页数据请求成功(%.2fs)", (long)page, duration); // 移除请求标记 [self.activeRequests removeObject:pageKey]; // 处理成功状态下的数据 [self handleDataFetchSuccess:data totalPage:totalPage forPage:page]; } failure:^(NSError *error) { @strongify(self) NSTimeInterval duration = [[NSDate date] timeIntervalSinceDate:startTime]; TJPLOG_ERROR(@"第 %ld 页数据请求失败(%.2fs): %@", (long)page, duration, error.localizedDescription); // 移除请求标记 [self.activeRequests removeObject:pageKey]; // 处理失败状态下的数据 [self handleDataFetchError:error forPage:page]; }]; } - (void)handleDataFetchSuccess:(NSArray *)data totalPage:(NSInteger)totalPage forPage:(NSInteger)requestPage { // 重置数据的条件:第一页或刷新状态 if (requestPage == 1 || self.currentState == TJPViewControllerStateRefreshing) { [self.dataArray removeAllObjects]; TJPLOG_DEBUG(@"重置数据"); } if (data.count > 0) { [self.dataArray addObjectsFromArray:data]; } // 更新状态 if (self.dataArray.count == 0) { [self.stateMachine transitionToState:TJPViewControllerStateEmpty]; } else { [self.stateMachine transitionToState:TJPViewControllerStateContent]; } [self.tableView endRefreshing]; } - (void)updatePaginationUI { BOOL hasMore = [self.basePresenter hasMoreData]; NSInteger currentPage = [self.basePresenter getCurrentPage]; if (!hasMore) { [self.tableView noMoreData]; TJPLOG_INFO(@"已加载全部数据,当前页: %ld", (long)currentPage); } else { [self.tableView resetNoMoreData]; } } //- (void)handleDataFetchSuccess:(NSArray *)data totalPage:(NSInteger)totalPage { // [self handleDataFetchSuccess:data totalPage:totalPage forPage:self.requestingPage]; //} - (void)handleDataFetchError:(NSError *)error forPage:(NSInteger)page { @weakify(self) [self.errorHandler handleError:error inContext:self completion:^(BOOL shouldRetry) { @strongify(self) if (shouldRetry) { [self fetchDataForPage:page]; } else { // 更新状态为错误 [self.stateMachine transitionToState:TJPViewControllerStateError]; [self.tableView endRefreshing]; } }]; } #pragma mark - UI State Methods - (void)showInitialLoadingState { } - (void)showContentState:(NSArray *)data { [self.tableView hideEmptyData]; [self.tableView reloadDataWithSectionModels:data]; } - (void)showEmptyState { [self.tableView showEmptyData]; } - (void)showErrorState:(NSError *)error { // 显示错误专用的空状态页 [self.tableView showEmptyData]; } //************************************************** - (void)configureRefreshControls { // 配置下拉刷新 if (self.shouldEnablePullDownRefresh) { [self.tableView configurePullDownRefreshControlWithTarget:self pullDownAction:@selector(pullDownRefresh)]; } // 配置上拉加载更多 if (self.shouldEnablePullUpRefresh) { [self.tableView configurePullUpRefreshControlWithTarget:self pullUpAction:@selector(pullUpLoadMore)]; } } #pragma mark - Pull to Refresh - (void)pullDownRefresh { [self refreshData]; } - (void)pullUpLoadMore { [self loadMoreData]; } #pragma mark - Helper Methods - (NSString *)cacheKeyForPage:(NSInteger)page { return [NSString stringWithFormat:@"%@_page_%ld", NSStringFromClass([self class]), (long)page]; } - (NSString *)requestKeyForPage:(NSInteger)page { return [NSString stringWithFormat:@"%@_request_%ld", NSStringFromClass([self class]), (long)page]; } - (void)updateTableViewUIForExtensionOperate { //交给子类去实现 } - (void)handleDataUpdate:(NSDictionary *)updateDict { // 子类可重写此方法处理特定的数据更新逻辑 TJPLOG_INFO(@"接收到数据更新: %@", updateDict); } - (void)handleDataFetchSuccess:(NSArray *)data error:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ [self updateEmptyDataViewForSectionModels:data error:nil]; [self.tableView endRefreshing]; }); } - (void)handleDataFetchError:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ NSString *errorMessage = [self getErrorMessageForError:error]; [self showError:errorMessage]; [self updateEmptyDataViewForSectionModels:nil error:error]; [self.tableView endRefreshing]; }); } - (void)updateEmptyDataViewForSectionModels:(NSArray *)sections error:(NSError *)error { if (error || sections.count == 0) { [self.tableView showEmptyData]; } else { [self.tableView hideEmptyData]; [self.tableView reloadDataWithSectionModels:sections]; } //对tableView进行额外扩展操作 [self updateTableViewUIForExtensionOperate]; } - (NSString *)getErrorMessageForError:(NSError *)error { if (error.code == NSURLErrorNotConnectedToInternet) { return @"网络连接失败,请检查您的网络设置"; } else if (error.code == NSURLErrorTimedOut) { return @"请求超时,请稍后再试"; } else { return error.localizedDescription ?: @"加载失败,请重试"; } } #pragma mark - #pragma mark Self Public Methods //************************************************** // Self Public Methods - (void)showError:(nonnull NSString *)error { [TJPToast show:error duration:1.0]; } - (void)tjpEmptyViewDidTapped:(UIView *)view { [self reloadData]; } //************************************************** #pragma mark - #pragma mark Override Public Methods //************************************************** // Override Public Methods //************************************************** #pragma mark - #pragma mark Override Private Methods //************************************************** // Override Public Methods - (void)childVCUpdateDatasource:(NSDictionary *)updateDict { //交给子类去重写 } //************************************************** #pragma mark - #pragma mark Properties Getter & Setter //************************************************** // Properties //************************************************** @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Architecture/View/TJPViperBaseViewControllerProtocol.h ================================================ // // TJPViperBaseViewControllerProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import NS_ASSUME_NONNULL_BEGIN @protocol TJPViperBaseViewControllerProtocol @required - (UIViewController *)currentViewController; @optional - (void)showError:(NSString *)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPAdCell.h ================================================ // // TJPAdCell.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseTableViewCell.h" #import "TJPAdCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPAdCell : TJPBaseTableViewCell @property (nonatomic, weak) TJPAdCellModel *cellModel; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPAdCell.m ================================================ // // TJPAdCell.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPAdCell.h" #import #import @interface TJPAdCell () @property (nonatomic, strong) UIView *containerView; @property (nonatomic, strong) UIImageView *adImageView; @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UILabel *subtitleLabel; @property (nonatomic, strong) UIButton *actionButton; @property (nonatomic, strong) UILabel *adTagLabel; // 翻转状态管理 @property (nonatomic, assign) BOOL isFlipped; @property (nonatomic, strong) NSTimer *flipTimer; @property (nonatomic, assign) BOOL hasFlipData; @end @implementation TJPAdCell @synthesize cellModel = _cellModel; - (void)prepareForReuse { [super prepareForReuse]; [self stopAnimation]; [self resetState]; [self clearContent]; } - (void)dealloc { [self stopAnimation]; } - (void)awakeFromNib { [super awakeFromNib]; // Initialization code } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self setupUI]; [self resetState]; } return self; } - (void)setupUI { self.selectionStyle = UITableViewCellSelectionStyleNone; self.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.0]; // 容器视图 self.containerView = [[UIView alloc] init]; self.containerView.backgroundColor = [UIColor whiteColor]; self.containerView.layer.cornerRadius = 8; self.containerView.layer.shadowColor = [UIColor blackColor].CGColor; self.containerView.layer.shadowOffset = CGSizeMake(0, 2); self.containerView.layer.shadowRadius = 4; self.containerView.layer.shadowOpacity = 0.1; [self.contentView addSubview:self.containerView]; // 广告标识 self.adTagLabel = [[UILabel alloc] init]; self.adTagLabel.text = @"广告"; self.adTagLabel.font = [UIFont systemFontOfSize:10]; self.adTagLabel.textColor = [UIColor whiteColor]; self.adTagLabel.backgroundColor = [UIColor systemBlueColor]; self.adTagLabel.textAlignment = NSTextAlignmentCenter; self.adTagLabel.layer.cornerRadius = 2; self.adTagLabel.clipsToBounds = YES; [self.containerView addSubview:self.adTagLabel]; // 广告图片 self.adImageView = [[UIImageView alloc] init]; self.adImageView.contentMode = UIViewContentModeScaleAspectFill; self.adImageView.clipsToBounds = YES; self.adImageView.layer.cornerRadius = 6; self.adImageView.backgroundColor = [UIColor lightGrayColor]; [self.containerView addSubview:self.adImageView]; // 标题 self.titleLabel = [[UILabel alloc] init]; self.titleLabel.font = [UIFont boldSystemFontOfSize:16]; self.titleLabel.textColor = [UIColor blackColor]; self.titleLabel.numberOfLines = 1; [self.containerView addSubview:self.titleLabel]; // 副标题 self.subtitleLabel = [[UILabel alloc] init]; self.subtitleLabel.font = [UIFont systemFontOfSize:14]; self.subtitleLabel.textColor = [UIColor grayColor]; self.subtitleLabel.numberOfLines = 1; [self.containerView addSubview:self.subtitleLabel]; // 行动按钮 self.actionButton = [[UIButton alloc] init]; self.actionButton.titleLabel.font = [UIFont boldSystemFontOfSize:14]; [self.actionButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; self.actionButton.backgroundColor = [UIColor systemBlueColor]; self.actionButton.layer.cornerRadius = 15; [self.containerView addSubview:self.actionButton]; [self setupConstraints]; } - (void)setupConstraints { [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.contentView).insets(UIEdgeInsetsMake(0, 15, 0, 15)); make.top.bottom.equalTo(self.contentView).insets(UIEdgeInsetsMake(8, 0, 8, 0)); }]; [self.adTagLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.top.equalTo(self.containerView).insets(UIEdgeInsetsMake(8, 8, 0, 0)); make.width.equalTo(@30); make.height.equalTo(@16); }]; [self.adImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.containerView).offset(12); make.top.equalTo(self.adTagLabel.mas_bottom).offset(8); make.bottom.equalTo(self.containerView).offset(-12); make.width.equalTo(@60); }]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.adImageView.mas_right).offset(12); make.top.equalTo(self.adImageView); make.right.lessThanOrEqualTo(self.actionButton.mas_left).offset(-8); }]; [self.subtitleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.titleLabel); make.top.equalTo(self.titleLabel.mas_bottom).offset(4); }]; [self.actionButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.containerView).offset(-12); make.centerY.equalTo(self.containerView); make.width.equalTo(@80); make.height.equalTo(@30); }]; } - (void)configureWithModel:(id)cellModel { [super configureWithModel:cellModel]; // 停止之前的动画 [self stopAnimation]; // 检查是否有翻转数据 self.hasFlipData = [self checkFlipContentAvailable]; // 显示原始内容 [self showOriginalContent]; // 开始动画(如果有翻转数据) if (self.hasFlipData) { NSLog(@"广告 '%@' 启用翻转动画", self.cellModel.title); [self startAnimationAfterDelay]; } else { NSLog(@"广告 '%@' 无翻转内容,显示静态内容", self.cellModel.title); } } #pragma mark - Private Method - (void)resetState { self.isFlipped = NO; self.hasFlipData = NO; self.cellModel = nil; } - (void)clearContent { self.titleLabel.text = nil; self.subtitleLabel.text = nil; [self.actionButton setTitle:nil forState:UIControlStateNormal]; self.adImageView.image = nil; [self resetStyles]; } - (void)resetStyles { self.actionButton.backgroundColor = [UIColor systemBlueColor]; self.containerView.backgroundColor = [UIColor whiteColor]; self.containerView.layer.borderWidth = 0; } - (BOOL)checkFlipContentAvailable { TJPAdFlipContentModel *flipContent = self.cellModel.flipContent; if (!flipContent) { return NO; } // 检查是否至少有一个有效的翻转内容 BOOL hasValidContent = (flipContent.title && flipContent.title.length > 0) || (flipContent.subtitle && flipContent.subtitle.length > 0) || (flipContent.actionText && flipContent.actionText.length > 0) || (flipContent.imageUrl && flipContent.imageUrl.length > 0); NSLog(@"flipContent检查: title=%@, subtitle=%@, actionText=%@, imageUrl=%@", flipContent.title, flipContent.subtitle, flipContent.actionText, flipContent.imageUrl); return hasValidContent; } #pragma mark - 显示内容 - (void)showOriginalContent { self.titleLabel.text = self.cellModel.title; self.subtitleLabel.text = self.cellModel.subtitle; [self.actionButton setTitle:self.cellModel.actionText forState:UIControlStateNormal]; if (self.cellModel.imageUrl && self.cellModel.imageUrl.length > 0) { [self.adImageView sd_setImageWithURL:[NSURL URLWithString:self.cellModel.imageUrl]]; } [self resetStyles]; } - (void)showFlippedContent { TJPAdFlipContentModel *flipContent = self.cellModel.flipContent; if (!flipContent) return; // 只更新有内容的字段,没有的保持原样 if (flipContent.title && flipContent.title.length > 0) { self.titleLabel.text = flipContent.title; } if (flipContent.subtitle && flipContent.subtitle.length > 0) { self.subtitleLabel.text = flipContent.subtitle; } if (flipContent.actionText && flipContent.actionText.length > 0) { [self.actionButton setTitle:flipContent.actionText forState:UIControlStateNormal]; } if (flipContent.imageUrl && flipContent.imageUrl.length > 0) { [self.adImageView sd_setImageWithURL:[NSURL URLWithString:flipContent.imageUrl]]; } // 应用翻转样式 [self applyFlippedStyles]; } - (void)applyFlippedStyles { // 使用橙色作为翻转强调色 UIColor *highlightColor = [UIColor systemOrangeColor]; self.actionButton.backgroundColor = highlightColor; // 为容器添加轻微的强调色边框 self.containerView.layer.borderWidth = 1.0; self.containerView.layer.borderColor = [highlightColor colorWithAlphaComponent:0.3].CGColor; } #pragma mark - 动画控制 - (void)startAnimationAfterDelay { // 延迟2秒开始,避免在滑动时启动 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 双重检查:确保cell还在屏幕上且有翻转数据 if (self.window && self.hasFlipData && self.superview) { [self startAnimation]; } }); } - (void)startAnimation { [self stopAnimation]; // 防重复 NSLog(@"开始翻转动画定时器"); // 每4秒翻转一次 self.flipTimer = [NSTimer scheduledTimerWithTimeInterval:4.0 target:self selector:@selector(performFlip) userInfo:nil repeats:YES]; // 立即执行第一次翻转 [self performFlip]; } - (void)stopAnimation { if (self.flipTimer) { [self.flipTimer invalidate]; self.flipTimer = nil; } } - (void)performFlip { // 更严格的检查:确保cell真的还在屏幕上 if (!self.window || !self.superview) { [self stopAnimation]; return; } // 检查cell是否真正可见(在父视图的bounds内) if (![self isCellVisible]) { NSLog(@"Cell不可见 停止动画定时器"); [self stopAnimation]; return; } [UIView transitionWithView:self.containerView duration:0.8 options:UIViewAnimationOptionTransitionFlipFromLeft | UIViewAnimationOptionAllowUserInteraction animations:^{ if (self.isFlipped) { [self showOriginalContent]; } else { [self showFlippedContent]; } } completion:^(BOOL finished) { if (finished) { self.isFlipped = !self.isFlipped; } }]; } - (BOOL)isCellVisible { if (!self.superview) return NO; // 获取cell在superview中的frame CGRect cellFrame = [self.superview convertRect:self.frame toView:self.superview]; CGRect superviewBounds = self.superview.bounds; // 检查是否有交集(即cell是否在可见区域内) return CGRectIntersectsRect(cellFrame, superviewBounds); } #pragma mark - Cell生命周期 - (void)willMoveToWindow:(UIWindow *)newWindow { [super willMoveToWindow:newWindow]; if (!newWindow) { // Cell即将离开window,立即停止动画 [self stopAnimation]; } } // 监听cell从父视图移除 - (void)willMoveToSuperview:(UIView *)newSuperview { [super willMoveToSuperview:newSuperview]; if (!newSuperview) { // Cell即将从父视图移除,立即停止动画 [self stopAnimation]; } } // 当cell的frame发生变化时也检查可见性 - (void)layoutSubviews { [super layoutSubviews]; // 如果有动画在运行,检查cell是否还可见 if (self.flipTimer && ![self isCellVisible]) { [self stopAnimation]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPImageCell.h ================================================ // // TJPImageCell.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseTableViewCell.h" #import "TJPImageCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPImageCell : TJPBaseTableViewCell @property (nonatomic, weak) TJPImageCellModel *cellModel; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPImageCell.m ================================================ // // TJPImageCell.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPImageCell.h" #import #import @interface TJPImageCell () @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UIScrollView *imageScrollView; @property (nonatomic, strong) UIStackView *imageStackView; @property (nonatomic, strong) UILabel *descriptionLabel; @property (nonatomic, strong) UIButton *likeButton; @property (nonatomic, strong) UIButton *commentButton; @end @implementation TJPImageCell @synthesize cellModel = _cellModel; - (void)awakeFromNib { [super awakeFromNib]; // Initialization code } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self setupUI]; } return self; } - (void)setupUI { self.selectionStyle = UITableViewCellSelectionStyleNone; // 标题 self.titleLabel = [[UILabel alloc] init]; self.titleLabel.font = [UIFont boldSystemFontOfSize:16]; self.titleLabel.textColor = [UIColor blackColor]; [self.contentView addSubview:self.titleLabel]; // 图片滚动视图 self.imageScrollView = [[UIScrollView alloc] init]; self.imageScrollView.showsHorizontalScrollIndicator = NO; [self.contentView addSubview:self.imageScrollView]; // 图片堆栈视图 self.imageStackView = [[UIStackView alloc] init]; self.imageStackView.axis = UILayoutConstraintAxisHorizontal; self.imageStackView.spacing = 8; [self.imageScrollView addSubview:self.imageStackView]; // 描述 self.descriptionLabel = [[UILabel alloc] init]; self.descriptionLabel.font = [UIFont systemFontOfSize:14]; self.descriptionLabel.textColor = [UIColor darkGrayColor]; // self.descriptionLabel.numberOfLines = 1; [self.contentView addSubview:self.descriptionLabel]; // 点赞按钮 self.likeButton = [[UIButton alloc] init]; [self.likeButton setImage:[UIImage systemImageNamed:@"heart"] forState:UIControlStateNormal]; [self.likeButton setTitleColor:[UIColor grayColor] forState:UIControlStateNormal]; self.likeButton.titleLabel.font = [UIFont systemFontOfSize:12]; [self.contentView addSubview:self.likeButton]; // 评论按钮 self.commentButton = [[UIButton alloc] init]; [self.commentButton setImage:[UIImage systemImageNamed:@"message"] forState:UIControlStateNormal]; [self.commentButton setTitleColor:[UIColor grayColor] forState:UIControlStateNormal]; self.commentButton.titleLabel.font = [UIFont systemFontOfSize:12]; [self.contentView addSubview:self.commentButton]; [self setupConstraints]; } - (void)setupConstraints { [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.contentView).insets(UIEdgeInsetsMake(0, 15, 0, 15)); make.top.equalTo(self.contentView).offset(12); }]; [self.imageScrollView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.contentView); make.top.equalTo(self.titleLabel.mas_bottom).offset(8); make.height.equalTo(@180); }]; [self.imageStackView mas_makeConstraints:^(MASConstraintMaker *make) { // make.edges.equalTo(self.imageScrollView); make.left.right.equalTo(self.imageScrollView).insets(UIEdgeInsetsMake(0, 15, 0, 15)); make.height.equalTo(self.imageScrollView); }]; [self.descriptionLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.titleLabel); make.top.equalTo(self.imageScrollView.mas_bottom).offset(8); }]; [self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.titleLabel); make.top.equalTo(self.descriptionLabel.mas_bottom).offset(8); make.width.equalTo(@60); }]; [self.commentButton mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.likeButton.mas_right).offset(20); make.centerY.equalTo(self.likeButton); make.width.equalTo(@60); }]; } - (void)configureWithModel:(id)cellModel { [super configureWithModel:cellModel]; self.titleLabel.text = self.cellModel.title; self.descriptionLabel.text = self.cellModel.imageDescription; [self.likeButton setTitle:[NSString stringWithFormat:@" %ld", self.cellModel.likes] forState:UIControlStateNormal]; [self.commentButton setTitle:[NSString stringWithFormat:@" %ld", self.cellModel.comments] forState:UIControlStateNormal]; // 清除之前的图片视图 for (UIView *subview in self.imageStackView.arrangedSubviews) { [self.imageStackView removeArrangedSubview:subview]; [subview removeFromSuperview]; } // 添加新的图片视图 for (NSString *imageUrl in self.cellModel.imageUrls) { UIImageView *imageView = [[UIImageView alloc] init]; imageView.contentMode = UIViewContentModeScaleAspectFill; imageView.clipsToBounds = YES; imageView.layer.cornerRadius = 4; imageView.backgroundColor = [UIColor lightGrayColor]; [imageView mas_makeConstraints:^(MASConstraintMaker *make) { make.width.equalTo(@150); }]; [self.imageStackView addArrangedSubview:imageView]; [imageView sd_setImageWithURL:[NSURL URLWithString:imageUrl]]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPNewsCell.h ================================================ // // TJPNewsCell.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseTableViewCell.h" #import "TJPNewsCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPNewsCell : TJPBaseTableViewCell @property (nonatomic, weak) TJPNewsCellModel *cellModel; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPNewsCell.m ================================================ // // TJPNewsCell.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPNewsCell.h" #import #import @interface TJPNewsCell () @property (nonatomic, strong) UIImageView *newsImageView; @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UILabel *summaryLabel; @property (nonatomic, strong) UILabel *sourceLabel; @property (nonatomic, strong) UILabel *timeLabel; @property (nonatomic, strong) UILabel *readCountLabel; @end @implementation TJPNewsCell @synthesize cellModel = _cellModel; - (void)awakeFromNib { [super awakeFromNib]; // Initialization code } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self setupUI]; } return self; } - (void)setupUI { self.selectionStyle = UITableViewCellSelectionStyleNone; // 新闻图片 self.newsImageView = [[UIImageView alloc] init]; self.newsImageView.contentMode = UIViewContentModeScaleAspectFill; self.newsImageView.clipsToBounds = YES; self.newsImageView.layer.cornerRadius = 4; self.newsImageView.backgroundColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.newsImageView]; // 标题 self.titleLabel = [[UILabel alloc] init]; self.titleLabel.font = [UIFont boldSystemFontOfSize:16]; self.titleLabel.textColor = [UIColor blackColor]; self.titleLabel.numberOfLines = 2; [self.contentView addSubview:self.titleLabel]; // 摘要 self.summaryLabel = [[UILabel alloc] init]; self.summaryLabel.font = [UIFont systemFontOfSize:14]; self.summaryLabel.textColor = [UIColor grayColor]; self.summaryLabel.numberOfLines = 2; [self.contentView addSubview:self.summaryLabel]; // 来源 self.sourceLabel = [[UILabel alloc] init]; self.sourceLabel.font = [UIFont systemFontOfSize:12]; self.sourceLabel.textColor = [UIColor systemBlueColor]; [self.contentView addSubview:self.sourceLabel]; // 时间 self.timeLabel = [[UILabel alloc] init]; self.timeLabel.font = [UIFont systemFontOfSize:12]; self.timeLabel.textColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.timeLabel]; // 阅读数 self.readCountLabel = [[UILabel alloc] init]; self.readCountLabel.font = [UIFont systemFontOfSize:12]; self.readCountLabel.textColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.readCountLabel]; [self setupConstraints]; } - (void)setupConstraints { [self.newsImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(15); make.top.equalTo(self.contentView).offset(12); make.width.height.equalTo(@80); }]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.newsImageView.mas_right).offset(12); make.right.equalTo(self.contentView).offset(-15); make.top.equalTo(self.newsImageView); }]; [self.summaryLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.titleLabel); make.top.equalTo(self.titleLabel.mas_bottom).offset(8); }]; [self.sourceLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.titleLabel); make.top.equalTo(self.summaryLabel.mas_bottom).offset(8); }]; [self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.sourceLabel.mas_right).offset(10); make.centerY.equalTo(self.sourceLabel); }]; [self.readCountLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.titleLabel); make.centerY.equalTo(self.sourceLabel); }]; } - (void)configureWithModel:(id)cellModel { [super configureWithModel:cellModel]; self.titleLabel.text = self.cellModel.title; self.summaryLabel.text = self.cellModel.summary; self.sourceLabel.text = self.cellModel.source; self.timeLabel.text = self.cellModel.publishTime; self.readCountLabel.text = [NSString stringWithFormat:@"%ld阅读", self.cellModel.readCount]; [self.newsImageView sd_setImageWithURL:[NSURL URLWithString:self.cellModel.imageUrl]]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPProductCell.h ================================================ // // TJPProductCell.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseTableViewCell.h" #import "TJPProductCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPProductCell : TJPBaseTableViewCell @property (nonatomic, weak) TJPProductCellModel *cellModel; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPProductCell.m ================================================ // // TJPProductCell.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPProductCell.h" #import #import @interface TJPProductCell () @property (nonatomic, strong) UIImageView *productImageView; @property (nonatomic, strong) UILabel *nameLabel; @property (nonatomic, strong) UILabel *priceLabel; @property (nonatomic, strong) UILabel *originalPriceLabel; @property (nonatomic, strong) UIStackView *ratingStackView; @property (nonatomic, strong) UILabel *salesLabel; @property (nonatomic, strong) UIStackView *tagStackView; @end @implementation TJPProductCell @synthesize cellModel = _cellModel; - (void)awakeFromNib { [super awakeFromNib]; // Initialization code } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self setupUI]; } return self; } - (void)setupUI { self.selectionStyle = UITableViewCellSelectionStyleNone; // 商品图片 self.productImageView = [[UIImageView alloc] init]; self.productImageView.contentMode = UIViewContentModeScaleAspectFill; self.productImageView.clipsToBounds = YES; self.productImageView.layer.cornerRadius = 8; self.productImageView.backgroundColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.productImageView]; // 商品名称 self.nameLabel = [[UILabel alloc] init]; self.nameLabel.font = [UIFont boldSystemFontOfSize:16]; self.nameLabel.textColor = [UIColor blackColor]; self.nameLabel.numberOfLines = 2; [self.contentView addSubview:self.nameLabel]; // 价格 self.priceLabel = [[UILabel alloc] init]; self.priceLabel.font = [UIFont boldSystemFontOfSize:18]; self.priceLabel.textColor = [UIColor systemRedColor]; [self.contentView addSubview:self.priceLabel]; // 原价 self.originalPriceLabel = [[UILabel alloc] init]; self.originalPriceLabel.font = [UIFont systemFontOfSize:14]; self.originalPriceLabel.textColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.originalPriceLabel]; // 评分视图 self.ratingStackView = [[UIStackView alloc] init]; self.ratingStackView.axis = UILayoutConstraintAxisHorizontal; self.ratingStackView.spacing = 2; [self.contentView addSubview:self.ratingStackView]; // 销量 self.salesLabel = [[UILabel alloc] init]; self.salesLabel.font = [UIFont systemFontOfSize:12]; self.salesLabel.textColor = [UIColor grayColor]; [self.contentView addSubview:self.salesLabel]; // 标签视图 self.tagStackView = [[UIStackView alloc] init]; self.tagStackView.axis = UILayoutConstraintAxisHorizontal; self.tagStackView.spacing = 4; [self.contentView addSubview:self.tagStackView]; [self setupConstraints]; } - (void)setupConstraints { [self.productImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(15); make.top.equalTo(self.contentView).offset(12); make.width.height.equalTo(@100); }]; [self.nameLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.productImageView.mas_right).offset(12); make.right.equalTo(self.contentView).offset(-15); make.top.equalTo(self.productImageView); }]; [self.priceLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.nameLabel); make.top.equalTo(self.nameLabel.mas_bottom).offset(8); }]; [self.originalPriceLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.priceLabel.mas_right).offset(8); make.centerY.equalTo(self.priceLabel); }]; [self.ratingStackView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.nameLabel); make.top.equalTo(self.priceLabel.mas_bottom).offset(8); }]; [self.salesLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.ratingStackView.mas_right).offset(8); make.centerY.equalTo(self.ratingStackView); }]; [self.tagStackView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.nameLabel); make.top.equalTo(self.ratingStackView.mas_bottom).offset(8); make.bottom.equalTo(self.contentView).offset(-12); }]; } - (void)configureWithModel:(id)cellModel { [super configureWithModel:cellModel]; self.nameLabel.text = self.cellModel.name; self.priceLabel.text = [NSString stringWithFormat:@"¥%.2f", self.cellModel.price]; // 设置原价(删除线效果) NSString *originalPriceText = [NSString stringWithFormat:@"¥%.2f", self.cellModel.originalPrice]; NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:originalPriceText]; [attributedString addAttribute:NSStrikethroughStyleAttributeName value:@(NSUnderlineStyleSingle) range:NSMakeRange(0, originalPriceText.length)]; self.originalPriceLabel.attributedText = attributedString; self.salesLabel.text = [NSString stringWithFormat:@"已售%ld", self.cellModel.sales]; // 设置评分星星 [self setupRatingStars:self.cellModel.rating]; // 设置标签 [self setupTags:self.cellModel.tags]; [self.productImageView sd_setImageWithURL:[NSURL URLWithString:self.cellModel.imageUrl]]; } - (void)setupRatingStars:(CGFloat)rating { // 清除之前的星星 for (UIView *subview in self.ratingStackView.arrangedSubviews) { [self.ratingStackView removeArrangedSubview:subview]; [subview removeFromSuperview]; } // 添加星星 for (int i = 0; i < 5; i++) { UIImageView *star = [[UIImageView alloc] init]; star.contentMode = UIViewContentModeScaleAspectFit; [star mas_makeConstraints:^(MASConstraintMaker *make) { make.width.height.equalTo(@12); }]; if (i < floor(rating)) { star.image = [UIImage systemImageNamed:@"star.fill"]; star.tintColor = [UIColor systemYellowColor]; } else if (i < rating) { star.image = [UIImage systemImageNamed:@"star.lefthalf.fill"]; star.tintColor = [UIColor systemYellowColor]; } else { star.image = [UIImage systemImageNamed:@"star"]; star.tintColor = [UIColor lightGrayColor]; } [self.ratingStackView addArrangedSubview:star]; } // 添加评分数字 UILabel *ratingLabel = [[UILabel alloc] init]; ratingLabel.text = [NSString stringWithFormat:@"%.1f", rating]; ratingLabel.font = [UIFont systemFontOfSize:12]; ratingLabel.textColor = [UIColor grayColor]; [self.ratingStackView addArrangedSubview:ratingLabel]; } - (void)setupTags:(NSArray *)tags { // 清除之前的标签 for (UIView *subview in self.tagStackView.arrangedSubviews) { [self.tagStackView removeArrangedSubview:subview]; [subview removeFromSuperview]; } // 添加标签 for (NSString *tagText in tags) { UILabel *tagLabel = [[UILabel alloc] init]; tagLabel.text = [NSString stringWithFormat:@" %@ ", tagText]; tagLabel.font = [UIFont systemFontOfSize:10]; tagLabel.textColor = [UIColor systemRedColor]; tagLabel.backgroundColor = [UIColor colorWithRed:1.0 green:0.9 blue:0.9 alpha:1.0]; tagLabel.layer.cornerRadius = 2; tagLabel.clipsToBounds = YES; tagLabel.textAlignment = NSTextAlignmentCenter; [self.tagStackView addArrangedSubview:tagLabel]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPUserDynamicCell.h ================================================ // // TJPUserDynamicCell.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseTableViewCell.h" #import "TJPUserDynamicCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPUserDynamicCell : TJPBaseTableViewCell @property (nonatomic, weak) TJPUserDynamicCellModel *cellModel; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPUserDynamicCell.m ================================================ // // TJPUserDynamicCell.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPUserDynamicCell.h" #import #import #import "TJPNineGridImageView.h" #import "TJPLikeCommentAreaView.h" @interface TJPUserDynamicCell () @property (nonatomic, strong) UIImageView *avatarImageView; @property (nonatomic, strong) UILabel *userNameLabel; @property (nonatomic, strong) UILabel *timeLabel; @property (nonatomic, strong) UILabel *contentLabel; @property (nonatomic, strong) UIView *actionView; @property (nonatomic, strong) UIButton *likeButton; @property (nonatomic, strong) UIButton *commentButton; // 九宫格图片 @property (nonatomic, strong) TJPNineGridImageView *nineGridView; // 点赞评论区域 @property (nonatomic, strong) TJPLikeCommentAreaView *likeCommentArea; @end @implementation TJPUserDynamicCell @synthesize cellModel = _cellModel; - (void)dealloc { } - (void)prepareForReuse { [super prepareForReuse]; // 重置动态内容 self.nineGridView.imageUrls = nil; self.likeCommentArea.hidden = YES; } - (void)awakeFromNib { [super awakeFromNib]; // Initialization code } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self setupUI]; } return self; } - (void)setupUI { self.selectionStyle = UITableViewCellSelectionStyleNone; // 头像 self.avatarImageView = [[UIImageView alloc] init]; self.avatarImageView.contentMode = UIViewContentModeScaleAspectFill; self.avatarImageView.clipsToBounds = YES; self.avatarImageView.layer.cornerRadius = 20; self.avatarImageView.backgroundColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.avatarImageView]; // 用户名 self.userNameLabel = [[UILabel alloc] init]; self.userNameLabel.font = [UIFont boldSystemFontOfSize:15]; self.userNameLabel.textColor = [UIColor blackColor]; [self.contentView addSubview:self.userNameLabel]; // 时间 self.timeLabel = [[UILabel alloc] init]; self.timeLabel.font = [UIFont systemFontOfSize:12]; self.timeLabel.textColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.timeLabel]; // 内容 self.contentLabel = [[UILabel alloc] init]; self.contentLabel.font = [UIFont systemFontOfSize:14]; self.contentLabel.textColor = [UIColor darkGrayColor]; self.contentLabel.numberOfLines = 0; [self.contentView addSubview:self.contentLabel]; // 九宫格图片视图 self.nineGridView = [[TJPNineGridImageView alloc] init]; [self.contentView addSubview:self.nineGridView]; // 操作区域 self.actionView = [[UIView alloc] init]; [self.contentView addSubview:self.actionView]; // 点赞按钮 self.likeButton = [[UIButton alloc] init]; [self.likeButton setImage:[UIImage systemImageNamed:@"heart"] forState:UIControlStateNormal]; [self.likeButton setTitleColor:[UIColor grayColor] forState:UIControlStateNormal]; self.likeButton.titleLabel.font = [UIFont systemFontOfSize:12]; [self.actionView addSubview:self.likeButton]; // 评论按钮 self.commentButton = [[UIButton alloc] init]; [self.commentButton setImage:[UIImage systemImageNamed:@"message"] forState:UIControlStateNormal]; [self.commentButton setTitleColor:[UIColor grayColor] forState:UIControlStateNormal]; self.commentButton.titleLabel.font = [UIFont systemFontOfSize:12]; [self.actionView addSubview:self.commentButton]; // 点赞评论区域 self.likeCommentArea = [[TJPLikeCommentAreaView alloc] init]; [self.contentView addSubview:self.likeCommentArea]; [self setupConstraints]; } - (void)setupConstraints { [self.avatarImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(15); make.top.equalTo(self.contentView).offset(12); make.width.height.equalTo(@40); }]; [self.userNameLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.avatarImageView.mas_right).offset(10); make.top.equalTo(self.avatarImageView); make.right.lessThanOrEqualTo(self.contentView).offset(-15); make.height.equalTo(@20); }]; [self.timeLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.userNameLabel); make.top.equalTo(self.userNameLabel.mas_bottom).offset(5); make.height.equalTo(@15); }]; [self.contentLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.userNameLabel); make.right.equalTo(self.contentView).offset(-15); make.top.equalTo(self.timeLabel.mas_bottom).offset(8); }]; // 九宫格图片约束 [self.nineGridView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentLabel); make.top.equalTo(self.contentLabel.mas_bottom).offset(8); make.width.lessThanOrEqualTo(@255); // 3 * 80 + 2 * 5 = 255 make.height.equalTo(@0); // 默认高度0,动态设置 }]; [self.actionView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.contentLabel); make.top.equalTo(self.nineGridView.mas_bottom).offset(10); make.height.equalTo(@30); }]; [self.commentButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.actionView); make.centerY.equalTo(self.actionView); make.width.equalTo(@60); make.height.equalTo(@30); }]; [self.likeButton mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.commentButton.mas_left).offset(-20); make.centerY.equalTo(self.actionView); make.width.equalTo(@60); make.height.equalTo(@30); }]; // 点赞评论区域约束 [self.likeCommentArea mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentLabel); make.right.equalTo(self.contentView).offset(-15); make.top.equalTo(self.actionView.mas_bottom).offset(8); make.height.equalTo(@0); // 默认高度0,动态设置 }]; } - (void)configureWithModel:(id)cellModel { [super configureWithModel:cellModel]; TJPUserDynamicCellModel *model = (TJPUserDynamicCellModel *)self.cellModel; // 设置基本信息 self.userNameLabel.text = model.userName; self.timeLabel.text = model.publishTime; self.contentLabel.text = model.content; // 设置内容高度 [self.contentLabel mas_updateConstraints:^(MASConstraintMaker *make) { make.height.equalTo(@([model calculateContentHeight])); }]; [self.avatarImageView sd_setImageWithURL:[NSURL URLWithString:model.userAvatar]]; [self.likeButton setTitle:[NSString stringWithFormat:@" %ld", model.likes] forState:UIControlStateNormal]; [self.commentButton setTitle:[NSString stringWithFormat:@" %ld", model.comments] forState:UIControlStateNormal]; // 配置九宫格图片 self.nineGridView.imageUrls = model.images; CGFloat gridHeight = [model calculateNineGridHeight]; [self.nineGridView mas_updateConstraints:^(MASConstraintMaker *make) { make.height.equalTo(@(gridHeight)); }]; self.nineGridView.hidden = (gridHeight == 0); // 配置点赞评论区域 self.likeCommentArea.likeUsers = model.likeUsers; self.likeCommentArea.commentList = model.commentList; CGFloat likeCommentHeight = [model calculateLikeCommentAreaHeight]; [self.likeCommentArea mas_updateConstraints:^(MASConstraintMaker *make) { make.height.equalTo(@(likeCommentHeight)); }]; self.likeCommentArea.hidden = (likeCommentHeight == 0); } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPVideoCell.h ================================================ // // TJPVideoCell.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseTableViewCell.h" #import "TJPVideoCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPVideoCell : TJPBaseTableViewCell @property (nonatomic, weak) TJPVideoCellModel *cellModel; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Cell/TJPVideoCell.m ================================================ // // TJPVideoCell.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPVideoCell.h" #import #import @interface TJPVideoCell () @property (nonatomic, strong) UIImageView *coverImageView; @property (nonatomic, strong) UIButton *playButton; @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UILabel *authorLabel; @property (nonatomic, strong) UILabel *durationLabel; @property (nonatomic, strong) UILabel *playCountLabel; @end @implementation TJPVideoCell @synthesize cellModel = _cellModel; - (void)awakeFromNib { [super awakeFromNib]; // Initialization code } - (void)setSelected:(BOOL)selected animated:(BOOL)animated { [super setSelected:selected animated:animated]; // Configure the view for the selected state } - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; if (self) { [self setupUI]; } return self; } - (void)setupUI { self.selectionStyle = UITableViewCellSelectionStyleNone; // 封面图片 self.coverImageView = [[UIImageView alloc] init]; self.coverImageView.contentMode = UIViewContentModeScaleAspectFill; self.coverImageView.clipsToBounds = YES; self.coverImageView.layer.cornerRadius = 8; self.coverImageView.backgroundColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.coverImageView]; // 播放按钮 self.playButton = [[UIButton alloc] init]; [self.playButton setImage:[UIImage systemImageNamed:@"play.circle.fill"] forState:UIControlStateNormal]; self.playButton.tintColor = [UIColor whiteColor]; [self.contentView addSubview:self.playButton]; // 时长标签 self.durationLabel = [[UILabel alloc] init]; self.durationLabel.font = [UIFont systemFontOfSize:12]; self.durationLabel.textColor = [UIColor whiteColor]; self.durationLabel.backgroundColor = [UIColor colorWithWhite:0 alpha:0.6]; self.durationLabel.textAlignment = NSTextAlignmentCenter; self.durationLabel.layer.cornerRadius = 4; self.durationLabel.clipsToBounds = YES; [self.contentView addSubview:self.durationLabel]; // 标题 self.titleLabel = [[UILabel alloc] init]; self.titleLabel.font = [UIFont boldSystemFontOfSize:16]; self.titleLabel.textColor = [UIColor blackColor]; self.titleLabel.numberOfLines = 2; [self.contentView addSubview:self.titleLabel]; // 作者 self.authorLabel = [[UILabel alloc] init]; self.authorLabel.font = [UIFont systemFontOfSize:14]; self.authorLabel.textColor = [UIColor grayColor]; [self.contentView addSubview:self.authorLabel]; // 播放数 self.playCountLabel = [[UILabel alloc] init]; self.playCountLabel.font = [UIFont systemFontOfSize:12]; self.playCountLabel.textColor = [UIColor lightGrayColor]; [self.contentView addSubview:self.playCountLabel]; [self setupConstraints]; } - (void)setupConstraints { [self.coverImageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.contentView).offset(15); make.top.equalTo(self.contentView).offset(12); make.bottom.equalTo(self.contentView).offset(-12); make.width.equalTo(@120); }]; [self.playButton mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(self.coverImageView); make.width.height.equalTo(@40); }]; [self.durationLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.right.equalTo(self.coverImageView).offset(-8); make.bottom.equalTo(self.coverImageView).offset(-8); make.height.equalTo(@20); make.width.greaterThanOrEqualTo(@40); }]; [self.titleLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.coverImageView.mas_right).offset(12); make.right.equalTo(self.contentView).offset(-15); make.top.equalTo(self.coverImageView); }]; [self.authorLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self.titleLabel); make.top.equalTo(self.titleLabel.mas_bottom).offset(8); }]; [self.playCountLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self.titleLabel); make.bottom.equalTo(self.coverImageView); }]; } - (void)configureWithModel:(id)cellModel { [super configureWithModel:cellModel]; self.titleLabel.text = self.cellModel.title; self.authorLabel.text = self.cellModel.author; self.durationLabel.text = [NSString stringWithFormat:@" %@ ", self.cellModel.duration]; self.playCountLabel.text = [NSString stringWithFormat:@"%ld次播放", self.cellModel.playCount]; [self.coverImageView sd_setImageWithURL:[NSURL URLWithString:self.cellModel.coverUrl]]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPAdCellModel.h ================================================ // // TJPAdCellModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseCellModel.h" NS_ASSUME_NONNULL_BEGIN @class TJPAdFlipContentModel; @interface TJPAdCellModel : TJPBaseCellModel @property (nonatomic, copy) NSString *adId; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *subtitle; @property (nonatomic, copy) NSString *imageUrl; @property (nonatomic, copy) NSString *actionText; @property (nonatomic, copy) NSString *actionUrl; @property (nonatomic, strong) TJPAdFlipContentModel *flipContent; @end @interface TJPAdFlipContentModel : NSObject @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *subtitle; @property (nonatomic, copy) NSString *imageUrl; @property (nonatomic, copy) NSString *actionText; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPAdCellModel.m ================================================ // // TJPAdCellModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPAdCellModel.h" @implementation TJPAdCellModel - (NSString *)cellName { return @"TJPAdCell"; } - (CGFloat)cellHeight { return 100.0f; } @end @implementation TJPAdFlipContentModel @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPImageCellModel.h ================================================ // // TJPImageCellModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPImageCellModel : TJPBaseCellModel @property (nonatomic, copy) NSString *imageId; @property (nonatomic, copy) NSString *title; @property (nonatomic, strong) NSArray *imageUrls; @property (nonatomic, assign) NSInteger likes; @property (nonatomic, assign) NSInteger comments; @property (nonatomic, copy) NSString *imageDescription; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPImageCellModel.m ================================================ // // TJPImageCellModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPImageCellModel.h" @implementation TJPImageCellModel - (NSString *)cellName { return @"TJPImageCell"; } - (CGFloat)cellHeight { return 280.0f; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPNewsCellModel.h ================================================ // // TJPNewsCellModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPNewsCellModel : TJPBaseCellModel @property (nonatomic, copy) NSString *newsId; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *summary; @property (nonatomic, copy) NSString *imageUrl; @property (nonatomic, copy) NSString *publishTime; @property (nonatomic, copy) NSString *source; @property (nonatomic, assign) NSInteger readCount; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPNewsCellModel.m ================================================ // // TJPNewsCellModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPNewsCellModel.h" @implementation TJPNewsCellModel - (NSString *)cellName { return @"TJPNewsCell"; } - (CGFloat)cellHeight { return 120.0f; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPProductCellModel.h ================================================ // // TJPProductCellModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPProductCellModel : TJPBaseCellModel @property (nonatomic, copy) NSString *productId; @property (nonatomic, copy) NSString *name; @property (nonatomic, assign) CGFloat price; @property (nonatomic, assign) CGFloat originalPrice; @property (nonatomic, copy) NSString *imageUrl; @property (nonatomic, assign) CGFloat rating; @property (nonatomic, assign) NSInteger sales; @property (nonatomic, strong) NSArray *tags; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPProductCellModel.m ================================================ // // TJPProductCellModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPProductCellModel.h" @implementation TJPProductCellModel - (NSString *)cellName { return @"TJPProductCell"; } - (CGFloat)cellHeight { return 150.0f; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPUserDynamicCellModel.h ================================================ // // TJPUserDynamicCellModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseCellModel.h" NS_ASSUME_NONNULL_BEGIN @class TJPUserLikeModel, TJPUserComentModel; @interface TJPUserDynamicCellModel : TJPBaseCellModel @property (nonatomic, copy) NSString *userId; @property (nonatomic, copy) NSString *userName; @property (nonatomic, copy) NSString *userAvatar; @property (nonatomic, copy) NSString *content; @property (nonatomic, strong) NSArray *images; @property (nonatomic, copy) NSString *publishTime; @property (nonatomic, assign) NSInteger likes; @property (nonatomic, assign) NSInteger comments; // 点赞用户列表 @property (nonatomic, strong) NSArray *likeUsers; // 评论列表 @property (nonatomic, strong) NSArray *commentList; // 计算相关高度的方法 - (CGFloat)calculateContentHeight; - (CGFloat)calculateNineGridHeight; - (CGFloat)calculateLikeCommentAreaHeight; @end @interface TJPUserLikeModel : NSObject @property (nonatomic, copy) NSString *userId; @property (nonatomic, copy) NSString *userName; @end @interface TJPUserComentModel : NSObject @property (nonatomic, copy) NSString *userId; @property (nonatomic, copy) NSString *commentId; @property (nonatomic, copy) NSString *userName; @property (nonatomic, copy) NSString *content; @property (nonatomic, copy) NSString *replyTo; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPUserDynamicCellModel.m ================================================ // // TJPUserDynamicCellModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPUserDynamicCellModel.h" #import @implementation TJPUserDynamicCellModel - (NSString *)cellName { return @"TJPUserDynamicCell"; } + (NSDictionary *)modelContainerPropertyGenericClass { return @{ @"likeUsers" : [TJPUserLikeModel class], @"commentList" : [TJPUserComentModel class] }; } - (CGFloat)calculateCellHeight { CGFloat totalHeight = 0; // 顶部头像区域:top(12) + avatar(40) totalHeight += 12 + 40; // 用户名区域:userNameLabel + timeLabel + 间距 totalHeight += 5; // userNameLabel 到 timeLabel 的间距 // 内容文本高度:timeLabel 到 contentLabel 的间距(8) + 内容高度 totalHeight += 8 + [self calculateContentHeight]; // 图片区域 CGFloat gridHeight = [self calculateNineGridHeight]; if (gridHeight > 0) { totalHeight += gridHeight + 10; // 图片高度 + 下间距 } // actionView 区域:height(30) + space(8) totalHeight += 30 + 8; // 点赞评论区域 CGFloat likeCommentHeight = [self calculateLikeCommentAreaHeight]; if (likeCommentHeight > 0) { totalHeight += likeCommentHeight + 12; // 点赞评论高度 + 底部间距 } else { totalHeight += 12; // 只有底部间距 } return totalHeight; } - (CGFloat)calculateContentHeight { if (self.content.length == 0) { return 0; } // 计算 contentLabel 的可用宽度 CGFloat screenWidth = [UIScreen mainScreen].bounds.size.width; CGFloat leftMargin = 15; // contentView 左边距 CGFloat avatarWidth = 40; // 头像宽度 CGFloat avatarToContent = 10; // 头像到内容的间距 CGFloat rightMargin = 15; // contentView 右边距 CGFloat contentWidth = screenWidth - leftMargin - avatarWidth - avatarToContent - rightMargin; // 计算文本高度 CGRect textRect = [self.content boundingRectWithSize:CGSizeMake(contentWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:14]} context:nil]; return ceil(textRect.size.height); } - (CGFloat)calculateNineGridHeight { if (self.images.count == 0) return 0; CGFloat itemWidth = 80; CGFloat itemSpacing = 5; NSInteger rows = (self.images.count - 1) / 3 + 1; // 计算行数 return rows * itemWidth + (rows - 1) * itemSpacing; } - (CGFloat)calculateLikeCommentAreaHeight { if (self.likeUsers.count == 0 && self.commentList.count == 0) { return 0; } CGFloat height = 16; // 上下内边距 8 + 8 // 点赞区域高度 if (self.likeUsers.count > 0) { height += 25; // 点赞行高度 } // 分割线高度 if (self.likeUsers.count > 0 && self.commentList.count > 0) { height += 8; // 分割线区域 } // 评论区域高度 - 需要计算每条评论的实际高度 for (TJPUserComentModel *comment in self.commentList) { height += [self calculateCommentHeight:comment]; } return height; } - (CGFloat)calculateCommentHeight:(TJPUserComentModel *)comment { // 构建完整的评论文本 NSMutableString *fullText = [NSMutableString string]; [fullText appendString:comment.userName]; if (comment.replyTo.length > 0) { [fullText appendFormat:@" 回复 %@", comment.replyTo]; } [fullText appendFormat:@": %@", comment.content]; // 计算文本高度 CGFloat commentWidth = [UIScreen mainScreen].bounds.size.width - 15 - 40 - 10 - 15 - 16; // 减去点赞评论区域的内边距 CGRect textRect = [fullText boundingRectWithSize:CGSizeMake(commentWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:13]} context:nil]; return MAX(ceil(textRect.size.height) + 8, 22); // 最小高度22,包含上下间距 } @end @implementation TJPUserLikeModel @end @implementation TJPUserComentModel @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPVideoCellModel.h ================================================ // // TJPVideoCellModel.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPVideoCellModel : TJPBaseCellModel @property (nonatomic, copy) NSString *videoId; @property (nonatomic, copy) NSString *title; @property (nonatomic, copy) NSString *coverUrl; @property (nonatomic, copy) NSString *videoUrl; @property (nonatomic, copy) NSString *duration; @property (nonatomic, assign) NSInteger playCount; @property (nonatomic, copy) NSString *author; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Entity/TJPVideoCellModel.m ================================================ // // TJPVideoCellModel.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPVideoCellModel.h" @implementation TJPVideoCellModel - (NSString *)cellName { return @"TJPVideoCell"; } - (CGFloat)cellHeight { return 180.0f; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Interactor/TJPVIPERDemoInteractorImpl.h ================================================ // // TJPVIPERDemoInteractorImpl.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/1. // #import "TJPViperBaseInteractorImpl.h" NS_ASSUME_NONNULL_BEGIN @interface TJPVIPERDemoInteractorImpl : TJPViperBaseInteractorImpl @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Interactor/TJPVIPERDemoInteractorImpl.m ================================================ // // TJPVIPERDemoInteractorImpl.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/1. // #import "TJPVIPERDemoInteractorImpl.h" #import #import "TJPNetworkDefine.h" #import "TJPCacheManager.h" #import "TJPBaseSectionModel.h" #import "TJPFeedResponse.h" #import "TJPPaginationInfo.h" #import "TJPNewsCellModel.h" #import "TJPImageCellModel.h" #import "TJPVideoCellModel.h" #import "TJPUserDynamicCellModel.h" #import "TJPProductCellModel.h" #import "TJPAdCellModel.h" @interface TJPVIPERDemoInteractorImpl () @property (nonatomic, assign) NSInteger totalCount; //@property (nonatomic, strong) RACCommand *selectedDemoDetilCommand; @property (nonatomic, strong) RACCommand *selectedNewsCommand; @property (nonatomic, strong) RACCommand *selectedImageCommand; @property (nonatomic, strong) RACCommand *selectedVideoCommand; @property (nonatomic, strong) RACCommand *selectedUserDynamicCommand; @property (nonatomic, strong) RACCommand *selectedProductCommand; @property (nonatomic, strong) RACCommand *selectedAdCommand; @end @implementation TJPVIPERDemoInteractorImpl - (instancetype)init { self = [super init]; if (self) { _totalCount = 0; } return self; } - (void)performDataRequestForPage:(NSInteger)page withPagination:(void (^)(NSArray * _Nullable, TJPPaginationInfo * _Nullable, NSError * _Nullable))completion { NSString *api = @"https://www.tjp.example.demo.api"; TJPLOG_INFO(@"开始请求第 %ld 页数据,API: %@", page, api); //网络请求类 请求服务器数据 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 从JSON文件加载数据(或者从真实API获取) TJPFeedResponse *feedResponse = [self loadFeedDataFromJSONWithPage:page pageSize:20]; if (feedResponse && feedResponse.isSuccess) { TJPLOG_INFO(@"Feed响应解析成功: %@", feedResponse.debugDescription); // 打印Feed统计信息 NSDictionary *stats = [feedResponse getFeedStatistics]; TJPLOG_INFO(@"Feed统计信息: %@", stats); // 转换数据模型 NSArray *cellModels = [self convertFeedsToModels:feedResponse.feeds]; TJPBaseSectionModel *section = [[TJPBaseSectionModel alloc] initWithCellModels:cellModels]; NSArray *sections = @[section]; // 创建分页信息 - 两种分页方式的兼容 TJPPaginationInfo *paginationInfo = [self createPaginationInfoFromResponse:feedResponse forPage:page]; TJPLOG_INFO(@"请求成功 - %@", paginationInfo); if (completion) completion(sections, paginationInfo, nil); } else { NSString *errorMessage = feedResponse ? [NSString stringWithFormat:@"服务器错误: %@ (code: %ld)", feedResponse.message, (long)feedResponse.code] : @"数据解析失败"; NSError *error = [NSError errorWithDomain:TJPViperErrorDomain code:TJPViperErrorDataProcessFailed userInfo:@{NSLocalizedDescriptionKey: errorMessage}]; if (completion) completion(nil, nil, error); } }); } - (TJPPaginationInfo *)createPaginationInfoFromResponse:(TJPFeedResponse *)response forPage:(NSInteger)page { // 根据实际业务需求选择分页方式 BOOL useCursorPagination = [self shouldUseCursorPaginationForPage:page]; if (useCursorPagination) { // 游标分页方式 return [self createCursorBasedPagination:response]; } else { // 页码分页方式 return [self createPageBasedPagination:response]; } } - (BOOL)shouldUseCursorPaginationForPage:(NSInteger)page { // 这里可以根据业务逻辑决定使用哪种分页方式 return page > 3; } - (TJPPaginationInfo *)createPageBasedPagination:(TJPFeedResponse *)response { TJPPaginationInfo *pagination = response.pagination; if (pagination.paginationType == TJPPaginationTypePageBased) { // 如果响应中已经是页码分页,直接使用 return pagination; } else { // 如果响应是游标分页,但我们想转换为页码分页 return [TJPPaginationInfo pageBasedPaginationWithPage:pagination.currentPage > 0 ? pagination.currentPage : 1 pageSize:pagination.pageSize totalCount:response.totalCount > 0 ? response.totalCount : 100]; // 假设总数 } } - (TJPPaginationInfo *)createCursorBasedPagination:(TJPFeedResponse *)response { TJPPaginationInfo *pagination = response.pagination; if (pagination.paginationType == TJPPaginationTypeCursorBased) { // 如果响应中已经是游标分页,直接使用 return pagination; } else { // 如果响应是页码分页,但我们想转换为游标分页 NSString *nextCursor = pagination.hasMore ? [NSString stringWithFormat:@"cursor_%ld", pagination.currentPage + 1] : nil; return [TJPPaginationInfo cursorBasedPaginationWithPageSize:pagination.pageSize nextCursor:nextCursor hasMore:pagination.hasMore]; } } #pragma mark - 重写分页相关的工具方法 - (NSDictionary *)parametersForPage:(NSInteger)page { NSMutableDictionary *params = [[super commonParameters] mutableCopy]; // 根据当前分页类型添加不同的参数 if (self.currentPagination && self.currentPagination.paginationType == TJPPaginationTypeCursorBased) { // 游标分页参数 params[@"cursor"] = self.currentPagination.nextCursor ?: @""; params[@"page_size"] = @(self.defaultPageSize); } else { // 页码分页参数 params[@"page"] = @(page); params[@"page_size"] = @(self.defaultPageSize); } return [params copy]; } - (TJPPaginationInfo *)extractPaginationFromResponse:(id)rawData { NSLog(@"[DEBUG] 服务端返回的原始分页数据: %@", rawData); if (![rawData isKindOfClass:[NSDictionary class]]) { return nil; } NSDictionary *responseDict = (NSDictionary *)rawData; NSDictionary *dataDict = responseDict[@"data"]; NSDictionary *paginationDict = dataDict[@"pagination"]; if (paginationDict) { return [TJPPaginationInfo paginationWithDict:paginationDict]; } return nil; } - (TJPFeedResponse *)loadFeedDataFromJSONWithPage:(NSInteger)page pageSize:(NSInteger)pageSize { // 根据页码选择不同的JSON文件或生成不同的数据 NSString *fileName = (page == 1) ? @"feedData" : [NSString stringWithFormat:@"feedData_page%ld", page]; NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:@"json"]; NSData *data = nil; if (path) { data = [NSData dataWithContentsOfFile:path]; } // 如果没有找到对应页码的JSON文件,生成模拟数据 if (!data) { NSLog(@"未找到对应页码的JSON文件"); return [self generateMockFeedResponseForPage:page pageSize:pageSize]; } NSError *error; NSDictionary *jsonDict = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error]; if (error) { TJPLOG_ERROR(@"JSON解析错误: %@", error.localizedDescription); return [self generateMockFeedResponseForPage:page pageSize:pageSize]; } TJPFeedResponse *response = [TJPFeedResponse feedResponseWithDict:jsonDict]; if (!response || !response.isSuccess) { TJPLOG_ERROR(@"Feed响应解析失败或服务器返回错误"); return [self generateMockFeedResponseForPage:page pageSize:pageSize]; } return response; } - (TJPFeedResponse *)generateMockFeedResponseForPage:(NSInteger)page pageSize:(NSInteger)pageSize { // 生成模拟响应数据 NSMutableArray *feeds = [NSMutableArray array]; // 生成不同类型的Feed数据 NSArray *feedTypes = @[@"news", @"image", @"video", @"userDynamic", @"product", @"ad"]; for (NSInteger i = 0; i < pageSize; i++) { NSString *feedType = feedTypes[arc4random() % feedTypes.count]; NSDictionary *feed = [self generateMockFeedOfType:feedType page:page index:i]; [feeds addObject:feed]; } // 创建分页信息 BOOL hasMore = page < 5; // 假设总共5页 NSInteger totalCount = 5 * pageSize; TJPPaginationInfo *pagination = [TJPPaginationInfo pageBasedPaginationWithPage:page pageSize:pageSize totalCount:totalCount]; // 构造响应字典 NSDictionary *responseDict = @{ @"code": @200, @"message": @"success", @"data": @{ @"feeds": feeds, @"pagination": [pagination toDictionary] } }; // 使用增强的Response类创建响应 TJPFeedResponse *response = [TJPFeedResponse feedResponseWithDict:responseDict]; TJPLOG_INFO(@"生成模拟Feed响应: %@", response.debugDescription); return response; } - (NSArray *)convertFeedsToModels:(NSArray *)feedsArray { NSMutableArray *models = [NSMutableArray array]; for (NSDictionary *feedDict in feedsArray) { TJPBaseCellModel *model = [self createModelFromDict:feedDict]; if (model) { [models addObject:model]; } } return models; } - (NSDictionary *)generateMockFeedOfType:(NSString *)feedType page:(NSInteger)page index:(NSInteger)index { NSString *feedId = [NSString stringWithFormat:@"mock_%@_%ld_%ld", feedType, page, index]; if ([feedType isEqualToString:@"news"]) { return @{ @"type": @"news", @"id": feedId, @"title": [NSString stringWithFormat:@"模拟新闻标题 - 第%ld页第%ld条", page, index+1], @"summary": @"这是一条模拟的新闻摘要内容,展示了新闻的主要信息...", @"imageUrl": @"https://example.com/mock_news_image.jpg", @"publishTime": @"刚刚", @"source": @"模拟新闻源", @"readCount": @(arc4random() % 10000) }; } else if ([feedType isEqualToString:@"image"]) { return @{ @"type": @"image", @"id": feedId, @"title": [NSString stringWithFormat:@"精美图片集 - 第%ld页第%ld条", page, index+1], @"imageUrls": @[ @"https://example.com/mock_image1.jpg", @"https://example.com/mock_image2.jpg", @"https://example.com/mock_image3.jpg" ], @"likes": @(arc4random() % 1000), @"comments": @(arc4random() % 100), @"description": @"这是一组精美的图片,展示了美丽的风景" }; } else if ([feedType isEqualToString:@"video"]) { return @{ @"type": @"video", @"id": feedId, @"title": [NSString stringWithFormat:@"精彩视频 - 第%ld页第%ld条", page, index+1], @"coverUrl": @"https://example.com/mock_video_cover.jpg", @"videoUrl": @"https://example.com/mock_video.mp4", @"duration": @"05:30", @"playCount": @(arc4random() % 50000), @"author": @"视频创作者" }; } else if ([feedType isEqualToString:@"userDynamic"]) { return @{ @"type": @"userDynamic", @"id": feedId, @"userName": [NSString stringWithFormat:@"用户%ld", index+1], @"userAvatar": @"https://example.com/mock_avatar.jpg", @"content": [NSString stringWithFormat:@"这是第%ld页第%ld条用户动态,分享一些有趣的内容...", page, index+1], @"images": @[@"https://example.com/dynamic_image.jpg"], @"publishTime": @"2小时前", @"likes": @(arc4random() % 500), @"comments": @(arc4random() % 50) }; } else if ([feedType isEqualToString:@"product"]) { return @{ @"type": @"product", @"id": feedId, @"name": [NSString stringWithFormat:@"热门商品 - 第%ld页第%ld条", page, index+1], @"price": @(199.99 + (arc4random() % 800)), @"originalPrice": @(299.99 + (arc4random() % 1000)), @"imageUrl": @"https://example.com/mock_product.jpg", @"rating": @(4.0 + (arc4random() % 10) / 10.0), @"sales": @(arc4random() % 5000), @"tags": @[@"热销", @"包邮", @"正品保证"] }; } else if ([feedType isEqualToString:@"ad"]) { return @{ @"type": @"ad", @"id": feedId, @"title": [NSString stringWithFormat:@"精品广告 - 第%ld页第%ld条", page, index+1], @"subtitle": @"限时优惠,不容错过", @"imageUrl": @"https://example.com/mock_ad.jpg", @"actionText": @"立即查看", @"actionUrl": @"https://example.com/ad_landing" }; } // 默认返回news类型 return @{ @"type": @"news", @"id": feedId, @"title": @"默认新闻", @"summary": @"默认摘要", @"imageUrl": @"https://example.com/default.jpg", @"publishTime": @"刚刚", @"source": @"默认来源", @"readCount": @100 }; } #pragma mark - Build CellModel - (TJPBaseCellModel *)createModelFromDict:(NSDictionary *)dict { NSString *type = dict[@"type"]; if ([type isEqualToString:@"news"]) { return [self createNewsModelFromDict:dict]; } else if ([type isEqualToString:@"image"]) { return [self createImageModelFromDict:dict]; } else if ([type isEqualToString:@"video"]) { return [self createVideoModelFromDict:dict]; } else if ([type isEqualToString:@"userDynamic"]) { return [self createUserDynamicModelFromDict:dict]; } else if ([type isEqualToString:@"product"]) { return [self createProductModelFromDict:dict]; } else if ([type isEqualToString:@"ad"]) { return [self createAdModelFromDict:dict]; } return nil; } - (TJPNewsCellModel *)createNewsModelFromDict:(NSDictionary *)dict { TJPNewsCellModel *model = [[TJPNewsCellModel alloc] init]; model.newsId = dict[@"id"]; // model.type = @"news"; model.title = dict[@"title"]; model.summary = dict[@"summary"]; model.imageUrl = dict[@"imageUrl"]; model.publishTime = dict[@"publishTime"]; model.source = dict[@"source"]; model.readCount = [dict[@"readCount"] integerValue]; model.selectedCommand = self.selectedNewsCommand; return model; } - (TJPImageCellModel *)createImageModelFromDict:(NSDictionary *)dict { TJPImageCellModel *model = [[TJPImageCellModel alloc] init]; model.imageId = dict[@"id"]; // model.type = @"image"; model.title = dict[@"title"]; model.imageUrls = dict[@"imageUrls"]; model.likes = [dict[@"likes"] integerValue]; model.comments = [dict[@"comments"] integerValue]; model.imageDescription = dict[@"description"]; model.selectedCommand = self.selectedImageCommand; return model; } - (TJPVideoCellModel *)createVideoModelFromDict:(NSDictionary *)dict { TJPVideoCellModel *model = [[TJPVideoCellModel alloc] init]; model.videoId = dict[@"id"]; // model.type = @"video"; model.title = dict[@"title"]; model.coverUrl = dict[@"coverUrl"]; model.videoUrl = dict[@"videoUrl"]; model.duration = dict[@"duration"]; model.playCount = [dict[@"playCount"] integerValue]; model.author = dict[@"author"]; model.selectedCommand = self.selectedVideoCommand; return model; } - (TJPUserDynamicCellModel *)createUserDynamicModelFromDict:(NSDictionary *)dict { TJPUserDynamicCellModel *model = [TJPUserDynamicCellModel yy_modelWithDictionary:dict]; // TJPUserDynamicCellModel *model = [[TJPUserDynamicCellModel alloc] init]; // model.userId = dict[@"id"]; // model.type = @"userDynamic"; // model.userName = dict[@"userName"]; // model.userAvatar = dict[@"userAvatar"]; // model.content = dict[@"content"]; // model.images = dict[@"images"]; // model.publishTime = dict[@"publishTime"]; // model.likes = [dict[@"likes"] integerValue]; // model.comments = [dict[@"comments"] integerValue]; model.selectedCommand = self.selectedUserDynamicCommand; return model; } - (TJPProductCellModel *)createProductModelFromDict:(NSDictionary *)dict { TJPProductCellModel *model = [[TJPProductCellModel alloc] init]; model.productId = dict[@"id"]; // model.type = @"product"; model.name = dict[@"name"]; model.price = [dict[@"price"] floatValue]; model.originalPrice = [dict[@"originalPrice"] floatValue]; model.imageUrl = dict[@"imageUrl"]; model.rating = [dict[@"rating"] floatValue]; model.sales = [dict[@"sales"] integerValue]; model.tags = dict[@"tags"]; model.selectedCommand = self.selectedProductCommand; return model; } - (TJPAdCellModel *)createAdModelFromDict:(NSDictionary *)dict { TJPAdCellModel *model = [TJPAdCellModel yy_modelWithDictionary:dict]; // TJPAdCellModel *model = [[TJPAdCellModel alloc] init]; // model.adId = dict[@"id"]; // model.type = @"ad"; // model.title = dict[@"title"]; // model.subtitle = dict[@"subtitle"]; // model.imageUrl = dict[@"imageUrl"]; // model.actionText = dict[@"actionText"]; // model.actionUrl = dict[@"actionUrl"]; model.selectedCommand = self.selectedAdCommand; return model; } #pragma mark - Commands //- (RACCommand *)selectedDemoDetilCommand { // if (nil == _selectedDemoDetilCommand) { // @weakify(self) // _selectedDemoDetilCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(TJPVIPERDemoCellModel * _Nullable input) { // @strongify(self) // [self.navigateToPageSubject sendNext:input]; // return [RACSignal empty]; // }]; // } // return _selectedDemoDetilCommand; //} - (RACCommand *)selectedNewsCommand { if (!_selectedNewsCommand) { @weakify(self) _selectedNewsCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(TJPNewsCellModel * _Nullable input) { @strongify(self) [self.navigateToPageSubject sendNext:input]; return [RACSignal empty]; }]; } return _selectedNewsCommand; } - (RACCommand *)selectedImageCommand { if (!_selectedImageCommand) { @weakify(self) _selectedImageCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(TJPImageCellModel * _Nullable input) { @strongify(self) [self.navigateToPageSubject sendNext:input]; return [RACSignal empty]; }]; } return _selectedImageCommand; } - (RACCommand *)selectedVideoCommand { if (!_selectedVideoCommand) { @weakify(self) _selectedVideoCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(TJPVideoCellModel * _Nullable input) { @strongify(self) [self.navigateToPageSubject sendNext:input]; return [RACSignal empty]; }]; } return _selectedVideoCommand; } - (RACCommand *)selectedUserDynamicCommand { if (!_selectedUserDynamicCommand) { @weakify(self) _selectedUserDynamicCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(TJPUserDynamicCellModel * _Nullable input) { @strongify(self) [self.navigateToPageSubject sendNext:input]; return [RACSignal empty]; }]; } return _selectedUserDynamicCommand; } - (RACCommand *)selectedProductCommand { if (!_selectedProductCommand) { @weakify(self) _selectedProductCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(TJPProductCellModel * _Nullable input) { @strongify(self) [self.navigateToPageSubject sendNext:input]; return [RACSignal empty]; }]; } return _selectedProductCommand; } - (RACCommand *)selectedAdCommand { if (!_selectedAdCommand) { @weakify(self) _selectedAdCommand = [[RACCommand alloc] initWithSignalBlock:^RACSignal * _Nonnull(TJPAdCellModel * _Nullable input) { @strongify(self) [self.navigateToPageSubject sendNext:input]; return [RACSignal empty]; }]; } return _selectedAdCommand; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Presenter/TJPVIPERDemoPresenter.h ================================================ // // TJPVIPERDemoPresenter.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/28. // #import "TJPViperBasePresenterImpl.h" NS_ASSUME_NONNULL_BEGIN @interface TJPVIPERDemoPresenter : TJPViperBasePresenterImpl @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Presenter/TJPVIPERDemoPresenter.m ================================================ // // TJPVIPERDemoPresenter.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/28. // #import "TJPVIPERDemoPresenter.h" #import "TJPNavigationModel.h" @implementation TJPVIPERDemoPresenter #pragma mark - 实现模板方法 /** * 重写模板方法:处理VIPER Demo模块相关的CellModel */ - (TJPNavigationModel *)buildNavigationModelFromCellModel:(id)cellModel { if (!cellModel) { return nil; } // 处理TJPVIPERDemoCellModel if ([cellModel isKindOfClass:NSClassFromString(@"TJPVIPERDemoCellModel")]) { return [self buildVIPERDemoNavigationModel:cellModel]; } // 处理其他本模块关心的CellModel类型 if ([cellModel isKindOfClass:NSClassFromString(@"TJPNewsCellModel")]) { return [self buildNewsNavigationModel:cellModel]; } // 对于不处理的类型,调用父类的默认实现 return [super buildNavigationModelFromCellModel:cellModel]; } /** * 构建VIPER Demo的NavigationModel */ - (TJPNavigationModel *)buildVIPERDemoNavigationModel:(id)cellModel { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; // 通过KVC安全地获取属性值 if ([cellModel respondsToSelector:@selector(detailId)]) { id detailId = [(NSObject *)cellModel valueForKey:@"detailId"]; if (detailId) parameters[@"detailId"] = detailId; } if ([cellModel respondsToSelector:@selector(title)]) { id title = [(NSObject *)cellModel valueForKey:@"title"]; if (title) parameters[@"title"] = title; } if ([cellModel respondsToSelector:@selector(subtitle)]) { id subtitle = [(NSObject *)cellModel valueForKey:@"subtitle"]; if (subtitle) parameters[@"subtitle"] = subtitle; } // 添加通用参数 parameters[@"timestamp"] = @([[NSDate date] timeIntervalSince1970]); TJPNavigationModel *model = [TJPNavigationModel modelWithRouteId:@"viperDemoDetail" parameters:[parameters copy] routeType:TJPNavigationRouteTypeViewPush]; model.animated = YES; return model; } /** * 构建新闻的NavigationModel(如果这个Presenter也处理新闻) */ - (TJPNavigationModel *)buildNewsNavigationModel:(id)cellModel { NSMutableDictionary *parameters = [NSMutableDictionary dictionary]; if ([cellModel respondsToSelector:@selector(newsId)]) { id newsId = [(NSObject *)cellModel valueForKey:@"newsId"]; if (newsId) parameters[@"newsId"] = newsId; } if ([cellModel respondsToSelector:@selector(title)]) { id title = [(NSObject *)cellModel valueForKey:@"title"]; if (title) parameters[@"title"] = title; } TJPNavigationModel *model = [TJPNavigationModel modelWithRouteId:@"newsDetail" parameters:[parameters copy] routeType:TJPNavigationRouteTypeViewPush]; model.animated = YES; return model; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Respone/TJPFeedResponse.h ================================================ // // TJPFeedResponse.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPBaseResponse.h" NS_ASSUME_NONNULL_BEGIN @class TJPPaginationInfo; @interface TJPFeedResponse : TJPBaseResponse // Feed特有属性 @property (nonatomic, strong, readonly, nullable) NSArray *feeds; // Feed特有方法 - (NSInteger)feedCount; - (BOOL)hasFeedData; - (NSArray *)getFeedsByType:(NSString *)feedType; - (NSDictionary *)getFeedStatistics; + (instancetype)feedResponseWithDict:(NSDictionary *)dict; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Respone/TJPFeedResponse.m ================================================ // // TJPFeedResponse.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPFeedResponse.h" #import "TJPPaginationInfo.h" @implementation TJPFeedResponse - (NSArray *)feeds { // 向后兼容:feeds属性实际就是data属性 return self.data; } + (instancetype)feedResponseWithDict:(NSDictionary *)dict { return [self responseWithDict:dict dataClass:[NSArray class]]; } - (NSArray *)parseDataFromDict:(NSDictionary *)dict { // Feed业务的特定解析逻辑 id dataValue = dict[@"data"]; if ([dataValue isKindOfClass:[NSDictionary class]]) { NSDictionary *dataDict = (NSDictionary *)dataValue; // 尝试多个可能的字段名 NSArray *feedsArray = dataDict[@"feeds"] ?: dataDict[@"list"] ?: dataDict[@"items"]; if ([feedsArray isKindOfClass:[NSArray class]]) { return feedsArray; } } else if ([dataValue isKindOfClass:[NSArray class]]) { // 数据直接是数组格式 return (NSArray *)dataValue; } // 容错处理:如果解析失败,返回空数组而不是nil NSLog(@"Feed数据解析失败,返回空数组"); return @[]; } - (TJPPaginationInfo *)parsePaginationFromDict:(NSDictionary *)dict { // 调用父类方法 TJPPaginationInfo *pagination = [super parsePaginationFromDict:dict]; // Feed业务的特殊处理 if (!pagination) { // 如果没有找到分页信息,尝试从feeds数据推断 NSArray *feeds = self.data; if (feeds && feeds.count > 0) { // 创建一个默认的分页信息 NSLog(@"未找到分页信息,创建默认分页信息"); pagination = [TJPPaginationInfo pageBasedPaginationWithPage:1 pageSize:feeds.count totalCount:feeds.count]; } } return pagination; } - (NSInteger)feedCount { return self.feeds ? self.feeds.count : 0; } - (BOOL)hasFeedData { return self.feedCount > 0; } - (NSArray *)getFeedsByType:(NSString *)feedType { if (!feedType || !self.feeds) { return @[]; } NSPredicate *predicate = [NSPredicate predicateWithFormat:@"type == %@", feedType]; return [self.feeds filteredArrayUsingPredicate:predicate]; } - (NSDictionary *)getFeedStatistics { if (!self.hasFeedData) { return @{}; } NSMutableDictionary *statistics = [NSMutableDictionary dictionary]; NSMutableDictionary *typeCounts = [NSMutableDictionary dictionary]; // 统计各种类型的数量 for (NSDictionary *feed in self.feeds) { if ([feed isKindOfClass:[NSDictionary class]]) { NSString *type = feed[@"type"] ?: @"unknown"; NSNumber *count = typeCounts[type] ?: @0; typeCounts[type] = @(count.integerValue + 1); } } statistics[@"total_count"] = @(self.feedCount); statistics[@"type_counts"] = [typeCounts copy]; statistics[@"types"] = [typeCounts allKeys]; return [statistics copy]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Respone/TJPPaginationInfo.h ================================================ // // TJPPaginationInfo.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, TJPPaginationType) { TJPPaginationTypePageBased = 0, // 基于页码的分页 TJPPaginationTypeCursorBased = 1 // 基于游标的分页 }; @interface TJPPaginationInfo : NSObject // 通用属性 @property (nonatomic, assign) TJPPaginationType paginationType; @property (nonatomic, assign) NSInteger pageSize; @property (nonatomic, assign) BOOL hasMore; // 基于页码的分页属性 (Page-based) @property (nonatomic, assign) NSInteger currentPage; @property (nonatomic, assign) NSInteger totalCount; @property (nonatomic, assign) NSInteger totalPages; // 基于游标的分页属性 (Cursor-based) @property (nonatomic, strong, nullable) NSString *nextCursor; @property (nonatomic, strong, nullable) NSString *previousCursor; + (instancetype)pageBasedPaginationWithPage:(NSInteger)page pageSize:(NSInteger)pageSize totalCount:(NSInteger)totalCount; + (instancetype)cursorBasedPaginationWithPageSize:(NSInteger)pageSize nextCursor:(nullable NSString *)nextCursor hasMore:(BOOL)hasMore; + (instancetype)paginationWithDict:(NSDictionary *)dict; // 工具方法 - (BOOL)canLoadNextPage; - (BOOL)canLoadPreviousPage; // 仅适用于页码分页 - (NSInteger)getNextPageNumber; - (NSDictionary *)toDictionary; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Respone/TJPPaginationInfo.m ================================================ // // TJPPaginationInfo.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/29. // #import "TJPPaginationInfo.h" @implementation TJPPaginationInfo + (instancetype)pageBasedPaginationWithPage:(NSInteger)page pageSize:(NSInteger)pageSize totalCount:(NSInteger)totalCount { TJPPaginationInfo *pagination = [[TJPPaginationInfo alloc] init]; pagination.paginationType = TJPPaginationTypePageBased; pagination.currentPage = page; pagination.pageSize = pageSize; pagination.totalCount = totalCount; pagination.totalPages = (totalCount + pageSize - 1) / pageSize; // 向上取整 pagination.hasMore = page < pagination.totalPages; return pagination; } + (instancetype)cursorBasedPaginationWithPageSize:(NSInteger)pageSize nextCursor:(NSString *)nextCursor hasMore:(BOOL)hasMore { TJPPaginationInfo *pagination = [[TJPPaginationInfo alloc] init]; pagination.paginationType = TJPPaginationTypeCursorBased; pagination.pageSize = pageSize; pagination.nextCursor = nextCursor; pagination.hasMore = hasMore; return pagination; } + (instancetype)paginationWithDict:(NSDictionary *)dict { TJPPaginationInfo *pagination = [[TJPPaginationInfo alloc] init]; // 判断分页类型 if (dict[@"next_cursor"] || dict[@"nextCursor"]) { // 游标分页 pagination.paginationType = TJPPaginationTypeCursorBased; pagination.nextCursor = dict[@"next_cursor"] ?: dict[@"nextCursor"]; pagination.previousCursor = dict[@"previous_cursor"] ?: dict[@"previousCursor"]; } else { // 页码分页 pagination.paginationType = TJPPaginationTypePageBased; pagination.currentPage = [dict[@"page"] ?: dict[@"current_page"] integerValue]; pagination.totalCount = [dict[@"total"] ?: dict[@"total_count"] integerValue]; pagination.totalPages = [dict[@"total_pages"] integerValue]; } pagination.pageSize = [dict[@"page_size"] ?: dict[@"pageSize"] integerValue]; pagination.hasMore = [dict[@"has_more"] ?: dict[@"hasMore"] boolValue]; return pagination; } #pragma mark - 工具方法 - (BOOL)canLoadNextPage { return self.hasMore; } - (BOOL)canLoadPreviousPage { if (self.paginationType == TJPPaginationTypePageBased) { return self.currentPage > 1; } else { return self.previousCursor.length > 0; } } - (NSInteger)getNextPageNumber { if (self.paginationType == TJPPaginationTypePageBased) { return self.hasMore ? self.currentPage + 1 : self.currentPage; } return 0; // 游标分页不适用 } - (NSDictionary *)toDictionary { NSMutableDictionary *dict = [NSMutableDictionary dictionary]; dict[@"pagination_type"] = @(self.paginationType); dict[@"page_size"] = @(self.pageSize); dict[@"has_more"] = @(self.hasMore); if (self.paginationType == TJPPaginationTypePageBased) { dict[@"current_page"] = @(self.currentPage); dict[@"total_count"] = @(self.totalCount); dict[@"total_pages"] = @(self.totalPages); } else { if (self.nextCursor) dict[@"next_cursor"] = self.nextCursor; if (self.previousCursor) dict[@"previous_cursor"] = self.previousCursor; } return [dict copy]; } #pragma mark - NSCopying - (id)copyWithZone:(NSZone *)zone { TJPPaginationInfo *copy = [[TJPPaginationInfo alloc] init]; copy.paginationType = self.paginationType; copy.pageSize = self.pageSize; copy.hasMore = self.hasMore; copy.currentPage = self.currentPage; copy.totalCount = self.totalCount; copy.totalPages = self.totalPages; copy.nextCursor = [self.nextCursor copy]; copy.previousCursor = [self.previousCursor copy]; return copy; } #pragma mark - NSCoding - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeInteger:self.paginationType forKey:@"paginationType"]; [coder encodeInteger:self.pageSize forKey:@"pageSize"]; [coder encodeBool:self.hasMore forKey:@"hasMore"]; [coder encodeInteger:self.currentPage forKey:@"currentPage"]; [coder encodeInteger:self.totalCount forKey:@"totalCount"]; [coder encodeInteger:self.totalPages forKey:@"totalPages"]; [coder encodeObject:self.nextCursor forKey:@"nextCursor"]; [coder encodeObject:self.previousCursor forKey:@"previousCursor"]; } - (instancetype)initWithCoder:(NSCoder *)coder { self = [super init]; if (self) { _paginationType = [coder decodeIntegerForKey:@"paginationType"]; _pageSize = [coder decodeIntegerForKey:@"pageSize"]; _hasMore = [coder decodeBoolForKey:@"hasMore"]; _currentPage = [coder decodeIntegerForKey:@"currentPage"]; _totalCount = [coder decodeIntegerForKey:@"totalCount"]; _totalPages = [coder decodeIntegerForKey:@"totalPages"]; _nextCursor = [coder decodeObjectForKey:@"nextCursor"]; _previousCursor = [coder decodeObjectForKey:@"previousCursor"]; } return self; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Router/TJPVIPERDemoRouter.h ================================================ // // TJPVIPERDemoRouter.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/28. // #import "TJPViperBaseRouterImpl.h" NS_ASSUME_NONNULL_BEGIN @interface TJPVIPERDemoRouter : TJPViperBaseRouterImpl @property (nonatomic, strong) id viperModuleProvider; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/Router/TJPVIPERDemoRouter.m ================================================ // // TJPVIPERDemoRouter.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/28. // #import "TJPVIPERDemoRouter.h" #import "TJPViperModuleProvider.h" #import "TJPNavigationDefines.h" @implementation TJPVIPERDemoRouter - (UIViewController *)createViewControllerForRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { // 根据策略选择创建方式 TJPRouterCreationStrategy strategy = [self creationStrategyForRoute:routeId]; switch (strategy) { case TJPRouterCreationStrategyDI: return [self createViewControllerWithDI:routeId parameters:parameters]; case TJPRouterCreationStrategyHardcode: return [self createViewControllerWithHardcode:routeId parameters:parameters]; case TJPRouterCreationStrategyFactory: return [self createViewControllerWithFactory:routeId parameters:parameters]; default: NSLog(@"[ViperDemoRouter] 未知的创建策略: %ld", (long)strategy); return nil; } } /** * DI方式创建ViewController * 通过Typhoon等DI框架创建,适用于复杂的VIPER模块 */ - (UIViewController *)createViewControllerWithDI:(NSString *)routeId parameters:(NSDictionary *)parameters { // VIPER Demo主页 if ([routeId isEqualToString:@"viperDemo"]) { return [self.viperModuleProvider viperDemoViewController]; } // VIPER Demo详情页 if ([routeId isEqualToString:@"viperDemoDetail"]) { UIViewController *detailVC = [self.viperModuleProvider viperDemoDetailViewController]; detailVC.hidesBottomBarWhenPushed = YES; return detailVC; } // 通过DI创建的新闻详情页 if ([routeId isEqualToString:@"newsDetail"]) { // 获取Title NSString *title = [parameters objectForKey:@"title"]; return [self.viperModuleProvider viperNewsDetailViewControllerWithTitle:title]; } return nil; } /** * 硬编码方式创建ViewController * 直接通过类名创建,适用于简单的页面 */ - (UIViewController *)createViewControllerWithHardcode:(NSString *)routeId parameters:(NSDictionary *)parameters { // 简单设置页 if ([routeId isEqualToString:@"simpleSettings"]) { return [[NSClassFromString(@"SettingsViewController") alloc] init]; } // Web页面 if ([routeId isEqualToString:@"webView"]) { return [[NSClassFromString(@"WebViewController") alloc] init]; } // 图片预览 if ([routeId isEqualToString:@"imagePreview"]) { return [[NSClassFromString(@"ImagePreviewViewController") alloc] init]; } // 用户资料页(硬编码方式) if ([routeId isEqualToString:@"userProfile"]) { return [[NSClassFromString(@"UserProfileViewController") alloc] init]; } // 商品详情页(硬编码方式) if ([routeId isEqualToString:@"productDetail"]) { return [[NSClassFromString(@"ProductDetailViewController") alloc] init]; } // 系统Alert if ([routeId isEqualToString:@"alert"]) { NSString *title = parameters[@"title"] ?: @"提示"; NSString *message = parameters[@"message"] ?: @""; UIAlertController *alert = [UIAlertController alertControllerWithTitle:title message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; [alert addAction:okAction]; return alert; } return nil; } /** * 工厂模式创建ViewController * 根据参数动态选择创建方式,适用于需要灵活配置的场景 */ - (UIViewController *)createViewControllerWithFactory:(NSString *)routeId parameters:(NSDictionary *)parameters { // 动态视图:根据参数选择不同的创建方式 if ([routeId isEqualToString:@"dynamicView"]) { NSString *viewType = parameters[@"viewType"]; if ([viewType isEqualToString:@"complex"]) { // 复杂视图用DI方式 return [self.viperModuleProvider viperDemoViewController]; } else { // 简单视图用硬编码方式 return [[NSClassFromString(@"SimpleViewController") alloc] init]; } } // 智能跳转:根据用户权限选择不同页面 if ([routeId isEqualToString:@"smartJump"]) { BOOL isVip = [parameters[@"isVip"] boolValue]; if (isVip) { return [[NSClassFromString(@"VipViewController") alloc] init]; } else { return [[NSClassFromString(@"NormalViewController") alloc] init]; } } return nil; } #pragma mark - 重写协议方法,自定义路由行为 - (TJPRouterCreationStrategy)creationStrategyForRoute:(NSString *)routeId { // 为不同的路由指定不同的创建策略 // VIPER相关页面使用DI方式 if ([routeId isEqualToString:@"viperDemo"] || [routeId isEqualToString:@"viperDemoDetail"] || [routeId isEqualToString:@"newsDetail"]) { return TJPRouterCreationStrategyDI; } // 简单页面使用硬编码方式 if ([routeId isEqualToString:@"simpleSettings"] || [routeId isEqualToString:@"webView"] || [routeId isEqualToString:@"imagePreview"] || [routeId isEqualToString:@"userProfile"] || [routeId isEqualToString:@"productDetail"] || [routeId isEqualToString:@"alert"]) { return TJPRouterCreationStrategyHardcode; } // 动态页面使用工厂模式 if ([routeId isEqualToString:@"dynamicView"] || [routeId isEqualToString:@"smartJump"]) { return TJPRouterCreationStrategyFactory; } // 其他使用默认策略 return [super creationStrategyForRoute:routeId]; } - (BOOL)validateRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { // 自定义验证逻辑 // 详情页必须有ID参数 if ([routeId isEqualToString:@"viperDemoDetail"]) { return parameters[@"detailId"] != nil; } if ([routeId isEqualToString:@"newsDetail"]) { return parameters[@"newsId"] != nil; } if ([routeId isEqualToString:@"userProfile"]) { return parameters[@"userId"] != nil; } if ([routeId isEqualToString:@"productDetail"]) { return parameters[@"productId"] != nil; } // WebView必须有URL参数 if ([routeId isEqualToString:@"webView"]) { return parameters[@"url"] != nil; } // 工厂模式页面需要特定参数 if ([routeId isEqualToString:@"dynamicView"]) { return parameters[@"viewType"] != nil; } // 其他使用默认验证 return [super validateRoute:routeId parameters:parameters]; } - (void)willNavigateToRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { // 导航前的统计埋点 NSLog(@"[Analytics] 准备导航到: %@, 参数: %@", routeId, parameters); // 可以在这里添加具体的统计代码 // [Analytics track:@"route_start" properties:@{@"route": routeId, @"params": parameters}]; } - (void)didNavigateToRoute:(NSString *)routeId success:(BOOL)success { // 导航完成后的统计 NSLog(@"[Analytics] 导航完成: %@, 成功: %@", routeId, success ? @"YES" : @"NO"); // 可以在这里添加具体的统计代码 // [Analytics track:@"route_end" properties:@{@"route": routeId, @"success": @(success)}]; } - (NSDictionary *)processParametersForRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { // 参数预处理:添加通用参数 NSMutableDictionary *processedParams = [parameters mutableCopy] ?: [NSMutableDictionary dictionary]; // 添加通用参数 processedParams[@"timestamp"] = @([[NSDate date] timeIntervalSince1970]); processedParams[@"source"] = @"ViperDemo"; // 特殊路由的参数处理 if ([routeId isEqualToString:@"viperDemoDetail"]) { // 为详情页添加默认标题 if (!processedParams[@"title"]) { processedParams[@"title"] = @"VIPER详情页"; } } if ([routeId isEqualToString:@"newsDetail"]) { // 新闻详情页参数处理 if (!processedParams[@"title"]) { processedParams[@"title"] = @"新闻详情"; } } return processedParams; } - (void)configureViewController:(UIViewController *)viewController forRoute:(NSString *)routeId parameters:(NSDictionary *)parameters { // 先调用父类的默认配置(KVC参数注入) [super configureViewController:viewController forRoute:routeId parameters:parameters]; // 自定义配置逻辑 // 详情页隐藏底部TabBar if ([routeId isEqualToString:@"viperDemoDetail"] || [routeId isEqualToString:@"newsDetail"] || [routeId isEqualToString:@"userProfile"] || [routeId isEqualToString:@"productDetail"]) { viewController.hidesBottomBarWhenPushed = YES; } // 设置导航栏标题 if (parameters[@"title"]) { viewController.title = parameters[@"title"]; } // WebView特殊配置 if ([routeId isEqualToString:@"webView"]) { viewController.title = @"网页"; // 可以在这里设置WebView的其他属性 } // 设置页面配置 if ([routeId isEqualToString:@"simpleSettings"]) { viewController.title = @"设置"; } } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/DetailVC/TJPNewsDetailTableViewController.h ================================================ // // TJPNewsDetailTableViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/8/29. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPNewsDetailTableViewController : UIViewController @property (nonatomic, copy) NSString *titleStr; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/DetailVC/TJPNewsDetailTableViewController.m ================================================ // // TJPNewsDetailTableViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/8/29. // #import "TJPNewsDetailTableViewController.h" @interface TJPNewsDetailTableViewController () @end @implementation TJPNewsDetailTableViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.title = self.titleStr; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/TJPLikeCommentAreaView.h ================================================ // // TJPLikeCommentAreaView.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/30. // #import #import "TJPUserDynamicCellModel.h" NS_ASSUME_NONNULL_BEGIN @interface TJPLikeCommentAreaView : UIView @property (nonatomic, strong) NSArray *likeUsers; @property (nonatomic, strong) NSArray *commentList; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/TJPLikeCommentAreaView.m ================================================ // // TJPLikeCommentAreaView.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/30. // #import "TJPLikeCommentAreaView.h" #import @implementation TJPLikeCommentAreaView - (instancetype)initWithFrame:(CGRect)frame { self = [super initWithFrame:frame]; if (self) { [self setupUI]; } return self; } - (void)setupUI { self.backgroundColor = [UIColor colorWithRed:0.95 green:0.95 blue:0.95 alpha:1.0]; self.layer.cornerRadius = 4; } - (void)setLikeUsers:(NSArray *)likeUsers { _likeUsers = likeUsers; [self updateLayout]; } - (void)setCommentList:(NSArray *)commentList { _commentList = commentList; [self updateLayout]; } - (void)updateLayout { // 清除所有子视图 for (UIView *subview in self.subviews) { [subview removeFromSuperview]; } if (self.likeUsers.count == 0 && self.commentList.count == 0) { return; } UIView *lastView = nil; CGFloat currentY = 8; // 顶部间距 // 添加点赞区域 if (self.likeUsers.count > 0) { UILabel *likeLabel = [self createLikeLabel]; [self addSubview:likeLabel]; [likeLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self).insets(UIEdgeInsetsMake(0, 8, 0, 8)); make.top.equalTo(self).offset(currentY); make.height.equalTo(@25); }]; lastView = likeLabel; currentY += 25; } // 添加分割线 if (self.likeUsers.count > 0 && self.commentList.count > 0) { UIView *separatorLine = [[UIView alloc] init]; separatorLine.backgroundColor = [UIColor colorWithWhite:0.8 alpha:1.0]; [self addSubview:separatorLine]; [separatorLine mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self).insets(UIEdgeInsetsMake(0, 8, 0, 8)); make.top.equalTo(lastView.mas_bottom).offset(5); make.height.equalTo(@0.5); }]; lastView = separatorLine; currentY += 10; // 4 + 0.5 + 5.5 } // 添加评论区域 for (TJPUserComentModel *comment in self.commentList) { UILabel *commentLabel = [self createCommentLabel:comment]; [self addSubview:commentLabel]; [commentLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.left.right.equalTo(self).insets(UIEdgeInsetsMake(0, 8, 0, 8)); if (lastView) { make.top.equalTo(lastView.mas_bottom).offset(8); } else { make.top.equalTo(self).offset(currentY); } }]; lastView = commentLabel; // 添加点击手势 commentLabel.userInteractionEnabled = YES; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(commentTapped:)]; [commentLabel addGestureRecognizer:tap]; } } - (UILabel *)createLikeLabel { UILabel *label = [[UILabel alloc] init]; label.font = [UIFont systemFontOfSize:13]; label.textColor = [UIColor colorWithRed:0.3 green:0.5 blue:0.8 alpha:1.0]; NSMutableString *likeText = [NSMutableString stringWithString:@"❤️ "]; NSMutableArray *userNames = [NSMutableArray array]; for (TJPUserLikeModel *user in self.likeUsers) { [userNames addObject:user.userName]; } [likeText appendString:[userNames componentsJoinedByString:@","]]; label.text = likeText; return label; } - (UILabel *)createCommentLabel:(TJPUserComentModel *)comment { UILabel *label = [[UILabel alloc] init]; label.font = [UIFont systemFontOfSize:13]; label.numberOfLines = 0; NSString *userName = comment.userName; NSString *content = comment.content; NSString *replyTo = comment.replyTo; NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] init]; // 用户名(蓝色) NSAttributedString *userNameAttr = [[NSAttributedString alloc] initWithString:userName attributes:@{NSForegroundColorAttributeName: [UIColor colorWithRed:0.3 green:0.5 blue:0.8 alpha:1.0]}]; [attributedText appendAttributedString:userNameAttr]; // 回复标识 if (replyTo.length > 0) { NSAttributedString *replyAttr = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" 回复 %@", replyTo] attributes:@{NSForegroundColorAttributeName: [UIColor colorWithRed:0.3 green:0.5 blue:0.8 alpha:1.0]}]; [attributedText appendAttributedString:replyAttr]; } // 评论内容 NSAttributedString *contentAttr = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@": %@", content] attributes:@{NSForegroundColorAttributeName: [UIColor darkGrayColor]}]; [attributedText appendAttributedString:contentAttr]; label.attributedText = attributedText; return label; } - (void)commentTapped:(UITapGestureRecognizer *)tap { NSLog(@"点击了评论区域"); // 这里可以实现评论回复功能 } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/TJPNineGridImageView.h ================================================ // // TJPNineGridImageView.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/30. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPNineGridImageView : UIView @property (nonatomic, strong) NSArray *imageUrls; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/TJPNineGridImageView.m ================================================ // // TJPNineGridImageView.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/7/30. // #import "TJPNineGridImageView.h" #import #import @implementation TJPNineGridImageView - (void)setImageUrls:(NSArray *)imageUrls { _imageUrls = imageUrls; // 清除旧的图片视图 for (UIView *subview in self.subviews) { [subview removeFromSuperview]; } if (imageUrls.count == 0) { return; } [self layoutImageViews]; } - (void)layoutImageViews { CGFloat itemWidth = 80; CGFloat itemSpacing = 5; for (NSInteger i = 0; i < self.imageUrls.count; i++) { UIImageView *imageView = [[UIImageView alloc] init]; imageView.contentMode = UIViewContentModeScaleAspectFill; imageView.clipsToBounds = YES; imageView.layer.cornerRadius = 4; imageView.backgroundColor = [UIColor lightGrayColor]; [self addSubview:imageView]; // 计算九宫格位置 NSInteger row = i / 3; NSInteger col = i % 3; [imageView mas_makeConstraints:^(MASConstraintMaker *make) { make.left.equalTo(self).offset(col * (itemWidth + itemSpacing)); make.top.equalTo(self).offset(row * (itemWidth + itemSpacing)); make.width.height.equalTo(@(itemWidth)); }]; // 加载图片 [imageView sd_setImageWithURL:[NSURL URLWithString:self.imageUrls[i]]]; // 添加点击手势 imageView.userInteractionEnabled = YES; UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(imageViewTapped:)]; imageView.tag = i; [imageView addGestureRecognizer:tap]; } } - (void)imageViewTapped:(UITapGestureRecognizer *)tap { NSInteger index = tap.view.tag; NSLog(@"点击了第 %ld 张图片", (long)index); // 现图片浏览器功能 } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/TJPVIPERDemoDetailViewController.h ================================================ // // TJPVIPERDemoDetailViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/1. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPVIPERDemoDetailViewController : UIViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/TJPVIPERDemoDetailViewController.m ================================================ // // TJPVIPERDemoDetailViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/1. // #import "TJPVIPERDemoDetailViewController.h" @interface TJPVIPERDemoDetailViewController () @end @implementation TJPVIPERDemoDetailViewController - (void)dealloc { NSLog(@"%@ dealloc", NSStringFromClass([self class])); } - (void)viewDidLoad { [super viewDidLoad]; self.title = @"VIPER DEMO详情页"; self.view.backgroundColor = [UIColor whiteColor]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/TJPVIPERDemoViewController.h ================================================ // // TJPVIPERDemoViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/1. // #import "TJPViperBaseTableViewController.h" NS_ASSUME_NONNULL_BEGIN @interface TJPVIPERDemoViewController : TJPViperBaseTableViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ArchitectureExtensions/VIPER-Integration/VIPER-Demo/Demo/View/TJPVIPERDemoViewController.m ================================================ // // TJPVIPERDemoViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/1. // #import "TJPVIPERDemoViewController.h" #import "TJPFPSLabel.h" @interface TJPVIPERDemoViewController () @end @implementation TJPVIPERDemoViewController - (void)dealloc { NSLog(@"%@ dealloc", NSStringFromClass([self class])); } - (void)viewDidLoad { [super viewDidLoad]; TJPFPSLabel *fpsLabel = [[TJPFPSLabel alloc] initWithFrame:CGRectMake(20, 100, 80, 30)]; [self.view addSubview:fpsLabel]; self.title = @"多类型Feed流应用 - VIPER架构实战"; self.view.backgroundColor = [UIColor whiteColor]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: iOS-Network-Stack-Dive/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "imag_app_icon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "imag_app_icon 1.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "tinted" } ], "filename" : "imag_app_icon 2.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: iOS-Network-Stack-Dive/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: iOS-Network-Stack-Dive/Base.lproj/LaunchScreen.storyboard ================================================ ================================================ FILE: iOS-Network-Stack-Dive/Base.lproj/Main.storyboard ================================================ ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Bulider/TJPMessageBuilder.h ================================================ // // TJPMessageBuilder.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/14. // 消息组装类 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @interface TJPMessageBuilder : NSObject /// 组装数据包 + (NSData *)buildPacketWithMessageType:(TJPMessageType)msgType sequence:(uint32_t)sequence payload:(NSData *)payload encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType sessionID:(NSString *)sessionID; + (uint16_t)sessionIDFromUUID:(NSString *)uuidString; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Bulider/TJPMessageBuilder.m ================================================ // // TJPMessageBuilder.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/14. // #import "TJPMessageBuilder.h" #import "TJPNetworkDefine.h" #import "TJPNetworkUtil.h" @implementation TJPMessageBuilder + (NSData *)buildPacketWithMessageType:(TJPMessageType)msgType sequence:(uint32_t)sequence payload:(NSData *)payload encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType sessionID:(NSString *)sessionID { if (!payload) { payload = [NSData data]; // 空载荷使用空数据 } // 确保数据大小不超过限制 if (payload.length > TJPMAX_BODY_SIZE) { TJPLOG_ERROR(@"负载数据过大: %lu > %d", (unsigned long)payload.length, TJPMAX_BODY_SIZE); return nil; } // 初始化协议头 TJPFinalAdavancedHeader header = {0}; //网络字节序转换 header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(msgType); header.sequence = htonl(sequence); header.timestamp = htonl((uint32_t)[[NSDate date] timeIntervalSince1970]); // 当前时间戳 header.encrypt_type = encryptType; header.compress_type = compressType; header.session_id = htons([self sessionIDFromUUID:sessionID]); header.bodyLength = htonl((uint32_t)payload.length); // 计算数据体的CRC32 uint32_t checksum = 0; if (payload.length > 0) { checksum = [TJPNetworkUtil crc32ForData:payload]; } header.checksum = htonl(checksum); // 注意要转换为网络字节序 // 构建完整协议包 NSMutableData *packet = [NSMutableData dataWithBytes:&header length:sizeof(header)]; [packet appendData:payload]; return packet; } // 从UUID字符串生成16位会话ID + (uint16_t)sessionIDFromUUID:(NSString *)uuidString { if (!uuidString || uuidString.length == 0) { return 0; } // 移除UUID中的"-"字符 NSString *cleanUUID = [uuidString stringByReplacingOccurrencesOfString:@"-" withString:@""]; // 取UUID的前8个字符,转换为32位整数 NSString *prefix = [cleanUUID substringToIndex:MIN(8, cleanUUID.length)]; unsigned int value = 0; [[NSScanner scannerWithString:prefix] scanHexInt:&value]; // 折叠32位值为16位:XOR高16位和低16位 uint16_t highPart = (uint16_t)(value >> 16); uint16_t lowPart = (uint16_t)(value & 0xFFFF); return highPart ^ lowPart; // XOR操作保持更好的分布 } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Connect/TJPConnectionManager.h ================================================ // // TJPConnectionManager.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/15. // #import #import "TJPConnectionDelegate.h" #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @interface TJPConnectionManager : NSObject @property (nonatomic, weak) id delegate; /// 内部状态 @property (nonatomic, readonly) TJPConnectionState internalState; /// 当前主机 @property (nonatomic, readonly) NSString *currentHost; /// 当前端口 @property (nonatomic, readonly) uint16_t currentPort; /// 断开原因 @property (nonatomic, readonly) TJPDisconnectReason disconnectReason; /// 使用TLS 默认为NO方便单元测试 @property (nonatomic, assign) BOOL useTLS; /// 连接时限窗口 默认30秒 @property (nonatomic, assign) NSTimeInterval connectionTimeout; /// 标志位 @property (nonatomic, readonly) BOOL isConnected; @property (nonatomic, readonly) BOOL isConnecting; /// 初始化方法 - (instancetype)initWithDelegateQueue:(dispatch_queue_t)delegateQueue; /// 连接方法 - (void)connectToHost:(NSString *)host port:(uint16_t)port; /// 断开连接方法 - (void)disconnect; /// 强制断开连接方法 - (void)forceDisconnect; /// 断开连接原因 - (void)disconnectWithReason:(TJPDisconnectReason)reason; /// 发送消息 - (void)sendData:(NSData *)data; /// 带超时的发送消息 - (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag; /// 开始TLS - (void)startTLS:(NSDictionary *)settings; /// 首次握手版本协商 - (void)setVersionInfo:(uint8_t)majorVersion minorVersion:(uint8_t)minorVersion; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Connect/TJPConnectionManager.m ================================================ // // TJPConnectionManager.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/15. // #import "TJPConnectionManager.h" #import #import "TJPNetworkDefine.h" #import "TJPConnectStateMachine.h" @interface TJPConnectionManager () @property (nonatomic, strong) GCDAsyncSocket *socket; @property (nonatomic, strong) dispatch_queue_t socketQueue; @property (nonatomic, copy) NSString *currentHost; @property (nonatomic, assign) uint16_t currentPort; @property (nonatomic, assign) TJPDisconnectReason disconnectReason; @property (nonatomic, strong) dispatch_source_t connectionTimeoutTimer; @property (nonatomic, assign) TJPConnectionState internalState; @property (nonatomic, assign) uint8_t majorVersion; @property (nonatomic, assign) uint8_t minorVersion; @end @implementation TJPConnectionManager - (instancetype)initWithDelegateQueue:(dispatch_queue_t)delegateQueue { if (self = [super init]) { _socketQueue = delegateQueue ?: dispatch_queue_create("com.connectionManager.tjp.socketQueue", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(_socketQueue, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)); _internalState = TJPConnectionStateDisconnected; _disconnectReason = TJPDisconnectReasonNone; _connectionTimeout = 30.0; // 默认超时时间 _useTLS = NO; // 默认不使用TLS _majorVersion = kProtocolVersionMajor; _minorVersion = kProtocolVersionMinor; } return self; } - (void)dealloc { TJPLOG_INFO(@"🚨 [TJPConnectionManager] 开始释放 ConnectionManager"); // 🔥 关键修复:立即清理 socket delegate,防止野指针回调 if (self.socket) { TJPLOG_INFO(@"🚨 [TJPConnectionManager] 清理 socket delegate"); // 在释放前先移除delegate,防止socket回调已释放的对象 self.socket.delegate = nil; self.socket.delegateQueue = nil; // 强制断开socket连接 [self.socket disconnect]; self.socket = nil; } // 取消定时器 [self cancelConnectionTimeoutTimer]; TJPLOG_INFO(@"🚨 [TJPConnectionManager] ConnectionManager 释放完成"); } #pragma mark - Properties - (BOOL)isConnected { return self.internalState == TJPConnectionStateConnected; } - (BOOL)isConnecting { return self.internalState == TJPConnectionStateConnecting; } #pragma mark - State Management - (void)setInternalState:(TJPConnectionState)newState { if (_internalState == newState) return; TJPConnectionState oldState = _internalState; _internalState = newState; TJPLOG_INFO(@"[TJPConnectionManager] 连接管理器状态变化: %d -> %d", (int)oldState, (int)newState); // 这里可以添加更复杂的状态监控和日志记录逻辑 } #pragma mark - Public Methods - (void)connectToHost:(NSString *)host port:(uint16_t)port { dispatch_async(self.socketQueue, ^{ if (self.internalState != TJPConnectionStateDisconnected) { TJPLOG_INFO(@"[TJPConnectionManager] 当前已有连接或正在连接中,无法发起新连接"); return; } if (host.length == 0) { TJPLOG_ERROR(@"[TJPConnectionManager] 主机地址不能为空,请检查!!"); return; } self.currentHost = host; self.currentPort = port; self.disconnectReason = TJPDisconnectReasonNone; // 更新内部状态 [self setInternalState:TJPConnectionStateConnecting]; // 通知代理将要连接 if ([self.delegate respondsToSelector:@selector(connectionWillConnect:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate connectionWillConnect:self]; }); } // 创建新的Socket实例 self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self.socketQueue]; // 执行连接操作 NSError *error = nil; if (![self.socket connectToHost:host onPort:port error:&error]) { [self handleError:error withReason:TJPDisconnectReasonSocketError]; return; } // 启动连接超时计时器 [self startConnectionTimeoutTimer]; }); } - (void)disconnect { [self disconnectWithReason:TJPDisconnectReasonUserInitiated]; } - (void)forceDisconnect { dispatch_async(self.socketQueue, ^{ TJPLOG_INFO(@"[TJPConnectionManager] 连接管理器强制断开"); // 立即关闭socket,不等待优雅断开 if (self.socket) { [self.socket disconnect]; self.socket = nil; } // 立即触发断开回调 if (self.delegate && [self.delegate respondsToSelector:@selector(connection:didDisconnectWithError:reason:)]) { NSError *error = [NSError errorWithDomain:@"TJPConnectionManager" code:-1 userInfo:@{NSLocalizedDescriptionKey: @"Force disconnect"}]; [self.delegate connection:self didDisconnectWithError:error reason:TJPDisconnectReasonForceReconnect]; } }); } - (void)disconnectWithReason:(TJPDisconnectReason)reason { if (!self) { TJPLOG_INFO(@"[TJPConnectionManager] self 为 nil,直接返回"); return; } // 打印调用栈,找出谁调用了这个方法 // NSArray *callStack = [NSThread callStackSymbols]; // TJPLOG_INFO(@"📞 [ConnectionManager] disconnect 调用栈:"); // for (NSInteger i = 0; i < MIN(callStack.count, 8); i++) { // TJPLOG_INFO(@"📞 %ld: %@", (long)i, callStack[i]); // } dispatch_async(self.socketQueue, ^{ if (!self) { TJPLOG_INFO(@"[TJPConnectionManager] 异步执行时 self 无效"); return; } @try { if (self.internalState == TJPConnectionStateDisconnected) { TJPLOG_INFO(@"[TJPConnectionManager] 已经是断开状态,跳过"); return; } } @catch (NSException *exception) { TJPLOG_INFO(@"[TJPConnectionManager] 访问 internalState 异常: %@", exception.reason); return; } [self cancelConnectionTimeoutTimer]; self.disconnectReason = reason; // 更新内部状态 [self setInternalState:TJPConnectionStateDisconnecting]; // 通知代理将要断开 if ([self.delegate respondsToSelector:@selector(connectionWillDisconnect:reason:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate connectionWillDisconnect:self reason:reason]; }); } if (self.socket) { [self.socket disconnect]; } }); } - (void)sendData:(NSData *)data { [self sendData:data withTimeout:-1 tag:0]; } - (void)sendData:(NSData *)data withTimeout:(NSTimeInterval)timeout tag:(long)tag { dispatch_async(self.socketQueue, ^{ if (self.internalState != TJPConnectionStateConnected) { TJPLOG_WARN(@"[TJPConnectionManager] 当前未连接,无法发送数据"); return; } [self.socket writeData:data withTimeout:timeout tag:tag]; }); } - (void)startTLS:(NSDictionary *)settings { dispatch_async(self.socketQueue, ^{ if (self.internalState != TJPConnectionStateConnected) { TJPLOG_WARN(@"[TJPConnectionManager] 当前未连接,无法启动TLS"); return; } [self.socket startTLS:settings ?: @{ (NSString *)kCFStreamSSLPeerName: self.currentHost }]; }); } - (void)setVersionInfo:(uint8_t)majorVersion minorVersion:(uint8_t)minorVersion { dispatch_async(self.socketQueue, ^{ self.majorVersion = majorVersion; self.minorVersion = minorVersion; }); } #pragma mark - Private Methods - (void)handleError:(NSError *)error withReason:(TJPDisconnectReason)reason { self.disconnectReason = reason; [self setInternalState:TJPConnectionStateDisconnected]; if ([self.delegate respondsToSelector:@selector(connection:didDisconnectWithError:reason:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate connection:self didDisconnectWithError:error reason:reason]; }); } } - (void)startConnectionTimeoutTimer { [self cancelConnectionTimeoutTimer]; self.connectionTimeoutTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.socketQueue); dispatch_source_set_timer(self.connectionTimeoutTimer, dispatch_time(DISPATCH_TIME_NOW, (int64_t)(self.connectionTimeout * NSEC_PER_SEC)), DISPATCH_TIME_FOREVER, (1ull * NSEC_PER_SEC) / 10); __weak typeof(self) weakSelf = self; dispatch_source_set_event_handler(self.connectionTimeoutTimer, ^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; if (strongSelf.internalState == TJPConnectionStateConnecting) { TJPLOG_ERROR(@"[TJPConnectionManager] 连接超时(%0.1f秒)", strongSelf.connectionTimeout); [strongSelf cancelConnectionTimeoutTimer]; [strongSelf disconnectWithReason:TJPDisconnectReasonConnectionTimeout]; } }); dispatch_resume(self.connectionTimeoutTimer); } - (void)cancelConnectionTimeoutTimer { if (self.connectionTimeoutTimer) { dispatch_source_cancel(self.connectionTimeoutTimer); self.connectionTimeoutTimer = nil; } } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { [self cancelConnectionTimeoutTimer]; [self setInternalState:TJPConnectionStateConnected]; if ([self.delegate respondsToSelector:@selector(connectionDidConnect:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate connectionDidConnect:self]; }); } // 如果需要TLS,自动启动 if (self.useTLS) { [self startTLS:nil]; } // 开始读取数据 [sock readDataWithTimeout:-1 tag:0]; } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { if ([self.delegate respondsToSelector:@selector(connection:didReceiveData:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate connection:self didReceiveData:data]; }); } // 继续读取数据 [sock readDataWithTimeout:-1 tag:0]; } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { TJPDisconnectReason reason = self.disconnectReason; // 如果没有明确设置断开原因,根据错误确定原因 if (reason == TJPDisconnectReasonNone && err) { if ([err.domain isEqualToString:NSPOSIXErrorDomain]) { switch (err.code) { case ETIMEDOUT: reason = TJPDisconnectReasonConnectionTimeout; break; case ECONNREFUSED: reason = TJPDisconnectReasonSocketError; break; case ENETDOWN: case ENETUNREACH: reason = TJPDisconnectReasonNetworkError; break; default: reason = TJPDisconnectReasonSocketError; break; } } else if ([err.domain isEqualToString:NSURLErrorDomain]) { switch (err.code) { case NSURLErrorNotConnectedToInternet: case NSURLErrorNetworkConnectionLost: reason = TJPDisconnectReasonNetworkError; break; case NSURLErrorTimedOut: reason = TJPDisconnectReasonConnectionTimeout; break; default: reason = TJPDisconnectReasonSocketError; break; } } else { reason = TJPDisconnectReasonSocketError; } } self.disconnectReason = reason; [self setInternalState:TJPConnectionStateDisconnected]; if ([self.delegate respondsToSelector:@selector(connection:didDisconnectWithError:reason:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate connection:self didDisconnectWithError:err reason:reason]; }); } } - (void)socketDidSecure:(GCDAsyncSocket *)sock { if ([self.delegate respondsToSelector:@selector(connectionDidSecure:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate connectionDidSecure:self]; }); } } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Container/TJPConcreteSession.h ================================================ // // TJPConcreteSession.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // 会话类 核心之一 #import #import "TJPSessionProtocol.h" #import "TJPSessionDelegate.h" NS_ASSUME_NONNULL_BEGIN @class TJPNetworkConfig, TJPConnectStateMachine, TJPMessageContext, TJPReconnectPolicy, TJPConnectionManager; @interface TJPConcreteSession : NSObject @property (nonatomic, weak) id delegate; /// 独立的会话id @property (nonatomic, copy) NSString *sessionId; /// 会话类型 @property (nonatomic, assign) TJPSessionType sessionType; /// 配置 @property (nonatomic, strong) TJPNetworkConfig *config; /// 连接状态机 @property (nonatomic, strong) TJPConnectStateMachine *stateMachine; /// 重试策略 @property (nonatomic, strong) TJPReconnectPolicy *reconnectPolicy; /// 待ACK确认消息 改为以消息ID为key @property (nonatomic, strong) NSMutableDictionary *pendingMessages; /// 序列号到消息ID映射 仅用于接收ACK时反向查找 @property (nonatomic, strong) NSMutableDictionary *sequenceToMessageId; /// 断开原因 @property (nonatomic, assign) TJPDisconnectReason disconnectReason; /// 重连标志符 @property (atomic, assign) BOOL isReconnecting; /// 是否允许自动重连 默认开启 @property (nonatomic, assign) BOOL autoReconnectEnabled; // 创建时间 @property (nonatomic, readonly) NSDate *createdTime; /// 初始化方法 - (instancetype)initWithConfiguration:(TJPNetworkConfig *)config; //***************************************************** // 埋点统计 具体实现看TJPConcreteSession+TJPMetrics.h 通过hook相关方法增加埋点 - (void)handleACKForSequence:(uint32_t)sequence; - (void)disconnectWithReason:(TJPDisconnectReason)reason; - (void)connection:(TJPConnectionManager *)connection didDisconnectWithError:(NSError *)error reason:(TJPDisconnectReason)reason; - (void)handleRetransmissionForSequence:(uint32_t)sequence; - (void)performVersionHandshake; //***************************************************** //***************************************************** // 多路复用支持相关 /// 最后活跃时间 @property (nonatomic, strong) NSDate *lastActiveTime; /// 最后释放时间 @property (nonatomic, strong) NSDate *lastReleaseTime; /// 使用次数 @property (nonatomic, assign) NSUInteger useCount; /// 是否在池中 @property (nonatomic, assign) BOOL isPooled; - (void)resetForReuse; - (BOOL)checkHealthyForSession; //***************************************************** @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Container/TJPConcreteSession.m ================================================ // // TJPConcreteSession.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #import "TJPConcreteSession.h" #import #import #import "TJPNetworkConfig.h" #import "TJPNetworkDefine.h" #import "TJPErrorUtil.h" #import "TJPNetworkCoordinator.h" #import "TJPReconnectPolicy.h" #import "TJPDynamicHeartbeat.h" #import "TJPMessageParser.h" #import "TJPMessageBuilder.h" #import "TJPMessageContext.h" #import "TJPParsedPacket.h" #import "TJPMessageManager.h" #import "TJPSequenceManager.h" #import "TJPNetworkUtil.h" #import "TJPConnectStateMachine.h" #import "TJPNetworkCondition.h" #import "TJPMetricsConsoleReporter.h" #import "TJPConnectionDelegate.h" #import "TJPConnectionManager.h" #import "TJPMessageStateMachine.h" static const NSTimeInterval kDefaultRetryInterval = 10; @interface TJPConcreteSession () @property (nonatomic, copy) NSString *host; @property (nonatomic, assign) uint16_t port; @property (nonatomic, strong) TJPConnectionManager *connectionManager; @property (nonatomic, strong) dispatch_queue_t sessionQueue; //消息超时重传定时器 @property (nonatomic, strong) NSMutableDictionary *retransmissionTimers; /// 动态心跳 @property (nonatomic, strong) TJPDynamicHeartbeat *heartbeatManager; /// 序列号管理 @property (nonatomic, strong) TJPSequenceManager *seqManager; /// 协议处理 @property (nonatomic, strong) TJPMessageParser *parser; /// 消息管理 @property (nonatomic, strong) TJPMessageManager *messageManager; /* 版本协商规则 */ //上次握手时间 @property (nonatomic, strong) NSDate *lastHandshakeTime; //断开连接事件 @property (nonatomic, strong) NSDate *disconnectionTime; //是否完成握手 @property (nonatomic, assign) BOOL hasCompletedHandshake; //协商后的版本号 @property (nonatomic, assign) uint16_t negotiatedVersion; //协商后的特性标志 @property (nonatomic, assign) uint16_t negotiatedFeatures; /* Debug */ @property (nonatomic, assign) BOOL hasSetupComponents; @end @implementation TJPConcreteSession - (void)dealloc { TJPLOG_INFO(@"🚨 [CRITICAL] 会话 %@ 开始释放", _sessionId ?: @"unknown"); // NSArray *callStack = [NSThread callStackSymbols]; // TJPLOG_INFO(@"🚨 [CRITICAL] 调用栈:"); // for (NSInteger i = 0; i < MIN(callStack.count, 10); i++) { // TJPLOG_INFO(@"🚨 [CRITICAL] %ld: %@", (long)i, callStack[i]); // } // 清理定时器 [self cancelAllRetransmissionTimersSync]; [self prepareForRelease]; TJPLOG_INFO(@"🚨 [CRITICAL] 会话 %@ 释放完成", _sessionId ?: @"unknown"); } #pragma mark - Lifecycle - (instancetype)initWithConfiguration:(TJPNetworkConfig *)config { TJPLOG_INFO(@"[TJPConcreteSession] 通过配置:%@ 开始初始化", config); if (self = [super init]) { _createdTime = [NSDate date]; _config = config; _autoReconnectEnabled = YES; _sessionId = [[NSUUID UUID] UUIDString]; _disconnectReason = TJPDisconnectReasonNone; _retransmissionTimers = [NSMutableDictionary dictionary]; _pendingMessages = [NSMutableDictionary dictionary]; _sequenceToMessageId = [NSMutableDictionary dictionary]; // 创建专用队列(串行,中等优先级) _sessionQueue = dispatch_queue_create("com.concreteSession.tjp.sessionQueue", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(_sessionQueue, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)); // 初始化各组件 [self setupComponentWithConfig:config]; // 注册心跳超时通知 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleHeartbeatTimeout:) name:kHeartbeatTimeoutNotification object:nil]; TJPLOG_INFO(@"[TJPConcreteSession] 初始化完成: %@", _sessionId); } return self; } - (void)setupComponentWithConfig:(TJPNetworkConfig *)config { // 检查是否已经设置过 if (self.hasSetupComponents) { TJPLOG_WARN(@"[WARNING] setupComponentWithConfig 已经执行过,跳过重复执行"); TJPLOG_WARN(@"⚠️ [WARNING] 调用栈: %@", [NSThread callStackSymbols]); return; } // 设置标志位 self.hasSetupComponents = YES; TJPLOG_DEBUG(@"[TJPConcreteSession] 开始初始化组件..."); // 初始化状态机(初始状态:断开连接) _stateMachine = [[TJPConnectStateMachine alloc] initWithInitialState:TJPConnectStateDisconnected setupStandardRules:YES]; [self setupStateMachine]; TJPLOG_DEBUG(@"[TJPConcreteSession] 状态机初始化完成: %@", _stateMachine); // 初始化连接管理器 _connectionManager = [[TJPConnectionManager alloc] initWithDelegateQueue:_sessionQueue]; _connectionManager.delegate = self; _connectionManager.connectionTimeout = 30.0; _connectionManager.useTLS = config.useTLS; TJPLOG_DEBUG(@"[TJPConcreteSession] 连接管理器初始化完成: %@", _connectionManager); // 初始化序列号管理 _seqManager = [[TJPSequenceManager alloc] initWithSessionId:_sessionId]; // 设置重置回调 __weak typeof(self) weakSelf = self; _seqManager.sequenceResetHandler = ^(TJPMessageCategory category) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; [strongSelf handleSequenceReset:category]; }; TJPLOG_DEBUG(@"[TJPConcreteSession] 序列号管理器初始化完成: %@", _seqManager); // 初始化协议解析器 _parser = [[TJPMessageParser alloc] initWithBufferStrategy:TJPBufferStrategyAuto]; TJPLOG_DEBUG(@"[TJPConcreteSession] 协议解析器初始化完成: %@", _parser); // 初始化重连策略 _reconnectPolicy = [[TJPReconnectPolicy alloc] initWithMaxAttempst:config.maxRetry baseDelay:config.baseDelay qos:TJPNetworkQoSDefault delegate:self]; TJPLOG_DEBUG(@"[TJPConcreteSession] 重连策略初始化完成: %@", _reconnectPolicy); // 初始化消息管理器 _messageManager = [[TJPMessageManager alloc] initWithSessionId:_sessionId]; _messageManager.delegate = self; _messageManager.networkDelegate = self; TJPLOG_DEBUG(@"[TJPConcreteSession] 消息管理器初始化完成: %@", _messageManager); TJPLOG_DEBUG(@"[TJPConcreteSession] setupComponentWithConfig 完成"); } - (void)ensureHeartbeatManagerInitialized { if (_heartbeatManager) { TJPLOG_DEBUG(@"[TJPConcreteSession] 心跳管理器已初始化,跳过"); return; } TJPLOG_INFO(@"[TJPConcreteSession] 延迟初始化心跳管理器: %@", self.sessionId); // 初始化心跳管理 _heartbeatManager = [[TJPDynamicHeartbeat alloc] initWithBaseInterval:self.config.heartbeat seqManager:_seqManager session:self]; // 自定义前台模式参数 [_heartbeatManager configureWithBaseInterval:30.0 minInterval:15.0 maxInterval:300.0 forMode:TJPHeartbeatModeForeground]; // 自定义后台模式参数 [_heartbeatManager configureWithBaseInterval:90.0 minInterval:45.0 maxInterval:600.0 forMode:TJPHeartbeatModeBackground]; TJPLOG_DEBUG(@"[TJPConcreteSession] 心跳管理器初始化完成: %@", _reconnectPolicy); } //制定转换规则 - (void)setupStateMachine { __weak typeof(self) weakSelf = self; // 设置无效转换处理器 [_stateMachine setInvalidTransitionHandler:^(TJPConnectState state, TJPConnectEvent event) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; TJPLOG_ERROR(@"[TJPConcreteSession] 会话 %@ 状态转换错误: %@ -> %@,尝试恢复", strongSelf.sessionId, state, event); // 尝试恢复逻辑 if ([event isEqualToString:TJPConnectEventConnect] && ![state isEqualToString:TJPConnectStateDisconnected]) { // 如果试图从非断开状态发起连接,先强制断开 [strongSelf.stateMachine sendEvent:TJPConnectEventForceDisconnect]; // 延迟后再尝试连接 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [strongSelf.stateMachine sendEvent:TJPConnectEventConnect]; }); } }]; // 设置状态变化监听 [_stateMachine onStateChange:^(TJPConnectState oldState, TJPConnectState newState) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; TJPLOG_INFO(@"[TJPConcreteSession] 会话 %@ 状态变化: %@ -> %@", strongSelf.sessionId, oldState, newState); // 通知代理 if (strongSelf.delegate && [strongSelf.delegate respondsToSelector:@selector(session:didChangeState:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [strongSelf.delegate session:strongSelf didChangeState:newState]; }); } // 根据新状态执行相应操作 if ([newState isEqualToString:TJPConnectStateConnecting]) { TJPLOG_INFO(@"[TJPConcreteSession] 开始连接,心跳管理器待命"); } else if ([newState isEqualToString:TJPConnectStateConnected]) { TJPLOG_INFO(@"[TJPConcreteSession] 连接成功,启动心跳监控"); // 此处只启动心跳 不初始化心跳 if (strongSelf.heartbeatManager) { [strongSelf.heartbeatManager updateSession:strongSelf]; TJPLOG_INFO(@"[TJPConcreteSession] 心跳已启动,当前间隔 %.1f 秒", strongSelf.heartbeatManager.currentInterval); } else { TJPLOG_ERROR(@"[TJPConcreteSession] 注意:心跳管理器未初始化,请检查心跳初始化逻辑!!!!"); } [strongSelf handleConnectedState]; } else if ([newState isEqualToString:TJPConnectStateDisconnecting]) { TJPLOG_INFO(@"[TJPConcreteSession] 开始断开连接"); // 状态改为开始断开就更新时间 [strongSelf handleDisconnectedState]; } else if ([newState isEqualToString:TJPConnectStateDisconnected]) { TJPLOG_INFO(@"[TJPConcreteSession] 连接已断开"); // 断开连接,停止心跳 [strongSelf handleDisconnectedState]; // 特殊处理强制断开后的逻辑 if (strongSelf.disconnectReason == TJPDisconnectReasonForceReconnect) { [strongSelf handleForceDisconnectComplete]; } } }]; } #pragma mark - TJPConnectionDelegate - (void)connectionWillConnect:(TJPConnectionManager *)connection { // 记录日志,不需要特殊处理 TJPLOG_INFO(@"[TJPConcreteSession] 连接即将建立"); } - (void)connectionDidConnect:(TJPConnectionManager *)connection { dispatch_async(self.sessionQueue, ^{ TJPLOG_INFO(@"[TJPConcreteSession] 连接成功,准备给状态机发送连接成功事件"); self.isReconnecting = NO; // 触发连接成功事件 状态转换为"已连接" [self.stateMachine sendEvent:TJPConnectEventConnectSuccess]; // 开始网络指标监控 [TJPMetricsConsoleReporter startWithConfig:self.config]; }); } - (void)connectionWillDisconnect:(TJPConnectionManager *)connection reason:(TJPDisconnectReason)reason { dispatch_async(self.sessionQueue, ^{ // 如果是从已连接状态断开,发送断开事件 if ([self.stateMachine.currentState isEqualToString:TJPConnectStateConnected]) { [self.stateMachine sendEvent:TJPConnectEventDisconnect]; } }); } - (void)connection:(TJPConnectionManager *)connection didDisconnectWithError:(NSError *)error reason:(TJPDisconnectReason)reason { dispatch_async(self.sessionQueue, ^{ self.isReconnecting = NO; // 保存断开原因,如果没有明确的原因,使用连接管理器的原因 if (self.disconnectReason == TJPDisconnectReasonNone) { self.disconnectReason = reason; } // 如果是从连接中状态断开,发送连接失败事件 if ([self.stateMachine.currentState isEqualToString:TJPConnectStateConnecting]) { [self.stateMachine sendEvent:TJPConnectEventConnectFailure]; } // 如果是从断开中状态断开,发送断开完成事件 else if ([self.stateMachine.currentState isEqualToString:TJPConnectStateDisconnecting]) { [self.stateMachine sendEvent:TJPConnectEventDisconnectComplete]; } // 如果是从已连接状态异常断开,发送网络错误事件后发送断开完成事件 else if ([self.stateMachine.currentState isEqualToString:TJPConnectStateConnected]) { [self.stateMachine sendEvent:TJPConnectEventNetworkError]; [self.stateMachine sendEvent:TJPConnectEventDisconnectComplete]; } // 清理资源 [self cleanupAfterDisconnect]; // 处理重连策略 [self handleReconnectionAfterDisconnect]; }); } - (void)connection:(TJPConnectionManager *)connection didReceiveData:(NSData *)data { dispatch_async([TJPNetworkCoordinator shared].parseQueue, ^{ TJPLOG_INFO(@"[TJPConcreteSession] 读取到数据,大小: %lu字节,准备解析", (unsigned long)data.length); // 使用解析器解析数据 [self.parser feedData:data]; int packetCount = 0; // 解析数据 while ([self.parser hasCompletePacket]) { packetCount++; TJPLOG_INFO(@"[TJPConcreteSession] 开始解析第 %d 个数据包", packetCount); TJPParsedPacket *packet = [self.parser nextPacket]; if (!packet) { TJPLOG_ERROR(@"[TJPConcreteSession] 第 %d 个数据包解析失败,TJPParsedPacket为空", packetCount); return; } TJPLOG_INFO(@"[TJPConcreteSession] 第 %d 个数据包解析成功 - 类型:%hu, 序列号:%u, 载荷大小:%lu", packetCount, packet.messageType, packet.sequence, (unsigned long)packet.payload.length); // 处理数据包 [self processReceivedPacket:packet]; } TJPLOG_INFO(@"[TJPConcreteSession] 本次数据解析完成,共处理 %d 个数据包", packetCount); }); } - (void)connectionDidSecure:(TJPConnectionManager *)connection { TJPLOG_INFO(@"[TJPConcreteSession] 连接已建立TLS安全层"); } #pragma mark - TJPSessionProtocol /// 连接方法 - (void)connectToHost:(NSString *)host port:(uint16_t)port { dispatch_async(self.sessionQueue, ^{ if (host.length == 0) { TJPLOG_ERROR(@"[TJPConcreteSession] 主机地址不能为空,请检查!!"); return; } self.host = host; self.port = port; //通过状态机检查当前状态 if (![self.stateMachine.currentState isEqualToString:TJPConnectStateDisconnected]) { TJPLOG_INFO(@"[TJPConcreteSession] 当前状态无法连接主机,当前状态为: %@", self.stateMachine.currentState); return; } TJPLOG_INFO(@"[TJPConcreteSession] 准备连接到 %@:%d", host, port); // 连接前的准备工作:确保心跳管理器已初始化 [self prepareForConnection]; // 触发连接事件 状态转换为"连接中" [self.stateMachine sendEvent:TJPConnectEventConnect]; // 使用连接管理器进行连接 职责拆分 session不再负责连接方法 [self.connectionManager connectToHost:host port:port]; }); } - (void)sendData:(NSData *)data { // 改为使用消息管理器 [self.messageManager sendMessage:data messageType:TJPMessageTypeNormalData completion:^(NSString * _Nonnull msgId, NSError * _Nonnull error) { if (error) { TJPLOG_ERROR(@"[TJPConcreteSession] 消息创建失败: %@", error); } else { TJPLOG_INFO(@"[TJPConcreteSession] 消息已创建: %@", msgId); } }]; } - (NSString *)sendData:(NSData *)data messageType:(TJPMessageType)messageType encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType completion:(void(^)(NSString *messageId, NSError *error))completion { return [self.messageManager sendMessage:data messageType:messageType encryptType:encryptType compressType:compressType completion:completion]; } /// 发送心跳包 - (void)sendHeartbeat:(NSData *)heartbeatData { dispatch_async(self.sessionQueue, ^{ if (![self.stateMachine.currentState isEqualToString:TJPConnectStateConnected]) { TJPLOG_INFO(@"[TJPConcreteSession] 当前状态发送心跳包失败, 当前状态为: %@", self.stateMachine.currentState); return; } TJPLOG_INFO(@"[TJPConcreteSession] 正在发送心跳包"); [self.connectionManager sendData:heartbeatData withTimeout:-1 tag:0]; }); } - (void)disconnectWithReason:(TJPDisconnectReason)reason { TJPLOG_INFO(@"[DISCONNECT] 会话 %@ 收到断开请求,原因: %d", self.sessionId ?: @"unknown", (int)reason); // 打印调用栈,找出是谁调用了断开 if (reason != TJPDisconnectReasonUserInitiated) { // 只在非用户主动断开时打印 NSArray *callStack = [NSThread callStackSymbols]; TJPLOG_INFO(@"📞 [DISCONNECT] 断开调用栈:"); for (NSInteger i = 0; i < MIN(callStack.count, 8); i++) { TJPLOG_INFO(@"📞 [DISCONNECT] %ld: %@", (long)i, callStack[i]); } } dispatch_async(self.sessionQueue, ^{ // 避免重复断开 if ([self.stateMachine.currentState isEqualToString:TJPConnectStateDisconnected]) { TJPLOG_INFO(@"[TJPConcreteSession] 当前已是断开状态,无需再次断开"); return; } //存储断开原因 self.disconnectReason = reason; // 状态转换为"断开中" [self.stateMachine sendEvent:TJPConnectEventDisconnect]; //使用管理器断开连接 [self.connectionManager disconnectWithReason:reason]; //停止心跳 [self.heartbeatManager stopMonitoring]; //清理资源 [self.pendingMessages removeAllObjects]; [self cancelAllRetransmissionTimers]; //停止监控 [TJPMetricsConsoleReporter stop]; //状态转换为"已断开连接" [self.stateMachine sendEvent:TJPConnectEventDisconnectComplete]; // 通知协调器处理可能的重连 if (reason == TJPDisconnectReasonNetworkError || reason == TJPDisconnectReasonHeartbeatTimeout || reason == TJPDisconnectReasonIdleTimeout) { if ([self.delegate respondsToSelector:@selector(sessionNeedsReconnect:)]) { [self.delegate sessionNeedsReconnect:self]; } } }); } - (void)disconnect { [self disconnectWithReason:TJPDisconnectReasonUserInitiated]; } - (void)updateConnectionState:(TJPConnectState)state { //事件驱动状态变更 TJPConnectEvent event = [self eventForTargetState:state]; if (event) { [self.stateMachine sendEvent:event]; } } - (TJPConnectState)connectState { return self.stateMachine.currentState; } - (void)forceReconnect { dispatch_async(self.sessionQueue, ^{ //重连之前确保连接断开 [self disconnectWithReason:TJPDisconnectReasonForceReconnect]; //延迟一点时间确保连接完全断开 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), self.sessionQueue, ^{ // 重置连接相关的状态 [self resetConnection]; // 重新连接 [self connectToHost:self.host port:self.port]; }); }); } - (void)networkDidBecomeAvailable { dispatch_async(self.sessionQueue, ^{ // 检查是否已经在重连 if (self.isReconnecting) { TJPLOG_INFO(@"[TJPConcreteSession] 已有重连过程在进行,忽略"); return; } // 只有当前状态为断开状态且启用了自动重连才尝试重连 if ([self.stateMachine.currentState isEqualToString:TJPConnectStateDisconnected] && self.autoReconnectEnabled && self.disconnectReason != TJPDisconnectReasonUserInitiated) { self.isReconnecting = YES; TJPLOG_INFO(@"[TJPConcreteSession] 网络恢复,尝试自动重连"); [self.reconnectPolicy attemptConnectionWithBlock:^{ [self connectToHost:self.host port:self.port]; }]; } }); } - (void)networkDidBecomeUnavailable { dispatch_async(self.sessionQueue, ^{ // 如果当前连接中或已连接,则标记为网络错误并断开 if ([self.stateMachine.currentState isEqualToString:TJPConnectStateConnecting] || [self.stateMachine.currentState isEqualToString:TJPConnectStateConnected]) { [self disconnectWithReason:TJPDisconnectReasonNetworkError]; } }); } - (void)prepareForRelease { [self.connectionManager disconnect]; [self.heartbeatManager stopMonitoring]; [TJPMetricsConsoleReporter stop]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)forceDisconnect { TJPLOG_INFO(@"[TJPConcreteSession] 强制断开连接 - 当前状态: %@", self.stateMachine.currentState); //更新断开原因 self.disconnectReason = TJPDisconnectReasonForceReconnect; //发送强制断开事件 [self.stateMachine sendEvent:TJPConnectEventForceDisconnect]; //关闭底层连接 [self.connectionManager forceDisconnect]; //停止心跳 [self.heartbeatManager stopMonitoring]; //清理定时器和待确认消息 [self cancelAllRetransmissionTimersSync]; [self.pendingMessages removeAllObjects]; //停止监控 [TJPMetricsConsoleReporter stop]; TJPLOG_INFO(@"[TJPConcreteSession] 强制断开完成"); } #pragma mark - TJPMessageManagerDelegate - (void)messageManager:(id)manager message:(TJPMessageContext *)message didChangeState:(TJPMessageState)newState fromState:(TJPMessageState)oldState { TJPLOG_INFO(@"[TJPConcreteSession] 消息状态变化 %@: %lu -> %lu", message.messageId, (unsigned long)oldState, (unsigned long)newState); } - (void)messageManager:(TJPMessageManager *)manager willSendMessage:(TJPMessageContext *)context { TJPLOG_INFO(@"[TJPConcreteSession] 即将发送消息: %@", context.messageId); } - (void)messageManager:(TJPMessageManager *)manager didSendMessage:(TJPMessageContext *)context { TJPLOG_INFO(@"[TJPConcreteSession] 消息发送完成: %@", context.messageId); // 发送成功同志 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:kTJPMessageSentNotification object:nil userInfo:@{ @"messageId": context.messageId, @"sequence": @(context.sequence), @"sessionId": self.sessionId ?: @"", @"timestamp": [NSDate date] }]; TJPLOG_INFO(@"[TJPConcreteSession] 消息发送成功通知已发出: %@", context.messageId); }); } - (void)messageManager:(TJPMessageManager *)manager didReceiveACK:(TJPMessageContext *)context { TJPLOG_INFO(@"[TJPConcreteSession] 收到消息ACK: %@", context.messageId); } - (void)messageManager:(TJPMessageManager *)manager didFailToSendMessage:(TJPMessageContext *)context error:(NSError *)error { TJPLOG_ERROR(@"[TJPConcreteSession] 消息发送失败 %@: %@", context.messageId, error.localizedDescription); // 发送失败通知 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:kTJPMessageFailedNotification object:nil userInfo:@{ @"messageId": context.messageId, @"error": error, @"sessionId": self.sessionId ?: @"", @"timestamp": [NSDate date] }]; TJPLOG_ERROR(@"[TJPConcreteSession] 消息发送失败通知已发出: %@", context.messageId); }); } #pragma mark - TJPMessageManagerNetworkDelegate - (void)messageManager:(TJPMessageManager *)manager needsSendMessage:(TJPMessageContext *)message { // 实际发送逻辑 dispatch_async(self.sessionQueue, ^{ if (![self.stateMachine.currentState isEqualToString:TJPConnectStateConnected]) { TJPLOG_INFO(@"[TJPConcreteSession] 当前状态发送消息失败,当前状态为: %@", self.stateMachine.currentState); // 通知消息管理器发送失败 [manager updateMessage:message.messageId toState:TJPMessageStateFailed]; return; } //创建序列号 uint32_t seq = [self.seqManager nextSequenceForCategory:TJPMessageCategoryNormal]; // 更新消息管理器对应的消息序列号 message.sequence = seq; // 建立序列号到消息ID的映射 self.sequenceToMessageId[@(seq)] = message.messageId; //构造协议包 实际通过Socket发送的协议包(协议头+原始数据) NSData *packet = [TJPMessageBuilder buildPacketWithMessageType:message.messageType sequence:seq payload:message.payload encryptType:message.encryptType compressType:message.compressType sessionID:self.sessionId]; if (!packet) { TJPLOG_ERROR(@"[TJPConcreteSession] 消息包构建失败"); return; } // 将消息加入待确认列表 self.pendingMessages[message.messageId] = message; //设置超时重传 [self scheduleRetransmissionForMessageId:message.messageId]; TJPLOG_INFO(@"[TJPConcreteSession] 消息即将发出, 序列号: %u, 大小: %lu字节", seq, (unsigned long)packet.length); //使用连接管理器发送消息 [self.connectionManager sendData:packet withTimeout:-1 tag:seq]; // 可以增加通知MessageManager消息已通过网络发送,等待ACK }); } #pragma mark - Version Handshake - (void)performVersionHandshake { //协议版本握手逻辑 uint8_t majorVersion = kProtocolVersionMajor; uint8_t minorVersion = kProtocolVersionMinor; //设置连接管理器的版本信息 [self.connectionManager setVersionInfo:majorVersion minorVersion:minorVersion]; //构建版本握手数据包 TJPFinalAdavancedHeader header; memset(&header, 0, sizeof(TJPFinalAdavancedHeader)); //转换网络字节序 header.magic = htonl(kProtocolMagic); header.version_major = majorVersion; header.version_minor = minorVersion; //控制类型消息 header.msgType = htons(TJPMessageTypeControl); header.timestamp = htonl((uint32_t)[[NSDate date] timeIntervalSince1970]); header.encrypt_type = TJPEncryptTypeNone; header.compress_type = TJPCompressTypeNone; header.session_id = htons([TJPMessageBuilder sessionIDFromUUID:self.sessionId]); // 获取序列号 uint32_t seq = [self.seqManager nextSequenceForCategory:TJPMessageCategoryControl]; header.sequence = htonl(seq); #warning //构建版本协商TLV数据 - 这里使用我构建的数据 实际环境需要替换成你需要的 NSMutableData *tlvData = [NSMutableData data]; //版本协商请求标签 uint16_t versionTag = htons(TJP_TLV_TAG_VERSION_REQUEST); //版本信息长度 uint32_t versionLength = htonl(4); // 版本值(Value第一部分): 将主版本号和次版本号打包为一个16位整数 // 主版本占用高8位,次版本占用低8位 uint16_t versionValue = htons((majorVersion << 8) | minorVersion); // 使用定义的特性标志 启用已读回执功能 uint16_t featureFlags = htons(TJP_FEATURE_BASIC | TJP_FEATURE_READ_RECEIPT | TJP_FEATURE_ENCRYPTION); [tlvData appendBytes:&versionTag length:sizeof(uint16_t)]; //Tag [tlvData appendBytes:&versionLength length:sizeof(uint32_t)]; //Length [tlvData appendBytes:&versionValue length:sizeof(uint16_t)]; // Value: 版本 [tlvData appendBytes:&featureFlags length:sizeof(uint16_t)]; // Value: 特性 // 记录日志,便于调试 TJPLOG_INFO(@"[TJPConcreteSession] 发送版本协商: 版本=%d.%d, 特性=0x%04X, TLV标签=0x%04X", majorVersion, minorVersion, (TJP_FEATURE_BASIC | TJP_FEATURE_READ_RECEIPT | TJP_FEATURE_ENCRYPTION), TJP_TLV_TAG_VERSION_REQUEST); header.bodyLength = htonl((uint32_t)tlvData.length); // CRC32计算校验和 客户端标准htonl uint32_t checksum = [TJPNetworkUtil crc32ForData:tlvData]; header.checksum = htonl(checksum); // 构建完整的握手数据包 NSMutableData *handshakeData = [NSMutableData dataWithBytes:&header length:sizeof(TJPFinalAdavancedHeader)]; [handshakeData appendData:tlvData]; // 创建上下文并加入待确认队列 TJPMessageContext *context = [TJPMessageContext contextWithData:tlvData seq:seq messageType:TJPMessageTypeControl encryptType:TJPEncryptTypeNone compressType:TJPCompressTypeNone sessionId:self.sessionId]; // 控制消息通常不需要重传 context.maxRetryCount = 0; // 存储待确认消息 self.pendingMessages[context.messageId] = context; self.sequenceToMessageId[@(seq)] = context.messageId; // 发送握手数据包 [self.connectionManager sendData:handshakeData withTimeout:10.0 tag:header.sequence]; TJPLOG_INFO(@"[TJPConcreteSession] 已发送版本握手包,等待服务器响应,消息ID: %@, 序列号: %u", context.messageId, seq); } #pragma mark - TJPReconnectPolicyDelegate - (void)reconnectPolicyDidReachMaxAttempts:(TJPReconnectPolicy *)reconnectPolicy { TJPLOG_ERROR(@"[TJPConcreteSession] 最大重连次数已达到,连接失败"); dispatch_async(self.sessionQueue, ^{ // 停止重连尝试 [self.reconnectPolicy stopRetrying]; self.isReconnecting = NO; // 将状态机转为断开状态 [self.stateMachine sendEvent:TJPConnectEventConnectFailure]; // 关闭 socket 连接 [self.connectionManager disconnect]; // 停止心跳 [self.heartbeatManager stopMonitoring]; // 停止Timer [self cancelAllRetransmissionTimers]; // 清理资源 [self.pendingMessages removeAllObjects]; // 停止网络指标监控 [TJPMetricsConsoleReporter stop]; TJPLOG_INFO(@"[TJPConcreteSession] 当前连接退出"); }); } - (NSString *)getCurrentConnectionState { return self.stateMachine.currentState; } #pragma mark - Public Methods - (void)resetForReuse { // 验证基本状态 if (!self.sessionId || self.sessionId.length == 0) { TJPLOG_ERROR(@"[TJPConcreteSession] resetForReuse 时 sessionId 无效"); return; } TJPLOG_INFO(@"[TJPConcreteSession] 开始重置会话: %@ (第 %lu 次使用)", self.sessionId, (unsigned long)self.useCount + 1); if (self.sessionQueue) { dispatch_sync(self.sessionQueue, ^{ [self performResetOperations]; }); } else { [self performResetOperations]; } TJPLOG_INFO(@"[TJPConcreteSession] 会话重置完成: %@", self.sessionId); } - (void)performResetOperations { // 清理状态但保持核心对象 if (self.pendingMessages) { [self.pendingMessages removeAllObjects]; } if (self.sequenceToMessageId) { [self.sequenceToMessageId removeAllObjects]; } // 取消定时器 [self cancelAllRetransmissionTimersSync]; // 重置状态变量 self.disconnectReason = TJPDisconnectReasonNone; self.isReconnecting = NO; self.lastActiveTime = [NSDate date]; self.useCount++; self.isPooled = NO; // 确保状态机处于正确状态 if (self.stateMachine && ![self.stateMachine.currentState isEqualToString:TJPConnectStateDisconnected]) { TJPLOG_WARN(@"[TJPConcreteSession] 重置时状态异常: %@", self.stateMachine.currentState); // 不要强制发送事件,可能导致意外的副作用 } } #pragma mark - Private Methods - (void)prepareForConnection { // 增加池化层后连接时才初始化心跳 但不启动 [self ensureHeartbeatManagerInitialized]; // 重置连接相关状态 self.disconnectReason = TJPDisconnectReasonNone; // 清理之前可能遗留的状态 self.lastActiveTime = [NSDate date]; } - (void)handleSequenceReset:(TJPMessageCategory)category { TJPLOG_WARN(@"[TJPConcreteSession] 会话 %@ 类别 %d 序列号即将重置", self.sessionId, (int)category); // 检查是否有该类别的待确认消息 NSMutableArray *affectedMessages = [NSMutableArray array]; for (NSString *messageId in self.pendingMessages.allKeys) { TJPMessageContext *context = self.pendingMessages[messageId]; if ([self.seqManager isSequenceForCategory:context.sequence category:category]) { [affectedMessages addObject:messageId]; } } if (affectedMessages.count > 0) { TJPLOG_WARN(@"[TJPConcreteSession] 序列号重置可能影响 %lu 条待确认消息", (unsigned long)affectedMessages.count); // 等待自然超时重传 for (NSString *messageId in affectedMessages) { TJPLOG_INFO(@"[TJPConcreteSession] 消息 %@ 受序列号重置影响,等待重传", messageId); } } } - (void)handleConnectedState { // 如果有积压消息 发送积压消息 [self flushPendingMessages]; // 判断是否需要握手 if ([self shouldPerformHandshake]) { [self performVersionHandshake]; } else { TJPLOG_INFO(@"[TJPConcreteSession] 使用现有协商结果,跳过版本握手"); } } - (void)handleDisconnectingState { self.disconnectionTime = [NSDate date]; } - (void)handleDisconnectedState { [self.heartbeatManager stopMonitoring]; } - (void)handleForceDisconnectComplete { TJPLOG_INFO(@"[TJPConcreteSession] 强制断开完成,会话 %@ 已就绪", self.sessionId); // 重置一些状态 self.isReconnecting = NO; // 通知协调器可以进行后续操作(如重连或回收) if (self.delegate && [self.delegate respondsToSelector:@selector(sessionDidForceDisconnect:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate sessionDidForceDisconnect:self]; }); } } - (void)scheduleRetransmissionForMessageId:(NSString *)messageId { // 取消之前可能存在的重传计时器 dispatch_source_t existingTimer = self.retransmissionTimers[messageId]; if (existingTimer) { TJPLOG_INFO(@"[TJPConcreteSession] 因重新安排重传而取消消息 %@ 的旧重传计时器", messageId); dispatch_source_cancel(existingTimer); [self.retransmissionTimers removeObjectForKey:messageId]; } //获取消息上下文 TJPMessageContext *context = self.pendingMessages[messageId]; if (!context) { TJPLOG_ERROR(@"[TJPConcreteSession] 无法为消息 %@ 安排重传! 原因:消息上下文不存在", messageId); return; } //如果已经达到最大重试次数,不再安排重传 if (context.retryCount >= context.maxRetryCount) { TJPLOG_WARN(@"[TJPConcreteSession] 消息 %@ 已达到最大重试次数 %ld,不再重试", messageId, (long)context.maxRetryCount); return; } //创建GCD定时器 __weak typeof(self) weakSelf = self; dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.sessionQueue); //设置定时器间隔 (默认3秒一次) NSTimeInterval retryInterval = context.retryTimeout > 0 ? context.retryTimeout : kDefaultRetryInterval; uint64_t intervalInNanoseconds = (uint64_t)(retryInterval * NSEC_PER_SEC); dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, intervalInNanoseconds), DISPATCH_TIME_FOREVER, // 不重复 (1ull * NSEC_PER_SEC) / 10); // 100ms的精度 dispatch_source_set_event_handler(timer, ^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; [strongSelf handleRetransmissionForMessageId:messageId]; }); // 设置定时器取消处理函数 dispatch_source_set_cancel_handler(timer, ^{ TJPLOG_INFO(@"[TJPConcreteSession] 取消消息 %@ 的重传计时器", messageId); }); // 保存定时器 self.retransmissionTimers[messageId] = timer; // 启动定时器 dispatch_resume(timer); TJPLOG_INFO(@"[TJPConcreteSession] 为消息 %@ 安排重传,间隔 %.1f 秒,当前重试次数 %ld", messageId, retryInterval, (long)context.retryCount); } // 重传处理方法 - (void)handleRetransmissionForMessageId:(NSString *)messageId { // 获取消息上下文 TJPMessageContext *context = self.pendingMessages[messageId]; // 清理计时器 dispatch_source_t timer = self.retransmissionTimers[messageId]; if (timer) { dispatch_source_cancel(timer); [self.retransmissionTimers removeObjectForKey:messageId]; } // 如果消息已确认,不需要重传 if (!context) { TJPLOG_INFO(@"[TJPConcreteSession] 消息 %@ 已确认,不需要重传", messageId); return; } // 检查连接状态 if (![self.stateMachine.currentState isEqualToString:TJPConnectStateConnected]) { TJPLOG_WARN(@"[TJPConcreteSession] 当前连接状态为 %@,无法重传消息 %@", self.stateMachine.currentState, messageId); // 通知MessageManager连接异常 [self.messageManager updateMessage:messageId toState:TJPMessageStateFailed]; return; } // 增加重试次数 context.retryCount++; // 检查重试次数是否已达上限 if (context.retryCount >= context.maxRetryCount) { TJPLOG_ERROR(@"[TJPConcreteSession] 消息 %@ 重传失败,已达最大重试次数 %ld", messageId, (long)context.maxRetryCount); // 移除待确认消息 [self.pendingMessages removeObjectForKey:messageId]; [self.sequenceToMessageId removeObjectForKey:@(context.sequence)]; // 通知MessageManager连接异常 [self.messageManager updateMessage:messageId toState:TJPMessageStateFailed]; return; } // 通知MessageManager状态变化:重试中 [self.messageManager updateMessage:messageId toState:TJPMessageStateRetrying]; // 执行重传 TJPLOG_INFO(@"[TJPConcreteSession] 重传消息 %@,第 %ld 次尝试", messageId, (long)context.retryCount + 1); NSData *packet = [context buildRetryPacket]; [self.connectionManager sendData:packet withTimeout:-1 tag:context.sequence]; // 通知MessageManager状态变化:重新发送中 [self.messageManager updateMessage:messageId toState:TJPMessageStateSending]; // 安排下一次重传 [self scheduleRetransmissionForMessageId:messageId]; } - (void)cancelAllRetransmissionTimers { dispatch_async(self.sessionQueue, ^{ [self cancelAllRetransmissionTimersSync]; }); } - (void)cancelAllRetransmissionTimersSync { if (!_retransmissionTimers) return; for (NSString *key in [_retransmissionTimers allKeys]) { dispatch_source_t timer = _retransmissionTimers[key]; if (timer) { dispatch_source_cancel(timer); } } [_retransmissionTimers removeAllObjects]; TJPLOG_INFO(@"[TJPConcreteSession] 已清理所有重传计时器"); } - (void)flushPendingMessages { dispatch_async(self.sessionQueue, ^{ if ([self.pendingMessages count] == 0) { TJPLOG_INFO(@"[TJPConcreteSession] 没有积压消息需要发送"); return; } TJPLOG_INFO(@"[TJPConcreteSession] 开始发送积压消息,共 %lu 条", (unsigned long)self.pendingMessages.count); for (NSString *messageId in [self.pendingMessages allKeys]) { TJPMessageContext *context = self.pendingMessages[messageId]; NSData *packet = [context buildRetryPacket]; [self.connectionManager sendData:packet withTimeout:-1 tag:context.sequence]; [self scheduleRetransmissionForMessageId:messageId]; } }); } - (BOOL)shouldPerformHandshake { // 首次连接或未完成握手 if (!self.hasCompletedHandshake) { return YES; } // 长时间未握手(超过24小时) NSTimeInterval timeSinceLastHandshake = [[NSDate date] timeIntervalSinceDate:self.lastHandshakeTime]; if (timeSinceLastHandshake > 24 * 3600) { // 24小时 return YES; } // 长时间断线后重连(超过5分钟) if (self.disconnectionTime) { NSTimeInterval disconnectionDuration = [[NSDate date] timeIntervalSinceDate:self.disconnectionTime]; if (disconnectionDuration > 300) { // 5分钟 return YES; } } return NO; } - (void)resetConnection { // [self.seqManager resetSequences]; // [self.heartbeatManager reset]; } - (void)handleReconnectionAfterDisconnect { // 检查是否需要自动重连 if (!self.autoReconnectEnabled || self.disconnectReason == TJPDisconnectReasonUserInitiated || self.isReconnecting) { return; } // 检查网络状态,只有在网络可达时才尝试重连 if ([[TJPNetworkCoordinator shared].reachability currentReachabilityStatus] != NotReachable && (self.disconnectReason == TJPDisconnectReasonNetworkError || self.disconnectReason == TJPDisconnectReasonHeartbeatTimeout || self.disconnectReason == TJPDisconnectReasonIdleTimeout)) { self.isReconnecting = YES; // TJPLOG_INFO(@"开始重连策略,原因: %@", [self reasonToString:self.disconnectReason]); // 准备重连 [self.reconnectPolicy attemptConnectionWithBlock:^{ // 再次检查状态 if ([self.stateMachine.currentState isEqualToString:TJPConnectStateDisconnected]) { [self connectToHost:self.host port:self.port]; } }]; } } - (void)cleanupAfterDisconnect { // 停止心跳 [self.heartbeatManager stopMonitoring]; // 取消所有重传计时器 [self cancelAllRetransmissionTimers]; // 清理待确认消息 [self.pendingMessages removeAllObjects]; // 停止网络监控 [TJPMetricsConsoleReporter stop]; } - (void)processReceivedPacket:(TJPParsedPacket *)packet { TJPLOG_INFO(@"[TJPConcreteSession] 处理数据包: 类型=%hu, 序列号=%u", packet.messageType, packet.sequence); switch (packet.messageType) { case TJPMessageTypeNormalData: TJPLOG_INFO(@"[TJPConcreteSession] 处理普通数据包,序列号: %u", packet.sequence); [self handleDataPacket:packet]; break; case TJPMessageTypeHeartbeat: TJPLOG_INFO(@"[TJPConcreteSession] 处理心跳包,序列号: %u", packet.sequence); [self.heartbeatManager heartbeatACKNowledgedForSequence:packet.sequence]; break; case TJPMessageTypeACK: TJPLOG_INFO(@"[TJPConcreteSession] 处理ACK包,序列号: %u", packet.sequence); [self handleACKForSequence:packet.sequence]; break; case TJPMessageTypeControl: TJPLOG_INFO(@"[TJPConcreteSession] 处理控制包,序列号: %u", packet.sequence); [self handleControlPacket:packet]; break; case TJPMessageTypeReadReceipt: TJPLOG_INFO(@"[TJPConcreteSession] 收到已读回执,序列号: %u", packet.sequence); [self handleReadReceiptPacket:packet]; break; default: TJPLOG_WARN(@"[TJPConcreteSession] 收到未知消息类型 %hu", packet.messageType); break; } } - (void)handleDataPacket:(TJPParsedPacket *)packet { if (!packet.payload) { TJPLOG_ERROR(@"[TJPConcreteSession] 数据包载荷为空"); return; } // 发送消息接收通知 用于UI更新 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:kTJPMessageReceivedNotification object:nil userInfo:@{ @"data": packet.payload, @"sequence": @(packet.sequence), @"sessionId": self.sessionId ?: @"", @"timestamp": [NSDate date], @"messageType": @(packet.messageType) }]; TJPLOG_INFO(@"[TJPConcreteSession] 消息接收通知已发出,序列号: %u", packet.sequence); }); // 向上层通知收到数据 用于核心业务逻辑处理 if (self.delegate && [self.delegate respondsToSelector:@selector(session:didReceiveRawData:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate session:self didReceiveRawData:packet.payload]; }); } // 发送ACK确认 - 确认接收到的数据包 [self sendAckForPacket:packet messageCategory:TJPMessageCategoryNormal]; // 简单策略:延迟2秒自动发送已读回执(应用层) 实际项目中可以根据需要手动调用 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), self.sessionQueue, ^{ [self sendReadReceiptForMessageSequence:packet.sequence]; }); } - (void)handleControlPacket:(TJPParsedPacket *)packet { // 解析控制包数据,处理版本协商等控制消息 TJPLOG_INFO(@"[TJPConcreteSession] 收到控制包,长度: %lu", (unsigned long)packet.payload.length); // 确保数据包长度足够 if (packet.payload.length >= 12) { // 至少包含 Tag(2) + Length(4) + Value(2) + Flags(2) const void *bytes = packet.payload.bytes; uint16_t tag = 0; uint32_t length = 0; uint16_t value = 0; uint16_t flags = 0; // 提取 TLV 字段 memcpy(&tag, bytes, sizeof(uint16_t)); memcpy(&length, bytes + 2, sizeof(uint32_t)); memcpy(&value, bytes + 6, sizeof(uint16_t)); memcpy(&flags, bytes + 8, sizeof(uint16_t)); // 转换网络字节序到主机字节序 tag = ntohs(tag); length = ntohl(length); value = ntohs(value); flags = ntohs(flags); // 检查是否是版本协商响应 if (tag == TJP_TLV_TAG_VERSION_RESPONSE) { // 此处是版本协商响应标签 // 提取版本信息 uint8_t majorVersion = (value >> 8) & 0xFF; uint8_t minorVersion = value & 0xFF; TJPLOG_INFO(@"[TJPConcreteSession] 收到版本协商响应: 版本=%d.%d, 特性=0x%04X", majorVersion, minorVersion, flags); // 保存协商结果到会话属性中 self.negotiatedVersion = value; self.negotiatedFeatures = flags; self.lastHandshakeTime = [NSDate date]; self.hasCompletedHandshake = YES; // 根据协商结果配置会话 [self configureSessionWithFeatures:flags]; // 通知代理版本协商完成 if (self.delegate && [self.delegate respondsToSelector:@selector(session:didCompleteVersionNegotiation:features:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate session:self didCompleteVersionNegotiation:self.negotiatedVersion features:self.negotiatedFeatures]; }); } } else { TJPLOG_INFO(@"[TJPConcreteSession] 收到未知控制消息,标签: 0x%04X", tag); } } else { TJPLOG_WARN(@"[TJPConcreteSession] 控制包数据长度不足,无法解析"); } // 发送ACK确认 [self sendAckForPacket:packet messageCategory:TJPMessageCategoryControl]; } - (void)configureSessionWithFeatures:(uint16_t)features { TJPLOG_INFO(@"[TJPConcreteSession] 根据协商特性配置会话: 0x%04X", features); // 检查各个特性位并配置相应功能 // 是否支持加密 if (features & TJP_FEATURE_ENCRYPTION) { TJPLOG_INFO(@"[TJPConcreteSession] 启用加密功能"); // 配置加密 } else { TJPLOG_INFO(@"[TJPConcreteSession] 禁用加密功能"); // 禁用加密 } // 示例:判断是否支持压缩 if (features & TJP_FEATURE_COMPRESSION) { TJPLOG_INFO(@"[TJPConcreteSession] 启用压缩功能"); // 配置压缩 } else { TJPLOG_INFO(@"[TJPConcreteSession] 禁用压缩功能"); // 禁用压缩 } // 配置其他功能 } - (void)sendAckForPacket:(TJPParsedPacket *)packet messageCategory:(TJPMessageCategory)messageCategory { // 创建ACK消息 uint32_t ackSeq = [self.seqManager nextSequenceForCategory:messageCategory]; TJPFinalAdavancedHeader header; memset(&header, 0, sizeof(TJPFinalAdavancedHeader)); // 注意:包头字段需要转换为网络字节序 header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(TJPMessageTypeACK); header.sequence = htonl(ackSeq); header.timestamp = htonl((uint32_t)[[NSDate date] timeIntervalSince1970]); header.encrypt_type = TJPEncryptTypeNone; header.compress_type = TJPCompressTypeNone; header.session_id = htons([TJPMessageBuilder sessionIDFromUUID:self.sessionId]); // ACK消息体 - 包含被确认的序列号 NSMutableData *ackData = [NSMutableData data]; uint32_t originalSeq = htonl(packet.sequence); [ackData appendBytes:&originalSeq length:sizeof(uint32_t)]; header.bodyLength = htonl((uint32_t)ackData.length); // 计算校验和 uint32_t checksum = [TJPNetworkUtil crc32ForData:ackData]; header.checksum = htonl(checksum); // 客户端标准:校验和转网络字节序 TJPLOG_INFO(@"[TJPConcreteSession] 客户端ACK校验和: 原值=%u, 网络序=0x%08X", checksum, ntohl(header.checksum)); // 构建完整的ACK数据包 NSMutableData *ackPacket = [NSMutableData dataWithBytes:&header length:sizeof(TJPFinalAdavancedHeader)]; [ackPacket appendData:ackData]; // 发送ACK数据包 [self.connectionManager sendData:ackPacket withTimeout:-1 tag:ackSeq]; TJPLOG_INFO(@"[TJPConcreteSession] 已发送 %@ ACK确认包,确认序列号: %u", [self messageTypeToString:packet.messageType], packet.sequence); } - (void)handleACKForSequence:(uint32_t)sequence { TJPLOG_INFO(@"[TJPConcreteSession] 进入handleACKForSequence方法,序列号: %u", sequence); dispatch_async(self.sessionQueue, ^{ // 通过序列号查找messageId NSString *messageId = self.sequenceToMessageId[@(sequence)]; TJPMessageContext *context = self.pendingMessages[messageId]; if (context) { switch (context.messageType) { case TJPMessageTypeNormalData: TJPLOG_INFO(@"[TJPConcreteSession] 收到消息ACK, ID: %@, 序列号: %u", messageId ?: @"unknown", sequence); break; case TJPMessageTypeControl: TJPLOG_INFO(@"[TJPConcreteSession] 收到控制消息ACK, ID: %@, 序列号: %u", messageId ?: @"unknown", sequence); break; case TJPMessageTypeReadReceipt: TJPLOG_INFO(@"[TJPConcreteSession] 收到已读回执ACK, ID: %@, 序列号: %u", messageId ?: @"unknown", sequence); break; default: TJPLOG_INFO(@"[TJPConcreteSession] 收到ACK, ID: %@, 序列号: %u", messageId ?: @"unknown", sequence); break; } // 通知MessageManager状态转换 [self.messageManager updateMessage:messageId toState:TJPMessageStateSent]; // 从待确认消息列表中移除 [self.pendingMessages removeObjectForKey:messageId]; // 取消对应的重传计时器 dispatch_source_t timer = self.retransmissionTimers[messageId]; if (timer) { TJPLOG_INFO(@"[TJPConcreteSession] 因收到ACK而取消消息 %u 的重传计时器", sequence); dispatch_source_cancel(timer); [self.retransmissionTimers removeObjectForKey:messageId]; } // 对于普通消息,启动延迟清理(等待已读回执) if (context.messageType == TJPMessageTypeNormalData) { [self scheduleSequenceMappingCleanupForSequence:sequence messageId:messageId]; } else { // 控制消息等不需要已读回执,直接清理 [self.sequenceToMessageId removeObjectForKey:@(sequence)]; } } else if ([self.heartbeatManager isHeartbeatSequence:sequence]) { // 处理心跳ACK TJPLOG_INFO(@"[TJPConcreteSession] 处理心跳ACK,序列号: %u", sequence); [self.heartbeatManager heartbeatACKNowledgedForSequence:sequence]; } else { TJPLOG_INFO(@"[TJPConcreteSession] 收到未知消息的ACK,序列号: %u", sequence); } }); } - (void)scheduleSequenceMappingCleanupForSequence:(uint32_t)sequence messageId:(NSString *)messageId { // 30秒后清理映射(如果还没收到已读回执) dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30.0 * NSEC_PER_SEC)), self.sessionQueue, ^{ if (self.sequenceToMessageId[@(sequence)]) { TJPLOG_INFO(@"[TJPConcreteSession] 超时清理序列号映射: %u -> %@", sequence, messageId); [self.sequenceToMessageId removeObjectForKey:@(sequence)]; } }); } - (void)handleHeartbeatTimeout:(NSNotification *)notification { id session = notification.userInfo[@"session"]; if (session == self) { dispatch_async(self.sessionQueue, ^{ TJPLOG_WARN(@"[TJPConcreteSession] 心跳超时,断开连接"); [self disconnectWithReason:TJPDisconnectReasonHeartbeatTimeout]; }); } } - (void)handleReadReceiptPacket:(TJPParsedPacket *)packet { if (!packet.payload || packet.payload.length < 10) { // TLV最小长度: 2+4+4=10字节 TJPLOG_ERROR(@"[TJPConcreteSession] 已读回执数据格式错误"); return; } // 解析TLV格式的已读回执数据 const void *bytes = packet.payload.bytes; uint16_t tag = 0; uint32_t length = 0; uint32_t originalSequence = 0; // 提取TLV字段 memcpy(&tag, bytes, sizeof(uint16_t)); memcpy(&length, bytes + 2, sizeof(uint32_t)); memcpy(&originalSequence, bytes + 6, sizeof(uint32_t)); // 跳过Tag(2) + Length(4) = 6字节 // 转换网络字节序到主机字节序 tag = ntohs(tag); length = ntohl(length); originalSequence = ntohl(originalSequence); // 验证TLV格式 if (tag == TJP_TLV_TAG_READ_RECEIPT && length == 4) { // 已读回执标签,长度为4字节 TJPLOG_INFO(@"[TJPConcreteSession] 消息序列号 %u 已被对方阅读", originalSequence); // 查找对应的消息ID NSString *messageId = self.sequenceToMessageId[@(originalSequence)]; if (messageId) { // 更新消息状态为已读 [self.messageManager updateMessage:messageId toState:TJPMessageStateRead]; // 收到已读回执后,立即清理序列号映射 [self.sequenceToMessageId removeObjectForKey:@(originalSequence)]; // 发送已读回执接收通知 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:kTJPMessageReadNotification object:nil userInfo:@{ @"messageId": messageId, @"originalSequence": @(originalSequence), @"sessionId": self.sessionId ?: @"" }]; }); } } else { TJPLOG_WARN(@"[TJPConcreteSession] 已读回执TLV格式不正确: tag=0x%04X, length=%u", tag, length); } // 发送ACK确认(传输层确认) [self sendAckForPacket:packet messageCategory:TJPMessageCategoryNormal]; } // 发送已读回执 - (void)sendReadReceiptForMessageSequence:(uint32_t)messageSequence { dispatch_async(self.sessionQueue, ^{ if (![self.stateMachine.currentState isEqualToString:TJPConnectStateConnected]) { TJPLOG_WARN(@"[TJPConcreteSession] 连接状态异常,无法发送已读回执"); return; } // 获取已读回执的序列号 uint32_t readReceiptSeq = [self.seqManager nextSequenceForCategory:TJPMessageCategoryNormal]; // 构建TLV格式的已读回执数据 NSMutableData *readReceiptData = [NSMutableData data]; // Tag: 已读回执标签 (网络字节序) uint16_t tag = htons(TJP_TLV_TAG_READ_RECEIPT); [readReceiptData appendBytes:&tag length:sizeof(uint16_t)]; // Length: 数据长度 (网络字节序) uint32_t length = htonl(4); [readReceiptData appendBytes:&length length:sizeof(uint32_t)]; // Value: 原消息序列号 (网络字节序) uint32_t networkSequence = htonl(messageSequence); [readReceiptData appendBytes:&networkSequence length:sizeof(uint32_t)]; TJPLOG_INFO(@"[TJPConcreteSession] 构建TLV已读回执TLV: Tag=0x%04X, Length=4, Value=%u", TJP_TLV_TAG_READ_RECEIPT, messageSequence); // 构建协议包 NSData *packet = [TJPMessageBuilder buildPacketWithMessageType:TJPMessageTypeReadReceipt sequence:readReceiptSeq payload:readReceiptData encryptType:TJPEncryptTypeNone compressType:TJPCompressTypeNone sessionID:self.sessionId]; if (packet) { [self.connectionManager sendData:packet withTimeout:-1 tag:readReceiptSeq]; TJPLOG_INFO(@"[TJPConcreteSession] 已读回执已发送,确认消息序列号: %u", messageSequence); } }); } - (TJPConnectEvent)eventForTargetState:(TJPConnectState)targetState { // 定义状态到事件的映射规则 static NSDictionary *stateEventMap; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ stateEventMap = @{ TJPConnectStateDisconnected: TJPConnectEventDisconnectComplete, TJPConnectStateConnecting: TJPConnectEventConnect, TJPConnectStateConnected: TJPConnectEventConnectSuccess, TJPConnectStateDisconnecting: TJPConnectEventDisconnect }; }); return stateEventMap[targetState]; } - (NSString *)reasonToString:(TJPDisconnectReason)reason { switch (reason) { case TJPDisconnectReasonNone: return @"默认状态"; case TJPDisconnectReasonUserInitiated: return @"用户手动断开"; case TJPDisconnectReasonNetworkError: return @"网络错误"; case TJPDisconnectReasonHeartbeatTimeout: return @"心跳超时"; case TJPDisconnectReasonIdleTimeout: return @"空闲超时"; case TJPDisconnectReasonConnectionTimeout: return @"连接超时"; case TJPDisconnectReasonSocketError: return @"套接字错误"; case TJPDisconnectReasonAppBackgrounded: return @"应用进入后台"; case TJPDisconnectReasonForceReconnect: return @"强制重连"; default: return @"未知原因"; } } - (NSString *)messageTypeToString:(uint16_t)messageType { switch (messageType) { case TJPMessageTypeNormalData: return @"普通消息"; case TJPMessageTypeHeartbeat: return @"心跳"; case TJPMessageTypeACK: return @"确认"; case TJPMessageTypeControl: return @"控制消息"; default: return @"未知类型"; } } - (void)handleDisconnectStateTransition { //先检查当前状态 TJPConnectState currentState = self.stateMachine.currentState; //根据当前状态决定如何处理 if ([currentState isEqualToString:TJPConnectStateDisconnecting]) { [self.stateMachine sendEvent:TJPConnectEventDisconnectComplete]; }else if ([currentState isEqualToString:TJPConnectStateConnected] || [currentState isEqualToString:TJPConnectStateConnecting]) { // 连接中或已连接,需要完整的断开流程 [self.stateMachine sendEvent:TJPConnectEventDisconnect]; [self.stateMachine sendEvent:TJPConnectEventDisconnectComplete]; } else if ([currentState isEqualToString:TJPConnectStateDisconnected]) { // 已经断开,无需处理 TJPLOG_INFO(@"[TJPConcreteSession] 已在断开状态,无需处理状态转换"); } } - (void)handleDisconnectError:(NSError *)err { // 判断错误类型 if (err) { TJPLOG_INFO(@"[TJPConcreteSession] 连接已断开,原因: %@", err.localizedDescription); // 设置断开原因 if (err.code == NSURLErrorNotConnectedToInternet) { self.disconnectReason = TJPDisconnectReasonNetworkError; TJPLOG_INFO(@"[TJPConcreteSession] 网络错误:无法连接到互联网"); } else { self.disconnectReason = TJPDisconnectReasonSocketError; TJPLOG_INFO(@"[TJPConcreteSession] 连接错误:%@", err.localizedDescription); } } else { // 如果没有错误,则正常处理断开 TJPLOG_INFO(@"[TJPConcreteSession] 连接已正常断开"); // 如果没有明确设置,这里可能是用户主动断开 if (self.disconnectReason == TJPDisconnectReasonNone) { self.disconnectReason = TJPDisconnectReasonUserInitiated; } } } #pragma mark - Healthy Check - (BOOL)checkHealthyForSession { if (self.heartbeatManager) { // 有心跳管理器 使用更严格检查 return [self isHealthyForReuse]; }else { // 无心跳管理器 使用宽松检查 return [self isHealthyForPromotion]; } return NO; } - (BOOL)isHealthyForReuse { // 必须是已连接状态 if (![self.connectState isEqualToString:TJPConnectStateConnected]) { return NO; } // 检查使用次数(避免过度复用) if (self.useCount > 50) { // 最多复用50次 TJPLOG_INFO(@"[TJPConcreteSession] 会话 %@ 使用次数过多(%lu),不适合复用", self.sessionId, (unsigned long)self.useCount); return NO; } // 检查待确认消息数量 if (self.pendingMessages.count > 20) { TJPLOG_INFO(@"[TJPConcreteSession] 会话 %@ 待确认消息过多(%lu),不适合复用", self.sessionId, (unsigned long)self.pendingMessages.count); return NO; } // 检查空闲时间 NSTimeInterval idleTime = [[NSDate date] timeIntervalSinceDate:self.lastActiveTime]; if (idleTime > 300) { // 空闲超过5分钟 TJPLOG_INFO(@"[TJPConcreteSession] 会话 %@ 空闲时间过长(%.0f秒),不适合复用", self.sessionId, idleTime); return NO; } return YES; } - (BOOL)isHealthyForPromotion { // 预热会话使用宽松检查标准 // 检查会话是否太旧(预热会话也有保质期) NSTimeInterval age = [[NSDate date] timeIntervalSinceDate:self.createdTime]; if (age > 600) { // 预热会话最多存活10分钟 TJPLOG_DEBUG(@"[TJPConcreteSession] 预热会话 %@ 存活时间过长(%.0f秒),不适合升级", self.sessionId, age); return NO; } // 预热会话不应该有心跳管理器 if (self.heartbeatManager != nil) { TJPLOG_WARN(@"[TJPConcreteSession] 预热会话 %@ 不应该有心跳管理器", self.sessionId); return NO; } // 预热会话不应该有待处理的消息 if (self.pendingMessages.count > 0) { TJPLOG_WARN(@"[TJPConcreteSession] 预热会话 %@ 存在待处理消息,状态异常", self.sessionId); return NO; } // 预热会话不应该有使用计数 if (self.useCount > 0) { TJPLOG_WARN(@"[TJPConcreteSession] 预热会话 %@ 已被使用过,状态异常", self.sessionId); return NO; } return YES; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Container/TJPLightweightSessionPool.h ================================================ // // TJPLightweightSessionPool.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/16. // 轻量级会话重用池 #import #import "TJPSessionProtocol.h" #import "TJPNetworkDefine.h" NS_ASSUME_NONNULL_BEGIN @class TJPNetworkConfig, TJPConcreteSession; //池配置结构体 typedef struct { NSUInteger maxPoolSize; //每种类型最大池大小 NSTimeInterval maxIdleTime; //最大空闲时间 NSTimeInterval cleanupInterval; //清理间隔 NSUInteger maxReuseCount; //最大复用次数 } TJPSessionPoolConfig; // 会话池统计信息 typedef struct { NSUInteger totalSessions; //总会话数 NSUInteger activeSessions; //活跃会话数 NSUInteger pooledSessions; //池中会话数 NSUInteger hitCount; //命中次数 NSUInteger missCount; //未命中次数 double hitRate; //命中率 } TJPSessionPoolStats; @interface TJPLightweightSessionPool : NSObject // 池配置 @property (nonatomic, assign) TJPSessionPoolConfig config; // 是否启用池功能 @property (nonatomic, assign) BOOL poolEnabled; // 单例访问 + (instancetype)sharedPool; /** * 启动会话池 * @param config 池配置 */ - (void)startWithConfig:(TJPSessionPoolConfig)config; /** * 停止会话池(清理所有会话) */ - (void)stop; /** * 暂停池功能(临时禁用复用) */ - (void)pause; /** * 恢复池功能 */ - (void)resume; /** * 获取会话(优先从池中复用) * @param type 会话类型 * @param config 网络配置 * @return 可用的会话实例 */ - (id)acquireSessionForType:(TJPSessionType)type withConfig:(TJPNetworkConfig *)config; /** * 归还会话到池中 * @param session 要归还的会话 */ - (void)releaseSession:(id)session; /** * 强制移除会话(不放入池中) * @param session 要移除的会话 */ - (void)removeSession:(id)session; /** * 手动触发清理 */ - (void)cleanup; /** * 清理指定类型的会话 * @param type 会话类型 */ - (void)cleanupSessionsForType:(TJPSessionType)type; /** * 预热池(提前创建会话) * @param type 会话类型 * @param count 预创建数量 * @param config 网络配置 */ - (void)warmupPoolForType:(TJPSessionType)type count:(NSUInteger)count withConfig:(TJPNetworkConfig *)config; /** * 获取池统计信息 */ - (TJPSessionPoolStats)getPoolStats; /** * 获取指定类型的会话数量 * @param type 会话类型 */ - (NSUInteger)getSessionCountForType:(TJPSessionType)type; /** * 获取池中会话数量 * @param type 会话类型 */ - (NSUInteger)getPooledSessionCountForType:(TJPSessionType)type; /** * 重置统计信息 */ - (void)resetStats; /** * 打印池状态 */ - (void)logPoolStatus; /** * 获取详细的池信息 */ - (NSDictionary *)getDetailedPoolInfo; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Container/TJPLightweightSessionPool.m ================================================ // // TJPLightweightSessionPool.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/16. // #import "TJPLightweightSessionPool.h" #import "TJPConcreteSession.h" #import "TJPNetworkCoordinator.h" #import "TJPNetworkConfig.h" #import "TJPNetworkDefine.h" // 默认配置常量 static const TJPSessionPoolConfig kDefaultPoolConfig = { .maxPoolSize = 5, // 每种类型最多5个会话 .maxIdleTime = 300, // 5分钟空闲超时 .cleanupInterval = 60, // 1分钟清理一次 .maxReuseCount = 50 // 最多复用50次 }; @interface TJPLightweightSessionPool () { // 健康检查缓存 NSMutableDictionary *_healthCache; NSTimeInterval _healthCacheValidDuration; NSUInteger _healthCheckCounter; } // 按类型存储的会话池 @property (nonatomic, strong) NSMutableDictionary *> *sessionPools; // 活跃会话池 @property (nonatomic, strong) NSMutableSet *activeSessions; // 池管理队列 @property (nonatomic, strong) dispatch_queue_t poolQueue; @property (nonatomic, strong) dispatch_source_t cleanupTimer; // 统计信息 @property (nonatomic, assign) NSUInteger hitCount; @property (nonatomic, assign) NSUInteger missCount; // 池状态 @property (nonatomic, assign) BOOL isRunning; @end @implementation TJPLightweightSessionPool #pragma mark - Lifecycle + (instancetype)sharedPool { static TJPLightweightSessionPool *instace = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instace = [[self alloc] init]; }); return instace; } - (instancetype)init { if (self = [super init]) { // 初始化属性 _config = kDefaultPoolConfig; _poolEnabled = YES; _isRunning = NO; _healthCache = [NSMutableDictionary dictionary]; // 健康状态缓存时间 _healthCacheValidDuration = 10.0; _healthCheckCounter = 0; _sessionPools = [NSMutableDictionary dictionary]; _activeSessions = [NSMutableSet set]; _poolQueue = dispatch_queue_create("com.tjp.sessionpool.queue", DISPATCH_QUEUE_SERIAL); [self setupApplicationNotifications]; } return self; } - (void)dealloc { TJPLOG_INFO(@"🚨 [TJPLightweightSessionPool] 开始释放"); [self stop]; [[NSNotificationCenter defaultCenter] removeObserver:self]; TJPLOG_INFO(@"🚨 [TJPLightweightSessionPool] 释放完成"); } #pragma Public Method - (void)startWithConfig:(TJPSessionPoolConfig)config { dispatch_async(self.poolQueue, ^{ if (self.isRunning) { TJPLOG_WARN(@"[SessionPool] 会话池已在运行中"); return; } self.config = config; self.isRunning = YES; TJPLOG_INFO(@"[SessionPool] 启动会话池 - 最大池大小:%lu, 空闲超时:%.0f秒, 清理间隔:%.0f秒", (unsigned long)config.maxPoolSize, config.maxIdleTime, config.cleanupInterval); [self startCleanupTimer]; }); } - (void)stop { dispatch_async(self.poolQueue, ^{ if (!self.isRunning) { return; } self.isRunning = NO; [self stopCleanupTimer]; // 断开活跃会话 for (TJPConcreteSession *session in [self.activeSessions copy]) { [session disconnectWithReason:TJPDisconnectReasonUserInitiated]; } [self.activeSessions removeAllObjects]; // 清理池中会话 for (NSNumber *typeKey in [self.sessionPools allKeys]) { // 获取对应类型的池数组 NSMutableArray *pool = self.sessionPools[typeKey]; for (TJPConcreteSession *session in [pool copy]) { [session disconnectWithReason:TJPDisconnectReasonUserInitiated]; session.isPooled = NO; } [pool removeAllObjects]; } [self.sessionPools removeAllObjects]; TJPLOG_INFO(@"[SessionPool] 会话池已停止"); }); } - (void)pause { dispatch_async(self.poolQueue, ^{ self.poolEnabled = NO; TJPLOG_INFO(@"[SessionPool] 会话池已暂停"); }); } - (void)resume { dispatch_async(self.poolQueue, ^{ self.poolEnabled = YES; TJPLOG_INFO(@"[SessionPool] 会话池已恢复"); }); } - (id)acquireSessionForType:(TJPSessionType)type withConfig:(TJPNetworkConfig *)config { __block TJPConcreteSession *session = nil; TJPLOG_INFO(@"[SessionPool] 开始获取会话,类型: %lu", (unsigned long)type); // 同步获取 确保会话有效 dispatch_sync(self.poolQueue, ^{ if (!self.isRunning || !self.poolEnabled) { TJPLOG_INFO(@"[SessionPool] 池未启用,创建新会话: %@", session.sessionId); //当前池未启用 直接创建新session session = [self createNewSessionForType:type withConfig:config]; if (session) { [self.activeSessions addObject:session]; self.missCount++; } return; } // 尝试从池中获取可复用的会话 session = [self getReusableSessionForType:type]; if (session) { // 获取到池 self.hitCount++; // 先加入活跃集合 避免提前释放 [self.activeSessions addObject:session]; //从池中移除 加入活跃列表 NSMutableArray *pool = [self getPoolForType:type]; [pool removeObject:session]; session.isPooled = NO; session.lastActiveTime = [NSDate date]; //重置session状态 供复用 [session resetForReuse]; TJPLOG_INFO(@"[SessionPool] 从池中复用会话 %@ (类型:%lu, 使用次数:%lu)", session.sessionId, (unsigned long)type, (unsigned long)session.useCount); }else { // 池未命中,创建新会话 session = [self createNewSessionForType:type withConfig:config]; if (session) { // 加入活跃集合 [self.activeSessions addObject:session]; self.missCount++; TJPLOG_INFO(@"[SessionPool] 创建新会话 %@ (类型:%lu)", session.sessionId, (unsigned long)type); } } }); return session; } - (void)releaseSession:(id)session { if (!session) { TJPLOG_ERROR(@"[SessionPool] releaseSession 收到 nil session"); return; } if (![session isKindOfClass:[TJPConcreteSession class]]) { TJPLOG_WARN(@"[SessionPool] 无法归还非TJPConcreteSession类型的会话: %@", [session class]); return; } TJPConcreteSession *concreteSession = (TJPConcreteSession *)session; // 验证会话完整性 if (![self validateSession:concreteSession withLabel:@"释放验证"]) { TJPLOG_ERROR(@"[SessionPool] 释放的会话验证失败,直接销毁"); [concreteSession prepareForRelease]; return; } dispatch_async(self.poolQueue, ^{ // 从活跃列表移除 [self.activeSessions removeObject:concreteSession]; if (!self.isRunning || !self.poolEnabled) { //池未启用,直接断开连接 [concreteSession disconnectWithReason:TJPDisconnectReasonUserInitiated]; TJPLOG_INFO(@"[SessionPool] 池未启用,直接断开会话: %@", concreteSession.sessionId); return; } // 检查会话是否适合放入池中 if ([self shouldPoolSession:concreteSession]) { [self addSessionToPool:concreteSession]; TJPLOG_INFO(@"[SessionPool] 会话 %@ 已归还到池中 (类型:%lu)", concreteSession.sessionId, (unsigned long)concreteSession.sessionType); } else { // 不适合放入池中,直接断开 [concreteSession disconnectWithReason:TJPDisconnectReasonUserInitiated]; TJPLOG_INFO(@"[SessionPool] 会话 %@ 不适合复用,已断开连接", concreteSession.sessionId); } }); } - (void)removeSession:(id)session { if (![session isKindOfClass:[TJPConcreteSession class]]) { return; } TJPConcreteSession *concreteSession = (TJPConcreteSession *)session; dispatch_async(self.poolQueue, ^{ //从活跃列表移除 [self.activeSessions removeObject:concreteSession]; //从池中移除 NSMutableArray *pool = [self getPoolForType:concreteSession.sessionType]; [pool removeObject:concreteSession]; //断开连接 [concreteSession disconnectWithReason:TJPDisconnectReasonUserInitiated]; concreteSession.isPooled = NO; TJPLOG_INFO(@"[SessionPool] 强制移除会话 %@", concreteSession.sessionId); }); } - (void)warmupPoolForType:(TJPSessionType)type count:(NSUInteger)count withConfig:(TJPNetworkConfig *)config { if (count == 0) return; dispatch_async(self.poolQueue, ^{ // 获取对应类型的池 NSMutableArray *pool = [self getPoolForType:type]; NSUInteger currentCount = pool.count; NSUInteger targetCount = MIN(count, self.config.maxPoolSize); if (currentCount >= targetCount) { TJPLOG_INFO(@"[SessionPool] 类型 %lu 的池已有足够会话,无需预热", (unsigned long)type); return; } NSUInteger createCount = targetCount - currentCount; TJPLOG_INFO(@"[SessionPool] 开始预热类型 %lu 的会话池,创建 %lu 个会话", (unsigned long)type, (unsigned long)createCount); for (NSUInteger i = 0; i < createCount; i++) { // 创建对应类型的会话 TJPConcreteSession *session = [self createNewSessionForType:type withConfig:config]; if (!session || !session.sessionId) { TJPLOG_ERROR(@"⚠️ [WARMUP] 第%lu个会话创建失败", (unsigned long)(i+1)); continue; } TJPLOG_INFO(@"🔥 [WARMUP] 创建会话 %@ 准备添加到池", session.sessionId); [self addSessionToPool:session]; } TJPLOG_INFO(@"[SessionPool] 完成预热,类型 %lu 的池现有 %lu 个会话", (unsigned long)type, (unsigned long)pool.count); }); } #pragma mark - Private Method - (void)setupApplicationNotifications { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; } - (TJPConcreteSession *)createNewSessionForType:(TJPSessionType)type withConfig:(TJPNetworkConfig *)config { // 如果没有提供配置,使用默认配置 if (!config) { config = [[TJPNetworkCoordinator shared] defaultConfigForSessionType:type]; } // 创建会话 TJPConcreteSession *session = [[TJPConcreteSession alloc] initWithConfiguration:config]; if (!session) { TJPLOG_ERROR(@"[SessionPool] TJPConcreteSession 创建失败,类型: %lu", (unsigned long)type); return nil; } // 验证 sessionId 是否有效 if (!session.sessionId || session.sessionId.length == 0) { TJPLOG_ERROR(@"[SessionPool] 新创建的会话 sessionId 无效,重新生成"); session.sessionId = [[NSUUID UUID] UUIDString]; if (!session.sessionId) { TJPLOG_ERROR(@"[SessionPool] 无法生成有效的 sessionId"); return nil; } } // 设置会话属性 session.sessionType = type; session.lastActiveTime = [NSDate date]; session.useCount = 0; session.isPooled = NO; TJPLOG_INFO(@"[SessionPool] 成功创建新会话: %@,类型: %lu", session.sessionId, (unsigned long)type); return session; } - (TJPConcreteSession *)getReusableSessionForType:(TJPSessionType)type { NSMutableArray *pool = [self getPoolForType:type]; if (!pool || pool.count == 0) { TJPLOG_INFO(@"[SessionPool] 类型 %lu 的池为空或不存在", (unsigned long)type); return nil; } // 创建数组副本,避免在遍历时修改原数组 NSArray *poolCopy = [pool copy]; // 寻找最适合复用的会话(空闲时间短且使用次数少的优先) TJPConcreteSession *bestSession = nil; NSTimeInterval shortestIdleTime = INFINITY; NSMutableArray *sessionsToRemove = [NSMutableArray array]; for (TJPConcreteSession *session in poolCopy) { // 强引用保持会话,防止在检查过程中被释放 TJPConcreteSession *strongSession = session; // 验证会话有效性 if (!strongSession || !strongSession.sessionId || strongSession.sessionId.length == 0) { TJPLOG_INFO(@"[SessionPool] 发现无效会话: %@,标记移除", strongSession.sessionId); [sessionsToRemove addObject:strongSession]; continue; } // 检查会话健康状况 @try { if (![strongSession checkHealthyForSession]) { TJPLOG_INFO(@"[SessionPool] 会话 %@ 健康检查失败,标记移除", strongSession.sessionId); [sessionsToRemove addObject:strongSession]; continue; } } @catch (NSException *exception) { TJPLOG_INFO(@"[SessionPool] 健康检查异常: %@,会话: %@", exception.reason, strongSession.sessionId ?: @"unknown"); [sessionsToRemove addObject:strongSession]; continue; } // 计算空闲时间 NSTimeInterval idleTime = 0; @try { NSDate *lastActiveTime = strongSession.lastActiveTime; if (lastActiveTime) { idleTime = [[NSDate date] timeIntervalSinceDate:lastActiveTime]; } } @catch (NSException *exception) { TJPLOG_INFO(@"[SessionPool] 计算空闲时间异常: %@", exception.reason); [sessionsToRemove addObject:strongSession]; continue; } // 选择最佳会话 if (idleTime < shortestIdleTime) { shortestIdleTime = idleTime; bestSession = strongSession; } TJPLOG_INFO(@"[SessionPool] 会话 %@ 检查通过,空闲时间: %.1f秒", strongSession.sessionId, idleTime); // 安全移除无效会话 [self safelyRemoveSessionsFromPool:sessionsToRemove fromPool:pool]; if (bestSession) { TJPLOG_INFO(@"[SessionPool] 找到最佳会话: %@,空闲时间: %.1f秒", bestSession.sessionId, shortestIdleTime); } else { TJPLOG_INFO(@"[SessionPool] 未找到可复用的会话"); } } return bestSession; } - (void)safelyRemoveSessionsFromPool:(NSArray *)sessionsToRemove fromPool:(NSMutableArray *)pool { if (sessionsToRemove.count == 0) return; TJPLOG_INFO(@"[SessionPool] 准备移除 %lu 个无效会话", (unsigned long)sessionsToRemove.count); for (TJPConcreteSession *session in sessionsToRemove) { @try { // 安全移除,不触发额外的释放 if ([pool containsObject:session]) { [pool removeObject:session]; TJPLOG_INFO(@"[SessionPool] 已移除会话: %@", session.sessionId ?: @"unknown"); // 标记为非池状态,但不调用断开 if (session) { session.isPooled = NO; } } } @catch (NSException *exception) { TJPLOG_INFO(@"[SessionPool] 移除会话异常: %@", exception.reason); } } } - (void)startCleanupTimer { if (self.cleanupTimer) { dispatch_source_cancel(self.cleanupTimer); } self.cleanupTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.poolQueue); uint64_t interval = (uint64_t)(self.config.cleanupInterval * NSEC_PER_SEC); dispatch_source_set_timer(self.cleanupTimer, dispatch_time(DISPATCH_TIME_NOW, interval), interval, (1ull * NSEC_PER_SEC) / 10); __weak typeof(self) weakSelf = self; dispatch_source_set_event_handler(self.cleanupTimer, ^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (strongSelf) { [strongSelf performCleanup]; } }); dispatch_resume(self.cleanupTimer); } - (void)stopCleanupTimer { if (self.cleanupTimer) { dispatch_source_cancel(self.cleanupTimer); self.cleanupTimer = nil; } } - (NSMutableArray *)getPoolForType:(TJPSessionType)type { // 验证 sessionPools if (!self.sessionPools) { TJPLOG_ERROR(@"[SessionPool] sessionPools 为 nil,重新初始化"); self.sessionPools = [NSMutableDictionary dictionary]; } NSNumber *typeKey = @(type); NSMutableArray *pool = self.sessionPools[typeKey]; if (!pool) { pool = [NSMutableArray array]; self.sessionPools[typeKey] = pool; TJPLOG_INFO(@"[SessionPool] 创建类型 %lu 的新池,容量: %lu", (unsigned long)type, (unsigned long)self.config.maxPoolSize); } return pool; } - (void)cleanup { dispatch_async(self.poolQueue, ^{ [self performCleanup]; }); } - (void)cleanupSessionsForType:(TJPSessionType)type { dispatch_async(self.poolQueue, ^{ [self performCleanupForType:type]; }); } - (BOOL)validateSession:(TJPConcreteSession *)session withLabel:(NSString *)label { if (!session) { TJPLOG_ERROR(@"[SessionPool][%@] 会话为 nil", label); return NO; } if (!session.sessionId || session.sessionId.length == 0) { TJPLOG_ERROR(@"[SessionPool][%@] 会话 sessionId 无效", label); return NO; } if (![session isKindOfClass:[TJPConcreteSession class]]) { TJPLOG_ERROR(@"[SessionPool][%@] 会话类型错误: %@", label, [session class]); return NO; } TJPLOG_INFO(@"[SessionPool][%@] 会话验证通过: %@", label, session.sessionId); return YES; } - (BOOL)shouldPoolSession:(TJPConcreteSession *)session { //检查池是否已满 NSMutableArray *pool = [self getPoolForType:session.sessionType]; if (pool.count >= self.config.maxPoolSize) { return NO; } //检查会话健康状况 if (![session checkHealthyForSession]) { return NO; } //检查使用次数 if (session.useCount >= self.config.maxReuseCount) { return NO; } //检查连接状态 if (session.connectState != TJPConnectStateConnected) { return NO; } return YES; } - (void)addSessionToPool:(TJPConcreteSession *)session { // 验证 session 不为 nil if (!session) { TJPLOG_ERROR(@"[SessionPool] addSessionToPool 收到 nil session,调用栈检查"); // 调试:打印调用栈 NSArray *callStack = [NSThread callStackSymbols]; for (NSString *frame in callStack) { TJPLOG_ERROR(@"📞 [SessionPool] %@", frame); } return; } // 验证 sessionId if (!session.sessionId || session.sessionId.length == 0) { TJPLOG_ERROR(@"[SessionPool] 会话 sessionId 无效,不能添加到池中"); return; } // 验证 sessionType if (session.sessionType < 0) { TJPLOG_ERROR(@"[SessionPool] 会话类型无效: %ld", (long)session.sessionType); return; } // 验证 sessionPools if (!self.sessionPools) { TJPLOG_ERROR(@"[SessionPool] sessionPools 字典为 nil,重新初始化"); self.sessionPools = [NSMutableDictionary dictionary]; } NSMutableArray *pool = [self getPoolForType:session.sessionType]; // 验证获取到的池 if (!pool) { TJPLOG_ERROR(@"[SessionPool] 无法获取类型 %lu 的池", (unsigned long)session.sessionType); return; } // 检查池是否已满 if (pool.count >= self.config.maxPoolSize) { TJPLOG_INFO(@"[SessionPool] 类型 %lu 的池已满,移除最旧会话", (unsigned long)session.sessionType); TJPConcreteSession *oldestSession = [pool firstObject]; if (oldestSession) { [pool removeObject:oldestSession]; [oldestSession prepareForRelease]; } } // 检查是否已在池中 if ([pool containsObject:session]) { TJPLOG_WARN(@"[SessionPool] 会话 %@ 已在池中,跳过添加", session.sessionId); return; } session.isPooled = YES; session.lastReleaseTime = [NSDate date]; // try-catch 防护 @try { [pool addObject:session]; TJPLOG_INFO(@"[SessionPool] 成功添加会话 %@ 到类型 %lu 的池中,池大小: %lu/%lu", session.sessionId, (unsigned long)session.sessionType, (unsigned long)pool.count, (unsigned long)self.config.maxPoolSize); } @catch (NSException *exception) { TJPLOG_ERROR(@"[SessionPool] 添加会话到池异常: %@, 会话: %@", exception.reason, session.sessionId ?: @"unknown"); // 重置会话状态 session.isPooled = NO; session.lastReleaseTime = nil; } } - (void)performCleanup { NSUInteger totalCleaned = 0; for (NSNumber *typeKey in [self.sessionPools allKeys]) { totalCleaned += [self performCleanupForType:[typeKey unsignedIntegerValue]]; } if (totalCleaned > 0) { TJPLOG_INFO(@"[SessionPool] 清理完成,共移除 %lu 个过期会话", (unsigned long)totalCleaned); } } - (NSUInteger)performCleanupForType:(TJPSessionType)type { NSMutableArray *pool = [self getPoolForType:type]; NSMutableArray *sessionsToRemove = [NSMutableArray array]; NSDate *now = [NSDate date]; for (TJPConcreteSession *session in [pool copy]) { BOOL shouldRemove = NO; // 检查空闲时间 NSTimeInterval idleTime = [now timeIntervalSinceDate:session.lastReleaseTime ?: session.lastActiveTime]; if (idleTime > self.config.maxIdleTime) { TJPLOG_INFO(@"[SessionPool] 会话 %@ 空闲时间过长(%.0f秒),移除", session.sessionId, idleTime); shouldRemove = YES; } // 检查健康状况 if (![session checkHealthyForSession]) { TJPLOG_INFO(@"[SessionPool] 会话 %@ 健康检查失败,移除", session.sessionId); shouldRemove = YES; } if (shouldRemove) { [sessionsToRemove addObject:session]; } } // 移除无效会话 for (TJPConcreteSession *session in sessionsToRemove) { [pool removeObject:session]; [session disconnectWithReason:TJPDisconnectReasonIdleTimeout]; session.isPooled = NO; } return sessionsToRemove.count; } #pragma mark - Analysis - (TJPSessionPoolStats)getPoolStats { __block TJPSessionPoolStats stats = {0}; dispatch_sync(self.poolQueue, ^{ stats.activeSessions = self.activeSessions.count; for (NSMutableArray *pool in self.sessionPools.allValues) { stats.pooledSessions += pool.count; } stats.totalSessions = stats.activeSessions + stats.pooledSessions; stats.hitCount = self.hitCount; stats.missCount = self.missCount; NSUInteger totalRequests = self.hitCount + self.missCount; stats.hitRate = totalRequests > 0 ? (double)self.hitCount / totalRequests : 0.0; }); return stats; } - (NSUInteger)getSessionCountForType:(TJPSessionType)type { __block NSUInteger count = 0; dispatch_sync(self.poolQueue, ^{ // 统计活跃会话 for (TJPConcreteSession *session in self.activeSessions) { if (session.sessionType == type) { count++; } } // 统计池中会话 NSMutableArray *pool = self.sessionPools[@(type)]; count += pool.count; }); return count; } - (NSUInteger)getPooledSessionCountForType:(TJPSessionType)type { __block NSUInteger count = 0; dispatch_sync(self.poolQueue, ^{ NSMutableArray *pool = self.sessionPools[@(type)]; count = pool.count; }); return count; } - (void)resetStats { dispatch_async(self.poolQueue, ^{ self.hitCount = 0; self.missCount = 0; TJPLOG_INFO(@"[SessionPool] 已重置会话池统计信息"); }); } #pragma mark - Notifications - (void)applicationDidEnterBackground:(NSNotification *)notification { dispatch_async(self.poolQueue, ^{ TJPLOG_INFO(@"[SessionPool] 应用进入后台,暂停会话池清理"); [self stopCleanupTimer]; // 可选:断开部分非关键会话以节省资源 // [self cleanupNonCriticalSessions]; }); } - (void)applicationWillEnterForeground:(NSNotification *)notification { dispatch_async(self.poolQueue, ^{ if (self.isRunning) { TJPLOG_INFO(@"[SessionPool] 应用回到前台,恢复会话池清理"); [self startCleanupTimer]; // 立即执行一次清理 [self performCleanup]; } }); } - (void)applicationWillTerminate:(NSNotification *)notification { [self stop]; } #pragma mark - Debug - (void)logPoolStatus { dispatch_async(self.poolQueue, ^{ TJPSessionPoolStats stats = [self getPoolStats]; TJPLOG_INFO(@"=== 会话池状态 ==="); TJPLOG_INFO(@"池状态: %@", self.isRunning ? @"运行中" : @"已停止"); TJPLOG_INFO(@"池功能: %@", self.poolEnabled ? @"启用" : @"禁用"); TJPLOG_INFO(@"总会话数: %lu", (unsigned long)stats.totalSessions); TJPLOG_INFO(@"活跃会话: %lu", (unsigned long)stats.activeSessions); TJPLOG_INFO(@"池中会话: %lu", (unsigned long)stats.pooledSessions); TJPLOG_INFO(@"命中率: %.2f%% (%lu/%lu)", stats.hitRate * 100, (unsigned long)stats.hitCount, (unsigned long)(stats.hitCount + stats.missCount)); for (NSNumber *typeKey in [self.sessionPools allKeys]) { TJPSessionType type = [typeKey unsignedIntegerValue]; NSUInteger poolCount = [self getPooledSessionCountForType:type]; NSUInteger totalCount = [self getSessionCountForType:type]; TJPLOG_INFO(@"类型 %lu: 总计 %lu, 池中 %lu", (unsigned long)type, (unsigned long)totalCount, (unsigned long)poolCount); } TJPLOG_INFO(@"================"); }); } - (NSDictionary *)getDetailedPoolInfo { __block NSMutableDictionary *info = [NSMutableDictionary dictionary]; dispatch_sync(self.poolQueue, ^{ TJPSessionPoolStats stats = [self getPoolStats]; info[@"isRunning"] = @(self.isRunning); info[@"poolEnabled"] = @(self.poolEnabled); info[@"config"] = @{ @"maxPoolSize": @(self.config.maxPoolSize), @"maxIdleTime": @(self.config.maxIdleTime), @"cleanupInterval": @(self.config.cleanupInterval), @"maxReuseCount": @(self.config.maxReuseCount) }; info[@"stats"] = @{ @"totalSessions": @(stats.totalSessions), @"activeSessions": @(stats.activeSessions), @"pooledSessions": @(stats.pooledSessions), @"hitCount": @(stats.hitCount), @"missCount": @(stats.missCount), @"hitRate": @(stats.hitRate) }; NSMutableDictionary *typeInfo = [NSMutableDictionary dictionary]; for (NSNumber *typeKey in [self.sessionPools allKeys]) { TJPSessionType type = [typeKey unsignedIntegerValue]; typeInfo[typeKey] = @{ @"pooledCount": @([self getPooledSessionCountForType:type]), @"totalCount": @([self getSessionCountForType:type]) }; } info[@"typeBreakdown"] = typeInfo; }); return [info copy]; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Container/TJPNetworkCoordinator.h ================================================ // // TJPNetworkCoordinator.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // 中心管理类 核心类之一 #import #import "TJPCoreTypes.h" #import "TJPSessionDelegate.h" NS_ASSUME_NONNULL_BEGIN @protocol TJPSessionProtocol; @class Reachability, TJPNetworkConfig, TJPLightweightSessionPool; @interface TJPNetworkCoordinator : NSObject /// 管理当前正在使用的会话 按sessionId索引 @property (nonatomic, strong, readonly) NSMapTable> *sessionMap; /// Session类型 为多路复用做支撑 @property (nonatomic, strong) NSMutableDictionary *sessionTypeMap; /// 会话池 管理会话复用 @property (nonatomic, strong) TJPLightweightSessionPool *sessionPool; /// 网络状态 @property (nonatomic, strong) Reachability *reachability; /// session专用队列 串行:增删改查操作 @property (nonatomic, strong) dispatch_queue_t sessionQueue; /// 解析专用队列 串行:数据解析专用 @property (nonatomic, strong) dispatch_queue_t parseQueue; /// 监控专用队列 串行:网络监控相关 @property (nonatomic, strong) dispatch_queue_t monitorQueue; /// 单例 + (instancetype)shared; /// 创建会话方法 - (id)createSessionWithConfiguration:(TJPNetworkConfig *)config; /// 通过类型创建会话方法 多路复用必须使用此方法 - (id)createSessionWithConfiguration:(TJPNetworkConfig *)config type:(TJPSessionType)type; /// 新增默认session配置方法 - (TJPNetworkConfig *)defaultConfigForSessionType:(TJPSessionType)type; /// 统一更新所有会话状态 - (void)updateAllSessionsState:(TJPConnectState)state; /// 统一管理重连 - (void)scheduleReconnectForSession:(id)session; /// 移除会话 - (void)removeSession:(id)session; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Container/TJPNetworkCoordinator.m ================================================ // // TJPNetworkCoordinator.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #import "TJPNetworkCoordinator.h" #import #import "TJPNetworkConfig.h" #import "TJPSessionDelegate.h" #import "TJPSessionProtocol.h" #import "TJPConcreteSession.h" #import "TJPNetworkDefine.h" #import "TJPReconnectPolicy.h" #import "TJPLightweightSessionPool.h" @interface TJPNetworkCoordinator () @property (nonatomic, strong) TJPNetworkConfig *currConfig; // 上次报告的状态 @property (nonatomic, assign) NetworkStatus lastReportedStatus; // 网络防抖 @property (nonatomic, strong) NSDate *lastNetworkChangeTime; @property (nonatomic, assign) NSTimeInterval networkChangeDebounceInterval; // 默认设为2秒 // 验证网络状态 @property (nonatomic, assign) BOOL isVerifyingConnectivity; @property (nonatomic, strong) NSTimer *connectivityVerifyTimer; @end @implementation TJPNetworkCoordinator #pragma mark - instance + (instancetype)shared { static TJPNetworkCoordinator *instace = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instace = [[self alloc] init]; }); return instace; } - (instancetype)init { if (self = [super init]) { _networkChangeDebounceInterval = 2; _sessionMap = [NSMapTable strongToStrongObjectsMapTable]; _sessionTypeMap = [NSMutableDictionary dictionary]; _sessionPool = [TJPLightweightSessionPool sharedPool]; // 初始化队列 [self setupQueues]; // 初始化网络监控 [self setupNetworkMonitoring]; // 初始化池配置 [self setupSessionPool]; } return self; } - (void)dealloc { [self cancelConnectivityVerification]; TJPLogDealloc(); } #pragma mark - Private Method - (void)setupQueues { // 串行队列,只处理会话 _sessionQueue = dispatch_queue_create("com.networkCoordinator.tjp.sessionQueue", DISPATCH_QUEUE_SERIAL); // 专用数据解析队列 并发高优先级 _parseQueue = dispatch_queue_create("com.networkCoordinator.tjp.parseQueue", DISPATCH_QUEUE_CONCURRENT); dispatch_set_target_queue(_parseQueue, dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0)); // 串行监控队列 _monitorQueue = dispatch_queue_create("com.networkCoordinator.tjp.monitorQueue", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(_monitorQueue, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)); } - (void)setupNetworkMonitoring { // 初始化网络监控 self.reachability = [Reachability reachabilityForInternetConnection]; __weak typeof(self) weakSelf = self; // 网络状态变更回调 self.reachability.reachableBlock = ^(Reachability *reachability) { [weakSelf handleNetworkStateChange:reachability]; }; self.reachability.unreachableBlock = ^(Reachability *reachability) { [weakSelf handleNetworkStateChange:reachability]; }; [self.reachability startNotifier]; } - (void)setupSessionPool { // 配置会话池 TJPSessionPoolConfig poolConfig = { .maxPoolSize = 3, // 每种类型最多3个会话 .maxIdleTime = 180, // 3分钟空闲超时 .cleanupInterval = 30, // 30秒清理一次 .maxReuseCount = 30 // 最多复用30次 }; [self.sessionPool startWithConfig:poolConfig]; // 预热常用类型的会话池 TJPNetworkConfig *chatConfig = [self defaultConfigForSessionType:TJPSessionTypeChat]; [self.sessionPool warmupPoolForType:TJPSessionTypeChat count:2 withConfig:chatConfig]; TJPLOG_INFO(@"[TJPNetworkCoordinator] 会话池初始化完成"); } - (void)handleNetworkStateChange:(Reachability *)reachability { NetworkStatus status = [reachability currentReachabilityStatus]; dispatch_async(self.monitorQueue, ^{ NSDate *now = [NSDate date]; NSTimeInterval timeSinceLastChange = 0; // 计算时间间隔 if (self.lastNetworkChangeTime) { timeSinceLastChange = [now timeIntervalSinceDate:self.lastNetworkChangeTime]; } // 详细记录网络变化信息 NSString *statusStr = [self networkStatusToString:status]; NSString *oldStatusStr = [self networkStatusToString:self.lastReportedStatus]; TJPLOG_INFO(@"=== 网络状态变化检测 === \n 当前状态: %@ (%d) \n 上次状态: %@ (%d) \n 时间间隔: %.2f秒 \n 是否在验证中: %@", statusStr, (int)status, oldStatusStr, (int)self.lastReportedStatus, timeSinceLastChange, self.isVerifyingConnectivity ? @"是" : @"否"); // 对WiFi连接使用更短的防抖时间 NSTimeInterval debounceInterval = (status == ReachableViaWiFi) ? 1.0 : self.networkChangeDebounceInterval; // 检查是否在防抖动时间内 if (self.lastNetworkChangeTime && [now timeIntervalSinceDate:self.lastNetworkChangeTime] < debounceInterval) { TJPLOG_INFO(@"[TJPNetworkCoordinator] 网络状态频繁变化,忽略当前变化"); return; } // 如果正在验证连通性,先取消之前的验证 if (self.isVerifyingConnectivity) { TJPLOG_INFO(@"[TJPNetworkCoordinator] 取消之前的连通性验证,开始新的验证"); [self cancelConnectivityVerification]; } // 更新最后变化时间 self.lastNetworkChangeTime = now; // 检查状态是否有变化 if (status == self.lastReportedStatus && self.lastReportedStatus != NotReachable) { // 如果状态相同且不是不可达状态,不重复处理 return; } // 更新状态 NetworkStatus oldStatus = self.lastReportedStatus; self.lastReportedStatus = status; // 记录状态变化 TJPLOG_INFO(@"[TJPNetworkCoordinator] 网络状态变更: %d -> %d", (int)oldStatus, (int)status); // 发送全局网络状态通知 [[NSNotificationCenter defaultCenter] postNotificationName:kNetworkStatusChangedNotification object:self userInfo:@{ @"status": @(status), @"oldStatus": @(oldStatus), @"statusString": statusStr }]; // 根据网络状态进行处理 [self handleNetworkStatusTransition:oldStatus toStatus:status]; }); } - (void)handleNetworkStatusTransition:(NetworkStatus)oldStatus toStatus:(NetworkStatus)newStatus { switch (newStatus) { case NotReachable: TJPLOG_INFO(@"[TJPNetworkCoordinator] 网络不可达,断开所有会话连接"); [self notifySessionsOfNetworkStatus:NO]; break; case ReachableViaWiFi: [self handleWiFiConnection:oldStatus]; break; case ReachableViaWWAN: [self handleCellularConnection:oldStatus]; break; } } - (void)handleWiFiConnection:(NetworkStatus)oldStatus { TJPLOG_INFO(@"[TJPNetworkCoordinator] WiFi网络连接,开始连通性验证"); // WiFi连接总是需要验证连通性,因为可能存在: // 1. 需要网页认证的WiFi // 2. DNS解析问题 // 3. 代理设置问题 [self verifyNetworkConnectivityWithRetry:^(BOOL isConnected) { if (isConnected) { TJPLOG_INFO(@"[TJPNetworkCoordinator] WiFi连通性验证成功,通知会话恢复连接"); dispatch_async(self.monitorQueue, ^{ [self notifySessionsOfNetworkStatus:YES]; }); } else { TJPLOG_WARN(@"[TJPNetworkCoordinator] WiFi连通性验证失败,可能需要认证或存在其他问题"); // WiFi连接但无法访问外网的情况 3秒后重试 [self scheduleConnectivityRetry:3.0]; } }]; } - (void)handleCellularConnection:(NetworkStatus)oldStatus { TJPLOG_INFO(@"[TJPNetworkCoordinator] 蜂窝网络连接"); if (oldStatus == NotReachable) { // 从无网络恢复到蜂窝网络,验证连通性 TJPLOG_INFO(@"[TJPNetworkCoordinator] 从无网络恢复到蜂窝网络,验证连通性"); [self verifyNetworkConnectivityWithRetry:^(BOOL isConnected) { if (isConnected) { TJPLOG_INFO(@"[TJPNetworkCoordinator] 蜂窝网络连通性验证成功"); dispatch_async(self.monitorQueue, ^{ [self notifySessionsOfNetworkStatus:YES]; }); } }]; } else { // WiFi切换到蜂窝网络,蜂窝网络通常比较稳定 TJPLOG_INFO(@"[TJPNetworkCoordinator] WiFi切换到蜂窝网络,直接通知连接恢复"); [self notifySessionsOfNetworkStatus:YES]; } } - (void)verifyNetworkConnectivityWithRetry:(void(^)(BOOL isConnected))completion { if (self.isVerifyingConnectivity) { TJPLOG_WARN(@"[TJPNetworkCoordinator] 已在进行连通性验证,跳过重复请求"); return; } self.isVerifyingConnectivity = YES; dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ // 使用多个测试点,提高准确性 NSArray *testUrls = @[ // 国内基础站点(检测基础网络) @"https://www.baidu.com", @"https://www.qq.com", // 运营商服务检测(检测强制门户/WiFi认证) @"http://connect.rom.miui.com", @"http://www.msftconnecttest.com", @"http://captive.apple.com/hotspot-detect.html" ]; __block NSInteger successCount = 0; __block NSInteger completedCount = 0; NSInteger totalCount = testUrls.count; NSTimeInterval timeout = 8.0; // 增加超时时间 dispatch_group_t group = dispatch_group_create(); for (NSString *urlString in testUrls) { dispatch_group_enter(group); NSURL *url = [NSURL URLWithString:urlString]; NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringLocalCacheData timeoutInterval:timeout]; NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { BOOL thisTestSuccess = NO; if (error == nil) { NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response; thisTestSuccess = (httpResponse.statusCode >= 200 && httpResponse.statusCode < 400); if (thisTestSuccess) { @synchronized(self) { successCount++; } } TJPLOG_INFO(@"[TJPNetworkCoordinator] 连通性测试 %@ - 状态码:%ld %@", urlString, (long)httpResponse.statusCode, thisTestSuccess ? @"✓" : @"✗"); } else { TJPLOG_WARN(@"[TJPNetworkCoordinator] 连通性测试 %@ - 错误:%@", urlString, error.localizedDescription); } @synchronized(self) { completedCount++; } dispatch_group_leave(group); }]; [task resume]; } // 等待所有请求完成或超时 dispatch_time_t timeout_time = dispatch_time(DISPATCH_TIME_NOW, (timeout + 2) * NSEC_PER_SEC); dispatch_group_wait(group, timeout_time); // 至少50%的测试成功才认为网络连通 BOOL isConnected = (successCount >= (totalCount / 2)); TJPLOG_INFO(@"[TJPNetworkCoordinator] 连通性验证完成: %ld/%ld 成功, 结果:%@ %@", (long)successCount, (long)totalCount, isConnected ? @"连通" : @"不连通", isConnected ? @"🟢" : @"🔴"); self.isVerifyingConnectivity = NO; if (completion) { dispatch_async(dispatch_get_main_queue(), ^{ completion(isConnected); }); } }); } - (void)cancelConnectivityVerification { self.isVerifyingConnectivity = NO; if (self.connectivityVerifyTimer) { [self.connectivityVerifyTimer invalidate]; self.connectivityVerifyTimer = nil; } } - (void)scheduleConnectivityRetry:(NSTimeInterval)delay { TJPLOG_INFO(@"[TJPNetworkCoordinator] 安排 %.1f 秒后重试连通性验证", delay); // 取消之前的定时器 if (self.connectivityVerifyTimer) { [self.connectivityVerifyTimer invalidate]; } self.connectivityVerifyTimer = [NSTimer scheduledTimerWithTimeInterval:delay repeats:NO block:^(NSTimer * _Nonnull timer) { TJPLOG_INFO(@"[TJPNetworkCoordinator] 定时器触发,重新检查网络状态"); [self handleNetworkStateChange:self.reachability]; }]; } - (void)handleSessionDisconnection:(id)session { if (!session) { TJPLOG_ERROR(@"[TJPNetworkCoordinator] 处理断开连接的会话为空"); return; } TJPDisconnectReason reason = [(TJPConcreteSession *)session disconnectReason]; NSString *sessionId = session.sessionId; // 使用全局队列处理重连逻辑,避免阻塞主要队列 dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ // 根据断开原因决定下一步操作 switch (reason) { case TJPDisconnectReasonNetworkError: case TJPDisconnectReasonHeartbeatTimeout: case TJPDisconnectReasonIdleTimeout: // 这些原因是需要尝试重连的 TJPLOG_INFO(@"[TJPNetworkCoordinator] 会话 %@ 因 %@ 断开,尝试自动重连", sessionId, [self reasonToString:reason]); [self scheduleReconnectForSession:session]; break; case TJPDisconnectReasonUserInitiated: case TJPDisconnectReasonForceReconnect: // 这些原因是不需要重连的,应直接移除会话 TJPLOG_INFO(@"[TJPNetworkCoordinator] 会话 %@ 因 %@ 断开,不会重连", sessionId, [self reasonToString:reason]); [self removeSession:session]; break; case TJPDisconnectReasonSocketError: { // 服务器关闭连接,需要根据业务策略决定是否重连 TJPLOG_WARN(@"[TJPNetworkCoordinator] 会话 %@ 因套接字错误断开,检查是否重连", sessionId); // 获取会话配置,决定是否重连 TJPConcreteSession *concreteSession = (TJPConcreteSession *)session; if (concreteSession.config.shouldReconnectAfterServerClose) { [self scheduleReconnectForSession:session]; } else { [self removeSession:session]; } break; } case TJPDisconnectReasonAppBackgrounded: { // 应用进入后台,根据配置决定是否保持连接 TJPLOG_INFO(@"[TJPNetworkCoordinator] 会话 %@ 因应用进入后台而断开", sessionId); TJPConcreteSession *concreteSessionBackground = (TJPConcreteSession *)session; if (concreteSessionBackground.config.shouldReconnectAfterBackground) { // 标记为需要在回到前台时重连 // concreteSessionBackground.needsReconnectOnForeground = YES; } else { [self removeSession:session]; } break; } default: TJPLOG_WARN(@"[TJPNetworkCoordinator] 会话 %@ 断开原因未知: %d,默认不重连", sessionId, (int)reason); [self removeSession:session]; break; } }); } - (NSString *)reasonToString:(TJPDisconnectReason)reason { switch (reason) { case TJPDisconnectReasonNone: return @"默认状态"; case TJPDisconnectReasonUserInitiated: return @"用户手动断开"; case TJPDisconnectReasonNetworkError: return @"网络错误"; case TJPDisconnectReasonHeartbeatTimeout: return @"心跳超时"; case TJPDisconnectReasonIdleTimeout: return @"空闲超时"; case TJPDisconnectReasonSocketError: return @"套接字错误"; case TJPDisconnectReasonForceReconnect: return @"强制重连"; default: return @"未知原因"; } } - (NSString *)networkStatusToString:(NetworkStatus)status { switch (status) { case NotReachable: return @"不可达"; case ReachableViaWiFi: return @"WiFi"; case ReachableViaWWAN: return @"蜂窝数据"; default: return [NSString stringWithFormat:@"未知(%d)", (int)status]; } } - (NSArray *)safeGetAllSessions { __block NSArray *sessions; dispatch_sync(self->_sessionQueue, ^{ sessions = [[_sessionMap objectEnumerator] allObjects]; }); return sessions; } //安全获取单个会话的方法 - (id)safeGetSessionWithId:(NSString *)sessionId { __block id session = nil; dispatch_sync(self->_sessionQueue, ^{ session = [self.sessionMap objectForKey:sessionId]; }); return session; } //获取当前会话总数 - (NSUInteger)sessionCount { __block NSUInteger count = 0; dispatch_sync(self->_sessionQueue, ^{ count = self.sessionMap.count; }); return count; } #pragma mark - Notification - (void)notifySessionsOfNetworkStatus:(BOOL)available { NSArray *sessions = [self safeGetAllSessions]; for (id session in sessions) { if (available) { // 通知会话网络恢复 [session networkDidBecomeAvailable]; } else { // 通知会话网络断开 [session networkDidBecomeUnavailable]; } } } #pragma mark - Public Method - (id)createSessionWithConfiguration:(TJPNetworkConfig *)config { return [self createSessionWithConfiguration:config type:TJPSessionTypeDefault]; } - (id)createSessionWithConfiguration:(TJPNetworkConfig *)config type:(TJPSessionType)type { if (!config) { TJPLOG_ERROR(@"[TJPNetworkCoordinator] 配置参数为空"); return nil; } _currConfig = config; __block id session = nil; // 不再直接创建session 而是从池中获取 session = [self.sessionPool acquireSessionForType:type withConfig:config]; // 验证获取到的会话是否有效 if (!session) { TJPLOG_ERROR(@"[TJPNetworkCoordinator] 从会话池获取会话失败,类型: %lu", (unsigned long)type); return nil; } // 设置会话属性 if ([session isKindOfClass:[TJPConcreteSession class]]) { TJPConcreteSession *concreteSession = (TJPConcreteSession *)session; // 先设置基本属性,确保会话稳定 concreteSession.sessionType = type; // 验证会话内部状态 if (!concreteSession.sessionId || concreteSession.sessionId.length == 0) { TJPLOG_ERROR(@"[TJPNetworkCoordinator] 会话sessionId无效,无法继续"); return nil; } concreteSession.delegate = self; // 验证代理设置成功 if (concreteSession.delegate != self) { TJPLOG_WARN(@"[TJPNetworkCoordinator] 会话代理设置失败: %@", concreteSession.sessionId); } else { TJPLOG_INFO(@"[TJPNetworkCoordinator] 会话代理设置成功: %@", concreteSession.sessionId); } } // 同步队列避免静态条件 dispatch_sync(self->_sessionQueue, ^{ // 再次验证 sessionId(防止在异步操作中被修改) if (!session.sessionId || session.sessionId.length == 0) { TJPLOG_ERROR(@"[TJPNetworkCoordinator] 会话ID在队列操作中变为无效"); return; } // 检查是否已存在相同 sessionId 的会话 id existingSession = [self.sessionMap objectForKey:session.sessionId]; if (existingSession) { TJPLOG_WARN(@"[TJPNetworkCoordinator] 发现重复sessionId: %@,移除旧会话", session.sessionId); [self.sessionMap removeObjectForKey:session.sessionId]; } // 加入活跃会话表 [self.sessionMap setObject:session forKey:session.sessionId]; // 记录会话类型映射 NSString *previousSessionId = self.sessionTypeMap[@(type)]; if (previousSessionId) { TJPLOG_INFO(@"[TJPNetworkCoordinator] 类型 %lu 的会话映射从 %@ 更新为 %@", (unsigned long)type, previousSessionId, session.sessionId); } // 记录会话类型映射 self.sessionTypeMap[@(type)] = session.sessionId; TJPLOG_INFO(@"[TJPNetworkCoordinator] 成功从池中获得会话: %@, 总活跃数 : %lu", session.sessionId, (unsigned long)self.sessionMap.count); }); return session; } // 根据类型获取会话 - (id)sessionForType:(TJPSessionType)type { __block id session = nil; dispatch_sync(self->_sessionQueue, ^{ NSString *sessionId = self.sessionTypeMap[@(type)]; if (sessionId) { session = [self.sessionMap objectForKey:sessionId]; } }); return session; } - (void)updateAllSessionsState:(TJPConnectState)state { dispatch_barrier_async(self->_sessionQueue, ^{ NSEnumerator *enumerator = [self.sessionMap objectEnumerator]; id session; while ((session = [enumerator nextObject])) { if ([session respondsToSelector:@selector(updateConnectionState:)]) { //事件驱动状态变更 [session updateConnectionState:state]; } } }); } - (void)removeSession:(id)session { // 移除逻辑修改 不再直接销毁 而是放入池中 dispatch_barrier_async(self->_sessionQueue, ^{ // 先从活跃会话表中移除 [self.sessionMap removeObjectForKey:session.sessionId]; // 从类型映射表中移除 TJPSessionType sessionType = TJPSessionTypeDefault; if ([session isKindOfClass:[TJPConcreteSession class]]) { sessionType = ((TJPConcreteSession *)session).sessionType; } NSString *currentSessionId = self.sessionTypeMap[@(sessionType)]; if ([currentSessionId isEqualToString:session.sessionId]) { [self.sessionTypeMap removeObjectForKey:@(sessionType)]; } TJPLOG_INFO(@"[TJPNetworkCoordinator] 移除活跃会话: %@, 剩下数量: %lu", session.sessionId, (unsigned long)self.sessionMap.count); // 新增归还到会话池逻辑 [self.sessionPool releaseSession:session]; }); } // 新增:强制移除会话(不放入池中) - (void)forceRemoveSession:(id)session { dispatch_barrier_async(self->_sessionQueue, ^{ // 从活跃会话表移除 [self.sessionMap removeObjectForKey:session.sessionId]; // 从类型映射移除 TJPSessionType sessionType = TJPSessionTypeDefault; if ([session isKindOfClass:[TJPConcreteSession class]]) { sessionType = ((TJPConcreteSession *)session).sessionType; } NSString *currentSessionId = self.sessionTypeMap[@(sessionType)]; if ([currentSessionId isEqualToString:session.sessionId]) { [self.sessionTypeMap removeObjectForKey:@(sessionType)]; } // 强制从池中移除(不复用) [self.sessionPool removeSession:session]; TJPLOG_INFO(@"[TJPNetworkCoordinator] 强制移除会话: %@", session.sessionId); }); } - (void)scheduleReconnectForSession:(id)session { dispatch_async(self->_sessionQueue, ^{ TJPConcreteSession *concreteSession = (TJPConcreteSession *)session; // 只有特定原因的断开才尝试重连 TJPDisconnectReason reason = concreteSession.disconnectReason; if (reason == TJPDisconnectReasonNetworkError || reason == TJPDisconnectReasonHeartbeatTimeout || reason == TJPDisconnectReasonIdleTimeout) { [concreteSession.reconnectPolicy attemptConnectionWithBlock:^{ [concreteSession connectToHost:concreteSession.host port:concreteSession.port]; }]; } }); } - (TJPNetworkConfig *)defaultConfigForSessionType:(TJPSessionType)type { TJPNetworkConfig *config = [TJPNetworkConfig new]; switch (type) { case TJPSessionTypeChat: // 聊天会话配置 - 重视低延迟 config.maxRetry = 5; config.heartbeat = 15.0; config.connectTimeout = 10.0; break; case TJPSessionTypeMedia: // 媒体会话配置 - 重视吞吐量 config.maxRetry = 3; config.heartbeat = 30.0; config.connectTimeout = 20.0; // 媒体会话可能需要更大的缓冲区 // config.readBufferSize = 65536; break; case TJPSessionTypeSignaling: // 信令会话配置 - 极致低延迟 config.maxRetry = 8; config.heartbeat = 5.0; config.connectTimeout = 5.0; break; default: // 默认配置 config.maxRetry = 5; config.heartbeat = 15.0; config.connectTimeout = 15.0; break; } return config; } #pragma mark - TJPSessionDelegate /// 接收到消息 - (void)session:(id)session didReceiveData:(NSData *)data { //分发处理 [[NSNotificationCenter defaultCenter] postNotificationName:kSessionDataReceiveNotification object:@{@"session": session, @"data": data}]; } /// 状态改变 - (void)session:(id)session stateChanged:(TJPConnectState)state { if ([state isEqualToString:TJPConnectStateDisconnected]) { [self handleSessionDisconnection:session]; } } #pragma mark - Manage Pool Method /** * 获取池中会话数量(新增) */ - (NSUInteger)getPooledSessionCount { TJPSessionPoolStats stats = [self.sessionPool getPoolStats]; return stats.pooledSessions; } /** * 获取总会话数量(活跃 + 池中) */ - (NSUInteger)getTotalSessionCount { NSUInteger activeCount = [self sessionCount]; NSUInteger pooledCount = [self getPooledSessionCount]; return activeCount + pooledCount; } /** * 预热会话池 */ - (void)warmupSessionPoolForType:(TJPSessionType)type count:(NSUInteger)count { TJPNetworkConfig *config = [self defaultConfigForSessionType:type]; [self.sessionPool warmupPoolForType:type count:count withConfig:config]; } /** * 获取会话池统计 */ - (TJPSessionPoolStats)getSessionPoolStats { return [self.sessionPool getPoolStats]; } /** * 调试:打印完整状态 */ - (void)logCompleteStatus { [self logSessionPoolStatus]; NSArray *activeSessions = [self safeGetAllSessions]; TJPLOG_INFO(@"=== 活跃会话状态 ==="); for (id session in activeSessions) { if ([session isKindOfClass:[TJPConcreteSession class]]) { TJPConcreteSession *concreteSession = (TJPConcreteSession *)session; TJPLOG_INFO(@"会话: %@, 类型: %lu, 状态: %@", session.sessionId, (unsigned long)concreteSession.sessionType, session.connectState); } } TJPLOG_INFO(@"=================="); } - (void)logSessionPoolStatus { [self.sessionPool logPoolStatus]; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Context/TJPMessageContext.h ================================================ // // TJPMessageContext.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // 消息上下文 用于记录相关元数据 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @interface TJPMessageContext : NSObject //基本信息 /// 消息唯一ID @property (nonatomic, copy) NSString *messageId; /// 会话ID @property (nonatomic, copy) NSString *sessionId; /// 消息类型 @property (nonatomic, assign) TJPMessageType messageType; /// 消息状态 @property (nonatomic, assign) TJPMessageState state; /// 消息内容 @property (nonatomic, strong, readonly) NSData *payload; /// 消息优先级 @property (nonatomic, assign) TJPMessagePriority priority; /// 加密类型 @property (nonatomic, assign) TJPEncryptType encryptType; /// 压缩类型 @property (nonatomic, assign) TJPCompressType compressType; //网络相关 /// 序列号 @property (nonatomic, assign) uint32_t sequence; //时间信息 /// 发送时间 @property (nonatomic, strong) NSDate *sendTime; /// 创建时间 @property (nonatomic, strong) NSDate *createTime; /// 送达时间 @property (nonatomic, strong) NSDate *deliveredTime; /// 已读时间 @property (nonatomic, strong) NSDate *readTime; /// 最后重试时间 @property (nonatomic, strong) NSDate *lastRetryTime; //重试信息 /// 重试次数 @property (nonatomic, assign) NSInteger retryCount; /// 最大重试次数 @property (nonatomic, assign) NSInteger maxRetryCount; /// 重试超时时间(秒) @property (nonatomic, assign) NSTimeInterval retryTimeout; /// 最后错误信息 @property (nonatomic, strong) NSError *lastError; /// 工厂创建方法 + (instancetype)contextWithData:(NSData *)data seq:(uint32_t)seq messageType:(TJPMessageType)messageType encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType sessionId:(NSString *)sessionId; //状态查询方法 - (BOOL)isInProgress; - (BOOL)isCompleted; - (BOOL)canRetry; - (NSTimeInterval)timeElapsed; - (BOOL)isWaitingForAck; - (BOOL)needsRetransmission; - (NSString *)stateDisplayString; //构建重传包 - (NSData *)buildRetryPacket; //是否重传 - (BOOL)shouldRetry; //计算经过时间 - (NSTimeInterval)timeElapsedSinceLastSend; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Context/TJPMessageContext.m ================================================ // // TJPMessageContext.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #import "TJPMessageContext.h" #import "TJPNetworkUtil.h" #import "TJPMessageBuilder.h" #import "TJPNetworkDefine.h" @interface TJPMessageContext () // 消息内容 @property (nonatomic, strong, readwrite) NSData *payload; @end @implementation TJPMessageContext + (instancetype)contextWithData:(NSData *)data seq:(uint32_t)seq messageType:(TJPMessageType)messageType encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType sessionId:(NSString *)sessionId { // 创建上下文实例对象 TJPMessageContext *context = [TJPMessageContext new]; context.payload = data; context.sequence = seq; context.messageType = messageType; context.encryptType = encryptType; context.compressType = compressType; context.sessionId = sessionId; // 默认重试设置 context.retryCount = 0; context.maxRetryCount = 3; // 默认最多重试3次 context.retryTimeout = 3.0; // 默认3秒超时 // 新增初始化 if (!context.messageId) { context.messageId = [[NSUUID UUID] UUIDString]; } context.state = TJPMessageStateCreated; context.createTime = [NSDate date]; return context; } - (BOOL)isInProgress { return self.state == TJPMessageStateSending || self.state == TJPMessageStateRetrying; } - (BOOL)isCompleted { return self.state == TJPMessageStateRead || self.state == TJPMessageStateCancelled || self.state == TJPMessageStateFailed; } - (BOOL)canRetry { return self.retryCount < self.maxRetryCount && (self.state == TJPMessageStateFailed || self.state == TJPMessageStateRetrying); } - (NSTimeInterval)timeElapsed { return [[NSDate date] timeIntervalSinceDate:self.createTime]; } // 新增状态查询方法 - (BOOL)isWaitingForAck { return self.state == TJPMessageStateSending || self.state == TJPMessageStateSent; } - (BOOL)needsRetransmission { if (![self isWaitingForAck]) { return NO; } NSTimeInterval elapsed = [[NSDate date] timeIntervalSinceDate:self.sendTime]; return elapsed > self.retryTimeout; } - (NSString *)stateDisplayString { switch (self.state) { case TJPMessageStateCreated: return @"已创建"; case TJPMessageStateSending: return @"发送中"; case TJPMessageStateSent: return @"已发送"; case TJPMessageStateDelivered: return @"已送达"; case TJPMessageStateRead: return @"已读"; case TJPMessageStateFailed: return @"发送失败"; case TJPMessageStateRetrying: return @"重试中"; case TJPMessageStateCancelled: return @"已取消"; default: return @"未知状态"; } } - (NSData *)buildRetryPacket { _retryCount++; // 更新发送时间 self.sendTime = [NSDate date]; self.lastRetryTime = [NSDate date]; return [TJPMessageBuilder buildPacketWithMessageType:self.messageType sequence:self.sequence payload:self.payload encryptType:self.encryptType compressType:self.compressType sessionID:self.sessionId]; } - (BOOL)shouldRetry { // 判断是否应该重试 return (self.retryCount < self.maxRetryCount) && (self.state == TJPMessageStateFailed || self.state == TJPMessageStateRetrying); } - (NSTimeInterval)timeElapsedSinceLastSend { // 计算自上次发送以来经过的时间 return [[NSDate date] timeIntervalSinceDate:self.sendTime]; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Error/TJPErrorUtil.h ================================================ // // TJPErrorUtil.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/14. // #import #import "TJPNetworkErrorDefine.h" #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @interface TJPErrorUtil : NSObject /** * 创建标准的网络错误对象 * @param code 错误代码 * @param description 错误描述 * @param userInfo 附加信息 (可选,如果为nil则创建空字典) * @return NSError实例 */ + (NSError *)errorWithCode:(TJPNetworkError)code description:(NSString *)description userInfo:(NSDictionary *)userInfo; /** * 创建带有失败原因的网络错误对象 * @param code 错误代码 * @param description 错误描述 * @param failureReason 失败原因 * @return NSError实例 */ + (NSError *)errorWithCode:(TJPNetworkError)code description:(NSString *)description failureReason:(NSString *)failureReason; /** * 将TLV解析错误代码转换为网络错误代码 * @param tlvError TLV解析错误代码 * @return 对应的网络错误代码 */ + (TJPNetworkError)networkErrorFromTLVError:(TJPTLVParseError)tlvError; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Error/TJPErrorUtil.m ================================================ // // TJPErrorUtil.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/14. // #import "TJPErrorUtil.h" // 实现网络层错误域 NSString * const TJPNetworkErrorDomain = @"com.tjp.network.error"; @implementation TJPErrorUtil + (NSError *)errorWithCode:(TJPNetworkError)code description:(NSString *)description userInfo:(NSDictionary *)userInfo { NSMutableDictionary *errorInfo = userInfo ? [NSMutableDictionary dictionaryWithDictionary:userInfo] : [NSMutableDictionary dictionary]; // 确保有错误描述 if (description) { errorInfo[NSLocalizedDescriptionKey] = description; } return [NSError errorWithDomain:TJPNetworkErrorDomain code:code userInfo:errorInfo]; } + (NSError *)errorWithCode:(TJPNetworkError)code description:(NSString *)description failureReason:(NSString *)failureReason { NSMutableDictionary *errorInfo = [NSMutableDictionary dictionary]; if (description) { errorInfo[NSLocalizedDescriptionKey] = description; } if (failureReason) { errorInfo[NSLocalizedFailureReasonErrorKey] = failureReason; } return [NSError errorWithDomain:TJPNetworkErrorDomain code:code userInfo:errorInfo]; } + (TJPNetworkError)networkErrorFromTLVError:(TJPTLVParseError)tlvError { switch (tlvError) { case TJPTLVParseErrorNone: return TJPErrorNone; case TJPTLVParseErrorIncompleteTag: return TJPErrorTLVIncompleteTag; case TJPTLVParseErrorIncompleteLength: return TJPErrorTLVIncompleteLength; case TJPTLVParseErrorIncompleteValue: return TJPErrorTLVIncompleteValue; case TJPTLVParseErrorNestedTooDeep: return TJPErrorTLVNestedTooDeep; case TJPTLVParseErrorDuplicateTag: return TJPErrorTLVDuplicateTag; case TJPTLVParseErrorInvalidNestedTag: return TJPErrorTLVParseError; default: return TJPErrorUnknown; } } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Error/TJPNETError.h ================================================ // // TJPNETError.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/19. // #import typedef NS_ENUM(NSInteger, TJPNETErrorCode) { TJPNETErrorConnectionFailed = 1000, // 连接失败 TJPNETErrorHeartbeatTimeout, // 心跳超时 TJPNETErrorInvalidProtocol, // 协议错误(魔数校验失败) TJPNETErrorSSLHandshakeFailed, // SSL握手失败 TJPNETErrorDataCorrupted, // 数据校验失败 TJPNETErrorACKTimeout // ACK确认超时 }; NS_ASSUME_NONNULL_BEGIN @interface TJPNETError : NSError + (instancetype)errorWithCode:(TJPNETErrorCode)code userInfo:(NSDictionary *)dict; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Error/TJPNETError.m ================================================ // // TJPNETError.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/19. // #import "TJPNETError.h" @implementation TJPNETError + (instancetype)errorWithCode:(TJPNETErrorCode)code userInfo:(NSDictionary *)dict { return [[self alloc] initWithDomain:@"com.tjpnetwork.error" code:code userInfo:dict]; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Error/TJPNETErrorHandler.h ================================================ // // TJPNETErrorHandler.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/19. // #import NS_ASSUME_NONNULL_BEGIN @class TJPNetworkManagerV1; @interface TJPNETErrorHandler : NSObject + (void)handleError:(NSError *)error inManager:(TJPNetworkManagerV1 *)manager; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Error/TJPNETErrorHandler.m ================================================ // // TJPNETErrorHandler.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/19. // #import "TJPNETErrorHandler.h" #import "TJPNETError.h" #import "TJPNetworkManagerV1.h" #import "TJPNetworkDefine.h" @implementation TJPNETErrorHandler + (void)handleError:(NSError *)error inManager:(TJPNetworkManagerV1 *)manager { switch ((TJPNETErrorCode)error.code) { case TJPNETErrorHeartbeatTimeout: [manager scheduleReconnect]; break; case TJPNETErrorInvalidProtocol: [manager resetConnection]; break; default: [self postNotificationForError:error]; break; } // TODO 后续接入AOP日志 NSLog(@"Network Error: %@", error.localizedDescription); } + (void)postNotificationForError:(NSError *)error { NSDictionary *userInfo = @{ @"error": error, @"timestamp": [NSDate date] }; [[NSNotificationCenter defaultCenter] postNotificationName:kNetworkFatalErrorNotification object:nil userInfo:userInfo]; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Heartbeat/TJPDynamicHeartbeat.h ================================================ // // TJPDynamicHeartbeat.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // 动态心跳 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @class TJPConcreteSession, TJPNetworkCondition, TJPSequenceManager; @protocol TJPSessionProtocol; @interface TJPDynamicHeartbeat : NSObject //网络质量采集器 @property (nonatomic, strong) TJPNetworkCondition *networkCondition; //序列号管理器 @property (nonatomic, strong) TJPSequenceManager *sequenceManager; //基础心跳时间 @property (nonatomic, assign) NSTimeInterval baseInterval; //当前心跳时间 @property (nonatomic, assign) NSTimeInterval currentInterval; //心跳队列 @property (nonatomic, strong) NSMutableDictionary *pendingHeartbeats; //*********************************************** //前后台心跳优化 //心跳模式及策略 @property (nonatomic, assign) TJPHeartbeatMode heartbeatMode; @property (nonatomic, assign) TJPHeartbeatStrategy heartbeatStrategy; //当前app状态 @property (nonatomic, assign) TJPAppState currentAppState; //配置参数 @property (nonatomic, strong) NSMutableDictionary *modeBaseIntervals; //基础频率 @property (nonatomic, strong) NSMutableDictionary *modeMinIntervals; //最小频率 @property (nonatomic, strong) NSMutableDictionary *modeMaxIntervals; //最大频率 //状态跟踪 @property (nonatomic, assign) NSTimeInterval lastModeChangeTime; //记录状态时间 @property (nonatomic, assign) BOOL isTransitioning; //是否为状态过度 @property (nonatomic, assign) NSUInteger backgroundTransitionCounter; //后台过渡次数 //后台任务支持 @property (nonatomic, assign) UIBackgroundTaskIdentifier backgroundTaskIdentifier; /// 修改心跳模式 - (void)changeToHeartbeatMode:(TJPHeartbeatMode)newMode; /** * 配置指定心跳模式的参数 * * @param baseInterval 基础心跳间隔(秒) * @param minInterval 最小心跳间隔(秒) * @param maxInterval 最大心跳间隔(秒) * @param mode 心跳模式 */ - (void)configureWithBaseInterval:(NSTimeInterval)baseInterval minInterval:(NSTimeInterval)minInterval maxInterval:(NSTimeInterval)maxInterval forMode:(TJPHeartbeatMode)mode; /** * 手动设置心跳模式 * * @param mode 心跳模式 * @param force 是否强制设置(忽略当前应用状态) */ - (void)setHeartbeatMode:(TJPHeartbeatMode)mode force:(BOOL)force; /// 获取心跳状态 用于Log日志或者调试问题 - (NSDictionary *)getHeartbeatStatus; //*********************************************** /// 初始化方法 - (instancetype)initWithBaseInterval:(NSTimeInterval)baseInterval seqManager:(TJPSequenceManager *)seqManager session:(id)session; /// 开始监听 - (void)startMonitoring; /// 停止监听 - (void)stopMonitoring; /// 更新session - (void)updateSession:(id)session; /// 调整心跳频率 - (void)adjustIntervalWithNetworkCondition:(TJPNetworkCondition *)condition; /// 发送心跳 - (void)sendHeartbeat; /// 发送心跳失败 - (void)sendHeartbeatFailed; /// 心跳回应 - (void)heartbeatACKNowledgedForSequence:(uint32_t)sequence; /// 心跳超时 - (void)handleHeaderbeatTimeoutForSequence:(uint32_t)sequence; /// 是否属于心跳 - (BOOL)isHeartbeatSequence:(uint32_t)sequence; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Heartbeat/TJPDynamicHeartbeat.m ================================================ // // TJPDynamicHeartbeat.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #import "TJPDynamicHeartbeat.h" #import "TJPConcreteSession.h" #import "TJPSessionProtocol.h" #import "TJPNetworkCondition.h" #import "TJPSequenceManager.h" #import "TJPNetworkDefine.h" #import "TJPMessageBuilder.h" @interface TJPDynamicHeartbeat () //@property (nonatomic, strong) NSMutableDictionary *pendingHeartbeats; @property (nonatomic, strong) dispatch_queue_t heartbeatQueue; @property (nonatomic, assign) NSInteger retryCount; @property (nonatomic, assign) NSInteger maxRetryCount; @end @implementation TJPDynamicHeartbeat { dispatch_source_t _heartbeatTimer; __weak id _session; } #pragma mark - Initialization - (instancetype)initWithBaseInterval:(NSTimeInterval)baseInterval seqManager:(nonnull TJPSequenceManager *)seqManager session:(id)session { if (self = [super init]) { // 初始化网络指标收集器 _networkCondition = [[TJPNetworkCondition alloc] init]; // 序列号管理器 _sequenceManager = seqManager; _baseInterval = baseInterval; _session = session; // 初始化字典 _pendingHeartbeats = [NSMutableDictionary dictionary]; // 专用串行队列,低优先级 _heartbeatQueue = dispatch_queue_create("com.tjp.dynamicHeartbeat.serialQueue", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(_heartbeatQueue, dispatch_get_global_queue(QOS_CLASS_UTILITY, 0)); // 最大重试次数 _maxRetryCount = 3; // 初始化心跳模式配置 [self initializeHeartbeatConfiguration]; // 注册应用生命周期通知 [self registerForAppLifecycleNotifications]; // 初始为前台模式 _heartbeatMode = TJPHeartbeatModeForeground; // 当前app状态为活跃 _currentAppState = TJPAppStateActive; // 默认平衡策略 _heartbeatStrategy = TJPHeartbeatStrategyBalanced; // 后台任务标志符 _backgroundTaskIdentifier = UIBackgroundTaskInvalid; TJPLOG_INFO(@"心跳管理器已初始化,等待连接成功后自动启动"); } return self; } - (void)dealloc { TJPLogDealloc(); [[NSNotificationCenter defaultCenter] removeObserver:self]; [self endBackgroundTask]; } #pragma mark - Configuration - (void)initializeHeartbeatConfiguration { // 初始化各模式下的基础间隔(秒) _modeBaseIntervals = [NSMutableDictionary dictionary]; [_modeBaseIntervals setObject:@(_baseInterval) forKey:@(TJPHeartbeatModeForeground)]; [_modeBaseIntervals setObject:@(_baseInterval * 2.5) forKey:@(TJPHeartbeatModeBackground)]; [_modeBaseIntervals setObject:@(_baseInterval * 4.0) forKey:@(TJPHeartbeatModeLowPower)]; // 初始化各模式下的最小间隔(秒) _modeMinIntervals = [NSMutableDictionary dictionary]; [_modeMinIntervals setObject:@(15.0) forKey:@(TJPHeartbeatModeForeground)]; [_modeMinIntervals setObject:@(30.0) forKey:@(TJPHeartbeatModeBackground)]; [_modeMinIntervals setObject:@(45.0) forKey:@(TJPHeartbeatModeLowPower)]; // 初始化各模式下的最大间隔(秒) _modeMaxIntervals = [NSMutableDictionary dictionary]; [_modeMaxIntervals setObject:@(300.0) forKey:@(TJPHeartbeatModeForeground)]; // 前台最大5分钟 [_modeMaxIntervals setObject:@(600.0) forKey:@(TJPHeartbeatModeBackground)]; // 后台最大10分钟 [_modeMaxIntervals setObject:@(900.0) forKey:@(TJPHeartbeatModeLowPower)]; // 低电量最大15分钟 } - (void)registerForAppLifecycleNotifications { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; // 应用进入前台 [center addObserver:self selector:@selector(handleAppWillEnterForeground:) name:UIApplicationWillEnterForegroundNotification object:nil]; // 应用变为活跃 [center addObserver:self selector:@selector(handleAppDidBecomeActive:) name:UIApplicationDidBecomeActiveNotification object:nil]; // 应用将变为非活跃 [center addObserver:self selector:@selector(handleAppWillResignActive:) name:UIApplicationWillResignActiveNotification object:nil]; // 应用进入后台 [center addObserver:self selector:@selector(handleAppDidEnterBackground:) name:UIApplicationDidEnterBackgroundNotification object:nil]; // 应用将被终止 [center addObserver:self selector:@selector(handleAppWillTerminate:) name:UIApplicationWillTerminateNotification object:nil]; // 低电量模式变化通知 if (@available(iOS 9.0, *)) { [center addObserver:self selector:@selector(handleLowPowerModeChanged:) name:NSProcessInfoPowerStateDidChangeNotification object:nil]; } // 内存警告通知 [center addObserver:self selector:@selector(handleMemoryWarning:) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; } #pragma mark - Public Method - (void)startMonitoring { dispatch_async(self.heartbeatQueue, ^{ // 如果定时器已存在,先停止当前的监控 if (self->_heartbeatTimer) { TJPLOG_INFO(@"心跳监控已在运行,先停止当前监控"); [self stopMonitoring]; } //重置状态 self.currentInterval = self.baseInterval; //清空 pendingHeartbeats 字典 [self.pendingHeartbeats removeAllObjects]; TJPLOG_INFO(@"heartbeat 准备开始发送心跳"); //发送心跳包 [self sendHeartbeat]; //获取旧定时器 if (self->_heartbeatTimer) { dispatch_source_cancel(self->_heartbeatTimer); self->_heartbeatTimer = nil; } //创建心跳包定时器 self->_heartbeatTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self.heartbeatQueue); //设置定时器的触发时间 [self _updateTimerInterval]; //设置定时器的事件处理顺序 dispatch_source_set_event_handler(self->_heartbeatTimer, ^{ [self sendHeartbeat]; }); //启动定时器 dispatch_resume(self->_heartbeatTimer); TJPLOG_INFO(@"心跳监控已启动,基础间隔: %.1f秒", self.baseInterval); }); } - (void)updateSession:(id)session { dispatch_async(self.heartbeatQueue, ^{ self->_session = session; TJPLOG_INFO(@"心跳管理器更新 session 引用"); // 同步状态检查 if (session && [session.connectState isEqualToString:TJPConnectStateConnected]) { // 如果会话已连接但心跳未启动,则启动心跳 if (!self->_heartbeatTimer) { TJPLOG_INFO(@"会话已连接但心跳未启动,自动启动心跳"); [self startMonitoring]; } } else { // 如果会话未连接但心跳已启动,则停止心跳 if (self->_heartbeatTimer) { TJPLOG_INFO(@"会话未连接但心跳仍在运行,自动停止心跳"); [self stopMonitoring]; } } }); } - (void)_updateTimerInterval { if (_heartbeatTimer) { uint64_t interval = (uint64_t)(_currentInterval * NSEC_PER_SEC); dispatch_source_set_timer(_heartbeatTimer, dispatch_time(DISPATCH_TIME_NOW, interval), interval, 1 * NSEC_PER_SEC); TJPLOG_INFO(@"心跳定时器间隔已更新为 %.1f 秒", _currentInterval); } } - (void)stopMonitoring { dispatch_async(self.heartbeatQueue, ^{ if (self->_heartbeatTimer) { dispatch_source_cancel(self->_heartbeatTimer); self->_heartbeatTimer = nil; } [self.pendingHeartbeats removeAllObjects]; self->_session = nil; }); } - (void)adjustIntervalWithNetworkCondition:(TJPNetworkCondition *)condition { dispatch_async(self.heartbeatQueue, ^{ //增强检查:如果session未连接,直接跳过 if (!self->_session || ![self->_session.connectState isEqualToString:TJPConnectStateConnected]) { TJPLOG_DEBUG(@"[TJPDynamicHeartbeat] 会话未连接,跳过心跳间隔调整"); return; } // 增加空指针保护,避免日志被污染 if (self->_heartbeatTimer == nil) { TJPLOG_DEBUG(@"[TJPDynamicHeartbeat] 心跳定时器未启动,跳过间隔调整"); return; } // 规则调整 [self _calculateQualityLevel:condition]; // 根据网络状态设置新间隔 [self _updateTimerInterval]; }); } - (void)sendHeartbeat { dispatch_async(self.heartbeatQueue, ^{ id strongSession = self->_session; if (!strongSession) { TJPLOG_ERROR(@"动态心跳管理的session已被销毁"); return; } if (![strongSession.connectState isEqualToString:TJPConnectStateConnected]) { TJPLOG_WARN(@"连接未就绪,当前连接状态为: %@", strongSession.connectState); [self sendHeartbeatFailed]; return; } //重置重试计数 self.retryCount = 0; //获取序列号 uint32_t sequence = [self.sequenceManager nextSequenceForCategory:TJPMessageCategoryHeartbeat]; TJPLOG_INFO(@"心跳包正在组装,准备发出 序列号为: %u", sequence); //组装心跳包 NSData *packet = [self buildHeartbeatPacket:sequence]; if (!packet) { TJPLOG_ERROR(@"心跳包构建失败,取消此次心跳"); return; } //记录发送时间(毫秒级) NSDate *sendTime = [NSDate date]; //log输出时间为转换后的北京时间 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss Z"]; [formatter setTimeZone:[NSTimeZone timeZoneForSecondsFromGMT:8*3600]]; // 东八区 NSString *beijingTime = [formatter stringFromDate:sendTime]; //将心跳包的序列号和发送时间存入 pendingHeartbeats 使用 dispatch_barrier_async 来安全地更新字典 dispatch_barrier_async(self.heartbeatQueue, ^{ //将心跳包的序列号和发送时间存入 pendingHeartbeats [self.pendingHeartbeats setObject:sendTime forKey:@(sequence)]; }); //发送心跳包 TJPLOG_INFO(@"heartbeatManager 准备将心跳包移交给 session 发送 当前北京时间:%@", beijingTime); [self->_session sendHeartbeat:packet]; // 通过统一方法调整间隔 [self adjustIntervalWithNetworkCondition:self.networkCondition]; // 设置动态超时(3倍RTT或最低15秒) NSTimeInterval timeout = MAX(self.networkCondition.roundTripTime * 3 / 1000.0, 15); //超时检测 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(timeout * NSEC_PER_SEC)), self.heartbeatQueue, ^{ if (self.pendingHeartbeats[@(sequence)]) { TJPLOG_INFO(@"触发序列号 %u 的心跳超时检测", sequence); [self handleHeaderbeatTimeoutForSequence:sequence]; } }); }); } - (void)sendHeartbeatFailed { dispatch_async(self.heartbeatQueue, ^{ TJPLOG_ERROR(@"心跳发送失败,准备重试"); id strongSession = self->_session; if (!strongSession) { return; } // 先增加重试计数,再进行判断 self.retryCount++; TJPLOG_INFO(@"当前重试次数: %ld/%ld", (long)self.retryCount, (long)self.maxRetryCount); if (self.retryCount >= self.maxRetryCount) { TJPLOG_ERROR(@"心跳连续失败 %ld 次,触发会话重建", (long)self.maxRetryCount); [strongSession forceReconnect]; self.retryCount = 0; return; } // 指数退避重试(2^retryCount 秒) NSTimeInterval delay = pow(2, self.retryCount - 1); // 1s 2s 4s 8s TJPLOG_INFO(@"安排在 %.1f 秒后进行第 %ld 次重试", delay, (long)self.retryCount); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), self.heartbeatQueue, ^{ [self sendHeartbeat]; }); }); } - (void)heartbeatACKNowledgedForSequence:(uint32_t)sequence { dispatch_async(self.heartbeatQueue, ^{ NSDate *sendTime = self.pendingHeartbeats[@(sequence)]; if (!sendTime) { TJPLOG_INFO(@"收到未知心跳包的ACK,序列号: %u", sequence); return; } TJPLOG_INFO(@"接收到 心跳ACK 数据包并进行处理"); //计算RTT并更新网络状态 NSTimeInterval rtt = [[NSDate date] timeIntervalSinceDate:sendTime] * 1000; //转毫秒 //更新网络状况 [self.networkCondition updateRTTWithSample:rtt]; [self.networkCondition updateLostWithSample:NO]; //收到ACK后主动调整间隔 [self adjustIntervalWithNetworkCondition:self.networkCondition]; //移除已确认心跳,避免超时逻辑误触发 [self _removeHeartbeatsForSequence:sequence]; }); } // 心跳超时处理 - 使用通知解耦 - (void)handleHeaderbeatTimeoutForSequence:(uint32_t)sequence { dispatch_async(self.heartbeatQueue, ^{ if (self.pendingHeartbeats[@(sequence)]) { TJPLOG_INFO(@"序列号为: %u的心跳包超时未确认 心跳丢失", sequence); // 更新丢包率 [self.networkCondition updateLostWithSample:YES]; // 触发动态调整 [self adjustIntervalWithNetworkCondition:self.networkCondition]; // 发送通知而不是直接操作session [[NSNotificationCenter defaultCenter] postNotificationName:kHeartbeatTimeoutNotification object:self userInfo:@{@"session": self->_session}]; // 移除超时的心跳 [self _removeHeartbeatsForSequence:sequence]; } }); } - (BOOL)isHeartbeatSequence:(uint32_t)sequence { // 判断序列号是否属于心跳类别 return [self.sequenceManager isSequenceForCategory:sequence category:TJPMessageCategoryHeartbeat]; } - (void)configureWithBaseInterval:(NSTimeInterval)baseInterval minInterval:(NSTimeInterval)minInterval maxInterval:(NSTimeInterval)maxInterval forMode:(TJPHeartbeatMode)mode { dispatch_async(self.heartbeatQueue, ^{ self.modeBaseIntervals[@(mode)] = @(baseInterval); self.modeMinIntervals[@(mode)] = @(minInterval); self.modeMaxIntervals[@(mode)] = @(maxInterval); TJPLOG_INFO(@"已配置模式 %lu:base=%.1fs, min=%.1fs, max=%.1fs", (unsigned long)mode, baseInterval, minInterval, maxInterval); // 如果是当前模式,立即应用新配置 if (self.heartbeatMode == mode) { self.baseInterval = baseInterval; [self adjustIntervalWithNetworkCondition:self.networkCondition]; } }); } - (void)setHeartbeatMode:(TJPHeartbeatMode)mode force:(BOOL)force { dispatch_async(self.heartbeatQueue, ^{ if (force || [self canSwitchToMode:mode]) { [self changeToHeartbeatMode:mode]; } else { TJPLOG_WARN(@"当前应用状态不适合切换至模式: %lu", (unsigned long)mode); } }); } #pragma mark - Notification Method - (void)handleAppWillEnterForeground:(NSNotification *)notifacation { dispatch_async(self.heartbeatQueue, ^{ TJPLOG_INFO(@"应用即将进入前台,准备心跳模式转换"); self.currentAppState = TJPAppStateActive; self.isTransitioning = YES; self.lastModeChangeTime = [[NSDate date] timeIntervalSince1970]; // 立即发送一次心跳确认当前状态 [self sendHeartbeat]; TJPLOG_INFO(@"已发送立即心跳以检测连接状态"); }); } - (void)handleAppDidBecomeActive:(NSNotification *)notification { dispatch_async(self.heartbeatQueue, ^{ TJPLOG_INFO(@"应用已变为活跃状态,切换到前台心跳模式"); // 改变心跳模式 [self changeToHeartbeatMode:TJPHeartbeatModeForeground]; // 结束后台任务 [self endBackgroundTask]; // 延迟解除过渡状态 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), self.heartbeatQueue, ^{ self.isTransitioning = NO; }); TJPLOG_INFO(@"已切换到前台心跳模式,当前间隔为 %.1f 秒", self.currentInterval); }); } - (void)handleAppWillResignActive:(NSNotification *)notification { dispatch_async(self.heartbeatQueue, ^{ TJPLOG_INFO(@"应用即将变为非活跃状态"); // 此阶段通常无需调整心跳,等待可能的后台状态 self.currentAppState = TJPAppStateInactive; }); } - (void)handleAppDidEnterBackground:(NSNotification *)notification { dispatch_async(self.heartbeatQueue, ^{ TJPLOG_INFO(@"应用已进入后台状态,切换到后台心跳模式"); self.currentAppState = TJPAppStateBackground; self.backgroundTransitionCounter++; self.lastModeChangeTime = [[NSDate date] timeIntervalSince1970]; // 开始后台任务以确保有足够时间完成心跳调整 [self beginBackgroundTask]; // 切换到后台模式 [self changeToHeartbeatMode:TJPHeartbeatModeBackground]; // 此时心跳间隔已经调整为后台模式的间隔,约为90秒左右 TJPLOG_INFO(@"已切换到后台心跳模式,当前间隔为 %.1f 秒", self.currentInterval); }); } - (void)handleAppWillTerminate:(NSNotification *)notification { dispatch_async(self.heartbeatQueue, ^{ TJPLOG_INFO(@"应用即将终止"); self.currentAppState = TJPAppStateTerminated; // 应用将被杀死,停止心跳 [self stopMonitoring]; // 结束后台任务 [self endBackgroundTask]; }); } - (void)handleLowPowerModeChanged:(NSNotification *)notification { if (@available(iOS 9.0, *)) { dispatch_async(self.heartbeatQueue, ^{ BOOL isLowPowerModeEnabled = [NSProcessInfo processInfo].lowPowerModeEnabled; if (isLowPowerModeEnabled) { TJPLOG_INFO(@"设备进入低电量模式,调整心跳策略"); [self changeToHeartbeatMode:TJPHeartbeatModeLowPower]; } else { TJPLOG_INFO(@"设备退出低电量模式,恢复正常心跳策略"); // 根据应用当前状态决定心跳模式 TJPHeartbeatMode newMode = (self.currentAppState == TJPAppStateBackground) ? TJPHeartbeatModeBackground : TJPHeartbeatModeForeground; [self changeToHeartbeatMode:newMode]; } }); } } - (void)handleMemoryWarning:(NSNotification *)notification { dispatch_async(self.heartbeatQueue, ^{ TJPLOG_WARN(@"收到内存警告,临时调整心跳策略"); // 暂时提高心跳间隔以减少资源消耗 self.currentInterval = MIN(self.currentInterval * 1.5, [self.modeMaxIntervals[@(self.heartbeatMode)] doubleValue]); if (self->_heartbeatTimer) { [self _updateTimerInterval]; } // 30秒后恢复正常策略 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(30 * NSEC_PER_SEC)), self.heartbeatQueue, ^{ // 重新计算心跳间隔 [self adjustIntervalWithNetworkCondition:self.networkCondition]; }); }); } #pragma mark - Private Method - (void)_calculateQualityLevel:(TJPNetworkCondition *)condition { if (condition.qualityLevel == TJPNetworkQualityPoor) { //恶劣网络大幅降低 _currentInterval = _baseInterval * 2.5; }else if (condition.qualityLevel == TJPNetworkQualityFair || condition.qualityLevel == TJPNetworkQualityUnknown) { //未知网络&&网络不佳时降低频率 _currentInterval = _baseInterval * 1.5; }else { //基于滑动窗口动态调整 CGFloat rttFactor = condition.roundTripTime / 200.0; _currentInterval = _baseInterval * MAX(rttFactor, 1.0); } //基于心跳模式应用策略调整 switch (self.heartbeatStrategy) { case TJPHeartbeatStrategyAggressive: // 激进策略,更频繁的心跳 _currentInterval *= 0.8; break; case TJPHeartbeatStrategyConservative: // 保守策略,更节省的心跳 _currentInterval *= 1.2; break; case TJPHeartbeatStrategyBalanced: default: // 平衡策略,不做额外调整 break; } //处于过渡状态时 适当减小间隔 if (self.isTransitioning) { _currentInterval = MIN(_currentInterval, [self.modeMinIntervals[@(self.heartbeatMode)] doubleValue] * 1.5); } //增加随机扰动 抗抖动设计 单元测试时需要注释 CGFloat randomFactor = 0.9 + (arc4random_uniform(200) / 1000.0); //0.9 - 1.1 _currentInterval *= randomFactor; // 应用模式特定的限制 NSNumber *minIntervalObj = self.modeMinIntervals[@(self.heartbeatMode)]; NSNumber *maxIntervalObj = self.modeMaxIntervals[@(self.heartbeatMode)]; NSTimeInterval minInterval = minIntervalObj ? [minIntervalObj doubleValue] : 15.0; NSTimeInterval maxInterval = maxIntervalObj ? [maxIntervalObj doubleValue] : 300.0; //再设置硬性限制 防止出现夸张边界问题 15-300s _currentInterval = MIN(MAX(_currentInterval, minInterval), maxInterval); } - (void)_removeHeartbeatsForSequence:(uint32_t)sequence { dispatch_barrier_async(self.heartbeatQueue, ^{ if (!sequence) return; [self.pendingHeartbeats removeObjectForKey:@(sequence)]; }); } - (NSData *)buildHeartbeatPacket:(uint32_t)sequence { NSData *emptyPayload = [NSData data]; // 心跳包通常没有负载 NSString *sessionID = _session.sessionId; // 使用TJPMessageBuilder统一构建心跳包 NSData *packet = [TJPMessageBuilder buildPacketWithMessageType:TJPMessageTypeHeartbeat sequence:sequence payload:emptyPayload encryptType:TJPEncryptTypeNone compressType:TJPCompressTypeNone sessionID:sessionID]; if (!packet) { TJPLOG_ERROR(@"心跳包构建失败"); return nil; } return packet; } - (void)changeToHeartbeatMode:(TJPHeartbeatMode)newMode { dispatch_async(self.heartbeatQueue, ^{ if (newMode == self.heartbeatMode) { return; } TJPLOG_INFO(@"心跳模式切换: %lu -> %lu", (unsigned long)self.heartbeatMode, (unsigned long)newMode); // 分别记录旧模式和新模式 TJPHeartbeatMode oldMode = self.heartbeatMode; self.heartbeatMode = newMode; // 记录模式变更事件 self.lastModeChangeTime = [[NSDate date] timeIntervalSince1970]; // 如果新模式为暂停,需要停止定时器 if (newMode == TJPHeartbeatModeSuspended) { if (self->_heartbeatTimer) { dispatch_source_cancel(self->_heartbeatTimer); self->_heartbeatTimer = nil; TJPLOG_INFO(@"心跳已暂停"); } return; } //读取配置中新模式的基础心跳频率 NSNumber *intervalObj = self.modeBaseIntervals[@(newMode)]; if (!intervalObj) { TJPLOG_ERROR(@"未找到模式 %lu 的基础间隔配置,使用默认值", (unsigned long)newMode); intervalObj = @(self.baseInterval); } //更新基础间隔 self.baseInterval = [intervalObj doubleValue]; TJPLOG_INFO(@"心跳基础间隔更新: %.1f -> %.1f 秒", self.baseInterval, [intervalObj doubleValue]); // 发送模式变更通知 [[NSNotificationCenter defaultCenter] postNotificationName:kHeartbeatModeChangedNotification object:self userInfo:@{ @"oldMode": @(oldMode), @"newMode": @(newMode) }]; // 记录埋点事件 // [[TJPMetricsCollector sharedInstance] addEvent:TJPMetricsEventHeartbeatModeChanged // parameters:@{@"oldMode": @(oldMode), @"newMode": @(newMode)}]; //更新当前模式下心跳频率 [self adjustIntervalWithNetworkCondition:self.networkCondition]; // 如果心跳定时器未启动但需要启动,则启动 if (!self->_heartbeatTimer && newMode != TJPHeartbeatModeSuspended) { TJPLOG_INFO(@"启动心跳定时器"); [self startMonitoring]; } }); } - (void)setHeartbeatStrategy:(TJPHeartbeatStrategy)heartbeatStrategy { dispatch_async(self.heartbeatQueue, ^{ if (self.heartbeatStrategy == heartbeatStrategy) { return; } TJPLOG_INFO(@"心跳策略变更: %lu -> %lu", (unsigned long)self.heartbeatStrategy, (unsigned long)heartbeatStrategy); self.heartbeatStrategy = heartbeatStrategy; // 立即应用新策略 [self adjustIntervalWithNetworkCondition:self.networkCondition]; }); } - (BOOL)canSwitchToMode:(TJPHeartbeatMode)mode { switch (mode) { case TJPHeartbeatModeForeground: return self.currentAppState == TJPAppStateActive; case TJPHeartbeatModeBackground: return self.currentAppState == TJPAppStateBackground || self.currentAppState == TJPAppStateInactive; case TJPHeartbeatModeLowPower: if (@available(iOS 9.0, *)) { return [NSProcessInfo processInfo].lowPowerModeEnabled; } return NO; case TJPHeartbeatModeSuspended: // 暂停模式随时可切换 return YES; default: return NO; } } - (NSDictionary *)getHeartbeatStatus { __block NSDictionary *status; dispatch_sync(self.heartbeatQueue, ^{ status = @{ @"currentMode": @(self.heartbeatMode), @"currentStrategy": @(self.heartbeatStrategy), @"appState": @(self.currentAppState), @"baseInterval": @(self.baseInterval), @"currentInterval": @(self.currentInterval), @"pendingHeartbeats": @(self.pendingHeartbeats.count), @"networkQuality": @(self.networkCondition.qualityLevel), @"roundTripTime": @(self.networkCondition.roundTripTime), @"packetLossRate": @(self.networkCondition.packetLossRate), @"lastModeChangeTime": @(self.lastModeChangeTime), @"isTransitioning": @(self.isTransitioning), @"backgroundTransitions": @(self.backgroundTransitionCounter) }; }); return status; } #pragma mark - Background Task - (void)beginBackgroundTask { if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) { //当前应用进入后台任务模式 [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier]; } self.backgroundTaskIdentifier = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{ dispatch_async(self.heartbeatQueue, ^{ TJPLOG_WARN(@"后台执行时间即将耗尽,调整心跳策略"); // 记录埋点事件 // [[TJPMetricsCollector sharedInstance] addEvent:@"heartbeat_background_expiring" parameters:nil]; // 获取当前模式的最大间隔 NSNumber *maxIntervalObj = self.modeMaxIntervals[@(self.heartbeatMode)]; NSTimeInterval maxInterval = maxIntervalObj ? [maxIntervalObj doubleValue] : 600.0; // 设置为最大间隔以最大程度节约资源 self.currentInterval = maxInterval; if (self->_heartbeatTimer) { [self _updateTimerInterval]; } // 结束后台任务 [self endBackgroundTask]; }); }]; } - (void)endBackgroundTask { if (self.backgroundTaskIdentifier != UIBackgroundTaskInvalid) { [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskIdentifier]; self.backgroundTaskIdentifier = UIBackgroundTaskInvalid; TJPLOG_INFO(@"结束后台任务"); } } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/IMClient/TJPIMClient.h ================================================ // // TJPIMClient.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // TCP框架入口 门面设计模式屏蔽底层实现 #import #import "TJPMessageProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPIMClient : NSObject /// 单例类 + (instancetype)shared; /** * 使用默认会话类型 */ - (void)connectToHost:(NSString *)host port:(uint16_t)port; /** * 连接指定类型的会话 */ - (void)connectToHost:(NSString *)host port:(uint16_t)port forType:(TJPSessionType)type; /** * 断开指定类型的会话 */ - (void)disconnectSessionType:(TJPSessionType)type; /** * 断开所有会话 */ - (void)disconnectAll; /** * 兼容原有方法(断开默认会话) */ - (void)disconnect; /** * 通过指定类型的会话发送消息 */ - (void)sendMessage:(id)message throughType:(TJPSessionType)type; /** * 兼容原有方法(使用默认会话类型) */ - (void)sendMessage:(id)message; /** * 自动路由发送消息 */ - (void)sendMessageWithAutoRoute:(id)message; /** * 带回调的发送方法 */ - (NSString *)sendMessage:(id)message throughType:(TJPSessionType)type completion:(void(^)(NSString *msgId, NSError *error))completion; - (NSString *)sendMessage:(id)message throughType:(TJPSessionType)type encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType completion:(void (^)(NSString *msgId, NSError *error))completion; /** * 检查指定类型的会话是否已连接 */ - (BOOL)isConnectedForType:(TJPSessionType)type; /** * 检查指定类型的会话是否已断开连接 */ - (BOOL)isDisConnectedForType:(TJPSessionType)type; /** * 获取指定类型会话的连接状态 */ - (TJPConnectState)getConnectionStateForType:(TJPSessionType)type; /** * 配置消息内容类型到会话类型的路由 */ - (void)configureRouting:(TJPContentType)contentType toSessionType:(TJPSessionType)sessionType; /** * 获取所有连接状态 */ - (NSDictionary *)getAllConnectionStates; - (BOOL)isStateConnected:(TJPConnectState)state; - (BOOL)isStateConnecting:(TJPConnectState)state; - (BOOL)isStateDisconnected:(TJPConnectState)state; - (BOOL)isStateDisconnecting:(TJPConnectState)state; - (BOOL)isStateConnectedOrConnecting:(TJPConnectState)state; - (BOOL)isStateDisconnectedOrDisconnecting:(TJPConnectState)state; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/IMClient/TJPIMClient.m ================================================ // // TJPIMClient.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // #import "TJPIMClient.h" #import "TJPNetworkCoordinator.h" #import "TJPConcreteSession.h" #import "TJPNetworkConfig.h" #import "TJPNetworkDefine.h" #import "TJPErrorUtil.h" @interface TJPIMClient () // 通道管理 @property (nonatomic, strong) NSMutableDictionary> *channels; // 消息类型到会话类型的映射 @property (nonatomic, strong) NSMutableDictionary *contentTypeToSessionType; // 连接状态跟踪 @property (nonatomic, strong) NSMutableDictionary *connectionStates; @end @implementation TJPIMClient + (instancetype)shared { static TJPIMClient *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[self alloc] init]; }); return instance; } - (instancetype)init { if (self = [super init]) { _channels = [NSMutableDictionary dictionary]; _contentTypeToSessionType = [NSMutableDictionary dictionary]; _connectionStates = [NSMutableDictionary dictionary]; // 设置默认映射 [self setupDefaultRouting]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSessionReacquisition:) name:kSessionNeedsReacquisitionNotification object:nil]; } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - Public Method //连接指定类型会话 - (void)connectToHost:(NSString *)host port:(uint16_t)port forType:(TJPSessionType)type { if (host.length == 0) { TJPLOG_ERROR(@"[TJPIMClient] 主机地址不能为空"); return; } // 检查是否已有该类型的连接 id existingSession = self.channels[@(type)]; if (existingSession) { TJPConnectState currentState = [self getConnectionStateForType:type]; if (currentState == TJPConnectStateConnected || currentState == TJPConnectStateConnecting) { TJPLOG_INFO(@"[TJPIMClient] 类型 %lu 已有活跃连接,跳过重复连接", (unsigned long)type); return; } } // 获取配置 TJPNetworkConfig *config = [[TJPNetworkCoordinator shared] defaultConfigForSessionType:type]; // 设置主机和端口 config.host = host; config.port = port; // 获取会话 优先从池中获取 id session = [[TJPNetworkCoordinator shared] createSessionWithConfiguration:config type:type]; if (!session) { TJPLOG_ERROR(@"[TJPIMClient] 获取会话失败,类型: %lu", (unsigned long)type); return; } // 保存到通道 self.channels[@(type)] = session; self.connectionStates[@(type)] = TJPConnectStateConnecting; TJPLOG_INFO(@"[TJPIMClient] 获取会话成功: %@,开始设置KVO", session.sessionId ?: @"nil"); // 监听会话状态变化 [self observeSessionStateChanges:session forType:type]; [session connectToHost:host port:port]; TJPLOG_INFO(@"[TJPIMClient] 开始连接类型 %lu 的会话,目标: %@:%u", (unsigned long)type, host, port); } // 兼容原有方法(使用默认会话类型) - (void)connectToHost:(NSString *)host port:(uint16_t)port { [self connectToHost:host port:port forType:TJPSessionTypeDefault]; } - (void)disconnectSessionType:(TJPSessionType)type { id session = self.channels[@(type)]; if (session) { TJPLOG_INFO(@"[TJPIMClient] 断开类型 %lu 的会话连接", (unsigned long)type); [session disconnectWithReason:TJPDisconnectReasonUserInitiated]; [self cleanupSessionForType:type]; } else { TJPLOG_INFO(@"[TJPIMClient] 类型 %lu 的会话不存在,无需断开", (unsigned long)type); } } - (void)disconnectAll { NSArray *allTypes = [self.channels.allKeys copy]; TJPLOG_INFO(@"[TJPIMClient] 断开所有会话连接,共 %lu 个", (unsigned long)allTypes.count); for (NSNumber *key in allTypes) { TJPSessionType type = [key unsignedIntegerValue]; [self disconnectSessionType:type]; } } - (void)disconnect { [self disconnectSessionType:TJPSessionTypeDefault]; } // 通过指定通道的session发送消息 - (void)sendMessage:(id)message throughType:(TJPSessionType)type { [self sendMessage:message throughType:type completion:^(NSString * _Nonnull messageId, NSError * _Nonnull error) { if (error) { TJPLOG_ERROR(@"[TJPIMClient] 消息发送失败: %@", error); } else { TJPLOG_INFO(@"[TJPIMClient] 消息已发送: %@", messageId); } }]; } // 兼容原有方法(使用默认会话类型) - (void)sendMessage:(id)message { [self sendMessage:message throughType:TJPSessionTypeDefault]; } // 自动路由版本的发送方法 - (void)sendMessageWithAutoRoute:(id)message { // 获取消息内容类型 TJPContentType contentType = message.contentType; // 查找会话类型 NSNumber *sessionTypeNum = self.contentTypeToSessionType[@(contentType)]; TJPSessionType sessionType = sessionTypeNum ? [sessionTypeNum unsignedIntegerValue] : TJPSessionTypeDefault; TJPLOG_INFO(@"[TJPIMClient] 自动路由消息,内容类型: %lu -> 会话类型: %lu", (unsigned long)contentType, (unsigned long)sessionType); // 调用发送方法 [self sendMessage:message throughType:sessionType]; } - (NSString *)sendMessage:(id)message throughType:(TJPSessionType)type completion:(nonnull void (^)(NSString *msgId, NSError *error))completion { return [self sendMessage:message throughType:type encryptType:TJPEncryptTypeCRC32 compressType:TJPCompressTypeZlib completion:completion]; } - (NSString *)sendMessage:(id)message throughType:(TJPSessionType)type encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType completion:(void (^)(NSString * msgId, NSError *error))completion { id session = self.channels[@(type)]; if (!session) { TJPLOG_INFO(@"[TJPIMClient] 未找到类型为 %lu 的会话通道", (unsigned long)type); NSError *error = [TJPErrorUtil errorWithCode:TJPErrorConnectionLost description:@"未找到会话通道" userInfo:@{}]; if (completion) completion(@"", error); return nil; } // 检查连接状态 TJPConnectState currentState = [self getConnectionStateForType:type]; if (currentState != TJPConnectStateConnected) { TJPLOG_INFO(@"[TJPIMClient] 当前状态发送消息失败,当前状态为: %@", currentState); return nil; } NSData *tlvData = [message tlvData]; if (!tlvData) { TJPLOG_ERROR(@"[TJPIMClient] 消息序列化失败,无法发送"); return nil; } NSString *messageId = [session sendData:tlvData messageType:message.messageType encryptType:encryptType compressType:compressType completion:completion]; TJPLOG_INFO(@"[TJPIMClient] 通过类型 %lu 的会话发送消息成功,大小: %lu 字节", (unsigned long)type, (unsigned long)tlvData.length); return messageId; } #pragma mark - State Management - (BOOL)isConnectedForType:(TJPSessionType)type { TJPConnectState state = [self getConnectionStateForType:type]; return [self isStateConnected:state]; } - (BOOL)isDisConnectedForType:(TJPSessionType)type { TJPConnectState state = [self getConnectionStateForType:type]; return [self isStateDisconnected:state]; } - (TJPConnectState)getConnectionStateForType:(TJPSessionType)type { id session = self.channels[@(type)]; if (!session) { return TJPConnectStateDisconnected; } // 直接从session获取最新状态 return session.connectState; } // 获取所有连接状态 - (NSDictionary *)getAllConnectionStates { NSMutableDictionary *states = [NSMutableDictionary dictionary]; for (NSNumber *typeKey in self.channels.allKeys) { TJPSessionType type = [typeKey unsignedIntegerValue]; TJPConnectState state = [self getConnectionStateForType:type]; states[typeKey] = state; } return [states copy]; } #pragma mark - State Helper Methods - (BOOL)isStateConnected:(TJPConnectState)state { return [state isEqualToString:TJPConnectStateConnected]; } - (BOOL)isStateConnecting:(TJPConnectState)state { return [state isEqualToString:TJPConnectStateConnecting]; } - (BOOL)isStateDisconnected:(TJPConnectState)state { return [state isEqualToString:TJPConnectStateDisconnected]; } - (BOOL)isStateDisconnecting:(TJPConnectState)state { return [state isEqualToString:TJPConnectStateDisconnecting]; } - (BOOL)isStateConnectedOrConnecting:(TJPConnectState)state { return [self isStateConnected:state] || [self isStateConnecting:state]; } - (BOOL)isStateDisconnectedOrDisconnecting:(TJPConnectState)state { return [self isStateDisconnected:state] || [self isStateDisconnecting:state]; } #pragma mark - KVO and State Change Handling - (void)observeSessionStateChanges:(id)session forType:(TJPSessionType)type { if (!session) { TJPLOG_ERROR(@"[TJPIMClient] observeSessionStateChanges 收到 nil session"); return; } // 确保session是TJPConcreteSession类型(支持KVO) if (![session isKindOfClass:[TJPConcreteSession class]]) { TJPLOG_ERROR(@"[TJPIMClient] Session 类型不支持KVO: %@", [session class]); return; } TJPConcreteSession *concreteSession = (TJPConcreteSession *)session; // 验证会话状态 if (!concreteSession.sessionId || concreteSession.sessionId.length == 0) { TJPLOG_ERROR(@"[TJPIMClient] 会话sessionId无效,无法设置KVO"); return; } @try { // 添加KVO监听 [concreteSession addObserver:self forKeyPath:@"connectState" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:(__bridge void *)(@(type))]; TJPLOG_INFO(@"[TJPIMClient] KVO设置成功,会话: %@", concreteSession.sessionId); } @catch (NSException *exception) { TJPLOG_ERROR(@"[TJPIMClient] KVO设置异常: %@,会话: %@", exception.reason, concreteSession.sessionId ?: @"nil"); } } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if ([keyPath isEqualToString:@"connectState"] && [object conformsToProtocol:@protocol(TJPSessionProtocol)]) { TJPSessionType type = [(__bridge NSNumber *)context unsignedIntegerValue]; // 正确获取新状态(字符串类型) TJPConnectState newState = change[NSKeyValueChangeNewKey]; TJPConnectState oldState = change[NSKeyValueChangeOldKey]; // 验证状态值 if (!newState || ![newState isKindOfClass:[NSString class]]) { TJPLOG_ERROR(@"收到无效的状态变化,类型: %lu", (unsigned long)type); return; } TJPLOG_INFO(@"收到KVO状态变化,类型: %lu,%@ -> %@", (unsigned long)type, oldState ?: @"nil", newState); [self handleSessionStateChange:newState forType:type session:object]; } } - (void)handleSessionStateChange:(TJPConnectState)newState forType:(TJPSessionType)type session:(id)session { // 更新本地状态缓存 self.connectionStates[@(type)] = newState; TJPLOG_INFO(@"类型 %lu 的会话状态变化: %@", (unsigned long)type, newState); // 根据新状态执行相应操作 if ([self isStateConnected:newState]) { TJPLOG_INFO(@"类型 %lu 的会话连接成功", (unsigned long)type); [self handleSessionConnected:session type:type]; } else if ([self isStateDisconnected:newState]) { TJPLOG_INFO(@"类型 %lu 的会话已断开", (unsigned long)type); [self handleSessionDisconnected:session type:type]; } else if ([self isStateConnecting:newState]) { TJPLOG_INFO(@"类型 %lu 的会话正在连接", (unsigned long)type); [self handleSessionConnecting:session type:type]; } else if ([self isStateDisconnecting:newState]) { TJPLOG_INFO(@"类型 %lu 的会话正在断开", (unsigned long)type); [self handleSessionDisconnecting:session type:type]; } } #pragma mark - Session State Handlers - (void)handleSessionConnected:(id)session type:(TJPSessionType)type { // 连接成功后的处理逻辑 // 可以在这里通知其他组件或执行初始化操作 } - (void)handleSessionDisconnected:(id)session type:(TJPSessionType)type { // 连接断开后的处理逻辑 // 根据断开原因决定是否需要重连 } - (void)handleSessionConnecting:(id)session type:(TJPSessionType)type { // 连接中状态的处理逻辑 } - (void)handleSessionDisconnecting:(id)session type:(TJPSessionType)type { // 断开中状态的处理逻辑 } #pragma mark - Cleanup and Configuration - (void)cleanupSessionForType:(TJPSessionType)type { id session = self.channels[@(type)]; if (session) { // 移除KVO观察 if ([session isKindOfClass:[TJPConcreteSession class]]) { TJPConcreteSession *concreteSession = (TJPConcreteSession *)session; @try { [concreteSession removeObserver:self forKeyPath:@"connectState"]; TJPLOG_INFO(@"移除类型 %lu 会话的KVO监听", (unsigned long)type); } @catch (NSException *exception) { TJPLOG_WARN(@"移除KVO观察时发生异常: %@", exception.reason); } } // 从通道中移除 [self.channels removeObjectForKey:@(type)]; [self.connectionStates removeObjectForKey:@(type)]; TJPLOG_INFO(@"清理类型 %lu 的会话: %@", (unsigned long)type, session.sessionId); } } - (void)configureRouting:(TJPContentType)contentType toSessionType:(TJPSessionType)sessionType { self.contentTypeToSessionType[@(contentType)] = @(sessionType); TJPLOG_INFO(@"配置路由: 内容类型 %lu -> 会话类型 %lu", (unsigned long)contentType, (unsigned long)sessionType); } - (void)handleSessionReacquisition:(NSNotification *)notification { TJPSessionType sessionType = [notification.userInfo[@"sessionType"] unsignedIntegerValue]; TJPLOG_INFO(@"收到会话重新获取通知,类型: %lu", (unsigned long)sessionType); // 清理旧会话 [self cleanupSessionForType:sessionType]; } - (void)setupDefaultRouting { // 文本消息走聊天会话,媒体消息走媒体会话,因为对资源性要求不一样 self.contentTypeToSessionType[@(TJPContentTypeText)] = @(TJPSessionTypeChat); self.contentTypeToSessionType[@(TJPContentTypeImage)] = @(TJPSessionTypeMedia); self.contentTypeToSessionType[@(TJPContentTypeAudio)] = @(TJPSessionTypeMedia); self.contentTypeToSessionType[@(TJPContentTypeVideo)] = @(TJPSessionTypeMedia); self.contentTypeToSessionType[@(TJPContentTypeFile)] = @(TJPSessionTypeMedia); self.contentTypeToSessionType[@(TJPContentTypeLocation)] = @(TJPSessionTypeChat); self.contentTypeToSessionType[@(TJPContentTypeCustom)] = @(TJPSessionTypeDefault); TJPLOG_INFO(@"默认路由配置完成"); // 添加新的内容类型时,需更新映射表 } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Log/TJPLogManager.h ================================================ // // TJPLogManager.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/19. // 日志管理器 #import NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TJPLogLevel) { TJPLogLevelDebug = 0, TJPLogLevelInfo, TJPLogLevelWarn, TJPLogLevelMock, TJPLogLevelError }; @interface TJPLogManager : NSObject /// 是否开启详细日志 @property (nonatomic, assign) BOOL debugLoggingEnabled; /// 最低日志级别 @property (nonatomic, assign) TJPLogLevel minLogLevel; /// Log日志限流间隔 @property (nonatomic, assign) NSTimeInterval logThrottleInterval; /// 单例 + (instancetype)sharedManager; + (TJPLogLevel)levelFromString:(NSString *)levelString; - (BOOL)shouldLogWithLevel:(TJPLogLevel)level; /// 过滤日志消息 - (void)throttledLog:(NSString *)message level:(NSUInteger)level tag:(NSString *)tag; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Log/TJPLogManager.m ================================================ // // TJPLogManager.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/19. // #import "TJPLogManager.h" @interface TJPLogManager () { NSMutableDictionary *_lastLogTimes; dispatch_queue_t _logQueue; dispatch_semaphore_t _lock; } @end @implementation TJPLogManager + (instancetype)sharedManager { static TJPLogManager *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[TJPLogManager alloc] init]; }); return instance; } - (instancetype)init { if (self = [super init]) { // 默认关闭详细日志 _debugLoggingEnabled = NO; // 限流 3秒 _logThrottleInterval = 3.0; _minLogLevel = TJPLogLevelWarn; _lastLogTimes = [NSMutableDictionary dictionary]; _lock = dispatch_semaphore_create(1); // 低优先级队列节省CPU _logQueue = dispatch_queue_create("com.TJPLogManager.logQueue", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(_logQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)); } return self; } + (TJPLogLevel)levelFromString:(NSString *)levelString { levelString = [levelString uppercaseString]; if ([levelString isEqualToString:@"DEBUG"]) return TJPLogLevelDebug; if ([levelString isEqualToString:@"INFO"]) return TJPLogLevelInfo; if ([levelString isEqualToString:@"WARN"]) return TJPLogLevelWarn; if ([levelString isEqualToString:@"MOCK"]) return TJPLogLevelMock; if ([levelString isEqualToString:@"ERROR"]) return TJPLogLevelError; return TJPLogLevelDebug; } + (NSString *)stringFromLevel:(TJPLogLevel)level { switch (level) { case TJPLogLevelDebug: return @"DEBUG"; case TJPLogLevelInfo: return @"INFO"; case TJPLogLevelWarn: return @"WARN"; case TJPLogLevelMock: return @"MOCK"; case TJPLogLevelError: return @"ERROR"; default: return @"DEBUG"; } } - (BOOL)shouldLogWithLevel:(TJPLogLevel)level { if (level < _minLogLevel) return NO; if (!_debugLoggingEnabled && level < TJPLogLevelWarn) return NO; return YES; } - (void)throttledLog:(NSString *)message level:(NSUInteger)level tag:(NSString *)tag { if (!tag || tag.length == 0) tag = @"Default"; NSTimeInterval now = [[NSDate date] timeIntervalSince1970]; __block BOOL shouldOutput = NO; dispatch_semaphore_wait(_lock, DISPATCH_TIME_FOREVER); NSNumber *lastTime = _lastLogTimes[tag]; if (!lastTime || (now - lastTime.doubleValue) >= _logThrottleInterval) { _lastLogTimes[tag] = @(now); shouldOutput = YES; } dispatch_semaphore_signal(_lock); if (!shouldOutput) return; // 异步低优先级输出,不阻塞主要逻辑 dispatch_async(_logQueue, ^{ NSString *levelString = [TJPLogManager stringFromLevel:level]; NSLog(@"[TJPIM][%@][%@] %@", levelString, tag, message); }); } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Message/TJPMessageFactory.h ================================================ // // TJPMessageFactory.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPMessageFactory : NSObject @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Message/TJPMessageFactory.m ================================================ // // TJPMessageFactory.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // #import "TJPMessageFactory.h" #import #import #import #import #import #import "TJPMessageProtocol.h" @implementation TJPMessageFactory + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self registerAllMessages]; }); } + (void)registerAllMessages { //在APP运行时通过 dladdr 和 getsectiondata 扫描所有注册的类 Dl_info info; dladdr(&_mh_execute_header, &info); unsigned long size = 0; uintptr_t *section = (uintptr_t *)getsectiondata(info.dli_fbase, "__DATA", "TJPMessages", &size); for (int i = 0; i < size/sizeof(uintptr_t); i++) { const char *className = (const char *)section[i]; Class cls = objc_getClass(className); if ([cls conformsToProtocol:@protocol(TJPMessageProtocol)]) { [self registerClass:cls]; } } } + (void)registerClass:(Class)messageClass { // 存储消息类型与类的映射 [[self classMap] setObject:messageClass forKey:@([messageClass messageTag])]; } + (NSMutableDictionary *)classMap { static NSMutableDictionary *map; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ map = [NSMutableDictionary new]; }); return map; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Message/TJPMessageManager.h ================================================ // // TJPMessageManager.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // 消息管理器 #import #import "TJPMessageManagerDelegate.h" #import "TJPMessageManagerNetworkDelegate.h" #import "TJPSessionProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPMessageManager : NSObject // 会话id @property (nonatomic, copy, readonly) NSString *sessionId; // 消息队列 @property (nonatomic, strong, readonly) dispatch_queue_t messageQueue; @property (nonatomic, weak) id networkDelegate; @property (nonatomic, weak) id delegate; // 初始化方法 - (instancetype)initWithSessionId:(NSString *)sessionId; /** * 创建并发送消息 * @param messageType 消息类型 * @param completion 回调 */ - (NSString *)sendMessage:(NSData *)data messageType:(TJPMessageType)messageType completion:(void(^)(NSString *messageId, NSError *error))completion; /** * 创建并发送消息 * @param messageType 消息类型 * @param encryptType 加密类型 * @param compressType 压缩类型 * @param completion 回调 */ - (NSString *)sendMessage:(NSData *)data messageType:(TJPMessageType)messageType encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType completion:(void(^)(NSString *messageId, NSError *error))completion; /// 获取消息上下文 - (TJPMessageContext *)messageWithId:(NSString *)messageId; /// 更新消息状态 - (void)updateMessage:(NSString *)messageId toState:(TJPMessageState)newState; /** * 获取所有消息 */ - (NSArray *)allMessages; /** * 清理过期消息 */ - (void)cleanupExpiredMessages; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Message/TJPMessageManager.m ================================================ // // TJPMessageManager.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // #import "TJPMessageManager.h" #import "TJPMessageStateMachine.h" #import "TJPMessageContext.h" #import "TJPNetworkDefine.h" #import "TJPErrorUtil.h" static const NSTimeInterval kDefaultRetryInterval = 10; @interface TJPMessageManager () // 会话ID @property (nonatomic, copy, readwrite) NSString *sessionId; @property (nonatomic, strong, readwrite) dispatch_queue_t messageQueue; // 消息存储:messageId -> TJPMessageContext @property (nonatomic, strong) NSMutableDictionary *messages; // 状态机映射:messageId -> TJPMessageStateMachine @property (nonatomic, strong) NSMutableDictionary *stateMachines; // 序列号映射:sequence -> messageId @property (nonatomic, strong) NSMutableDictionary *sequenceToMessageId; @end @implementation TJPMessageManager #pragma mark - Life Cycle - (instancetype)initWithSessionId:(NSString *)sessionId { if (self = [super init]) { _sessionId = [sessionId copy]; _messages = [NSMutableDictionary dictionary]; _sequenceToMessageId = [NSMutableDictionary dictionary]; _stateMachines = [NSMutableDictionary dictionary]; // 创建专用队列 NSString *queueName = [NSString stringWithFormat:@"com.tjp.messageManager.%@", sessionId]; _messageQueue = dispatch_queue_create([queueName UTF8String], DISPATCH_QUEUE_SERIAL); TJPLOG_INFO(@"[TJPMessageManager] 初始化完成,会话ID: %@", sessionId); } return self; } - (void)dealloc { TJPLOG_INFO(@"[TJPMessageManager] 开始释放,会话ID: %@", self.sessionId); TJPLOG_INFO(@"[TJPMessageManager] 释放完成"); } #pragma mark - Public Method - (NSString *)sendMessage:(NSData *)data messageType:(TJPMessageType)messageType completion:(void (^)(NSString *messageId, NSError *error))completion { return [self sendMessage:data messageType:messageType encryptType:TJPEncryptTypeCRC32 compressType:TJPCompressTypeNone completion:completion]; } - (NSString *)sendMessage:(NSData *)data messageType:(TJPMessageType)messageType encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType completion:(void (^)(NSString *messageId, NSError *error))completion { __block NSString *messageId = nil; __block NSError *validationError = nil; dispatch_sync(self.messageQueue, ^{ // 参数校验 原Session逻辑 if (!data) { TJPLOG_ERROR(@"[TJPMessageManager] 发送数据为空"); validationError = [TJPErrorUtil errorWithCode:TJPErrorMessageIsEmpty description:@"消息数据为空" userInfo:@{}]; return; } if (data.length > TJPMAX_BODY_SIZE) { TJPLOG_ERROR(@"[TJPMessageManager] 数据大小超过限制: %lu > %d", (unsigned long)data.length, TJPMAX_BODY_SIZE); validationError = [TJPErrorUtil errorWithCode:TJPErrorMessageTooLarge description:@"消息体长度超过限制" userInfo:@{@"length": @(data.length), @"maxSize": @(TJPMAX_BODY_SIZE)}]; return; } // 创建消息上下文 序列号稍后由会话分配 TJPMessageContext *context = [TJPMessageContext contextWithData:data seq:0 messageType:messageType encryptType:TJPEncryptTypeCRC32 compressType:TJPCompressTypeNone sessionId:self.sessionId]; messageId = context.messageId; // 消息状态机管理消息状态 TJPMessageStateMachine *stateMachine = [[TJPMessageStateMachine alloc] initWithMessageId:messageId]; // 设置状态变化回调 __weak typeof(self) weakSelf = self; stateMachine.stateChangeCallback = ^(TJPMessageContext * _Nonnull context, TJPMessageState oldState, TJPMessageState newState) { __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; if (strongSelf && strongSelf.delegate) { dispatch_async(dispatch_get_main_queue(), ^{ // 状态转换回调 [strongSelf.delegate messageManager:strongSelf message:context didChangeState:newState fromState:oldState]; }); // 统一处理状态转换时的逻辑 [strongSelf handleStateTransitionEffects:context newState:newState oldState:oldState]; } }; // 存储消息和状态机 [self storeMessage:context withStateMachine:stateMachine]; // 状态转换:创建 -> 发送 [stateMachine transitionToState:TJPMessageStateSending context:context]; // 实际的发送逻辑 [self performActualSendForMessage:context]; }); if (validationError) { if (completion) { completion(@"", validationError); } return @""; } // 成功回调 if (completion) { completion(messageId, validationError); } return messageId; } - (TJPMessageContext *)messageWithId:(NSString *)messageId { __block TJPMessageContext *message = nil; dispatch_sync(self.messageQueue, ^{ message = self.messages[messageId]; }); return message; } - (void)updateMessage:(NSString *)messageId toState:(TJPMessageState)newState { dispatch_async(self.messageQueue, ^{ TJPMessageContext *message = self.messages[messageId]; TJPMessageStateMachine *stateMachine = self.stateMachines[messageId]; if (message && stateMachine) { [stateMachine transitionToState:newState context:message]; } }); } - (NSArray *)allMessages { __block NSArray *result = nil; dispatch_sync(self.messageQueue, ^{ result = [self.messages.allValues copy]; }); return result; } #pragma mark - Private Methods - (void)handleStateTransitionEffects:(TJPMessageContext *)message newState:(TJPMessageState)newState oldState:(TJPMessageState)oldState { // 专注于状态管理相关的作用,不处理重传逻辑 dispatch_async(self.messageQueue, ^{ switch (newState) { case TJPMessageStateSending: // 触发willSend回调 if (self.delegate && [self.delegate respondsToSelector:@selector(messageManager:willSendMessage:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate messageManager:self willSendMessage:message]; }); } break; case TJPMessageStateSent: // 触发didSend回调 if (self.delegate && [self.delegate respondsToSelector:@selector(messageManager:didSendMessage:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate messageManager:self didSendMessage:message]; }); } break; case TJPMessageStateRetrying: // 重试中状态 TJPLOG_INFO(@"[TJPMessageManager] 消息 %@ 进入重试状态,第 %ld 次重试", message.messageId, (long)message.retryCount); // 可以添加重试回调(如果需要) // if (self.delegate && [self.delegate respondsToSelector:@selector(messageManager:willRetryMessage:attemptCount:)]) { // // [self.delegate messageManager:self willRetryMessage:message attemptCount:message.retryCount]; // } break; case TJPMessageStateFailed: // 触发失败回调 TJPLOG_ERROR(@"[TJPMessageManager] 消息 %@ 发送失败: %@", message.messageId, message.lastError.localizedDescription); if (self.delegate && [self.delegate respondsToSelector:@selector(messageManager:didFailToSendMessage:error:)]) { dispatch_async(dispatch_get_main_queue(), ^{ [self.delegate messageManager:self didFailToSendMessage:message error:message.lastError]; }); } break; case TJPMessageStateDelivered: // 已送达状态(如果支持送达回执) TJPLOG_INFO(@"[TJPMessageManager] 消息 %@ 已送达", message.messageId); // if (self.delegate && [self.delegate respondsToSelector:@selector(messageManager:messageDidDeliver:)]) { // [self.delegate messageManager:self messageDidDeliver:message]; // } break; case TJPMessageStateCancelled: // 已取消状态 TJPLOG_INFO(@"[TJPMessageManager] 消息 %@ 已取消", message.messageId); // if (self.delegate && [self.delegate respondsToSelector:@selector(messageManager:messageDidCancel:)]) { // [self.delegate messageManager:self messageDidCancel:message]; // } break; default: break; } }); } - (void)storeMessage:(TJPMessageContext *)message withStateMachine:(TJPMessageStateMachine *)stateMachine { // 存储消息 self.messages[message.messageId] = message; // 存储状态机 self.stateMachines[message.messageId] = stateMachine; // 序列号映射 if (message.sequence > 0) { self.sequenceToMessageId[@(message.sequence)] = message.messageId; } } - (void)performActualSendForMessage:(TJPMessageContext *)message { if (self.networkDelegate && [self.networkDelegate respondsToSelector:@selector(messageManager:needsSendMessage:)]) { [self.networkDelegate messageManager:self needsSendMessage:message]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Message/TJPMessageSerializer.h ================================================ // // TJPMessageSerializer.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // 序列化工具 #import NS_ASSUME_NONNULL_BEGIN @interface TJPMessageSerializer : NSObject /// 文本内容序列化成TLV格式的二进制数据 /// - Parameters: /// - text: 要序列化的文本内容(UTF-8编码) /// - tag: 消息类型标识 详见 TJPContentType + (NSData *)serializeText:(NSString *)text tag:(uint16_t)tag; /// 图片序列化成TLV格式的二进制数据 /// - Parameters: /// - image: 要序列化的图片 /// - tag: 消息类型标识 详见 TJPContentType + (NSData *)serializeImage:(UIImage *)image tag:(uint16_t)tag; // 后续增加别的消息类型直接增加方法即可 @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Message/TJPMessageSerializer.m ================================================ // // TJPMessageSerializer.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // #import "TJPMessageSerializer.h" #import #import #import "TJPNetworkDefine.h" #import "UIImage+TJPImageOrientation.h" @implementation TJPMessageSerializer + (NSData *)serializeText:(NSString *)text tag:(uint16_t)tag { if (!text.length && text == nil) { TJPLOG_ERROR(@"图片序列化失败:image参数为nil"); return nil; } NSData *textData = [text dataUsingEncoding:NSUTF8StringEncoding]; //构建TLV结构 return [self buildTLVWithTag:tag value:textData]; } + (NSData *)serializeImage:(UIImage *)image tag:(uint16_t)tag { //参数校验 NSCParameterAssert(image && [image isKindOfClass:[UIImage class]]); if (!image) { TJPLOG_ERROR(@"图片序列化失败:image参数为nil"); return nil; } //图片预处理 调整尺寸和方向 UIImage *processedImage = [self _processImageBeforeEncoding:image]; // 3. 智能选择编码格式 NSData *imageData = [self _encodeImageData:processedImage]; if (!imageData) { TJPLOG_ERROR(@"图片编码失败:无法生成有效数据"); return nil; } // 4. 构建TLV结构(复用基础方法) return [self buildTLVWithTag:tag value:imageData]; } #pragma mark - Private Method + (UIImage *)_processImageBeforeEncoding:(UIImage *)srcImage { //最大允许尺寸 static const CGSize kMaxSize = {1024, 1024}; //方向修正 (解决图片拍摄旋转问题) UIImage *fixedImage = [srcImage fixOrientation]; // 尺寸缩放检查 if (fixedImage.size.width <= kMaxSize.width && fixedImage.size.height <= kMaxSize.height) { return fixedImage; } // 等比例缩放 CGFloat ratio = MIN(kMaxSize.width / fixedImage.size.width, kMaxSize.height / fixedImage.size.height); //处理后的尺寸 CGSize newSize = CGSizeMake(floor(fixedImage.size.width * ratio), floor(fixedImage.size.height * ratio)); UIGraphicsBeginImageContextWithOptions(newSize, YES, [UIScreen mainScreen].scale); [fixedImage drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; UIImage *scaledImage = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return scaledImage ?: fixedImage; } + (NSData *)_encodeImageData:(UIImage *)image { // 格式选择策略 BOOL hasAlpha = [self _imageHasAlphaChannel:image]; // 编码参数配置 NSDictionary *options = @{ // 透明图用PNG,不透明用JPEG (id)kCGImageDestinationLossyCompressionQuality: @(hasAlpha ? 1.0 : 0.85) }; // 自动选择最佳格式 CFStringRef type = hasAlpha ? (CFStringRef)@"public.png" : (CFStringRef)@"public.jpeg"; // 编码为二进制数据 NSMutableData *data = [NSMutableData data]; CGImageDestinationRef dest = CGImageDestinationCreateWithData((__bridge CFMutableDataRef)data, type, 1, NULL); if (!dest) return nil; CGImageDestinationAddImage(dest, image.CGImage, (__bridge CFDictionaryRef)options); BOOL success = CGImageDestinationFinalize(dest); CFRelease(dest); return success ? [data copy] : nil; } #pragma mark - Common Method + (NSData *)buildTLVWithTag:(uint16_t)tag value:(NSData *)value { //字节序转换 (主机序→网络序) uint16_t netTag = CFSwapInt16HostToBig(tag); // 2字节Tag转大端 uint32_t netLength = CFSwapInt32HostToBig((uint32_t)value.length); // 4字节Length转大端 //单次内存分配 避免频繁扩容 (Tag2 + Length4 + valueN) NSUInteger totalLength = 2 + 4 + value.length; NSMutableData *data = [NSMutableData dataWithCapacity:totalLength]; //直接操作底层缓冲区(零拷贝) [data setLength:totalLength]; uint8_t *buffer = [data mutableBytes]; memcpy(buffer, &netTag, 2); memcpy(buffer + 2, &netLength, 4); memcpy(buffer + 6, value.bytes, value.length); return [data copy]; } /** * 检测图片是否包含透明通道 */ + (BOOL)_imageHasAlphaChannel:(UIImage *)image { CGImageAlphaInfo alpha = CGImageGetAlphaInfo(image.CGImage); return (alpha == kCGImageAlphaFirst || alpha == kCGImageAlphaLast || alpha == kCGImageAlphaPremultipliedFirst || alpha == kCGImageAlphaPremultipliedLast); } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Message/TJPTextMessage.h ================================================ // // TJPTextMessage.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // #import #import "TJPMessageProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPTextMessage : NSObject @property (nonatomic, copy) NSString *text; - (instancetype)initWithText:(NSString *)text; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Message/TJPTextMessage.m ================================================ // // TJPTextMessage.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // #import "TJPTextMessage.h" #import "TJPMessageSerializer.h" #import "TJPCoreTypes.h" /* 利用 __attribute__((section)) 将类名写入 Mach-O 文件的 __DATA 段 在程序启动时通过工厂加载 */ __attribute__((used, section("__DATA,TJPMessages"))) static const char *kTJPTextMessageRegistration = "TJPTextMessage"; @implementation TJPTextMessage - (instancetype)initWithText:(NSString *)text { if (self = [super init]) { _text = text; } return self; } + (uint16_t)messageTag { return TJPContentTypeText; } - (TJPMessageType)messageType { return TJPMessageTypeNormalData; } - (NSData *)tlvData { return [TJPMessageSerializer serializeText:self.text tag:[self.class messageTag]]; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Network/TJPNetworkCondition.h ================================================ // // TJPNetworkCondition.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/22. // 加权滑动窗口式的网络质量采样器 #import NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TJPNetworkQualityLevel) { TJPNetworkQualityUnknown, //未知状态 TJPNetworkQualityExcellent, //网络很好 TJPNetworkQualityGood, //网络良好 TJPNetworkQualityFair, //网络一般 TJPNetworkQualityPoor //网络很差 }; @interface TJPNetworkCondition : NSObject /// 往返延迟 毫秒 @property (nonatomic, assign) NSTimeInterval roundTripTime; /// 丢包率 百分比 @property (nonatomic, assign) CGFloat packetLossRate; /// 宽带估算 Mbps //@property (nonatomic, assign) CGFloat bandwidthEstimate; /// 网络质量等级 (根据指标自动计算) @property (nonatomic, assign, readonly) TJPNetworkQualityLevel qualityLevel; /// 是否拥塞 @property (nonatomic, assign, readonly) BOOL isCongested; - (void)updateRTTWithSample:(NSTimeInterval)rtt; - (void)updateLostWithSample:(BOOL)isLost; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Network/TJPNetworkCondition.m ================================================ // // TJPNetworkCondition.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/22. // #import "TJPNetworkCondition.h" #define kMaxRTTSamples 10 //RTT样本数量 #define kWeightFactor 0.1 //丢包率加权因子 @interface TJPNetworkCondition () /// 记录数据 @property (nonatomic, strong) NSMutableArray *rttSamples; @property (nonatomic, strong) NSMutableArray *lostSamples; @end @implementation TJPNetworkCondition - (instancetype)init { if (self = [super init]) { //默认数据 RTT0ms 丢包率0.0 _roundTripTime = 0; _packetLossRate = 0.0; _rttSamples = [[NSMutableArray alloc] initWithCapacity:kMaxRTTSamples]; _lostSamples = [[NSMutableArray alloc] initWithCapacity:kMaxRTTSamples]; } return self; } - (void)updateRTTWithSample:(NSTimeInterval)rtt { @synchronized (self) { if (_rttSamples.count >= kMaxRTTSamples) { [_rttSamples removeObjectAtIndex:0]; } [_rttSamples addObject:@(rtt)]; //更新当前RTT _roundTripTime = [self _weightAverageForSamples:_rttSamples]; } } - (void)updateLostWithSample:(BOOL)isLost { @synchronized (self) { if (_lostSamples.count >= kMaxRTTSamples) { [_lostSamples removeObjectAtIndex:0]; } [_lostSamples addObject:@(isLost ? 1.0 : 0.0)]; //计算丢包率 CGFloat totalLost = 0.0; for (NSNumber *lost in _lostSamples) { totalLost += lost.floatValue; } _packetLossRate = (totalLost / _lostSamples.count) * 100; } } - (CGFloat)_weightAverageForSamples:(NSMutableArray *)samples { //总和 CGFloat sum = 0; //权重 CGFloat weightSum = 0; for (int i = 0; i < samples.count; i++) { //越新的样本权重越高 CGFloat weight = 1.0 + (i * 0.2); sum += samples[i].floatValue * weight; weightSum += weight; } return sum / weightSum; } - (TJPNetworkQualityLevel)qualityLevel { if (_roundTripTime < 100 && _packetLossRate < 2) { return TJPNetworkQualityExcellent; } else if (_roundTripTime < 300 && _packetLossRate < 5) { return TJPNetworkQualityGood; } else if (_roundTripTime < 500 && _packetLossRate < 10) { return TJPNetworkQualityFair; } else if (_roundTripTime < 800 && _packetLossRate < 15) { return TJPNetworkQualityPoor; }else { return TJPNetworkQualityUnknown; } } - (BOOL)isCongested { return (_packetLossRate > 10 || _roundTripTime > 500); } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/NetworkUtility/TJPNetworkDefine.h ================================================ // // TJPNetworkDefine.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/30. // #import "TJPLogManager.h" #ifndef TJPNetworkDefine_h #define TJPNetworkDefine_h //网络监控开关 #if ENABLE_NETWORK_MONITORING #define MONITOR_ENABLED 1 #else #define MONITOR_ENABLED 0 #endif //安全断言 #define AssertMainThread() NSAssert([NSThread isMainThread], @"必须在主线程执行") #define TJPLOG(levelString, fmt, ...) \ do { \ TJPLogLevel __logLevel = [TJPLogManager levelFromString:(levelString)]; \ if ([[TJPLogManager sharedManager] shouldLogWithLevel:__logLevel]) { \ NSString *__msg = [NSString stringWithFormat:(fmt), ##__VA_ARGS__]; \ [[TJPLogManager sharedManager] throttledLog:__msg \ level:__logLevel \ tag:@(__FUNCTION__)]; \ } \ } while (0) #define TJPLOG_DEBUG(fmt, ...) TJPLOG(@"DEBUG", fmt, ##__VA_ARGS__) #define TJPLOG_INFO(fmt, ...) TJPLOG(@"INFO", fmt, ##__VA_ARGS__) #define TJPLOG_WARN(fmt, ...) TJPLOG(@"WARN", fmt, ##__VA_ARGS__) #define TJPLOG_ERROR(fmt, ...) TJPLOG(@"ERROR", fmt, ##__VA_ARGS__) #define TJPLOG_MOCK(fmt, ...) TJPLOG(@"MOCK", fmt, ##__VA_ARGS__) #define TJPLogDealloc() TJPLOG(@"INFO", @"|DEALLOC| %s", __PRETTY_FUNCTION__) #define kNetworkFatalErrorNotification @"kNetworkFatalErrorNotification" #define kSessionDataReceiveNotification @"kSessionDataReceiveNotification" #define kNetworkStatusChangedNotification @"kNetworkStatusChangedNotification" #define kHeartbeatTimeoutNotification @"kHeartbeatTimeoutNotification" #define kHeartbeatModeChangedNotification @"kHeartbeatModeChangedNotification" #define kSessionNeedsReacquisitionNotification @"kSessionNeedsReacquisitionNotification" // 消息相关通知 #define kTJPMessageSentNotification @"kTJPMessageSentNotification" #define kTJPMessageFailedNotification @"kTJPMessageFailedNotification" #define kTJPMessageReceivedNotification @"kTJPMessageReceivedNotification" #define kTJPMessageStatusUpdateNotification @"kTJPMessageStatusUpdateNotification" #define kTJPMessageReadNotification @"kTJPMessageReadNotification" #define TJPMAX_BODY_SIZE (10 * 1024 * 1024) // 10MB 最大消息体大小 #define TJPMAX_BUFFER_SIZE (20 * 1024 * 1024) // 20MB 最大缓冲区大小 #define TJPMAX_TIME_WINDOW 60 // 60秒时间窗口,防重放攻击 #define TJP_DEFAULT_RING_BUFFER_CAPACITY (128 * 1024) // 缓冲区大小 初始128kb #define TJPSCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width) #define TJPSCREEN_HEIGHT ([UIScreen mainScreen].bounds.size.height) static const uint32_t TJPSEQUENCE_CATEGORY_BITS = 8; // 类别占用位数 static const uint32_t TJPSEQUENCE_BODY_BITS = 24; // 序列号占用位数 static const uint32_t TJPSEQUENCE_BODY_MASK = 0x00FFFFFF; // 24位掩码 static const uint32_t TJPSEQUENCE_CATEGORY_MASK = 0xFF; // 8位掩码 static const uint32_t TJPSEQUENCE_MAX_VALUE = 0x00FFFFFF; // 最大序列号 static const uint32_t TJPSEQUENCE_WARNING_THRESHOLD = 0x00F00000; // 警告阈值(15M) static const uint32_t TJPSEQUENCE_RESET_THRESHOLD = 0x00FF0000; // 重置阈值(16M-1M) //*************************************** // 心跳相关定义 // 前台心跳参数(秒) static const NSTimeInterval kTJPHeartbeatForegroundBase = 30.0; // 基础间隔 static const NSTimeInterval kTJPHeartbeatForegroundMin = 15.0; // 最小间隔 static const NSTimeInterval kTJPHeartbeatForegroundMax = 300.0; // 最大间隔 // 后台心跳参数(秒) static const NSTimeInterval kTJPHeartbeatBackgroundBase = 90.0; // 基础间隔 static const NSTimeInterval kTJPHeartbeatBackgroundMin = 45.0; // 最小间隔 static const NSTimeInterval kTJPHeartbeatBackgroundMax = 600.0; // 最大间隔 // 低电量模式心跳参数(秒) static const NSTimeInterval kTJPHeartbeatLowPowerBase = 120.0; // 基础间隔 static const NSTimeInterval kTJPHeartbeatLowPowerMin = 60.0; // 最小间隔 static const NSTimeInterval kTJPHeartbeatLowPowerMax = 900.0; // 最大间隔 // 调整因子 static const CGFloat kTJPHeartbeatNetworkPoorFactor = 2.5; // 恶劣网络调整因子 static const CGFloat kTJPHeartbeatNetworkFairFactor = 1.5; // 一般网络调整因子 static const CGFloat kTJPHeartbeatRTTRefValue = 200.0; // RTT参考值(ms) static const CGFloat kTJPHeartbeatRandomFactorMin = 0.9; // 随机因子最小值 static const CGFloat kTJPHeartbeatRandomFactorMax = 1.1; // 随机因子最大值 // 重试相关 static const NSUInteger kTJPHeartbeatMaxRetryCount = 3; // 最大重试次数 static const NSTimeInterval kTJPHeartbeatMinTimeout = 15.0; // 最小超时时间(秒) //*************************************** #endif /* TJPNetworkDefine_h */ ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/NetworkUtility/TJPNetworkErrorDefine.h ================================================ // // TJPNetworkErrorDefine.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/10. // #ifndef TJPErrorUtil_h #define TJPErrorUtil_h #import /** * 网络错误代码枚举 * 错误域: "com.tjp.network.error" */ typedef NS_ENUM(NSInteger, TJPNetworkError) { // 一般错误 (0-999) TJPErrorNone = 0, // 无错误 TJPErrorUnknown = 1, // 未知错误 TJPErrorTimeout = 2, // 超时 TJPErrorCancelled = 3, // 操作被取消 // 连接相关错误 (1000-1999) TJPErrorConnectionFailed = 1000, // 连接失败 TJPErrorConnectionTimeout = 1001, // 连接超时 TJPErrorConnectionLost = 1002, // 连接丢失 TJPErrorConnectionRefused = 1003, // 连接被拒绝 TJPErrorNetworkUnavailable = 1004, // 网络不可用 TJPErrorServerUnavailable = 1005, // 服务器不可用 TJPErrorTLSHandshakeFailed = 1006, // TLS握手失败 // 消息传输错误 (2000-2999) TJPErrorMessageSendFailed = 2000, // 消息发送失败 TJPErrorMessageReceiveFailed = 2001, // 消息接收失败 TJPErrorMessageTimeout = 2002, // 消息超时未收到响应 TJPErrorMessageTooLarge = 2003, // 消息体过大 TJPErrorMessageIsEmpty = 2004, // 消息为空 TJPErrorMessageFormatInvalid = 2005, // 消息格式无效 TJPErrorMessageACKMissing = 2006, // 未收到ACK确认 TJPErrorMessageRetryExceeded = 2007, // 超过最大重试次数 // 协议解析错误 (3000-3999) TJPErrorProtocolVersionMismatch = 3000, // 协议版本不匹配 TJPErrorProtocolMagicInvalid = 3001, // 魔数无效 TJPErrorProtocolChecksumMismatch = 3002, // 校验和不匹配 TJPErrorProtocolHeaderInvalid = 3003, // 协议头无效 TJPErrorProtocolPayloadLengthMismatch = 3004, // 负载长度不匹配 TJPErrorProtocolUnsupportedEncryption = 3005, // 不支持的加密类型 TJPErrorProtocolUnsupportedCompression= 3006, // 不支持的压缩类型 TJPErrorProtocolTimestampInvalid = 3007, // 时间戳无效 // TLV解析错误 (4000-4999) TJPErrorTLVParseError = 4000, // TLV解析错误 TJPErrorTLVIncompleteTag = 4001, // 不完整的Tag TJPErrorTLVIncompleteLength = 4002, // 不完整的Length TJPErrorTLVIncompleteValue = 4003, // 不完整的Value TJPErrorTLVDuplicateTag = 4004, // 重复的Tag TJPErrorTLVNestedTooDeep = 4005, // 嵌套深度过大 // 安全相关错误 (5000-5999) TJPErrorSecurityEncryptionFailed = 5000, // 加密失败 TJPErrorSecurityDecryptionFailed = 5001, // 解密失败 TJPErrorSecurityUnauthorized = 5002, // 未授权 TJPErrorSecurityReplayAttackDetected = 5003, // 检测到重放攻击 TJPErrorSecurityInvalidSignature = 5004, // 签名无效 // 会话相关错误 (6000-6999) TJPErrorSessionExpired = 6000, // 会话过期 TJPErrorSessionInvalid = 6001, // 会话无效 TJPErrorSessionLimitExceeded = 6002, // 超出会话限制 TJPErrorSessionHeartbeatTimeout = 6003, // 心跳超时 TJPErrorSessionStateError = 6004, // 会话状态错误 // 业务逻辑错误 (7000-7999) TJPErrorBusinessLogicFailed = 7000, // 业务逻辑失败 // 系统错误 (8000-8999) TJPErrorSystemMemoryLow = 8000, // 系统内存不足 TJPErrorSystemDiskFull = 8001, // 磁盘空间不足 TJPErrorSystemIOFailure = 8002 // IO操作失败 }; // 错误域常量 FOUNDATION_EXPORT NSString * const TJPNetworkErrorDomain; #endif /* TJPErrorUtil_h */ ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Parser/TJPMessageParser.h ================================================ // // TJPMessageParser.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // 协议解析器 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @class TJPParsedPacket, TJPMessageParser; @protocol TJPMessageStrategyDelegate @optional /// 是否使用环形缓冲区 YES使用 NO不使用 - (BOOL)shouldUserRingBufferForParser:(TJPMessageParser *)parser; /// 推荐缓冲区容量 - (NSUInteger)recommendedCapacityForParser:(TJPMessageParser *)parser; /// 缓冲区切换通知 - (void)parser:(TJPMessageParser *)parser didSwitchToImplementation:(NSString *)impl reason:(NSString *)reason; /// 缓冲区错误处理策略 YES继续使用当前实现 NO切换到备用实现 - (BOOL)parser:(TJPMessageParser *)parser shouldContinueAfterError:(NSError *)error; @end @interface TJPMessageParser : NSObject @property (nonatomic, weak) id strategyDelegate; /// 当前状态 @property (nonatomic, assign, readonly) TJPParseState currentState; /// 缓冲区 用于数据监控 @property (nonatomic, readonly) NSMutableData *buffer; /// 当前协议头 @property (nonatomic, readonly) TJPFinalAdavancedHeader currentHeader; /// 当前策略 @property (nonatomic, readonly) TJPBufferStrategy currentStrategy; /// 开关控制是否使用环形缓冲区 @property (nonatomic, readonly) BOOL isUseRingBuffer; /// 缓冲区总容量 @property (nonatomic, readonly) NSUInteger bufferCapacity; /// 已使用大小 @property (nonatomic, readonly) NSUInteger userdBufferSize; /// 使用率 0.0 - 1.0 @property (nonatomic, readonly) CGFloat bufferUsageRatio; /// 缓冲区添加数据 - (void)feedData:(NSData *)data; /// 是否是完整数据 - (BOOL)hasCompletePacket; /// 获取下一个数据 - (TJPParsedPacket *)nextPacket; /// 重置数据 - (void)reset; - (instancetype)init; /// 是否使用环形缓冲区初始化 - (instancetype)initWithRingBufferEnabled:(BOOL)enabled; /// 使用缓冲区选择策略进行初始化 - (instancetype)initWithBufferStrategy:(TJPBufferStrategy)strategy; /// 完整配置初始化 - (instancetype)initWithBufferStrategy:(TJPBufferStrategy)strategy capacity:(NSUInteger)capacity; //************************************************ //新增控制切换 调试方法 /// 切换到环形缓冲区 - (BOOL)switchToRingBuffer; /// 切换到环形缓冲区并指定容量 - (BOOL)switchToRingBufferWithCapacity:(NSUInteger)capacity; /// 切换到传统缓冲区 - (void)switchToTraditionBuffer; /// 切换到最优模式 根据条件自动选择 - (BOOL)switchToOptimalMode; /// 打印当前缓冲区信息 - (void)printBufferStatus; /// 获取缓冲区统计信息 - (NSDictionary *)bufferStatistics; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Parser/TJPMessageParser.m ================================================ // // TJPMessageParser.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #import "TJPMessageParser.h" #import "TJPNetworkDefine.h" #import "TJPParsedPacket.h" #import "TJPCoreTypes.h" #import "TJPNetworkUtil.h" #import "TJPErrorUtil.h" #import "TJPRingBuffer.h" @interface TJPMessageParser () { // 协议解析状态 TJPFinalAdavancedHeader _currentHeader; TJPParseState _state; // 双缓冲区实现 - 通过开关控制 NSMutableData *_traditionBuffer; // 旧实现:NSMutableData TJPRingBuffer *_ringBuffer; // 新实现:环形缓冲区 BOOL _isUseRingBuffer; // 实现切换开关 TJPBufferStrategy _strategy; // 用户设置的策略 NSUInteger _requestCapacity; // 用户请求的容量 // 安全相关 NSMutableSet *_recentSequences; //防重放攻击 NSDate *_lastCleanupTime; //定期清理计数器 // 简单的错误统计 NSUInteger _errorCount; NSUInteger _totalOperations; NSUInteger _switchCount; // 切换次数统计 // 增加性能统计 CFTimeInterval _totalParseTime; // 总解析时间 NSUInteger _totalPacketCount; // 总包数量 CFTimeInterval _lastBenchmarkTime; // 上次基准测试时间 } @end @implementation TJPMessageParser #pragma mark - Lifecycle - (instancetype)init { return [self initWithBufferStrategy:TJPBufferStrategyAuto]; } - (instancetype)initWithRingBufferEnabled:(BOOL)enabled { TJPBufferStrategy strategy = enabled ? TJPBufferStrategyRingBuffer : TJPBufferStrategyTradition; return [self initWithBufferStrategy:strategy]; } - (instancetype)initWithBufferStrategy:(TJPBufferStrategy)strategy { NSUInteger defaultCapacity = [self class].recommendedDefaultCapacity; return [self initWithBufferStrategy:strategy capacity:defaultCapacity]; } - (instancetype)initWithBufferStrategy:(TJPBufferStrategy)strategy capacity:(NSUInteger)capacity { if (self = [super init]) { _state = TJPParseStateHeader; _strategy = strategy; _requestCapacity = capacity; _errorCount = 0; _totalOperations = 0; _switchCount = 0; // 安全相关初始化 _recentSequences = [NSMutableSet setWithCapacity:1000]; _lastCleanupTime = [NSDate date]; // 初始化缓冲区 [self setupBuffersWithStrategy:strategy capacity:capacity]; // 性能统计初始化 _totalParseTime = 0; _totalPacketCount = 0; _lastBenchmarkTime = CFAbsoluteTimeGetCurrent(); TJPLOG_INFO(@"MessageParser 初始化完成,使用%@缓冲区", _isUseRingBuffer ? @"环形" : @"传统"); } return self; } - (void)setupBuffersWithStrategy:(TJPBufferStrategy)strategy capacity:(NSUInteger)capacity { _traditionBuffer = [NSMutableData data]; switch (strategy) { case TJPBufferStrategyTradition: _isUseRingBuffer = NO; _ringBuffer = nil; break; case TJPBufferStrategyRingBuffer: _isUseRingBuffer = [self setupRingBufferWithCapacity:capacity]; break; case TJPBufferStrategyAuto: default: _isUseRingBuffer = [self autoSetupWithCapacity:capacity]; break; } } - (BOOL)setupRingBufferWithCapacity:(NSUInteger)capacity { // 检查容量合理性 capacity = [self validateCapacity:capacity]; _ringBuffer = [[TJPRingBuffer alloc] initWithCapacity:capacity]; if (!_ringBuffer) { TJPLOG_ERROR(@"环形缓冲区初始化失败,容量: %luKB", (unsigned long)capacity / 1024); [self recordError:@"环形缓冲区初始化失败"]; return NO; } return YES; } - (BOOL)autoSetupWithCapacity:(NSUInteger)capacity { if (self.strategyDelegate && [self.strategyDelegate respondsToSelector:@selector(shouldUserRingBufferForParser:)]) { BOOL shouldUse = [_strategyDelegate shouldUserRingBufferForParser:self]; if (!shouldUse) { TJPLOG_INFO(@"策略代理建议使用传统缓冲区"); return NO; } } // 简单抉择逻辑 后续可以扩展为更完善逻辑 if ([self shouldUseRingBufferByDefault]) { return [self setupRingBufferWithCapacity:capacity]; }else { TJPLOG_INFO(@"自动选择传统缓冲区"); return NO; } } #pragma mark - Public Method - (void)feedData:(NSData *)data { CFTimeInterval startTime = CFAbsoluteTimeGetCurrent(); if (!data || data.length == 0) { return; } _totalOperations++; // 防止缓冲区过大导致内存耗尽 if (data.length > TJPMAX_BUFFER_SIZE ) { TJPLOG_ERROR(@"数据大小超过限制: %lu > %d", (unsigned long)data.length, TJPMAX_BUFFER_SIZE); [self reset]; _state = TJPParseStateError; [self recordError:@"数据大小超限"]; return; } // 根据开关选择实现 if (_isUseRingBuffer) { [self feedDataWithRingBuffer:data]; } else { [self feedDataWithLegacyBuffer:data]; } //定期清理过期序列号 [self cleanupExpiredSequences]; _totalParseTime += (CFAbsoluteTimeGetCurrent() - startTime); } - (BOOL)hasCompletePacket { if (_state == TJPParseStateError) { return NO; } if (_isUseRingBuffer) { return [self hasCompletePacketWithRingBuffer]; } else { return [self hasCompletePacketWithLegacyBuffer]; } } - (TJPParsedPacket *)nextPacket { // 错误状态下不处理 if (_state == TJPParseStateError) { TJPLOG_ERROR(@"解析器处于错误状态,请先重置"); return nil; } CFTimeInterval startTime = CFAbsoluteTimeGetCurrent(); TJPParsedPacket *result = nil; if (_isUseRingBuffer) { result = [self nextPacketWithRingBuffer]; } else { result = [self nextPacketWithLegacyBuffer]; } // 性能统计 if (result) { _totalPacketCount++; _totalParseTime += (CFAbsoluteTimeGetCurrent() - startTime); } return result; } - (void)reset { [_traditionBuffer setLength:0]; [_ringBuffer reset]; _currentHeader = (TJPFinalAdavancedHeader){0}; _state = TJPParseStateHeader; TJPLOG_INFO(@"MessageParser 重置完成"); } #pragma mark - Ring Buffer - (void)feedDataWithRingBuffer:(NSData *)data { @try { // 检查剩余空间 if (_ringBuffer.availableSpace < data.length) { TJPLOG_ERROR(@"环形缓冲区空间不足: 需要 %lu, 可用 %lu", (unsigned long)data.length, (unsigned long)_ringBuffer.availableSpace); [self reset]; _state = TJPParseStateError; return; } NSUInteger written = [_ringBuffer writeData:data]; if (written != data.length) { TJPLOG_ERROR(@"环形缓冲区写入不完整: 期望 %lu, 实际 %lu", (unsigned long)data.length, (unsigned long)written); _state = TJPParseStateError; return; } } @catch (NSException *exception) { TJPLOG_ERROR(@"环形缓冲区异常: %@", exception.reason); [self handleRingBufferError:exception.reason]; } // TJPLOG_INFO(@"[环形Buffer] 收到数据: %lu 字节, 缓冲区使用率: %.1f%%", // (unsigned long)data.length, _ringBuffer.usageRatio * 100); } - (BOOL)hasCompletePacketWithRingBuffer { if (_state == TJPParseStateHeader) { return [_ringBuffer hasAvailableData:sizeof(TJPFinalAdavancedHeader)]; } else if (_state == TJPParseStateBody) { uint32_t bodyLength = ntohl(_currentHeader.bodyLength); return [_ringBuffer hasAvailableData:bodyLength]; } return NO; } - (TJPParsedPacket *)nextPacketWithRingBuffer { // 解析头部 if (_state == TJPParseStateHeader) { if (![self parseHeaderWithRingBuffer]) { return nil; } } // 解析消息体 if (_state == TJPParseStateBody) { return [self parseBodyWithRingBuffer]; } return nil; } - (BOOL)parseHeaderWithRingBuffer { if (![_ringBuffer hasAvailableData:sizeof(TJPFinalAdavancedHeader)]) { TJPLOG_WARN(@"环形缓冲区数据不足,无法解析头部"); return NO; } // 从环形缓冲区读取头部数据 TJPFinalAdavancedHeader header = {0}; NSUInteger readBytes = [_ringBuffer readBytes:&header length:sizeof(TJPFinalAdavancedHeader)]; if (readBytes != sizeof(TJPFinalAdavancedHeader)) { TJPLOG_ERROR(@"头部数据读取不完整: 期望 %lu, 实际 %lu", (unsigned long)sizeof(TJPFinalAdavancedHeader), (unsigned long)readBytes); _state = TJPParseStateError; return NO; } // 头部验证 NSError *validationError = nil; if (![self validateHeader:header error:&validationError]) { TJPLOG_ERROR(@"头部验证失败: %@", validationError.localizedDescription); _state = TJPParseStateError; return NO; } _currentHeader = header; _state = TJPParseStateBody; // TJPLOG_INFO(@"[环形Buffer] 解析序列号:%u 的头部成功", ntohl(_currentHeader.sequence)); return YES; } - (TJPParsedPacket *)parseBodyWithRingBuffer { uint32_t bodyLength = ntohl(_currentHeader.bodyLength); if (![_ringBuffer hasAvailableData:bodyLength]) { TJPLOG_INFO(@"环形缓冲区数据不足,等待更多数据..."); return nil; } // 读取消息体数据 NSData *payload = [_ringBuffer readData:bodyLength]; if (!payload || payload.length != bodyLength) { TJPLOG_ERROR(@"消息体数据读取失败: 期望 %u, 实际 %lu", bodyLength, (unsigned long)payload.length); _state = TJPParseStateError; return nil; } // 验证校验和 if (![self validateChecksum:_currentHeader.checksum forData:payload]) { TJPLOG_ERROR(@"校验和验证失败,可能数据已被篡改"); _state = TJPParseStateError; return nil; } // 创建解析结果 NSError *error = nil; TJPParsedPacket *packet = [TJPParsedPacket packetWithHeader:_currentHeader payload:payload policy:TJPTLVTagPolicyRejectDuplicates maxNestedDepth:4 error:&error]; if (error) { TJPLOG_ERROR(@"[环形Buffer] 解析序列号:%u 的内容失败: %@", ntohl(_currentHeader.sequence), error.localizedDescription); _state = TJPParseStateError; return nil; } // TJPLOG_INFO(@"[环形Buffer] 解析序列号:%u 的内容成功", ntohl(_currentHeader.sequence)); _state = TJPParseStateHeader; return packet; } #pragma mark Tradition Buffer - (void)feedDataWithLegacyBuffer:(NSData *)data { @synchronized (self) { if ((_traditionBuffer.length + data.length) > TJPMAX_BUFFER_SIZE) { TJPLOG_ERROR(@"传统缓冲区大小超过限制: 当前 %lu, 新增 %lu, 限制 %d", (unsigned long)_traditionBuffer.length, (unsigned long)data.length, TJPMAX_BUFFER_SIZE); [self reset]; _state = TJPParseStateError; [self recordError:@"缓冲区超限"]; return; } [_traditionBuffer appendData:data]; } // TJPLOG_INFO(@"[传统Buffer] 收到数据: %lu 字节", (unsigned long)data.length); } - (BOOL)hasCompletePacketWithLegacyBuffer { if (_state == TJPParseStateHeader) { return _traditionBuffer.length >= sizeof(TJPFinalAdavancedHeader); } else if (_state == TJPParseStateBody) { uint32_t bodyLength = ntohl(_currentHeader.bodyLength); return _traditionBuffer.length >= bodyLength; } return NO; } - (TJPParsedPacket *)nextPacketWithLegacyBuffer { // 解析头部 if (_state == TJPParseStateHeader) { if (![self parseHeaderWithLegacyBuffer]) { return nil; } } // 解析消息体 if (_state == TJPParseStateBody) { return [self parseBodyWithLegacyBuffer]; } return nil; } - (BOOL)parseHeaderWithLegacyBuffer { if (_traditionBuffer.length < sizeof(TJPFinalAdavancedHeader)) { TJPLOG_INFO(@"数据长度不够数据头解析"); return NO; } TJPFinalAdavancedHeader currentHeader = {0}; // 解析头部 [_traditionBuffer getBytes:¤tHeader length:sizeof(TJPFinalAdavancedHeader)]; // 安全验证 NSError *validationError = nil; if (![self validateHeader:currentHeader error:&validationError]) { TJPLOG_ERROR(@"头部验证失败: %@", validationError.localizedDescription); _state = TJPParseStateError; return NO; } TJPLOG_INFO(@"解析数据头部成功...魔数校验成功!"); _currentHeader = currentHeader; // 移除已处理的Header数据 [_traditionBuffer replaceBytesInRange:NSMakeRange(0, sizeof(TJPFinalAdavancedHeader)) withBytes:NULL length:0]; // TJPLOG_INFO(@"解析序列号:%u 的头部成功", ntohl(_currentHeader.sequence)); _state = TJPParseStateBody; return YES; } - (TJPParsedPacket *)parseBodyWithLegacyBuffer { uint32_t bodyLength = ntohl(_currentHeader.bodyLength); if (_traditionBuffer.length < bodyLength) { TJPLOG_INFO(@"数据长度不够内容解析,等待更多数据..."); return nil; } NSData *payload = [_traditionBuffer subdataWithRange:NSMakeRange(0, bodyLength)]; [_traditionBuffer replaceBytesInRange:NSMakeRange(0, bodyLength) withBytes:NULL length:0]; // 验证CRC32校验和 if (![self validateChecksum:_currentHeader.checksum forData:payload]) { TJPLOG_ERROR(@"校验和验证失败,可能数据已被篡改"); _state = TJPParseStateError; return nil; } NSError *error = nil; TJPParsedPacket *body = [TJPParsedPacket packetWithHeader:_currentHeader payload:payload policy:TJPTLVTagPolicyRejectDuplicates maxNestedDepth:4 error:&error]; if (error) { TJPLOG_INFO(@"解析序列号:%u 的内容失败: %@", ntohl(_currentHeader.sequence), error.localizedDescription); _state = TJPParseStateError; return nil; } // TJPLOG_INFO(@"解析序列号:%u 的内容成功", ntohl(_currentHeader.sequence)); _state = TJPParseStateHeader; return body; } - (BOOL)validateChecksum:(uint32_t)expectedChecksum forData:(NSData *)data { uint32_t calculatedChecksum = [TJPNetworkUtil crc32ForData:data]; if (calculatedChecksum != expectedChecksum) { TJPLOG_ERROR(@"校验和不匹配: 期望 %u, 计算得到 %u", expectedChecksum, calculatedChecksum); return NO; } return YES; } #pragma mark - Private Method - (BOOL)validateHeader:(TJPFinalAdavancedHeader)header error:(NSError **)error { //魔数校验 if (ntohl(header.magic) != kProtocolMagic) { if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorProtocolMagicInvalid description:@"无效的魔数" userInfo:@{@"receivedMagic": @(ntohl(header.magic)), @"expectedMagic": @(kProtocolMagic)}]; } TJPLOG_ERROR(@"魔数校验失败: 0x%X != 0x%X", ntohl(header.magic), kProtocolMagic); return NO; } //版本校验 if (header.version_major != kProtocolVersionMajor || header.version_minor > kProtocolVersionMinor) { if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorProtocolVersionMismatch description:@"不支持的协议版本" userInfo:@{@"receivedVersion": [NSString stringWithFormat:@"%d.%d", header.version_major, header.version_minor], @"supportedVersion": [NSString stringWithFormat:@"%d.%d", kProtocolVersionMajor, kProtocolVersionMinor]}]; } TJPLOG_ERROR(@"协议版本不支持: %d.%d (当前支持: %d.%d)", header.version_major, header.version_minor, kProtocolVersionMajor, kProtocolVersionMinor); return NO; } //消息体长度校验 uint32_t bodyLength = ntohl(header.bodyLength); if (bodyLength > TJPMAX_BODY_SIZE) { if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorMessageTooLarge description:@"消息体长度超过限制" userInfo:@{@"bodyLength": @(bodyLength), @"maxSize": @(TJPMAX_BODY_SIZE)}]; } TJPLOG_ERROR(@"消息体长度超过限制: %u > %d", bodyLength, TJPMAX_BODY_SIZE); return NO; } //时间戳校验 uint32_t currTime = (uint32_t)[[NSDate date] timeIntervalSince1970]; uint32_t timestamp = ntohl(header.timestamp); // 确保字节序转换 int32_t timeDiff = (int32_t)currTime - (int32_t)timestamp; if (abs(timeDiff) > TJPMAX_TIME_WINDOW) { if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorProtocolTimestampInvalid description:@"时间戳超出有效窗口" userInfo:@{@"currentTime": @(currTime), @"messageTime": @(timestamp), @"difference": @(timeDiff)}]; } TJPLOG_ERROR(@"时间戳超出有效窗口: 当前时间 %u, 消息时间 %u, 差值 %d秒", currTime, timestamp, timeDiff); return NO; } //根据消息类型决定是否进行重放攻击检测 uint16_t messageType = ntohs(header.msgType); uint32_t sequence = ntohl(header.sequence); // ACK包和心跳包不进行重放攻击检测 if (messageType == TJPMessageTypeACK) { TJPLOG_DEBUG(@"[TJPMessageParser] ACK包跳过重放攻击检测,序列号: %u", sequence); } else if (messageType == TJPMessageTypeHeartbeat) { TJPLOG_DEBUG(@"[TJPMessageParser] 心跳包跳过重放攻击检测,序列号: %u", sequence); } else { //序列号防重放检查 NSString *uniqueID = [NSString stringWithFormat:@"%u-%u", sequence, timestamp]; @synchronized (_recentSequences) { if ([_recentSequences containsObject:uniqueID]) { if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorSecurityReplayAttackDetected description:@"检测到重放攻击" userInfo:@{@"sequence": @(sequence), @"timestamp": @(timestamp)}]; } TJPLOG_ERROR(@"检测到重放攻击: 序列号 %u, 时间戳 %u", sequence, timestamp); return NO; } [_recentSequences addObject:uniqueID]; } } //校验加密类型和压缩类型 if (![self isSupportedEncryptType:header.encrypt_type]) { if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorProtocolUnsupportedEncryption description:@"不支持的加密类型" userInfo:@{@"encryptType": @(header.encrypt_type)}]; } TJPLOG_ERROR(@"不支持的加密类型: %d", header.encrypt_type); return NO; } if (![self isSupportedCompressType:header.compress_type]) { if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorProtocolUnsupportedCompression description:@"不支持的压缩类型" userInfo:@{@"compressType": @(header.compress_type)}]; } TJPLOG_ERROR(@"不支持的压缩类型: %d", header.compress_type); return NO; } return YES; } - (BOOL)isSupportedEncryptType:(TJPEncryptType)type { // 根据实际支持的加密类型进行验证 switch (type) { case TJPEncryptTypeNone: case TJPEncryptTypeCRC32: case TJPEncryptTypeAES256: return YES; default: return NO; } } - (BOOL)isSupportedCompressType:(TJPCompressType)type { // 根据实际支持的压缩类型进行验证 switch (type) { case TJPCompressTypeNone: case TJPCompressTypeZlib: // case TJPCompressTypeLZ4: return YES; default: return NO; } } - (void)cleanupExpiredSequences { NSDate *now = [NSDate date]; NSTimeInterval elapsed = [now timeIntervalSinceDate:_lastCleanupTime]; // 每分钟清理一次 if (elapsed > 60) { @synchronized (_recentSequences) { // 过期的序列号集合会随着时间增加而增长,定期清理 TJPLOG_INFO(@"清理过期序列号缓存,当前数量: %lu", (unsigned long)_recentSequences.count); [_recentSequences removeAllObjects]; _lastCleanupTime = now; } } } - (BOOL)shouldUseRingBufferByDefault { // 内存检查 NSUInteger totalMemoryMB = [NSProcessInfo processInfo].physicalMemory / (1024 * 1024); if (totalMemoryMB < 1024) { TJPLOG_INFO(@"设备内存较少(%luMB),选择传统缓冲区", (unsigned long)totalMemoryMB); return NO; } // 历史错误率检查(简单版本) if (_totalOperations > 100 && (CGFloat)_errorCount / _totalOperations > 0.1) { TJPLOG_INFO(@"历史错误率较高(%.1f%%),选择传统缓冲区", (CGFloat)_errorCount / _totalOperations * 100); return NO; } // 默认倾向于使用环形缓冲区 return YES; } - (NSUInteger)validateCapacity:(NSUInteger)capacity { if (self.strategyDelegate && [self.strategyDelegate respondsToSelector:@selector(recommendedCapacityForParser:)]) { NSUInteger delegateCapacity = [self.strategyDelegate recommendedCapacityForParser:self]; if (delegateCapacity > 0) { capacity = delegateCapacity; } } // 边界检查 NSUInteger minCapacity = 16 * 1024; //最小16KB NSUInteger maxCapacity = 1024 * 1024; //最大1MB if (capacity < minCapacity) { TJPLOG_WARN(@"容量过小(%luKB),调整为最小值%luKB", (unsigned long)capacity / 1024, (unsigned long)minCapacity / 1024); capacity = minCapacity; } else if (capacity > maxCapacity) { TJPLOG_WARN(@"容量过大(%luKB),调整为最大值%luKB", (unsigned long)capacity / 1024, (unsigned long)maxCapacity / 1024); capacity = maxCapacity; } return capacity; } + (NSUInteger)recommendedDefaultCapacity { NSUInteger totalMemoryMB = [NSProcessInfo processInfo].physicalMemory / (1024 * 1024); if (totalMemoryMB < 1024) { return 16 * 1024; // 16KB - 低端设备 } else if (totalMemoryMB < 2048) { return 32 * 1024; // 32KB - 中端设备 } else if (totalMemoryMB < 4096) { return 64 * 1024; // 64KB - 高端设备 } else { return 128 * 1024; // 128KB - 顶级设备 } } - (NSString *)strategyDescription:(TJPBufferStrategy)strategy { switch (strategy) { case TJPBufferStrategyAuto: return @"自动选择"; case TJPBufferStrategyTradition: return @"强制传统"; case TJPBufferStrategyRingBuffer: return @"强制环形"; default: return @"未知策略"; } } - (NSString *)stateDescription:(TJPParseState)state { switch (state) { case TJPParseStateHeader: return @"等待头部"; case TJPParseStateBody: return @"等待消息体"; case TJPParseStateError: return @"错误状态"; default: return @"未知状态"; } } #pragma mark - Method Change - (BOOL)switchToRingBuffer { return [self switchToRingBufferWithCapacity:_requestCapacity]; } - (BOOL)switchToRingBufferWithCapacity:(NSUInteger)capacity { if (_isUseRingBuffer) { TJPLOG_INFO(@"已经在使用环形缓冲区"); return YES; } // 创建新的环形缓冲区 capacity = [self validateCapacity:capacity]; TJPRingBuffer *newRingBuffer = [[TJPRingBuffer alloc] initWithCapacity:capacity]; if (!newRingBuffer) { TJPLOG_ERROR(@"环形缓冲区创建失败"); [self recordError:@"环形缓冲区创建失败"]; return NO; } // 数据迁移 if (_traditionBuffer.length > 0) { NSUInteger written = [newRingBuffer writeData:_traditionBuffer]; if (written != _traditionBuffer.length) { TJPLOG_WARN(@"数据迁移不完整: %lu/%lu", (unsigned long)written, (unsigned long)_traditionBuffer.length); } TJPLOG_INFO(@"成功迁移 %lu 字节数据到环形缓冲区", (unsigned long)written); } // 切换实现 _ringBuffer = newRingBuffer; [_traditionBuffer setLength:0]; _isUseRingBuffer = YES; _switchCount++; TJPLOG_INFO(@"成功切换到环形缓冲区,容量: %luKB", (unsigned long)capacity / 1024); // 通知代理 [self notifyStrategySwitch:@"环形缓冲区" reason:@"手动切换"]; return YES; } - (void)switchToTraditionBuffer { if (!_isUseRingBuffer) { TJPLOG_INFO(@"已经在使用传统缓冲区"); return; } // 数据迁移 if (_ringBuffer.usedSize > 0) { NSData *data = [_ringBuffer readData:_ringBuffer.usedSize]; if (data) { [_traditionBuffer appendData:data]; TJPLOG_INFO(@"成功迁移 %lu 字节数据到传统缓冲区", (unsigned long)data.length); } } // 切换实现 _isUseRingBuffer = NO; _switchCount++; TJPLOG_INFO(@"成功切换到传统缓冲区"); // 通知代理 [self notifyStrategySwitch:@"传统缓冲区" reason:@"手动切换"]; } - (BOOL)switchToOptimalMode { BOOL shouldUseRing = [self shouldUseRingBufferByDefault]; if (shouldUseRing && !_isUseRingBuffer) { return [self switchToRingBuffer]; } else if (!shouldUseRing && _isUseRingBuffer) { [self switchToTraditionBuffer]; return YES; } TJPLOG_INFO(@"当前模式已是最优模式"); return YES; } - (void)handleRingBufferError:(NSString *)reason { [self recordError:reason]; // 咨询策略代理 if ([_strategyDelegate respondsToSelector:@selector(parser:shouldContinueAfterError:)]) { NSError *error = [NSError errorWithDomain:@"TJPRingBufferError" code:-1 userInfo:@{NSLocalizedDescriptionKey: reason}]; BOOL shouldContinue = [_strategyDelegate parser:self shouldContinueAfterError:error]; if (!shouldContinue) { TJPLOG_WARN(@"策略代理建议切换实现,原因: %@", reason); [self switchToTraditionBuffer]; return; } } // 简单的错误处理策略 if (_errorCount > 5 && _totalOperations > 10) { CGFloat errorRate = (CGFloat)_errorCount / _totalOperations; if (errorRate > 0.3) { // 错误率超过30% TJPLOG_WARN(@"环形缓冲区错误率过高(%.1f%%),切换到传统模式", errorRate * 100); [self switchToTraditionBuffer]; return; } } // 继续使用环形缓冲区,但重置状态 [self reset]; _state = TJPParseStateError; } - (void)recordError:(NSString *)reason { _errorCount++; TJPLOG_ERROR(@"记录错误: %@ (总错误: %lu, 总操作: %lu)", reason, (unsigned long)_errorCount, (unsigned long)_totalOperations); } - (void)notifyStrategySwitch:(NSString *)implementation reason:(NSString *)reason { if ([_strategyDelegate respondsToSelector:@selector(parser:didSwitchToImplementation:reason:)]) { [_strategyDelegate parser:self didSwitchToImplementation:implementation reason:reason]; } } #pragma mark - 属性实现 - (BOOL)isUsingRingBuffer { return _isUseRingBuffer; } - (NSUInteger)bufferCapacity { if (_isUseRingBuffer) { return _ringBuffer.capacity; } else { return TJPMAX_BUFFER_SIZE; // 传统缓冲区的理论最大容量 } } - (NSUInteger)usedBufferSize { if (_isUseRingBuffer) { return _ringBuffer.usedSize; } else { return _traditionBuffer.length; } } - (CGFloat)bufferUsageRatio { if (_isUseRingBuffer) { return _ringBuffer.usageRatio; } else { return (CGFloat)_traditionBuffer.length / TJPMAX_BUFFER_SIZE; } } - (TJPBufferStrategy)currentStrategy { return _strategy; } - (TJPParseState)currentState { return _state; } - (NSMutableData *)buffer { if (_isUseRingBuffer) { NSUInteger usedSize = _ringBuffer.usedSize; if (usedSize > 0) { NSMutableData *testBuffer = [NSMutableData dataWithCapacity:usedSize]; char *tempBuffer = malloc(usedSize); if (tempBuffer) { NSUInteger peeked = [_ringBuffer peekBytes:tempBuffer length:usedSize]; if (peeked == usedSize) { [testBuffer appendBytes:tempBuffer length:usedSize]; } free(tempBuffer); } return testBuffer; } return [NSMutableData data]; } else { return _traditionBuffer; } } - (TJPFinalAdavancedHeader)currentHeader { return _currentHeader; } #pragma mark - 监控和调试方法 - (void)printBufferStatus { TJPLOG_INFO(@"=== MessageParser 缓冲区状态 ==="); TJPLOG_INFO(@"当前策略: %@", [self strategyDescription:_strategy]); TJPLOG_INFO(@"当前实现: %@", _isUseRingBuffer ? @"环形缓冲区" : @"传统缓冲区"); TJPLOG_INFO(@"缓冲区容量: %luKB", (unsigned long)self.bufferCapacity / 1024); TJPLOG_INFO(@"已使用大小: %luKB", (unsigned long)self.usedBufferSize / 1024); TJPLOG_INFO(@"使用率: %.1f%%", self.bufferUsageRatio * 100); TJPLOG_INFO(@"解析状态: %@", [self stateDescription:_state]); TJPLOG_INFO(@"错误统计: %lu/%lu (%.2f%%)", (unsigned long)_errorCount, (unsigned long)_totalOperations, _totalOperations > 0 ? (CGFloat)_errorCount / _totalOperations * 100 : 0); TJPLOG_INFO(@"切换次数: %lu", (unsigned long)_switchCount); } - (NSDictionary *)bufferStatistics { return @{ @"strategy": [self strategyDescription:_strategy], @"implementation": _isUseRingBuffer ? @"ring_buffer" : @"legacy", @"capacity": @(self.bufferCapacity), @"usedSize": @(self.usedBufferSize), @"usageRatio": @(self.bufferUsageRatio), @"state": [self stateDescription:_state], @"errorCount": @(_errorCount), @"totalOperations": @(_totalOperations), @"errorRate": @(_totalOperations > 0 ? (CGFloat)_errorCount / _totalOperations : 0), @"switchCount": @(_switchCount) }; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Parser/TJPParsedPacket.h ================================================ // // TJPParsedPacket.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/22. // 用于表示解析后的协议包 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @interface TJPParsedPacket : NSObject /// 消息类型 @property (nonatomic, assign) TJPMessageType messageType; /// 序列号 @property (nonatomic, assign) uint32_t sequence; /// 消息头 @property (nonatomic, assign) TJPFinalAdavancedHeader header; /// 消息内容 @property (nonatomic, strong) NSData *payload; /// TLV解析后的字段(Tag -> Value) @property (nonatomic, strong) NSDictionary *tlvEntries; // 支持嵌套存储 /// TLV策略 @property (nonatomic, assign) TJPTLVTagPolicy tagPolicy; + (instancetype)packetWithHeader:(TJPFinalAdavancedHeader)header; + (instancetype)packetWithHeader:(TJPFinalAdavancedHeader)header payload:(NSData *)payload policy:(TJPTLVTagPolicy)policy maxNestedDepth:(NSUInteger)maxDepth error:(NSError **)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Parser/TJPParsedPacket.m ================================================ // // TJPParsedPacket.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/22. // #import "TJPParsedPacket.h" #import "TJPNetworkDefine.h" #import "TJPErrorUtil.h" static const uint16_t kTLVReservedNestedTag = 0xFFFF; @implementation TJPParsedPacket + (instancetype)packetWithHeader:(TJPFinalAdavancedHeader)header payload:(NSData *)payload policy:(TJPTLVTagPolicy)policy maxNestedDepth:(NSUInteger)maxDepth error:(NSError **)error { TJPParsedPacket *packet = [[TJPParsedPacket alloc] init]; packet.header = header; packet.payload = payload; packet.tagPolicy = policy; NSError *parseError = nil; // 新增TLV解析 packet.tlvEntries = [self parseTLVFromData:payload policy:policy maxNestedDepth:maxDepth currentDepth:0 error:&parseError]; if (parseError) { if (error) *error = parseError; return nil; } packet.messageType = ntohs(header.msgType); // 需要用ntohs反转消息类型字节序 packet.sequence = ntohl(header.sequence); // 使用ntohl转换序列号为主机字节序 return packet; } + (instancetype)packetWithHeader:(TJPFinalAdavancedHeader)header { TJPParsedPacket *packet = [[TJPParsedPacket alloc] init]; packet.header = header; packet.messageType = ntohs(header.msgType); // 需要用ntohs反转消息类型字节序 packet.sequence = ntohl(header.sequence); // 使用ntohl转换序列号为主机字节序 return packet; } // TLV解析核心逻辑 + (NSDictionary *)parseTLVFromData:(NSData *)data policy:(TJPTLVTagPolicy)policy maxNestedDepth:(NSUInteger)maxDepth currentDepth:(NSUInteger)currentDepth error:(NSError **)error { if (currentDepth > maxDepth) { if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorTLVNestedTooDeep description:[NSString stringWithFormat:@"嵌套深度超过限制:%lu", maxDepth] userInfo:@{@"maxDepth": @(maxDepth), @"currentDepth": @(currentDepth)}]; } return nil; } NSMutableDictionary *tlvDict = [NSMutableDictionary dictionary]; const uint8_t *bytes = data.bytes; NSUInteger length = data.length; NSUInteger offset = 0; while (offset < length) { //解析Tag if (offset + 2 > length) { TJPLOG_ERROR(@"TLV解析失败:Tag不完整 (offset=%lu, total=%lu)", offset, length); if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorTLVIncompleteTag description:@"TLV解析失败:Tag不完整" userInfo:@{@"offset": @(offset), @"length": @(length)}]; } return nil; } uint16_t tag = CFSwapInt16BigToHost(*(uint16_t *)(bytes + offset)); offset += 2; //解析Length if (offset + 4 > length) { TJPLOG_ERROR(@"TLV解析失败:Length不完整 (offset=%lu)", offset); if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorTLVIncompleteLength description:@"TLV解析失败:Length不完整" userInfo:@{@"offset": @(offset), @"length": @(length)}]; } return nil; } uint32_t valueLen = CFSwapInt32BigToHost(*(uint32_t *)(bytes + offset)); offset += 4; //解析Value if (offset + valueLen > length) { TJPLOG_ERROR(@"TLV解析失败:Value长度越界 (声明长度=%u, 剩余长度=%lu)", valueLen, (length - offset)); if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorTLVIncompleteValue description:@"TLV解析失败:Value长度越界" userInfo:@{@"valueLen": @(valueLen)}]; } return nil; } NSData *valueData = [NSData dataWithBytes:bytes + offset length:valueLen]; offset += valueLen; //查重Tag if (policy == TJPTLVTagPolicyRejectDuplicates && tlvDict[@(tag)]) { TJPLOG_ERROR(@"%@", [NSString stringWithFormat:@"重复Tag:0x%04X", tag]); if (error) { *error = [TJPErrorUtil errorWithCode:TJPErrorTLVDuplicateTag description:@"TLV解析失败:重复Tag" userInfo:@{@"tag": @(tag)}]; } return nil; } //处理嵌套TLV if (tag == kTLVReservedNestedTag) { NSError *nestedError = nil; //递归处理嵌套 NSDictionary *nested = [self parseTLVFromData:valueData policy:policy maxNestedDepth:maxDepth currentDepth:currentDepth + 1 error:&nestedError]; if (nestedError) { if (error) *error = nestedError; return nil; } tlvDict[@(tag)] = nested; }else { tlvDict[@(tag)] = valueData; } } return [tlvDict copy]; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Parser/TJPRingBuffer.h ================================================ // // TJPRingBuffer.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/26. // 环形缓冲区 #import NS_ASSUME_NONNULL_BEGIN @interface TJPRingBuffer : NSObject /// 缓冲区总容量大小 单位字节 @property (nonatomic, readonly) NSUInteger capacity; /// 当前缓冲区已使用大小 @property (nonatomic, readonly) NSUInteger usedSize; /// 当前缓冲区剩余可写入空间 @property (nonatomic, readonly) NSUInteger availableSpace; /// 当前读取索引位置 @property (nonatomic, readonly) NSUInteger readIndex; /// 当前写入索引位置 @property (nonatomic, readonly) NSUInteger writeIndex; /// 初始化方法 /// - Parameter capacity: 缓冲区容量 - (instancetype)initWithCapacity:(NSUInteger)capacity; /// 向缓冲区写入数据 /// - Parameter data: 要写入的数据 - (NSUInteger)writeData:(NSData *)data; /// 向缓冲区写入原始字节数据 /// - Parameters: /// - bytes: 字节数据指针 /// - length: 数据长度 - (NSUInteger)writeBytes:(const void *)bytes length:(NSUInteger)length; /// 冲缓冲区读取指定长度的数据 /// - Parameter length: 要读取的字节数 - (NSData *)readData:(NSUInteger)length; /// 从缓冲区读取数据到指定buffer /// - Parameters: /// - buffer: 目标缓冲区 /// - length: 要读取的字节数 - (NSUInteger)readBytes:(void *)buffer length:(NSUInteger)length; /// 预览指定长度数据 /// - Parameter length: 预览字节数 - (NSData *)peekData:(NSUInteger)length; /// 预览数据到指定buffer(不移动读指针) /// - Parameters: /// - buffer: 目标缓冲区 /// - length: 要预览的字节数 - (NSUInteger)peekBytes:(void *)buffer length:(NSUInteger)length; /// 跳过指定长度数据 仅仅移动读指针 /// - Parameter length: 要跳过的长度 - (NSUInteger)skipBytes:(NSUInteger)length; /// 检查是否有足够的数据可读 /// - Parameter length: 要检查的数据长度 - (BOOL)hasAvailableData:(NSUInteger)length; /// 缓冲区使用率 - (CGFloat)usageRatio; - (void)reset; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Parser/TJPRingBuffer.m ================================================ // // TJPRingBuffer.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/26. // #import "TJPRingBuffer.h" #import "TJPNetworkDefine.h" @interface TJPRingBuffer () { char *_buffer; NSUInteger _capacity; NSUInteger _readIndex; NSUInteger _writeIndex; NSUInteger _usedSize; dispatch_queue_t _accessQueue; } @end @implementation TJPRingBuffer #pragma mark - Lifecycle - (instancetype)initWithCapacity:(NSUInteger)capacity { if (self = [super init]) { // 边界判断 if (capacity == 0 || capacity > TJPMAX_BUFFER_SIZE) { TJPLOG_ERROR(@"无效的缓冲区容量: %lu", capacity); return nil; } _capacity = capacity; // 开辟内存空间 _buffer = malloc(capacity); if (!_buffer) { TJPLOG_ERROR(@"环形缓冲区内存分配失败,容量: %lu", capacity); return nil; } _readIndex = 0; _writeIndex = 0; _usedSize = 0; _accessQueue = dispatch_queue_create("com.tjp.ringBuffer.accessQueue", DISPATCH_QUEUE_SERIAL); TJPLOG_INFO(@"环形缓冲区初始化成功,容量: %lu bytes", capacity); } return self; } - (void)dealloc { if (_buffer) { free(_buffer); _buffer = NULL; } } #pragma mark - Public Method - (NSUInteger)writeData:(NSData *)data { if (!data || data.length == 0) { return 0;; } return [self writeBytes:data.bytes length:data.length]; } - (NSUInteger)writeBytes:(const void *)bytes length:(NSUInteger)length { if (!bytes || length == 0) { return 0; } __block NSUInteger writtenBytes = 0; dispatch_sync(_accessQueue, ^{ writtenBytes = [self _unsafeWriteBytes:bytes length:length]; }); return writtenBytes; } - (NSUInteger)_unsafeWriteBytes:(const void*)bytes length:(NSUInteger)length { //计算可写入的字节数 NSUInteger availableSpace = _capacity - _usedSize; NSUInteger bytesToWrite = MIN(length, availableSpace); if (bytesToWrite == 0) { TJPLOG_WARN(@"环形缓冲区空间不足,无法写入数据"); return 0; } const char *sourceBytes = (const char *)bytes; //计算写入到缓冲区末尾的字节数 NSUInteger bytesToEnd = _capacity - _writeIndex; if (bytesToWrite <= bytesToEnd) { //数据可以连续写入 memcpy(_buffer + _writeIndex, sourceBytes, bytesToWrite); }else { //到尾部了需要拼接头部写入 环绕 memcpy(_buffer + _writeIndex, sourceBytes, bytesToEnd); //写头部 memcpy(_buffer, sourceBytes + bytesToEnd, bytesToWrite - bytesToEnd); } //更新写指针 _writeIndex = (_writeIndex + bytesToWrite) % _capacity; _usedSize += bytesToWrite; return bytesToWrite; } - (NSData *)readData:(NSUInteger)length { if (length == 0) { return [NSData data]; } __block NSData *result = nil; dispatch_sync(_accessQueue, ^{ if (self->_usedSize < length) { return; } char *tempBuffer = malloc(length); if (!tempBuffer) { TJPLOG_ERROR(@"临时缓冲区分配失败"); return; } NSUInteger readBytes = [self _unsafeReadBytes:tempBuffer length:length]; if (readBytes == length) { result = [NSData dataWithBytes:tempBuffer length:length]; } free(tempBuffer); }); return result; } - (NSUInteger)readBytes:(void *)buffer length:(NSUInteger)length { if (!buffer || length == 0) { return 0; } __block NSUInteger readBytes = 0; dispatch_sync(_accessQueue, ^{ readBytes = [self _unsafeReadBytes:buffer length:length]; }); return readBytes; } - (NSUInteger)_unsafeReadBytes:(void*)buffer length:(NSUInteger)length { NSUInteger bytesToRead = MIN(length, _usedSize); if (bytesToRead == 0) { return 0; } char *destBuffer = (char *)buffer; //计算从读指针到缓冲区末尾的字节数 NSUInteger bytesToEnd = _capacity - _readIndex; if (bytesToRead <= bytesToEnd) { //数据可以连续读取 memcpy(destBuffer, _buffer + _readIndex, bytesToRead); }else { //需要环绕读取 memcpy(destBuffer, _buffer + _readIndex, bytesToEnd); memcpy(destBuffer + bytesToEnd, _buffer, bytesToRead - bytesToEnd); } //更新读指针大小 _readIndex = (_readIndex + bytesToRead) % _capacity; _usedSize -= bytesToRead; return bytesToRead; } - (NSData *)peekData:(NSUInteger)length { if (length == 0) { return [NSData data]; } __block NSData *result = nil; dispatch_sync(_accessQueue, ^{ if (self->_usedSize < length) { return; } char *tempBuffer = malloc(length); if (!tempBuffer) { TJPLOG_ERROR(@"临时缓冲区分配失败"); return; } NSUInteger peekBytes = [self _unsafePeekBytes:tempBuffer length:length]; if (peekBytes == length) { result = [NSData dataWithBytes:tempBuffer length:length]; } free(tempBuffer); }); return result; } - (NSUInteger)peekBytes:(void *)buffer length:(NSUInteger)length { if (!buffer || length == 0) { return 0; } __block NSUInteger peekBytes = 0; dispatch_sync(_accessQueue, ^{ peekBytes = [self _unsafePeekBytes:buffer length:length]; }); return peekBytes; } - (NSUInteger)_unsafePeekBytes:(void *)buffer length:(NSUInteger)length { NSUInteger bytesToPeek = MIN(length, _usedSize); if (bytesToPeek == 0) { return 0; } char *destBuffer = (char *)buffer; NSUInteger tempReadIndex = _readIndex; //计算从读指针到缓冲区末尾的字节数 NSUInteger bytesToEnd = _capacity - tempReadIndex; if (bytesToPeek <= bytesToEnd) { //数据可以连续读取 memcpy(destBuffer, _buffer + tempReadIndex, bytesToPeek); }else { //分段读取 环绕 memcpy(destBuffer, _buffer + tempReadIndex, bytesToEnd); memcpy(destBuffer + bytesToEnd, _buffer, bytesToPeek - bytesToEnd); } // peek时操作部更新读指针和大小 return bytesToPeek; } - (NSUInteger)skipBytes:(NSUInteger)length { __block NSUInteger skippedBytes = 0; dispatch_sync(_accessQueue, ^{ NSUInteger bytesToSkip = MIN(length, self->_usedSize); self->_readIndex = (self->_readIndex + bytesToSkip) % self->_capacity; self->_usedSize -= bytesToSkip; skippedBytes = bytesToSkip; }); return skippedBytes; } - (void)reset { dispatch_sync(_accessQueue, ^{ self->_readIndex = 0; self->_writeIndex = 0; self->_usedSize = 0; }); TJPLOG_INFO(@"环形缓冲区已重置"); } - (BOOL)hasAvailableData:(NSUInteger)length { return self.usedSize >= length; } - (CGFloat)usageRatio { return (CGFloat)self.usedSize / (CGFloat)_capacity; } #pragma mark - Debug - (NSString *)description { return [NSString stringWithFormat:@"", _capacity, self.usedSize, self.readIndex, self.writeIndex, self.usageRatio * 100]; } #pragma mark - Getter Method - (NSUInteger)capacity { return _capacity; } - (NSUInteger)usedSize { __block NSUInteger result; dispatch_sync(_accessQueue, ^{ result = self->_usedSize; }); return result; } - (NSUInteger)availableSpace { __block NSUInteger result; dispatch_sync(_accessQueue, ^{ result = self->_capacity - self->_usedSize; }); return result; } - (NSUInteger)readIndex { __block NSUInteger result; dispatch_sync(_accessQueue, ^{ result = self->_readIndex; }); return result; } - (NSUInteger)writeIndex { __block NSUInteger result; dispatch_sync(_accessQueue, ^{ result = self->_writeIndex; }); return result; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Policy/TJPReconnectPolicy.h ================================================ // // TJPReconnectPolicy.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // 重试策略 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @class TJPReconnectPolicy; @protocol TJPReconnectPolicyDelegate @optional - (void)reconnectPolicyDidReachMaxAttempts:(TJPReconnectPolicy *)reconnectPolicy; - (NSString *)getCurrentConnectionState; @end @interface TJPReconnectPolicy : NSObject @property (nonatomic, weak) id delegate; /// 最大尝试数 @property (nonatomic, assign) NSInteger maxAttempts; /// 当前尝试次数 @property (nonatomic, assign) NSInteger currentAttempt; /// 基础延迟 @property (nonatomic, assign) NSTimeInterval baseDelay; /// 最大延迟 @property (nonatomic, assign) NSTimeInterval maxDelay; //单元测试用 @property (nonatomic, readonly) dispatch_qos_class_t qosClass; /// 初始化方法 - (instancetype)initWithMaxAttempst:(NSInteger)attempts baseDelay:(NSTimeInterval)delay qos:(TJPNetworkQoS)qos delegate:(id)delegate; /// 尝试连接 - (void)attemptConnectionWithBlock:(dispatch_block_t)connectionBlock; /// 计算延迟 - (NSTimeInterval)calculateDelay; /// 最大重试次数 - (void)notifyReachMaxAttempts; /// 停止重试 - (void)stopRetrying; /// 重置 - (void)reset; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Policy/TJPReconnectPolicy.m ================================================ // // TJPReconnectPolicy.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #import "TJPReconnectPolicy.h" #import #import "TJPNetworkCoordinator.h" #import "TJPNetworkDefine.h" static const NSTimeInterval kMaxReconnectDelay = 30; @interface TJPReconnectPolicy () @property (nonatomic, strong) dispatch_block_t currentRetryTask; @end @implementation TJPReconnectPolicy { //当前尝试次数 // NSInteger _currentAttempt; //网络任务的QoS级别 dispatch_qos_class_t _qosClass; } - (instancetype)initWithMaxAttempst:(NSInteger)attempts baseDelay:(NSTimeInterval)delay qos:(TJPNetworkQoS)qos delegate:(id)delegate { if (self = [super init]) { _maxAttempts = attempts; _baseDelay = delay; _qosClass = [self qosClassFromEnum:qos]; _delegate = delegate; _currentAttempt = 0; } return self; } - (dispatch_qos_class_t)qosClassFromEnum:(TJPNetworkQoS)qos { switch (qos) { case TJPNetworkQoSUserInitiated: return QOS_CLASS_USER_INITIATED; case TJPNetworkQoSBackground: return QOS_CLASS_BACKGROUND; default: return QOS_CLASS_DEFAULT; } } - (void)attemptConnectionWithBlock:(dispatch_block_t)connectionBlock { // 在开始重连前检查会话当前状态 if (self.delegate && [self.delegate respondsToSelector:@selector(getCurrentConnectionState)]) { NSString *currentState = [self.delegate getCurrentConnectionState]; if ([currentState isEqualToString:TJPConnectStateConnected] || [currentState isEqualToString:TJPConnectStateConnecting]) { TJPLOG_INFO(@"会话已在连接状态(%@),不需要重连", currentState); return; } } TJPLOG_INFO(@"开始连接尝试,当前尝试次数%ld/%ld", (long)_currentAttempt, (long)_maxAttempts); //如果超过最大重试次数 停止重试 if (_currentAttempt >= _maxAttempts) { TJPLOG_ERROR(@"已达到最大重试次数%ld/%ld,停止重试", (long)_currentAttempt, (long)_maxAttempts); [self notifyReachMaxAttempts]; return; } //指数退避+随机延迟的方式 避免服务器惊群效应 NSTimeInterval delay = [self calculateDelay]; //在指定的QoS级别的全局队列中调度重试任务 dispatch_queue_t queue = dispatch_get_global_queue(_qosClass, 0); self.currentRetryTask = dispatch_block_create(DISPATCH_BLOCK_INHERIT_QOS_CLASS, ^{ // 检查网络是否真的可达 if ([[TJPNetworkCoordinator shared].reachability currentReachabilityStatus] != NotReachable) { TJPLOG_INFO(@"网络状态可达,执行连接块"); if (connectionBlock) connectionBlock(); self->_currentAttempt++; TJPLOG_INFO(@"当前尝试次数更新为%ld", (long)self->_currentAttempt); } else { TJPLOG_INFO(@"网络不可达,跳过本次重连"); // 网络不可达时延迟再次尝试 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC), queue, ^{ [self attemptConnectionWithBlock:connectionBlock]; }); } }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), queue, self.currentRetryTask); } - (NSTimeInterval)calculateDelay { // 使用更标准的指数退避公式: 基础延迟 * (2^尝试次数) + 随机扰动 mock测试时要关闭随机数 double randomJitter = ((double)arc4random_uniform(1000)) / 1000.0; // 0-1的随机数 NSTimeInterval delay = _baseDelay * pow(2, _currentAttempt) + randomJitter; // 设置上限 return MIN(delay, kMaxReconnectDelay); } - (void)notifyReachMaxAttempts { TJPLOG_INFO(@"已达到最大重连次数: %ld", (long)_maxAttempts); if (self.delegate && [self.delegate respondsToSelector:@selector(reconnectPolicyDidReachMaxAttempts:)]) { [self.delegate reconnectPolicyDidReachMaxAttempts:self]; } } - (void)stopRetrying { // 停止当前的重试任务 if (self.currentRetryTask) { dispatch_block_cancel(self.currentRetryTask); self.currentRetryTask = nil; TJPLOG_INFO(@"停止当前重试任务"); } TJPLOG_INFO(@"重试操作已停止"); } - (void)reset { _currentAttempt = 0; } #pragma mark - 单元测试 - (dispatch_qos_class_t)qosClass { return _qosClass; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Protocol/TJPConnectionDelegate.h ================================================ // // TJPConnectionDelegate.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/15. // #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN //简化的连接状态 仅供连接管理类内部使用 后期重构 typedef NS_ENUM(NSInteger, TJPConnectionState) { TJPConnectionStateDisconnected, TJPConnectionStateConnecting, TJPConnectionStateConnected, TJPConnectionStateDisconnecting }; @class TJPConnectionManager; @protocol TJPConnectionDelegate @required /// 已连接 - (void)connectionDidConnect:(TJPConnectionManager *)connection; /// 断开连接 - (void)connection:(TJPConnectionManager *)connection didDisconnectWithError:(NSError *)error reason:(TJPDisconnectReason)reason; /// 收到消息 - (void)connection:(TJPConnectionManager *)connection didReceiveData:(NSData *)data; @optional /// 将要连接 - (void)connectionWillConnect:(TJPConnectionManager *)connection; /// 将要断开连接 - (void)connectionWillDisconnect:(TJPConnectionManager *)connection reason:(TJPDisconnectReason)reason; /// 连接已加密 - (void)connectionDidSecure:(TJPConnectionManager *)connection; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Protocol/TJPHeartbeatProtocol.h ================================================ // // TJPHeartbeatProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/4/22. // #import NS_ASSUME_NONNULL_BEGIN @protocol TJPHeartbeatProtocol // 开始监听 - (void)startMonitoring; // 停止监听 - (void)stopMonitoring; // 自适应心跳 - (void)adjustInterval:(NSTimeInterval)interval; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Protocol/TJPMessageManagerDelegate.h ================================================ // // TJPMessageManagerDelegate.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // 消息管理器回调代理 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @class TJPMessageContext; @protocol TJPMessageManagerDelegate @required - (void)messageManager:(id)manager message:(TJPMessageContext *)message didChangeState:(TJPMessageState)newState fromState:(TJPMessageState)oldState; @optional - (void)messageManager:(id)manager willSendMessage:(TJPMessageContext *)context; - (void)messageManager:(id)manager didSendMessage:(TJPMessageContext *)context; - (void)messageManager:(id)manager didReceiveACK:(TJPMessageContext *)context; - (void)messageManager:(id)manager didFailToSendMessage:(TJPMessageContext *)context error:(NSError *)error; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Protocol/TJPMessageManagerNetworkDelegate.h ================================================ // // TJPMessageManagerNetworkDelegate.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/24. // 网络发送代理协议 #import NS_ASSUME_NONNULL_BEGIN @class TJPMessageManager, TJPMessageContext; @protocol TJPMessageManagerNetworkDelegate /** * 请求网络层发送消息 */ - (void)messageManager:(TJPMessageManager *)manager needsSendMessage:(TJPMessageContext *)message; @optional /** * 请求网络层重传消息 */ - (void)messageManager:(TJPMessageManager *)manager needsRetransmitMessage:(TJPMessageContext *)message; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Protocol/TJPMessageProtocol.h ================================================ // // TJPMessageProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // 消息接口 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @protocol TJPMessageProtocol @required // 内容类型 @property (nonatomic, readonly) TJPContentType contentType; // 消息类型 如普通消息/ACK消息/控制消息 @property (nonatomic, readonly) TJPMessageType messageType; /// 消息类型 + (uint16_t)messageTag; /// TLV数据格式 - (NSData *)tlvData; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Protocol/TJPSessionDelegate.h ================================================ // // TJPSessionDelegate.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/5/13. // 整合所有代理 #import #import #import "TJPCoreTypes.h" #import "TJPSessionProtocol.h" NS_ASSUME_NONNULL_BEGIN @protocol TJPSessionDelegate @optional // 通知协调器处理重连 - (void)sessionNeedsReconnect:(id)session; // === 状态回调 === // 连接状态变化 - (void)session:(id)session didChangeState:(TJPConnectState)state; // 连接断开 - (void)session:(id)session didDisconnectWithReason:(TJPDisconnectReason)reason; // 连接失败 - (void)session:(id)session didFailWithError:(NSError *)error; - (void)sessionDidForceDisconnect:(id)session; // === 内容回调 === // 接收文本 - (void)session:(id)session didReceiveText:(NSString *)text; // 接收图片 - (void)session:(id)session didReceiveImage:(UIImage *)image; // 接收音频 - (void)session:(id)session didReceiveAudio:(NSData *)audioData; // 接收视频 - (void)session:(id)session didReceiveVideo:(NSData *)videoData; // 接收文件 - (void)session:(id)session didReceiveFile:(NSData *)fileData filename:(NSString *)filename; // 接收位置 - (void)session:(id)session didReceiveLocation:(CLLocation *)location; // 接收自定义内容 - (void)session:(id)session didReceiveCustomData:(NSData *)data withType:(uint16_t)customType; // 发送消息失败 - (void)session:(id)session didFailToSendMessageWithMessage:(NSString *)messageId error:(NSError *)error; /** * 版本协商完成时调用 * @param session 会话实例 * @param version 协商后的版本号 * @param features 协商后的特性标志 */ - (void)session:(id)session didCompleteVersionNegotiation:(uint16_t)version features:(uint16_t)features; // === 原始数据回调(高级用户) === // 接收原始数据 - (void)session:(id)session didReceiveRawData:(NSData *)data; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Protocol/TJPSessionProtocol.h ================================================ // // TJPSessionProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @protocol TJPSessionProtocol /// 状态获取 @property (nonatomic, readonly) TJPConnectState connectState; /// 每个会话会有独立的id 使用UUID保证唯一 @property (nonatomic, copy, readonly) NSString *sessionId; /// 主机地址 @property (nonatomic, readonly) NSString *host; /// 端口号 @property (nonatomic, readonly) uint16_t port; /// 网络断开 - (void)networkDidBecomeAvailable; /// 网络恢复 - (void)networkDidBecomeUnavailable; /// 会话连接方法 - (void)connectToHost:(NSString *)host port:(uint16_t)port; /// 会话断开连接 - (void)disconnect; /// 强制断开连接 - (void)forceDisconnect; /// 准备重连 - (void)forceReconnect; /// 更新连接状态 - (void)updateConnectionState:(TJPConnectState)state; /// 断开连接原因 - (void)disconnectWithReason:(TJPDisconnectReason)reason; /// 清理资源方法 - (void)prepareForRelease; /// 发送消息 - (void)sendData:(NSData *)data; /// 带回调的发送消息 - (NSString *)sendData:(NSData *)data messageType:(TJPMessageType)messageType encryptType:(TJPEncryptType)encryptType compressType:(TJPCompressType)compressType completion:(void(^)(NSString *msgId, NSError *error))completion; /// 发送心跳包 - (void)sendHeartbeat:(NSData *)heartbeatData; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Sequence/TJPSequenceManager.h ================================================ // // TJPSequenceManager.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/22. // 序列号管理器 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN /** * 序列号管理器 * * 设计说明: * - 32位序列号 = 8位类别 + 24位序列号 * - 支持4种消息类别,每种独立计数 * - 线程安全,使用 os_unfair_lock * - 会话级隔离,避免不同会话序列号冲突 * - 提前重置机制,避免序列号溢出 */ @interface TJPSequenceManager : NSObject /// 关联的会话ID(只读) @property (nonatomic, copy, readonly) NSString *sessionId; /// 序列号重置回调 @property (nonatomic, copy) void (^sequenceResetHandler)(TJPMessageCategory category); /// 初始化方法 - (instancetype)initWithSessionId:(nullable NSString *)sessionId; /// 根据类型获取下个序列号 - (uint32_t)nextSequenceForCategory:(TJPMessageCategory)category; /// 检查是否为该类别的序列号 - (BOOL)isSequenceForCategory:(uint32_t)sequence category:(TJPMessageCategory)category; /// 获取指定类别的当前序列号 - (uint32_t)currentSequenceForCategory:(TJPMessageCategory)category; /// 获取指定类别的原始序列号 - (uint32_t)currentRawSequenceForCategory:(TJPMessageCategory)category; /// 重置序列号 - (void)resetSequence; /// 重置当前类别序列号 - (void)resetSequence:(TJPMessageCategory)category; /// 检查指定类别的序列号是否在安全范围内 - (BOOL)isSequenceInSafeRange:(TJPMessageCategory)category; /// 获取详细统计信息 - (NSDictionary *)getStatistics; /// 健康检查 - (BOOL)isHealthy; /// 预测指定类别下次重置的时间 - (NSTimeInterval)estimateTimeToResetForCategory:(TJPMessageCategory)category averageQPS:(double)qps; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Sequence/TJPSequenceManager.m ================================================ // // TJPSequenceManager.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/22. // #import "TJPSequenceManager.h" #import #import "TJPNetworkDefine.h" @interface TJPSequenceManager () @property (nonatomic, copy, readwrite) NSString *sessionId; @property (nonatomic, assign) uint32_t sessionSeed; // 会话种子,用于避免不同会话间冲突 @end @implementation TJPSequenceManager { //根据类型对序列号分区 uint32_t _sequences[4]; os_unfair_lock _lock; // 统计信息 uint64_t _totalGenerated[4]; // 每个类别生成的总数 NSDate *_lastResetTime[4]; // 每个类别最后重置时间 } - (instancetype)initWithSessionId:(NSString *)sessionId { if (self = [super init]) { _lock = OS_UNFAIR_LOCK_INIT; _sessionId = sessionId; //基于sessionId生成种子,确保不同会话的序列号有差异 _sessionSeed = [self generateSessionSeed:_sessionId]; //初始化序列号(从种子开始,避免从0开始) [self initializeSequences]; //初始化统计信息 memset(_totalGenerated, 0, sizeof(_totalGenerated)); for (int i = 0; i < 4; i++) { _lastResetTime[i] = [NSDate date]; } } return self; } - (void)initializeSequences { os_unfair_lock_lock(&_lock); //使用会话种子初始化,避免所有会话都从0开始 for (int i = 0; i < 4; i++) { _sequences[i] = (_sessionSeed + i * 1000) & TJPSEQUENCE_BODY_MASK; // 确保不会太接近最大值 if (_sequences[i] > TJPSEQUENCE_RESET_THRESHOLD) { _sequences[i] = _sequences[i] % 10000; } } os_unfair_lock_unlock(&_lock); } - (uint32_t)nextSequenceForCategory:(TJPMessageCategory)category { os_unfair_lock_lock(&_lock); //获取目标类别的当前序列号 uint32_t *categorySequence = &_sequences[category]; //计算新序列号:类别8位 + 24位序列号 *categorySequence = (*categorySequence + 1) & TJPSEQUENCE_BODY_MASK; //更新统计 _totalGenerated[category]++; //增加最大值判断 if (*categorySequence > TJPSEQUENCE_WARNING_THRESHOLD) { //接近最大值时警告 if (self.sequenceResetHandler) { NSString *sessionId = [_sessionId copy]; dispatch_async(dispatch_get_main_queue(), ^{ self.sequenceResetHandler(category); }); TJPLOG_WARN(@"[TJPSequenceManager] 会话 %@ 类别 %d 序列号接近上限: %u", sessionId, (int)category, *categorySequence); } } //检查是否需要重置(提前重置,避免到达真正的最大值) if (*categorySequence >= TJPSEQUENCE_RESET_THRESHOLD) { TJPLOG_INFO(@"[TJPSequenceManager] 会话 %@ 类别 %d 序列号重置: %u -> 0", _sessionId, (int)category, *categorySequence); *categorySequence = 0; _lastResetTime[category] = [NSDate date]; // 通知重置事件 if (self.sequenceResetHandler) { dispatch_async(dispatch_get_main_queue(), ^{ self.sequenceResetHandler(category); }); } } //通过与上掩码取出24位序列号 uint32_t nextSeq = ((uint32_t)category << TJPSEQUENCE_BODY_BITS) | *categorySequence; os_unfair_lock_unlock(&_lock); return nextSeq; } - (BOOL)isSequenceForCategory:(uint32_t)sequence category:(TJPMessageCategory)category { // 提取序列号中的类别部分(高8位) uint8_t sequenceCategory = (sequence >> TJPSEQUENCE_BODY_BITS) & TJPSEQUENCE_CATEGORY_MASK; return sequenceCategory == category; } - (void)resetSequence:(TJPMessageCategory)category { os_unfair_lock_lock(&_lock); _sequences[category] = 0; _lastResetTime[category] = [NSDate date]; os_unfair_lock_unlock(&_lock); TJPLOG_INFO(@"[TJPSequenceManager] 手动重置会话 %@ 类别 %d 序列号", _sessionId, (int)category); } // 重置所有类别的序列号 - (void)resetSequence { os_unfair_lock_lock(&_lock); memset(_sequences, 0, sizeof(_sequences)); for (int i = 0; i < 4; i++) { _lastResetTime[i] = [NSDate date]; } os_unfair_lock_unlock(&_lock); TJPLOG_INFO(@"[TJPSequenceManager] 手动重置会话 %@ 所有序列号", _sessionId); } // 获取类别的当前序列号 - (uint32_t)currentSequenceForCategory:(TJPMessageCategory)category { os_unfair_lock_lock(&_lock); uint32_t seq = _sequences[category]; os_unfair_lock_unlock(&_lock); return ((uint32_t)category << TJPSEQUENCE_BODY_BITS) | seq; } - (uint32_t)currentRawSequenceForCategory:(TJPMessageCategory)category { os_unfair_lock_lock(&_lock); uint32_t seq = _sequences[category]; os_unfair_lock_unlock(&_lock); return seq; } - (uint32_t)generateSessionSeed:(NSString *)sessionId { // 简单的hash算法,将sessionId转换为种子 uint32_t hash = 5381; const char *str = [sessionId UTF8String]; while (*str) { hash = ((hash << 5) + hash) + *str++; } return hash & TJPSEQUENCE_BODY_MASK; } // 检查序列号是否在安全范围内 - (BOOL)isSequenceInSafeRange:(TJPMessageCategory)category { os_unfair_lock_lock(&_lock); uint32_t seq = _sequences[category]; os_unfair_lock_unlock(&_lock); return seq < TJPSEQUENCE_WARNING_THRESHOLD; } - (NSDictionary *)getStatistics { os_unfair_lock_lock(&_lock); NSMutableDictionary *stats = [NSMutableDictionary dictionary]; stats[@"sessionId"] = _sessionId; stats[@"sessionSeed"] = @(_sessionSeed); for (int i = 0; i < 4; i++) { NSString *categoryKey = [NSString stringWithFormat:@"category_%d", i]; stats[categoryKey] = @{ @"current": @(_sequences[i]), @"total_generated": @(_totalGenerated[i]), @"last_reset": _lastResetTime[i], @"utilization": @((double)_sequences[i] / TJPSEQUENCE_MAX_VALUE * 100), @"safe": @(_sequences[i] < TJPSEQUENCE_WARNING_THRESHOLD) }; } os_unfair_lock_unlock(&_lock); return [stats copy]; } // 新增:健康检查 - (BOOL)isHealthy { for (int i = 0; i < 4; i++) { if (![self isSequenceInSafeRange:i]) { return NO; } } return YES; } // 新增:预测下次重置时间 - (NSTimeInterval)estimateTimeToResetForCategory:(TJPMessageCategory)category averageQPS:(double)qps { if (category >= 4 || qps <= 0) return -1; os_unfair_lock_lock(&_lock); uint32_t current = _sequences[category]; os_unfair_lock_unlock(&_lock); uint32_t remaining = TJPSEQUENCE_RESET_THRESHOLD - current; return remaining / qps; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/TCP-Machine/TJPConnectStateMachine.h ================================================ // // TJPConnectStateMachine.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/24. // 连接状态机 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @interface TJPConnectStateMachine : NSObject /// 当前状态(只读) @property (nonatomic, readonly) TJPConnectState currentState; /// 初始化状态 @property (nonatomic, assign, readonly) BOOL isInitializing; /// 回调设置标记位 @property (nonatomic, assign, readonly) BOOL hasSetInvalidHandler; /// 初始化方法 - (instancetype)initWithInitialState:(TJPConnectState)initialState; /// 初始化方法 - 可选是否自动设置标准转换规则 - (instancetype)initWithInitialState:(TJPConnectState)initialState setupStandardRules:(BOOL)autoSetup; /// 设置标准转换规则 - (void)setupStandardTransitions; /// 转换规则 - (void)addTransitionFromState:(TJPConnectState)fromState toState:(TJPConnectState)toState forEvent:(TJPConnectEvent)event; /// 触发事件 - (void)sendEvent:(TJPConnectEvent)event; /// 状态变更回调 - (void)onStateChange:(void(^)(TJPConnectState oldState, TJPConnectState newState))handler; /// 设置无效转换处理器 - (void)setInvalidTransitionHandler:(void (^)(TJPConnectState currentState, TJPConnectEvent event))handler; /// 强制设置状态(仅在特殊情况下使用) - (void)forceState:(TJPConnectState)state; /// 验证事件在当前状态下是否有效 - (BOOL)canHandleEvent:(TJPConnectEvent)event; /// Log方法 - (void)logAllTransitions; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/TCP-Machine/TJPConnectStateMachine.m ================================================ // // TJPConnectStateMachine.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/24. // #import "TJPConnectStateMachine.h" #import "TJPNetworkDefine.h" TJPConnectState const TJPConnectStateDisconnected = @"Disconnected"; TJPConnectState const TJPConnectStateConnecting = @"Connecting"; TJPConnectState const TJPConnectStateConnected = @"Connected"; TJPConnectState const TJPConnectStateDisconnecting = @"Disconnecting"; TJPConnectEvent const TJPConnectEventConnect = @"Connect"; //连接事件 TJPConnectEvent const TJPConnectEventConnectSuccess = @"ConnectSuccess"; //连接成功事件 TJPConnectEvent const TJPConnectEventConnectFailure = @"ConnectFailure"; //连接错误事件 TJPConnectEvent const TJPConnectEventNetworkError = @"NetworkError"; //网络错误事件 TJPConnectEvent const TJPConnectEventDisconnect = @"Disconnect"; //断开连接事件 TJPConnectEvent const TJPConnectEventDisconnectComplete = @"DisconnectComplete"; //断开完成事件 TJPConnectEvent const TJPConnectEventForceDisconnect = @"ForceDisconnect"; //强制断开事件 TJPConnectEvent const TJPConnectEventReconnect = @"Reconnect"; //重新连接事件 @interface TJPConnectStateMachine () @property (nonatomic, assign, readwrite) BOOL isInitializing; @property (nonatomic, assign, readwrite) BOOL hasSetInvalidHandler; @property (nonatomic, readwrite) TJPConnectState currentState; @property (nonatomic, copy, nullable) void (^invalidTransitionHandler)(TJPConnectState, TJPConnectEvent); @end @implementation TJPConnectStateMachine { dispatch_queue_t _eventQueue; NSMutableDictionary *_transitions; NSMutableArray *_stateChangeHandlers; } #pragma mark - Initialization - (instancetype)initWithInitialState:(TJPConnectState)initialState { return [self initWithInitialState:initialState setupStandardRules:NO]; } - (instancetype)initWithInitialState:(TJPConnectState)initialState setupStandardRules:(BOOL)autoSetup { if (self = [super init]) { TJPLOG_INFO(@"[TJPConnectStateMachine] 开始初始化状态机,临时禁用指标收集"); // 初始化状态 _isInitializing = YES; _hasSetInvalidHandler = NO; // 初始化直接设置ivar _currentState = [initialState copy]; _transitions = [NSMutableDictionary dictionary]; _stateChangeHandlers = [NSMutableArray array]; _eventQueue = dispatch_queue_create("com.statemachine.queue", DISPATCH_QUEUE_SERIAL); if (autoSetup) { [self setupStandardTransitions]; } TJPLOG_INFO(@"[TJPConnectStateMachine] 基础初始化完成,准备启动指标收集"); // 异步启动指标收集 通过 setter 设置初始状态,触发 swizzled setCurrentState: __weak typeof(self) weakSelf = self; dispatch_async(_eventQueue, ^{ __strong typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) return; strongSelf.isInitializing = NO; // 通过 setter 重新设置状态,这次会被指标收集捕获 strongSelf.currentState = initialState; TJPLOG_INFO(@"[TJPConnectStateMachine] 状态机初始化完成,指标收集已启动"); }); } return self; } - (void)dealloc { TJPLogDealloc(); } #pragma mark - Public Methods - (void)addTransitionFromState:(TJPConnectState)fromState toState:(TJPConnectState)toState forEvent:(TJPConnectEvent)event { if (![self validateTransitionFromState:fromState toState:toState forEvent:event]) { return; } NSString *key = [NSString stringWithFormat:@"%@:%@", fromState, event]; _transitions[key] = toState; } - (void)sendEvent:(TJPConnectEvent)event { dispatch_async(_eventQueue, ^{ // 检查当前状态和事件是否有效 if (![self canHandleEvent:event]) { TJPLOG_ERROR(@"[TJPConnectStateMachine] 无效状态转换: %@ -> %@", self.currentState, event); // 调用无效转换处理器 if (self.invalidTransitionHandler) { self.invalidTransitionHandler(self.currentState, event); } return; } // 查找转换规则 NSString *key = [NSString stringWithFormat:@"%@:%@", self.currentState, event]; TJPConnectState newState = self->_transitions[key]; if (!newState) { TJPLOG_INFO(@"[TJPConnectStateMachine] 无效事件 当前状态:%@ -> 状态事件:%@", self.currentState, event); return; } // 如果新状态与当前状态相同,可以考虑跳过或仅记录日志 if ([newState isEqualToString:self.currentState]) { TJPLOG_INFO(@"[TJPConnectStateMachine] 状态保持不变: %@ (事件: %@)", self.currentState, event); return; } TJPConnectState oldState = self.currentState; self.currentState = newState; TJPLOG_INFO(@"[TJPConnectStateMachine] 状态转换: %@ -> %@ (事件: %@)", oldState, newState, event); // 状态变更回调 for (void(^handler)(TJPConnectState, TJPConnectState) in self->_stateChangeHandlers) { handler(oldState, newState); } }); } - (void)forceState:(TJPConnectState)state { dispatch_async(_eventQueue, ^{ TJPConnectState oldState = self.currentState; self.currentState = state; TJPLOG_INFO(@"[TJPConnectStateMachine] 状态强制切换: %@ -> %@", oldState, state); // 通知状态变更 for (void(^handler)(TJPConnectState, TJPConnectState) in self->_stateChangeHandlers) { handler(oldState, state); } }); } - (void)onStateChange:(void (^)(TJPConnectState, TJPConnectState))handler { dispatch_async(_eventQueue, ^{ [self->_stateChangeHandlers addObject:handler]; }); } - (void)setInvalidTransitionHandler:(void (^)(TJPConnectState, TJPConnectEvent))handler { TJPLOG_INFO(@"[TJPConnectStateMachine] 设置 InvalidTransitionHandler"); if (self.hasSetInvalidHandler && handler != nil) { TJPLOG_INFO(@"[TJPConnectStateMachine] handler已设置,跳过重复设置"); return; } dispatch_async(_eventQueue, ^{ self.invalidTransitionHandler = handler; self.hasSetInvalidHandler = (handler != nil); TJPLOG_INFO(@"[TJPConnectStateMachine] InvalidTransitionHandler 设置完成,状态: %@", self.hasSetInvalidHandler ? @"已设置" : @"已清除"); }); } - (BOOL)canHandleEvent:(TJPConnectEvent)event { NSString *key = [NSString stringWithFormat:@"%@:%@", _currentState, event]; return _transitions[key] != nil; } - (void)logAllTransitions { dispatch_sync(_eventQueue, ^{ TJPLOG_INFO(@"[TJPConnectStateMachine] 当前状态转换表:"); for (NSString *key in self->_transitions) { TJPLOG_INFO(@"%@ -> %@", key, self->_transitions[key]); } }); } #pragma mark - Private Methods - (BOOL)validateTransitionFromState:(TJPConnectState)fromState toState:(TJPConnectState)toState forEvent:(TJPConnectEvent)event { // 示例:禁止从 Connected 直接到 Connecting(原本应该是通过 Connect 事件触发) if ([fromState isEqualToString:TJPConnectStateConnected] && [toState isEqualToString:TJPConnectStateConnecting] && [event isEqualToString:TJPConnectEventConnect]) { TJPLOG_ERROR(@"[TJPConnectStateMachine] 禁止从 Connected 直接到 Connecting"); return NO; } return YES; } - (BOOL)isValidTransitionFrom:(TJPConnectState)fromState to:(TJPConnectState)toState forEvent:(TJPConnectEvent)event { NSString *key = [NSString stringWithFormat:@"%@:%@", fromState, event]; TJPConnectState expectedToState = _transitions[key]; return [expectedToState isEqualToString:toState]; } #pragma mark - Standard Transitions Setup - (void)setupStandardTransitions { // 增加强制断开规则:允许从任何状态直接进入 Disconnected [self addTransitionFromState:TJPConnectStateConnected toState:TJPConnectStateDisconnected forEvent:TJPConnectEventForceDisconnect]; [self addTransitionFromState:TJPConnectStateConnecting toState:TJPConnectStateDisconnected forEvent:TJPConnectEventForceDisconnect]; [self addTransitionFromState:TJPConnectStateDisconnecting toState:TJPConnectStateDisconnected forEvent:TJPConnectEventForceDisconnect]; [self addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateDisconnected forEvent:TJPConnectEventForceDisconnect]; // 状态保留规则 [self addTransitionFromState:TJPConnectStateConnecting toState:TJPConnectStateConnecting forEvent:TJPConnectEventConnect]; [self addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateDisconnected forEvent:TJPConnectEventDisconnectComplete]; // 网络错误 [self addTransitionFromState:TJPConnectStateConnecting toState:TJPConnectStateDisconnected forEvent:TJPConnectEventNetworkError]; [self addTransitionFromState:TJPConnectStateConnected toState:TJPConnectStateDisconnected forEvent:TJPConnectEventNetworkError]; // 基本状态流转规则 // 未连接->连接中 连接事件 [self addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateConnecting forEvent:TJPConnectEventConnect]; // 重连事件(新增) [self addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateConnecting forEvent:TJPConnectEventReconnect]; // 连接中->已连接 连接成功事件 [self addTransitionFromState:TJPConnectStateConnecting toState:TJPConnectStateConnected forEvent:TJPConnectEventConnectSuccess]; [self addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateConnected forEvent:TJPConnectEventConnectSuccess]; // 连接中->未连接 连接失败事件 [self addTransitionFromState:TJPConnectStateConnecting toState:TJPConnectStateDisconnected forEvent:TJPConnectEventConnectFailure]; [self addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateDisconnected forEvent:TJPConnectEventConnectFailure]; // 已连接->断开中 断开连接事件 [self addTransitionFromState:TJPConnectStateConnected toState:TJPConnectStateDisconnecting forEvent:TJPConnectEventDisconnect]; [self addTransitionFromState:TJPConnectStateConnecting toState:TJPConnectStateDisconnecting forEvent:TJPConnectEventDisconnect]; // 断开中->未连接 断开完成事件 [self addTransitionFromState:TJPConnectStateDisconnecting toState:TJPConnectStateDisconnected forEvent:TJPConnectEventDisconnectComplete]; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/TCP-Machine/TJPMessageStateMachine.h ================================================ // // TJPMessageStateMachine.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // 消息状态机 #import #import "TJPCoreTypes.h" @class TJPMessageContext; NS_ASSUME_NONNULL_BEGIN @interface TJPMessageStateMachine : NSObject /// 消息id @property (nonatomic, copy) NSString *messageId; /// 当前消息状态 @property (nonatomic, assign, readonly) TJPMessageState currentState; /// 状态回调 @property (nonatomic, copy) void(^stateChangeCallback)(TJPMessageContext *context, TJPMessageState oldState, TJPMessageState newState); /// 初始化方法 - (instancetype)initWithMessageId:(NSString *)messageId; - (instancetype)initWithMessageId:(NSString *)messageId initialState:(TJPMessageState)initialState; /** * 验证状态转换是否合法 */ - (BOOL)canTransitionFrom:(TJPMessageState)fromState to:(TJPMessageState)toState; - (void)transitionToState:(TJPMessageState)newState context:(TJPMessageContext *)context; - (NSString *)stateDisplayString; + (BOOL)isTerminalState:(TJPMessageState)state; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/TCP-Machine/TJPMessageStateMachine.m ================================================ // // TJPMessageStateMachine.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // #import "TJPMessageStateMachine.h" #import "TJPMessageContext.h" #import "TJPNetworkDefine.h" @interface TJPMessageStateMachine () @property (nonatomic, assign, readwrite) TJPMessageState currentState; @end @implementation TJPMessageStateMachine - (instancetype)initWithMessageId:(NSString *)messageId { if (self = [super init]) { _messageId = [messageId copy]; // 消息初试状态 _currentState = TJPMessageStateCreated; } return self; } - (instancetype)initWithMessageId:(NSString *)messageId initialState:(TJPMessageState)initialState { if (self = [super init]) { _messageId = [messageId copy]; _currentState = initialState; } return self; } - (BOOL)canTransitionFrom:(TJPMessageState)fromState to:(TJPMessageState)toState { // 防止自环转换 if (fromState == toState && fromState != TJPMessageStateRetrying) { return NO; } // 状态转换规则矩阵 static NSDictionary *> *transitionRules; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ transitionRules = @{ // 创建状态 -> 可转换为发送中、已取消 @(TJPMessageStateCreated): [NSSet setWithObjects:@(TJPMessageStateSending), @(TJPMessageStateCancelled), nil], // 发送中 -> 可转换为已发送、发送失败、已取消 @(TJPMessageStateSending): [NSSet setWithObjects:@(TJPMessageStateSent), @(TJPMessageStateFailed), @(TJPMessageStateCancelled), nil], // 已发送 -> 可转换为已送达、发送失败 @(TJPMessageStateSent): [NSSet setWithObjects:@(TJPMessageStateDelivered), @(TJPMessageStateFailed), nil], // 重试中 -> 可转换为发送中、发送失败、已取消 @(TJPMessageStateRetrying): [NSSet setWithObjects:@(TJPMessageStateSending), @(TJPMessageStateFailed), @(TJPMessageStateCancelled), nil], // 发送失败 -> 可转换为重试中、已取消 @(TJPMessageStateFailed): [NSSet setWithObjects:@(TJPMessageStateRetrying), @(TJPMessageStateCancelled), nil], // 已送达 -> 可转换为已读 @(TJPMessageStateDelivered): [NSSet setWithObjects:@(TJPMessageStateRead), nil], // 终态:已读、已取消 - 不能转换到其他状态 @(TJPMessageStateRead): [NSSet set], // 终态 @(TJPMessageStateCancelled): [NSSet set] // 终态 }; }); NSSet *allowedStates = transitionRules[@(fromState)]; return [allowedStates containsObject:@(toState)]; } - (void)transitionToState:(TJPMessageState)newState context:(TJPMessageContext *)context { if (![self canTransitionFrom:_currentState to:newState]) { TJPLOG_ERROR(@"[TJPMessageStateMachine] 无效状态转换: %@ -> %@", [self stateDisplayString], [self stateStringForState:newState]); return; } TJPMessageState oldState = _currentState; _currentState = newState; context.state = newState; // 更新时间戳 switch (newState) { case TJPMessageStateSending: context.sendTime = [NSDate date]; break; case TJPMessageStateSent: // 已发送状态不需要再更新发送时间 break; case TJPMessageStateDelivered: context.deliveredTime = [NSDate date]; break; case TJPMessageStateRead: context.readTime = [NSDate date]; break; case TJPMessageStateRetrying: context.lastRetryTime = [NSDate date]; context.retryCount++; // 增加重试次数 break; default: break; } // 触发回调 if (self.stateChangeCallback) { self.stateChangeCallback(context, oldState, newState); } TJPLOG_INFO(@"[TJPMessageStateMachine] 消息 %@ 状态转换: %@ -> %@", self.messageId, [self stateStringForState:oldState], [self stateDisplayString]); } - (NSString *)stateDisplayString { return [self stateStringForState:self.currentState]; } - (NSString *)stateStringForState:(TJPMessageState)state { switch (state) { case TJPMessageStateCreated: return @"已创建"; case TJPMessageStateSending: return @"发送中"; case TJPMessageStateSent: return @"已发送"; case TJPMessageStateDelivered: return @"已送达"; case TJPMessageStateRead: return @"已读"; case TJPMessageStateFailed: return @"发送失败"; case TJPMessageStateRetrying: return @"重试中"; case TJPMessageStateCancelled: return @"已取消"; default: return @"未知状态"; } } + (BOOL)isTerminalState:(TJPMessageState)state { return state == TJPMessageStateRead || state == TJPMessageStateCancelled || state == TJPMessageStateFailed; } + (NSArray *)possibleNextStatesFrom:(TJPMessageState)state { static NSDictionary *> *nextStates; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ nextStates = @{ @(TJPMessageStateCreated): @[@(TJPMessageStateSending), @(TJPMessageStateCancelled)], @(TJPMessageStateSending): @[@(TJPMessageStateSent), @(TJPMessageStateFailed), @(TJPMessageStateCancelled)], @(TJPMessageStateSent): @[@(TJPMessageStateDelivered), @(TJPMessageStateFailed)], @(TJPMessageStateDelivered): @[@(TJPMessageStateRead)], @(TJPMessageStateFailed): @[@(TJPMessageStateRetrying), @(TJPMessageStateCancelled)], @(TJPMessageStateRetrying): @[@(TJPMessageStateSending), @(TJPMessageStateFailed), @(TJPMessageStateCancelled)], @(TJPMessageStateRead): @[], @(TJPMessageStateCancelled): @[] }; }); return nextStates[@(state)] ?: @[]; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Utility/TJPCoreTypes.h ================================================ // // TJPCoreTypes.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #ifndef TJPCoreTypes_h #define TJPCoreTypes_h // 协议支持的特性定义 typedef enum { // 基本消息能力 (必须支持) TJP_FEATURE_BASIC = 0x0001, // 0000 0000 0000 0001 // 加密能力 TJP_FEATURE_ENCRYPTION = 0x0002, // 0000 0000 0000 0010 // 压缩能力 TJP_FEATURE_COMPRESSION = 0x0004, // 0000 0000 0000 0100 // 已读回执能力 TJP_FEATURE_READ_RECEIPT = 0x0008, // 0000 0000 0000 1000 // 群聊能力 TJP_FEATURE_GROUP_CHAT = 0x0010, // 0000 0000 0001 0000 } TJPFeatureFlag; // 当前客户端支持的特性组合 // 这里表示支持: 基本消息 + 加密 + 压缩 #define TJP_SUPPORTED_FEATURES (TJP_FEATURE_BASIC | TJP_FEATURE_ENCRYPTION | TJP_FEATURE_COMPRESSION) typedef enum { // 版本协商相关 TJP_TLV_TAG_VERSION_REQUEST = 0x0001, // 版本协商请求 TJP_TLV_TAG_VERSION_RESPONSE = 0x0002, // 版本协商响应 // 业务消息相关 TJP_TLV_TAG_READ_RECEIPT = 0x0010, // 已读回执 TJP_TLV_TAG_GROUP_MESSAGE = 0x0011, // 群聊消息 TJP_TLV_TAG_FILE_TRANSFER = 0x0012, // 文件传输 // 状态相关 TJP_TLV_TAG_USER_STATUS = 0x0020, // 用户状态 TJP_TLV_TAG_TYPING_INDICATOR = 0x0021, // 正在输入 } TJPTLVTag; typedef NS_ENUM(NSUInteger, TJPSessionType) { TJPSessionTypeDefault = 0, // 默认通用会话 TJPSessionTypeChat = 1, // 聊天会话 TJPSessionTypeMedia = 2, // 媒体传输会话 TJPSessionTypeSignaling = 3, // 信令会话 TJPSessionTypeFile = 4 // 文件传输 }; typedef NS_ENUM(NSUInteger, TJPTLVTagPolicy) { TJPTLVTagPolicyAllowDuplicates, //允许重复Tag TJPTLVTagPolicyRejectDuplicates //不允许重复Tag }; typedef NS_ENUM(NSUInteger, TJPTLVParseError) { TJPTLVParseErrorNone = 0, TJPTLVParseErrorIncompleteTag, // 数据不足以读取完整Tag(需至少2字节) TJPTLVParseErrorIncompleteLength, // 数据不足以读取完整Length(需至少4字节) TJPTLVParseErrorIncompleteValue, // Value长度不足(声明的Length超过剩余数据长度) TJPTLVParseErrorNestedTooDeep, // 嵌套层级超过maxNestedDepth限制 TJPTLVParseErrorDuplicateTag, // 发现重复Tag(当策略为TJPTLVTagPolicyRejectDuplicates时触发) TJPTLVParseErrorInvalidNestedTag // 非法嵌套Tag(未使用保留Tag进行嵌套) }; typedef NS_ENUM(uint8_t, TJPEncryptType) { TJPEncryptTypeNone = 0, // 不使用加密 TJPEncryptTypeCRC32, // CRC32校验 TJPEncryptTypeAES256, // AES256加密 }; typedef NS_ENUM(uint8_t, TJPCompressType) { TJPCompressTypeNone = 0, // 不使用压缩 TJPCompressTypeZlib, // Zlib方式压缩 }; // 内容类型标签枚举 typedef NS_ENUM(uint16_t, TJPContentType) { TJPContentTypeText = 0x1001, // 文本消息 TJPContentTypeImage = 0x1002, // 图片消息 TJPContentTypeAudio = 0x1003, // 音频消息 TJPContentTypeVideo = 0x1004, // 视频消息 TJPContentTypeFile = 0x1005, // 文件消息 TJPContentTypeLocation = 0x1006, // 位置消息 TJPContentTypeCustom = 0x1007, // 自定义消息 }; typedef NS_ENUM(uint16_t, TJPMessageType) { TJPMessageTypeNormalData = 0, // 普通数据消息 TJPMessageTypeHeartbeat = 1, // 心跳消息 TJPMessageTypeACK = 2, // 确认消息 TJPMessageTypeControl = 3, // 控制消息 TJPMessageTypeReadReceipt // 已读回执 }; typedef NS_ENUM(NSUInteger, TJPMessageState) { TJPMessageStateCreated = 0, // 已创建 TJPMessageStateSending, // 发送中 TJPMessageStateSent, // 已发送 TJPMessageStateDelivered, // 已送达 TJPMessageStateRead, // 已读 TJPMessageStateFailed, // 发送失败 TJPMessageStateRetrying, // 重试中 TJPMessageStateCancelled // 已取消 }; typedef NS_ENUM(NSUInteger, TJPMessagePriority) { TJPMessagePriorityLow = 0, TJPMessagePriorityNormal, TJPMessagePriorityHigh, TJPMessagePriorityUrgent }; typedef NS_ENUM(uint8_t, TJPMessageCategory) { TJPMessageCategoryNormal = 0, // 普通消息 TJPMessageCategoryHeartbeat = 1, // 心跳消息 TJPMessageCategoryControl = 2, // 控制消息 TJPMessageCategoryMedia = 3, // 媒体消息 TJPMessageCategoryBroadcast = 4 // 广播消息 }; typedef NS_ENUM(NSInteger, TJPDisconnectReason) { TJPDisconnectReasonNone, // 默认状态 TJPDisconnectReasonUserInitiated, // 手动断开连接 TJPDisconnectReasonNetworkError, // 网络错误导致断开 TJPDisconnectReasonHeartbeatTimeout, // 心跳超时导致断开 TJPDisconnectReasonIdleTimeout, // 空闲超时导致断开 TJPDisconnectReasonConnectionTimeout, // 连接超时导致断开 TJPDisconnectReasonSocketError, // 套接字错误导致断开 TJPDisconnectReasonAppBackgrounded, // APP进入后台误导致断开 TJPDisconnectReasonForceReconnect // 强制重连导致断开 }; typedef NS_ENUM(NSUInteger, TJPParseState) { TJPParseStateHeader = 1 << 0, // 解析协议头 TJPParseStateBody = 1 << 1, // 解析协议体 TJPParseStateError = 1 << 2 // 解析出错 }; typedef NS_ENUM(NSUInteger, TJPBufferStrategy) { TJPBufferStrategyAuto = 0, //默认自动选择 TJPBufferStrategyTradition, //传统NSMutableData缓冲区 TJPBufferStrategyRingBuffer //环形缓冲区 }; typedef NS_ENUM(NSUInteger, TJPNetworkQoS) { TJPNetworkQoSDefault = 1 << 0, TJPNetworkQoSBackground = 1 << 1, TJPNetworkQoSUserInitiated = 1 << 2 }; //网络指标收集级别 typedef NS_ENUM(NSInteger, TJPMetricsLevel) { TJPMetricsLevelNone = 0, // 禁用指标收集 TJPMetricsLevelBasic = 1, // 基本指标(连接状态、成功率) TJPMetricsLevelStandard = 2, // 标准指标(包括流量统计、心跳检测) TJPMetricsLevelDetailed = 3, // 详细指标(包括每个消息的RTT、重试统计) TJPMetricsLevelDebug = 4 // 调试级别(包括所有可能的指标和原始数据) }; //定义心跳模式 typedef NS_ENUM(NSUInteger, TJPHeartbeatMode) { TJPHeartbeatModeForeground, // 应用在前台 TJPHeartbeatModeBackground, // 应用在后台 TJPHeartbeatModeSuspended, // 心跳暂停 TJPHeartbeatModeLowPower // 低功耗模式 }; //运营商类型定义 typedef NS_ENUM(NSUInteger, TJPCarrierType) { TJPCarrierTypeUnknown, // 未知运营商 TJPCarrierTypeChinaMobile, // 中国移动 TJPCarrierTypeChinaUnicom, // 中国联通 TJPCarrierTypeChinaTelecom, // 中国电信 TJPCarrierTypeOther, // 其他运营商 }; //网络类型定义 typedef NS_ENUM(NSUInteger, TJPNetworkType) { TJPNetworkTypeUnknown, //未知网络 TJPNetworkTypeWiFi, //WIFI TJPNetworkType5G, //5G TJPNetworkType4G, //4G TJPNetworkType3G, //3G TJPNetworkType2G, //2G TJPNetworkTypeNone, //无网络 }; //网络健康状态 typedef NS_ENUM(NSUInteger, TJPNetworkHealthStatus) { TJPNetworkHealthStatusGood, // 健康心跳 TJPNetworkHealthStatusFair, // 一般心跳 TJPNetworkHealthStatusPoor, // 心跳较差 TJPNetworkHealthStatusCritical // 心跳严重问题 }; //应用状态类型 typedef NS_ENUM(NSUInteger, TJPAppState) { TJPAppStateActive, // 应用激活状态(前台运行) TJPAppStateInactive, // 应用非激活状态,挂起状态(如接到电话) TJPAppStateBackground, // 应用后台状态 TJPAppStateTerminated // 应用终止状态 }; //心跳策略类型 typedef NS_ENUM(NSUInteger, TJPHeartbeatStrategy) { TJPHeartbeatStrategyBalanced, // 平衡策略(默认策略) TJPHeartbeatStrategyAggressive, // 激进策略(较短间隔,适用于重要连接) TJPHeartbeatStrategyConservative, // 保守策略(较长间隔,省电模式) TJPHeartbeatStrategyCustom // 自定义策略 提供自定义接口 }; //心跳状态变更事件类型 typedef NS_ENUM(NSUInteger, TJPHeartbeatStateEvent) { TJPHeartbeatStateEventStarted, // 心跳启动 TJPHeartbeatStateEventStopped, // 心跳停止 TJPHeartbeatStateEventPaused, // 心跳暂停 TJPHeartbeatStateEventResumed, // 心跳恢复 TJPHeartbeatStateEventModeChanged, // 心跳模式变更 TJPHeartbeatStateEventIntervalChanged // 心跳间隔变更 }; //基于V2的协议头扩充完善 #pragma pack(push, 1) typedef struct { uint32_t magic; //魔数 0xDECAFBAD 4字节 uint8_t version_major; //协议主版本 1字节 uint8_t version_minor; //协议次版本 1字节 uint16_t msgType; //消息类型 2字节 uint32_t sequence; //序列号 4字节 uint32_t timestamp; //时间戳 (秒级,防重放攻击) 4字节 TJPEncryptType encrypt_type; //加密类型 1字节 TJPCompressType compress_type; //压缩类型 1字节 uint16_t session_id; //会话ID 2字节 uint32_t bodyLength; //Body长度(网络字节序) 4字节 uint32_t checksum; //CRC32 4字节 } TJPFinalAdavancedHeader; #pragma pack(pop) static const uint32_t kProtocolMagic = 0xDECAFBAD; static const uint8_t kProtocolVersionMajor = 1; static const uint8_t kProtocolVersionMinor = 0; //定义状态和事件 typedef NSString * TJPConnectState NS_STRING_ENUM; typedef NSString * TJPConnectEvent NS_STRING_ENUM; //状态 extern TJPConnectState const TJPConnectStateDisconnected; //未连接 extern TJPConnectState const TJPConnectStateConnecting; //正在连接 extern TJPConnectState const TJPConnectStateConnected; //已连接 extern TJPConnectState const TJPConnectStateDisconnecting; //正在断开 //事件 extern TJPConnectEvent const TJPConnectEventConnect; extern TJPConnectEvent const TJPConnectEventConnectSuccess; extern TJPConnectEvent const TJPConnectEventConnectFailure; extern TJPConnectEvent const TJPConnectEventNetworkError; extern TJPConnectEvent const TJPConnectEventDisconnect; extern TJPConnectEvent const TJPConnectEventDisconnectComplete; extern TJPConnectEvent const TJPConnectEventForceDisconnect; extern TJPConnectEvent const TJPConnectEventReconnect; #endif /* TJPCoreTypes_h */ ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Utility/TJPNetworkConfig.h ================================================ // // TJPNetworkConfig.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/23. // 配置类 #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @interface TJPNetworkConfig : NSObject /// 主机 @property (nonatomic, copy) NSString *host; /// 端口号 @property (nonatomic, assign) uint16_t port; /// 最大重试次数 默认5 @property (nonatomic, assign) NSUInteger maxRetry; /// 心跳时间 默认15秒 @property (nonatomic, assign) CGFloat heartbeat; /// 基础延迟默认 2秒 @property (nonatomic, assign) NSTimeInterval baseDelay; /// 连接超时 默认15秒 @property (nonatomic, assign) NSTimeInterval connectTimeout; /// 是否在服务器关闭连接后重连 @property (nonatomic, assign) BOOL shouldReconnectAfterServerClose; /// 是否在应用进入后台后重连 @property (nonatomic, assign) BOOL shouldReconnectAfterBackground; /// 是否使用TLS @property (nonatomic, assign) BOOL useTLS; /// 指标收集级别,默认为基本级别 @property (nonatomic, assign) TJPMetricsLevel metricsLevel; /// 指标上报间隔(秒),默认15秒 @property (nonatomic, assign) NSTimeInterval metricsReportInterval; /// 是否将指标打印到控制台,默认为YES @property (nonatomic, assign) BOOL metricsConsoleEnabled; /// 初始化方法 + (instancetype)configWithHost:(NSString *)host port:(uint16_t)port maxRetry:(NSUInteger)maxRetry heartbeat:(CGFloat)heartbeat; + (instancetype)defaultConfig; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Utility/TJPNetworkConfig.m ================================================ // // TJPNetworkConfig.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/23. // #import "TJPNetworkConfig.h" #import "TJPNetworkDefine.h" @implementation TJPNetworkConfig - (void)dealloc { TJPLogDealloc(); } + (instancetype)configWithHost:(NSString *)host port:(uint16_t)port maxRetry:(NSUInteger)maxRetry heartbeat:(CGFloat)heartbeat { return [[TJPNetworkConfig alloc] initWithHost:host port:port maxRetry:maxRetry heartbeat:heartbeat]; } + (instancetype)defaultConfig { return [[TJPNetworkConfig alloc] init]; } - (instancetype)init { return [self initWithHost:@"127.0.0.1" port:8080 maxRetry:5 heartbeat:15.0]; } - (instancetype)initWithHost:(NSString *)host port:(uint16_t)port maxRetry:(NSUInteger)maxRetry heartbeat:(CGFloat)heartbeat { if (self = [super init]) { _host = host; _port = port; _maxRetry = maxRetry; _heartbeat = heartbeat; _baseDelay = 2.0; _shouldReconnectAfterBackground = YES; _shouldReconnectAfterServerClose = NO; _useTLS = NO; _connectTimeout = 15.0; // 默认指标设置 #ifdef DEBUG _metricsLevel = TJPMetricsLevelStandard; _metricsConsoleEnabled = YES; #else _metricsLevel = TJPMetricsLevelBasic; _metricsConsoleEnabled = NO; #endif _metricsReportInterval = 15.0; } return self; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Utility/TJPNetworkUtil.h ================================================ // // TJPNetworkUtil.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/23. // #import #import "TJPCoreTypes.h" NS_ASSUME_NONNULL_BEGIN @interface TJPNetworkUtil : NSObject /// crc32校验 + (uint32_t)crc32ForData:(NSData *)data; /// 使用zlib 数据压缩 + (NSData *)compressData:(NSData *)data; /// 数据解压 + (NSData *)decompressData:(NSData *)data; + (NSString *)base64EncodeData:(NSData *)data; + (NSData *)base64DecodeString:(NSString *)string; /// 获取当前设备IP地址 + (NSString *)deviceIPAddress; + (BOOL)isValidIPAddress:(NSString *)ip; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TJPIMCore/Utility/TJPNetworkUtil.m ================================================ // // TJPNetworkUtil.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/23. // #import "TJPNetworkUtil.h" #import #import #import #import "TJPNetworkDefine.h" @implementation TJPNetworkUtil //+ (NSData *)buildPacketWithData:(NSData *)data type:(TJPMessageType)type sequence:(uint32_t)sequence { // // //初始化协议头 // TJPFinalAdavancedHeader header = {0}; // header.magic = htonl(kProtocolMagic); // header.version_major = kProtocolVersionMajor; // header.version_minor = kProtocolVersionMinor; // header.msgType = htons(type); // header.sequence = htonl(sequence); // header.bodyLength = htonl((uint32_t)data.length); // //crc32ForData需要转换为网络字节序 // header.checksum = htonl([self crc32ForData:data]); // // //构建完整包 // NSMutableData *packet = [NSMutableData dataWithBytes:&header length:sizeof(header)]; // [packet appendData:data]; // return packet; // //} + (uint32_t)crc32ForData:(NSData *)data { if (!data || data.length == 0) { return 0; // 对空数据的处理 } // 限制最大数据大小,防止DoS攻击 if (data.length > TJPMAX_BODY_SIZE) { TJPLOG_ERROR(@"数据大小超过限制: %lu", (unsigned long)data.length); return 0; } uLong crc = crc32(0L, Z_NULL, 0); crc = crc32(crc, [data bytes], (uInt)[data length]); #ifdef DEBUG NSLog(@"Calculated CRC32: %u", (uint32_t)crc); #endif return (uint32_t)crc; } // 未来升级成256加密 + (NSData *)hmacSHA256ForData:(NSData *)data withKey:(NSData *)key { return nil; } + (NSData *)compressData:(NSData *)data { if (data.length == 0) return data; //设置zlib压缩流属性 z_stream stream; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; //输入数据的长度 stream.avail_in = (uint)data.length; //输出数据的长度 stream.next_in = (Bytef *)data.bytes; //输出数据总长度 stream.total_out = 0; //关键步骤 初始化压缩流 if (deflateInit(&stream, Z_DEFAULT_COMPRESSION) != Z_OK) { return nil; } //分配16k的缓冲区用于存储压缩后的数据 NSMutableData *compressed = [NSMutableData dataWithLength:16384]; while (stream.avail_out == 0) { if (stream.total_out >= compressed.length) { //如果初始化16k不够则再增加16k [compressed increaseLengthBy:16384]; } //输出数据的位置 stream.next_out = compressed.mutableBytes + stream.total_out; //输出数据的剩余空间 stream.avail_out = (uint)(compressed.length - stream.total_out); //关键步骤 开始压缩 deflate(&stream, Z_FINISH); } //关键步骤 结束压缩并释放资源 deflateEnd(&stream); //压缩后的实际长度 compressed.length = stream.total_out; return compressed; } + (NSData *)decompressData:(NSData *)data { if (data.length == 0) return data; //设置解压流属性 z_stream stream; stream.zalloc = Z_NULL; stream.zfree = Z_NULL; stream.opaque = Z_NULL; stream.avail_in = (uint)data.length; stream.next_in = (Bytef *)data.bytes; stream.total_out = 0; //关键步骤 初始化解压流 if (inflateInit(&stream) != Z_OK) { return nil; } //初始化缓冲区空间 NSMutableData *decompressed = [NSMutableData dataWithLength:data.length * 2]; while (stream.avail_out == 0) { if (stream.total_out >= decompressed.length) { [decompressed increaseLengthBy:data.length]; } //输出数据的位置 stream.next_out = decompressed.mutableBytes + stream.total_out; //输出数据的剩余空间 stream.avail_out = (uint)(decompressed.length - stream.total_out); //关键步骤 执行解压操作 inflate(&stream, Z_FINISH); } //关键步骤 结束解压并释放相关资源 inflateEnd(&stream); //解压后的实际大小 decompressed.length = stream.total_out; return decompressed; } #pragma mark - 数据编码 + (NSString *)base64EncodeData:(NSData *)data { return [data base64EncodedStringWithOptions:0]; } + (NSData *)base64DecodeString:(NSString *)string { return [[NSData alloc] initWithBase64EncodedString:string options:0]; } #pragma mark - 网络工具 + (NSString *)deviceIPAddress { NSString *address = nil; struct ifaddrs *interfaces = NULL; if (getifaddrs(&interfaces) == 0) { struct ifaddrs *addr = interfaces; while (addr != NULL) { if (addr->ifa_addr->sa_family == AF_INET) { if ([[NSString stringWithUTF8String:addr->ifa_name] isEqualToString:@"en0"]) { address = [NSString stringWithUTF8String: inet_ntoa(((struct sockaddr_in *)addr->ifa_addr)->sin_addr)]; break; } } addr = addr->ifa_next; } freeifaddrs(interfaces); } return address; } + (BOOL)isValidIPAddress:(NSString *)ip { const char *utf8 = [ip UTF8String]; struct in_addr dst; return (inet_pton(AF_INET, utf8, &dst) == 1); } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TransportLayer/Tests/TJPMockFinalVersionTCPServer.h ================================================ // // TJPMockFinalVersionTCPServer.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/25. // #import #import NS_ASSUME_NONNULL_BEGIN @interface TJPMockFinalVersionTCPServer : NSObject @property (nonatomic, strong) GCDAsyncSocket *serverSocket; @property (nonatomic, strong) NSMutableArray *connectedSockets; @property (nonatomic, copy) void (^didReceiveDataHandler)(NSData *data, uint32_t seq); @property (nonatomic, assign) uint16_t port; - (void)startWithPort:(uint16_t)port; - (void)stop; - (void)sendACKForSequence:(uint32_t)seq toSocket:(GCDAsyncSocket *)socket; - (void)sendHeartbeatACKForSequence:(uint32_t)seq toSocket:(GCDAsyncSocket *)socket; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TransportLayer/Tests/TJPMockFinalVersionTCPServer.m ================================================ // // TJPMockFinalVersionTCPServer.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/25. // 模拟服务端统一使用网络字节序 大端 #import "TJPMockFinalVersionTCPServer.h" #import #import "TJPCoreTypes.h" #import "TJPNetworkUtil.h" #import "TJPSequenceManager.h" #import "TJPNetworkDefine.h" static const NSUInteger kHeaderLength = sizeof(TJPFinalAdavancedHeader); @interface TJPMockFinalVersionTCPServer () @property (nonatomic, strong) NSMutableData *receiveBuffer; @property (nonatomic, strong) TJPSequenceManager *sequenceManager; @end @implementation TJPMockFinalVersionTCPServer - (void)dealloc { NSLog(@"[MOCK SERVER] dealloc 被调用,MockServer 被销毁了!"); } - (instancetype)init { self = [super init]; if (self) { _connectedSockets = [NSMutableArray array]; _receiveBuffer = [NSMutableData data]; // 初始化服务器端序列号管理器 _sequenceManager = [[TJPSequenceManager alloc] initWithSessionId:@"mock_server_session"]; NSLog(@"[MOCK SERVER] 初始化完成,序列号管理器已创建"); } return self; } - (void)startWithPort:(uint16_t)port { self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; NSError *error = nil; if ([self.serverSocket acceptOnPort:port error:&error]) { self.port = port; NSLog(@"Mock server started on port %d", port); } else { NSLog(@"Failed to start mock server: %@", error); } } - (void)stop { [self.serverSocket disconnect]; [self.connectedSockets makeObjectsPerformSelector:@selector(disconnect)]; [self.connectedSockets removeAllObjects]; NSLog(@"Mock server stopped"); } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { NSLog(@"[MOCK SERVER] 接收到客户端连接"); [self.connectedSockets addObject:newSocket]; // 先读取协议头 [newSocket readDataWithTimeout:-1 tag:0]; } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { NSLog(@"[MOCK SERVER] 📥 接收到客户端发送的数据,大小: %lu字节", (unsigned long)data.length); // 解析协议头 if (data.length < kHeaderLength) { [sock readDataWithTimeout:-1 tag:0]; return; } TJPFinalAdavancedHeader header; [data getBytes:&header length:kHeaderLength]; // 验证Magic Number if (ntohl(header.magic) != kProtocolMagic) { NSLog(@"❌ Invalid magic number"); [sock disconnect]; return; } // 解析消息内容 uint32_t seq = ntohl(header.sequence); uint32_t bodyLength = ntohl(header.bodyLength); uint16_t msgType = ntohs(header.msgType); TJPEncryptType encryptType = header.encrypt_type; TJPCompressType compressType = header.compress_type; uint16_t sessionId = ntohs(header.session_id); uint32_t timestamp = ntohl(header.timestamp); NSLog(@"[MOCK SERVER] 📥 解析消息: 类型=%hu, 序列号=%u, 时间戳=%u, 会话ID=%hu, 加密类型=%d, 压缩类型=%d", msgType, seq, timestamp, sessionId, encryptType, compressType); // 读取完整消息体 NSData *payload = [data subdataWithRange:NSMakeRange(kHeaderLength, bodyLength)]; // 校验checksum uint32_t receivedChecksum = ntohl(header.checksum); // 转换为主机字节序 uint32_t calculatedChecksum = [TJPNetworkUtil crc32ForData:payload]; NSLog(@"[MOCK SERVER] 🔍 校验和检查: 接收=%u, 计算=%u", receivedChecksum, calculatedChecksum); if (receivedChecksum != calculatedChecksum) { NSLog(@"Checksum 不匹配, 期望: %u, 收到: %u", calculatedChecksum, receivedChecksum); [sock disconnect]; return; } // 根据消息类型验证序列号类别 [self validateReceivedMessage:msgType sequence:seq]; // 处理消息 switch (msgType) { case TJPMessageTypeNormalData: // 普通数据消息 { NSLog(@"[MOCK SERVER] 🔄 处理普通消息,序列号: %u", seq); if (self.didReceiveDataHandler) { self.didReceiveDataHandler(payload, seq); } // 发送传输层ACK [self sendACKForSequence:seq sessionId:sessionId toSocket:sock]; // 模拟接收端自动发送已读回执 [self simulateAutoReadReceiptForMessage:seq sessionId:sessionId toSocket:sock]; } break; case TJPMessageTypeHeartbeat: // 心跳消息 { NSLog(@"[MOCK SERVER] 💓 处理心跳消息,序列号: %u", seq); if (self.didReceiveDataHandler) { self.didReceiveDataHandler(payload, seq); } [self sendHeartbeatACKForSequence:seq sessionId:sessionId toSocket:sock]; } break; case TJPMessageTypeControl: // 控制消息 { NSLog(@"[MOCK SERVER] 🎛️ 处理控制消息,序列号: %u", seq); if (self.didReceiveDataHandler) { self.didReceiveDataHandler(payload, seq); } [self handleControlMessage:payload seq:seq sessionId:sessionId toSocket:sock]; //发送控制消息ACK [self sendControlACKForSequence:seq sessionId:sessionId toSocket:sock]; } break; case TJPMessageTypeReadReceipt: // 已读回执 { NSLog(@"[MOCK SERVER] 收到已读回执,序列号: %u", seq); if (payload.length >= 4) { uint32_t originalMsgSeq = 0; memcpy(&originalMsgSeq, payload.bytes, sizeof(uint32_t)); originalMsgSeq = ntohl(originalMsgSeq); NSLog(@"[MOCK SERVER] 消息序列号 %u 已被阅读", originalMsgSeq); // 模拟转发给其他客户端(实际项目中根据用户ID路由) [self forwardReadReceiptToOtherClients:payload fromSocket:sock]; } // 发送ACK确认 [self sendReadReceiptACK:seq sessionId:sessionId toSocket:sock]; } break; case TJPMessageTypeACK: // 🔧 添加这个 NSLog(@"[MOCK SERVER] 收到ACK确认,序列号: %u", seq); // ACK消息通常不需要特殊处理,只需要记录即可 break; default: NSLog(@"[MOCK SERVER] 收到未知消息类型 type: %d", msgType); break; } [sock readDataWithTimeout:-1 tag:0]; } - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag { NSLog(@"[MOCK SERVER] ✅ 数据发送完成,tag: %ld", tag); if (tag > 0) { NSLog(@"[MOCK SERVER] ✅ ACK包发送成功,序列号: %ld", tag); } } // 5. 添加错误处理 - (void)socket:(GCDAsyncSocket *)sock didWritePartialDataOfLength:(NSUInteger)partialLength tag:(long)tag { NSLog(@"[MOCK SERVER] 📤 部分数据发送: %lu字节, tag: %ld", (unsigned long)partialLength, tag); } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { [self.connectedSockets removeObject:sock]; } - (void)socket:(GCDAsyncSocket *)sock didReceiveError:(NSError *)error { NSLog(@"[MOCK SERVER] ❌ Socket错误: %@", error.localizedDescription); } #pragma mark - Response Methods - (void)sendACKForSequence:(uint32_t)seq sessionId:(uint16_t)sessionId toSocket:(GCDAsyncSocket *)socket { NSLog(@"[MOCK SERVER] 📤 准备发送普通消息ACK,序列号: %u", seq); // 使用与客户端相同的时间戳生成ACK响应 uint32_t currentTime = (uint32_t)[[NSDate date] timeIntervalSince1970]; TJPFinalAdavancedHeader header = {0}; header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(TJPMessageTypeACK); header.sequence = htonl(seq); header.timestamp = htonl(currentTime); // 使用当前时间戳 header.encrypt_type = TJPEncryptTypeNone; header.compress_type = TJPCompressTypeNone; header.session_id = htons(sessionId); // 保持与请求相同的会话ID header.bodyLength = 0; // ACK没有数据体 // ACK包没有数据体,checksum设为0 header.checksum = 0; NSData *ackData = [NSData dataWithBytes:&header length:sizeof(header)]; NSLog(@"[MOCK SERVER] 📤 即将发送普通消息ACK包,大小: %lu字节", (unsigned long)ackData.length); NSLog(@"[MOCK SERVER] 📤 ACK包字段:magic=0x%X, msgType=%hu, sequence=%u, timestamp=%u, sessionId=%hu", ntohl(header.magic), ntohs(header.msgType), ntohl(header.sequence), ntohl(header.timestamp), ntohs(header.session_id)); [socket writeData:ackData withTimeout:10.0 tag:0]; NSLog(@"[MOCK SERVER] ✅ 普通消息ACK包已提交发送,序列号: %u", seq); } - (void)sendControlACKForSequence:(uint32_t)seq sessionId:(uint16_t)sessionId toSocket:(GCDAsyncSocket *)socket { NSLog(@"[MOCK SERVER] 📤 准备发送控制消息ACK,序列号: %u", seq); // 使用当前时间戳 uint32_t currentTime = (uint32_t)[[NSDate date] timeIntervalSince1970]; TJPFinalAdavancedHeader reply = {0}; reply.magic = htonl(kProtocolMagic); reply.version_major = kProtocolVersionMajor; reply.version_minor = kProtocolVersionMinor; reply.msgType = htons(TJPMessageTypeACK); // 仍然使用ACK类型,但可以考虑使用TJPMessageTypeControl reply.sequence = htonl(seq); reply.timestamp = htonl(currentTime); reply.encrypt_type = TJPEncryptTypeNone; reply.compress_type = TJPCompressTypeNone; reply.session_id = htons(sessionId); reply.bodyLength = 0; // 没有数据体,checksum设为0 reply.checksum = 0; NSData *ackData = [NSData dataWithBytes:&reply length:sizeof(reply)]; NSLog(@"[MOCK SERVER] 📤 即将发送控制消息ACK包,大小: %lu字节", (unsigned long)ackData.length); NSLog(@"[MOCK SERVER] 📤 控制ACK包字段:magic=0x%X, msgType=%hu, sequence=%u, timestamp=%u, sessionId=%hu", ntohl(reply.magic), ntohs(reply.msgType), ntohl(reply.sequence), ntohl(reply.timestamp), ntohs(reply.session_id)); [socket writeData:ackData withTimeout:10.0 tag:0]; NSLog(@"[MOCK SERVER] ✅ 控制消息ACK包已提交发送,序列号: %u", seq); } - (void)handleControlMessage:(NSData *)payload seq:(uint32_t)seq sessionId:(uint16_t)sessionId toSocket:(GCDAsyncSocket *)socket { // 现有的版本协商逻辑 if (payload.length >= 12) { // TLV解析逻辑(保持不变) uint16_t tag; uint32_t length; uint16_t value; uint16_t flags; const void *bytes = payload.bytes; memcpy(&tag, bytes, sizeof(uint16_t)); memcpy(&length, bytes + 2, sizeof(uint32_t)); memcpy(&value, bytes + 6, sizeof(uint16_t)); memcpy(&flags, bytes + 8, sizeof(uint16_t)); tag = ntohs(tag); length = ntohl(length); value = ntohs(value); flags = ntohs(flags); NSLog(@"[MOCK SERVER] 版本协商:Tag=%u, Length=%u, Value=0x%04X, Flags=0x%04X", tag, length, value, flags); if (tag == TJP_TLV_TAG_VERSION_REQUEST) { uint8_t clientMajorVersion = (value >> 8) & 0xFF; uint8_t clientMinorVersion = value & 0xFF; NSLog(@"[MOCK SERVER] 客户端版本: %u.%u", clientMajorVersion, clientMinorVersion); NSLog(@"[MOCK SERVER] 客户端特性: %@", [self featureDescriptionWithFlags:flags]); [self sendVersionNegotiationResponseForSequence:seq sessionId:sessionId clientVersion:value supportedFeatures:flags toSocket:socket]; } } [self sendControlACKForSequence:seq sessionId:sessionId toSocket:socket]; } - (void)sendHeartbeatACKForSequence:(uint32_t)seq sessionId:(uint16_t)sessionId toSocket:(GCDAsyncSocket *)socket { NSLog(@"[MOCK SERVER] 收到心跳包,序列号: %u", seq); // 使用当前时间戳 uint32_t currentTime = (uint32_t)[[NSDate date] timeIntervalSince1970]; TJPFinalAdavancedHeader reply = {0}; reply.magic = htonl(kProtocolMagic); reply.version_major = kProtocolVersionMajor; reply.version_minor = kProtocolVersionMinor; reply.msgType = htons(TJPMessageTypeACK); reply.sequence = htonl(seq); reply.timestamp = htonl(currentTime); // 使用当前时间戳 reply.encrypt_type = TJPEncryptTypeNone; reply.compress_type = TJPCompressTypeNone; reply.session_id = htons(sessionId); // 保持与请求相同的会话ID reply.bodyLength = 0; // 心跳ACK没有数据体,checksum设为0 reply.checksum = 0; NSData *ackData = [NSData dataWithBytes:&reply length:sizeof(reply)]; NSLog(@"[MOCK SERVER] 心跳响应包字段:magic=0x%X, msgType=%hu, sequence=%u, timestamp=%u, sessionId=%hu", ntohl(reply.magic), ntohs(reply.msgType), ntohl(reply.sequence), ntohl(reply.timestamp), ntohs(reply.session_id)); [socket writeData:ackData withTimeout:-1 tag:0]; } - (void)sendVersionNegotiationResponseForSequence:(uint32_t)seq sessionId:(uint16_t)sessionId clientVersion:(uint16_t)clientVersion supportedFeatures:(uint16_t)features toSocket:(GCDAsyncSocket *)socket { NSLog(@"[MOCK SERVER] 收到控制消息,序列号: %u", seq); // 使用当前时间戳 uint32_t currentTime = (uint32_t)[[NSDate date] timeIntervalSince1970]; // 服务器选择的版本和功能 uint8_t serverMajorVersion = kProtocolVersionMajor; uint8_t serverMinorVersion = kProtocolVersionMinor; uint16_t serverVersion = (serverMajorVersion << 8) | serverMinorVersion; uint16_t agreedFeatures = features & 0x000F; // 仅支持客户端请求的部分功能 // 构建TLV数据 NSMutableData *tlvData = [NSMutableData data]; // 版本协商响应TLV uint16_t versionResponseTag = htons(TJP_TLV_TAG_VERSION_RESPONSE); // 版本协商响应标签 uint32_t versionResponseLength = htonl(4); uint16_t versionResponseValue = htons(serverVersion); uint16_t agreedFeaturesValue = htons(agreedFeatures); [tlvData appendBytes:&versionResponseTag length:sizeof(uint16_t)]; [tlvData appendBytes:&versionResponseLength length:sizeof(uint32_t)]; [tlvData appendBytes:&versionResponseValue length:sizeof(uint16_t)]; [tlvData appendBytes:&agreedFeaturesValue length:sizeof(uint16_t)]; // 计算校验和 uint32_t checksum = [TJPNetworkUtil crc32ForData:tlvData]; // 构建响应头 TJPFinalAdavancedHeader responseHeader = {0}; responseHeader.magic = htonl(kProtocolMagic); responseHeader.version_major = serverMajorVersion; responseHeader.version_minor = serverMinorVersion; responseHeader.msgType = htons(TJPMessageTypeControl); responseHeader.sequence = htonl(seq + 1); // 响应序列号+1 responseHeader.timestamp = htonl(currentTime); responseHeader.encrypt_type = TJPEncryptTypeNone; responseHeader.compress_type = TJPCompressTypeNone; responseHeader.session_id = htons(sessionId); responseHeader.bodyLength = htonl((uint32_t)tlvData.length); responseHeader.checksum = htonl(checksum); // 构建完整响应 NSMutableData *responseData = [NSMutableData dataWithBytes:&responseHeader length:sizeof(responseHeader)]; [responseData appendData:tlvData]; NSLog(@"[MOCK SERVER] 发送版本协商响应:服务器版本 %u.%u,协商功能 0x%04X", serverMajorVersion, serverMinorVersion, agreedFeatures); [socket writeData:responseData withTimeout:-1 tag:0]; } // 模拟自动已读回执 - (void)simulateAutoReadReceiptForMessage:(uint32_t)originalSequence sessionId:(uint16_t)sessionId toSocket:(GCDAsyncSocket *)socket { NSLog(@"[MOCK SERVER] 🤖 开始模拟自动已读回执,原消息序列号: %u", originalSequence); // 延迟2秒模拟用户阅读时间 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self sendReadReceiptToClientForMessage:originalSequence sessionId:sessionId toSocket:socket]; }); } // 向客户端发送已读回执 - 统一使用网络字节序 - (void)sendReadReceiptToClientForMessage:(uint32_t)originalSequence sessionId:(uint16_t)sessionId toSocket:(GCDAsyncSocket *)socket { NSLog(@"[MOCK SERVER] 📖 模拟发送已读回执,原消息序列号: %u", originalSequence); uint32_t currentTime = (uint32_t)[[NSDate date] timeIntervalSince1970]; uint32_t readReceiptSeq = [self.sequenceManager nextSequenceForCategory:TJPMessageCategoryNormal]; // 🔧 关键修复:使用TLV格式包装已读回执数据 NSMutableData *readReceiptData = [NSMutableData data]; // 构建TLV格式的已读回执 // Tag: 已读回执标签 uint16_t tag = htons(TJP_TLV_TAG_READ_RECEIPT); [readReceiptData appendBytes:&tag length:sizeof(uint16_t)]; // Length: 数据长度 (4字节序列号) uint32_t length = htonl(4); [readReceiptData appendBytes:&length length:sizeof(uint32_t)]; // Value: 原消息序列号 (网络字节序) uint32_t networkSequence = htonl(originalSequence); [readReceiptData appendBytes:&networkSequence length:sizeof(uint32_t)]; // 🔍 调试信息 - 验证网络字节序 NSLog(@"[MOCK SERVER] 🔍 统一网络字节序调试:"); NSLog(@"[MOCK SERVER] 🔍 原序列号(主机序): %u (0x%08X)", originalSequence, originalSequence); NSLog(@"[MOCK SERVER] 🔍 网络字节序: 0x%08X", networkSequence); // 以十六进制打印网络字节序数据 const unsigned char *bytes = readReceiptData.bytes; NSMutableString *hexString = [NSMutableString string]; for (NSUInteger i = 0; i < readReceiptData.length; i++) { [hexString appendFormat:@"%02X ", bytes[i]]; } // NSLog(@"[MOCK SERVER] 🔍 TLV十六进制: %@", hexString); // 🔧 关键:对网络字节序数据计算校验和 uint32_t checksum = [TJPNetworkUtil crc32ForData:readReceiptData]; NSLog(@"[MOCK SERVER] 🔍 网络字节序CRC32: %u (0x%08X)", checksum, checksum); // 构建包头 - 已读回执有自己独立的序列号 TJPFinalAdavancedHeader header = {0}; header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(TJPMessageTypeReadReceipt); header.sequence = htonl(readReceiptSeq); // 使用独立的序列号 header.timestamp = htonl(currentTime); header.encrypt_type = TJPEncryptTypeNone; header.compress_type = TJPCompressTypeNone; header.session_id = htons(sessionId); header.bodyLength = htonl((uint32_t)readReceiptData.length); header.checksum = checksum; // 🔧 关键修复:校验和不做字节序转换! // 🔍 调试包头信息 // NSLog(@"[MOCK SERVER] 🔍 包头调试信息:"); // NSLog(@"[MOCK SERVER] 🔍 magic: 0x%08X", ntohl(header.magic)); // NSLog(@"[MOCK SERVER] 🔍 msgType: %hu", ntohs(header.msgType)); // NSLog(@"[MOCK SERVER] 🔍 sequence: %u", ntohl(header.sequence)); // NSLog(@"[MOCK SERVER] 🔍 timestamp: %u", ntohl(header.timestamp)); // NSLog(@"[MOCK SERVER] 🔍 sessionId: %hu", ntohs(header.session_id)); // NSLog(@"[MOCK SERVER] 🔍 bodyLength: %u", ntohl(header.bodyLength)); // NSLog(@"[MOCK SERVER] 🔍 checksum(网络序): 0x%08X", ntohl(header.checksum)); // NSLog(@"[MOCK SERVER] 🔍 checksum(主机序): %u", checksum); // 构建完整的已读回执包 NSMutableData *readReceiptPacket = [NSMutableData dataWithBytes:&header length:sizeof(header)]; [readReceiptPacket appendData:readReceiptData]; // 发送数据 [socket writeData:readReceiptPacket withTimeout:-1 tag:0]; NSLog(@"[MOCK SERVER] ✅ 已读回执已发送(TLV格式),序列号: %u,确认原消息: %u", readReceiptSeq, originalSequence); } // 转发已读回执 - (void)forwardReadReceiptToOtherClients:(NSData *)readReceiptPayload fromSocket:(GCDAsyncSocket *)senderSocket { // 简单实现:转发给除发送者外的所有连接 for (GCDAsyncSocket *socket in self.connectedSockets) { if (socket != senderSocket) { [self sendReadReceiptToSocket:socket payload:readReceiptPayload]; } } } // 转发已读回执给客户端 - (void)sendReadReceiptToSocket:(GCDAsyncSocket *)socket payload:(NSData *)readReceiptPayload { uint32_t currentTime = (uint32_t)[[NSDate date] timeIntervalSince1970]; uint32_t forwardSeq = [self.sequenceManager nextSequenceForCategory:TJPMessageCategoryNormal]; NSLog(@"[MOCK SERVER] 📤 转发已读回执,序列号: %u", forwardSeq); // 🔧 注意:这里的 payload 应该已经是正确的网络字节序格式 // 因为它是从客户端接收到的,客户端期望的格式 // 计算校验和 uint32_t checksum = [TJPNetworkUtil crc32ForData:readReceiptPayload]; // 构建转发包 TJPFinalAdavancedHeader header = {0}; header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(TJPMessageTypeReadReceipt); header.sequence = htonl(forwardSeq); header.timestamp = htonl(currentTime); header.encrypt_type = TJPEncryptTypeNone; header.compress_type = TJPCompressTypeNone; header.session_id = htons(1234); // 简化处理 header.bodyLength = htonl((uint32_t)readReceiptPayload.length); header.checksum = htonl(checksum); NSMutableData *forwardPacket = [NSMutableData dataWithBytes:&header length:sizeof(header)]; [forwardPacket appendData:readReceiptPayload]; [socket writeData:forwardPacket withTimeout:-1 tag:0]; NSLog(@"[MOCK SERVER] 📤 已读回执已转发,序列号: %u", forwardSeq); } // 发送已读回执ACK - (void)sendReadReceiptACK:(uint32_t)seq sessionId:(uint16_t)sessionId toSocket:(GCDAsyncSocket *)socket { uint32_t currentTime = (uint32_t)[[NSDate date] timeIntervalSince1970]; TJPFinalAdavancedHeader header = {0}; header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(TJPMessageTypeACK); header.sequence = htonl(seq); header.timestamp = htonl(currentTime); header.encrypt_type = TJPEncryptTypeNone; header.compress_type = TJPCompressTypeNone; header.session_id = htons(sessionId); header.bodyLength = 0; header.checksum = 0; NSData *ackData = [NSData dataWithBytes:&header length:sizeof(header)]; [socket writeData:ackData withTimeout:10.0 tag:0]; NSLog(@"[MOCK SERVER] ✅ 已读回执ACK已发送,序列号: %u", seq); } - (NSString *)featureDescriptionWithFlags:(uint16_t)flags { NSMutableString *desc = [NSMutableString string]; if (flags & 0x0001) [desc appendString:@"基本消息 "]; if (flags & 0x0002) [desc appendString:@"加密 "]; if (flags & 0x0004) [desc appendString:@"压缩 "]; if (flags & 0x0008) [desc appendString:@"已读回执 "]; if (flags & 0x0010) [desc appendString:@"群聊 "]; return desc.length > 0 ? desc : @"无特性"; } - (void)logSequenceManagerStats { NSDictionary *stats = [self.sequenceManager getStatistics]; NSLog(@"[MOCK SERVER] 📊 序列号管理器统计:"); NSLog(@"[MOCK SERVER] 📊 会话ID: %@", stats[@"sessionId"]); NSLog(@"[MOCK SERVER] 📊 会话种子: %@", stats[@"sessionSeed"]); // 输出各类别统计 for (int i = 0; i < 4; i++) { NSString *categoryKey = [NSString stringWithFormat:@"category_%d", i]; NSDictionary *categoryStats = stats[categoryKey]; if (categoryStats) { NSString *categoryName = [self categoryNameForIndex:i]; NSLog(@"[MOCK SERVER] 📊 %@: 当前=%@, 总数=%@, 利用率=%.1f%%", categoryName, categoryStats[@"current"], categoryStats[@"total_generated"], [categoryStats[@"utilization"] doubleValue]); } } } // 类别名称映射 - (NSString *)categoryNameForIndex:(int)index { switch (index) { case TJPMessageCategoryNormal: return @"普通消息"; case TJPMessageCategoryControl: return @"控制消息"; case TJPMessageCategoryHeartbeat: return @"心跳消息"; case TJPMessageCategoryBroadcast: return @"广播消息"; default: return [NSString stringWithFormat:@"未知类别_%d", index]; } } - (void)validateSequenceNumber:(uint32_t)sequence expectedCategory:(TJPMessageCategory)expectedCategory { BOOL isCorrectCategory = [self.sequenceManager isSequenceForCategory:sequence category:expectedCategory]; // 提取类别和序列号 uint8_t category = (sequence >> TJPSEQUENCE_BODY_BITS) & TJPSEQUENCE_CATEGORY_MASK; uint32_t seqNumber = sequence & TJPSEQUENCE_BODY_MASK; NSLog(@"[MOCK SERVER] 🔍 序列号验证: %u", sequence); NSLog(@"[MOCK SERVER] 🔍 - 类别: %d (%@)", category, [self categoryNameForIndex:category]); NSLog(@"[MOCK SERVER] 🔍 - 序列号: %u", seqNumber); NSLog(@"[MOCK SERVER] 🔍 - 期望类别: %d (%@)", (int)expectedCategory, [self categoryNameForIndex:expectedCategory]); NSLog(@"[MOCK SERVER] 🔍 - 类别匹配: %@", isCorrectCategory ? @"✅" : @"❌"); } - (void)validateReceivedMessage:(uint16_t)msgType sequence:(uint32_t)sequence { TJPMessageCategory expectedCategory; switch (msgType) { case TJPMessageTypeNormalData: case TJPMessageTypeReadReceipt: expectedCategory = TJPMessageCategoryNormal; break; case TJPMessageTypeControl: expectedCategory = TJPMessageCategoryControl; break; case TJPMessageTypeHeartbeat: expectedCategory = TJPMessageCategoryHeartbeat; break; case TJPMessageTypeACK: // ACK消息的序列号类别取决于它确认的原消息类型 // 但由于我们无法从序列号直接确定原消息类型,可以跳过验证 NSLog(@"[MOCK SERVER] 🔍 ACK消息序列号验证跳过: %u", sequence); return; default: NSLog(@"[MOCK SERVER] ⚠️ 未知消息类型 %hu,跳过序列号验证", msgType); return; } BOOL isValid = [self.sequenceManager isSequenceForCategory:sequence category:expectedCategory]; if (!isValid) { NSLog(@"[MOCK SERVER] ⚠️ 序列号类别不匹配!消息类型: %hu, 序列号: %u, 期望类别: %d", msgType, sequence, (int)expectedCategory); } } //旧方法 已废弃目前单元测试在用 后续移除 - (void)sendHeartbeatACKForSequence:(uint32_t)seq toSocket:(nonnull GCDAsyncSocket *)socket { } - (void)sendACKForSequence:(uint32_t)seq toSocket:(nonnull GCDAsyncSocket *)socket { } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TransportLayer/Tests/TJPMockTCPServer.h ================================================ // // TJPMockTCPServer.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // 单元测试的mock TCP服务器 #import #import NS_ASSUME_NONNULL_BEGIN @interface TJPMockTCPServer : NSObject @property (nonatomic, strong, readonly) GCDAsyncSocket *serverSocket; @property (nonatomic, strong, readonly) GCDAsyncSocket *connectedClient; @property (nonatomic, assign, readonly) uint16_t port; // 启动服务 - (BOOL)startOnPort:(uint16_t)port error:(NSError **)error; // 停止服务 - (void)stop; // 发送模拟数据(完整数据包) - (void)sendPacket:(NSData *)packet; // 用字符串构建完整数据包(Header + Payload) - (NSData *)buildPacketWithMessage:(NSString *)message; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/TransportLayer/Tests/TJPMockTCPServer.m ================================================ // // TJPMockTCPServer.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #import "TJPMockTCPServer.h" #import #import "TJPNetworkProtocol.h" @interface TJPMockTCPServer () @property (nonatomic, strong) GCDAsyncSocket *serverSocket; @property (nonatomic, strong) GCDAsyncSocket *connectedClient; @end @implementation TJPMockTCPServer - (void)dealloc { NSLog(@"[MOCK SERVER] dealloc 被调用,MockServer 被销毁了!"); } - (BOOL)startOnPort:(uint16_t)port error:(NSError **)error { self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; BOOL success = [self.serverSocket acceptOnPort:port error:error]; if (success) { _port = port; } return success; } - (void)stop { [self.serverSocket disconnect]; [self.connectedClient disconnect]; self.serverSocket = nil; self.connectedClient = nil; } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { NSLog(@"[MOCK SERVER] 接收到客户端连接"); self.connectedClient = newSocket; [self.connectedClient setDelegate:self delegateQueue:dispatch_get_main_queue()]; // 一步设置 delegate 和 queue [self.connectedClient readDataWithTimeout:-1 tag:0]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSData *packet = [self buildPacketWithMessage:@"hello world"]; [self.connectedClient writeData:packet withTimeout:-1 tag:0]; }); } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { NSLog(@"[MOCK SERVER] 收到客户端发来的数据长度: %lu", (unsigned long)data.length); const TJPAdavancedHeader *header = (const TJPAdavancedHeader *)data.bytes; if (ntohs(header->msgType) == TJPMessageTypeHeartbeat) { NSLog(@"[MOCK SERVER] 收到心跳包,序列号: %u", ntohl(header->sequence)); // 构造心跳响应头 TJPAdavancedHeader reply = {0}; reply.magic = htonl(kProtocolMagic); reply.msgType = htons(TJPMessageTypeHeartbeat); reply.sequence = header->sequence; // 保持原网络字节序 reply.bodyLength = htonl(0); // 计算校验和(将checksum字段置零后计算整个头部的CRC) reply.checksum = 0; // 临时置零 uLong crc = crc32(0L, Z_NULL, 0); crc = crc32(crc, (const Bytef *)&reply, sizeof(reply)); reply.checksum = htonl((uint32_t)crc); NSData *replyData = [NSData dataWithBytes:&reply length:sizeof(reply)]; NSLog(@"[MOCK SERVER] 心跳响应包字段:magic=0x%X, msgType=%hu, sequence=%u, checksum=%u", ntohl(reply.magic), ntohs(reply.msgType), ntohl(reply.sequence), ntohl(reply.checksum)); [sock writeData:replyData withTimeout:-1 tag:tag]; }else if (ntohs(header->msgType) == TJPMessageTypeNormalData) { NSLog(@"[MOCK SERVER] 收到业务包,序列号: %u", ntohl(header->sequence)); // 构造 ACK TJPAdavancedHeader ack = {0}; ack.magic = htonl(kProtocolMagic); ack.msgType = htons(TJPMessageTypeACK); ack.sequence = header->sequence; ack.bodyLength = htonl(0); ack.checksum = htonl(0); NSData *ackData = [NSData dataWithBytes:&ack length:sizeof(ack)]; [sock writeData:ackData withTimeout:-1 tag:tag]; } [sock readDataWithTimeout:-1 tag:tag + 1]; } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { NSLog(@"[MOCK SERVER] socketDidDisconnect 被调用,服务端断开连接,err = %@", err); } #pragma mark - Public - (void)sendPacket:(NSData *)packet { if (self.connectedClient.isConnected) { NSLog(@"[MOCK SERVER] 即将发送 packet,总长度: %lu", (unsigned long)packet.length); [self.connectedClient writeData:packet withTimeout:-1 tag:100]; } else { NSLog(@"[MOCK SERVER] 写入失败:客户端未连接"); } } - (void)socket:(GCDAsyncSocket *)sock didWriteDataWithTag:(long)tag { NSLog(@"[MOCK SERVER] 数据写入完成,tag: %ld", tag); } - (NSData *)buildPacketWithMessage:(NSString *)message { TJPAdavancedHeader header = {0}; NSData *payload = [message dataUsingEncoding:NSUTF8StringEncoding]; uint32_t payloadLength = (uint32_t)payload.length; // Header 构建 header.magic = htonl(kProtocolMagic); header.msgType = htons(TJPMessageTypeNormalData); header.sequence = htonl(1); header.bodyLength = htonl((uint32_t)payloadLength); uLong crc = crc32(0L, Z_NULL, 0); header.checksum = htonl((uint32_t)crc32(crc, payload.bytes, payloadLength)); // 拼接 header + payload NSMutableData *packet = [NSMutableData dataWithCapacity:sizeof(header) + payloadLength]; [packet appendBytes:&header length:sizeof(header)]; [packet appendData:payload]; NSLog(@"构造 packet 完成,总长度: %lu", (unsigned long)packet.length); return packet; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/V1_BasicFunction/TJPNetworkManagerV1.h ================================================ // // TJPNetworkManagerV1.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/19. // 1.0版本的网络核心是一个能用但存在并发问题的管理类 仅仅用于学习演示 // **并发问题** // - `pendingMessages` 是 `NSMutableDictionary`,多线程访问时可能会崩溃。 // - `_currentSequence` 不是线程安全的,多线程操作可能导致序列号重复或丢失。 // - `isConnected` 虽然是 `atomic`,但仍然在高并发情况下存在竞态条件。 // // **断线重连机制问题** // - `scheduleReconnect` 逻辑可能导致重复连接,特别是在 `reachableBlock` 回调中。 // // **数据解析问题** // - `parseBuffer` 在高并发情况下可能出现数据解析不完整的问题。 // - `isParsingHeader` 状态可能导致数据处理异常。 #import #import NS_ASSUME_NONNULL_BEGIN @interface TJPNetworkManagerV1 : NSObject { //当前序列号 // NSUInteger _currentSequence; } //声明成属性用于单元测试 @property (nonatomic, assign) NSUInteger currentSequence; //待确认消息 @property (nonatomic, strong) NSMutableDictionary *pendingMessages; @property (nonatomic, strong) GCDAsyncSocket *socket; @property (atomic, assign) BOOL isConnected; + (instancetype)shared; /// 连接方法 - (void)connectToHost:(NSString *)host port:(uint16_t)port; /// 发送消息 - (void)sendData:(NSData *)data; /// 重连策略 - (void)scheduleReconnect; /// 重置连接 - (void)resetConnection; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/V1_BasicFunction/TJPNetworkManagerV1.m ================================================ // // TJPNetworkManagerV1.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/19. // #import "TJPNetworkManagerV1.h" #import #import #import #import "TJPNetworkProtocol.h" #import "TJPNetworkDefine.h" #import "TJPNETError.h" #import "TJPNETErrorHandler.h" static const NSInteger kMaxReconnectAttempts = 5; //一般应用来说 30秒的最大延迟时间基本够用 static const NSTimeInterval kMaxReconnectDelay = 30; @interface TJPNetworkManagerV1 () { //网络状态 Reachability *_networkReachability; //网络队列 dispatch_queue_t _networkQueue; //处理队列 dispatch_queue_t _parseQueue; //重试次数 NSInteger _reconnectAttempt; //当前协议头 TJPAdavancedHeader _currentHeader; } @property (nonatomic, copy) NSString *host; @property (nonatomic, assign) uint16_t port; //缓冲区 @property (nonatomic, strong) NSMutableData *parseBuffer; //标志位 @property (nonatomic, assign) BOOL isParsingHeader; //待确认心跳包 @property (nonatomic, strong) NSMutableDictionary *pendingHeartbeats; //心跳机制 @property (nonatomic, strong) dispatch_source_t heartbeatTimer; @property (nonatomic, strong) NSDate *lastHeartbeatTime; @end @implementation TJPNetworkManagerV1 #pragma mark - Instancetype + (instancetype)shared { static TJPNetworkManagerV1 *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[TJPNetworkManagerV1 alloc] init]; }); return instance; } - (void)dealloc { [self stopHeartbeat]; } - (instancetype)init { if (self = [super init]) { _currentSequence = 0; _reconnectAttempt = 0; _isParsingHeader = YES; _networkQueue = dispatch_queue_create("com.tjp.networkManager.netQueue", DISPATCH_QUEUE_SERIAL); _parseQueue = dispatch_queue_create("com.tjp.networkManager.parseQueue", DISPATCH_QUEUE_SERIAL); [self setupNetworkReachability]; } return self; } #pragma mark - Public Method - (void)connectToHost:(NSString *)host port:(uint16_t)port { dispatch_async(self->_networkQueue, ^{ self.host = host; self.port = port; [self disconnect]; self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self->_networkQueue]; NSError *error = nil; if (![self.socket connectToHost:host onPort:port error:&error]) { [self handleError:error]; } }); } - (void)sendData:(NSData *)data { dispatch_async(self->_networkQueue, ^{ if (!self.isConnected) return; NSData *packet = [self _buildPacketWithType:TJPMessageTypeNormalData data:data]; [self.socket writeData:packet withTimeout:-1 tag:self->_currentSequence]; //加入队列 self.pendingMessages[@(self->_currentSequence)] = data; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(15 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (self.pendingMessages[@(self->_currentSequence)]) { TJPLOG_WARN(@"消息 %lu 超时未确认", (unsigned long)self->_currentSequence); [self resendPacket:self->_currentSequence]; } }); }); } //断线重连策略 - (void)scheduleReconnect { if (_reconnectAttempt >= kMaxReconnectAttempts) { TJPLOG_ERROR(@"已达到最大重连次数,重连停止"); [self postNotification:kNetworkFatalErrorNotification]; return; }; //延迟时间=指数退避策略+随机抖动优化 避免服务器惊群效应 NSInteger baseDelay = 2; NSTimeInterval delay = pow(baseDelay, _reconnectAttempt) + arc4random_uniform(3); delay = MIN(delay, kMaxReconnectDelay); TJPLOG_WARN(@"%ld秒后尝试第%ld次重连", (long)delay, _reconnectAttempt + 1); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), self->_networkQueue, ^{ if ([self->_networkReachability isReachable]) { [self connectToHost:self.host port:self.port]; } else { [self scheduleReconnect]; } }); _reconnectAttempt++; } - (void)resetConnection { //断开连接并重置 [self disconnect]; [self.pendingHeartbeats removeAllObjects]; [self.pendingMessages removeAllObjects]; //重新连接 [self scheduleReconnect]; } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { TJPLOG_INFO(@"已连接 %@:%d", host, port); self.isConnected = YES; _reconnectAttempt = 0; //开启TLS/SSL NSDictionary *settings = @{(id)kCFStreamSSLPeerName: host}; [sock startTLS:settings]; //开始心跳 [self startHeartbeat]; //开始接收数据 [sock readDataWithTimeout:-1 tag:0]; } //接收到数据时会触发 - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { [self.parseBuffer appendData:data]; while (YES) { if (self.isParsingHeader) { if (self.parseBuffer.length < sizeof(TJPAdavancedHeader)) break; TJPAdavancedHeader currentHeader = {0}; //解析头部 [self.parseBuffer getBytes:¤tHeader length:sizeof(TJPAdavancedHeader)]; //校验魔数 if (ntohl(currentHeader.magic) != kProtocolMagic) { //魔数校验失败 [self handleInvalidData]; [self resetParse]; return; } _currentHeader = currentHeader; self.isParsingHeader = NO; // 移除已处理的Header数据 [self.parseBuffer replaceBytesInRange:NSMakeRange(0, sizeof(TJPAdavancedHeader)) withBytes:NULL length:0]; } //读取消息体 uint32_t bodyLength = ntohl(_currentHeader.bodyLength); if (self.parseBuffer.length < bodyLength) break; //处理消息体 NSData *payload = [self.parseBuffer subdataWithRange:NSMakeRange(0, bodyLength)]; [self processMessage:_currentHeader payload:payload]; //移除已处理部分 [self.parseBuffer replaceBytesInRange:NSMakeRange(0, bodyLength) withBytes:NULL length:0]; self.isParsingHeader = YES; } //继续监听数据 [sock readDataWithTimeout:-1 tag:0]; } - (void)processMessage:(TJPAdavancedHeader)header payload:(NSData *)payload { switch (ntohs(header.msgType)) { case TJPMessageTypeNormalData: [self handleDataMessage:header payload:payload]; break; case TJPMessageTypeHeartbeat: [self handleHeartbeat]; break; case TJPMessageTypeACK: [self handleACK:ntohl(header.sequence)]; break; } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { self.isConnected = NO; //停止心跳 [self stopHeartbeat]; //准备重连 [self scheduleReconnect]; } #pragma mark - Private Method - (void)setupNetworkReachability { _networkReachability = [Reachability reachabilityForInternetConnection]; __weak typeof(self) weakSelf = self; _networkReachability.reachableBlock = ^(Reachability *reach) { if (!weakSelf.isConnected) { [weakSelf scheduleReconnect]; } }; [_networkReachability startNotifier]; } - (void)startHeartbeat { [self stopHeartbeat]; __weak typeof(self) weakSelf = self; self.heartbeatTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self->_networkQueue); //从当前时间开始延迟15秒后开始第一次执行 DISPATCH_TIME_NOW->15 * NSEC_PER_SEC //周期:每15秒执行一次 15 * NSEC_PER_SEC //最小时间间隔:1秒 1 * NSEC_PER_SEC dispatch_source_set_timer(self.heartbeatTimer, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC), 15 * NSEC_PER_SEC, 1 * NSEC_PER_SEC); dispatch_source_set_event_handler(self.heartbeatTimer, ^{ //发送心跳包 [weakSelf sendHeartbeat]; //更新上次心跳时间 weakSelf.lastHeartbeatTime = [NSDate date]; //超时心跳检查 if ([[NSDate date] timeIntervalSinceDate:weakSelf.lastHeartbeatTime] > 30) { TJPLOG_WARN(@"心跳超时,主动断开连接"); [weakSelf disconnectAndRetry]; } }); dispatch_resume(self.heartbeatTimer); } - (void)stopHeartbeat { if (self.heartbeatTimer) { dispatch_source_cancel(self.heartbeatTimer); self.heartbeatTimer = nil; } } - (void)sendHeartbeat { //心跳包双向检测机制 TJPAdavancedHeader header = {0}; header.magic = htonl(kProtocolMagic); header.msgType = htons(TJPMessageTypeHeartbeat); //携带序列号 header.sequence = htonl(++_currentSequence); NSData *packet = [NSData dataWithBytes:&header length:sizeof(header)]; [self.socket writeData:packet withTimeout:-1 tag:0]; // 记录发出的心跳包 self.pendingHeartbeats[@(_currentSequence)] = [NSDate date]; } - (void)handleDataMessage:(TJPAdavancedHeader)header payload:(NSData *)payload { //校验数据完整性 if ([self validateChecksum:header payload:payload]) { //分发业务层 [self dispatchMessage:header payload:payload]; //发送ack确认 [self sendACKForSequence:ntohl(header.sequence)]; }else { NSError *error = [TJPNETError errorWithCode:TJPNETErrorDataCorrupted userInfo:@{NSLocalizedDescriptionKey: @"数据校验失败"}]; [TJPNETErrorHandler handleError:error inManager:self]; } } - (void)dispatchMessage:(TJPAdavancedHeader)header payload:(NSData *)payload { uint16_t msgType = ntohs(header.msgType); switch (msgType) { case TJPMessageTypeNormalData: TJPLOG_INFO(@"收到正常消息"); //TODO 准备分发给业务层 break; case TJPMessageTypeHeartbeat: TJPLOG_INFO(@"收到心跳响应"); break; case TJPMessageTypeACK: TJPLOG_INFO(@"收到 ACK 确认, 序列号: %u", ntohl(header.sequence)); break; default: TJPLOG_WARN(@"收到未知类型消息: %u", msgType); break; } } //ack确认 - (void)sendACKForSequence:(uint32_t)sequence { dispatch_async(self->_networkQueue, ^{ TJPAdavancedHeader ackHeader = {0}; ackHeader.magic = htonl(kProtocolMagic); ackHeader.msgType = htons(TJPMessageTypeACK); ackHeader.sequence = htonl(sequence); NSData *packet = [NSData dataWithBytes:&ackHeader length:sizeof(ackHeader)]; if (self.isConnected) { [self.socket writeData:packet withTimeout:-1 tag:sequence]; } else { TJPLOG_WARN(@"连接断开,ACK 发送失败"); } }); } - (BOOL)validateChecksum:(TJPAdavancedHeader)header payload:(NSData *)payload { uint32_t receivedChecksum = ntohl(header.checksum); uint32_t calculatedChecksum = [self _crc32ForData:payload]; return receivedChecksum == calculatedChecksum; } - (void)handleHeartbeat { _lastHeartbeatTime = [NSDate date]; } - (void)handleACK:(uint32_t)sequence { dispatch_async(self->_networkQueue, ^{ [self.pendingMessages removeObjectForKey:@(sequence)]; TJPLOG_INFO(@"收到 %u 的ACK", sequence); }); } - (void)handleInvalidData { //魔数校验失败 NSError *error = [TJPNETError errorWithCode:TJPNETErrorInvalidProtocol userInfo:@{NSLocalizedDescriptionKey: @"魔数校验失败"}]; [TJPNETErrorHandler handleError:error inManager:self]; TJPLOG_ERROR(@"魔数校验失败,正在重置连接..."); //重置连接 [self resetConnection]; } - (void)resetParse { [self.parseBuffer setLength:0]; _currentHeader = (TJPAdavancedHeader){0}; } - (void)disconnectAndRetry { [self disconnect]; [self scheduleReconnect]; } - (void)resendPacket:(NSUInteger)sequence { NSData *data = self.pendingMessages[@(sequence)]; if (data) { TJPLOG_INFO(@"重发消息 %lu", sequence); NSData *packet = [self _buildPacketWithType:TJPMessageTypeNormalData data:data]; [self.socket writeData:packet withTimeout:-1 tag:sequence]; } } - (NSData *)_buildPacketWithType:(TJPMessageType)msgType data:(NSData *)msgData { //初始化协议头 TJPAdavancedHeader header; //清空内存 避免不必要错误 memset(&header, 0, sizeof(header)); //字段填充 header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(msgType); header.sequence = htonl(++_currentSequence); header.bodyLength = htonl((uint32_t)msgData.length); header.checksum = [self _crc32ForData:msgData]; NSMutableData *packet = [NSMutableData dataWithBytes:&header length:sizeof(header)]; [packet appendData:msgData]; return packet; } - (void)disconnect { [_socket disconnect]; _socket = nil; [self stopHeartbeat]; } - (uint32_t)_crc32ForData:(NSData *)data { uLong crc = crc32(0L, Z_NULL, 0); crc = crc32(crc, [data bytes], (uInt)[data length]); return (uint32_t)crc; } - (void)handleError:(NSError *)error { TJPLOG_ERROR(@"网络错误: %@", error); if ([error.domain isEqualToString:GCDAsyncSocketErrorDomain]) { [self scheduleReconnect]; } } - (void)postNotification:(NSString *)notification { dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:notification object:nil]; }); } #pragma mark - Lazy - (NSMutableDictionary *)pendingMessages { if (!_pendingMessages) { _pendingMessages = [NSMutableDictionary dictionary]; } return _pendingMessages; } - (NSMutableDictionary *)pendingHeartbeats { if (!_pendingHeartbeats) { _pendingHeartbeats = [NSMutableDictionary dictionary]; } return _pendingHeartbeats; } - (NSMutableData *)parseBuffer { if (!_parseBuffer) { _parseBuffer = [NSMutableData data]; } return _parseBuffer; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/V2_Concurrency/TJPConcurrentNetworkManager.h ================================================ // // TJPConcurrentNetworkManager.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // 2.0版本是解决了并发问题的单TCP长连接网络管理类 可用在小型项目/中型项目早期中 但单连接在现代中大型项目中会造成性能瓶颈 // **并发问题解决思路** // 使用GCD的串行队列管理数据收发与状态更新 没有使用加锁的核心原因: 简化并发控制逻辑,提升代码的执行效率与可维护性 // 关键修改代码见: // #pragma mark - Thread Safe Method // **方案选型对比** // 加锁:锁的粒度控制相对困难,更容易出现死锁或者串行过度化 多处加锁相对维护困难 // gcd串行队列: 行队列的任务切换开销更小,避免了死锁、锁竞争等复杂问题 // 结果:万级并发量依然稳定运行 // // v2.0具备:线程安全/工程级可复用/高并发下的稳定性 #import #import #import "TJPNetworkProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPConcurrentNetworkManager : NSObject { //当前序列号 // NSUInteger _currentSequence; } //声明成属性用于单元测试 @property (nonatomic, assign) NSUInteger currentSequence; //待确认消息 @property (nonatomic, strong) NSMutableDictionary *pendingMessages; //待确认心跳包 @property (nonatomic, strong) NSMutableDictionary *pendingHeartbeats; @property (nonatomic, strong) GCDAsyncSocket *socket; @property (atomic, assign) BOOL isConnected; + (instancetype)shared; /// 连接方法 - (void)connectToHost:(NSString *)host port:(uint16_t)port; /// 发送消息 - (void)sendData:(NSData *)data; /// 重连策略 - (void)scheduleReconnect; /// 重置连接 - (void)resetConnection; - (void)resetParse; //并发安全写入 - (void)addPendingMessage:(NSData *)data forSequence:(NSUInteger)sequence; //单元测试用的hook @property (nonatomic, copy) void (^onMessageParsed)(NSString *payloadStr); @property (nonatomic, copy) void (^onSocketWrite)(NSData *data, long tag); @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/V2_Concurrency/TJPConcurrentNetworkManager.m ================================================ // // TJPConcurrentNetworkManager.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/21. // #import "TJPConcurrentNetworkManager.h" #import #import #import #import "TJPNetworkDefine.h" #import "TJPNETError.h" #import "TJPNETErrorHandler.h" static const NSInteger kMaxReconnectAttempts = 5; //一般应用来说 30秒的最大延迟时间基本够用 static const NSTimeInterval kMaxReconnectDelay = 30; @interface TJPConcurrentNetworkManager () { //网络状态 Reachability *_networkReachability; //网络队列 dispatch_queue_t _networkQueue; //处理队列 dispatch_queue_t _parseQueue; //重试次数 NSInteger _reconnectAttempt; //当前协议头 TJPAdavancedHeader _currentHeader; } @property (nonatomic, copy) NSString *host; @property (nonatomic, assign) uint16_t port; //缓冲区 @property (nonatomic, strong) NSMutableData *parseBuffer; //标志位 @property (nonatomic, assign) BOOL isParsingHeader; //心跳机制 @property (nonatomic, strong) dispatch_source_t heartbeatTimer; @property (nonatomic, strong) NSDate *lastHeartbeatTime; //单元测试时关闭TLS @property (nonatomic, assign) BOOL enableTLS; @end @implementation TJPConcurrentNetworkManager #pragma mark - Instancetype + (instancetype)shared { static TJPConcurrentNetworkManager *instance = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ instance = [[TJPConcurrentNetworkManager alloc] init]; }); return instance; } - (void)dealloc { [self stopHeartbeat]; } - (instancetype)init { if (self = [super init]) { _currentSequence = 0; _reconnectAttempt = 0; _isParsingHeader = YES; _networkQueue = dispatch_queue_create("com.tjp.networkManager.netQueue", DISPATCH_QUEUE_SERIAL); _parseQueue = dispatch_queue_create("com.tjp.networkManager.parseQueue", DISPATCH_QUEUE_SERIAL); [self setupNetworkReachability]; } return self; } #pragma mark - Public Method - (void)connectToHost:(NSString *)host port:(uint16_t)port { dispatch_async(self->_networkQueue, ^{ self.host = host; self.port = port; [self disconnect]; self.socket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:self->_networkQueue]; NSError *error = nil; if (![self.socket connectToHost:host onPort:port error:&error]) { [self handleError:error]; } }); } - (void)sendData:(NSData *)data { dispatch_async(self->_networkQueue, ^{ if (!self.isConnected) return; NSData *packet = [self _buildPacketWithType:TJPMessageTypeNormalData data:data]; [self.socket writeData:packet withTimeout:-1 tag:self->_currentSequence]; //加入队列 [self addPendingMessage:data forSequence:self->_currentSequence]; //检查超时 [self checkPendingMessageTimeoutForSequence:self->_currentSequence]; }); } //断线重连策略 - (void)scheduleReconnect { if (_reconnectAttempt >= kMaxReconnectAttempts) { TJPLOG_ERROR(@"已达到最大重连次数,重连停止"); [self postNotification:kNetworkFatalErrorNotification]; return; }; //延迟时间=指数退避策略+随机抖动优化 避免服务器惊群效应 NSInteger baseDelay = 2; NSTimeInterval delay = pow(baseDelay, _reconnectAttempt) + arc4random_uniform(3); delay = MIN(delay, kMaxReconnectDelay); TJPLOG_WARN(@"%ld秒后尝试第%ld次重连", (long)delay, _reconnectAttempt + 1); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), self->_networkQueue, ^{ if ([self->_networkReachability isReachable]) { [self connectToHost:self.host port:self.port]; } else { [self scheduleReconnect]; } }); _reconnectAttempt++; } - (void)resetConnection { //断开连接并重置 [self disconnect]; [self.pendingHeartbeats removeAllObjects]; [self.pendingMessages removeAllObjects]; //重新连接 [self scheduleReconnect]; } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { TJPLOG_INFO(@"已连接 %@:%d", host, port); self.isConnected = YES; _reconnectAttempt = 0; if (self.enableTLS) { //开启TLS/SSL NSDictionary *settings = @{(id)kCFStreamSSLPeerName: host}; [sock startTLS:settings]; } //开始心跳 [self startHeartbeat]; //开始接收数据 [sock readDataWithTimeout:-1 tag:0]; } //接收到数据时会触发 - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { dispatch_async(self->_networkQueue, ^{ //缓冲区填充内容 [self.parseBuffer appendData:data]; TJPLOG_MOCK(@"[客户端] 收到数据,parseBuffer 当前长度: %lu", (unsigned long)self.parseBuffer.length); [self tryParseNextMessageIfNeeded]; }); //继续监听数据 [sock readDataWithTimeout:-1 tag:0]; } - (void)tryParseNextMessageIfNeeded { while (true) { if (self->_isParsingHeader) { if (self.parseBuffer.length < sizeof(TJPAdavancedHeader)) return; TJPAdavancedHeader currentHeader = {0}; //解析头部 [self.parseBuffer getBytes:¤tHeader length:sizeof(TJPAdavancedHeader)]; //校验魔数 if (ntohl(currentHeader.magic) != kProtocolMagic) { //魔数校验失败 [self handleInvalidData]; [self resetParse]; return; } self->_currentHeader = currentHeader; // 移除已处理的Header数据 [self.parseBuffer replaceBytesInRange:NSMakeRange(0, sizeof(TJPAdavancedHeader)) withBytes:NULL length:0]; self->_isParsingHeader = NO; } //读取消息体 uint32_t bodyLength = ntohl(self->_currentHeader.bodyLength); if (self.parseBuffer.length < bodyLength) return; //处理消息体 NSData *payload = [self.parseBuffer subdataWithRange:NSMakeRange(0, bodyLength)]; [self.parseBuffer replaceBytesInRange:NSMakeRange(0, bodyLength) withBytes:NULL length:0]; //移除已处理部分 [self processMessage:self->_currentHeader payload:payload]; self->_isParsingHeader = YES; } } - (void)processMessage:(TJPAdavancedHeader)header payload:(NSData *)payload { TJPLOG_MOCK(@"[客户端] 成功触发 processMessage,payload 长度 = %lu 类型为:%i", payload.length, header.msgType); switch (ntohs(header.msgType)) { case TJPMessageTypeNormalData: [self handleDataMessage:header payload:payload]; break; case TJPMessageTypeHeartbeat: [self handleHeartbeat]; break; case TJPMessageTypeACK: [self handleACK:ntohl(header.sequence)]; break; } if (self.onMessageParsed) { NSString *str = [[NSString alloc] initWithData:payload encoding:NSUTF8StringEncoding]; self.onMessageParsed(str); } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { TJPLOG_ERROR(@"[客户端] socketDidDisconnect 被调用,error = %@", err); // TJPLOG_WARN(@"[DEBUG] 断开原因堆栈: %@", [NSThread callStackSymbols]); self.isConnected = NO; //停止心跳 [self stopHeartbeat]; //准备重连 [self scheduleReconnect]; } #pragma mark - Private Method - (void)setupNetworkReachability { _networkReachability = [Reachability reachabilityForInternetConnection]; __weak typeof(self) weakSelf = self; _networkReachability.reachableBlock = ^(Reachability *reach) { if (!weakSelf.isConnected) { [weakSelf scheduleReconnect]; } }; [_networkReachability startNotifier]; } - (void)startHeartbeat { [self stopHeartbeat]; __weak typeof(self) weakSelf = self; self.heartbeatTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self->_networkQueue); //从当前时间开始延迟15秒后开始第一次执行 DISPATCH_TIME_NOW->15 * NSEC_PER_SEC //周期:每15秒执行一次 15 * NSEC_PER_SEC //最小时间间隔:1秒 1 * NSEC_PER_SEC dispatch_source_set_timer(self.heartbeatTimer, dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC), 15 * NSEC_PER_SEC, 1 * NSEC_PER_SEC); dispatch_source_set_event_handler(self.heartbeatTimer, ^{ //发送心跳包 [weakSelf sendHeartbeat]; //更新上次心跳时间 weakSelf.lastHeartbeatTime = [NSDate date]; //超时心跳检查 if ([[NSDate date] timeIntervalSinceDate:self->_lastHeartbeatTime] > 30) { TJPLOG_WARN(@"心跳超时,主动断开连接"); [weakSelf disconnectAndRetry]; } }); dispatch_resume(self.heartbeatTimer); } - (void)stopHeartbeat { if (self.heartbeatTimer) { dispatch_source_cancel(self.heartbeatTimer); self.heartbeatTimer = nil; } } - (void)sendHeartbeat { //心跳包双向检测机制 TJPAdavancedHeader header = {0}; header.magic = htonl(kProtocolMagic); header.msgType = htons(TJPMessageTypeHeartbeat); //携带序列号 header.sequence = htonl(++_currentSequence); NSData *packet = [NSData dataWithBytes:&header length:sizeof(header)]; [self.socket writeData:packet withTimeout:-1 tag:0]; // 记录发出的心跳包 self.pendingHeartbeats[@(_currentSequence)] = [NSDate date]; // 触发测试 hook if (self.onSocketWrite) { self.onSocketWrite(packet, 0); } } - (void)handleDataMessage:(TJPAdavancedHeader)header payload:(NSData *)payload { //校验数据完整性 if ([self validateChecksum:header payload:payload]) { //分发业务层 [self dispatchMessage:header payload:payload]; //发送ack确认 [self sendACKForSequence:ntohl(header.sequence)]; }else { NSError *error = [TJPNETError errorWithCode:TJPNETErrorDataCorrupted userInfo:@{NSLocalizedDescriptionKey: @"数据校验失败"}]; [TJPNETErrorHandler handleError:error inManager:self]; } } - (void)dispatchMessage:(TJPAdavancedHeader)header payload:(NSData *)payload { uint16_t msgType = ntohs(header.msgType); switch (msgType) { case TJPMessageTypeNormalData: TJPLOG_INFO(@"收到正常消息 消息为:%@", [[NSString alloc] initWithData:payload encoding:NSUTF8StringEncoding]); //TODO 准备分发给业务层 break; case TJPMessageTypeHeartbeat: TJPLOG_INFO(@"收到心跳响应"); break; case TJPMessageTypeACK: TJPLOG_INFO(@"收到 ACK 确认, 序列号: %u", ntohl(header.sequence)); break; default: TJPLOG_WARN(@"收到未知类型消息: %u", msgType); break; } } //ack确认 - (void)sendACKForSequence:(uint32_t)sequence { dispatch_async(self->_networkQueue, ^{ TJPAdavancedHeader ackHeader = {0}; ackHeader.magic = htonl(kProtocolMagic); ackHeader.msgType = htons(TJPMessageTypeACK); ackHeader.sequence = htonl(sequence); NSData *packet = [NSData dataWithBytes:&ackHeader length:sizeof(ackHeader)]; if (self.isConnected) { [self.socket writeData:packet withTimeout:-1 tag:sequence]; } else { TJPLOG_WARN(@"连接断开,ACK 发送失败"); } }); } - (BOOL)validateChecksum:(TJPAdavancedHeader)header payload:(NSData *)payload { uint32_t receivedChecksum = ntohl(header.checksum); uint32_t calculatedChecksum = [self _crc32ForData:payload]; return receivedChecksum == calculatedChecksum; } - (void)handleHeartbeat { TJPLOG_INFO(@"收到心跳响应,更新时间戳"); self.lastHeartbeatTime = [NSDate date]; } - (void)handleACK:(uint32_t)sequence { TJPLOG_INFO(@"收到 %u 的ACK", sequence); [self removePendingMessageForSequence:sequence]; } - (void)handleInvalidData { //魔数校验失败 NSError *error = [TJPNETError errorWithCode:TJPNETErrorInvalidProtocol userInfo:@{NSLocalizedDescriptionKey: @"魔数校验失败"}]; [TJPNETErrorHandler handleError:error inManager:self]; TJPLOG_ERROR(@"魔数校验失败,正在重置连接..."); //重置连接 [self resetConnection]; } - (void)resetParse { [self.parseBuffer setLength:0]; _currentHeader = (TJPAdavancedHeader){0}; } - (void)disconnectAndRetry { [self disconnect]; [self scheduleReconnect]; } - (void)resendPacket:(NSUInteger)sequence { NSData *data = self.pendingMessages[@(sequence)]; if (data) { TJPLOG_INFO(@"重发消息 %lu", sequence); NSData *packet = [self _buildPacketWithType:TJPMessageTypeNormalData data:data]; [self.socket writeData:packet withTimeout:-1 tag:sequence]; } } - (NSData *)_buildPacketWithType:(TJPMessageType)msgType data:(NSData *)msgData { //初始化协议头 TJPAdavancedHeader header; //清空内存 避免不必要错误 memset(&header, 0, sizeof(header)); //字段填充 header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(msgType); header.sequence = htonl(++_currentSequence); header.bodyLength = htonl((uint32_t)msgData.length); header.checksum = [self _crc32ForData:msgData]; NSMutableData *packet = [NSMutableData dataWithBytes:&header length:sizeof(header)]; [packet appendData:msgData]; return packet; } - (void)disconnect { [_socket disconnect]; _socket = nil; [self stopHeartbeat]; } - (uint32_t)_crc32ForData:(NSData *)data { uLong crc = crc32(0L, Z_NULL, 0); crc = crc32(crc, [data bytes], (uInt)[data length]); return (uint32_t)crc; } - (void)handleError:(NSError *)error { TJPLOG_ERROR(@"网络错误: %@", error); if ([error.domain isEqualToString:GCDAsyncSocketErrorDomain]) { [self scheduleReconnect]; } } - (void)postNotification:(NSString *)notification { dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:notification object:nil]; }); } #pragma mark - Thread Safe Method - (void)setIsParsingHeaderSafe:(BOOL)value { dispatch_async(self->_networkQueue, ^{ self->_isParsingHeader = value; }); } - (void)addPendingMessage:(NSData *)data forSequence:(NSUInteger)sequence { dispatch_async(self->_networkQueue, ^{ self.pendingMessages[@(sequence)] = data; }); } - (void)removePendingMessageForSequence:(NSUInteger)sequence { dispatch_async(self->_networkQueue, ^{ [self.pendingMessages removeObjectForKey:@(sequence)]; }); } - (void)checkPendingMessageTimeoutForSequence:(NSUInteger)sequence { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), self->_networkQueue, ^{ if (self.pendingMessages[@(sequence)]) { TJPLOG_WARN(@"消息 %lu 超时未确认", sequence); [self resendPacket:sequence]; } }); } - (void)clearParseBuffer { dispatch_async(self->_networkQueue, ^{ [self.parseBuffer setLength:0]; }); } - (void)readFromParseBufferWithLength:(NSUInteger)length completion:(void (^)(NSData *data))completion { dispatch_async(self->_networkQueue, ^{ if (self.parseBuffer.length < length) { if (completion) completion(nil); return; } NSData *data = [self.parseBuffer subdataWithRange:NSMakeRange(0, length)]; if (completion) completion(data); }); } - (void)replaceParseBufferBytesInRange:(NSRange)range { dispatch_async(self->_networkQueue, ^{ if (NSMaxRange(range) <= self.parseBuffer.length) { [self.parseBuffer replaceBytesInRange:range withBytes:NULL length:0]; } else { TJPLOG_WARN(@"替换超出 buffer 范围,已忽略操作"); } }); } - (void)getBytesFromParseBufferWithLength:(NSUInteger)length completion:(void (^)(const void *bytes))completion { dispatch_async(self->_networkQueue, ^{ if (self.parseBuffer.length < length) { if (completion) completion(NULL); return; } const void *bytes = [self.parseBuffer bytes]; if (completion) completion(bytes); }); } #pragma mark - Lazy - (NSMutableDictionary *)pendingMessages { if (!_pendingMessages) { _pendingMessages = [NSMutableDictionary dictionary]; } return _pendingMessages; } - (NSMutableDictionary *)pendingHeartbeats { if (!_pendingHeartbeats) { _pendingHeartbeats = [NSMutableDictionary dictionary]; } return _pendingHeartbeats; } - (NSMutableData *)parseBuffer { if (!_parseBuffer) { _parseBuffer = [NSMutableData data]; } return _parseBuffer; } @end ================================================ FILE: iOS-Network-Stack-Dive/CoreNetworkStack/V2_Concurrency/TJPNetworkProtocol.h ================================================ // // TJPNetworkProtocol.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/19. // 协议头接口 #import NS_ASSUME_NONNULL_BEGIN @protocol TJPNetworkProtocol typedef NS_ENUM(uint16_t, TJPMessageType) { TJPMessageTypeNormalData = 1, //普通数据消息 TJPMessageTypeHeartbeat = 2, //心跳消息 TJPMessageTypeACK = 3 //确认消息 }; @end #pragma pack(push, 1) typedef struct { uint32_t magic; //魔数 0xDECAFBAD 4字节 uint8_t version_major; //协议主版本(大端) 1字节 uint8_t version_minor; //协议次版本 1字节 uint16_t msgType; //消息类型 2字节 uint32_t sequence; //序列号 4字节 uint32_t bodyLength; //Body长度(网络字节序) 4字节 uint32_t checksum; //CRC32 4字节 } TJPAdavancedHeader; #pragma pack(pop) static const uint32_t kProtocolMagic = 0xDECAFBAD; /* 协议版本策略: 语义化版本控制 主版本变更:必须断开连接并升级 次版本变更:服务端需支持最近3个次版本 不定版本:客户端自动适配 平衡效率与可读性后,采用uint8_t分段.总占用2字节 */ static const uint8_t kProtocolVersionMajor = 1; static const uint8_t kProtocolVersionMinor = 0; NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Delegate/AppDelegate.h ================================================ // // AppDelegate.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/17. // #import @class HomeViewController; @interface AppDelegate : UIResponder @property (strong, nonatomic) UIWindow * window; @property (nonatomic, strong) HomeViewController *homeViewController; @end ================================================ FILE: iOS-Network-Stack-Dive/Delegate/AppDelegate.m ================================================ // // AppDelegate.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/17. // #import "AppDelegate.h" #import "TJPNavigationCoordinator.h" #import "HomeViewController.h" #import "TJPViewPushHandler.h" #import "TJPViewPresentHandler.h" #import "TJPMessageFactory.h" #import "TJPLogManager.h" @interface AppDelegate () @property (nonatomic, strong) TJPViewPushHandler *pushHandler; @property (nonatomic, strong) TJPViewPresentHandler *presentHandler; @end @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // 0.自动注册所有消息类型 必须 [TJPMessageFactory load]; // 控制台输出 [[TJPLogManager sharedManager] setDebugLoggingEnabled:YES]; [TJPLogManager sharedManager].minLogLevel = TJPLogLevelDebug; // 设置导航跳转处理容器 [self setupNavigationCoordinator]; [self setupWindow]; return YES; } - (void)setupNavigationCoordinator { TJPNavigationCoordinator *coordinator = [TJPNavigationCoordinator sharedInstance]; self.pushHandler = [TJPViewPushHandler new]; self.presentHandler = [TJPViewPresentHandler new]; // 注册标准处理器 [coordinator registerHandler:self.pushHandler forRouteType:TJPNavigationRouteTypeViewPush]; [coordinator registerHandler:self.presentHandler forRouteType:TJPNavigationRouteTypeViewPresent]; // // // 注册服务处理器 // [coordinator registerHandler:[TJPServiceHandler handlerWithServiceCenter:self.serviceCenter] // forRouteType:TJPNavigationRouteTypeServiceCall]; } - (void)setupWindow { self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; self.window.rootViewController = self.homeViewController; //[[UINavigationController alloc] initWithRootViewController:[[HomeViewController alloc] init]]; [self.window makeKeyAndVisible]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Docs/ArchitectureExtensions/AspectLoggerDesign.md ================================================ # 切面日志系统设计 ## 1. 背景与需求 切面日志(AOP日志),允许我们在方法执行的前后插入自定义逻辑(如日志记录、性能分析、异常捕捉等),而不需要修改方法本身的实现。 ### 主要需求: - 对**类方法、对象方法**执行的过程进行日志记录。 - 支持**无参、带参、不同返回值类型**的方法。 - 方法调用不应影响原有功能。 - 日志功能可配置,支持输出到控制台、文件或上传到服务器。 ## 2. 方案设计概述 通过 **方法交换(Swizzling)** 和 **动态方法调用** 的方式实现 AOP 日志系统。 ### 核心方案: 1. 使用 **Method Swizzling** 动态替换方法实现。 2. 使用 **NSInvocation** 捕获和传递方法参数。 核心思路:使用 **Method Swizzling** + **NSInvocation** 实现 AOP 切面,再去处理参数,返回值,替换新旧方法、 ### 遇到的难点: **hook任意方法时包含多种类型**:有无参数、基础类型/对象类型/结构体、有无返回值、不知道方法签名,运行时动态处理 ### 解决方式: 1.block + 可变参数 (...) + va_list的形式,可以解决部分场景,比如无参方法可以成功hook,但可变参数在ARC下是不安全的,会崩溃。**不推荐** 2.使用 **`libffi`** 来实现更灵活的函数调用与参数处理。 ### 原生 Objective-C 的局限 | 场景 | 能力 | 局限 | |--------|-------|---------| | Swizzling |基础支持 | 需要知道方法签名/IMP必须静态写死固定(block会不安全)| | NSInvocation | 调用/设置参数 | 不能自动调用IMP,会走 hook 后的代码(导致无限递归、死循环)| | va_list | 解析参数 | 使用block+...动态参数的方式在ARC下会出错(未定义行为) | | forwardInvocation: | 动态消息转发 | 只能用于没实现的方法,已有方法还得交换 | | C函数调用IMP | 性能快 | 参数类型必须写死,不能通用 | ### libffi的优势 | 能力 | 描述 | |--------|-------| | 动态生成调用桥 | 在运行时根据方法签名构建一个可以处理参数、返回值的桥 | | 支持任意参数类型 | 基础类型、枚举、id等都可以动态识别和传入 | | 安全调用原始IMP | 用 ffi_call 精准调用原始方法,防止无限递归 | | 支持任意类型、任意方法 | 不用固定参数类型,或者block+动态参数 | | 主流框架都在使用 | 成熟的 hook 解决方案 | ## 3. 切面日志系统设计 ### 3.1 基本架构 1. **注册日志**:通过调用 `TJPLogManager` 中的 `registerLogWithConfig:` 方法来注册日志切面,指定要增强的目标类、目标方法及日志触发点(前置、后置、异常时等)。 2. **方法替换(Swizzling)**:使用 `class_replaceMethod` 将目标方法的原始实现替换为带有日志功能的新实现。新实现会在方法执行前后触发日志记录,并调用原方法。 3. **日志处理**:在新实现中,我们使用 `NSInvocation` 来触发原方法,并通过 `va_list` 来传递可变参数。通过 `handler` 回调来触发日志记录。 4. **日志输出**:日志输出可通过回调函数定制,支持输出到控制台、保存到文件或上传到服务器等。 ### 3.2 核心代码流程 1. **注册日志切面** ```objc + (void)registerLogWithConfig:(TJPLogConfig)config trigger:(TJPLogTriggerPoint)trigger handler:(void (^)(TJPLogModel *log))handler { // 获取目标类和目标方法 Class cls = config.targetClass; SEL originSEL = config.targetSelector; // 获取原始方法实现 Method originMethod = class_getInstanceMethod(cls, originSEL); IMP originIMP = method_getImplementation(originMethod); // 创建新的实现方法(动态生成) IMP newIMP = imp_implementationWithBlock(^(id self, ...) { // 日志触发前 TJPLogModel *logModel = [TJPLogModel new]; logModel.clsName = NSStringFromClass(cls); logModel.methodName = NSStringFromSelector(originSEL); // 参数处理、日志触发 // ... handler(logModel); // 触发日志 // 调用原始方法 [self invokeOriginalIMP:originIMP withInvocation:invocation]; // 日志触发后 // ... }); // 替换方法实现 class_replaceMethod(cls, originSEL, newIMP, method_getTypeEncoding(originMethod)); } ``` ## 参数和返回值处理 由于可变参数和不同返回值类型的问题,我们通过 `NSInvocation` 来处理参数,并且使用 `libffi` 动态调用原方法,避免直接使用 `va_list`。 ```objc void *raw = va_arg(args, void *); id value = (__bridge id)(raw); ``` ## 4. 为什么要引用 `libffi` 来解决这些问题? ### 4.1 `libffi` 的优势 - **动态生成函数调用桥(trampoline)**: - `libffi` 可以根据方法签名动态生成调用桥,这样我们就不需要手动解析每个参数类型,可以支持更复杂的参数类型(如结构体、对象等),并避免栈溢出问题。 - **自动处理各种参数类型**: - `libffi` 可以通过 `ffi_type` 自动处理各种类型的参数(包括 `id`、`SEL`、`CGRect` 等),而不需要我们手动解析和转换。 - **支持任意方法签名**: - `libffi` 允许我们在运行时动态生成函数签名,并执行原始方法。这样就不需要知道方法签名,能够真正实现“通用方法 hook”。 - **高效、安全**: - 使用 `libffi` 可以绕开递归问题,确保在不改变原有方法逻辑的基础上实现日志记录等功能,避免了递归调用导致的死循环问题。 --- ### 4.2 使用 `libffi` 的方案 通过 `libffi`,我们可以在运行时根据目标方法的签名生成函数调用桥,然后调用原始 `IMP`。`ffi_call` 函数可以直接执行目标方法,避免了 `NSInvocation` 的递归调用问题。 **实现步骤**: 1. 使用 `NSMethodSignature` 获取方法签名。 2. 使用 `ffi_prep_cif` 来准备参数类型和方法签名。 3. 使用 `ffi_call` 执行原方法,并处理返回值。 ## 5.总结 使用 **libffi**,能够构建一个更为健壮、通用和高效的切面日志系统,同时也解决了原有的 **NSInvocation + va_list** 模式中的问题 ================================================ FILE: iOS-Network-Stack-Dive/Docs/CoreNetworkStackDoc/FinalNetworkTests.md ================================================ 各组件单元测试 TJPMessageContext contextWithData: 验证 sendTime 是否为当前时间。 验证 retryCount 是否初始化为 0。 验证 sequence 是否正确设置。 buildRetryPacket 验证重试次数是否递增。 验证返回的数据包是否正确构建。 TJPDynamicHeartbeat startMonitoringForSession: 验证定时器是否启动,心跳是否按预期发送。 测试心跳是否在正确的时间间隔内发送。 adjustIntervalWithNetworkCondition: 测试不同网络条件下,心跳频率是否调整正确。 heartbeatACKNowledgedForSequence: 验证 ACK 收到后,是否正确清除待确认心跳。 sendHeartbeat 验证心跳包是否正确构建并发送。 测试心跳超时后是否触发重试。 TJPNetworkCondition conditionWithMetrics: 测试根据 NSURLSessionTaskMetrics 创建的条件是否正确计算 RTT 和丢包率。 calculatePacketLoss 验证丢包率计算是否符合预期。 qualityLevel 测试不同 RTT 和丢包率下的网络质量评估。 isCongested 测试不同丢包率和 RTT 下,网络是否被判断为拥塞。 TJPMessageParser feedData: 测试数据是否正确添加到缓冲区。 hasCompletePacket 测试缓冲区是否能够正确判断是否有完整的数据包。 nextPacket 测试数据是否被正确解析成包,并触发正确的回调。 parseHeader 测试魔数校验和头部解析是否正确。 parseBody 测试消息体解析是否正确。 TJPReconnectPolicy initWithMaxAttempst:baseDelay:qos: 验证重试策略的初始化是否正确。 attemptConnectionWithBlock: 测试连接重试逻辑,验证延迟计算和重试次数。 calculateDelay 测试延迟计算是否符合预期的指数退避和随机延迟策略。 notifyReachMaxAttempts 测试最大重试次数到达时,是否正确触发通知。 TJPConnectStateMachine addTransitionFromState:toState:forEvent: 测试状态转换规则是否正确添加。 sendEvent: 测试事件触发后的状态转换。 onStateChange: 测试状态变化回调是否被正确触发。 无效状态转换 测试无效事件是否被正确处理并打印日志。 TJPNetworkUtil buildPacketWithData:type:sequence: 测试数据包是否正确构建,验证协议头、CRC 校验等。 crc32ForData: 测试 CRC32 校验值是否正确。 compressData: 和 decompressData: 测试数据压缩和解压是否正确,验证压缩后的数据与原始数据是否一致。 base64EncodeData: 和 base64DecodeString: 测试 Base64 编解码是否正确。 deviceIPAddress: 测试设备 IP 地址获取是否正常。 isValidIPAddress: 测试 IP 地址的合法性验证是否正确。 TJPSequenceManager nextSequence 测试序列号是否正确递增,确保循环逻辑正确。 resetSequence 测试序列号重置功能是否正常。 currentSequence 验证 currentSequence 是否返回最新的序列号。 线程安全 测试多线程环境下序列号生成的线程安全性。 TJPConcreteSession connectToHost:port: 测试连接过程,验证是否能正确触发状态变化。 sendData: 测试消息发送和重试机制是否正常工作。 disconnectWithReason: 测试会话断开连接的过程。 socket:didConnectToHost:port: 测试连接成功后的处理。 socket:didReadData:withTag: 测试数据接收和解析过程。 processReceivedPacket: 验证不同类型的数据包(普通数据、心跳、ACK)是否正确处理。 flushPendingMessages 测试未确认消息的重传。 handleError: 测试错误处理和断开连接的逻辑。 TJPNetworkCoordinator createSessionWithConfiguration: 测试会话的创建和添加到 sessionMap。 updateAllSessionsState: 验证所有会话状态的统一更新。 handleNetworkStateChange: 测试网络状态变化时,会话的处理逻辑。 triggerAutoReconnect 验证网络恢复时自动重连的功能。 session:didReceiveData: 测试接收到数据时,是否正确发布通知。 session:stateChanged: 验证会话状态变化时,是否正确移除断开连接的会话。 ================================================ FILE: iOS-Network-Stack-Dive/Docs/CoreNetworkStackDoc/HeartbeatKeepaliveDesign.md ================================================ # IM心跳保活机制设计与实现文档 ## 一、概述 本文档详细介绍基于`TJPSequenceManager`的IM应用心跳保活机制设计与实现,包括自适应心跳策略、运营商适配、网络变化感知和序列号管理等核心功能。 ### 1.1 需求背景 IM应用需要保持与服务器的长连接以确保消息实时送达。然而,不同的网络环境(特别是移动网络)存在NAT超时问题,需要通过定期发送心跳包来维持连接。同时,心跳频率需要平衡消息实时性与设备电量、流量消耗。 ### 1.2 主要挑战 1. **运营商NAT超时差异**:中国移动、联通、电信等运营商NAT超时时间从60秒到900秒不等 2. **网络环境多变**:WiFi、5G、4G、3G等不同网络环境下连接稳定性差异大 3. **设备资源限制**:需要在保证连接的同时最小化电量和流量消耗 4. **应用状态适应**:前台/后台状态下需要采用不同的心跳策略 5. **弱网与断网处理**:在网络不稳定或频繁切换时需要保持连接或快速恢复 ## 二、系统架构 ### 2.1 核心组件 ![系统架构图](architecture.png) 1. **TJPAdaptiveHeartbeatManager**:自适应心跳管理器,负责心跳策略的动态调整 2. **TJPConnectionManager**:连接管理器,负责连接状态管理和重连策略 3. **TJPCarrierDetector**:运营商探测器,负责识别当前网络类型和运营商 4. **TJPSequenceManager**:序列号管理器,负责生成和管理心跳序列号 5. **TJPHeartbeatHealthMonitor**:心跳健康监控,负责监测和分析心跳数据 ### 2.2 组件交互流程 ``` 用户 -> TJPConnectionManager -> TJPAdaptiveHeartbeatManager -> TJPSequenceManager -> 网络传输 -> 服务器 -> 心跳响应 -> TJPHeartbeatHealthMonitor -> 策略调整 ``` ## 三、关键功能实现 ### 3.1 自适应心跳策略 #### 3.1.1 心跳间隔计算公式 心跳间隔通过多因素综合动态计算: ```objc - (NSTimeInterval)calculateOptimalIntervalWithContext:(TJPHeartbeatContext *)context { // 基于运营商的基础间隔 NSTimeInterval baseInterval = [self baseIntervalForCarrier:context.carrierType]; // 网络类型调整因子 double networkFactor = [self factorForNetworkType:context.networkType]; // 应用状态调整因子 double appStateFactor = (context.appState == TJPHeartbeatModeForeground) ? 0.7 : 1.5; // 电池状态调整因子 double batteryFactor = [self factorForBatteryLevel:context.batteryLevel]; // 网络质量调整因子 double qualityFactor = [self factorForNetworkQuality:context.lastRTT packetLossRate:context.packetLossRate]; // 计算最终心跳间隔 NSTimeInterval adjustedInterval = baseInterval * networkFactor * appStateFactor * batteryFactor * qualityFactor; // 添加随机扰动 (±5%) double randomFactor = 0.95 + (arc4random_uniform(100) / 1000.0); // 0.95-1.05 adjustedInterval *= randomFactor; // 确保心跳间隔在合理范围内 return MIN(MAX(adjustedInterval, self.minHeartbeatInterval), self.maxHeartbeatInterval); } ``` #### 3.1.2 运营商特化配置 为不同运营商设置特定的心跳参数: ```objc - (NSTimeInterval)baseIntervalForCarrier:(TJPCarrierType)carrierType { switch (carrierType) { case TJPCarrierTypeChinaMobile: return 45.0; // 移动超时时间较短,基准值设置小一些 case TJPCarrierTypeChinaUnicom: return 90.0; // 联通超时时间适中 case TJPCarrierTypeChinaTelecom: return 120.0; // 电信超时时间较长 default: return 60.0; // 默认保守值 } } ``` #### 3.1.3 应用状态适应 前后台切换时的心跳调整: ```objc - (void)switchToMode:(TJPHeartbeatMode)mode { self.appState = mode; switch (mode) { case TJPHeartbeatModeForeground: self.minHeartbeatInterval = 15.0; self.maxHeartbeatInterval = 180.0; break; case TJPHeartbeatModeBackground: // 后台模式下减少心跳频率 self.minHeartbeatInterval = 60.0; self.maxHeartbeatInterval = 300.0; break; case TJPHeartbeatModeLowPower: // 低电量模式,最大限度减少心跳 self.minHeartbeatInterval = 120.0; self.maxHeartbeatInterval = 500.0; break; } // 重新计算心跳间隔 self.currentInterval = [self calculateOptimalIntervalWithContext:self.context]; [self _updateTimerInterval]; } ``` ### 3.2 序列号管理与溢出处理 #### 3.2.1 序列号结构 序列号采用32位整数,分为两部分: - 高8位:消息类别(如普通消息、心跳消息等) - 低24位:序列号体(最大值16,777,215) #### 3.2.2 溢出处理机制 ```objc - (uint32_t)nextSequenceForCategory:(TJPMessageCategory)category { os_unfair_lock_lock(&_lock); // 获取目标类别的当前序列号 uint32_t *categorySequence = &_sequences[category]; // 计算新序列号 *categorySequence = (*categorySequence + 1) & TJPSEQUENCE_BODY_MASK; // 检查是否接近最大值 if (*categorySequence > TJPSEQUENCE_WARNING_THRESHOLD) { // 接近最大值时发出警告 if (self.sequenceResetHandler) { dispatch_async(dispatch_get_main_queue(), ^{ self.sequenceResetHandler(category); }); } } // 如果达到最大值,自动重置 if (*categorySequence >= TJPSEQUENCE_MAX_VALUE) { *categorySequence = 0; // 更新重置信息 if (self.sequenceDidResetHandler) { dispatch_async(dispatch_get_main_queue(), ^{ self.sequenceDidResetHandler(category); }); } } // 通过与上掩码取出24位序列号 uint32_t nextSeq = ((uint32_t)category << 24) | *categorySequence; os_unfair_lock_unlock(&_lock); return nextSeq; } ``` ### 3.3 智能重连策略 #### 3.3.1 指数退避算法 ```objc - (NSTimeInterval)calculateReconnectDelay { switch (self.reconnectStrategy) { case TJPReconnectStrategyExponential: // 指数退避重连 NSTimeInterval delay = self.reconnectInterval * pow(1.5, MIN(_reconnectAttempts, 12)); // 添加随机扰动 ±15% double randomFactor = 0.85 + (arc4random_uniform(300) / 1000.0); // 0.85-1.15 delay *= randomFactor; return MIN(delay, self.maxReconnectInterval); case TJPReconnectStrategyAggressive: // 快速多次尝试,然后再延长间隔 if (_reconnectAttempts < 3) { return 0.5; } else if (_reconnectAttempts < 5) { return 1.0; } else { return MIN(self.maxReconnectInterval, self.reconnectInterval * pow(1.5, _reconnectAttempts - 5)); } case TJPReconnectStrategyFixedInterval: return self.reconnectInterval; case TJPReconnectStrategyImmediate: return 0; case TJPReconnectStrategyNone: default: return MAXFLOAT; // 不重连 } } ``` #### 3.3.2 网络感知重连 在检测到网络恢复时立即尝试重连: ```objc - (void)handleNetworkChange:(TJPNetworkType)networkType { TJPNetworkType oldNetworkType = self.currentNetworkType; // 更新网络类型 _currentNetworkType = networkType; // 检测到网络从无到有 if (oldNetworkType == TJPNetworkTypeNone && networkType != TJPNetworkTypeNone && ![self.currentState isEqualToString:@"connected"] && !self->_userInitiatedDisconnect) { NSLog(@"网络恢复,尝试重连"); [self connect]; } // 检测到网络从有到无 else if (oldNetworkType != TJPNetworkTypeNone && networkType == TJPNetworkTypeNone && [self.currentState isEqualToString:@"connected"]) { NSLog(@"网络丢失,断开连接"); [self transitionToState:@"disconnected" withReason:1]; // 网络错误原因 } } ``` ### 3.4 心跳健康监控 #### 3.4.1 心跳数据收集 ```objc - (void)recordHeartbeatResult:(BOOL)success rtt:(NSTimeInterval)rtt { // 创建心跳记录 TJPHeartbeatRecord *record = [[TJPHeartbeatRecord alloc] init]; record.timestamp = [NSDate date]; record.success = success; record.rtt = rtt; // 添加到记录 [self.recentHeartbeats addObject:record]; // 维护固定大小的记录窗口 if (self.recentHeartbeats.count > 50) { [self.recentHeartbeats removeObjectAtIndex:0]; } // 更新统计数据 self.totalHeartbeats++; if (success) { self.successfulHeartbeats++; } else { self.failedHeartbeats++; } // 重新计算健康指标 [self recalculateHealthMetrics]; // 异常检测 [self detectAnomalies]; } ``` #### 3.4.2 异常检测机制 ```objc - (void)detectAnomalies { // 检测严重的网络问题 if (self.packetLossRate > 0.3) { // 30%丢包率 [self.delegate heartbeatHealthMonitor:self didDetectAnomaly:1 // 高丢包异常 withSeverity:2]; // 高严重度 } // 检测RTT突然增加 if (self.averageRTT > 0 && self.recentHeartbeats.count >= 3) { TJPHeartbeatRecord *latest = self.recentHeartbeats.lastObject; if (latest.success && latest.rtt > self.averageRTT * 2.5) { [self.delegate heartbeatHealthMonitor:self didDetectAnomaly:2 // RTT异常 withSeverity:1]; // 中等严重度 } } // 检测连续失败 int consecutiveFailures = 0; for (NSInteger i = self.recentHeartbeats.count - 1; i >= 0; i--) { TJPHeartbeatRecord *record = self.recentHeartbeats[i]; if (!record.success) { consecutiveFailures++; } else { break; } } if (consecutiveFailures >= 3) { [self.delegate heartbeatHealthMonitor:self didDetectAnomaly:3 // 连续失败异常 withSeverity:3]; // 最高严重度 } } ``` ### 3.5 运营商识别与适配 #### 3.5.1 运营商探测 ```objc - (TJPCarrierType)detectCarrierType { CTCarrier *carrier = [_networkInfo subscriberCellularProvider]; if (!carrier.mobileNetworkCode) { return TJPCarrierTypeUnknown; } // 通过运营商名称识别 NSString *carrierName = carrier.carrierName; if ([carrierName containsString:@"移动"] || [carrierName containsString:@"CMCC"] || [carrierName containsString:@"China Mobile"]) { return TJPCarrierTypeChinaMobile; } else if ([carrierName containsString:@"联通"] || [carrierName containsString:@"Unicom"] || [carrierName containsString:@"China Unicom"]) { return TJPCarrierTypeChinaUnicom; } else if ([carrierName containsString:@"电信"] || [carrierName containsString:@"Telecom"] || [carrierName containsString:@"China Telecom"]) { return TJPCarrierTypeChinaTelecom; } // 通过MCC和MNC识别 NSString *mcc = carrier.mobileCountryCode; NSString *mnc = carrier.mobileNetworkCode; if ([mcc isEqualToString:@"460"]) { // 中国 if ([mnc isEqualToString:@"00"] || [mnc isEqualToString:@"02"] || [mnc isEqualToString:@"07"]) { return TJPCarrierTypeChinaMobile; } else if ([mnc isEqualToString:@"01"] || [mnc isEqualToString:@"06"]) { return TJPCarrierTypeChinaUnicom; } else if ([mnc isEqualToString:@"03"] || [mnc isEqualToString:@"05"] || [mnc isEqualToString:@"11"]) { return TJPCarrierTypeChinaTelecom; } } return TJPCarrierTypeOther; } ``` #### 3.5.2 运营商特定配置 ```objc + (instancetype)configForCarrierType:(TJPCarrierType)carrierType { TJPCarrierConfig *config = [[TJPCarrierConfig alloc] init]; switch (carrierType) { case TJPCarrierTypeChinaMobile: config.minHeartbeatInterval = 40.0; // 最小40秒 config.maxHeartbeatInterval = 120.0; // 最大2分钟 config.recommendedInterval = 60.0; // 推荐1分钟 config.natTimeout = 180.0; // NAT超时约3分钟 break; case TJPCarrierTypeChinaUnicom: config.minHeartbeatInterval = 60.0; // 最小60秒 config.maxHeartbeatInterval = 240.0; // 最大4分钟 config.recommendedInterval = 120.0; // 推荐2分钟 config.natTimeout = 300.0; // NAT超时约5分钟 break; case TJPCarrierTypeChinaTelecom: config.minHeartbeatInterval = 90.0; // 最小90秒 config.maxHeartbeatInterval = 300.0; // 最大5分钟 config.recommendedInterval = 180.0; // 推荐3分钟 config.natTimeout = 360.0; // NAT超时约6分钟 break; } return config; } ``` ## 四、性能优化 ### 4.1 电量优化 1. **动态心跳频率调整**:根据网络质量和电池状态动态调整心跳频率 2. **应用状态感知**:后台状态下降低心跳频率 3. **低电量模式**:电池电量低于15%时进入低功耗模式 ### 4.2 流量优化 1. **最小化心跳包大小**:心跳包只包含必要信息,减少数据传输 2. **WiFi/移动网络区分**:在WiFi网络下可以适当增加心跳间隔 ### 4.3 并发控制与多线程安全 1. **专用队列**:所有心跳操作在专用串行队列中进行 2. **锁机制**:序列号管理使用`os_unfair_lock`确保线程安全 3. **异步通知**:状态变更通过主线程异步通知,避免阻塞 ## 五、边界情况处理 ### 5.1 序列号溢出处理 序列号使用24位(最大值16,777,215),当接近最大值时: 1. **提前警告**:当序列号超过0xFFFFF0时触发警告 2. **自动重置**:达到最大值时自动重置为0 3. **通知系统**:通过回调通知应用层序列号重置事件 ### 5.2 极端网络环境适应 1. **可变超时策略**:根据历史RTT动态调整心跳超时时间 2. **指数退避重试**:心跳失败后使用指数退避算法重试 3. **降级策略**:在高丢包率环境下适当降低心跳频率 ### 5.3 频繁网络切换处理 1. **快速重连机制**:网络变化时立即尝试重新建立连接 2. **状态保持**:保留会话状态,实现无感知重连 3. **重连去抖**:防止在短时间内频繁重连 ## 六、实施效果与指标 ### 6.1 关键性能指标 1. **连接稳定性**:99.5%的连接保持率(非弱网环境) 2. **消息实时性**:95%的消息在3秒内送达 3. **资源消耗**: - 电量消耗降低40%(相比固定心跳间隔) - 流量消耗降低35%(相比固定心跳间隔) 4. **重连性能**:90%的重连在3秒内完成 ### 6.2 测试结果 | 测试场景 | 固定心跳 | 自适应心跳 | 改进率 | |---------|---------|-----------|-------| | WiFi环境连接稳定性 | 99.1% | 99.8% | +0.7% | | 4G弱网连接稳定性 | 85.3% | 94.7% | +9.4% | | 前台电量消耗(mAh/小时) | 42 | 28 | -33.3% | | 后台电量消耗(mAh/小时) | 18 | 8 | -55.6% | | 日均流量消耗(KB) | 580 | 320 | -44.8% | | 网络切换恢复时间(秒) | 5.2 | 1.8 | -65.4% | ## 七、面试准备 ### 7.1 常见面试问题与答案 #### Q1: 什么是心跳保活机制,为什么IM应用需要它? A: 心跳保活机制是IM应用维持长连接的关键技术,通过定期发送小数据包(心跳包)确保连接不会被中间设备(如NAT网关、运营商设备)关闭。IM应用需要它的原因: - 保持与服务器的长连接,确保消息实时接收 - 及时感知网络状态变化 - 防止NAT超时导致的连接断开 - 监控网络质量,优化通信策略 #### Q2: 不同运营商的NAT超时时间有何不同?如何适配? A: 主要运营商NAT超时时间差异较大: - 中国移动:通常60-180秒(相对较短) - 中国联通:通常300-600秒 - 中国电信:通常300-900秒 我们通过以下方式进行适配: 1. 运营商识别技术,自动探测当前网络运营商 2. 针对不同运营商设置不同的心跳基准间隔 3. 动态调整策略,根据实际网络情况优化心跳频率 4. 保守设计,设置安全边界值防止意外断连 #### Q3: 您如何优化前后台切换时的心跳策略? A: 应用在前后台状态下有不同的优化要求: 1. **前台状态**: - 用户正在活跃使用,需要更高的实时性 - 使用较短的心跳间隔(通常30-60秒) - 优先保证消息实时送达 2. **后台状态**: - 延长心跳间隔(通常是前台间隔的1.5-3倍) - 使用iOS后台任务API延长执行时间 - 结合推送通知作为备份通道 - 低电量时进一步降低频率 实现上,我们在`UIApplicationDidEnterBackgroundNotification`和`UIApplicationWillEnterForegroundNotification`通知中动态切换心跳模式。 #### Q4: 如何处理序列号耗尽的问题? A: 我们的序列号使用32位整数,其中高8位用于消息类别,低24位用于实际序列号值,最大可表示16,777,215个序列号。 处理序列号耗尽的策略包括: 1. **提前预警**:当序列号接近最大值(0xFFFFF0)时,触发预警机制通知应用层 2. **自动重置**:达到最大值时自动重置为0,继续使用 3. **重置通知**:通过回调函数通知应用层序列号已重置 4. **会话ID关联**:每个连接会话有唯一ID,与序列号组合确保全局唯一性 5. **统计监控**:记录序列号使用情况,供调试和优化 #### Q5: 在复杂网络环境下如何保证心跳的可靠性? A: 我们采用多层次策略保证复杂网络环境下的心跳可靠性: 1. **自适应心跳间隔**:根据网络RTT和丢包率动态调整心跳频率 2. **动态超时计算**:超时时间为平均RTT的3倍,确保不会过早判定心跳失败 3. **指数退避重试**:心跳失败后,使用指数退避算法进行重试 4. **心跳健康监控**:持续监控心跳成功率和RTT波动,及时发现异常 5. **降级策略**:在弱网环境下适当降低心跳频率,保持最低连接需求 6. **备份通道**:极端情况下使用推送通知唤醒应用重建连接 #### Q6: 您是如何平衡心跳频率与设备资源消耗的? A: 平衡心跳频率与资源消耗是一个关键挑战,我们采取以下措施: 1. **多因素动态调整**:考虑网络类型、电量、应用状态等因素 2. **资源感知**:电池电量低于15%时自动进入低功耗模式 3. **WiFi/移动网络区分**:在WiFi下可以适当增加心跳间隔 4. **最小化心跳包大小**:心跳包只包含必要信息,减少传输数据量 5. **批量处理**:将多个操作合并到一次网络交互中 6. **A/B测试优化**:通过对比测试找到最佳心跳参数 我们通过这种平衡策略,在保证95%消息实时性的同时,将电量消耗降低了40%,流量消耗降低了35%。 ### 7.2 技术挑战与解决方案 #### 挑战1: 运营商NAT策略不透明且经常变化 **解决方案**: - 实现自适应探测系统,通过实时测量心跳成功率和超时情况,动态推断当前NAT超时时间 - 建立运营商策略数据库,定期更新各运营商的最新NAT策略 - 保守设计心跳间隔,留出足够安全边界 #### 挑战2: iOS后台限制 **解决方案**: - 使用`UIBackgroundTasks`框架申请后台执行时间 - 实现智能休眠机制,在系统限制下最大化心跳效率 - 结合推送通知作为备份通知通道 - 应用回到前台时快速恢复连接状态 #### 挑战3: 弱网环境导致的频繁重连 **解决方案**: - 实现连接状态机,清晰定义不同连接状态下的行为 - 指数退避重连算法,避免在弱网下频繁无效重连 - 网络质量评估系统,根据实际网络状况调整策略 - 增加随机扰动因子,防止多客户端同步重连 ### 7.3 最复杂的技术挑战及解决过程 #### 挑战描述: 不同设备在同一运营商下NAT超时表现差异大 在我们早期版本中,尽管已经针对不同运营商设置了不同的心跳间隔,但在大规模用户测试中发现,即使是相同运营商的用户,不同设备和地区的NAT超时行为也有显著差异。这导致部分用户频繁掉线,另一部分用户却心跳过于频繁,消耗过多电量和流量。 #### 分析过程: 1. **数据收集**: - 部署了大规模日志系统,收集不同设备、区域和运营商的心跳数据 - 分析超过500万次心跳交互,建立心跳成功率和间隔的关系模型 - 进行用户调研,收集不同场景下的应用使用体验 2. **问题定位**: - 发现同一运营商在不同地区的NAT设备配置差异大 - 不同基站负载和拥塞状况会动态影响NAT超时时间 - 用户网络环境(如家庭WiFi、公司网络、公共热点)存在额外NAT层 3. **深入分析**: - 建立了网络环境分类系统,将用户环境划分为10种典型场景 - 开发专用测试工具,模拟不同网络环境下的NAT行为 - 发现单一心跳策略无法适应所有场景,需要自适应系统 #### 解决方案: 1. **实时自学习心跳系统**: - 设计基于历史数据的自适应算法,每个设备独立学习自己的最优心跳间隔 - 实现滑动窗口分析,持续评估心跳成功率和网络延迟 - 建立心跳间隔自调整机制,在保证连接的前提下最大化间隔 2. **多层次保活策略**: - 实现核心心跳层、辅助保活层和紧急恢复层三级保活机制 - 核心心跳负责常规连接维护 - 辅助保活在连续心跳失败时启动,发送特殊心跳包 - 紧急恢复层使用系统级推送通知唤醒应用 3. **场景感知引擎**: - 开发网络环境识别引擎,自动检测当前网络场景 - 针对不同场景预设不同的初始心跳策略 - 实现场景切换检测,在网络环境变化时快速适应 #### 实施效果: 1. **连接稳定性提升**: - 掉线率从4.7%降低到0.5%(提升89.4%) - 平均重连时间从7.2秒减少到2.1秒(减少70.8%) 2. **资源消耗降低**: - 心跳相关的流量消耗降低42.3% - 电量消耗降低38.6% - 服务器负载降低26.8% 3. **用户体验改善**: - 消息延迟降低65.2% - 应用崩溃率降低15.3% - 用户反馈的网络相关问题减少72.4% ### 7.4 面试技巧要点 1. **强调系统思维**:展示对整个通信架构的理解,而不仅仅是单个心跳机制 2. **数据驱动**:使用具体数据支持您的解决方案和优化效果 3. **权衡取舍**:展示在实时性、资源消耗、复杂度之间如何做出平衡决策 4. **强调持续优化**:描述如何通过数据分析和用户反馈持续改进系统 5. **技术深度**:展示对底层网络原理(如NAT、TCP)的理解 6. **关注用户体验**:强调技术决策如何最终转化为更好的用户体验 ## 八、未来优化方向 1. **机器学习预测模型**:利用历史数据训练模型,预测最优心跳间隔 2. **多通道保活**:探索WebSocket、HTTP长轮询等多种连接方式的协同工作 3. **端到端加密心跳**:增强心跳包安全性,防止网络嗅探和劫持 4. **推送通道集成**:与系统推送机制更深入集成,优化后台唤醒机制 5. **更精细的电量管理**:根据设备型号和电池健康状况调整策略 6. **跨设备协同**:用户多设备同时在线时协调心跳策略,减少总体资源消耗 ================================================ FILE: iOS-Network-Stack-Dive/Docs/CoreNetworkStackDoc/NetworkMonitorDesign.md ================================================ # 全链路追踪(End-to-End Trace)设计思路 ================================================ FILE: iOS-Network-Stack-Dive/Docs/CoreNetworkStackDoc/ProtocolParseDesign.md ================================================ # 自定义协议完整流程设计 ## 协议解析流程图 ![流程图](./协议解析流程图.png) ## 缓冲区、协议头生命周期设计 ### TCP协议的流式数据特性 由于 TCP 协议是流式数据传输,数据可能会被拆分成多个片段接收,可能是: - 一个完整的消息 - 一个消息拆分成多个 TCP 包(**拆包**) - 多个消息合并到一个 TCP 包中(**粘包**) ### 缓冲区的生命周期设计 考虑以下几个方面: - **数据存储**:当接收到的数据不完整时,需存储数据,等待后续数据补全,确保可靠传输。 - **初始化与清空**: - 缓冲区在 `NetworkManager` 初始化时创建,并会一直存在,直到网络关闭。 - 在处理完完整消息后,清空已处理部分。 - 在魔数验证失败或解析错误时,应清空缓冲区,避免数据污染。 - **数据解析**: - 解析过程中,缓冲区存储的数据会被部分消耗。 - 未解析的部分继续保留,等待后续数据到达。 ### 协议头的生命周期设计 协议头的作用:描述消息的基本信息,如 **魔数、版本、消息类型、消息体积、CRC 校验**,用于指导消息体的解析。 设计考虑: - **初始化与销毁**: - 在每个数据包开始解析时初始化。 - 解析完成后销毁,因为每个数据包的协议头都是唯一的。 - **及时更新**: - 每次解析消息时,当前协议头都会被更新,始终代表当前正在解析的数据包。 ### 总结 - **缓冲区**:设计为 **成员变量**,全程存在。 - **协议头**:设计为 **临时变量**,解析过程中动态更新。 --- ## 解析数据时的方案设计 ### 直接判断缓冲区大小 vs 解析标志位 **疑问**:既然协议头包含魔数和标准语义化控制版本,为什么解析过程中还需要使用标志位判断头部? **核心原因**:TCP 的 **拆包问题**。 TCP 是流式传输,数据包的边界不固定。当缓冲区内容小于协议头时,可能出现以下情况: - **拆包问题**:当前缓冲区数据可能是上个消息的最后部分。 - **数据污染**:收到的包是错误的,可能导致误解析。 - **边界问题**:如果缓冲区大小刚好等于消息头,可能会误识别为消息头。 **解决方案**: - 使用 `BOOL` 类型的 **标志位** 指示解析阶段: - `YES`:当前缓冲区数据优先解析消息头。 - `NO`:当前缓冲区数据优先解析消息体。 --- ### 为什么优秀的协议都采用TLV? TLV(Type-Length-Value)是一种广泛应用于通信协议和数据交换的编码格式,其核心由三个部分组成: - Type(类型):标识数据的种类或用途,如温度、湿度或其他业务参数。 - Length(长度):明确后续 Value 字段的字节数,确保接收方能准确读取数据边界。 - Value(值):存储实际数据内容,格式由 Type 和 Length 共同定义,可以是简单数值、字符串或嵌套的 TLV 结构。 #### **TLV 作用** 核心价值在于**自描述性**和**高扩展性**,错误处理和可靠性强。 ```objc // 示例:嵌套 TLV 结构(JSON 对比) // 文本消息体 [ {type:0x01, length:4, value:"Hello"}, // 文本内容 {type:0x02, length:8, value:1715584000}, // 时间戳 {type:0x03, length:N, value:[ // 嵌套附件列表 {type:0x0A, length:M, value:"image.jpg"}, {type:0x0B, length:K, value:"video.mp4"} ]} ] ``` IM领域业内常用设计: - 即时消息传输:文本、表情采用基础TLV,图片/视频通过嵌套TLV携带元数据和分段内容 - 协议升级协商:通过特定命令类型交换双方支持的版本号,触发动态升降级 - 版本兼容:收到新版本服务器响应时,忽略未知TLV标签,若检测到旧客户端版本请求时,不返回新增Tag字段 ### Body如何改造为 TLV 结构? #### **TLV 条目定义** 每个 TLV 条目格式如下: | Tag | Length | Value | | ---- | ------ | ----- | | 2字节| 4字节 | N字节 | 要求:严格遵循 Tag(2) → Length(4) → Value(N) 的结构,且正确处理大端(网络)字节序 #### 协议头与 Body 的协作 - bodyLength 字段:正确表示 TLV 数据区的总长度,确保接收方完整读取。 - tlvEntries 属性:将解析后的 TLV 条目存储为字典(Tag → Value),便于业务逻辑访问。 | 特性 | 说明 | | ------ | ------ | | 动态扩展性 | 新增字段只需定义新 Tag,无需修改协议头或已有逻辑 | | 兼容性 | 旧版本解析器可跳过未知 Tag(依赖 parseTLVFromData 的跳过逻辑) | | 方便调试 | 通过 tlvEntries 可直观查看所有字段,便于日志记录和问题排查 | ### **关键实现细节** 1. **Mach-O Section 注册** 利用 `__attribute__((section))` 将类名写入 Mach-O 文件的 `__DATA` 段,运行时通过 `dladdr` 和 `getsectiondata` 扫描所有注册的类 2. 引入 `TJPMessage` 协议,所有消息类型必须实现数据序列化和类型标记 3. 在现有 `TJPConcreteSession` 中增加 `sendMessage:` 方法,自动调用消息对象的 `tlvData` 方法 4. 新增 `TJPMessageSerializer` 处理公共字段的字节序转换、CRC 计算等底层细节 ### **TLV 解析器 `TJPParsedPacket` 核心作用** - **TLV 解包**:将二进制流按 TLV 格式拆解为 `Tag(2B) + Length(4B) + Value(NB)` 三元组 - **嵌套处理**:递归解析保留标签(如 `0xFFFF`),支持树形数据结构 - **字节序转换**:自动处理网络字节序 → 主机字节序(`ntohs/ntohl`) ### **序列化器 `TJPMessageSerializer` 核心作用** - **TLV 打包**:将对象属性转换为 `Tag + Length + Value` 字节流 - 智能优化 - 字符串自动 UTF-8 编码 - 图片智能压缩(根据网络质量选择 JPEG/WebP) - 数值类型变长编码(Varint) 双模块协作流程: ``` 发送端: 业务对象 → TJPMessageSerializer → TLV 二进制 → Socket 发送 接收端: Socket 数据 → TJPParsedPacket → 结构化字典 → 业务对象映射 ``` ## 并发场景下的问题与解决方案 ### 并发问题 多线程环境下,使用标志位或者多线程写操作可能会引发 **线程安全问题**,导致: - 多个线程同时修改解析状态,出现状态错乱。 - 解析过程中被其他线程打断,无法正确完成解析。 ### 解决方案 #### 1. 传统方案 - **加锁 (`lock`)**:能够解决问题,但会导致**锁竞争**,影响高并发性能。 - **信号量 (`semaphore`)**:可用于控制访问,但仍可能降低吞吐量。 - **串行队列 (`serial queue`)**:避免数据竞争,但会影响多个连接的并发能力。 #### 2. 企业级方案 —— **会话隔离模式** **方案核心**:每个连接维护独立的状态 + 解析逻辑在独立的串行队列中执行。 - **每个连接维护自己的会话对象 (`Session`)**,包含: - **缓冲区 (`buffer`)**:存储数据。 - **当前解析的协议头 (`TJPAdvancedHeader`)**。 - **解析标志位 (`_isParsingHeader`)**。 - **解析操作在独立的串行队列 (`dispatch_queue_t`) 中执行**: - 避免多个线程同时修改解析状态,确保线程安全。 - 解析过程不会影响其他会话,提高并发性能。 ### Log完整流程分析 1. 消息发送准备: ``` [INFO] [TJPConcreteSession.m:242 -[TJPConcreteSession sendData:]_block_invoke] session 准备构造数据包 ``` - 客户端开始准备构造数据包,准备发送"Hello World!!!!!111112223333"消息 2. 计算校验和: ``` Calculated CRC32: 1789856453 ``` - 成功计算了消息体的CRC32校验值: 1789856453 3. 安排重传计时器: ``` [INFO] [TJPConcreteSession.m:693 -[TJPConcreteSession scheduleRetransmissionForSequence:]] 为消息 8 安排重传,间隔 3.0 秒,当前重试次数 0 ``` - 为序列号为8的消息安排了重传计时器,超时时间3秒,当前是第一次发送(重试次数0) 4. 发送消息: ``` [INFO] [TJPConcreteSession.m:281 -[TJPConcreteSession sendData:]_block_invoke] session 消息即将发出, 序列号: 8, 大小: 62字节 ``` - 消息即将发送,序列号8,总大小62字节(包括协议头和消息体) 5. 服务端接收数据: ``` [MOCK SERVER] 接收到客户端发送的数据 [MOCK SERVER] 接收到的消息: 类型=0, 序列号=8, 时间戳=1747215938, 会话ID=4850, 加密类型=1, 压缩类型=1 ``` - 服务端成功接收到数据 - 正确解析出消息类型(0=普通数据)、序列号(8)、时间戳、会话ID和加密/压缩类型 6. 服务端验证校验和: ``` Calculated CRC32: 1789856453 [MOCK SERVER] 接收到的校验和: 1789856453, 计算的校验和: 1789856453 ``` - 服务端计算的CRC32校验值与接收到的校验值完全匹配,验证通过 7. 服务端处理消息并发送ACK: ``` [MOCK SERVER] 收到普通消息,序列号: 8 [MOCK SERVER] 普通消息响应包字段:magic=0xDECAFBAD, msgType=2, sequence=8, timestamp=1747215938, sessionId=4850 ``` - 服务端识别为普通消息,序列号8 - 发送ACK响应,消息类型为2(ACK),保持相同的序列号、时间戳和会话ID 8. 客户端接收ACK: ``` [INFO] [TJPConcreteSession.m:456 -[TJPConcreteSession socket:didReadData:withTag:]_block_invoke] 读取到数据 缓冲区准备添加数据 [INFO] [TJPConcreteSession.m:462 -[TJPConcreteSession socket:didReadData:withTag:]_block_invoke] 开始解析数据 ``` - 客户端接收到服务端的响应数据并开始解析 9. 客户端解析ACK: ``` [INFO] [TJPMessageParser.m:108 -[TJPMessageParser parseHeaderData]] 解析数据头部成功...魔数校验成功! [INFO] [TJPMessageParser.m:113 -[TJPMessageParser parseHeaderData]] 解析序列号:8 的头部成功 [INFO] [TJPMessageParser.m:144 -[TJPMessageParser parseBodyData]] 解析序列号:8 的内容成功 ``` - 客户端成功解析ACK数据包的头部和内容 - 验证魔数成功,确认序列号为8 10. 客户端处理ACK: ``` [INFO] [TJPConcreteSession.m:800 -[TJPConcreteSession handleACKForSequence:]] 接收到 ACK 数据包并进行处理 [INFO] [TJPConcreteSession.m:831 -[TJPConcreteSession handleACKForSequence:]] 处理普通消息ACK,序列号: 8 ``` - 客户端识别并处理ACK数据包 - 确认这是针对序列号8的普通消息的ACK 11. 取消重传计时器: ``` [INFO] [TJPConcreteSession.m:684 -[TJPConcreteSession scheduleRetransmissionForSequence:]_block_invoke] 取消消息 8 的重传计时器 ``` - 由于已收到序列号8的ACK确认,取消相应的重传计时器 ### 流程评估 整个流程完全符合预期,展示了一个健壮的消息发送-确认机制: 1. ✅ **消息构建正确**:包含了所有必要字段,校验和计算无误 2. ✅ **重传机制运作良好**:安排了重传计时器,并在接收到ACK后正确取消 3. ✅ **服务端处理正确**:验证校验和,发送正确格式的ACK响应 4. ✅ **客户端处理ACK正确**:识别并处理ACK,取消重传 5. ✅ **所有时间戳、会话ID、序列号匹配**:确保消息追踪的一致性 6. ✅ **日志信息完整详细**:包含了关键步骤的所有必要信息,便于调试和监控 ### 规则定义 1.包头字段:maing,sequence,timestap,session_id,bodyLength 统一使用网络字节序 2.校验和字段:checksum 使用网络字节序。 客户端需要做转换处理htonl(shecksum) 3.消息体数据:统一使用网络字节序 4.已读回执格式:统一使用TLV格式 ### **总结** - 最终会使用**会话隔离+中心管理**方案。支持统一重连策略,更细粒度的并发控制,减少内存占用;**标志位控制解析阶段,确保数据完整性,解析逻辑独立执行,提升系统吞吐量与响应速度。** ================================================ FILE: iOS-Network-Stack-Dive/Docs/CoreNetworkStackDoc/TJPNetworkManagerV2Design.md ================================================ # TJPNetworkManagerV2:高性能、线程安全的单 TCP 通信模块 > 经过实际生产环境验证的,为 iOS/macOS 打造的 GCD 异步、单 TCP 管理器,支持心跳、重连、ACK、重发机制,使用串行队列实现高性能线程安全,适用于小型项目以及中型项目早期。 > 注意:不适用于现代大型项目 --- ## 模块特点 - **线程安全、无锁实现:** 所有共享状态通过 `dispatch_queue_t` 串行调度,避免加锁与死锁风险; - **稳定可靠的通信机制:** - 心跳机制(Dispatch Timer); - 重连机制(指数退避 + 抖动); - ACK + 消息重发; - **强模块化设计:** 支持协议扩展、消息处理解耦; - **适用于:** 游戏、物联网、IM、轻量服务通信等场景。 --- ## 已实现的能力(Features) | 类型 | 能力描述 | |------------------|----------| | **连接管理** | 支持单连接的建立与断开,支持 TLS 可选配置 | | **消息解析机制** | 使用串行队列 `_networkQueue` 实现无锁串行解析,避免竞态 | | **自定义协议结构** | 包含 `magic` 魔数校验、消息类型、序列号、数据长度、CRC 校验 | | **ACK 回执机制** | 发送数据后会保存待确认消息,收到 ACK 后移除,支持自动重发 | | **心跳机制** | 使用 `dispatch_source` 定时器发送心跳,支持对端响应、超时断线 | | **断线重连机制** | 支持指数退避 + 随机抖动的重连策略,避免惊群问题 | | **完整单元测试** | 提供 `TJPMockTCPServer` 实现模拟服务端,支持心跳/ACK 单测 | | **测试注入接口** | 例如 `onSocketWrite` / `onMessageParsed` 等 hook,便于行为验证 | | **错误处理模块** | 支持协议错误、数据校验失败等异常处理与日志抛出 | --- ## 核心架构 GCDAsyncSocket ↓ TJPNetworkManagerV2 ├── connect / disconnect ├── sendData / resendPacket ├── parseBuffer(串行解析) ├── heartbeatTimer(定时心跳) └── pendingMessages(ACK机制) --- ## 线程安全实现策略 | 共享资源 | 并发问题 | 解决方式 | |-------------------|--------------------------|--------------------------------------------------------------| | `parseBuffer` | 多线程读写、替换、截取 | 封装为 GCD 串行队列 `_networkQueue` 内方法,如 `appendToParseBuffer:` | | `pendingMessages` | 多线程添加/移除 | 同样使用 `_networkQueue` 串行访问 | | `_isParsingHeader`| 状态切换竞争 | 封装为 `setIsParsingHeaderSafe:` 和 `isParsingHeaderSafe` | | `socket` | 多线程收发数据 | 委托 GCDAsyncSocket 自动调度,委托队列设为 `_networkQueue` | | 心跳 + 重连 | 计时器和网络状态变化并发 | dispatch_source + Reachability 回调 block 同步调度 | --- ================================================ FILE: iOS-Network-Stack-Dive/Docs/CoreNetworkStackDoc/TJPNetworkV3FinalDesign.md ================================================ # 中心管理 + 会话自治架构设计文档 ## 1. 架构概述 基于 **中心协调 + 会话自治** 的设计理念,旨在实现高并发网络连接场景下的灵活管理和资源优化。核心思想如下: - **中心管理**:由全局协调器(`TJPNetworkCoordinator`)统一管理网络状态、会话生命周期和公共资源。 - **会话自治**:每个会话(`TJPConcreteSession`)独立管理自身的连接、消息收发和状态逻辑。 - **事件驱动**:通过状态机(`TJPStateMachine`)实现状态转换与业务逻辑的解耦,提升代码可维护性。 - **串行化控制**:通过分级串行队列保证线程安全 ### 1.1分层架构模型 ```objc // 三级架构总体设计 NetworkCoordinator(管理层) └─ ConcreteSession(会话层) └─ GCDAsyncSocket(传输层) // 细分设计 TJPIMClient (门面模式,屏蔽底层实现细节) ├── TJPNetworkCoordinator (多会话管理) │ ├── TJPConcreteSession (会话自制模型) │ │ ├── TJPConnectionManager (连接管理) │ │ ├── TJPDynamicHeartbeat (动态心跳) │ │ ├── TJPMessageParser (消息解析) │ │ ├── TJPMessageBuilder (消息组装) │ │ ├── TJPSequenceManager (序列号管理) ``` ### 1.2状态机驱动设计 ```objc // 状态转换矩阵示例 StateMachineTransitionMatrix: Disconnected → Connecting : ConnectEvent Connecting → Connected : ConnectSuccessEvent Connected → Disconnecting : DisconnectEvent ``` ### 1.3模块化设计 - 独立的心跳管理器(TJPDynamicHeartbeat) - 可插拔的协议解析器(TJPMessageParser) - 可配置的重连策略(TJPReconnectPolicy) ### 1.4明确组件责任界限 - Coordinator: 全局单例,用于管理、会话创建,网络状态监控和全局事件分发,不直接处理TCP连接细节 - Session: 负责单个TCP连接的生命周期管理、数据收发,维护连接状态 - HeartbeatManager: 仅负责心跳管理,向Session报告心跳状态,不直接控制连接 - StateMachine: 仅负责状态管理和转换验证,不执行业务逻辑 - ReconnectPolicy:只负责重连逻辑、计算重连时间,不直接操作TCP连接 ### 1.5队列设计 - 并发队列:并行处理任务,如多个会话的数据解析(但解析器本身要是线程安全),网络状态变更的通知分发 - 串行队列:按顺序执行任务,如单TCP的生命周期管理、心跳管理、状态管理、共享资源操作 ## 2. 核心组件 ### 2.1 TJPNetworkCoordinator(全局协调器) - **职责**: - 管理所有会话的创建和销毁。 - 监控全局网络状态,如:可达性、带宽 - 分配共享资源,如:线程池、解析队列 - **关键特性**: - 串行队列管理会话池 - 独立监控和解析队列 ### 2.2 TJPConcreteSession(会话实例) - **职责**: - 管理单个连接的完整生命周期(连接、断开、重连) - TJPMessageParser统一处理消息的发送、接收和解析 - TJPDynamicHeartbeat动态维护心跳机制 - **关键特性**: - 独立的状态机(`TJPStateMachine`)管理会话状态 - 支持自定义重连策略(`TJPReconnectPolicy`) ### 2.3 TJPStateMachine(状态机) - **职责**: - 定义合法的状态转换规则(如 `Disconnected → Connecting`) - 通过事件(`Event`)驱动状态变更 - 触发状态变更时的副作用(如通知代理、刷新消息队列) - **关键特性**: - 轻量级实现,无第三方依赖。 - 支持动态添加状态和事件。 ## 3. 架构图 ``` +---------------------+ | TJPNetworkCoordinator | +---------------------+ | - Session Pool | | - Network Monitor | | - Global Queues | +----------+----------+ | | manages v +---------------------+ | TJPConcreteSession | +---------------------+ | - State Machine | | - Socket Instance | | - Message Queue | +---------------------+ | | uses v +---------------------+ | TJPStateMachine | +---------------------+ | - State Transitions | | - Event Handlers | +---------------------+ +---------------------+ +-----------------------+ | TJPDynamicHeartbeat | | TJPNetworkCondition | |---------------------| |-----------------------| | - heartbeatQueue |<>---->| - rttWindow[10] | | - networkCondition | | - lossWindow[10] | +---------------------+ +-----------------------+ | 更新RTT/丢包率 ^ |------------------------------| v | +---------------------+ +-----------------------+ | TCP/UDP Session | | Adjustment Logic | |---------------------| |-----------------------| | - sendData() | | - 加权平均计算 | | - disconnect() | | - 梯度调整策略 | +---------------------+ +-----------------------+ ``` ## 4. 核心流程 ### 4.1 连接管理流程 1. **用户(User)** 发起会话创建请求,传递所需配置给 **全局协调器(Coordinator)**。 2. **全局协调器(Coordinator)** 接收到请求后,初始化 **会话实例(Session)** 并传递配置。 3. **会话实例(Session)** 初始化状态机(`StateMachine`)。 4. 用户调用 `connectToHost:port` 方法,**会话实例(Session)** 接收连接请求并触发 **状态机(StateMachine)**。 5. **状态机(StateMachine)** 向 **会话实例(Session)** 发送 **连接请求(Connect 事件)**,并将状态更改为 `Connecting`。 6. **会话实例(Session)** 向目标主机发起连接请求(通过 **Socket**)。 7. **Socket** 连接成功后,返回连接成功的消息。 8. **会话实例(Session)** 接收到连接成功消息后,向 **状态机(StateMachine)** 发送 **连接成功事件(ConnectSuccess)**,并将状态更改为 `Connected`。 9. **会话实例(Session)** 向 **全局协调器(Coordinator)** 通知状态已变更为 `Connected`。 ### 4.2 消息收发流程 1. **用户(User)** 向 **会话实例(Session)** 发送数据。 2. **会话实例(Session)** 构建协议包(包含头部和 CRC32 校验)。 3. **会话实例(Session)** 将数据发送给 **Socket**。 4. **Socket** 收到数据并返回响应数据。 5. **会话实例(Session)** 使用 **消息解析器(MessageParser)** 解析响应数据。 6. **消息解析器(MessageParser)** 将数据解析为 `ParsedPacket` 并返回给 **会话实例(Session)**。 7. **会话实例(Session)** 根据消息类型触发事件(例如:`ACK` 确认)。 8. **状态机(StateMachine)** 处理事件并反馈给 **会话实例(Session)**。 9. **会话实例(Session)** 将处理结果回调给 **用户(User)**。 ## 5. 优势总结 ### 5.1 中心管理的优势 - **资源优化**:通过共享线程池和解析队列,降低内存开销,提升资源利用率。 - **统一监控**:实现全局网络状态与会话健康度的集中监控,提供实时反馈。 - **扩展性强**:支持动态扩展和添加新的会话类型,如 HTTP、WebSocket 等协议,提升系统的灵活性和可扩展性。 ### 5.2 会话自治的优势 - **隔离性**:每个会话独立管理,不同会话间故障隔离,确保单一会话的故障不会影响其他连接。 - **灵活性**:每个会话可以独立配置其策略,如心跳间隔、超时时间等,适应不同的应用场景。 - **易测试**:会话的逻辑与状态独立,能够单独进行单元测试,确保系统的可测试性与可靠性。 ### 5.3 状态机的优势 - **清晰的状态转换**:通过规则明确定义合法的状态路径,确保系统运行的规范性与一致性。 - **解耦业务逻辑**:事件驱动设计有效避免了传统的 `if-else` 嵌套,增强了代码的可读性和维护性。 - **可维护性**:系统状态变更时会产生日志或调试信息,方便开发人员跟踪和排查问题。 --- ## 6. 为什么使用 zlib 进行压缩? - **减少网络带宽占用**:数据压缩后,传输数据的大小大幅减少,节省带宽,提高数据传输效率。 - **提高传输速度**:压缩后的数据更小,传输所需的时间更短,降低了网络延迟,提升了传输速度。 - **广泛支持**:zlib 是一个高效、广泛使用的压缩库,支持几乎所有主流平台,兼容性强。 --- ## 7. 核心技术细节 ### 无锁设计 - 串行队列替代锁和屏障,保证`Session` 内部的线程安全,避免数据竞争。 ### 灵活扩展 - 通过 `Configuration` 配置字典支持会话参数的动态配置,方便根据不同需求调整会话行为。 ### 性能隔离 - 将操作队列与协议解析队列分离,避免阻塞操作,确保 Socket 操作的高效性和实时性。 ### 智能重连 - 集成指数退避算法,并结合网络状态动态调整重连策略,确保在不稳定的网络环境下,系统能够自适应并恢复连接。 --- ## 8. 可扩展组件(后期实现) 以下组件可根据需求进一步扩展: - **加密模块**:在协议层加入 AES 加密,提升通信安全性。 - **流量统计**:在 `Coordinator` 中集成流量监控模块,跟踪和记录网络流量数据。 - **优先级队列**:实现带优先级的消息发送队列,保证高优先级消息的优先传输。 - **协议压缩**:在 `MessageParser` 中集成 zlib 压缩功能,减少数据传输量。 --- ================================================ FILE: iOS-Network-Stack-Dive/Docs/CoreNetworkStackDoc/轻量级多路复用架构设计.md ================================================ # 轻量级多路复用架构设计 ### **什么是会话池(Session Pool)?** **类比生活中的例子**: - 传统做法(无池化): - 用户A需要骑车 → 购买新车 → 使用后丢弃 ❌ - **问题**:每次开销极大,资源浪费严重。 - 池化做法(会话池): - 用户A需要骑车 → 从停车点取车 → 使用后归还 ✅ - 用户B需要骑车 → 复用用户A归还的车 ✅ ​**会话池本质**​:网络连接的“共享单车系统”,实现连接复用。 **为什么要设计会话池**? 1.创建会话的真实成本(单次操作): - TCP 三次握手:50-200ms - TLS 握手:100-300ms - 版本协商:50-100ms - 内存分配:2MB左右 - 线程创建:10-50ms - 总计:210-650ms + 2MB 内存 2.频繁创建和销毁问题 ```objc 消息1: 创建会话 → 发送 → 销毁(耗时500ms ) 消息2: 创建会话 → 发送 → 销毁(耗时500ms ) 消息3: 创建会话 → 发送 → 销毁(耗时500ms ) → 总耗时:1500ms ❌ ``` 3.池化后的效果 ```objc 消息1: 创建会话 → 发送 → 放回池子(耗时500ms) 消息2: 从池子取用 → 发送 → 放回池子(耗时10ms) 消息3: 从池子取用 → 发送 → 放回池子(耗时10ms) → 总耗时:520ms(节省80%+) ``` ### 会话池架构设计 1.基础概念图 ``` ┌─────────────────────────────────────────────────────┐ │ TJPSessionPool │ │ ┌─────────────────┐ ┌─────────────────┐ │ │ │可用池 (Available)│ │ 活跃池 (Active) │ │ │ │ │ │ │ │ │ │ [Chat-1] 💤 │ │ [Chat-2] 🟢 │ │ │ │ [Chat-3] 💤 │ │ [Media-1] 🟢 │ │ │ │ [Media-2] 💤 │ │ [Signal-1] 🟢 │ │ │ └─────────────────┘ └─────────────────┘ │ │ 流向:使用 Available → Active;释放 Active → Available │ └─────────────────────────────────────────────────────┘ 💤 = 空闲状态 | 🟢 = 活跃状态 ``` 2.数据流向图 ``` [应用] ↓ 发送消息 [TJPIMClient] ← 自动路由 (文本→聊天, 媒体→媒体) ↓ 获取会话 [TJPNetworkCoordinator] ← 全局协调 ↓ 请求会话 [TJPLightweightSessionPool] ← 会话复用/创建 ↓ 复用/新建 [TJPConcreteSession] ← 具体连接处理 ↓ 连接管理 [TJPConnectionManager] ← 底层连接 ↓ 数据传输 [GCDAsyncSocket] ← 系统Socket ``` 3.分类管理设计 ``` Objc // 按会话类型存储: sessionPool = { TJPSessionTypeChat: { available: [session1, session3], // 空闲聊天会话 active: [session2] // 活跃聊天会话 }, TJPSessionTypeMedia: { available: [session4], // 空闲媒体会话 active: [session5, session6] // 活跃媒体会话 }, TJPSessionTypeSignaling: { available: [], // 无空闲信令会话 active: [session7] // 活跃信令会话 } } ``` ### 工作流程详解 1.检查目标类型 available 池,若有空闲会话:取出 -> 移入active ->返回 2.若 active 数量 >= 最大限制,返回空或者等待空闲 3.创建新会话 -> 移入 active -> 返回 ### 释放流程详解 1.从 active 池移除 2.若会话健康:重置状态 -> 移入 available 池 3.若会话异常直接销毁 4.记录释放时间 ### 智能清理流程 - 遍历所有 available 池中的会话 - 若空闲时间 > 5分钟,销毁会话并从池中移除 ### 多路复用核心思想 会话Session自治 + 中心调度控制 - 会话自治:每个会话独立管理状态、心跳、重连等 - 中心调度:coordinator仅负责会话生命周期管理及调度 ### 配置参数说明 - 聊天应用:低频率消息,2个会话支持基本可覆盖 - 游戏应用:高频实时数据,需要更多会话支持突发流量 - 文件上传:单线程操作,1个会话避免资源竞争 ### 关键设计 - 基于待确认消息的负载均衡,非复杂算法,一般中小型应用可覆盖 - 固定大小会话池,每个类型最多3个 - 定期清理空闲会话,避免资源囤积 - 按消息类型路由 ### 组件职责划分 应用层 业务逻辑处理 用户界面交互 消息内容构建 TJPIMClient (门面层) 职责: - 统一API接口 - 消息类型路由 - 多通道管理 - 连接状态监控 特点: - 隐藏底层复杂性 - 支持多种会话类型 - 自动路由机制 TJPNetworkCoordinator (协调层) 职责: - 全局网络状态管理 - 会话生命周期协调 - 重连策略执行 - 代理关系管理 特点: - 单例模式 - 网络状态监听 - 会话池集成 TJPLightweightSessionPool (池化层) 职责: - 会话对象复用 - 内存使用优化 - 自动清理机制 - 预热和预创建 特点: - 提升性能 - 降低内存碎片 - 智能健康检查 - 容量自动管理 TJPConcreteSession (会话层) 职责: - 单一连接管理 - 协议实现 - 状态机驱动 - 消息可靠性 特点: - 状态机模式 - 心跳检测 - 自动重传 - 协议版本协商 TJPConnectionManager (连接层) 职责: - Socket连接管理 - 超时控制 - TLS支持 - 连接事件通知 特点: - 连接抽象 - 安全传输 - 异常处理 GCDAsyncSocket (传输层) 职责: - 底层网络IO - 异步数据传输 - 连接建立/断开 特点: - 高性能异步IO - 系统级网络接口 ================================================ FILE: iOS-Network-Stack-Dive/Docs/VIPER-Integration/VIPER-Design.md ================================================ # VIPER 架构设计思路文档 ## 目录 1. [简介](#简介) 2. [VIPER 架构概述](#VIPER-架构概述) 3. [各个模块的职责](#各个模块的职责) - [View](#View) - [Interactor](#Interactor) - [Presenter](#Presenter) - [Entity](#Entity) - [Router](#Router) 4. [模块间通信](#模块间通信) 5. [方法和命名规范](#方法和命名规范) 6. [日志记录与性能优化](#日志记录与性能优化) 7. [错误处理与用户提示](#错误处理与用户提示) 8. [总结](#总结) ## 简介 VIPER(View, Interactor, Presenter, Entity, Router)是一种常见的架构模式,旨在将应用程序的不同职责分离,以提高可维护性、可扩展性和可测试性。它通过将逻辑划分到不同的模块中,减少模块之间的耦合,保证每个模块的单一职责。 在本设计中,我们遵循 VIPER 架构模式,主要包括以下几个模块: - **View**:负责显示 UI 并接收用户输入。 - **Interactor**:处理业务逻辑,与数据源交互。 - **Presenter**:处理视图与交互器之间的协调,准备和格式化数据以供视图显示。 - **Entity**:表示业务数据模型。 - **Router**:负责页面导航和路由控制。 ## VIPER 架构概述 VIPER 架构是一种分层架构设计模式,它将应用的不同职责分配给不同的模块,每个模块之间通过明确的协议进行通信。VIPER 各模块的职责划分如下: ### View - **职责**:展示 UI 并处理用户输入,直接与 Presenter 进行交互。 - **操作**: - 向 Presenter 请求数据更新。 - 显示或隐藏加载状态。 - 显示错误信息。 ### Interactor - **职责**:处理业务逻辑,管理数据操作,如从服务器获取数据、数据验证等。 - **操作**: - 从网络或本地存储中获取数据。 - 执行与业务相关的操作。 - 将结果传递给 Presenter。 ### Presenter - **职责**:协调 View 和 Interactor,处理业务逻辑与数据格式化,将数据准备好以便 View 展示。 - **操作**: - 从 Interactor 获取数据。 - 格式化数据或处理逻辑。 - 更新 View 的 UI。 ### Entity - **职责**:代表数据模型或对象结构,通常是从服务器或数据库中获取的数据。 - **操作**: - 持有业务数据。 - 供 Presenter 或 Interactor 使用。 ### Router - **职责**:管理页面导航,负责视图之间的切换与数据传递。 - **操作**: - 根据用户的操作进行页面跳转。 - 处理页面之间的参数传递。 ## 各个模块的职责 ### View - **TJPViperBaseTableViewController**:作为 View,负责显示表格视图并响应用户的下拉刷新和上拉加载请求。 - **职责**: - 向 Presenter 请求数据。 - 更新 UI 状态,如显示加载指示器、错误提示等。 ### Interactor - **TJPViperBaseInteractorImpl**:作为 Interactor,负责与数据源交互(如网络请求、数据库操作)。 - **职责**: - 执行数据请求操作。 - 提供数据更新信号。 - 实现具体的业务逻辑。 ### Presenter - **TJPViperBasePresenterImpl**:作为 Presenter,协调 View 和 Interactor,处理数据请求和业务逻辑。 - **职责**: - 请求 Interactor 获取数据。 - 格式化数据并将其传递给 View。 - 订阅 Interactor 中的信号并更新 View。 ### Router - **TJPViperBaseRouterImpl**:作为 Router,负责页面的导航和路由管理。 - **职责**: - 根据指定的跳转类型(如 Push、Present、Modal)进行页面跳转。 - 处理页面间的数据传递。 ## 模块间通信 - **View 与 Presenter**:View 通过协议调用 Presenter 中的方法来请求数据,并接收 Presenter 提供的数据更新。 - **Presenter 与 Interactor**:Presenter 请求 Interactor 获取数据,Interactor 将数据返回给 Presenter。 - **Interactor 与 Router**:Interactor 触发数据更新信号,Presenter 和 Router 可以通过订阅信号来响应页面跳转操作。 ## 方法和命名规范 - **命名规范**:方法名应遵循清晰、简洁且描述性强的原则,采用驼峰命名法。例如: - `fetchDataForPageWithCompletion`:表示获取某一页的数据。 - `handleDataFetchSuccess`:表示处理数据获取成功的逻辑。 - `handleDataFetchError`:表示处理数据获取失败的错误逻辑。 - **回调机制**:使用回调闭包(如成功和失败回调)来处理异步操作的结果。 ### 示例方法: ```objc - (void)fetchDataForPageWithCompletion:(NSInteger)page success:(nonnull void (^)(NSArray * _Nullable data, NSInteger totalPage))success failure:(nonnull void (^)(NSError * _Nullable error))failure; ================================================ FILE: iOS-Network-Stack-Dive/Docs/VIPER-Integration/VIPER-RouterGuide.md ================================================ # VIPER 路由层指南 ## 1. 路由类型说明 | 路由类型枚举值 | 使用场景 | 对应处理器协议 | |------------------------------|---------------------------|--------------------------| | TJPNavigationRouteTypeViewPush | 常规视图压栈跳转 | TJPViewPushHandler | | TJPNavigationRouteTypeViewPresent | 模态呈现视图 | TJPViewPresentHandler | | TJPNavigationRouteTypeServiceCall | 后台服务调用 | TJPServiceHandler | | TJPNavigationRouteTypeHybrid | 混合跳转(视图+服务组合) | TJPHybridHandler | ## 2. 参数格式规范 ### 视图跳转参数模板 ```objc NSDictionary *params = @{ @"viewControllerClass": @"MessageDetailViewController", @"navigationType": @(TJPNavigationTypePush), @"messageId": self.messageId, @"timestamp": @([[NSDate date] timeIntervalSince1970]) }; return [TJPNavigationModel modelWithRouteID:@"message/detail" parameters:params]; ================================================ FILE: iOS-Network-Stack-Dive/Docs/Version/v1.2.0版本内容.md ================================================ # 网络协议改造技术文档 ## 一、协议头结构设计(TLV格式) ### 1. C结构体定义 ```c #pragma pack(push, 1) typedef struct { uint32_t magic; // 魔数 0xDECAFBAD (4字节) uint8_t version_major; // 协议主版本 (1字节) uint8_t version_minor; // 协议次版本 (1字节) uint16_t msgType; // 消息类型 (2字节) uint32_t sequence; // 序列号 (4字节) uint32_t timestamp; // 秒级时间戳 (4字节防重放) TJPEncryptType encrypt_type; // 加密类型枚举 (1字节) TJPCompressType compress_type; // 压缩类型枚举 (1字节) uint16_t session_id; // 会话ID (2字节) uint32_t bodyLength; // Body长度 (网络字节序 4字节) uint32_t checksum; // 安全校验码 (4字节) } TJPFinalAdavancedHeader; #pragma pack(pop) ``` 协议改造为TLV格式,并兼容Protobuf,协议头增加压缩类型,加密类型,时间戳 ### 2. 兼容Protobuf设计方案 ``` message ProtocolBody { bytes protobuf_payload = 1; // Protobuf序列化数据 map tlv_fields = 2; // TLV扩展字段 } enum TJPEncryptType { NONE = 0; AES256_CBC = 1; SM4_GCM = 2; } enum TJPCompressType { RAW = 0; ZLIB = 1; LZ4 = 2; } ``` crc32增加更健全的安全防护机制加盐。后续开放AES256加密接口 ### 3. 新建TJPMessageBuilder类用于专门构建数据包,单一职责原则 ### 4.消息重传使用更灵活的GCD定时器代替dispatch_after,添加重传计时器的生命周期管理。 ================================================ FILE: iOS-Network-Stack-Dive/HomeVC/HomeViewController.h ================================================ // // HomeViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // #import NS_ASSUME_NONNULL_BEGIN @protocol TJPViperModuleProvider; @interface HomeViewController : UIViewController @property (nonatomic, strong) id tjpViperModuleProvider; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/HomeVC/HomeViewController.m ================================================ // // HomeViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // #import "HomeViewController.h" #import "TJPViperModuleProvider.h" #import "StickPacketDemoController.h" #import "StickPacketSolutionController.h" #import "TJPLoggerViewController.h" #import "TJPCustomTableViewDemoViewController.h" #import "TJPSectionTableViewDemoViewController.h" #import "TJPVIPERDemoViewController.h" #import "TJPNetworkMonitorViewController.h" #import "TJPChatViewController.h" @interface HomeViewController () @property (nonatomic, strong) UITableView *tableView; @property (nonatomic, strong) NSArray *sectionsData; @end @implementation HomeViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.title = @"iOS-Network-Stack"; [self initData]; [self setupTableView]; } - (void)initData { self.sectionsData = @[ @{ @"title": @"网络通信", @"viewControllers": @[ @{ @"title": @"TCP粘包问题演示", @"viewController": [StickPacketDemoController class] }, @{ @"title": @"粘包问题解决方案", @"viewController": [StickPacketSolutionController class] }, @{ @"title": @"TCP通信监控分析", @"viewController": [TJPNetworkMonitorViewController class] } ] }, // @{ // @"title": @"AOP实现", // @"viewControllers": @[ // @{ @"title": @"轻量级切面日志", @"viewController": [TJPLoggerViewController class] }, // ] // }, @{ @"title": @"架构与解耦", @"viewControllers": @[ @{ @"title": @"模块化TableView实践", @"viewController": [TJPCustomTableViewDemoViewController class] }, @{ @"title": @"模块化TableView多Section列表", @"viewController": [TJPSectionTableViewDemoViewController class] }, @{ @"title": @"多类型Feed流应用 - VIPER架构实战", @"viewController": @"VIPERDemo" }, @{ @"title": @"聊天界面实战", @"viewController": [TJPChatViewController class] } ] } ]; } - (void)setupTableView { self.tableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStyleGrouped]; self.tableView.delegate = self; self.tableView.dataSource = self; [self.view addSubview:self.tableView]; } #pragma mark - UITableViewDataSource - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView { return self.sectionsData.count; } - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { NSArray *viewControllers = self.sectionsData[section][@"viewControllers"]; return viewControllers.count; } - (nullable NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section { return self.sectionsData[section][@"title"]; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"Cell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (!cell) { cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier]; } NSArray *viewControllers = self.sectionsData[indexPath.section][@"viewControllers"]; NSDictionary *vcInfo = viewControllers[indexPath.row]; cell.textLabel.text = vcInfo[@"title"]; return cell; } #pragma mark - UITableViewDelegate - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath { NSArray *viewControllers = self.sectionsData[indexPath.section][@"viewControllers"]; NSDictionary *vcInfo = viewControllers[indexPath.row]; id vcEntry = vcInfo[@"viewController"]; UIViewController *vc = nil; if ([vcEntry isKindOfClass:[NSString class]]) { if ([vcEntry isEqualToString:@"VIPERDemo"]) { vc = [self.tjpViperModuleProvider viperDemoViewController]; } } else { // 是 class,需要 alloc init vc = [[(Class)vcEntry alloc] init]; } if (vc) { [self.navigationController pushViewController:vc animated:YES]; } [tableView deselectRowAtIndexPath:indexPath animated:YES]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Info.plist ================================================ CFBundleName $(PRODUCT_NAME) UIMainStoryboardFile UIApplicationMainClassName AppDelegate TyphoonInitialAssemblies TJPViperModuleAssembly ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketDemo/SocketChatClient.h ================================================ // // SocketChatClient.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/17. // 基于TCP的文本聊天客户端 #import NS_ASSUME_NONNULL_BEGIN @interface SocketChatClient : NSObject - (void)connectToHost:(NSString *)host port:(uint16_t)port; - (void)sendMessage:(NSString *)message; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketDemo/SocketChatClient.m ================================================ // // SocketChatClient.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/17. // #import "SocketChatClient.h" #import "GCDAsyncSocket.h" @interface SocketChatClient () @property (nonatomic, strong) GCDAsyncSocket *client; @end @implementation SocketChatClient - (instancetype)init { if (self = [super init]) { dispatch_queue_t serialQueue = dispatch_queue_create("com.SocketChatClient.client", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(serialQueue, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)); self.client = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:serialQueue]; } return self; } - (void)connectToHost:(NSString *)host port:(uint16_t)port { NSError *error; if (![self.client connectToHost:host onPort:port error:&error]) { NSLog(@"Connect failed: %@", error); }else { NSLog(@"Connecting to %@:%d", host, port); } } - (void)sendMessage:(NSString *)message { NSLog(@"Send Message :%@", message); NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding]; //-1代表无限等待,永不超时 [self.client writeData:data withTimeout:-1 tag:0]; [self.client readDataWithTimeout:-1 tag:0]; } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { //连接成功 NSLog(@"Connected to server"); [sock readDataWithTimeout:-1 tag:0]; } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { NSString *msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"[Server]: %@", msg); [sock readDataWithTimeout:-1 tag:0]; } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { NSLog(@"Client disconnected: %@", err); } @end ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketDemo/SocketChatServer.h ================================================ // // SocketChatServer.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/17. // 基于TCP的文本聊天服务器 #import NS_ASSUME_NONNULL_BEGIN @protocol SocketChatServerDelegate - (void)didReceiveMessageFromClient:(NSString *)message; @end @interface SocketChatServer : NSObject @property (nonatomic, weak) id delegate; - (void)startServerOnPort:(uint16_t)port; - (void)stopServer; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketDemo/SocketChatServer.m ================================================ // // SocketChatServer.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/17. // #import "SocketChatServer.h" #import "GCDAsyncSocket.h" @interface SocketChatServer () @property (nonatomic, strong) GCDAsyncSocket *serverSocket; @property (nonatomic, strong) NSMutableArray *clientSockets; @end @implementation SocketChatServer - (instancetype)init { if (self = [super init]) { self.clientSockets = [NSMutableArray array]; } return self; } - (void)startServerOnPort:(uint16_t)port { dispatch_queue_t serialQueue = dispatch_queue_create("com.SocketChatServer.server", DISPATCH_QUEUE_SERIAL); dispatch_set_target_queue(serialQueue, dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)); self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:serialQueue]; NSError *error = nil; if (![self.serverSocket acceptOnPort:port error:&error]) { NSLog(@"Server failed to start: %@", error); }else { NSLog(@"Server started on port %d", port); } } - (void)stopServer { [self.serverSocket disconnect]; NSLog(@"Server stopped"); } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { [self.clientSockets addObject:newSocket]; //新客户端接入 NSLog(@"Client connected: %@", newSocket.connectedHost); [newSocket readDataWithTimeout:-1 tag:0]; } - (void)socket:(GCDAsyncSocket *)socket didReadData:(NSData *)data withTag:(long)tag { //收到消息 NSString *msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"Received: %@", msg); if (self.delegate && [self.delegate respondsToSelector:@selector(didReceiveMessageFromClient:)]) { [self.delegate didReceiveMessageFromClient:msg]; } for (GCDAsyncSocket *client in self.clientSockets) { if (client != socket) { [client writeData:data withTimeout:-1 tag:0]; } } //继续等待数据 [socket readDataWithTimeout:-1 tag:0]; } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { //客户端断开连接 [self.clientSockets removeObject:sock]; NSLog(@"Client disconnected"); } @end ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketDemo/StickPacketDemoController.h ================================================ // // StickPacketDemoController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // 粘包问题演示 #import NS_ASSUME_NONNULL_BEGIN @interface StickPacketDemoController : UIViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketDemo/StickPacketDemoController.m ================================================ // // StickPacketDemoController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // #import "StickPacketDemoController.h" #import "SocketChatServer.h" #import "SocketChatClient.h" @interface StickPacketDemoController () @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UIButton *sendButton; @property (nonatomic, strong) UITextView *messageTextView; @property (nonatomic, strong) SocketChatServer *server; @property (nonatomic, strong) SocketChatClient *client; @end @implementation StickPacketDemoController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.server = [[SocketChatServer alloc] init]; self.server.delegate = self; [self.server startServerOnPort:8080]; self.client = [[SocketChatClient alloc] init]; [self.client connectToHost:@"127.0.0.1" port:8080]; [self setupUI]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // 停止服务器 [self.server stopServer]; } - (void)setupUI { self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(50, 100, 300, 30)]; self.titleLabel.text = @"粘包问题演示"; self.titleLabel.textAlignment = NSTextAlignmentCenter; self.titleLabel.font = [UIFont systemFontOfSize:18]; [self.view addSubview:self.titleLabel]; self.sendButton = [UIButton buttonWithType:UIButtonTypeSystem]; self.sendButton.frame = CGRectMake(self.view.bounds.size.width / 2 - 50, CGRectGetMaxY(self.titleLabel.frame) + 20, 100, 40); [self.sendButton setTitle:@"Send" forState:UIControlStateNormal]; [self.sendButton addTarget:self action:@selector(sendMessage) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.sendButton]; self.messageTextView = [[UITextView alloc] initWithFrame:CGRectMake(20, 300, 350, 200)]; self.messageTextView.font = [UIFont systemFontOfSize:14]; self.messageTextView.textColor = [UIColor blackColor]; self.messageTextView.backgroundColor = [UIColor lightGrayColor]; self.messageTextView.editable = NO; [self.view addSubview:self.messageTextView]; } - (void)sendMessage { [self.client sendMessage:@"Message 1"]; NSString *message = @"Hello, Server! This is a test message for sticky packet problem."; [self.client sendMessage:message]; [self.client sendMessage:@"Message 2"]; [self.client sendMessage:@"Message 3"]; } #pragma mark - SocketChatServerDelegate - (void)didReceiveMessageFromClient:(NSString *)message { self.messageTextView.text = [self.messageTextView.text stringByAppendingFormat:@"Server: \n%@", message]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketSolution/SolutionStickyPacketClient.h ================================================ // // SolutionStickyPacketClient.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // #import NS_ASSUME_NONNULL_BEGIN @interface SolutionStickyPacketClient : NSObject - (void)connectToHost:(NSString *)host port:(uint16_t)port; - (void)sendMessage:(NSString *)message; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketSolution/SolutionStickyPacketClient.m ================================================ // // SolutionStickyPacketClient.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // #import "SolutionStickyPacketClient.h" #import "GCDAsyncSocket.h" @interface SolutionStickyPacketClient () @property (nonatomic, strong) GCDAsyncSocket *client; @end @implementation SolutionStickyPacketClient - (instancetype)init { if (self = [super init]) { self.client = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; } return self; } - (void)connectToHost:(NSString *)host port:(uint16_t)port { NSError *error; if (![self.client connectToHost:host onPort:port error:&error]) { NSLog(@"Connect failed: %@", error); }else { NSLog(@"Connecting to %@:%d", host, port); } } - (void)sendMessage:(NSString *)message { NSLog(@"Send Message: %@", message); //消息体数据 NSData *messageData = [message dataUsingEncoding:NSUTF8StringEncoding]; uint32_t messageLength = (uint32_t)messageData.length; //转换成网络字节序 messageLength = ntohl(messageLength); //消息头 NSData *headerData = [NSData dataWithBytes:&messageLength length:sizeof(messageLength)]; NSMutableData *finData = [NSMutableData data]; [finData appendData:headerData]; [finData appendData:messageData]; [self.client writeData:finData withTimeout:-1 tag:0]; [self.client readDataWithTimeout:-1 tag:0]; } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didConnectToHost:(NSString *)host port:(uint16_t)port { //连接成功 NSLog(@"Connected to server"); [sock readDataWithTimeout:-1 tag:0]; } - (void)socket:(GCDAsyncSocket *)sock didReadData:(NSData *)data withTag:(long)tag { if (tag == 0) { //先读取消息头长度 uint32_t messageLength = 0; [data getBytes:&messageLength length:sizeof(messageLength)]; messageLength = ntohl(messageLength); //继续读取消息体 [sock readDataToLength:messageLength withTimeout:-1 tag:1]; }else if (tag == 1) { NSString *response = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"Received from server: %@", response); } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { NSLog(@"Client disconnected: %@", err); } @end ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketSolution/SolutionStickyPacketServer.h ================================================ // // SolutionStickyPacketServer.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // #import NS_ASSUME_NONNULL_BEGIN @interface SolutionStickyPacketServer : NSObject @property (nonatomic, copy) void(^serverReceiveChatComplete)(NSString *message); - (void)startServerOnPort:(uint16_t)port; - (void)stopServer; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketSolution/SolutionStickyPacketServer.m ================================================ // // SolutionStickyPacketServer.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // #import "SolutionStickyPacketServer.h" #import "GCDAsyncSocket.h" @interface SolutionStickyPacketServer () @property (nonatomic, strong) GCDAsyncSocket *serverSocket; @property (nonatomic, strong) NSMutableArray *clientSockets; @end @implementation SolutionStickyPacketServer - (instancetype)init { if (self = [super init]) { self.clientSockets = [NSMutableArray array]; } return self; } - (void)startServerOnPort:(uint16_t)port { self.serverSocket = [[GCDAsyncSocket alloc] initWithDelegate:self delegateQueue:dispatch_get_main_queue()]; NSError *error = nil; if (![self.serverSocket acceptOnPort:port error:&error]) { NSLog(@"Server failed to start: %@", error); }else { NSLog(@"Server started on port %d", port); } } - (void)stopServer { [self.serverSocket disconnect]; NSLog(@"Server stopped"); } #pragma mark - Private Method - (void)readMessageHeader:(GCDAsyncSocket *)socket { [socket readDataToLength:4 withTimeout:-1 tag:0]; } - (void)readMessageBody:(GCDAsyncSocket *)socket length:(uint32_t)length { [socket readDataToLength:length withTimeout:-1 tag:1]; } #pragma mark - GCDAsyncSocketDelegate - (void)socket:(GCDAsyncSocket *)sock didAcceptNewSocket:(GCDAsyncSocket *)newSocket { [self.clientSockets addObject:newSocket]; NSLog(@"New client connected: %@", newSocket.connectedHost); //读取header [self readMessageHeader:newSocket]; } - (void)socket:(GCDAsyncSocket *)socket didReadData:(NSData *)data withTag:(long)tag { if (tag == 0) { //读取消息头 uint32_t messageLength = 0; [data getBytes:&messageLength length:sizeof(messageLength)]; //转换为网络字节序 messageLength = ntohl(messageLength); //根据消息长度读取消息体 [self readMessageBody:socket length:messageLength]; }else if (tag == 1) { //读取消息体 NSString *message = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"Received message: %@", message); //回复客户端 NSString *response = [NSString stringWithFormat:@"Echo: %@", message]; NSData *responseData = [response dataUsingEncoding:NSUTF8StringEncoding]; //发送消息头+消息体 uint32_t responseLength = (uint32_t)responseData.length; //转换为网络字节序 responseLength = ntohl(responseLength); NSData *headerData = [NSData dataWithBytes:&responseLength length:sizeof(responseLength)]; NSMutableData *finData = [NSMutableData data]; [finData appendData:headerData]; [finData appendData:responseData]; [socket writeData:finData withTimeout:-1 tag:0]; //继续读取消息头 [self readMessageHeader:socket]; } } - (void)socketDidDisconnect:(GCDAsyncSocket *)sock withError:(NSError *)err { //客户端断开连接 [self.clientSockets removeObject:sock]; NSLog(@"Client disconnected"); } @end ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketSolution/StickPacketSolutionController.h ================================================ // // StickPacketSolutionController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // 解决粘包问题的基本方式 #import NS_ASSUME_NONNULL_BEGIN @interface StickPacketSolutionController : UIViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Labs/NetworkFundamentals/Lab-Socket-API/StickyPacketSolution/StickPacketSolutionController.m ================================================ // // StickPacketSolutionController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/18. // 解决粘包问题 #import "StickPacketSolutionController.h" #import "SolutionStickyPacketClient.h" #import "SolutionStickyPacketServer.h" @interface StickPacketSolutionController () @property (nonatomic, strong) UILabel *titleLabel; @property (nonatomic, strong) UIButton *sendButton; @property (nonatomic, strong) UITextView *messageTextView; @property (nonatomic, strong) SolutionStickyPacketServer *server; @property (nonatomic, strong) SolutionStickyPacketClient *client; @end @implementation StickPacketSolutionController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor whiteColor]; self.server = [[SolutionStickyPacketServer alloc] init]; [self.server startServerOnPort:8080]; self.client = [[SolutionStickyPacketClient alloc] init]; [self.client connectToHost:@"127.0.0.1" port:8080]; [self setupUI]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; // 停止服务器 [self.server stopServer]; } - (void)setupUI { self.titleLabel = [[UILabel alloc] initWithFrame:CGRectMake(50, 100, 300, 30)]; self.titleLabel.text = @"解决粘包问题"; self.titleLabel.textAlignment = NSTextAlignmentCenter; self.titleLabel.font = [UIFont systemFontOfSize:18]; [self.view addSubview:self.titleLabel]; self.sendButton = [UIButton buttonWithType:UIButtonTypeSystem]; self.sendButton.frame = CGRectMake(self.view.bounds.size.width / 2 - 50, CGRectGetMaxY(self.titleLabel.frame) + 20, 100, 40); [self.sendButton setTitle:@"Send" forState:UIControlStateNormal]; [self.sendButton addTarget:self action:@selector(sendMessage) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:self.sendButton]; self.messageTextView = [[UITextView alloc] initWithFrame:CGRectMake(20, 300, 350, 200)]; self.messageTextView.font = [UIFont systemFontOfSize:14]; self.messageTextView.textColor = [UIColor blackColor]; self.messageTextView.backgroundColor = [UIColor lightGrayColor]; self.messageTextView.editable = NO; [self.view addSubview:self.messageTextView]; } - (void)sendMessage { [self.client sendMessage:@"Message 1"]; NSString *message = @"Hello, Server! This is a test message for sticky packet problem."; [self.client sendMessage:message]; [self.client sendMessage:@"Message 2"]; [self.client sendMessage:@"Message 3"]; } #pragma mark - SocketChatServerDelegate - (void)didReceiveMessageFromClient:(NSString *)message { self.messageTextView.text = [self.messageTextView.text stringByAppendingFormat:@"Server: \n%@", message]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/Entity/TJPChatMessage.h ================================================ // // TJPChatMessage.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // #import #import "TJPChatMessageDefine.h" NS_ASSUME_NONNULL_BEGIN @interface TJPChatMessage : NSObject @property (nonatomic, copy) NSString *messageId; @property (nonatomic, copy) NSString *content; @property (nonatomic, assign) BOOL isFromSelf; @property (nonatomic, strong) NSDate *timestamp; @property (nonatomic, assign) TJPChatMessageType messageType; // 文本、图片等 @property (nonatomic, strong) UIImage *image; // 图片消息 @property (nonatomic, assign) TJPChatMessageStatus status; // 发送中、已发送、失败 @property (nonatomic, assign) uint32_t sequence; // 消息序列号 @property (nonatomic, strong) NSDate *readTime; // 消息已读时间 @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/Entity/TJPChatMessage.m ================================================ // // TJPChatMessage.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // #import "TJPChatMessage.h" @implementation TJPChatMessage @end ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/Manager/TJPMessageTimeoutManager.h ================================================ // // TJPMessageTimeoutManager.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/26. // #import NS_ASSUME_NONNULL_BEGIN @class TJPChatMessage; @interface TJPMessageTimeoutManager : NSObject // 存储所有正在发送的消息 @property (nonatomic, strong) NSMutableArray *pendingMessages; // 统一处理定时器 @property (nonatomic, strong) dispatch_source_t timeoutTimer; + (instancetype)sharedManager; - (void)addMessageForTimeoutCheck:(TJPChatMessage *)message; - (void)removeMessageFromTimeoutCheck:(TJPChatMessage *)message; // 定时检查消息超时 - (void)checkMessagesTimeout; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/Manager/TJPMessageTimeoutManager.m ================================================ // // TJPMessageTimeoutManager.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/26. // #import "TJPMessageTimeoutManager.h" #import "TJPChatMessage.h" #import "TJPNetworkDefine.h" @interface TJPMessageTimeoutManager () @end @implementation TJPMessageTimeoutManager + (instancetype)sharedManager { static TJPMessageTimeoutManager *sharedManager; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sharedManager = [[self alloc] init]; sharedManager.pendingMessages = [NSMutableArray array]; dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); sharedManager.timeoutTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); dispatch_source_set_timer(sharedManager.timeoutTimer, dispatch_time(DISPATCH_TIME_NOW, 0), 1.0 * NSEC_PER_SEC, 0); dispatch_source_set_event_handler(sharedManager.timeoutTimer, ^{ [sharedManager checkMessagesTimeout]; }); dispatch_resume(sharedManager.timeoutTimer); }); return sharedManager; } // 添加正在发送的消息到超时检查队列 - (void)addMessageForTimeoutCheck:(TJPChatMessage *)message { [self.pendingMessages addObject:message]; } // 从超时检查队列中移除已经发送成功的消息 - (void)removeMessageFromTimeoutCheck:(TJPChatMessage *)message { if ([self.pendingMessages containsObject:message]) { [self.pendingMessages removeObject:message]; } } // 定时检查所有正在发送的消息是否超时 - (void)checkMessagesTimeout { // 遍历队列中的每一条正在发送的消息 for (TJPChatMessage *message in self.pendingMessages) { // 如果消息仍处于发送中状态,并且超时 if (message.status == TJPChatMessageStatusSending && [self isTimeoutForMessage:message]) { // 超时处理 message.status = TJPChatMessageStatusFailed; // 通知UI更新消息状态 [[NSNotificationCenter defaultCenter] postNotificationName:kTJPMessageStatusUpdateNotification object:message]; } } } // 判断消息是否超时 - (BOOL)isTimeoutForMessage:(TJPChatMessage *)message { NSTimeInterval elapsedTime = [[NSDate date] timeIntervalSinceDate:message.timestamp]; return elapsedTime > 5.0; // 超过5秒视为超时 } @end ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/TJPChatViewController.h ================================================ // // TJPChatViewController.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPChatViewController : UIViewController @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/TJPChatViewController.m ================================================ // // TJPChatViewController.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // #import "TJPChatViewController.h" #import #import #import "TJPChatInputView.h" #import "TJPConnectionStatusView.h" #import "TJPChatMessage.h" #import "TJPChatMessageCell.h" #import "TJPMockFinalVersionTCPServer.h" #import "TJPIMClient.h" #import "TJPSessionProtocol.h" #import "TJPSessionDelegate.h" #import "TJPTextMessage.h" #import "TJPNetworkDefine.h" #import "TJPMessageTimeoutManager.h" @interface TJPChatViewController () @property (nonatomic, strong) TJPMockFinalVersionTCPServer *mockServer; @property (nonatomic, strong) TJPIMClient *client; // UI组件 @property (nonatomic, strong) TJPConnectionStatusView *statusBarView; @property (nonatomic, strong) UITableView *messagesTableView; @property (nonatomic, strong) TJPChatInputView *chatInputView; @property (nonatomic, strong) MASConstraint *inputViewBottomConstraint; // 数据 @property (nonatomic, strong) NSMutableArray *messages; @property (nonatomic, assign) NSInteger messageIdCounter; @property (nonatomic, strong) NSMutableDictionary *messageMap; // 状态监控 @property (nonatomic, strong) NSTimer *statusUpdateTimer; @end @implementation TJPChatViewController - (void)dealloc { [self.statusUpdateTimer invalidate]; [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor systemBackgroundColor]; self.title = @"TCP聊天实战"; self.messageMap = [NSMutableDictionary dictionary]; [self initializeData]; [self setupNetwork]; [self setupUI]; [self startStatusMonitoring]; [self autoConnect]; [self setupNotificationListeners]; } - (void)viewWillDisappear:(BOOL)animated { [super viewWillDisappear:animated]; [self.mockServer stop]; [self.client disconnectAll]; [self.statusUpdateTimer invalidate]; } #pragma mark - Initialization - (void)initializeData { self.messages = [NSMutableArray array]; self.messageIdCounter = 1; // 添加示例消息 [self addWelcomeMessages]; } - (void)addWelcomeMessages { TJPChatMessage *welcomeMsg = [[TJPChatMessage alloc] init]; welcomeMsg.messageId = @"welcome_1"; welcomeMsg.content = @"欢迎来到TCP聊天实战演示!"; welcomeMsg.isFromSelf = NO; welcomeMsg.timestamp = [NSDate date]; welcomeMsg.messageType = TJPChatMessageTypeText; welcomeMsg.status = TJPChatMessageStatusSent; [self.messages addObject:welcomeMsg]; TJPChatMessage *infoMsg = [[TJPChatMessage alloc] init]; infoMsg.messageId = @"info_1"; infoMsg.content = @"你可以发送文本消息和图片,体验完整的TCP通信流程"; infoMsg.isFromSelf = NO; infoMsg.timestamp = [NSDate dateWithTimeIntervalSinceNow:1]; infoMsg.messageType = TJPChatMessageTypeText; infoMsg.status = TJPChatMessageStatusSent; [self.messages addObject:infoMsg]; } #pragma mark - Network Setup - (void)setupNetwork { // 启动模拟服务器 self.mockServer = [[TJPMockFinalVersionTCPServer alloc] init]; [self.mockServer startWithPort:12345]; // 获取IM客户端实例 self.client = [TJPIMClient shared]; } - (void)autoConnect { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self.client connectToHost:@"127.0.0.1" port:12345 forType:TJPSessionTypeChat]; // 连接后尝试获取session设置代理 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self setupSessionDelegateAfterConnection]; }); }); } - (void)setupSessionDelegateAfterConnection { } - (void)setupNotificationListeners { NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; // 监听消息发送成功 [center addObserver:self selector:@selector(handleMessageSent:) name:kTJPMessageSentNotification object:nil]; // 监听消息发送失败 [center addObserver:self selector:@selector(handleMessageFailed:) name:kTJPMessageFailedNotification object:nil]; // 监听消息接收 [center addObserver:self selector:@selector(handleMessageReceived:) name:kTJPMessageReceivedNotification object:nil]; // 状态更新 [center addObserver:self selector:@selector(handleMessageStatusUpdated:) name:kTJPMessageStatusUpdateNotification object:nil]; // 新增键盘通知监听 [center addObserver:self selector:@selector(keyboardWillShow:) name:UIKeyboardWillShowNotification object:nil]; [center addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; // 消息已读处理 [center addObserver:self selector:@selector(handleMessageRead:) name:kTJPMessageReadNotification object:nil]; NSLog(@"[TJPChatViewController] 监听器设置完成"); } #pragma mark - UI Setup - (void)setupUI { [self setupStatusBar]; [self setupMessagesTableView]; [self setupChatInputView]; [self setupConstraints]; } - (void)setupStatusBar { self.statusBarView = [[TJPConnectionStatusView alloc] init]; [self.view addSubview:self.statusBarView]; } - (void)setupMessagesTableView { self.messagesTableView = [[UITableView alloc] init]; self.messagesTableView.delegate = self; self.messagesTableView.dataSource = self; self.messagesTableView.separatorStyle = UITableViewCellSeparatorStyleNone; self.messagesTableView.backgroundColor = [UIColor systemBackgroundColor]; [self.messagesTableView registerClass:[TJPChatMessageCell class] forCellReuseIdentifier:@"ChatMessageCell"]; [self.view addSubview:self.messagesTableView]; } - (void)setupChatInputView { self.chatInputView = [[TJPChatInputView alloc] init]; self.chatInputView.delegate = self; [self.view addSubview:self.chatInputView]; } - (void)setupConstraints { // 状态栏约束 [self.statusBarView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.view.mas_safeAreaLayoutGuideTop); make.leading.trailing.equalTo(self.view); make.height.mas_equalTo(44); }]; // 消息列表约束 [self.messagesTableView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.statusBarView.mas_bottom); make.leading.trailing.equalTo(self.view); make.bottom.equalTo(self.chatInputView.mas_top); }]; // 聊天输入框约束 [self.chatInputView mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.trailing.equalTo(self.view); make.height.mas_equalTo(52); // 修改:使用正确的最小高度 self.inputViewBottomConstraint = make.bottom.equalTo(self.view); }]; } #pragma mark - Status Monitoring - (void)startStatusMonitoring { self.statusUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(updateConnectionStatus) userInfo:nil repeats:YES]; } - (void)updateConnectionStatus { TJPConnectionStatus newStatus; if ([self.client isDisConnectedForType:TJPSessionTypeChat]) { newStatus = TJPConnectionStatusDisconnected; } else if ([self.client isConnectedForType:TJPSessionTypeChat]) { newStatus = TJPConnectionStatusConnected; } else { newStatus = TJPConnectionStatusReconnecting; } // 更新状态栏 if (self.statusBarView.status != newStatus) { [self.statusBarView updateStatus:newStatus]; } // 更新消息计数 [self.statusBarView updateMessageCount:self.messages.count]; // 计算各种状态的消息数量 NSInteger sendingCount = 0; NSInteger failedCount = 0; for (TJPChatMessage *message in self.messages) { if (message.isFromSelf) { switch (message.status) { case TJPChatMessageStatusSending: sendingCount++; break; case TJPChatMessageStatusFailed: failedCount++; break; default: break; } } } // 更新待发送消息计数(发送中 + 失败的) [self.statusBarView updatePendingCount:sendingCount + failedCount]; // 如果有失败的消息,可以在状态栏显示额外提示 if (failedCount > 0) { NSLog(@"[TJPChatViewController] ⚠️ 有 %ld 条消息发送失败,可点击重试", (long)failedCount); } } #pragma mark - Private Method - (void)showTemporaryFailureNotification { // 显示临时的发送失败提示 UIView *notificationView = [[UIView alloc] init]; notificationView.backgroundColor = [[UIColor systemRedColor] colorWithAlphaComponent:0.9]; notificationView.layer.cornerRadius = 8; UILabel *notificationLabel = [[UILabel alloc] init]; notificationLabel.text = @"消息发送失败,请点击重试"; notificationLabel.textColor = [UIColor whiteColor]; notificationLabel.font = [UIFont systemFontOfSize:14]; notificationLabel.textAlignment = NSTextAlignmentCenter; [notificationView addSubview:notificationLabel]; [self.view addSubview:notificationView]; [notificationView mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self.view); make.bottom.equalTo(self.chatInputView.mas_top).offset(-16); make.height.mas_equalTo(40); }]; [notificationLabel mas_makeConstraints:^(MASConstraintMaker *make) { make.center.equalTo(notificationView); make.leading.equalTo(notificationView.mas_leading).offset(16); make.trailing.equalTo(notificationView.mas_trailing).offset(-16); }]; // 显示和隐藏动画 notificationView.alpha = 0; notificationView.transform = CGAffineTransformMakeTranslation(0, 20); [UIView animateWithDuration:0.3 animations:^{ notificationView.alpha = 1; notificationView.transform = CGAffineTransformIdentity; } completion:^(BOOL finished) { // 3秒后自动隐藏 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [UIView animateWithDuration:0.3 animations:^{ notificationView.alpha = 0; notificationView.transform = CGAffineTransformMakeTranslation(0, -20); } completion:^(BOOL finished) { [notificationView removeFromSuperview]; }]; }); }]; } #pragma mark - Actions - (void)imageButtonTapped { UIImagePickerController *picker = [[UIImagePickerController alloc] init]; picker.delegate = self; picker.sourceType = UIImagePickerControllerSourceTypePhotoLibrary; [self presentViewController:picker animated:YES completion:nil]; } #pragma mark - Notification - (void)handleMessageSent:(NSNotification *)notification { NSString *messageId = notification.userInfo[@"messageId"]; NSNumber *sequence = notification.userInfo[@"sequence"]; TJPChatMessage *chatMessage = self.messageMap[messageId]; if (chatMessage) { NSLog(@"[TJPChatViewController] ✅ 消息发送成功: %@ (序列:%@)", messageId, sequence); chatMessage.status = TJPChatMessageStatusSent; chatMessage.timestamp = [NSDate date]; [self updateMessageCell:chatMessage]; // 更新状态栏(减少待发送计数) [self updateConnectionStatus]; // 可选:成功反馈 [self playMessageSentSound]; // 从超时队列中移除 [[TJPMessageTimeoutManager sharedManager] removeMessageFromTimeoutCheck:chatMessage]; } } - (void)handleMessageReceived:(NSNotification *)notification { // NSData *data = notification.userInfo[@"data"]; // NSNumber *sequence = notification.userInfo[@"sequence"]; // // // 解析消息内容(这里简化为文本) // NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; // // NSLog(@"[Chat] 📥 收到消息: %@ (序列:%@)", text, sequence); // // // 创建接收消息 // TJPChatMessage *receivedMessage = [self createReceivedMessageWithContent:text]; // [self.messages addObject:receivedMessage]; // [self reloadMessagesAndScrollToBottom]; // // // 可选:新消息提示 // [self playMessageReceivedSound]; // [self updateBadgeCount]; } // 标记消息已读 手动发送已读回执 - (void)markMessageAsRead:(uint32_t)messageSequence { // id session = [self.client getSessionForType:TJPSessionTypeChat]; // if (session && [session respondsToSelector:@selector(sendReadReceiptForMessageSequence:)]) { // [session performSelector:@selector(sendReadReceiptForMessageSequence:) // withObject:@(messageSequence)]; // // NSLog(@"[Chat] 📖 手动发送已读回执,消息序列号: %u", messageSequence); // } } - (void)keyboardWillShow:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; CGRect keyboardFrame = [userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; CGFloat keyboardHeight = keyboardFrame.size.height; [UIView animateWithDuration:duration delay:0 options:(UIViewAnimationOptions)curve animations:^{ self.inputViewBottomConstraint.offset = -keyboardHeight; [self.view layoutIfNeeded]; [self scrollToBottomAnimated:NO]; } completion:nil]; } - (void)keyboardWillHide:(NSNotification *)notification { NSDictionary *userInfo = notification.userInfo; NSTimeInterval duration = [userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; UIViewAnimationCurve curve = [userInfo[UIKeyboardAnimationCurveUserInfoKey] integerValue]; [UIView animateWithDuration:duration delay:0 options:(UIViewAnimationOptions)curve animations:^{ self.inputViewBottomConstraint.offset = 0; [self.view layoutIfNeeded]; } completion:nil]; } - (void)handleMessageStatusUpdated:(NSNotification *)notification { TJPChatMessage *message = notification.object; [self updateMessageCell:message]; } - (void)playMessageSentSound { dispatch_async(dispatch_get_main_queue(), ^{ // 播放发送成功音效 AudioServicesPlaySystemSound(1003); }); } - (void)playMessageReceivedSound { dispatch_async(dispatch_get_main_queue(), ^{ // 播放接收消息音效 AudioServicesPlaySystemSound(1002); }); } - (void)updateMessageCell:(TJPChatMessage *)message { // 找到对应的cell并更新UI NSUInteger index = [self.messages indexOfObject:message]; if (index != NSNotFound) { NSIndexPath *indexPath = [NSIndexPath indexPathForRow:index inSection:0]; dispatch_async(dispatch_get_main_queue(), ^{ [self.messagesTableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone]; }); } } - (void)handleMessageFailed:(NSNotification *)notification { NSString *messageId = notification.userInfo[@"messageId"]; NSError *error = notification.userInfo[@"error"]; TJPChatMessage *chatMessage = self.messageMap[messageId]; if (chatMessage) { NSLog(@"[TJPChatViewController] ❌ 消息发送失败: %@ - %@", messageId, error.localizedDescription); chatMessage.status = TJPChatMessageStatusFailed; // chatMessage.failureReason = error.localizedDescription; [self updateMessageCell:chatMessage]; // 更新状态栏 [self updateConnectionStatus]; // 显示失败提示 [self showTemporaryFailureNotification]; } } - (void)handleMessageRead:(NSNotification *)notification { NSString *messageId = notification.userInfo[@"messageId"]; NSNumber *originalSequence = notification.userInfo[@"originalSequence"]; // 查找对应的聊天消息 TJPChatMessage *chatMessage = self.messageMap[messageId]; if (chatMessage && chatMessage.isFromSelf) { NSLog(@"[TJPChatViewController] 消息已被对方阅读: %@ (序列:%@)", messageId, originalSequence); // 更新消息状态 chatMessage.status = TJPChatMessageStatusRead; chatMessage.readTime = [NSDate date]; // 更新UI dispatch_async(dispatch_get_main_queue(), ^{ [self updateMessageCell:chatMessage]; // 可选:播放已读提示音 AudioServicesPlaySystemSound(1000); }); } } #pragma mark - TJPChatInputViewDelegate - (void)chatInputView:(TJPChatInputView *)inputView didSendText:(NSString *)text { [self sendTextMessage:text]; } - (void)chatInputViewDidTapImageButton:(TJPChatInputView *)inputView { [self imageButtonTapped]; } - (void)chatInputView:(TJPChatInputView *)inputView didChangeHeight:(CGFloat)height { // 更新输入框高度约束 [self.chatInputView mas_updateConstraints:^(MASConstraintMaker *make) { make.height.mas_equalTo(height); }]; [UIView animateWithDuration:0.25 animations:^{ [self.view layoutIfNeeded]; [self scrollToBottomAnimated:NO]; }]; } - (void)chatInputViewDidBeginEditing:(TJPChatInputView *)inputView { // 输入开始时,滚动到底部 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self scrollToBottomAnimated:YES]; }); } #pragma mark - Message Handling - (void)sendTextMessage:(NSString *)text { // 创建聊天消息对象 TJPChatMessage *chatMessage = [self createChatMessageWithContent:text type:TJPChatMessageTypeText image:nil]; chatMessage.status = TJPChatMessageStatusSending; // 添加到消息列表 [self.messages addObject:chatMessage]; [self reloadMessagesAndScrollToBottom]; // 更新状态栏 [self updateConnectionStatus]; // 创建网络消息对象并发送 TJPTextMessage *networkMessage = [[TJPTextMessage alloc] initWithText:text]; // 使用TJPIMClient发送消息 NSString *messageId = [self.client sendMessage:networkMessage throughType:TJPSessionTypeChat encryptType:TJPEncryptTypeCRC32 compressType:TJPCompressTypeZlib completion:^(NSString * msgId, NSError *error) { if (!error) { self.messageMap[msgId] = chatMessage; chatMessage.messageId = msgId; NSLog(@"[TJPChatViewController] 消息已提交: 内容: %@ 消息ID:%@", text, msgId); }else { // 发送失败,立即更新状态 dispatch_async(dispatch_get_main_queue(), ^{ chatMessage.status = TJPChatMessageStatusFailed; [self updateMessageCell:chatMessage]; [self updateConnectionStatus]; // 显示发送失败提示 [self showSendFailureAlert]; }); } }]; [self reloadMessagesAndScrollToBottom]; } - (void)sendImageMessage:(UIImage *)image { // // 创建聊天消息对象 // TJPChatMessage *chatMessage = [self createChatMessageWithContent:@"[图片]" type:TJPChatMessageTypeImage image:image]; // chatMessage.status = TJPChatMessageStatusSending; // // // 添加到消息列表 // [self.messages addObject:chatMessage]; // [self reloadMessagesAndScrollToBottom]; // // // 将图片转换为数据 // NSData *imageData = UIImageJPEGRepresentation(image, 0.8); // // // 发送图片消息(根据你的实际API调整) // uint32_t messageSequence = [self.client sendImageData:imageData throughType:TJPSessionTypeChat]; // // // 跟踪发送中的消息 // if (messageSequence > 0) { // self.sendingMessages[@(messageSequence)] = chatMessage; // } else { // // 发送失败 // chatMessage.status = TJPChatMessageStatusFailed; // [self reloadMessagesAndScrollToBottom]; // [self showSendFailureAlert]; // } } - (void)scrollToBottomAnimated:(BOOL)animated { if (self.messages.count > 0) { NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:self.messages.count - 1 inSection:0]; [self.messagesTableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:animated]; } } #pragma mark - TJPSessionDelegate // === 状态回调 === - (void)session:(id)session didChangeState:(TJPConnectState)state { dispatch_async(dispatch_get_main_queue(), ^{ // 根据连接状态更新UI if ([state isEqualToString:TJPConnectStateConnected]) { [self.statusBarView updateStatus:TJPConnectionStatusConnected]; [self logConnectionMessage:@"🟢 TCP连接已建立"]; [self handleConnectionEstablished]; } else if ([state isEqualToString:TJPConnectStateConnecting]) { [self.statusBarView updateStatus:TJPConnectionStatusConnecting]; [self logConnectionMessage:@"🟠 正在建立连接..."]; } else if ([state isEqualToString:TJPConnectStateDisconnected]) { [self.statusBarView updateStatus:TJPConnectionStatusDisconnected]; [self logConnectionMessage:@"🔴 连接已断开"]; [self handleConnectionLost]; } // 同时更新消息计数 [self.statusBarView updateMessageCount:self.messages.count]; }); } - (void)session:(id)session didDisconnectWithReason:(TJPDisconnectReason)reason { dispatch_async(dispatch_get_main_queue(), ^{ [self logConnectionMessage:[NSString stringWithFormat:@"⚠️ 连接断开,原因: %@", reason]]; [self updateConnectionStatus]; }); } - (void)session:(id)session didFailWithError:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ [self logConnectionMessage:[NSString stringWithFormat:@"❌ 连接失败: %@", error.localizedDescription]]; [self updateConnectionStatus]; }); } - (void)sessionDidForceDisconnect:(id)session { dispatch_async(dispatch_get_main_queue(), ^{ [self logConnectionMessage:@"⚠️ 连接被强制断开"]; [self updateConnectionStatus]; }); } // === 内容回调 === - (void)session:(id)session didReceiveText:(NSString *)text { dispatch_async(dispatch_get_main_queue(), ^{ [self handleReceivedTextMessage:text]; }); } - (void)session:(id)session didReceiveImage:(UIImage *)image { dispatch_async(dispatch_get_main_queue(), ^{ [self handleReceivedImageMessage:image]; }); } - (void)session:(id)session didReceiveAudio:(NSData *)audioData { dispatch_async(dispatch_get_main_queue(), ^{ [self handleReceivedAudioMessage:audioData]; }); } - (void)session:(id)session didReceiveVideo:(NSData *)videoData { dispatch_async(dispatch_get_main_queue(), ^{ [self handleReceivedVideoMessage:videoData]; }); } - (void)session:(id)session didReceiveFile:(NSData *)fileData filename:(NSString *)filename { dispatch_async(dispatch_get_main_queue(), ^{ [self handleReceivedFileMessage:fileData filename:filename]; }); } - (void)session:(id)session didReceiveLocation:(CLLocation *)location { dispatch_async(dispatch_get_main_queue(), ^{ [self handleReceivedLocationMessage:location]; }); } - (void)session:(id)session didReceiveCustomData:(NSData *)data withType:(uint16_t)customType { dispatch_async(dispatch_get_main_queue(), ^{ [self handleReceivedCustomMessage:data customType:customType]; }); } // 发送消息失败 - (void)session:(id)session didFailToSendMessageWithSequence:(uint32_t)sequence error:(NSError *)error { dispatch_async(dispatch_get_main_queue(), ^{ NSLog(@"[TJPChatViewController] 消息发送失败,序列号: %u, 错误: %@", sequence, error.localizedDescription); [self showSendFailureAlert]; // 可以在这里找到对应的消息并更新状态为失败 // 由于简化了消息跟踪,这里暂时只显示提示 }); } // 版本协商完成 - (void)session:(id)session didCompleteVersionNegotiation:(uint16_t)version features:(uint16_t)features { dispatch_async(dispatch_get_main_queue(), ^{ [self logConnectionMessage:[NSString stringWithFormat:@"🤝 协议协商完成 - 版本: %d, 特性: %d", version, features]]; }); } // 原始数据回调 - (void)session:(id)session didReceiveRawData:(NSData *)data { // 通常不需要处理原始数据,除非有特殊需求 } #pragma mark - Message Handling Helpers - (void)handleReceivedTextMessage:(NSString *)text { TJPChatMessage *chatMessage = [self createReceivedMessageWithContent:text type:TJPChatMessageTypeText image:nil]; [self.messages addObject:chatMessage]; [self reloadMessagesAndScrollToBottom]; } - (void)handleReceivedImageMessage:(UIImage *)image { TJPChatMessage *chatMessage = [self createReceivedMessageWithContent:@"[图片]" type:TJPChatMessageTypeImage image:image]; [self.messages addObject:chatMessage]; [self reloadMessagesAndScrollToBottom]; } - (void)handleReceivedAudioMessage:(NSData *)audioData { // 处理音频消息 TJPChatMessage *chatMessage = [self createReceivedMessageWithContent:@"[语音消息]" type:TJPChatMessageTypeAudio image:nil]; [self.messages addObject:chatMessage]; [self reloadMessagesAndScrollToBottom]; } - (void)handleReceivedVideoMessage:(NSData *)videoData { // 处理视频消息 TJPChatMessage *chatMessage = [self createReceivedMessageWithContent:@"[视频消息]" type:TJPChatMessageTypeVideo image:nil]; [self.messages addObject:chatMessage]; [self reloadMessagesAndScrollToBottom]; } - (void)handleReceivedFileMessage:(NSData *)fileData filename:(NSString *)filename { // 处理文件消息 NSString *content = [NSString stringWithFormat:@"[文件: %@]", filename]; TJPChatMessage *chatMessage = [self createReceivedMessageWithContent:content type:TJPChatMessageTypeFile image:nil]; [self.messages addObject:chatMessage]; [self reloadMessagesAndScrollToBottom]; } - (void)handleReceivedLocationMessage:(CLLocation *)location { // 处理位置消息 NSString *content = [NSString stringWithFormat:@"[位置: %.6f, %.6f]", location.coordinate.latitude, location.coordinate.longitude]; TJPChatMessage *chatMessage = [self createReceivedMessageWithContent:content type:TJPChatMessageTypeText image:nil]; [self.messages addObject:chatMessage]; [self reloadMessagesAndScrollToBottom]; } - (void)handleReceivedCustomMessage:(NSData *)data customType:(uint16_t)customType { // 处理自定义消息 NSString *content = [NSString stringWithFormat:@"[自定义消息 类型:%d]", customType]; TJPChatMessage *chatMessage = [self createReceivedMessageWithContent:content type:TJPChatMessageTypeText image:nil]; [self.messages addObject:chatMessage]; [self reloadMessagesAndScrollToBottom]; } - (TJPChatMessage *)createReceivedMessageWithContent:(NSString *)content type:(TJPChatMessageType)type image:(UIImage *)image { TJPChatMessage *message = [[TJPChatMessage alloc] init]; message.messageId = [NSString stringWithFormat:@"received_%ld", (long)self.messageIdCounter++]; message.content = content; message.isFromSelf = NO; message.timestamp = [NSDate date]; message.messageType = type; message.image = image; message.status = TJPChatMessageStatusSent; return message; } - (void)logConnectionMessage:(NSString *)message { // 可以在这里添加连接日志显示逻辑 NSLog(@"连接状态: %@", message); } - (void)handleConnectionEstablished { // 连接建立后的处理逻辑 NSLog(@"[TJPChatViewController] TCP连接已建立,可以正常发送消息"); } - (void)handleConnectionLost { // 连接丢失后的处理逻辑 NSLog(@"[TJPChatViewController] TCP连接丢失"); } - (TJPChatMessage *)createChatMessageWithContent:(NSString *)content type:(TJPChatMessageType)type image:(UIImage *)image { TJPChatMessage *message = [[TJPChatMessage alloc] init]; message.messageId = [NSString stringWithFormat:@"msg_%ld", (long)self.messageIdCounter++]; message.content = content; message.isFromSelf = YES; message.timestamp = [NSDate date]; message.messageType = type; message.image = image; return message; } - (void)reloadMessagesAndScrollToBottom { [self.messagesTableView reloadData]; [self scrollToBottomAnimated:YES]; } - (void)showSendFailureAlert { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"发送失败" message:@"消息发送失败,请检查网络连接" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; } #pragma mark - UITableViewDataSource - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { return self.messages.count; } - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { TJPChatMessageCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ChatMessageCell" forIndexPath:indexPath]; cell.delegate = self; TJPChatMessage *message = self.messages[indexPath.row]; [cell configureWithMessage:message]; return cell; } #pragma mark - UITableViewDelegate - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { TJPChatMessage *message = self.messages[indexPath.row]; return [TJPChatMessageCell heightForMessage:message inWidth:tableView.frame.size.width]; } #pragma mark - UIImagePickerControllerDelegate - (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info { UIImage *selectedImage = info[UIImagePickerControllerOriginalImage]; [picker dismissViewControllerAnimated:YES completion:^{ [self sendImageMessage:selectedImage]; }]; } - (void)imagePickerControllerDidCancel:(UIImagePickerController *)picker { [picker dismissViewControllerAnimated:YES completion:nil]; } #pragma mark - TJPChatMessageCellDelegate (新增) - (void)chatMessageCell:(TJPChatMessageCell *)cell didRequestRetryForMessage:(TJPChatMessage *)message { NSLog(@"[TJPChatViewController] 用户请求重试消息: %@", message.content); // 确认是失败的消息 if (message.status != TJPChatMessageStatusFailed) { NSLog(@"[TJPChatViewController] 消息状态不是失败状态,无法重试"); return; } // 重置消息状态为发送中 message.status = TJPChatMessageStatusSending; // 更新Cell显示 [self updateMessageCell:message]; // 更新状态栏(减少失败消息计数) [self updateConnectionStatus]; // 根据消息类型重新发送 if (message.messageType == TJPChatMessageTypeText) { [self retryTextMessage:message]; } else if (message.messageType == TJPChatMessageTypeImage) { // [self retryImageMessage:message]; } } - (void)retryTextMessage:(TJPChatMessage *)message { // 创建网络消息对象 TJPTextMessage *networkMessage = [[TJPTextMessage alloc] initWithText:message.content]; // 发送消息 NSString *messageId = [self.client sendMessage:networkMessage throughType:TJPSessionTypeChat encryptType:TJPEncryptTypeCRC32 compressType:TJPCompressTypeZlib completion:^(NSString *msgId, NSError *error) { if (!error) { // 更新消息映射 if (message.messageId && self.messageMap[message.messageId]) { [self.messageMap removeObjectForKey:message.messageId]; } self.messageMap[msgId] = message; message.messageId = msgId; NSLog(@"[TJPChatViewController] ✅ 重试消息已提交: %@", msgId); } else { // 重试也失败了 dispatch_async(dispatch_get_main_queue(), ^{ message.status = TJPChatMessageStatusFailed; [self updateMessageCell:message]; [self updateConnectionStatus]; // 显示重试失败提示 [self showRetryFailureAlert]; }); } }]; } - (void)showRetryFailureAlert { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"重试失败" message:@"消息重试发送失败,请检查网络连接后再试" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; } - (void)showSendFailureAlertWithRetryOption { UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"发送失败" message:@"消息发送失败,你可以点击消息旁的重试按钮重新发送" preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleDefault handler:nil]]; [self presentViewController:alert animated:YES completion:nil]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/Utility/TJPChatMessageDefine.h ================================================ // // TJPChatMessageDefine.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // #ifndef TJPChatMessageDefine_h #define TJPChatMessageDefine_h //************************************************************************ // Chat相关,后期增加防腐层后抽离出此文件 typedef NS_ENUM(NSUInteger, TJPChatMessageType) { TJPChatMessageTypeText = 0, // 文本消息 TJPChatMessageTypeImage = 1, // 图片消息 TJPChatMessageTypeAudio = 2, // 语音消息 TJPChatMessageTypeVideo = 3, // 视频消息 TJPChatMessageTypeFile = 4, // 文件消息 TJPChatMessageTypeLocation = 5, // 位置消息 }; typedef NS_ENUM(NSUInteger, TJPChatMessageStatus) { TJPChatMessageStatusNone = 0, TJPChatMessageStatusSending = 1, // 发送中 TJPChatMessageStatusSent = 2, // 已发送 TJPChatMessageStatusDelivered = 3, // 已送达 TJPChatMessageStatusRead = 4, // 已读 TJPChatMessageStatusFailed = 5, // 发送失败 }; #endif /* TJPChatMessageDefine_h */ ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/View/TJPChatInputView.h ================================================ // // TJPChatInputView.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/25. // #import NS_ASSUME_NONNULL_BEGIN @class TJPChatInputView; @protocol TJPChatInputViewDelegate @required - (void)chatInputView:(TJPChatInputView *)inputView didSendText:(NSString *)text; - (void)chatInputViewDidTapImageButton:(TJPChatInputView *)inputView; @optional - (void)chatInputView:(TJPChatInputView *)inputView didChangeHeight:(CGFloat)height; - (void)chatInputView:(TJPChatInputView *)inputView didChangeText:(NSString *)text; - (void)chatInputViewDidBeginEditing:(TJPChatInputView *)inputView; - (void)chatInputViewDidEndEditing:(TJPChatInputView *)inputView; @end @interface TJPChatInputView : UIView @property (nonatomic, weak) id delegate; // 配置属性 @property (nonatomic, assign) CGFloat maxHeight; // 最大高度,默认120 @property (nonatomic, assign) CGFloat minHeight; // 最小高度,默认50 @property (nonatomic, assign) BOOL enabled; // 是否启用,默认YES @property (nonatomic, assign, readonly) CGFloat currentHeight; // 当前高度 @property (nonatomic, assign, readonly) BOOL isEditing; // 是否正在编辑 // 公开方法 - (void)setText:(NSString *)text; - (NSString *)text; - (void)clearText; - (void)resignFirstResponder; - (void)becomeFirstResponder; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/View/TJPChatInputView.m ================================================ // // TJPChatInputView.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/25. // #import "TJPChatInputView.h" #import @interface TJPChatInputView () @property (nonatomic, strong) UIView *containerView; @property (nonatomic, strong) UIButton *imageButton; @property (nonatomic, strong) UITextView *textView; @property (nonatomic, strong) UIButton *sendButton; @property (nonatomic, strong) UIView *separatorLine; @property (nonatomic, assign) CGFloat currentHeight; @end @implementation TJPChatInputView #pragma mark - Initialization - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupDefaultValues]; [self setupUI]; [self setupConstraints]; // [self registerNotifications]; } return self; } - (void)dealloc { // [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - UI - (void)setupDefaultValues { _maxHeight = 120.0; _minHeight = 52.0; _enabled = YES; _currentHeight = _minHeight; } - (void)setupUI { self.backgroundColor = [UIColor systemGray6Color]; // 分割线 self.separatorLine = [[UIView alloc] init]; self.separatorLine.backgroundColor = [[UIColor separatorColor] colorWithAlphaComponent:0.3]; [self addSubview:self.separatorLine]; // 容器视图 self.containerView = [[UIView alloc] init]; [self addSubview:self.containerView]; // 图片按钮 self.imageButton = [UIButton buttonWithType:UIButtonTypeSystem]; [self.imageButton setImage:[UIImage imageNamed:@"img_chat_bar_camera"] forState:UIControlStateNormal]; self.imageButton.titleLabel.font = [UIFont systemFontOfSize:20]; [self.imageButton addTarget:self action:@selector(imageButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; [self.containerView addSubview:self.imageButton]; // 文本输入框 self.textView = [[UITextView alloc] init]; self.textView.delegate = self; self.textView.font = [UIFont systemFontOfSize:16]; self.textView.layer.cornerRadius = 6; self.textView.layer.borderWidth = 0; // self.textView.layer.borderColor = [UIColor systemGray4Color].CGColor; self.textView.backgroundColor = [UIColor whiteColor]; self.textView.textContainerInset = UIEdgeInsetsMake(8, 12, 8, 12); self.textView.scrollIndicatorInsets = self.textView.textContainerInset; self.textView.returnKeyType = UIReturnKeySend; self.textView.enablesReturnKeyAutomatically = YES; self.textView.showsVerticalScrollIndicator = NO; [self.containerView addSubview:self.textView]; // 发送按钮 self.sendButton = [UIButton buttonWithType:UIButtonTypeSystem]; [self.sendButton setTitle:@"发送" forState:UIControlStateNormal]; [self.sendButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal]; [self.sendButton setTitleColor:[UIColor systemGray3Color] forState:UIControlStateDisabled]; self.sendButton.layer.cornerRadius = 8; self.sendButton.backgroundColor = [UIColor colorWithWhite:0 alpha:0.2]; self.sendButton.titleLabel.font = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium]; [self.sendButton addTarget:self action:@selector(sendButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; [self.containerView addSubview:self.sendButton]; [self updateSendButtonState]; } - (void)setupConstraints { // 分割线 [self.separatorLine mas_makeConstraints:^(MASConstraintMaker *make) { make.top.leading.trailing.equalTo(self); make.height.mas_equalTo(0.5); }]; // 容器视图 [self.containerView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.equalTo(self.separatorLine.mas_bottom).offset(8); make.leading.equalTo(self).offset(12); make.trailing.equalTo(self).offset(-12); make.bottom.equalTo(self).offset(-8); }]; // 图片按钮 [self.imageButton mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.equalTo(self.containerView); make.bottom.equalTo(self.containerView).offset(-2); make.size.mas_equalTo(CGSizeMake(36, 36)); }]; // 发送按钮 [self.sendButton mas_makeConstraints:^(MASConstraintMaker *make) { make.trailing.equalTo(self.containerView); make.bottom.equalTo(self.containerView).offset(-2); make.size.mas_equalTo(CGSizeMake(58, 32)); }]; // 文本输入框 - 关键修复:给一个最小高度约束 [self.textView mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.equalTo(self.imageButton.mas_trailing).offset(10); make.trailing.equalTo(self.sendButton.mas_leading).offset(-10); make.top.bottom.equalTo(self.containerView); make.height.mas_greaterThanOrEqualTo(36).priorityMedium(); }]; } #pragma mark - Actions - (void)imageButtonTapped:(UIButton *)sender { if ([self.delegate respondsToSelector:@selector(chatInputViewDidTapImageButton:)]) { [self.delegate chatInputViewDidTapImageButton:self]; } } - (void)sendButtonTapped:(UIButton *)sender { [self sendCurrentText]; } - (void)sendCurrentText { NSString *text = [self.textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; if (text.length > 0) { if ([self.delegate respondsToSelector:@selector(chatInputView:didSendText:)]) { [self.delegate chatInputView:self didSendText:text]; } [self clearText]; } } #pragma mark - Public Methods - (void)setText:(NSString *)text { self.textView.text = text ?: @""; [self textViewDidChange:self.textView]; } - (NSString *)text { return self.textView.text ?: @""; } - (void)clearText { [self setText:@""]; } - (void)resignFirstResponder { [self.textView resignFirstResponder]; } - (void)becomeFirstResponder { [self.textView becomeFirstResponder]; } - (BOOL)isEditing { return self.textView.isFirstResponder; } - (void)setEnabled:(BOOL)enabled { _enabled = enabled; self.textView.editable = enabled; self.imageButton.enabled = enabled; [self updateSendButtonState]; } #pragma mark - Private Methods - (void)updateSendButtonState { NSString *text = [self.textView.text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; BOOL hasText = text.length > 0; self.sendButton.enabled = self.enabled && hasText; self.sendButton.backgroundColor = (self.sendButton.enabled) ? [UIColor systemBlueColor] : [UIColor colorWithWhite:0 alpha:0.2]; } - (void)updateHeightWithTextView:(UITextView *)textView { // 计算文本需要的高度 CGSize textSize = [textView sizeThatFits:CGSizeMake(textView.frame.size.width, CGFLOAT_MAX)]; CGFloat textHeight = textSize.height; // 计算新的容器高度 textHeight = MAX(36, textHeight); // 计算整个输入框需要的总高度:分割线0.5 + 上边距8 + 文本高度 + 下边距8 CGFloat newTotalHeight = 0.5 + 8 + textHeight + 8; // 限制在最小和最大高度之间 newTotalHeight = MAX(self.minHeight, MIN(self.maxHeight, newTotalHeight)); if (fabs(newTotalHeight - self.currentHeight) > 1.0) { _currentHeight = newTotalHeight; // 通知代理高度变化 if ([self.delegate respondsToSelector:@selector(chatInputView:didChangeHeight:)]) { [self.delegate chatInputView:self didChangeHeight:newTotalHeight]; } } // 启用或禁用滚动 CGFloat maxTextHeight = self.maxHeight - 16.5; // 减去分割线和边距 textView.scrollEnabled = (textHeight > maxTextHeight); } #pragma mark - UITextViewDelegate - (void)textViewDidBeginEditing:(UITextView *)textView { if ([self.delegate respondsToSelector:@selector(chatInputViewDidBeginEditing:)]) { [self.delegate chatInputViewDidBeginEditing:self]; } } - (void)textViewDidEndEditing:(UITextView *)textView { if ([self.delegate respondsToSelector:@selector(chatInputViewDidEndEditing:)]) { [self.delegate chatInputViewDidEndEditing:self]; } } - (void)textViewDidChange:(UITextView *)textView { [self updateSendButtonState]; [self updateHeightWithTextView:textView]; if ([self.delegate respondsToSelector:@selector(chatInputView:didChangeText:)]) { [self.delegate chatInputView:self didChangeText:textView.text]; } } - (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text { // 处理发送按钮逻辑 if ([text isEqualToString:@"\n"]) { [self sendCurrentText]; return NO; } return YES; } @end ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/View/TJPChatMessageCell.h ================================================ // // TJPChatMessageCell.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // #import NS_ASSUME_NONNULL_BEGIN @class TJPChatMessage, TJPMessageStatusIndicator; @protocol TJPChatMessageCellDelegate @optional - (void)chatMessageCell:(id)cell didRequestRetryForMessage:(TJPChatMessage *)message; @end @interface TJPChatMessageCell : UITableViewCell @property (nonatomic, weak) id delegate; @property (nonatomic, strong) TJPChatMessage *chatMessage; @property (nonatomic, strong) UIView *bubbleView; @property (nonatomic, strong) UILabel *messageLabel; @property (nonatomic, strong) UIImageView *messageImageView; @property (nonatomic, strong) UILabel *timeLabel; @property (nonatomic, strong) TJPMessageStatusIndicator *statusIndicator; - (void)configureWithMessage:(TJPChatMessage *)message; + (CGFloat)heightForMessage:(TJPChatMessage *)message inWidth:(CGFloat)width; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/View/TJPChatMessageCell.m ================================================ // // TJPChatMessageCell.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/23. // #import "TJPChatMessageCell.h" #import "TJPChatMessage.h" #import "TJPMessageStatusIndicator.h" #import "TJPMessageTimeoutManager.h" @interface TJPChatMessageCell () @end @implementation TJPChatMessageCell - (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { if (self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]) { [self setupUI]; } return self; } - (void)setupUI { self.selectionStyle = UITableViewCellSelectionStyleNone; self.backgroundColor = [UIColor clearColor]; // 气泡背景 self.bubbleView = [[UIView alloc] init]; self.bubbleView.layer.cornerRadius = 12; [self.contentView addSubview:self.bubbleView]; // 消息文本 self.messageLabel = [[UILabel alloc] init]; self.messageLabel.numberOfLines = 0; self.messageLabel.font = [UIFont systemFontOfSize:16]; [self.bubbleView addSubview:self.messageLabel]; // 消息图片 self.messageImageView = [[UIImageView alloc] init]; self.messageImageView.contentMode = UIViewContentModeScaleAspectFill; self.messageImageView.clipsToBounds = YES; self.messageImageView.layer.cornerRadius = 8; self.messageImageView.hidden = YES; [self.bubbleView addSubview:self.messageImageView]; // 时间标签 self.timeLabel = [[UILabel alloc] init]; self.timeLabel.font = [UIFont systemFontOfSize:12]; self.timeLabel.textColor = [UIColor grayColor]; self.timeLabel.textAlignment = NSTextAlignmentCenter; [self.contentView addSubview:self.timeLabel]; // 加载指示器 self.statusIndicator = [[TJPMessageStatusIndicator alloc] init]; self.statusIndicator.delegate = self; self.statusIndicator.hidden = YES; // 默认隐藏,只对自己的消息显示 [self.contentView addSubview:self.statusIndicator]; } - (void)configureWithMessage:(TJPChatMessage *)message { self.chatMessage = message; // 时间格式化 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; formatter.dateFormat = @"HH:mm"; self.timeLabel.text = [formatter stringFromDate:message.timestamp]; // 根据消息类型配置UI if (message.messageType == TJPChatMessageTypeText) { self.messageLabel.text = message.content; self.messageLabel.hidden = NO; self.messageImageView.hidden = YES; } else if (message.messageType == TJPChatMessageTypeImage) { self.messageImageView.image = message.image; self.messageImageView.hidden = NO; self.messageLabel.hidden = YES; } // 根据发送方设置样式 if (message.isFromSelf) { // 自己发送的消息 - 右侧,蓝色 self.bubbleView.backgroundColor = [UIColor systemBlueColor]; self.messageLabel.textColor = [UIColor whiteColor]; // 配置状态指示器 [self configureStatusIndicatorForMessage:message]; } else { // 接收的消息 - 左侧,灰色 self.bubbleView.backgroundColor = [UIColor systemGray5Color]; self.messageLabel.textColor = [UIColor blackColor]; self.statusIndicator.hidden = YES; } [self setNeedsLayout]; } - (void)configureStatusIndicatorForMessage:(TJPChatMessage *)message { self.statusIndicator.hidden = NO; self.statusIndicator.messageId = message.messageId; // 根据消息状态配置指示器 TJPMessageIndicatorStatus indicatorStatus; BOOL animated = YES; switch (message.status) { case TJPChatMessageStatusSending: indicatorStatus = TJPMessageIndicatorStatusSending; break; case TJPChatMessageStatusSent: indicatorStatus = TJPMessageIndicatorStatusSent; break; case TJPChatMessageStatusFailed: indicatorStatus = TJPMessageIndicatorStatusFailed; break; case TJPChatMessageStatusRead: indicatorStatus = TJPMessageIndicatorStatusRead; break; default: indicatorStatus = TJPMessageIndicatorStatusSending; animated = NO; break; } [self.statusIndicator updateStatus:indicatorStatus animated:animated]; // 启动超时检查 if (message.status == TJPChatMessageStatusSending) { [[TJPMessageTimeoutManager sharedManager] addMessageForTimeoutCheck:message]; } } - (void)layoutSubviews { [super layoutSubviews]; CGFloat margin = 15; CGFloat bubbleMaxWidth = self.contentView.frame.size.width * 0.7; // 时间标签 self.timeLabel.frame = CGRectMake(0, 5, self.contentView.frame.size.width, 20); CGSize messageSize = CGSizeZero; if (!self.messageLabel.hidden) { messageSize = [self.messageLabel.text boundingRectWithSize:CGSizeMake(bubbleMaxWidth - 20, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: self.messageLabel.font} context:nil].size; messageSize.width = MIN(messageSize.width + 20, bubbleMaxWidth); messageSize.height += 20; } else if (!self.messageImageView.hidden) { messageSize = CGSizeMake(150, 150); // 固定图片大小 } // 气泡位置 CGFloat bubbleY = CGRectGetMaxY(self.timeLabel.frame) + 5; if (self.chatMessage.isFromSelf) { self.bubbleView.frame = CGRectMake(self.contentView.frame.size.width - messageSize.width - margin, bubbleY, messageSize.width, messageSize.height); // 状态指示器位置(在气泡右侧) if (!self.statusIndicator.hidden) { CGSize indicatorSize = [TJPMessageStatusIndicator indicatorSize]; self.statusIndicator.frame = CGRectMake(CGRectGetMinX(self.bubbleView.frame) - indicatorSize.width - 8, CGRectGetMaxY(self.bubbleView.frame) - indicatorSize.height - 5, indicatorSize.width, indicatorSize.height); } } else { // 左侧 self.bubbleView.frame = CGRectMake(margin, bubbleY, messageSize.width, messageSize.height); } // 内容位置 if (!self.messageLabel.hidden) { self.messageLabel.frame = CGRectMake(10, 10, messageSize.width - 20, messageSize.height - 20); } if (!self.messageImageView.hidden) { self.messageImageView.frame = CGRectMake(10, 10, messageSize.width - 20, messageSize.height - 20); } } + (CGFloat)heightForMessage:(TJPChatMessage *)message inWidth:(CGFloat)width { CGFloat bubbleMaxWidth = width * 0.7; CGFloat height = 30; // 时间标签 + 间距 if (message.messageType == TJPChatMessageTypeText) { CGSize messageSize = [message.content boundingRectWithSize:CGSizeMake(bubbleMaxWidth - 20, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName: [UIFont systemFontOfSize:16]} context:nil].size; height += messageSize.height + 30; // 文本高度 + 气泡内边距 } else if (message.messageType == TJPChatMessageTypeImage) { height += 170; // 固定图片高度 + 气泡内边距 } return height + 15; // 底部间距 } #pragma mark - TJPMessageStatusIndicatorDelegate - (void)messageStatusIndicatorDidTapRetry:(TJPMessageStatusIndicator *)sender { if (self.chatMessage && self.chatMessage.status == TJPChatMessageStatusFailed && [self.delegate respondsToSelector:@selector(chatMessageCell:didRequestRetryForMessage:)]) { [self.delegate chatMessageCell:self didRequestRetryForMessage:self.chatMessage]; } } #pragma mark - Reuse - (void)prepareForReuse { [super prepareForReuse]; // 重置状态指示器 self.statusIndicator.hidden = YES; [self.statusIndicator stopSendingAnimation]; // 重置其他UI状态 self.messageLabel.hidden = NO; self.messageImageView.hidden = YES; [[TJPMessageTimeoutManager sharedManager] removeMessageFromTimeoutCheck:self.chatMessage]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/View/TJPConnectionStatusView.h ================================================ // // TJPConnectionStatusView.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/26. // #import NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, TJPConnectionStatus) { TJPConnectionStatusDisconnected, // 断开连接 TJPConnectionStatusConnecting, // 连接中 TJPConnectionStatusConnected, // 已连接 TJPConnectionStatusReconnecting // 重连中 }; @interface TJPConnectionStatusView : UIView @property (nonatomic, assign) TJPConnectionStatus status; @property (nonatomic, assign) NSInteger messageCount; // 消息计数 @property (nonatomic, assign) NSInteger pendingCount; // 待发送消息数 - (void)updateStatus:(TJPConnectionStatus)status; - (void)updateMessageCount:(NSInteger)count; - (void)updatePendingCount:(NSInteger)count; // 动画效果 - (void)startPulseAnimation; - (void)stopPulseAnimation; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/View/TJPConnectionStatusView.m ================================================ // // TJPConnectionStatusView.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/26. // #import "TJPConnectionStatusView.h" #import @interface TJPConnectionStatusView () @property (nonatomic, strong) UIView *statusIndicator; // 状态指示圆点 @property (nonatomic, strong) UILabel *statusLabel; // 状态文字 @property (nonatomic, strong) UILabel *messageCountLabel; // 消息计数 @property (nonatomic, strong) UIView *separatorLine; // 分割线 @property (nonatomic, strong) UIStackView *contentStack; // 内容容器 @property (nonatomic, strong) CAShapeLayer *pulseLayer; // 脉搏动画层 @property (nonatomic, strong) NSTimer *reconnectTimer; // 重连动画定时器 @end @implementation TJPConnectionStatusView - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupUI]; [self setupConstraints]; [self updateStatus:TJPConnectionStatusDisconnected]; } return self; } - (void)dealloc { [self.reconnectTimer invalidate]; } #pragma mark - UI Setup - (void)setupUI { // 背景设置 if (@available(iOS 13.0, *)) { self.backgroundColor = [UIColor secondarySystemBackgroundColor]; } else { self.backgroundColor = [UIColor colorWithRed:0.98 green:0.98 blue:0.98 alpha:1.0]; } // 主要内容容器 self.contentStack = [[UIStackView alloc] init]; self.contentStack.axis = UILayoutConstraintAxisHorizontal; self.contentStack.alignment = UIStackViewAlignmentCenter; self.contentStack.spacing = 8; [self addSubview:self.contentStack]; // 状态指示器 - 圆点 self.statusIndicator = [[UIView alloc] init]; self.statusIndicator.layer.cornerRadius = 4; self.statusIndicator.backgroundColor = [UIColor systemGrayColor]; [self.contentStack addArrangedSubview:self.statusIndicator]; // 状态文字 self.statusLabel = [[UILabel alloc] init]; self.statusLabel.font = [UIFont systemFontOfSize:14 weight:UIFontWeightMedium]; self.statusLabel.textColor = [UIColor labelColor]; self.statusLabel.text = @"连接状态"; [self.contentStack addArrangedSubview:self.statusLabel]; // 弹性空间 UIView *spacer = [[UIView alloc] init]; [spacer setContentHuggingPriority:UILayoutPriorityDefaultLow forAxis:UILayoutConstraintAxisHorizontal]; [self.contentStack addArrangedSubview:spacer]; // 消息计数标签 self.messageCountLabel = [[UILabel alloc] init]; self.messageCountLabel.font = [UIFont monospacedDigitSystemFontOfSize:12 weight:UIFontWeightRegular]; self.messageCountLabel.textColor = [UIColor secondaryLabelColor]; self.messageCountLabel.text = @"0 条消息"; [self.contentStack addArrangedSubview:self.messageCountLabel]; // 底部分割线 self.separatorLine = [[UIView alloc] init]; self.separatorLine.backgroundColor = [[UIColor separatorColor] colorWithAlphaComponent:0.3]; [self addSubview:self.separatorLine]; // 设置脉搏动画层 [self setupPulseLayer]; } - (void)setupConstraints { // 主容器约束 [self.contentStack mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.equalTo(self).offset(16); make.trailing.equalTo(self).offset(-16); make.centerY.equalTo(self); }]; // 状态指示器约束 [self.statusIndicator mas_makeConstraints:^(MASConstraintMaker *make) { make.size.mas_equalTo(CGSizeMake(8, 8)); }]; // 分割线约束 [self.separatorLine mas_makeConstraints:^(MASConstraintMaker *make) { make.leading.trailing.bottom.equalTo(self); make.height.mas_equalTo(0.5); }]; } - (void)setupPulseLayer { self.pulseLayer = [CAShapeLayer layer]; self.pulseLayer.frame = CGRectMake(0, 0, 16, 16); self.pulseLayer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 16, 16)].CGPath; self.pulseLayer.fillColor = [UIColor clearColor].CGColor; self.pulseLayer.strokeColor = [UIColor systemBlueColor].CGColor; self.pulseLayer.lineWidth = 1.0; self.pulseLayer.opacity = 0.8; } #pragma mark - Public Methods - (void)updateStatus:(TJPConnectionStatus)status { _status = status; [UIView animateWithDuration:0.3 delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{ [self configureForStatus:status]; } completion:^(BOOL finished) { [self handleStatusAnimations:status]; }]; } - (void)updateMessageCount:(NSInteger)count { _messageCount = count; NSString *text; if (count == 0) { text = @"暂无消息"; } else if (count == 1) { text = @"1 条消息"; } else { text = [NSString stringWithFormat:@"%ld 条消息", (long)count]; } self.messageCountLabel.text = text; // 数字变化动画 // [self animateCounterChange]; } - (void)updatePendingCount:(NSInteger)count { _pendingCount = count; if (count > 0) { NSString *pendingText = [NSString stringWithFormat:@"(%ld 待发送)", (long)count]; self.messageCountLabel.text = [self.messageCountLabel.text stringByAppendingString:pendingText]; // 待发送消息有问题时,文字变红提醒 self.messageCountLabel.textColor = [UIColor systemOrangeColor]; }else { self.messageCountLabel.textColor = [UIColor secondaryLabelColor]; } } - (void)startPulseAnimation { if (self.pulseLayer.superlayer) return; [self.statusIndicator.layer addSublayer:self.pulseLayer]; self.pulseLayer.position = CGPointMake(4, 4); CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:@"transform.scale"]; scaleAnimation.fromValue = @0.5; scaleAnimation.toValue = @2.0; scaleAnimation.duration = 1.0; scaleAnimation.repeatCount = INFINITY; scaleAnimation.autoreverses = NO; CABasicAnimation *opacityAnimation = [CABasicAnimation animationWithKeyPath:@"opacity"]; opacityAnimation.fromValue = @0.8; opacityAnimation.toValue = @0.0; opacityAnimation.duration = 1.0; opacityAnimation.repeatCount = INFINITY; opacityAnimation.autoreverses = NO; CAAnimationGroup *group = [CAAnimationGroup animation]; group.animations = @[scaleAnimation, opacityAnimation]; group.duration = 1.0; group.repeatCount = INFINITY; [self.pulseLayer addAnimation:group forKey:@"pulse"]; } - (void)stopPulseAnimation { [self.pulseLayer removeAllAnimations]; [self.pulseLayer removeFromSuperlayer]; } #pragma mark - Private Methods - (void)configureForStatus:(TJPConnectionStatus)status { switch (status) { case TJPConnectionStatusDisconnected: self.statusIndicator.backgroundColor = [UIColor systemRedColor]; self.statusLabel.text = @"连接断开"; self.statusLabel.textColor = [UIColor systemRedColor]; break; case TJPConnectionStatusConnecting: self.statusIndicator.backgroundColor = [UIColor systemOrangeColor]; self.statusLabel.text = @"连接中..."; self.statusLabel.textColor = [UIColor systemOrangeColor]; break; case TJPConnectionStatusConnected: self.statusIndicator.backgroundColor = [UIColor systemGreenColor]; self.statusLabel.text = @"连接正常"; self.statusLabel.textColor = [UIColor systemGreenColor]; break; case TJPConnectionStatusReconnecting: self.statusIndicator.backgroundColor = [UIColor systemBlueColor]; self.statusLabel.text = @"重连中..."; self.statusLabel.textColor = [UIColor systemBlueColor]; break; } } - (void)handleStatusAnimations:(TJPConnectionStatus)status { // 停止所有动画 [self stopPulseAnimation]; [self.reconnectTimer invalidate]; switch (status) { case TJPConnectionStatusConnecting: case TJPConnectionStatusReconnecting: [self startPulseAnimation]; [self startReconnectAnimation]; break; case TJPConnectionStatusConnected: [self animateSuccessfulConnection]; break; case TJPConnectionStatusDisconnected: [self animateDisconnection]; break; } } - (void)startReconnectAnimation { // 文字闪烁效果 self.reconnectTimer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(toggleTextOpacity) userInfo:nil repeats:YES]; } - (void)toggleTextOpacity { [UIView animateWithDuration:0.3 animations:^{ self.statusLabel.alpha = self.statusLabel.alpha > 0.5 ? 0.5 : 1.0; }]; } - (void)animateSuccessfulConnection { // 成功连接的缩放动画 self.statusLabel.alpha = 1.0; self.statusIndicator.transform = CGAffineTransformMakeScale(0.8, 0.8); [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.6 initialSpringVelocity:0.5 options:0 animations:^{ self.statusIndicator.transform = CGAffineTransformIdentity; } completion:nil]; } - (void)animateDisconnection { // 断开连接的摇摆动画 self.statusLabel.alpha = 1.0; CAKeyframeAnimation *shake = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.x"]; shake.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; shake.duration = 0.5; shake.values = @[@(-8), @(8), @(-6), @(6), @(-4), @(4), @(0)]; [self.statusIndicator.layer addAnimation:shake forKey:@"shake"]; } - (void)animateCounterChange { // 数字变化的缩放动画 [UIView animateWithDuration:0.2 animations:^{ self.messageCountLabel.transform = CGAffineTransformMakeScale(1.1, 1.1); } completion:^(BOOL finished) { [UIView animateWithDuration:0.2 animations:^{ self.messageCountLabel.transform = CGAffineTransformIdentity; }]; }]; } @end ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/View/TJPMessageStatusIndicator.h ================================================ // // TJPMessageStatusIndicator.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/26. // #import NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSInteger, TJPMessageIndicatorStatus) { TJPMessageIndicatorStatusSending, // 发送中(转圈) TJPMessageIndicatorStatusSent, // 发送成功(勾号) TJPMessageIndicatorStatusFailed, // 发送失败(感叹号) TJPMessageIndicatorStatusRead // 已读(双勾号) }; @protocol TJPMessageStatusIndicatorDelegate @optional - (void)messageStatusIndicatorDidTapRetry:(id)sender; @end @interface TJPMessageStatusIndicator : UIView @property (nonatomic, weak) id delegate; @property (nonatomic, assign) TJPMessageIndicatorStatus status; // 用于重试时识别消息 @property (nonatomic, strong) NSString *messageId; - (void)updateStatus:(TJPMessageIndicatorStatus)status animated:(BOOL)animated; - (void)startSendingAnimation; - (void)stopSendingAnimation; // 便捷方法 + (CGSize)indicatorSize; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/ProductionBridge/VIPER-Sample/MessageModule/View/TJPMessageStatusIndicator.m ================================================ // // TJPMessageStatusIndicator.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/6/26. // #import "TJPMessageStatusIndicator.h" #import @interface TJPMessageStatusIndicator () @property (nonatomic, strong) UIActivityIndicatorView *loadingIndicator; // 转圈指示器 @property (nonatomic, strong) UIImageView *statusIcon; // 状态图标 @property (nonatomic, strong) UIButton *retryButton; // 重试按钮 @end @implementation TJPMessageStatusIndicator - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { [self setupUI]; [self updateStatus:TJPMessageIndicatorStatusSending animated:NO]; } return self; } + (CGSize)indicatorSize { return CGSizeMake(20, 20); } - (void)setupUI { // 设置转圈指示器 if (@available(iOS 13.0, *)) { self.loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleMedium]; } else { self.loadingIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleGray]; } self.loadingIndicator.hidesWhenStopped = YES; self.loadingIndicator.color = [UIColor systemBlueColor]; [self addSubview:self.loadingIndicator]; // 设置状态图标 self.statusIcon = [[UIImageView alloc] init]; self.statusIcon.contentMode = UIViewContentModeScaleAspectFit; self.statusIcon.alpha = 0.0; [self addSubview:self.statusIcon]; // 设置重试按钮 self.retryButton = [UIButton buttonWithType:UIButtonTypeCustom]; self.retryButton.alpha = 0.0; [self.retryButton setImage:[UIImage imageNamed:@"img_msg_fail"] forState:UIControlStateNormal]; [self.retryButton addTarget:self action:@selector(retryButtonTapped:) forControlEvents:UIControlEventTouchUpInside]; [self addSubview:self.retryButton]; [self setupConstraints]; } - (void)setupConstraints { // 转圈指示器约束 [self.loadingIndicator mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self); make.centerY.equalTo(self); make.width.mas_equalTo(16); make.height.mas_equalTo(16); }]; // 状态图标约束 [self.statusIcon mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self); make.centerY.equalTo(self); make.width.mas_equalTo(16); make.height.mas_equalTo(16); }]; // 重试按钮约束 [self.retryButton mas_makeConstraints:^(MASConstraintMaker *make) { make.centerX.equalTo(self); make.centerY.equalTo(self); make.width.mas_equalTo(20); make.height.mas_equalTo(20); }]; } #pragma mark - Public Methods - (void)updateStatus:(TJPMessageIndicatorStatus)status animated:(BOOL)animated { _status = status; if (animated) { [UIView animateWithDuration:0.3 animations:^{ [self configureForStatus:status]; }]; } else { [self configureForStatus:status]; } } - (void)startSendingAnimation { [self.loadingIndicator startAnimating]; } - (void)stopSendingAnimation { [self.loadingIndicator stopAnimating]; } #pragma mark - Private Methods - (void)configureForStatus:(TJPMessageIndicatorStatus)status { // 重置所有视图的可见性 [self.loadingIndicator stopAnimating]; self.statusIcon.alpha = 0.0; self.retryButton.alpha = 0.0; switch (status) { case TJPMessageIndicatorStatusSending: [self configureSendingStatus]; break; case TJPMessageIndicatorStatusSent: [self configureSentStatus]; break; case TJPMessageIndicatorStatusFailed: [self configureFailedStatus]; break; case TJPMessageIndicatorStatusRead: [self configureReadStatus]; break; } } - (void)configureSendingStatus { [self.loadingIndicator startAnimating]; } - (void)configureSentStatus { // 单勾号 - 发送成功 self.statusIcon.image = [self createCheckmarkImage]; self.statusIcon.tintColor = [UIColor systemBlueColor]; self.statusIcon.alpha = 1.0; // 成功动画 [self animateSuccessWithScale]; } - (void)configureFailedStatus { // 重试按钮 图标为感叹号 // self.statusIcon.image = [self createExclamationImage]; // self.statusIcon.tintColor = [UIColor systemRedColor]; self.statusIcon.alpha = 0.0; self.retryButton.alpha = 1.0; // 失败动画 // [self animateFailureWithShake]; } - (void)configureReadStatus { // 双勾号 - 已读 self.statusIcon.image = [self createDoubleCheckmarkImage]; self.statusIcon.tintColor = [UIColor systemBlueColor]; self.statusIcon.alpha = 1.0; } #pragma mark - Icon Creation - (UIImage *)createCheckmarkImage { // 创建单勾号图标 UIGraphicsBeginImageContextWithOptions(CGSizeMake(16, 16), NO, 0); UIBezierPath *path = [UIBezierPath bezierPath]; [path moveToPoint:CGPointMake(3, 8)]; [path addLineToPoint:CGPointMake(6, 11)]; [path addLineToPoint:CGPointMake(13, 4)]; [[UIColor systemBlueColor] setStroke]; path.lineWidth = 2.0; path.lineCapStyle = kCGLineCapRound; path.lineJoinStyle = kCGLineJoinRound; [path stroke]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; } - (UIImage *)createDoubleCheckmarkImage { // 创建双勾号图标 UIGraphicsBeginImageContextWithOptions(CGSizeMake(16, 16), NO, 0); // 第一个勾号 UIBezierPath *path1 = [UIBezierPath bezierPath]; [path1 moveToPoint:CGPointMake(1, 8)]; [path1 addLineToPoint:CGPointMake(4, 11)]; [path1 addLineToPoint:CGPointMake(8, 7)]; // 第二个勾号 UIBezierPath *path2 = [UIBezierPath bezierPath]; [path2 moveToPoint:CGPointMake(6, 8)]; [path2 addLineToPoint:CGPointMake(9, 11)]; [path2 addLineToPoint:CGPointMake(15, 5)]; [[UIColor systemBlueColor] setStroke]; path1.lineWidth = 2.0; path1.lineCapStyle = kCGLineCapRound; path1.lineJoinStyle = kCGLineJoinRound; [path1 stroke]; path2.lineWidth = 2.0; path2.lineCapStyle = kCGLineCapRound; path2.lineJoinStyle = kCGLineJoinRound; [path2 stroke]; UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; } //- (UIImage *)createExclamationImage { // // 创建感叹号图标 // UIGraphicsBeginImageContextWithOptions(CGSizeMake(16, 16), NO, 0); // // // 感叹号主体 // UIBezierPath *path = [UIBezierPath bezierPath]; // [path moveToPoint:CGPointMake(8, 3)]; // [path addLineToPoint:CGPointMake(8, 10)]; // // [[UIColor systemRedColor] setStroke]; // path.lineWidth = 2.0; // path.lineCapStyle = kCGLineCapRound; // [path stroke]; // // // 感叹号底部圆点 // UIBezierPath *dotPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(7, 12, 2, 2)]; // [[UIColor systemRedColor] setFill]; // [dotPath fill]; // // UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); // UIGraphicsEndImageContext(); // // return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; //} #pragma mark - Animations - (void)animateSuccessWithScale { self.statusIcon.transform = CGAffineTransformMakeScale(0.5, 0.5); [UIView animateWithDuration:0.4 delay:0 usingSpringWithDamping:0.6 initialSpringVelocity:0.5 options:0 animations:^{ self.statusIcon.transform = CGAffineTransformIdentity; } completion:nil]; } //- (void)animateFailureWithShake { // CAKeyframeAnimation *shake = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.x"]; // shake.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; // shake.duration = 0.5; // shake.values = @[@(-4), @(4), @(-3), @(3), @(-2), @(2), @(0)]; // // [self.statusIcon.layer addAnimation:shake forKey:@"shake"]; //} #pragma mark - Actions - (void)retryButtonTapped:(UIButton *)sender { if ([self.delegate respondsToSelector:@selector(messageStatusIndicatorDidTapRetry:)]) { [self.delegate messageStatusIndicatorDidTapRetry:self]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Resources/feedData.json ================================================ { "code": 200, "message": "success", "data": { "feeds": [ { "type": "news", "id": "news_001", "title": "苹果发布最新iOS 17.4版本", "summary": "新版本带来了多项新功能和安全性改进,包括全新的控制中心设计和更智能的Siri响应机制。此次更新还优化了电池续航表现...", "imageUrl": "https://images.unsplash.com/photo-1555774698-0b77e0d5fac6?w=400", "publishTime": "2024-01-15 10:30:00", "source": "科技新闻", "readCount": 1520 }, { "type": "image", "id": "img_001", "title": "美丽的春日樱花", "imageUrls": [ "https://plus.unsplash.com/premium_photo-1670444760243-155db6c38716?w=400", "https://images.unsplash.com/photo-1648468091968-3b9c5d84c101?w=400", "https://images.unsplash.com/photo-1429019857339-35b401806fa1?w=400" ], "likes": 892, "comments": 45, "description": "春天的樱花盛开,美不胜收,每一朵花都承载着希望与美好" }, { "type": "video", "id": "video_001", "title": "SwiftUI完整教程 - 从零开始构建iOS应用", "coverUrl": "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400", "videoUrl": "https://example.com/video1.mp4", "duration": "15:30", "playCount": 3420, "author": "iOS开发大师" }, { "type": "userDynamic", "id": "dynamic_001", "userName": "张小明", "userAvatar": "https://images.unsplash.com/photo-1535713875002-d1d0cf377fde?w=80", "content": "今天学习了VIPER架构,感觉收获很大!分享给大家一些心得体会。VIPER架构确实能让项目结构更清晰,各个模块的职责划分更明确,特别适合大型项目的开发和维护。", "images": [ "https://images.unsplash.com/photo-1626785774573-4b799315345d?w=400" ], "publishTime": "2小时前", "likes": 156, "comments": 23 }, { "type": "product", "id": "product_001", "name": "iPhone 15 Pro Max 1TB 原色钛金属", "price": 9999.00, "originalPrice": 10999.00, "imageUrl": "https://images.unsplash.com/photo-1603921326210-6edd2d60ca68?w=400", "rating": 4.8, "sales": 1280, "tags": ["热门", "限时特价", "送货上门"] }, { "type": "ad", "id": "ad_001", "title": "学编程,就来慕课网", "subtitle": "千门课程,总有一门适合你", "imageUrl": "https://images.unsplash.com/photo-1555066931-4365d14bab8c?w=400", "actionText": "立即学习", "actionUrl": "https://imooc.com", "flipContent": { "title": "限时优惠!立减1000元", "subtitle": "新用户专享 | 前10名抽取免费机会", "actionText": "抢购", "imageUrl": "https://images.unsplash.com/photo-1607082349566-187342175e2f?w=400", "highlightColor": "#FF6B35" } }, { "type": "news", "id": "news_002", "title": "特斯拉FSD测试版全面开放,自动驾驶技术再次突破", "summary": "特斯拉宣布其全自动驾驶(FSD)测试版将向所有车主开放,这标志着自动驾驶技术迈向了新的里程碑。新版本在城市道路的表现尤为出色...", "imageUrl": "https://images.unsplash.com/photo-1580913428735-bd3c269d6a82?w=400", "publishTime": "1小时前", "source": "汽车科技", "readCount": 2340 }, { "type": "userDynamic", "id": "dynamic_002", "userName": "李设计师", "userAvatar": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=80", "content": "刚完成的APP界面设计,使用了最新的设计语言。整个设计过程花了两周时间,从用户调研到最终交付,每一个细节都精心打磨。希望大家喜欢!", "images": [ "https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=400", "https://images.unsplash.com/photo-1579403124614-197f69d8187b?w=400", "https://images.unsplash.com/photo-1551650975-87deedd944c3?w=400", "https://images.unsplash.com/photo-1512486130939-2c4f79935e4f?w=400", "https://images.unsplash.com/photo-1547658719-da2b51169166?w=400", "https://images.unsplash.com/photo-1517245386807-bb43f82c33c4?w=400", "https://images.unsplash.com/photo-1517077304055-6e89abbf09b0?w=400", "https://images.unsplash.com/photo-1541462608143-67571c6738dd?w=400", "https://images.unsplash.com/photo-1559028012-481c04fa702d?w=400" ], "publishTime": "4小时前", "likes": 423, "comments": 67, "likeUsers": [ {"userName": "张小明", "userId": "user_001"}, {"userName": "王小花", "userId": "user_002"}, {"userName": "李小强", "userId": "user_003"}, {"userName": "赵小美", "userId": "user_004"}, {"userName": "陈小帅", "userId": "user_005"} ], "commentList": [ { "commentId": "comment_001", "userName": "张小明", "userId": "user_001", "content": "设计风格很棒,简洁大方!", "publishTime": "3小时前" }, { "commentId": "comment_002", "userName": "王小花", "userId": "user_002", "content": "用了什么设计工具呀?", "publishTime": "2小时前" }, { "commentId": "comment_003", "userName": "李设计师", "userId": "dynamic_002", "content": "主要用Figma,配合Principle做动效", "replyTo": "王小花", "replyToUserId": "user_002", "publishTime": "2小时前" }, { "commentId": "comment_004", "userName": "赵小美", "userId": "user_004", "content": "色彩搭配很有层次感,学习了!", "publishTime": "1小时前" } ] }, { "type": "video", "id": "video_002", "title": "WWDC 2024 主题演讲精华回顾", "coverUrl": "https://images.unsplash.com/photo-1522542550221-31fd19575a2d?w=400", "videoUrl": "https://example.com/wwdc_video.mp4", "duration": "08:45", "playCount": 8750, "author": "苹果官方" }, { "type": "image", "id": "img_002", "title": "北京四季风光摄影集", "imageUrls": [ "https://images.unsplash.com/photo-1547981609-4b6bfe67ca0b?w=400", "https://images.unsplash.com/photo-1627573098568-6847a7d440bf?w=400", "https://images.unsplash.com/photo-1556595187-0243f5c916ca?w=400", "https://images.unsplash.com/photo-1666331468685-f6a39ee64cc2?w=400", "https://images.unsplash.com/photo-1550107319-b90144fc945d?w=400" ], "likes": 1205, "comments": 89, "description": "穿越千年时光,感受北京的多维魅力。从皇家宫殿到现代地标,从春花秋月到夏荷冬雪,这些影像记录着这座城市的历史脉动与时代风采。每张照片都是摄影师与北京城的深情对话,邀您一同品味这座古都的独特韵味。" } ], "pagination": { "page": 1, "page_size": 10, "total": 30, "total_pages": 3, "has_more": true } } } ================================================ FILE: iOS-Network-Stack-Dive/Resources/feedData_page2.json ================================================ { "code": 200, "message": "success", "data": { "feeds": [ { "type": "product", "id": "product_002", "name": "MacBook Pro 14英寸 M3 Max芯片", "price": 25999.00, "originalPrice": 27999.00, "imageUrl": "https://images.unsplash.com/photo-1569770218135-bea267ed7e84?w=400", "rating": 4.9, "sales": 856, "tags": ["新品", "高性能", "专业创作"] }, { "type": "ad", "id": "ad_002", "title": "双11狂欢节,全场5折起", "subtitle": "错过再等一年,立即抢购", "imageUrl": "https://images.unsplash.com/photo-1607082352121-fa243f3dde32?w=400", "actionText": "立即抢购", "actionUrl": "https://shop.example.com/sale" }, { "type": "news", "id": "news_003", "title": "ChatGPT-5即将发布,AI能力再次飞跃", "summary": "OpenAI官方透露ChatGPT-5正在最后的测试阶段,新版本在推理能力、多模态理解和创作能力方面都有显著提升...", "imageUrl": "https://images.unsplash.com/photo-1645680827507-9f392edae51c?w=400", "publishTime": "30分钟前", "source": "AI科技", "readCount": 5680 }, { "type": "video", "id": "video_003", "title": "Xcode 15新特性详解 - 提升开发效率的利器", "coverUrl": "https://images.unsplash.com/photo-1551033406-611cf9a28f67?w=400", "videoUrl": "https://example.com/xcode_video.mp4", "duration": "12:20", "playCount": 2156, "author": "开发者小王" }, { "type": "userDynamic", "id": "dynamic_003", "userName": "产品经理老张", "userAvatar": "https://images.unsplash.com/photo-1560250097-0b93528c311a?w=80", "content": "产品迭代中最重要的是什么?我认为是用户反馈。今天收到了用户的建议邮件,虽然只有几行字,但每一条都很宝贵。用户的声音就是我们前进的方向。", "images": [], "publishTime": "6小时前", "commentList": [ { "commentId": "comment_001", "userName": "张小明", "userId": "user_001", "content": "还是要多倾听用户的声音", "publishTime": "3小时前" }, { "commentId": "comment_004", "userName": "赵小美", "userId": "user_004", "content": "保证一个正常迭代周期也很重要", "publishTime": "1小时前" } ], "likes": 89, "comments": 12 }, { "type": "image", "id": "img_003", "title": "城市夜景延时摄影", "imageUrls": [ "https://images.unsplash.com/photo-1699872074472-963846fca564?w=400", "https://images.unsplash.com/photo-1633635485151-bb1ac6097418?w=400", "https://images.unsplash.com/photo-1603287348993-6dbee5a70add?w=400" ], "likes": 756, "comments": 34, "description": "用延时摄影记录城市的夜晚,车水马龙中的生活节奏" }, { "type": "product", "id": "product_003", "name": "AirPods Pro 2代 USB-C充电版", "price": 1899.00, "originalPrice": 1999.00, "imageUrl": "https://images.unsplash.com/photo-1606841837239-c5a1a4a07af7?w=400", "rating": 4.7, "sales": 3420, "tags": ["主动降噪", "空间音频", "长续航"] }, { "type": "news", "id": "news_004", "title": "微信小程序推出AI助手功能,智能客服新体验", "summary": "微信团队宣布在小程序平台集成AI助手功能,开发者可以快速为自己的小程序添加智能客服能力,提升用户服务体验...", "imageUrl": "https://images.unsplash.com/photo-1611162616305-c69b3fa7fbe0?w=400", "publishTime": "1天前", "source": "移动互联网", "readCount": 4280 }, { "type": "userDynamic", "id": "dynamic_004", "userName": "UI设计菲菲", "userAvatar": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=80", "content": "分享一个设计小技巧:在做移动端设计时,按钮的最小点击区域应该保持在44x44pt,这样能保证用户的操作体验。很多新手设计师容易忽略这个细节。", "images": [ "https://images.unsplash.com/photo-1579403124614-197f69d8187b" ], "likeUsers": [ {"userName": "王小花", "userId": "user_002"}, {"userName": "陈小帅", "userId": "user_005"} ], "publishTime": "1天前", "likes": 234, "comments": 45 }, { "type": "video", "id": "video_004", "title": "Flutter 3.0 跨平台开发实战", "coverUrl": "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400", "videoUrl": "https://example.com/flutter_video.mp4", "duration": "25:15", "playCount": 1876, "author": "跨平台开发专家" } ], "pagination": { "page": 2, "page_size": 10, "total": 30, "total_pages": 3, "has_more": true } } } ================================================ FILE: iOS-Network-Stack-Dive/Resources/feedData_page3.json ================================================ { "code": 200, "message": "success", "data": { "feeds": [ { "type": "product", "id": "product_005", "name": "MacBook Pro 14英寸 M3 Pro芯片 1TB 深空灰色", "price": 16999, "originalPrice": 18999, "imageUrl": "https://images.unsplash.com/photo-1611186871348-b1ce696e52c9?w=400", "rating": 4.8, "sales": 892, "tags": ["Apple官方", "教育优惠", "分期免息"] }, { "type": "ad", "id": "ad_005", "title": "双十一提前抢,科技好物特惠", "subtitle": "限时24小时,数量有限先到先得", "imageUrl": "https://plus.unsplash.com/premium_photo-1681488183639-f38511a647ef?w=400", "actionText": "马上抢购", "actionUrl": "https://shop.example.com/tech-sale" }, { "type": "news", "id": "news_006", "title": "苹果发布iOS 18开发者预览版,引入全新AI功能", "summary": "苹果在WWDC 2024上正式发布iOS 18开发者预览版,新系统集成了Apple Intelligence,为用户带来更智能的体验...", "imageUrl": "https://images.unsplash.com/photo-1727093493807-f11b48fa31a8?w=400", "publishTime": "2小时前", "source": "苹果资讯", "readCount": 8923 }, { "type": "video", "id": "video_005", "title": "SwiftUI 动画进阶教程 - 打造流畅用户体验", "coverUrl": "https://images.unsplash.com/photo-1633356122544-f134324a6cee?w=400", "videoUrl": "https://example.com/swiftui_animation.mp4", "duration": "18:45", "playCount": 3247, "author": "iOS开发达人" }, { "type": "userDynamic", "id": "dynamic_005", "userName": "架构师小李", "userAvatar": "https://images.unsplash.com/photo-1560250097-0b93528c311a?w=80", "content": "最近在研究微服务架构的服务治理,发现服务间的依赖关系管理是个大学问。今天尝试了新的服务网格方案,效果还不错。分享给大家一些心得体会。", "images": [ "https://images.unsplash.com/photo-1513759338966-5de23c844b3a?w=400" ], "publishTime": "4小时前", "likes": 156, "comments": 28 }, { "type": "image", "id": "img_005", "title": "极光下的雪山风光", "imageUrls": [ "https://plus.unsplash.com/premium_photo-1673002092591-207291eb728c?w=400", "https://images.unsplash.com/photo-1531512073830-ba890ca4eba2?w=400", "https://images.unsplash.com/photo-1652120271356-749cf627f5b0?w=400", "https://images.unsplash.com/photo-1451159212029-6f35f0d0a501?w=400", "https://plus.unsplash.com/premium_photo-1671987556132-313d34dfa559?w=400" ], "likes": 1234, "comments": 67, "description": "北欧之旅第三天,终于等到了梦寐以求的极光,配上雪山简直太美了" }, { "type": "product", "id": "product_006", "name": "iPad Air 11英寸 M2芯片 256GB WiFi版 星光色", "price": 4799.00, "originalPrice": 5199.00, "imageUrl": "https://images.unsplash.com/photo-1682426526490-667d4912b8de?w=400", "rating": 4.6, "sales": 2156, "tags": ["轻薄便携", "学习办公", "创意设计"] }, { "type": "news", "id": "news_007", "title": "GitHub推出AI编程助手Copilot企业版,团队协作更高效", "summary": "GitHub宣布推出Copilot企业版,针对团队开发场景进行了深度优化,支持代码库上下文理解和团队知识共享...", "imageUrl": "https://images.unsplash.com/photo-1645680827507-9f392edae51c?w=400", "publishTime": "8小时前", "source": "开发者资讯", "readCount": 6751 }, { "type": "userDynamic", "id": "dynamic_006", "userName": "前端工程师Amy", "userAvatar": "https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=80", "content": "今天遇到了一个有趣的CSS问题:如何实现文字的渐变色效果?用background-clip: text配合transparent color就能搞定!前端的世界总是充满惊喜。", "images": [], "publishTime": "12小时前", "likes": 89, "comments": 15 }, { "type": "video", "id": "video_006", "title": "React Native 0.74新特性解析与升级指南", "coverUrl": "https://images.unsplash.com/photo-1579403124614-197f69d8187b?w=400", "videoUrl": "https://example.com/rn_074_video.mp4", "duration": "22:30", "playCount": 1598, "author": "跨端开发专家" } ], "pagination": { "page": 3, "page_size": 10, "total": 30, "total_pages": 3, "has_more": false } } } ================================================ FILE: iOS-Network-Stack-Dive/Tools/Color/UIColor+TJPColor.h ================================================ // // UIColor+TJPColor.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/30. // #import NS_ASSUME_NONNULL_BEGIN @interface UIColor (TJPColor) + (UIColor *)tjp_White; + (UIColor *)tjp_tablebackColor ; + (UIColor *)tjp_systemColor ; + (UIColor *)tjp_lightSystemColor; + (UIColor *)tjp_blueColor ; /** 背景色*/ + (UIColor *)tjp_backgroundGrayColor; /** 浅色背景*/ + (UIColor *)tjp_lightBackgroundGrayColor; /** 深黑背景*/ + (UIColor *)tjp_darkBlackBackgroundGrayColor; /** 用于重要标题*/ + (UIColor *)tjp_blackColor ; + (UIColor *)tjp_lightBlackTextColor; /** 用于辅助、次要信息内容*/ + (UIColor *)tjp_lightTextColor; /** 用于基本信息内容*/ + (UIColor *)tjp_lightDarkTextColor; /** 用于分割*/ + (UIColor *)tjp_separateColor; + (UIColor *)tjp_garyBorderLineColor; + (UIColor *)tjp_lightGrayBorderColor; + (UIColor *)tjp_coolGreyColor; + (UIColor *)tjp_slateGreyTextColor; + (UIColor *)tjp_paleOliveGreenColor; + (UIColor *)tjp_darkMintColor; + (UIColor *)tjp_saffronColor; + (UIColor *)tjp_sandyBrownColor; + (UIColor *)tjp_lightKhakiColor; + (UIColor *)tjp_colorWithHexString:(NSString *)hexString; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Tools/Color/UIColor+TJPColor.m ================================================ // // UIColor+TJPColor.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/30. // #import "UIColor+TJPColor.h" @implementation UIColor (TJPColor) + (UIColor *)tjp_White { return [UIColor whiteColor]; } + (UIColor *)tjp_tablebackColor { return [UIColor colorWithWhite:216.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_systemColor { return [UIColor colorWithRed:255.0f / 255.0f green:136.0f / 255.0f blue:102.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_blueColor { return [UIColor colorWithRed:0.0 green:148.0f / 255.0f blue:255.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_lightSystemColor { return [UIColor colorWithRed:255.0f / 255.0f green:134.0f / 255.0f blue:43.0f / 255.0f alpha:0.4f]; } + (UIColor *)tjp_backgroundGrayColor { return [UIColor tjp_colorWithHexString:@"#F2F2F2"];//245,245,245 } + (UIColor *)tjp_lightBackgroundGrayColor { return [UIColor tjp_colorWithHexString:@"#F7F8F9"]; } /** 深黑背景*/ + (UIColor *)tjp_darkBlackBackgroundGrayColor { return [UIColor tjp_colorWithHexString:@"#484848"]; } + (UIColor *)tjp_blackColor { return [UIColor colorWithWhite:51.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_lightBlackTextColor { return [UIColor colorWithWhite:34.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_lightTextColor { return [UIColor colorWithWhite:153.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_lightDarkTextColor { return [UIColor colorWithWhite:102.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_separateColor { return [UIColor colorWithWhite:222.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_garyBorderLineColor { return [UIColor tjp_colorWithHexString:@"#F5F5F5"]; } + (UIColor *)tjp_lightGrayBorderColor { return [UIColor tjp_colorWithHexString:@"#CCCCCC"]; } + (UIColor *)tjp_coolGreyColor { return [UIColor colorWithRed:173.0f / 255.0f green:177.0f / 255.0f blue:191.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_slateGreyTextColor { return [UIColor colorWithRed:94.0f / 255.0f green:105.0f / 255.0f blue:119.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_paleOliveGreenColor { return [UIColor colorWithRed:152.0f / 255.0f green:211.0f / 255.0f blue:100.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_darkMintColor { return [UIColor colorWithRed:92.0f / 255.0f green:198.0f / 255.0f blue:91.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_saffronColor { return [UIColor colorWithRed:253.0f / 255.0f green:178.0f / 255.0f blue:9.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_sandyBrownColor { return [UIColor colorWithRed:192.0f / 255.0f green:164.0f / 255.0f blue:108.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_lightKhakiColor { return [UIColor colorWithRed:252.0f / 255.0f green:248.0f / 255.0f blue:238.0f / 255.0f alpha:1.0f]; } + (UIColor *)tjp_colorWithHexWithLong:(long)hexColor alpha:(CGFloat)a { float red = ((float)((hexColor & 0xFF0000) >> 16))/255.0; float green = ((float)((hexColor & 0xFF00) >> 8))/255.0; float blue = ((float)(hexColor & 0xFF))/255.0; return [UIColor colorWithRed:red green:green blue:blue alpha:a]; } + (UIColor *)tjp_colorWithHexString:(NSString *)hexString { NSString *removeSharpMarkhexString = [hexString stringByReplacingOccurrencesOfString:@"#" withString:@""]; NSScanner *scanner = [NSScanner scannerWithString:removeSharpMarkhexString]; unsigned result = 0; [scanner scanHexInt:&result]; return [self tjp_colorWithHexWithLong:result alpha:1.0]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshAutoFooter.h ================================================ // // MJRefreshAutoFooter.h // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshFooter.h" @interface MJRefreshAutoFooter : MJRefreshFooter /** 是否自动刷新(默认为YES) */ @property (assign, nonatomic, getter=isAutomaticallyRefresh) BOOL automaticallyRefresh; /** 当底部控件出现多少时就自动刷新(默认为1.0,也就是底部控件完全出现时,才会自动刷新) */ @property (assign, nonatomic) CGFloat appearencePercentTriggerAutoRefresh MJRefreshDeprecated("请使用triggerAutomaticallyRefreshPercent属性"); /** 当底部控件出现多少时就自动刷新(默认为1.0,也就是底部控件完全出现时,才会自动刷新) */ @property (assign, nonatomic) CGFloat triggerAutomaticallyRefreshPercent; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshAutoFooter.m ================================================ // // MJRefreshAutoFooter.m // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshAutoFooter.h" @interface MJRefreshAutoFooter() @end @implementation MJRefreshAutoFooter #pragma mark - 初始化 - (void)willMoveToSuperview:(UIView *)newSuperview { [super willMoveToSuperview:newSuperview]; if (newSuperview) { // 新的父控件 if (self.hidden == NO) { self.scrollView.mj_insetB += self.mj_h; } // 设置位置 self.mj_y = _scrollView.mj_contentH; } else { // 被移除了 if (self.hidden == NO) { self.scrollView.mj_insetB -= self.mj_h; } } } #pragma mark - 过期方法 - (void)setAppearencePercentTriggerAutoRefresh:(CGFloat)appearencePercentTriggerAutoRefresh { self.triggerAutomaticallyRefreshPercent = appearencePercentTriggerAutoRefresh; } - (CGFloat)appearencePercentTriggerAutoRefresh { return self.triggerAutomaticallyRefreshPercent; } #pragma mark - 实现父类的方法 - (void)prepare { [super prepare]; // 默认底部控件100%出现时才会自动刷新 self.triggerAutomaticallyRefreshPercent = 1.0; // 设置为默认状态 self.automaticallyRefresh = YES; } - (void)scrollViewContentSizeDidChange:(NSDictionary *)change { [super scrollViewContentSizeDidChange:change]; // 设置位置 self.mj_y = self.scrollView.mj_contentH; } - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change { [super scrollViewContentOffsetDidChange:change]; if (self.state != MJRefreshStateIdle || !self.automaticallyRefresh || self.mj_y == 0) return; if (_scrollView.mj_insetT + _scrollView.mj_contentH > _scrollView.mj_h) { // 内容超过一个屏幕 // 这里的_scrollView.mj_contentH替换掉self.mj_y更为合理 if (_scrollView.mj_offsetY >= _scrollView.mj_contentH - _scrollView.mj_h + self.mj_h * self.triggerAutomaticallyRefreshPercent + _scrollView.mj_insetB - self.mj_h) { // 防止手松开时连续调用 CGPoint old = [change[@"old"] CGPointValue]; CGPoint new = [change[@"new"] CGPointValue]; if (new.y <= old.y) return; // 当底部刷新控件完全出现时,才刷新 [self beginRefreshing]; } } } - (void)scrollViewPanStateDidChange:(NSDictionary *)change { [super scrollViewPanStateDidChange:change]; if (self.state != MJRefreshStateIdle) return; if (_scrollView.panGestureRecognizer.state == UIGestureRecognizerStateEnded) {// 手松开 if (_scrollView.mj_insetT + _scrollView.mj_contentH <= _scrollView.mj_h) { // 不够一个屏幕 if (_scrollView.mj_offsetY >= - _scrollView.mj_insetT) { // 向上拽 [self beginRefreshing]; } } else { // 超出一个屏幕 if (_scrollView.mj_offsetY >= _scrollView.mj_contentH + _scrollView.mj_insetB - _scrollView.mj_h) { [self beginRefreshing]; } } } } - (void)setState:(MJRefreshState)state { MJRefreshCheckState if (state == MJRefreshStateRefreshing) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [self executeRefreshingCallback]; }); } else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) { if (MJRefreshStateRefreshing == oldState) { if (self.endRefreshingCompletionBlock) { self.endRefreshingCompletionBlock(); } } } } - (void)setHidden:(BOOL)hidden { BOOL lastHidden = self.isHidden; [super setHidden:hidden]; if (!lastHidden && hidden) { self.state = MJRefreshStateIdle; self.scrollView.mj_insetB -= self.mj_h; } else if (lastHidden && !hidden) { self.scrollView.mj_insetB += self.mj_h; // 设置位置 self.mj_y = _scrollView.mj_contentH; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshBackFooter.h ================================================ // // MJRefreshBackFooter.h // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshFooter.h" @interface MJRefreshBackFooter : MJRefreshFooter @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshBackFooter.m ================================================ // // MJRefreshBackFooter.m // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshBackFooter.h" @interface MJRefreshBackFooter() @property (assign, nonatomic) NSInteger lastRefreshCount; @property (assign, nonatomic) CGFloat lastBottomDelta; @end @implementation MJRefreshBackFooter #pragma mark - 初始化 - (void)willMoveToSuperview:(UIView *)newSuperview { [super willMoveToSuperview:newSuperview]; [self scrollViewContentSizeDidChange:nil]; } #pragma mark - 实现父类的方法 - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change { [super scrollViewContentOffsetDidChange:change]; // 如果正在刷新,直接返回 if (self.state == MJRefreshStateRefreshing) return; _scrollViewOriginalInset = self.scrollView.contentInset; // 当前的contentOffset CGFloat currentOffsetY = self.scrollView.mj_offsetY; // 尾部控件刚好出现的offsetY CGFloat happenOffsetY = [self happenOffsetY]; // 如果是向下滚动到看不见尾部控件,直接返回 if (currentOffsetY <= happenOffsetY) return; CGFloat pullingPercent = (currentOffsetY - happenOffsetY) / self.mj_h; // 如果已全部加载,仅设置pullingPercent,然后返回 if (self.state == MJRefreshStateNoMoreData) { self.pullingPercent = pullingPercent; return; } if (self.scrollView.isDragging) { self.pullingPercent = pullingPercent; // 普通 和 即将刷新 的临界点 CGFloat normal2pullingOffsetY = happenOffsetY + self.mj_h; if (self.state == MJRefreshStateIdle && currentOffsetY > normal2pullingOffsetY) { // 转为即将刷新状态 self.state = MJRefreshStatePulling; } else if (self.state == MJRefreshStatePulling && currentOffsetY <= normal2pullingOffsetY) { // 转为普通状态 self.state = MJRefreshStateIdle; } } else if (self.state == MJRefreshStatePulling) {// 即将刷新 && 手松开 // 开始刷新 [self beginRefreshing]; } else if (pullingPercent < 1) { self.pullingPercent = pullingPercent; } } - (void)scrollViewContentSizeDidChange:(NSDictionary *)change { [super scrollViewContentSizeDidChange:change]; // 内容的高度 CGFloat contentHeight = self.scrollView.mj_contentH + self.ignoredScrollViewContentInsetBottom; // 表格的高度 CGFloat scrollHeight = self.scrollView.mj_h - self.scrollViewOriginalInset.top - self.scrollViewOriginalInset.bottom + self.ignoredScrollViewContentInsetBottom; // 设置位置和尺寸 self.mj_y = MAX(contentHeight, scrollHeight); } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根据状态来设置属性 if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) { // 刷新完毕 if (MJRefreshStateRefreshing == oldState) { [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{ self.scrollView.mj_insetB -= self.lastBottomDelta; // 自动调整透明度 if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0; } completion:^(BOOL finished) { self.pullingPercent = 0.0; if (self.endRefreshingCompletionBlock) { self.endRefreshingCompletionBlock(); } }]; } CGFloat deltaH = [self heightForContentBreakView]; // 刚刷新完毕 if (MJRefreshStateRefreshing == oldState && deltaH > 0 && self.scrollView.mj_totalDataCount != self.lastRefreshCount) { self.scrollView.mj_offsetY = self.scrollView.mj_offsetY; } } else if (state == MJRefreshStateRefreshing) { // 记录刷新前的数量 self.lastRefreshCount = self.scrollView.mj_totalDataCount; [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ CGFloat bottom = self.mj_h + self.scrollViewOriginalInset.bottom; CGFloat deltaH = [self heightForContentBreakView]; if (deltaH < 0) { // 如果内容高度小于view的高度 bottom -= deltaH; } self.lastBottomDelta = bottom - self.scrollView.mj_insetB; self.scrollView.mj_insetB = bottom; self.scrollView.mj_offsetY = [self happenOffsetY] + self.mj_h; } completion:^(BOOL finished) { [self executeRefreshingCallback]; }]; } } - (void)endRefreshing { dispatch_async(dispatch_get_main_queue(), ^{ self.state = MJRefreshStateIdle; }); } - (void)endRefreshingWithNoMoreData { dispatch_async(dispatch_get_main_queue(), ^{ self.state = MJRefreshStateNoMoreData; }); } #pragma mark - 私有方法 #pragma mark 获得scrollView的内容 超出 view 的高度 - (CGFloat)heightForContentBreakView { CGFloat h = self.scrollView.frame.size.height - self.scrollViewOriginalInset.bottom - self.scrollViewOriginalInset.top; return self.scrollView.contentSize.height - h; } #pragma mark 刚好看到上拉刷新控件时的contentOffset.y - (CGFloat)happenOffsetY { CGFloat deltaH = [self heightForContentBreakView]; if (deltaH > 0) { return deltaH - self.scrollViewOriginalInset.top; } else { return - self.scrollViewOriginalInset.top; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshComponent.h ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // MJRefreshComponent.h // MJRefreshExample // // Created by MJ Lee on 15/3/4. // Copyright (c) 2015年 小码哥. All rights reserved. // 刷新控件的基类 #import #import "MJRefreshConst.h" #import "UIView+MJExtension.h" #import "UIScrollView+MJExtension.h" #import "UIScrollView+MJRefresh.h" #import "NSBundle+MJRefresh.h" /** 刷新控件的状态 */ typedef NS_ENUM(NSInteger, MJRefreshState) { /** 普通闲置状态 */ MJRefreshStateIdle = 1, /** 松开就可以进行刷新的状态 */ MJRefreshStatePulling, /** 正在刷新中的状态 */ MJRefreshStateRefreshing, /** 即将刷新的状态 */ MJRefreshStateWillRefresh, /** 所有数据加载完毕,没有更多的数据了 */ MJRefreshStateNoMoreData }; /** 进入刷新状态的回调 */ typedef void (^MJRefreshComponentRefreshingBlock)(); /** 开始刷新后的回调(进入刷新状态后的回调) */ typedef void (^MJRefreshComponentbeginRefreshingCompletionBlock)(); /** 结束刷新后的回调 */ typedef void (^MJRefreshComponentEndRefreshingCompletionBlock)(); /** 刷新控件的基类 */ @interface MJRefreshComponent : UIView { /** 记录scrollView刚开始的inset */ UIEdgeInsets _scrollViewOriginalInset; /** 父控件 */ __weak UIScrollView *_scrollView; } #pragma mark - 刷新回调 /** 正在刷新的回调 */ @property (copy, nonatomic) MJRefreshComponentRefreshingBlock refreshingBlock; /** 设置回调对象和回调方法 */ - (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action; /** 回调对象 */ @property (weak, nonatomic) id refreshingTarget; /** 回调方法 */ @property (assign, nonatomic) SEL refreshingAction; /** 触发回调(交给子类去调用) */ - (void)executeRefreshingCallback; #pragma mark - 刷新状态控制 /** 进入刷新状态 */ - (void)beginRefreshing; - (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock; /** 开始刷新后的回调(进入刷新状态后的回调) */ @property (copy, nonatomic) MJRefreshComponentbeginRefreshingCompletionBlock beginRefreshingCompletionBlock; /** 结束刷新的回调 */ @property (copy, nonatomic) MJRefreshComponentEndRefreshingCompletionBlock endRefreshingCompletionBlock; /** 结束刷新状态 */ - (void)endRefreshing; - (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock; /** 是否正在刷新 */ - (BOOL)isRefreshing; /** 刷新状态 一般交给子类内部实现 */ @property (assign, nonatomic) MJRefreshState state; #pragma mark - 交给子类去访问 /** 记录scrollView刚开始的inset */ @property (assign, nonatomic, readonly) UIEdgeInsets scrollViewOriginalInset; /** 父控件 */ @property (weak, nonatomic, readonly) UIScrollView *scrollView; #pragma mark - 交给子类们去实现 /** 初始化 */ - (void)prepare NS_REQUIRES_SUPER; /** 摆放子控件frame */ - (void)placeSubviews NS_REQUIRES_SUPER; /** 当scrollView的contentOffset发生改变的时候调用 */ - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change NS_REQUIRES_SUPER; /** 当scrollView的contentSize发生改变的时候调用 */ - (void)scrollViewContentSizeDidChange:(NSDictionary *)change NS_REQUIRES_SUPER; /** 当scrollView的拖拽状态发生改变的时候调用 */ - (void)scrollViewPanStateDidChange:(NSDictionary *)change NS_REQUIRES_SUPER; #pragma mark - 其他 /** 拉拽的百分比(交给子类重写) */ @property (assign, nonatomic) CGFloat pullingPercent; /** 根据拖拽比例自动切换透明度 */ @property (assign, nonatomic, getter=isAutoChangeAlpha) BOOL autoChangeAlpha MJRefreshDeprecated("请使用automaticallyChangeAlpha属性"); /** 根据拖拽比例自动切换透明度 */ @property (assign, nonatomic, getter=isAutomaticallyChangeAlpha) BOOL automaticallyChangeAlpha; @end @interface UILabel(MJRefresh) + (instancetype)mj_label; - (CGFloat)mj_textWith; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshComponent.m ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // MJRefreshComponent.m // MJRefreshExample // // Created by MJ Lee on 15/3/4. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshComponent.h" #import "MJRefreshConst.h" @interface MJRefreshComponent() @property (strong, nonatomic) UIPanGestureRecognizer *pan; @end @implementation MJRefreshComponent #pragma mark - 初始化 - (instancetype)initWithFrame:(CGRect)frame { if (self = [super initWithFrame:frame]) { // 准备工作 [self prepare]; // 默认是普通状态 self.state = MJRefreshStateIdle; } return self; } - (void)prepare { // 基本属性 self.autoresizingMask = UIViewAutoresizingFlexibleWidth; self.backgroundColor = [UIColor clearColor]; } - (void)layoutSubviews { [self placeSubviews]; [super layoutSubviews]; } - (void)placeSubviews{} - (void)willMoveToSuperview:(UIView *)newSuperview { [super willMoveToSuperview:newSuperview]; // 如果不是UIScrollView,不做任何事情 if (newSuperview && ![newSuperview isKindOfClass:[UIScrollView class]]) return; // 旧的父控件移除监听 [self removeObservers]; if (newSuperview) { // 新的父控件 // 设置宽度 self.mj_w = newSuperview.mj_w; // 设置位置 self.mj_x = 0; // 记录UIScrollView _scrollView = (UIScrollView *)newSuperview; // 设置永远支持垂直弹簧效果 _scrollView.alwaysBounceVertical = YES; // 记录UIScrollView最开始的contentInset _scrollViewOriginalInset = _scrollView.contentInset; // 添加监听 [self addObservers]; } } - (void)drawRect:(CGRect)rect { [super drawRect:rect]; if (self.state == MJRefreshStateWillRefresh) { // 预防view还没显示出来就调用了beginRefreshing self.state = MJRefreshStateRefreshing; } } #pragma mark - KVO监听 - (void)addObservers { NSKeyValueObservingOptions options = NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld; [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentOffset options:options context:nil]; [self.scrollView addObserver:self forKeyPath:MJRefreshKeyPathContentSize options:options context:nil]; self.pan = self.scrollView.panGestureRecognizer; [self.pan addObserver:self forKeyPath:MJRefreshKeyPathPanState options:options context:nil]; } - (void)removeObservers { [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentOffset]; [self.superview removeObserver:self forKeyPath:MJRefreshKeyPathContentSize];; [self.pan removeObserver:self forKeyPath:MJRefreshKeyPathPanState]; self.pan = nil; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { // 遇到这些情况就直接返回 if (!self.userInteractionEnabled) return; // 这个就算看不见也需要处理 if ([keyPath isEqualToString:MJRefreshKeyPathContentSize]) { [self scrollViewContentSizeDidChange:change]; } // 看不见 if (self.hidden) return; if ([keyPath isEqualToString:MJRefreshKeyPathContentOffset]) { [self scrollViewContentOffsetDidChange:change]; } else if ([keyPath isEqualToString:MJRefreshKeyPathPanState]) { [self scrollViewPanStateDidChange:change]; } } - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change{} - (void)scrollViewContentSizeDidChange:(NSDictionary *)change{} - (void)scrollViewPanStateDidChange:(NSDictionary *)change{} #pragma mark - 公共方法 #pragma mark 设置回调对象和回调方法 - (void)setRefreshingTarget:(id)target refreshingAction:(SEL)action { self.refreshingTarget = target; self.refreshingAction = action; } - (void)setState:(MJRefreshState)state { _state = state; // 加入主队列的目的是等setState:方法调用完毕、设置完文字后再去布局子控件 dispatch_async(dispatch_get_main_queue(), ^{ [self setNeedsLayout]; }); } #pragma mark 进入刷新状态 - (void)beginRefreshing { [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ self.alpha = 1.0; }]; self.pullingPercent = 1.0; // 只要正在刷新,就完全显示 if (self.window) { self.state = MJRefreshStateRefreshing; } else { // 预防正在刷新中时,调用本方法使得header inset回置失败 if (self.state != MJRefreshStateRefreshing) { self.state = MJRefreshStateWillRefresh; // 刷新(预防从另一个控制器回到这个控制器的情况,回来要重新刷新一下) [self setNeedsDisplay]; } } } - (void)beginRefreshingWithCompletionBlock:(void (^)())completionBlock { self.beginRefreshingCompletionBlock = completionBlock; [self beginRefreshing]; } #pragma mark 结束刷新状态 - (void)endRefreshing { self.state = MJRefreshStateIdle; } - (void)endRefreshingWithCompletionBlock:(void (^)())completionBlock { self.endRefreshingCompletionBlock = completionBlock; [self endRefreshing]; } #pragma mark 是否正在刷新 - (BOOL)isRefreshing { return self.state == MJRefreshStateRefreshing || self.state == MJRefreshStateWillRefresh; } #pragma mark 自动切换透明度 - (void)setAutoChangeAlpha:(BOOL)autoChangeAlpha { self.automaticallyChangeAlpha = autoChangeAlpha; } - (BOOL)isAutoChangeAlpha { return self.isAutomaticallyChangeAlpha; } - (void)setAutomaticallyChangeAlpha:(BOOL)automaticallyChangeAlpha { _automaticallyChangeAlpha = automaticallyChangeAlpha; if (self.isRefreshing) return; if (automaticallyChangeAlpha) { self.alpha = self.pullingPercent; } else { self.alpha = 1.0; } } #pragma mark 根据拖拽进度设置透明度 - (void)setPullingPercent:(CGFloat)pullingPercent { _pullingPercent = pullingPercent; if (self.isRefreshing) return; if (self.isAutomaticallyChangeAlpha) { self.alpha = pullingPercent; } } #pragma mark - 内部方法 - (void)executeRefreshingCallback { dispatch_async(dispatch_get_main_queue(), ^{ if (self.refreshingBlock) { self.refreshingBlock(); } if ([self.refreshingTarget respondsToSelector:self.refreshingAction]) { MJRefreshMsgSend(MJRefreshMsgTarget(self.refreshingTarget), self.refreshingAction, self); } if (self.beginRefreshingCompletionBlock) { self.beginRefreshingCompletionBlock(); } }); } @end @implementation UILabel(MJRefresh) + (instancetype)mj_label { UILabel *label = [[self alloc] init]; label.font = MJRefreshLabelFont; label.textColor = MJRefreshLabelTextColor; label.autoresizingMask = UIViewAutoresizingFlexibleWidth; label.textAlignment = NSTextAlignmentCenter; label.backgroundColor = [UIColor clearColor]; return label; } - (CGFloat)mj_textWith { CGFloat stringWidth = 0; CGSize size = CGSizeMake(MAXFLOAT, MAXFLOAT); if (self.text.length > 0) { #if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 70000 stringWidth =[self.text boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:self.font} context:nil].size.width; #else stringWidth = [self.text sizeWithFont:self.font constrainedToSize:size lineBreakMode:NSLineBreakByCharWrapping].width; #endif } return stringWidth; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshFooter.h ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // MJRefreshFooter.h // MJRefreshExample // // Created by MJ Lee on 15/3/5. // Copyright (c) 2015年 小码哥. All rights reserved. // 上拉刷新控件 #import "MJRefreshComponent.h" @interface MJRefreshFooter : MJRefreshComponent /** 创建footer */ + (instancetype)footerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock; /** 创建footer */ + (instancetype)footerWithRefreshingTarget:(id)target refreshingAction:(SEL)action; /** 提示没有更多的数据 */ - (void)endRefreshingWithNoMoreData; - (void)noticeNoMoreData MJRefreshDeprecated("使用endRefreshingWithNoMoreData"); /** 重置没有更多的数据(消除没有更多数据的状态) */ - (void)resetNoMoreData; /** 忽略多少scrollView的contentInset的bottom */ @property (assign, nonatomic) CGFloat ignoredScrollViewContentInsetBottom; /** 自动根据有无数据来显示和隐藏(有数据就显示,没有数据隐藏。默认是NO) */ @property (assign, nonatomic, getter=isAutomaticallyHidden) BOOL automaticallyHidden; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshFooter.m ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // MJRefreshFooter.m // MJRefreshExample // // Created by MJ Lee on 15/3/5. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshFooter.h" @interface MJRefreshFooter() @end @implementation MJRefreshFooter #pragma mark - 构造方法 + (instancetype)footerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock { MJRefreshFooter *cmp = [[self alloc] init]; cmp.refreshingBlock = refreshingBlock; return cmp; } + (instancetype)footerWithRefreshingTarget:(id)target refreshingAction:(SEL)action { MJRefreshFooter *cmp = [[self alloc] init]; [cmp setRefreshingTarget:target refreshingAction:action]; return cmp; } #pragma mark - 重写父类的方法 - (void)prepare { [super prepare]; // 设置自己的高度 self.mj_h = MJRefreshFooterHeight; // 默认不会自动隐藏 self.automaticallyHidden = NO; } - (void)willMoveToSuperview:(UIView *)newSuperview { [super willMoveToSuperview:newSuperview]; if (newSuperview) { // 监听scrollView数据的变化 if ([self.scrollView isKindOfClass:[UITableView class]] || [self.scrollView isKindOfClass:[UICollectionView class]]) { [self.scrollView setMj_reloadDataBlock:^(NSInteger totalDataCount) { if (self.isAutomaticallyHidden) { self.hidden = (totalDataCount == 0); } }]; } } } #pragma mark - 公共方法 - (void)endRefreshingWithNoMoreData { self.state = MJRefreshStateNoMoreData; } - (void)noticeNoMoreData { [self endRefreshingWithNoMoreData]; } - (void)resetNoMoreData { self.state = MJRefreshStateIdle; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshHeader.h ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // MJRefreshHeader.h // MJRefreshExample // // Created by MJ Lee on 15/3/4. // Copyright (c) 2015年 小码哥. All rights reserved. // 下拉刷新控件:负责监控用户下拉的状态 #import "MJRefreshComponent.h" @interface MJRefreshHeader : MJRefreshComponent /** 创建header */ + (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock; /** 创建header */ + (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action; /** 这个key用来存储上一次下拉刷新成功的时间 */ @property (copy, nonatomic) NSString *lastUpdatedTimeKey; /** 上一次下拉刷新成功的时间 */ @property (strong, nonatomic, readonly) NSDate *lastUpdatedTime; /** 忽略多少scrollView的contentInset的top */ @property (assign, nonatomic) CGFloat ignoredScrollViewContentInsetTop; /** * @author block * * 启动下拉动作,但是不会调用下拉的target:Action或者block * * @since 1.0 */ - (void)beginRefreshingWithoutCallBack; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Base/MJRefreshHeader.m ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // MJRefreshHeader.m // MJRefreshExample // // Created by MJ Lee on 15/3/4. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshHeader.h" @interface MJRefreshHeader() @property (assign, nonatomic) CGFloat insetTDelta; @property (nonatomic, assign) BOOL useCallBack; @end @implementation MJRefreshHeader #pragma mark - 构造方法 + (instancetype)headerWithRefreshingBlock:(MJRefreshComponentRefreshingBlock)refreshingBlock { MJRefreshHeader *cmp = [[self alloc] init]; cmp.refreshingBlock = refreshingBlock; cmp.useCallBack = YES; return cmp; } + (instancetype)headerWithRefreshingTarget:(id)target refreshingAction:(SEL)action { MJRefreshHeader *cmp = [[self alloc] init]; [cmp setRefreshingTarget:target refreshingAction:action]; cmp.useCallBack = YES; return cmp; } - (void)beginRefreshingWithoutCallBack { self.useCallBack = NO; [super beginRefreshing]; } #pragma mark - 覆盖父类的方法 - (void)prepare { [super prepare]; // 设置key self.lastUpdatedTimeKey = MJRefreshHeaderLastUpdatedTimeKey; // 设置高度 self.mj_h = MJRefreshHeaderHeight; } - (void)placeSubviews { [super placeSubviews]; // 设置y值(当自己的高度发生改变了,肯定要重新调整Y值,所以放到placeSubviews方法中设置y值) self.mj_y = - self.mj_h - self.ignoredScrollViewContentInsetTop; } - (void)scrollViewContentOffsetDidChange:(NSDictionary *)change { [super scrollViewContentOffsetDidChange:change]; // 在刷新的refreshing状态 if (self.state == MJRefreshStateRefreshing) { if (self.window == nil) return; // sectionheader停留解决 CGFloat insetT = - self.scrollView.mj_offsetY > _scrollViewOriginalInset.top ? - self.scrollView.mj_offsetY : _scrollViewOriginalInset.top; insetT = insetT > self.mj_h + _scrollViewOriginalInset.top ? self.mj_h + _scrollViewOriginalInset.top : insetT; self.scrollView.mj_insetT = insetT; self.insetTDelta = _scrollViewOriginalInset.top - insetT; return; } // 跳转到下一个控制器时,contentInset可能会变 _scrollViewOriginalInset = self.scrollView.contentInset; // 当前的contentOffset CGFloat offsetY = self.scrollView.mj_offsetY; // 头部控件刚好出现的offsetY CGFloat happenOffsetY = - self.scrollViewOriginalInset.top; // 如果是向上滚动到看不见头部控件,直接返回 // >= -> > if (offsetY > happenOffsetY) return; // 普通 和 即将刷新 的临界点 CGFloat normal2pullingOffsetY = happenOffsetY - self.mj_h; CGFloat pullingPercent = (happenOffsetY - offsetY) / self.mj_h; if (self.scrollView.isDragging) { // 如果正在拖拽 self.pullingPercent = pullingPercent; if (self.state == MJRefreshStateIdle && offsetY < normal2pullingOffsetY) { // 转为即将刷新状态 self.state = MJRefreshStatePulling; } else if (self.state == MJRefreshStatePulling && offsetY >= normal2pullingOffsetY) { // 转为普通状态 self.state = MJRefreshStateIdle; } } else if (self.state == MJRefreshStatePulling) {// 即将刷新 && 手松开 // 开始刷新 self.useCallBack = YES; [self beginRefreshing]; } else if (pullingPercent < 1) { self.pullingPercent = pullingPercent; } } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根据状态做事情 if (state == MJRefreshStateIdle) { if (oldState != MJRefreshStateRefreshing) return; // 保存刷新时间 [[NSUserDefaults standardUserDefaults] setObject:[NSDate date] forKey:self.lastUpdatedTimeKey]; [[NSUserDefaults standardUserDefaults] synchronize]; // 恢复inset和offset [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{ self.scrollView.mj_insetT += self.insetTDelta; // 自动调整透明度 if (self.isAutomaticallyChangeAlpha) self.alpha = 0.0; } completion:^(BOOL finished) { self.pullingPercent = 0.0; if (self.endRefreshingCompletionBlock) { self.endRefreshingCompletionBlock(); } }]; } else if (state == MJRefreshStateRefreshing) { dispatch_async(dispatch_get_main_queue(), ^{ [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ CGFloat top = self.scrollViewOriginalInset.top + self.mj_h; // 增加滚动区域top self.scrollView.mj_insetT = top; // 设置滚动位置 [self.scrollView setContentOffset:CGPointMake(0, -top) animated:NO]; } completion:^(BOOL finished) { if(self.useCallBack) { [self executeRefreshingCallback]; }else { self.useCallBack = YES; } }]; }); } } #pragma mark - 公共方法 - (void)endRefreshing { dispatch_async(dispatch_get_main_queue(), ^{ self.state = MJRefreshStateIdle; }); } - (NSDate *)lastUpdatedTime { return [[NSUserDefaults standardUserDefaults] objectForKey:self.lastUpdatedTimeKey]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Auto/MJRefreshAutoGifFooter.h ================================================ // // MJRefreshAutoGifFooter.h // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshAutoStateFooter.h" @interface MJRefreshAutoGifFooter : MJRefreshAutoStateFooter @property (weak, nonatomic, readonly) UIImageView *gifView; /** 设置state状态下的动画图片images 动画持续时间duration*/ - (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state; - (void)setImages:(NSArray *)images forState:(MJRefreshState)state; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Auto/MJRefreshAutoGifFooter.m ================================================ // // MJRefreshAutoGifFooter.m // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshAutoGifFooter.h" @interface MJRefreshAutoGifFooter() { __unsafe_unretained UIImageView *_gifView; } /** 所有状态对应的动画图片 */ @property (strong, nonatomic) NSMutableDictionary *stateImages; /** 所有状态对应的动画时间 */ @property (strong, nonatomic) NSMutableDictionary *stateDurations; @end @implementation MJRefreshAutoGifFooter #pragma mark - 懒加载 - (UIImageView *)gifView { if (!_gifView) { UIImageView *gifView = [[UIImageView alloc] init]; [self addSubview:_gifView = gifView]; } return _gifView; } - (NSMutableDictionary *)stateImages { if (!_stateImages) { self.stateImages = [NSMutableDictionary dictionary]; } return _stateImages; } - (NSMutableDictionary *)stateDurations { if (!_stateDurations) { self.stateDurations = [NSMutableDictionary dictionary]; } return _stateDurations; } #pragma mark - 公共方法 - (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state { if (images == nil) return; self.stateImages[@(state)] = images; self.stateDurations[@(state)] = @(duration); /* 根据图片设置控件的高度 */ UIImage *image = [images firstObject]; if (image.size.height > self.mj_h) { self.mj_h = image.size.height; } } - (void)setImages:(NSArray *)images forState:(MJRefreshState)state { [self setImages:images duration:images.count * 0.1 forState:state]; } #pragma mark - 实现父类的方法 - (void)prepare { [super prepare]; // 初始化间距 self.labelLeftInset = 20; } - (void)placeSubviews { [super placeSubviews]; if (self.gifView.constraints.count) return; self.gifView.frame = self.bounds; if (self.isRefreshingTitleHidden) { self.gifView.contentMode = UIViewContentModeCenter; } else { self.gifView.contentMode = UIViewContentModeRight; self.gifView.mj_w = self.mj_w * 0.5 - self.labelLeftInset - self.stateLabel.mj_textWith * 0.5; } } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根据状态做事情 if (state == MJRefreshStateRefreshing) { NSArray *images = self.stateImages[@(state)]; if (images.count == 0) return; [self.gifView stopAnimating]; self.gifView.hidden = NO; if (images.count == 1) { // 单张图片 self.gifView.image = [images lastObject]; } else { // 多张图片 self.gifView.animationImages = images; self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue]; [self.gifView startAnimating]; } } else if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) { [self.gifView stopAnimating]; self.gifView.hidden = YES; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Auto/MJRefreshAutoNormalFooter.h ================================================ // // MJRefreshAutoNormalFooter.h // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshAutoStateFooter.h" @interface MJRefreshAutoNormalFooter : MJRefreshAutoStateFooter /** 菊花的样式 */ @property (assign, nonatomic) UIActivityIndicatorViewStyle activityIndicatorViewStyle; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Auto/MJRefreshAutoNormalFooter.m ================================================ // // MJRefreshAutoNormalFooter.m // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshAutoNormalFooter.h" @interface MJRefreshAutoNormalFooter() @property (weak, nonatomic) UIActivityIndicatorView *loadingView; @end @implementation MJRefreshAutoNormalFooter #pragma mark - 懒加载子控件 - (UIActivityIndicatorView *)loadingView { if (!_loadingView) { UIActivityIndicatorView *loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:self.activityIndicatorViewStyle]; loadingView.hidesWhenStopped = YES; [self addSubview:_loadingView = loadingView]; } return _loadingView; } - (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)activityIndicatorViewStyle { _activityIndicatorViewStyle = activityIndicatorViewStyle; self.loadingView = nil; [self setNeedsLayout]; } #pragma mark - 重写父类的方法 - (void)prepare { [super prepare]; self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; } - (void)placeSubviews { [super placeSubviews]; if (self.loadingView.constraints.count) return; // 圈圈 CGFloat loadingCenterX = self.mj_w * 0.5; if (!self.isRefreshingTitleHidden) { loadingCenterX -= self.stateLabel.mj_textWith * 0.5 + self.labelLeftInset; } CGFloat loadingCenterY = self.mj_h * 0.5; self.loadingView.center = CGPointMake(loadingCenterX, loadingCenterY); } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根据状态做事情 if (state == MJRefreshStateNoMoreData || state == MJRefreshStateIdle) { [self.loadingView stopAnimating]; } else if (state == MJRefreshStateRefreshing) { [self.loadingView startAnimating]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Auto/MJRefreshAutoStateFooter.h ================================================ // // MJRefreshAutoStateFooter.h // MJRefreshExample // // Created by MJ Lee on 15/6/13. // Copyright © 2015年 小码哥. All rights reserved. // #import "MJRefreshAutoFooter.h" @interface MJRefreshAutoStateFooter : MJRefreshAutoFooter /** 文字距离圈圈、箭头的距离 */ @property (assign, nonatomic) CGFloat labelLeftInset; /** 显示刷新状态的label */ @property (weak, nonatomic, readonly) UILabel *stateLabel; /** 设置state状态下的文字 */ - (void)setTitle:(NSString *)title forState:(MJRefreshState)state; /** 隐藏刷新状态的文字 */ @property (assign, nonatomic, getter=isRefreshingTitleHidden) BOOL refreshingTitleHidden; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Auto/MJRefreshAutoStateFooter.m ================================================ // // MJRefreshAutoStateFooter.m // MJRefreshExample // // Created by MJ Lee on 15/6/13. // Copyright © 2015年 小码哥. All rights reserved. // #import "MJRefreshAutoStateFooter.h" @interface MJRefreshAutoStateFooter() { /** 显示刷新状态的label */ __unsafe_unretained UILabel *_stateLabel; } /** 所有状态对应的文字 */ @property (strong, nonatomic) NSMutableDictionary *stateTitles; @end @implementation MJRefreshAutoStateFooter #pragma mark - 懒加载 - (NSMutableDictionary *)stateTitles { if (!_stateTitles) { self.stateTitles = [NSMutableDictionary dictionary]; } return _stateTitles; } - (UILabel *)stateLabel { if (!_stateLabel) { [self addSubview:_stateLabel = [UILabel mj_label]]; } return _stateLabel; } #pragma mark - 公共方法 - (void)setTitle:(NSString *)title forState:(MJRefreshState)state { if (title == nil) return; self.stateTitles[@(state)] = title; self.stateLabel.text = self.stateTitles[@(self.state)]; } #pragma mark - 私有方法 - (void)stateLabelClick { if (self.state == MJRefreshStateIdle) { [self beginRefreshing]; } } #pragma mark - 重写父类的方法 - (void)prepare { [super prepare]; // 初始化间距 self.labelLeftInset = MJRefreshLabelLeftInset; // 初始化文字 [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterIdleText] forState:MJRefreshStateIdle]; [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterRefreshingText] forState:MJRefreshStateRefreshing]; [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshAutoFooterNoMoreDataText] forState:MJRefreshStateNoMoreData]; // 监听label self.stateLabel.userInteractionEnabled = YES; [self.stateLabel addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(stateLabelClick)]]; } - (void)placeSubviews { [super placeSubviews]; if (self.stateLabel.constraints.count) return; // 状态标签 self.stateLabel.frame = self.bounds; } - (void)setState:(MJRefreshState)state { MJRefreshCheckState if (self.isRefreshingTitleHidden && state == MJRefreshStateRefreshing) { self.stateLabel.text = nil; } else { self.stateLabel.text = self.stateTitles[@(state)]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Back/MJRefreshBackGifFooter.h ================================================ // // MJRefreshBackGifFooter.h // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshBackStateFooter.h" @interface MJRefreshBackGifFooter : MJRefreshBackStateFooter @property (weak, nonatomic, readonly) UIImageView *gifView; /** 设置state状态下的动画图片images 动画持续时间duration*/ - (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state; - (void)setImages:(NSArray *)images forState:(MJRefreshState)state; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Back/MJRefreshBackGifFooter.m ================================================ // // MJRefreshBackGifFooter.m // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshBackGifFooter.h" @interface MJRefreshBackGifFooter() { __unsafe_unretained UIImageView *_gifView; } /** 所有状态对应的动画图片 */ @property (strong, nonatomic) NSMutableDictionary *stateImages; /** 所有状态对应的动画时间 */ @property (strong, nonatomic) NSMutableDictionary *stateDurations; @end @implementation MJRefreshBackGifFooter #pragma mark - 懒加载 - (UIImageView *)gifView { if (!_gifView) { UIImageView *gifView = [[UIImageView alloc] init]; [self addSubview:_gifView = gifView]; } return _gifView; } - (NSMutableDictionary *)stateImages { if (!_stateImages) { self.stateImages = [NSMutableDictionary dictionary]; } return _stateImages; } - (NSMutableDictionary *)stateDurations { if (!_stateDurations) { self.stateDurations = [NSMutableDictionary dictionary]; } return _stateDurations; } #pragma mark - 公共方法 - (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state { if (images == nil) return; self.stateImages[@(state)] = images; self.stateDurations[@(state)] = @(duration); /* 根据图片设置控件的高度 */ UIImage *image = [images firstObject]; if (image.size.height > self.mj_h) { self.mj_h = image.size.height; } } - (void)setImages:(NSArray *)images forState:(MJRefreshState)state { [self setImages:images duration:images.count * 0.1 forState:state]; } #pragma mark - 实现父类的方法 - (void)prepare { [super prepare]; // 初始化间距 self.labelLeftInset = 20; } - (void)setPullingPercent:(CGFloat)pullingPercent { [super setPullingPercent:pullingPercent]; NSArray *images = self.stateImages[@(MJRefreshStateIdle)]; if (self.state != MJRefreshStateIdle || images.count == 0) return; [self.gifView stopAnimating]; NSUInteger index = images.count * pullingPercent; if (index >= images.count) index = images.count - 1; self.gifView.image = images[index]; } - (void)placeSubviews { [super placeSubviews]; if (self.gifView.constraints.count) return; self.gifView.frame = self.bounds; if (self.stateLabel.hidden) { self.gifView.contentMode = UIViewContentModeCenter; } else { self.gifView.contentMode = UIViewContentModeRight; self.gifView.mj_w = self.mj_w * 0.5 - self.labelLeftInset - self.stateLabel.mj_textWith * 0.5; } } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根据状态做事情 if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) { NSArray *images = self.stateImages[@(state)]; if (images.count == 0) return; self.gifView.hidden = NO; [self.gifView stopAnimating]; if (images.count == 1) { // 单张图片 self.gifView.image = [images lastObject]; } else { // 多张图片 self.gifView.animationImages = images; self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue]; [self.gifView startAnimating]; } } else if (state == MJRefreshStateIdle) { self.gifView.hidden = NO; } else if (state == MJRefreshStateNoMoreData) { self.gifView.hidden = YES; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Back/MJRefreshBackNormalFooter.h ================================================ // // MJRefreshBackNormalFooter.h // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshBackStateFooter.h" @interface MJRefreshBackNormalFooter : MJRefreshBackStateFooter @property (weak, nonatomic, readonly) UIImageView *arrowView; /** 菊花的样式 */ @property (assign, nonatomic) UIActivityIndicatorViewStyle activityIndicatorViewStyle; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Back/MJRefreshBackNormalFooter.m ================================================ // // MJRefreshBackNormalFooter.m // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshBackNormalFooter.h" #import "NSBundle+MJRefresh.h" @interface MJRefreshBackNormalFooter() { __unsafe_unretained UIImageView *_arrowView; } @property (weak, nonatomic) UIActivityIndicatorView *loadingView; @end @implementation MJRefreshBackNormalFooter #pragma mark - 懒加载子控件 - (UIImageView *)arrowView { if (!_arrowView) { UIImageView *arrowView = [[UIImageView alloc] initWithImage:[NSBundle mj_arrowImage]]; [self addSubview:_arrowView = arrowView]; } return _arrowView; } - (UIActivityIndicatorView *)loadingView { if (!_loadingView) { UIActivityIndicatorView *loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:self.activityIndicatorViewStyle]; loadingView.hidesWhenStopped = YES; [self addSubview:_loadingView = loadingView]; } return _loadingView; } - (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)activityIndicatorViewStyle { _activityIndicatorViewStyle = activityIndicatorViewStyle; self.loadingView = nil; [self setNeedsLayout]; } #pragma mark - 重写父类的方法 - (void)prepare { [super prepare]; self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; } - (void)placeSubviews { [super placeSubviews]; // 箭头的中心点 CGFloat arrowCenterX = self.mj_w * 0.5; if (!self.stateLabel.hidden) { arrowCenterX -= self.labelLeftInset + self.stateLabel.mj_textWith * 0.5; } CGFloat arrowCenterY = self.mj_h * 0.5; CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY); // 箭头 if (self.arrowView.constraints.count == 0) { self.arrowView.mj_size = self.arrowView.image.size; self.arrowView.center = arrowCenter; } // 圈圈 if (self.loadingView.constraints.count == 0) { self.loadingView.center = arrowCenter; } self.arrowView.tintColor = self.stateLabel.textColor; } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根据状态做事情 if (state == MJRefreshStateIdle) { if (oldState == MJRefreshStateRefreshing) { self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI); [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{ self.loadingView.alpha = 0.0; } completion:^(BOOL finished) { self.loadingView.alpha = 1.0; [self.loadingView stopAnimating]; self.arrowView.hidden = NO; }]; } else { self.arrowView.hidden = NO; [self.loadingView stopAnimating]; [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI); }]; } } else if (state == MJRefreshStatePulling) { self.arrowView.hidden = NO; [self.loadingView stopAnimating]; [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ self.arrowView.transform = CGAffineTransformIdentity; }]; } else if (state == MJRefreshStateRefreshing) { self.arrowView.hidden = YES; [self.loadingView startAnimating]; } else if (state == MJRefreshStateNoMoreData) { self.arrowView.hidden = YES; [self.loadingView stopAnimating]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Back/MJRefreshBackStateFooter.h ================================================ // // MJRefreshBackStateFooter.h // MJRefreshExample // // Created by MJ Lee on 15/6/13. // Copyright © 2015年 小码哥. All rights reserved. // #import "MJRefreshBackFooter.h" @interface MJRefreshBackStateFooter : MJRefreshBackFooter /** 文字距离圈圈、箭头的距离 */ @property (assign, nonatomic) CGFloat labelLeftInset; /** 显示刷新状态的label */ @property (weak, nonatomic, readonly) UILabel *stateLabel; /** 设置state状态下的文字 */ - (void)setTitle:(NSString *)title forState:(MJRefreshState)state; /** 获取state状态下的title */ - (NSString *)titleForState:(MJRefreshState)state; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Footer/Back/MJRefreshBackStateFooter.m ================================================ // // MJRefreshBackStateFooter.m // MJRefreshExample // // Created by MJ Lee on 15/6/13. // Copyright © 2015年 小码哥. All rights reserved. // #import "MJRefreshBackStateFooter.h" @interface MJRefreshBackStateFooter() { /** 显示刷新状态的label */ __unsafe_unretained UILabel *_stateLabel; } /** 所有状态对应的文字 */ @property (strong, nonatomic) NSMutableDictionary *stateTitles; @end @implementation MJRefreshBackStateFooter #pragma mark - 懒加载 - (NSMutableDictionary *)stateTitles { if (!_stateTitles) { self.stateTitles = [NSMutableDictionary dictionary]; } return _stateTitles; } - (UILabel *)stateLabel { if (!_stateLabel) { [self addSubview:_stateLabel = [UILabel mj_label]]; } return _stateLabel; } #pragma mark - 公共方法 - (void)setTitle:(NSString *)title forState:(MJRefreshState)state { if (title == nil) return; self.stateTitles[@(state)] = title; self.stateLabel.text = self.stateTitles[@(self.state)]; } - (NSString *)titleForState:(MJRefreshState)state { return self.stateTitles[@(state)]; } #pragma mark - 重写父类的方法 - (void)prepare { [super prepare]; // 初始化间距 self.labelLeftInset = MJRefreshLabelLeftInset; // 初始化文字 [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterIdleText] forState:MJRefreshStateIdle]; [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterPullingText] forState:MJRefreshStatePulling]; [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterRefreshingText] forState:MJRefreshStateRefreshing]; [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshBackFooterNoMoreDataText] forState:MJRefreshStateNoMoreData]; } - (void)placeSubviews { [super placeSubviews]; if (self.stateLabel.constraints.count) return; // 状态标签 self.stateLabel.frame = self.bounds; } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 设置状态文字 self.stateLabel.text = self.stateTitles[@(state)]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Header/MJRefreshGifHeader.h ================================================ // // MJRefreshGifHeader.h // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshStateHeader.h" @interface MJRefreshGifHeader : MJRefreshStateHeader @property (weak, nonatomic, readonly) UIImageView *gifView; /** 设置state状态下的动画图片images 动画持续时间duration*/ - (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state; - (void)setImages:(NSArray *)images forState:(MJRefreshState)state; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Header/MJRefreshGifHeader.m ================================================ // // MJRefreshGifHeader.m // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshGifHeader.h" @interface MJRefreshGifHeader() { __unsafe_unretained UIImageView *_gifView; } /** 所有状态对应的动画图片 */ @property (strong, nonatomic) NSMutableDictionary *stateImages; /** 所有状态对应的动画时间 */ @property (strong, nonatomic) NSMutableDictionary *stateDurations; @end @implementation MJRefreshGifHeader #pragma mark - 懒加载 - (UIImageView *)gifView { if (!_gifView) { UIImageView *gifView = [[UIImageView alloc] init]; [self addSubview:_gifView = gifView]; } return _gifView; } - (NSMutableDictionary *)stateImages { if (!_stateImages) { self.stateImages = [NSMutableDictionary dictionary]; } return _stateImages; } - (NSMutableDictionary *)stateDurations { if (!_stateDurations) { self.stateDurations = [NSMutableDictionary dictionary]; } return _stateDurations; } #pragma mark - 公共方法 - (void)setImages:(NSArray *)images duration:(NSTimeInterval)duration forState:(MJRefreshState)state { if (images == nil) return; self.stateImages[@(state)] = images; self.stateDurations[@(state)] = @(duration); /* 根据图片设置控件的高度 */ UIImage *image = [images firstObject]; if (image.size.height > self.mj_h) { self.mj_h = image.size.height; } } - (void)setImages:(NSArray *)images forState:(MJRefreshState)state { [self setImages:images duration:images.count * 0.1 forState:state]; } #pragma mark - 实现父类的方法 - (void)prepare { [super prepare]; // 初始化间距 self.labelLeftInset = 20; } - (void)setPullingPercent:(CGFloat)pullingPercent { [super setPullingPercent:pullingPercent]; NSArray *images = self.stateImages[@(MJRefreshStateIdle)]; if (self.state != MJRefreshStateIdle || images.count == 0) return; // 停止动画 [self.gifView stopAnimating]; // 设置当前需要显示的图片 NSUInteger index = images.count * pullingPercent; if (index >= images.count) index = images.count - 1; self.gifView.image = images[index]; } - (void)placeSubviews { [super placeSubviews]; if (self.gifView.constraints.count) return; self.gifView.frame = self.bounds; if (self.stateLabel.hidden && self.lastUpdatedTimeLabel.hidden) { self.gifView.contentMode = UIViewContentModeCenter; } else { self.gifView.contentMode = UIViewContentModeRight; CGFloat stateWidth = self.stateLabel.mj_textWith; CGFloat timeWidth = 0.0; if (!self.lastUpdatedTimeLabel.hidden) { timeWidth = self.lastUpdatedTimeLabel.mj_textWith; } CGFloat textWidth = MAX(stateWidth, timeWidth); self.gifView.mj_w = self.mj_w * 0.5 - textWidth * 0.5 - self.labelLeftInset; } } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根据状态做事情 if (state == MJRefreshStatePulling || state == MJRefreshStateRefreshing) { NSArray *images = self.stateImages[@(state)]; if (images.count == 0) return; [self.gifView stopAnimating]; if (images.count == 1) { // 单张图片 self.gifView.image = [images lastObject]; } else { // 多张图片 self.gifView.animationImages = images; self.gifView.animationDuration = [self.stateDurations[@(state)] doubleValue]; [self.gifView startAnimating]; } } else if (state == MJRefreshStateIdle) { [self.gifView stopAnimating]; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Header/MJRefreshNormalHeader.h ================================================ // // MJRefreshNormalHeader.h // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshStateHeader.h" @interface MJRefreshNormalHeader : MJRefreshStateHeader @property (weak, nonatomic, readonly) UIImageView *arrowView; /** 菊花的样式 */ @property (assign, nonatomic) UIActivityIndicatorViewStyle activityIndicatorViewStyle; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Header/MJRefreshNormalHeader.m ================================================ // // MJRefreshNormalHeader.m // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshNormalHeader.h" #import "NSBundle+MJRefresh.h" @interface MJRefreshNormalHeader() { __unsafe_unretained UIImageView *_arrowView; } @property (weak, nonatomic) UIActivityIndicatorView *loadingView; @end @implementation MJRefreshNormalHeader #pragma mark - 懒加载子控件 - (UIImageView *)arrowView { if (!_arrowView) { UIImageView *arrowView = [[UIImageView alloc] initWithImage:[NSBundle mj_arrowImage]]; [self addSubview:_arrowView = arrowView]; } return _arrowView; } - (UIActivityIndicatorView *)loadingView { if (!_loadingView) { UIActivityIndicatorView *loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:self.activityIndicatorViewStyle]; loadingView.hidesWhenStopped = YES; [self addSubview:_loadingView = loadingView]; } return _loadingView; } #pragma mark - 公共方法 - (void)setActivityIndicatorViewStyle:(UIActivityIndicatorViewStyle)activityIndicatorViewStyle { _activityIndicatorViewStyle = activityIndicatorViewStyle; self.loadingView = nil; [self setNeedsLayout]; } #pragma mark - 重写父类的方法 - (void)prepare { [super prepare]; self.activityIndicatorViewStyle = UIActivityIndicatorViewStyleGray; } - (void)placeSubviews { [super placeSubviews]; // 箭头的中心点 CGFloat arrowCenterX = self.mj_w * 0.5; if (!self.stateLabel.hidden) { CGFloat stateWidth = self.stateLabel.mj_textWith; CGFloat timeWidth = 0.0; if (!self.lastUpdatedTimeLabel.hidden) { timeWidth = self.lastUpdatedTimeLabel.mj_textWith; } CGFloat textWidth = MAX(stateWidth, timeWidth); arrowCenterX -= textWidth / 2 + self.labelLeftInset; } CGFloat arrowCenterY = self.mj_h * 0.5; CGPoint arrowCenter = CGPointMake(arrowCenterX, arrowCenterY); // 箭头 if (self.arrowView.constraints.count == 0) { self.arrowView.mj_size = self.arrowView.image.size; self.arrowView.center = arrowCenter; } // 圈圈 if (self.loadingView.constraints.count == 0) { self.loadingView.center = arrowCenter; } self.arrowView.tintColor = self.stateLabel.textColor; } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 根据状态做事情 if (state == MJRefreshStateIdle) { if (oldState == MJRefreshStateRefreshing) { self.arrowView.transform = CGAffineTransformIdentity; [UIView animateWithDuration:MJRefreshSlowAnimationDuration animations:^{ self.loadingView.alpha = 0.0; } completion:^(BOOL finished) { // 如果执行完动画发现不是idle状态,就直接返回,进入其他状态 if (self.state != MJRefreshStateIdle) return; self.loadingView.alpha = 1.0; [self.loadingView stopAnimating]; self.arrowView.hidden = NO; }]; } else { [self.loadingView stopAnimating]; self.arrowView.hidden = NO; [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ self.arrowView.transform = CGAffineTransformIdentity; }]; } } else if (state == MJRefreshStatePulling) { [self.loadingView stopAnimating]; self.arrowView.hidden = NO; [UIView animateWithDuration:MJRefreshFastAnimationDuration animations:^{ self.arrowView.transform = CGAffineTransformMakeRotation(0.000001 - M_PI); }]; } else if (state == MJRefreshStateRefreshing) { self.loadingView.alpha = 1.0; // 防止refreshing -> idle的动画完毕动作没有被执行 [self.loadingView startAnimating]; self.arrowView.hidden = YES; } } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Header/MJRefreshStateHeader.h ================================================ // // MJRefreshStateHeader.h // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshHeader.h" @interface MJRefreshStateHeader : MJRefreshHeader #pragma mark - 刷新时间相关 /** 利用这个block来决定显示的更新时间文字 */ @property (copy, nonatomic) NSString *(^lastUpdatedTimeText)(NSDate *lastUpdatedTime); /** 显示上一次刷新时间的label */ @property (weak, nonatomic, readonly) UILabel *lastUpdatedTimeLabel; #pragma mark - 状态相关 /** 文字距离圈圈、箭头的距离 */ @property (assign, nonatomic) CGFloat labelLeftInset; /** 显示刷新状态的label */ @property (weak, nonatomic, readonly) UILabel *stateLabel; /** 设置state状态下的文字 */ - (void)setTitle:(NSString *)title forState:(MJRefreshState)state; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/Custom/Header/MJRefreshStateHeader.m ================================================ // // MJRefreshStateHeader.m // MJRefreshExample // // Created by MJ Lee on 15/4/24. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "MJRefreshStateHeader.h" @interface MJRefreshStateHeader() { /** 显示上一次刷新时间的label */ __unsafe_unretained UILabel *_lastUpdatedTimeLabel; /** 显示刷新状态的label */ __unsafe_unretained UILabel *_stateLabel; } /** 所有状态对应的文字 */ @property (strong, nonatomic) NSMutableDictionary *stateTitles; @end @implementation MJRefreshStateHeader #pragma mark - 懒加载 - (NSMutableDictionary *)stateTitles { if (!_stateTitles) { self.stateTitles = [NSMutableDictionary dictionary]; } return _stateTitles; } - (UILabel *)stateLabel { if (!_stateLabel) { [self addSubview:_stateLabel = [UILabel mj_label]]; } return _stateLabel; } - (UILabel *)lastUpdatedTimeLabel { if (!_lastUpdatedTimeLabel) { [self addSubview:_lastUpdatedTimeLabel = [UILabel mj_label]]; } return _lastUpdatedTimeLabel; } #pragma mark - 公共方法 - (void)setTitle:(NSString *)title forState:(MJRefreshState)state { if (title == nil) return; self.stateTitles[@(state)] = title; self.stateLabel.text = self.stateTitles[@(self.state)]; } #pragma mark - 日历获取在9.x之后的系统使用currentCalendar会出异常。在8.0之后使用系统新API。 - (NSCalendar *)currentCalendar { if ([NSCalendar respondsToSelector:@selector(calendarWithIdentifier:)]) { return [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian]; } return [NSCalendar currentCalendar]; } #pragma mark key的处理 - (void)setLastUpdatedTimeKey:(NSString *)lastUpdatedTimeKey { [super setLastUpdatedTimeKey:lastUpdatedTimeKey]; // 如果label隐藏了,就不用再处理 if (self.lastUpdatedTimeLabel.hidden) return; NSDate *lastUpdatedTime = [[NSUserDefaults standardUserDefaults] objectForKey:lastUpdatedTimeKey]; // 如果有block if (self.lastUpdatedTimeText) { self.lastUpdatedTimeLabel.text = self.lastUpdatedTimeText(lastUpdatedTime); return; } if (lastUpdatedTime) { // 1.获得年月日 NSCalendar *calendar = [self currentCalendar]; NSUInteger unitFlags = NSCalendarUnitYear| NSCalendarUnitMonth | NSCalendarUnitDay |NSCalendarUnitHour |NSCalendarUnitMinute; NSDateComponents *cmp1 = [calendar components:unitFlags fromDate:lastUpdatedTime]; NSDateComponents *cmp2 = [calendar components:unitFlags fromDate:[NSDate date]]; // 2.格式化日期 NSDateFormatter *formatter = [[NSDateFormatter alloc] init]; BOOL isToday = NO; if ([cmp1 day] == [cmp2 day]) { // 今天 formatter.dateFormat = @" HH:mm"; isToday = YES; } else if ([cmp1 year] == [cmp2 year]) { // 今年 formatter.dateFormat = @"MM-dd HH:mm"; } else { formatter.dateFormat = @"yyyy-MM-dd HH:mm"; } NSString *time = [formatter stringFromDate:lastUpdatedTime]; // 3.显示日期 self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@%@", [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText], isToday ? [NSBundle mj_localizedStringForKey:MJRefreshHeaderDateTodayText] : @"", time]; } else { self.lastUpdatedTimeLabel.text = [NSString stringWithFormat:@"%@%@", [NSBundle mj_localizedStringForKey:MJRefreshHeaderLastTimeText], [NSBundle mj_localizedStringForKey:MJRefreshHeaderNoneLastDateText]]; } } #pragma mark - 覆盖父类的方法 - (void)prepare { [super prepare]; // 初始化间距 self.labelLeftInset = MJRefreshLabelLeftInset; // 初始化文字 [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderIdleText] forState:MJRefreshStateIdle]; [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderPullingText] forState:MJRefreshStatePulling]; [self setTitle:[NSBundle mj_localizedStringForKey:MJRefreshHeaderRefreshingText] forState:MJRefreshStateRefreshing]; } - (void)placeSubviews { [super placeSubviews]; if (self.stateLabel.hidden) return; BOOL noConstrainsOnStatusLabel = self.stateLabel.constraints.count == 0; if (self.lastUpdatedTimeLabel.hidden) { // 状态 if (noConstrainsOnStatusLabel) self.stateLabel.frame = self.bounds; } else { CGFloat stateLabelH = self.mj_h * 0.5; // 状态 if (noConstrainsOnStatusLabel) { self.stateLabel.mj_x = 0; self.stateLabel.mj_y = 0; self.stateLabel.mj_w = self.mj_w; self.stateLabel.mj_h = stateLabelH; } // 更新时间 if (self.lastUpdatedTimeLabel.constraints.count == 0) { self.lastUpdatedTimeLabel.mj_x = 0; self.lastUpdatedTimeLabel.mj_y = stateLabelH; self.lastUpdatedTimeLabel.mj_w = self.mj_w; self.lastUpdatedTimeLabel.mj_h = self.mj_h - self.lastUpdatedTimeLabel.mj_y; } } } - (void)setState:(MJRefreshState)state { MJRefreshCheckState // 设置状态文字 self.stateLabel.text = self.stateTitles[@(state)]; // 重新设置key(重新显示时间) self.lastUpdatedTimeKey = self.lastUpdatedTimeKey; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/MJRefresh.bundle/zh-Hant.lproj/Localizable.strings ================================================ "MJRefreshHeaderIdleText" = "下拉可以刷新"; "MJRefreshHeaderPullingText" = "鬆開立即刷新"; "MJRefreshHeaderRefreshingText" = "正在刷新數據中..."; "MJRefreshAutoFooterIdleText" = "點擊或上拉加載更多"; "MJRefreshAutoFooterRefreshingText" = "正在加載更多的數據..."; "MJRefreshAutoFooterNoMoreDataText" = "已經全部加載完畢"; "MJRefreshBackFooterIdleText" = "上拉可以加載更多"; "MJRefreshBackFooterPullingText" = "鬆開立即加載更多"; "MJRefreshBackFooterRefreshingText" = "正在加載更多的數據..."; "MJRefreshBackFooterNoMoreDataText" = "已經全部加載完畢"; "MJRefreshHeaderLastTimeText" = "最後更新:"; "MJRefreshHeaderDateTodayText" = "今天"; "MJRefreshHeaderNoneLastDateText" = "無記錄"; ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/MJRefresh.h ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 #import "UIScrollView+MJRefresh.h" #import "UIScrollView+MJExtension.h" #import "UIView+MJExtension.h" #import "MJRefreshNormalHeader.h" #import "MJRefreshGifHeader.h" #import "MJRefreshBackNormalFooter.h" #import "MJRefreshBackGifFooter.h" #import "MJRefreshAutoNormalFooter.h" #import "MJRefreshAutoGifFooter.h" ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/MJRefreshConst.h ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 #import #import // 弱引用 #define MJWeakSelf __weak typeof(self) weakSelf = self; // 日志输出 #ifdef DEBUG #define MJRefreshLog(...) NSLog(__VA_ARGS__) #else #define MJRefreshLog(...) #endif // 过期提醒 #define MJRefreshDeprecated(instead) NS_DEPRECATED(2_0, 2_0, 2_0, 2_0, instead) // 运行时objc_msgSend #define MJRefreshMsgSend(...) ((void (*)(void *, SEL, UIView *))objc_msgSend)(__VA_ARGS__) #define MJRefreshMsgTarget(target) (__bridge void *)(target) // RGB颜色 #define MJRefreshColor(r, g, b) [UIColor colorWithRed:(r)/255.0 green:(g)/255.0 blue:(b)/255.0 alpha:1.0] // 文字颜色 #define MJRefreshLabelTextColor MJRefreshColor(90, 90, 90) // 字体大小 #define MJRefreshLabelFont [UIFont boldSystemFontOfSize:14] // 常量 UIKIT_EXTERN const CGFloat MJRefreshLabelLeftInset; UIKIT_EXTERN const CGFloat MJRefreshHeaderHeight; UIKIT_EXTERN const CGFloat MJRefreshFooterHeight; UIKIT_EXTERN const CGFloat MJRefreshFastAnimationDuration; UIKIT_EXTERN const CGFloat MJRefreshSlowAnimationDuration; UIKIT_EXTERN NSString *const MJRefreshKeyPathContentOffset; UIKIT_EXTERN NSString *const MJRefreshKeyPathContentSize; UIKIT_EXTERN NSString *const MJRefreshKeyPathContentInset; UIKIT_EXTERN NSString *const MJRefreshKeyPathPanState; UIKIT_EXTERN NSString *const MJRefreshHeaderLastUpdatedTimeKey; UIKIT_EXTERN NSString *const MJRefreshHeaderIdleText; UIKIT_EXTERN NSString *const MJRefreshHeaderPullingText; UIKIT_EXTERN NSString *const MJRefreshHeaderRefreshingText; UIKIT_EXTERN NSString *const MJRefreshAutoFooterIdleText; UIKIT_EXTERN NSString *const MJRefreshAutoFooterRefreshingText; UIKIT_EXTERN NSString *const MJRefreshAutoFooterNoMoreDataText; UIKIT_EXTERN NSString *const MJRefreshBackFooterIdleText; UIKIT_EXTERN NSString *const MJRefreshBackFooterPullingText; UIKIT_EXTERN NSString *const MJRefreshBackFooterRefreshingText; UIKIT_EXTERN NSString *const MJRefreshBackFooterNoMoreDataText; UIKIT_EXTERN NSString *const MJRefreshHeaderLastTimeText; UIKIT_EXTERN NSString *const MJRefreshHeaderDateTodayText; UIKIT_EXTERN NSString *const MJRefreshHeaderNoneLastDateText; // 状态检查 #define MJRefreshCheckState \ MJRefreshState oldState = self.state; \ if (state == oldState) return; \ [super setState:state]; ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/MJRefreshConst.m ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 #import const CGFloat MJRefreshLabelLeftInset = 25; const CGFloat MJRefreshHeaderHeight = 54.0; const CGFloat MJRefreshFooterHeight = 44.0; const CGFloat MJRefreshFastAnimationDuration = 0.25; const CGFloat MJRefreshSlowAnimationDuration = 0.4; NSString *const MJRefreshKeyPathContentOffset = @"contentOffset"; NSString *const MJRefreshKeyPathContentInset = @"contentInset"; NSString *const MJRefreshKeyPathContentSize = @"contentSize"; NSString *const MJRefreshKeyPathPanState = @"state"; NSString *const MJRefreshHeaderLastUpdatedTimeKey = @"MJRefreshHeaderLastUpdatedTimeKey"; NSString *const MJRefreshHeaderIdleText = @"MJRefreshHeaderIdleText"; NSString *const MJRefreshHeaderPullingText = @"MJRefreshHeaderPullingText"; NSString *const MJRefreshHeaderRefreshingText = @"MJRefreshHeaderRefreshingText"; NSString *const MJRefreshAutoFooterIdleText = @"MJRefreshAutoFooterIdleText"; NSString *const MJRefreshAutoFooterRefreshingText = @"MJRefreshAutoFooterRefreshingText"; NSString *const MJRefreshAutoFooterNoMoreDataText = @"MJRefreshAutoFooterNoMoreDataText"; NSString *const MJRefreshBackFooterIdleText = @"MJRefreshBackFooterIdleText"; NSString *const MJRefreshBackFooterPullingText = @"MJRefreshBackFooterPullingText"; NSString *const MJRefreshBackFooterRefreshingText = @"MJRefreshBackFooterRefreshingText"; NSString *const MJRefreshBackFooterNoMoreDataText = @"MJRefreshBackFooterNoMoreDataText"; NSString *const MJRefreshHeaderLastTimeText = @"MJRefreshHeaderLastTimeText"; NSString *const MJRefreshHeaderDateTodayText = @"MJRefreshHeaderDateTodayText"; NSString *const MJRefreshHeaderNoneLastDateText = @"MJRefreshHeaderNoneLastDateText"; ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/NSBundle+MJRefresh.h ================================================ // // NSBundle+MJRefresh.h // MJRefreshExample // // Created by MJ Lee on 16/6/13. // Copyright © 2016年 小码哥. All rights reserved. // #import @interface NSBundle (MJRefresh) + (instancetype)mj_refreshBundle; + (UIImage *)mj_arrowImage; + (NSString *)mj_localizedStringForKey:(NSString *)key value:(NSString *)value; + (NSString *)mj_localizedStringForKey:(NSString *)key; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/NSBundle+MJRefresh.m ================================================ // // NSBundle+MJRefresh.m // MJRefreshExample // // Created by MJ Lee on 16/6/13. // Copyright © 2016年 小码哥. All rights reserved. // #import "NSBundle+MJRefresh.h" #import "MJRefreshComponent.h" @implementation NSBundle (MJRefresh) + (instancetype)mj_refreshBundle { static NSBundle *refreshBundle = nil; if (refreshBundle == nil) { // 这里不使用mainBundle是为了适配pod 1.x和0.x refreshBundle = [NSBundle bundleWithPath:[[NSBundle bundleForClass:[MJRefreshComponent class]] pathForResource:@"MJRefresh" ofType:@"bundle"]]; } return refreshBundle; } + (UIImage *)mj_arrowImage { static UIImage *arrowImage = nil; if (arrowImage == nil) { arrowImage = [[UIImage imageWithContentsOfFile:[[self mj_refreshBundle] pathForResource:@"arrow@2x" ofType:@"png"]] imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; } return arrowImage; } + (NSString *)mj_localizedStringForKey:(NSString *)key { return [self mj_localizedStringForKey:key value:nil]; } + (NSString *)mj_localizedStringForKey:(NSString *)key value:(NSString *)value { static NSBundle *bundle = nil; if (bundle == nil) { // (iOS获取的语言字符串比较不稳定)目前框架只处理en、zh-Hans、zh-Hant三种情况,其他按照系统默认处理 NSString *language = [NSLocale preferredLanguages].firstObject; if ([language hasPrefix:@"en"]) { language = @"en"; } else if ([language hasPrefix:@"zh"]) { if ([language rangeOfString:@"Hans"].location != NSNotFound) { language = @"zh-Hans"; // 简体中文 } else { // zh-Hant\zh-HK\zh-TW language = @"zh-Hant"; // 繁體中文 } } else { language = @"en"; } // 从MJRefresh.bundle中查找资源 bundle = [NSBundle bundleWithPath:[[NSBundle mj_refreshBundle] pathForResource:language ofType:@"lproj"]]; } value = [bundle localizedStringForKey:key value:value table:nil]; return [[NSBundle mainBundle] localizedStringForKey:key value:value table:nil]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/UIScrollView+MJExtension.h ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // UIScrollView+Extension.h // MJRefreshExample // // Created by MJ Lee on 14-5-28. // Copyright (c) 2014年 小码哥. All rights reserved. // #import @interface UIScrollView (MJExtension) @property (assign, nonatomic) CGFloat mj_insetT; @property (assign, nonatomic) CGFloat mj_insetB; @property (assign, nonatomic) CGFloat mj_insetL; @property (assign, nonatomic) CGFloat mj_insetR; @property (assign, nonatomic) CGFloat mj_offsetX; @property (assign, nonatomic) CGFloat mj_offsetY; @property (assign, nonatomic) CGFloat mj_contentW; @property (assign, nonatomic) CGFloat mj_contentH; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/UIScrollView+MJExtension.m ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // UIScrollView+Extension.m // MJRefreshExample // // Created by MJ Lee on 14-5-28. // Copyright (c) 2014年 小码哥. All rights reserved. // #import "UIScrollView+MJExtension.h" #import @implementation UIScrollView (MJExtension) - (void)setMj_insetT:(CGFloat)mj_insetT { UIEdgeInsets inset = self.contentInset; inset.top = mj_insetT; self.contentInset = inset; } - (CGFloat)mj_insetT { return self.contentInset.top; } - (void)setMj_insetB:(CGFloat)mj_insetB { UIEdgeInsets inset = self.contentInset; inset.bottom = mj_insetB; self.contentInset = inset; } - (CGFloat)mj_insetB { return self.contentInset.bottom; } - (void)setMj_insetL:(CGFloat)mj_insetL { UIEdgeInsets inset = self.contentInset; inset.left = mj_insetL; self.contentInset = inset; } - (CGFloat)mj_insetL { return self.contentInset.left; } - (void)setMj_insetR:(CGFloat)mj_insetR { UIEdgeInsets inset = self.contentInset; inset.right = mj_insetR; self.contentInset = inset; } - (CGFloat)mj_insetR { return self.contentInset.right; } - (void)setMj_offsetX:(CGFloat)mj_offsetX { CGPoint offset = self.contentOffset; offset.x = mj_offsetX; self.contentOffset = offset; } - (CGFloat)mj_offsetX { return self.contentOffset.x; } - (void)setMj_offsetY:(CGFloat)mj_offsetY { CGPoint offset = self.contentOffset; offset.y = mj_offsetY; self.contentOffset = offset; } - (CGFloat)mj_offsetY { return self.contentOffset.y; } - (void)setMj_contentW:(CGFloat)mj_contentW { CGSize size = self.contentSize; size.width = mj_contentW; self.contentSize = size; } - (CGFloat)mj_contentW { return self.contentSize.width; } - (void)setMj_contentH:(CGFloat)mj_contentH { CGSize size = self.contentSize; size.height = mj_contentH; self.contentSize = size; } - (CGFloat)mj_contentH { return self.contentSize.height; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/UIScrollView+MJRefresh.h ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // UIScrollView+MJRefresh.h // MJRefreshExample // // Created by MJ Lee on 15/3/4. // Copyright (c) 2015年 小码哥. All rights reserved. // 给ScrollView增加下拉刷新、上拉刷新的功能 #import #import "MJRefreshConst.h" @class MJRefreshHeader, MJRefreshFooter; @interface UIScrollView (MJRefresh) /** 下拉刷新控件 */ @property (strong, nonatomic) MJRefreshHeader *mj_header; @property (strong, nonatomic) MJRefreshHeader *header MJRefreshDeprecated("使用mj_header"); /** 上拉刷新控件 */ @property (strong, nonatomic) MJRefreshFooter *mj_footer; @property (strong, nonatomic) MJRefreshFooter *footer MJRefreshDeprecated("使用mj_footer"); #pragma mark - other - (NSInteger)mj_totalDataCount; @property (copy, nonatomic) void (^mj_reloadDataBlock)(NSInteger totalDataCount); @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/UIScrollView+MJRefresh.m ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // UIScrollView+MJRefresh.m // MJRefreshExample // // Created by MJ Lee on 15/3/4. // Copyright (c) 2015年 小码哥. All rights reserved. // #import "UIScrollView+MJRefresh.h" #import "MJRefreshHeader.h" #import "MJRefreshFooter.h" #import @implementation NSObject (MJRefresh) + (void)exchangeInstanceMethod1:(SEL)method1 method2:(SEL)method2 { method_exchangeImplementations(class_getInstanceMethod(self, method1), class_getInstanceMethod(self, method2)); } + (void)exchangeClassMethod1:(SEL)method1 method2:(SEL)method2 { method_exchangeImplementations(class_getClassMethod(self, method1), class_getClassMethod(self, method2)); } @end @implementation UIScrollView (MJRefresh) #pragma mark - header static const char MJRefreshHeaderKey = '\0'; - (void)setMj_header:(MJRefreshHeader *)mj_header { if (mj_header != self.mj_header) { // 删除旧的,添加新的 [self.mj_header removeFromSuperview]; [self insertSubview:mj_header atIndex:0]; // 存储新的 [self willChangeValueForKey:@"mj_header"]; // KVO objc_setAssociatedObject(self, &MJRefreshHeaderKey, mj_header, OBJC_ASSOCIATION_ASSIGN); [self didChangeValueForKey:@"mj_header"]; // KVO } } - (MJRefreshHeader *)mj_header { return objc_getAssociatedObject(self, &MJRefreshHeaderKey); } #pragma mark - footer static const char MJRefreshFooterKey = '\0'; - (void)setMj_footer:(MJRefreshFooter *)mj_footer { if (mj_footer != self.mj_footer) { // 删除旧的,添加新的 [self.mj_footer removeFromSuperview]; [self insertSubview:mj_footer atIndex:0]; // 存储新的 [self willChangeValueForKey:@"mj_footer"]; // KVO objc_setAssociatedObject(self, &MJRefreshFooterKey, mj_footer, OBJC_ASSOCIATION_ASSIGN); [self didChangeValueForKey:@"mj_footer"]; // KVO } } - (MJRefreshFooter *)mj_footer { return objc_getAssociatedObject(self, &MJRefreshFooterKey); } #pragma mark - 过期 - (void)setFooter:(MJRefreshFooter *)footer { self.mj_footer = footer; } - (MJRefreshFooter *)footer { return self.mj_footer; } - (void)setHeader:(MJRefreshHeader *)header { self.mj_header = header; } - (MJRefreshHeader *)header { return self.mj_header; } #pragma mark - other - (NSInteger)mj_totalDataCount { NSInteger totalCount = 0; if ([self isKindOfClass:[UITableView class]]) { UITableView *tableView = (UITableView *)self; for (NSInteger section = 0; section @interface UIView (MJExtension) @property (assign, nonatomic) CGFloat mj_x; @property (assign, nonatomic) CGFloat mj_y; @property (assign, nonatomic) CGFloat mj_w; @property (assign, nonatomic) CGFloat mj_h; @property (assign, nonatomic) CGSize mj_size; @property (assign, nonatomic) CGPoint mj_origin; @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/MJRefresh/UIView+MJExtension.m ================================================ // 代码地址: https://github.com/CoderMJLee/MJRefresh // 代码地址: http://code4app.com/ios/%E5%BF%AB%E9%80%9F%E9%9B%86%E6%88%90%E4%B8%8B%E6%8B%89%E4%B8%8A%E6%8B%89%E5%88%B7%E6%96%B0/52326ce26803fabc46000000 // UIView+Extension.m // MJRefreshExample // // Created by MJ Lee on 14-5-28. // Copyright (c) 2014年 小码哥. All rights reserved. // #import "UIView+MJExtension.h" @implementation UIView (MJExtension) - (void)setMj_x:(CGFloat)mj_x { CGRect frame = self.frame; frame.origin.x = mj_x; self.frame = frame; } - (CGFloat)mj_x { return self.frame.origin.x; } - (void)setMj_y:(CGFloat)mj_y { CGRect frame = self.frame; frame.origin.y = mj_y; self.frame = frame; } - (CGFloat)mj_y { return self.frame.origin.y; } - (void)setMj_w:(CGFloat)mj_w { CGRect frame = self.frame; frame.size.width = mj_w; self.frame = frame; } - (CGFloat)mj_w { return self.frame.size.width; } - (void)setMj_h:(CGFloat)mj_h { CGRect frame = self.frame; frame.size.height = mj_h; self.frame = frame; } - (CGFloat)mj_h { return self.frame.size.height; } - (void)setMj_size:(CGSize)mj_size { CGRect frame = self.frame; frame.size = mj_size; self.frame = frame; } - (CGSize)mj_size { return self.frame.size; } - (void)setMj_origin:(CGPoint)mj_origin { CGRect frame = self.frame; frame.origin = mj_origin; self.frame = frame; } - (CGPoint)mj_origin { return self.frame.origin; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/TJPCache/TJPCacheManager.h ================================================ // // TJPCacheManager.h // // // Created by 唐佳鹏 on 2025/1/18. // #import #import "TJPCacheProtocol.h" NS_ASSUME_NONNULL_BEGIN typedef NS_ENUM(NSUInteger, TJPCacheStrategy) { TJPCacheStrategyCacheFirst, // 缓存优先 TJPCacheStrategyNetworkFirst, // 网络优先 TJPCacheStrategyStaleWhileRevalidate // 缓存返回,后台更新 }; typedef NS_ENUM(NSUInteger, TJPCacheUpdateReason) { TJPCacheUpdateReasonExpired, // 过期 TJPCacheUpdateReasonUserRefresh, // 用户刷新 TJPCacheUpdateReasonDataChanged, // 数据变更 TJPCacheUpdateReasonForceUpdate // 强制更新 }; @interface TJPCacheManager : NSObject // 缓存策略的选择(可以是内存、磁盘或数据库缓存) @property (nonatomic, strong) id cacheStrategy; @property (nonatomic, assign) TJPCacheStrategy defaultStrategy; // 缓存配置 @property (nonatomic, assign) NSUInteger maxCacheSize; // 最大缓存大小 @property (nonatomic, assign) NSUInteger maxCacheCount; // 最大缓存数量 @property (nonatomic, assign) BOOL autoCleanupEnabled; // 自动清理 // 常用缓存时间 extern NSTimeInterval const TJPCacheExpireTimeShort; // 短期缓存:5分钟 extern NSTimeInterval const TJPCacheExpireTimeMedium; // 中期缓存:1小时 extern NSTimeInterval const TJPCacheExpireTimeLong; // 长期缓存:24小时 // 初始化 - (instancetype)initWithCacheStrategy:(id)cacheStrategy defaultStrategy:(TJPCacheStrategy)strategy; // 智能缓存操作 - (void)fetchDataForKey:(NSString *)key strategy:(TJPCacheStrategy)strategy networkFetch:(id(^)(void))networkFetch completion:(void(^)(id data, BOOL fromCache, NSError *error))completion; // 缓存失效管理 - (void)invalidateCacheForKey:(NSString *)key reason:(TJPCacheUpdateReason)reason; - (void)invalidateCacheWithPattern:(NSString *)pattern; // 缓存预加载 //- (void)preloadCacheForKeys:(NSArray *)keys // networkFetch:(NSDictionary *(^)(NSArray *keys))networkFetch; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Tools/TJPCache/TJPCacheManager.m ================================================ // // TJPCacheManager.m // // // Created by 唐佳鹏 on 2025/1/18. // #import "TJPCacheManager.h" // 常用缓存时间定义 NSTimeInterval const TJPCacheExpireTimeShort = 5 * 60; // 5分钟 NSTimeInterval const TJPCacheExpireTimeMedium = 60 * 60; // 1小时 NSTimeInterval const TJPCacheExpireTimeLong = 24 * 60 * 60; // 24小时 @interface TJPCacheManager () @property (nonatomic, strong) NSMutableDictionary *refreshStrategies; @property (nonatomic, strong) NSMutableSet *refreshingKeys; @property (nonatomic, strong) dispatch_queue_t cacheQueue; @property (nonatomic, strong) NSTimer *cleanupTimer; @end @implementation TJPCacheManager - (instancetype)initWithCacheStrategy:(id)cacheStrategy defaultStrategy:(TJPCacheStrategy)strategy { self = [super init]; if (self) { _cacheStrategy = cacheStrategy; _defaultStrategy = strategy; _maxCacheSize = 50 * 1024 * 1024; // 50MB _maxCacheCount = 1000; _autoCleanupEnabled = YES; [self setupAutoCleanup]; } return self; } - (void)fetchDataForKey:(NSString *)key strategy:(TJPCacheStrategy)strategy networkFetch:(id(^)(void))networkFetch completion:(void(^)(id data, BOOL fromCache, NSError *error))completion { switch (strategy) { case TJPCacheStrategyCacheFirst: [self fetchWithCacheFirst:key networkFetch:networkFetch completion:completion]; break; case TJPCacheStrategyNetworkFirst: [self fetchWithNetworkFirst:key networkFetch:networkFetch completion:completion]; break; case TJPCacheStrategyStaleWhileRevalidate: [self fetchWithStaleWhileRevalidate:key networkFetch:networkFetch completion:completion]; break; } } #pragma mark - 缓存策略实现 - (void)fetchWithCacheFirst:(NSString *)key networkFetch:(id(^)(void))networkFetch completion:(void(^)(id data, BOOL fromCache, NSError *error))completion { // 1. 先检查缓存 id cachedData = [self.cacheStrategy loadCacheForKey:key]; if (cachedData) { if (completion) completion(cachedData, YES, nil); return; } // 2. 缓存未命中,请求网络 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @try { id networkData = networkFetch ? networkFetch() : nil; if (networkData) { // 保存到缓存 [self saveCacheWithData:networkData forKey:key expireTime:TJPCacheExpireTimeMedium]; dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(networkData, NO, nil); }); } else { dispatch_async(dispatch_get_main_queue(), ^{ NSError *error = [NSError errorWithDomain:@"TJPCacheError" code:1001 userInfo:@{NSLocalizedDescriptionKey: @"网络请求返回空数据"}]; if (completion) completion(nil, NO, error); }); } } @catch (NSException *exception) { dispatch_async(dispatch_get_main_queue(), ^{ NSError *error = [NSError errorWithDomain:@"TJPCacheError" code:1002 userInfo:@{NSLocalizedDescriptionKey: exception.reason}]; if (completion) completion(nil, NO, error); }); } }); } - (void)fetchWithNetworkFirst:(NSString *)key networkFetch:(id(^)(void))networkFetch completion:(void(^)(id data, BOOL fromCache, NSError *error))completion { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @try { // 1. 先请求网络 id networkData = networkFetch ? networkFetch() : nil; if (networkData) { // 保存到缓存 [self saveCacheWithData:networkData forKey:key expireTime:TJPCacheExpireTimeMedium]; dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(networkData, NO, nil); }); return; } } @catch (NSException *exception) { NSLog(@"网络请求失败: %@", exception.reason); } // 2. 网络失败,尝试缓存 id cachedData = [self.cacheStrategy loadCacheForKey:key]; dispatch_async(dispatch_get_main_queue(), ^{ if (cachedData) { if (completion) completion(cachedData, YES, nil); } else { NSError *error = [NSError errorWithDomain:@"TJPCacheError" code:1003 userInfo:@{NSLocalizedDescriptionKey: @"网络请求失败且无缓存数据"}]; if (completion) completion(nil, NO, error); } }); }); } - (void)fetchWithStaleWhileRevalidate:(NSString *)key networkFetch:(id(^)(void))networkFetch completion:(void(^)(id data, BOOL fromCache, NSError *error))completion { // 1. 立即返回缓存数据(如果有) id cachedData = [self.cacheStrategy loadCacheForKey:key]; if (cachedData) { if (completion) completion(cachedData, YES, nil); } // 2. 同时在后台更新数据 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ @try { id networkData = networkFetch ? networkFetch() : nil; if (networkData) { // 更新缓存 [self saveCacheWithData:networkData forKey:key expireTime:TJPCacheExpireTimeMedium]; // 如果之前没有缓存,现在返回网络数据 if (!cachedData) { dispatch_async(dispatch_get_main_queue(), ^{ if (completion) completion(networkData, NO, nil); }); } // 发送数据更新通知 dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:@"TJPCacheDidUpdateNotification" object:nil userInfo:@{@"key": key, @"data": networkData}]; }); } } @catch (NSException *exception) { NSLog(@"后台更新失败: %@", exception.reason); } }); // 3. 如果没有缓存数据,返回错误 if (!cachedData) { NSError *error = [NSError errorWithDomain:@"TJPCacheError" code:1004 userInfo:@{NSLocalizedDescriptionKey: @"无缓存数据且后台更新中"}]; if (completion) completion(nil, NO, error); } } #pragma mark - 缓存失效管理 - (void)invalidateCacheForKey:(NSString *)key reason:(TJPCacheUpdateReason)reason { [self.cacheStrategy removeCacheForKey:key]; NSLog(@"缓存失效 - Key: %@, 原因: %lu", key, (unsigned long)reason); // 可以根据原因执行不同的后续操作 switch (reason) { case TJPCacheUpdateReasonDataChanged: // 数据变更时,可能需要同步更新相关缓存 [self invalidateRelatedCacheForKey:key]; break; default: break; } } - (void)invalidateCacheWithPattern:(NSString *)pattern { NSArray *allKeys = [self.cacheStrategy allCacheKeys]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF LIKE %@", pattern]; NSArray *matchingKeys = [allKeys filteredArrayUsingPredicate:predicate]; for (NSString *key in matchingKeys) { [self.cacheStrategy removeCacheForKey:key]; } NSLog(@"批量清理缓存 - 模式: %@, 清理数量: %lu", pattern, (unsigned long)matchingKeys.count); } #pragma mark - 私有方法 - (void)invalidateRelatedCacheForKey:(NSString *)key { // 根据业务逻辑清理相关缓存 // 例如:用户信息更新时,清理用户相关的所有缓存 if ([key containsString:@"user_"]) { [self invalidateCacheWithPattern:@"user_*"]; } } - (void)setupAutoCleanup { if (!self.autoCleanupEnabled) return; // 监听内存警告 [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleMemoryWarning) name:UIApplicationDidReceiveMemoryWarningNotification object:nil]; // 定期清理过期缓存 NSTimer *cleanupTimer = [NSTimer scheduledTimerWithTimeInterval:300 // 5分钟 target:self selector:@selector(performPeriodicCleanup) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:cleanupTimer forMode:NSRunLoopCommonModes]; } - (void)handleMemoryWarning { NSLog(@"收到内存警告,开始清理缓存"); // 清理一部分缓存以释放内存 NSArray *allKeys = [self.cacheStrategy allCacheKeys]; NSUInteger clearCount = allKeys.count / 2; // 清理一半 for (NSUInteger i = 0; i < clearCount && i < allKeys.count; i++) { [self.cacheStrategy removeCacheForKey:allKeys[i]]; } } - (void)performPeriodicCleanup { // 定期清理:检查缓存大小和数量 NSUInteger currentSize = [self.cacheStrategy cacheSize]; NSUInteger currentCount = [self.cacheStrategy allCacheKeys].count; if (currentSize > self.maxCacheSize || currentCount > self.maxCacheCount) { NSLog(@"缓存超限,开始清理 - 当前大小: %lu, 当前数量: %lu", (unsigned long)currentSize, (unsigned long)currentCount); // 可以实现LRU清理策略 [self performLRUCleanup]; } } - (void)performLRUCleanup { //简化为清理一部分缓存 NSArray *allKeys = [self.cacheStrategy allCacheKeys]; NSUInteger clearCount = allKeys.count / 4; // 清理1/4 for (NSUInteger i = 0; i < clearCount && i < allKeys.count; i++) { [self.cacheStrategy removeCacheForKey:allKeys[i]]; } } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; } #pragma mark - TJPCacheProtocol Implementation - (void)saveCacheWithData:(id)data forKey:(NSString *)key expireTime:(NSTimeInterval)expireTime { [self.cacheStrategy saveCacheWithData:data forKey:key expireTime:expireTime]; } - (id)loadCacheForKey:(NSString *)key { return [self.cacheStrategy loadCacheForKey:key]; } - (void)removeCacheForKey:(NSString *)key { [self.cacheStrategy removeCacheForKey:key]; } - (void)clearAllCache { [self.cacheStrategy clearAllCache]; } - (BOOL)hasCacheForKey:(NSString *)key { return [self.cacheStrategy hasCacheForKey:key]; } - (NSTimeInterval)remainingTimeForKey:(NSString *)key { return [self.cacheStrategy remainingTimeForKey:key]; } - (NSUInteger)cacheSize { return [self.cacheStrategy cacheSize]; } - (void)removeCacheWithKeyPrefix:(NSString *)keyPrefix { [self.cacheStrategy removeCacheWithKeyPrefix:keyPrefix]; } - (NSArray *)allCacheKeys { return [self.cacheStrategy allCacheKeys]; } //- (void)preloadCacheForKeys:(nonnull NSArray *)keys networkFetch:(nonnull NSDictionary * _Nonnull (^)(NSArray * _Nonnull __strong))networkFetch { //} @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/TJPCache/TJPCacheProtocol.h ================================================ // // TJPCacheProtocol.h // // // Created by 唐佳鹏 on 2025/1/18. // #import NS_ASSUME_NONNULL_BEGIN @protocol TJPCacheProtocol // 基础缓存操作 - (void)saveCacheWithData:(id)data forKey:(NSString *)key expireTime:(NSTimeInterval)expireTime; - (id)loadCacheForKey:(NSString *)key; - (void)removeCacheForKey:(NSString *)key; - (void)clearAllCache; // 缓存数据查询 - (BOOL)hasCacheForKey:(NSString *)key; - (NSTimeInterval)remainingTimeForKey:(NSString *)key; - (NSUInteger)cacheSize; // 批量操作 - (void)removeCacheWithKeyPrefix:(NSString *)keyPrefix; - (NSArray *)allCacheKeys; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Tools/TJPCache/TJPDiskCache.h ================================================ // // TJPDiskCache.h // // // Created by 唐佳鹏 on 2025/1/18. // #import #import "TJPCacheProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPDiskCache : NSObject @property (nonatomic, strong) NSString *cacheDirectory; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Tools/TJPCache/TJPDiskCache.m ================================================ // // TJPDiskCache.m // // // Created by 唐佳鹏 on 2025/1/18. // #import "TJPDiskCache.h" @implementation TJPDiskCache - (instancetype)init { self = [super init]; if (self) { // 设定缓存目录,默认在应用沙盒的Documents目录 NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); _cacheDirectory = [paths.firstObject stringByAppendingPathComponent:@"cache"]; // 创建缓存目录 if (![[NSFileManager defaultManager] fileExistsAtPath:self.cacheDirectory]) { [[NSFileManager defaultManager] createDirectoryAtPath:self.cacheDirectory withIntermediateDirectories:YES attributes:nil error:nil]; } } return self; } // 存储缓存数据,添加过期时间 - (void)saveCacheWithData:(id)data forKey:(NSString *)key expireTime:(NSTimeInterval)expireTime { if (data && key) { NSString *filePath = [self cacheFilePathForKey:key]; [data writeToFile:filePath atomically:YES]; // 存储过期时间 NSTimeInterval expiryTimestamp = [[NSDate date] timeIntervalSince1970] + expireTime; NSNumber *expiryNumber = @(expiryTimestamp); NSData *expiryData = [NSKeyedArchiver archivedDataWithRootObject:expiryNumber]; NSString *expiryFilePath = [self cacheExpiryFilePathForKey:key]; [expiryData writeToFile:expiryFilePath atomically:YES]; } } // 读取缓存数据,并检查是否过期 - (id)loadCacheForKey:(NSString *)key { NSString *filePath = [self cacheFilePathForKey:key]; NSString *expiryFilePath = [self cacheExpiryFilePathForKey:key]; if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { NSData *expiryData = [NSData dataWithContentsOfFile:expiryFilePath]; NSNumber *expiryTimestamp = [NSKeyedUnarchiver unarchiveObjectWithData:expiryData]; if (expiryTimestamp) { NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; if (currentTimestamp > [expiryTimestamp doubleValue]) { // 缓存过期,删除缓存并返回nil [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; [[NSFileManager defaultManager] removeItemAtPath:expiryFilePath error:nil]; return nil; } } return [NSData dataWithContentsOfFile:filePath]; // 返回数据 } return nil; } // 删除缓存数据 - (void)removeCacheForKey:(NSString *)key { NSString *filePath = [self cacheFilePathForKey:key]; NSString *expiryFilePath = [self cacheExpiryFilePathForKey:key]; if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) { [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; } if ([[NSFileManager defaultManager] fileExistsAtPath:expiryFilePath]) { [[NSFileManager defaultManager] removeItemAtPath:expiryFilePath error:nil]; } } // 清除所有缓存 - (void)clearAllCache { NSError *error = nil; NSArray *files = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.cacheDirectory error:&error]; if (!error) { for (NSString *file in files) { NSString *filePath = [self.cacheDirectory stringByAppendingPathComponent:file]; [[NSFileManager defaultManager] removeItemAtPath:filePath error:nil]; } } } // 获取缓存文件路径 - (NSString *)cacheFilePathForKey:(NSString *)key { return [self.cacheDirectory stringByAppendingPathComponent:key]; } // 获取缓存过期时间文件路径 - (NSString *)cacheExpiryFilePathForKey:(NSString *)key { return [[self.cacheDirectory stringByAppendingPathComponent:key] stringByAppendingPathExtension:@"expiry"]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/TJPCache/TJPMemoryCache.h ================================================ // // TJPMemoryCache.h // // // Created by 唐佳鹏 on 2025/1/18. // #import #import "TJPCacheProtocol.h" NS_ASSUME_NONNULL_BEGIN @interface TJPMemoryCache : NSObject @property (nonatomic, strong) NSCache *cache; @property (nonatomic, strong) NSMutableDictionary *cacheExpiryTimes; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Tools/TJPCache/TJPMemoryCache.m ================================================ // // TJPMemoryCache.m // // // Created by 唐佳鹏 on 2025/1/18. // #import "TJPMemoryCache.h" #import "TJPNetworkDefine.h" @interface TJPMemoryCache () @property (nonatomic, strong) dispatch_queue_t cacheQueue; @end @implementation TJPMemoryCache - (instancetype)init { self = [super init]; if (self) { _cache = [[NSCache alloc] init]; _cacheExpiryTimes = [NSMutableDictionary dictionary]; _cacheQueue = dispatch_queue_create("com.tjp.memory.cache", DISPATCH_QUEUE_CONCURRENT); // 设置NSCache的一些默认配置 _cache.countLimit = 100; // 默认最大缓存对象数量 _cache.totalCostLimit = 10 * 1024 * 1024; // 默认最大缓存大小 10MB } return self; } #pragma mark - TJPCacheProtocol Implementation - (void)saveCacheWithData:(id)data forKey:(NSString *)key { [self saveCacheWithData:data forKey:key expireTime:3600]; // 默认1小时过期 } // 存储缓存数据,添加过期时间 - (void)saveCacheWithData:(id)data forKey:(NSString *)key expireTime:(NSTimeInterval)expireTime { if (data && key) { dispatch_barrier_async(self.cacheQueue, ^{ [self.cache setObject:data forKey:key]; TJPLOG_INFO(@"TJPMemoryCache save cache with data for key: %@", key); // 设置过期时间 NSTimeInterval expiryTimestamp = [[NSDate date] timeIntervalSince1970] + expireTime; self.cacheExpiryTimes[key] = @(expiryTimestamp); }); } } // 读取缓存数据,并检查是否过期 - (id)loadCacheForKey:(NSString *)key { if (!key) return nil; __block id result = nil; dispatch_sync(self.cacheQueue, ^{ NSNumber *expiryTimestamp = self.cacheExpiryTimes[key]; if (expiryTimestamp) { NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; if (currentTimestamp > [expiryTimestamp doubleValue]) { // 缓存过期,删除缓存并返回nil [self.cache removeObjectForKey:key]; [self.cacheExpiryTimes removeObjectForKey:key]; TJPLOG_INFO(@"TJPMemoryCache cache expired for key: %@", key); return; } } result = [self.cache objectForKey:key]; if (result) { TJPLOG_INFO(@"TJPMemoryCache load cache for key: %@", key); } }); return result; } // 删除缓存数据 - (void)removeCacheForKey:(NSString *)key { if (!key) return; dispatch_barrier_async(self.cacheQueue, ^{ [self.cache removeObjectForKey:key]; [self.cacheExpiryTimes removeObjectForKey:key]; TJPLOG_INFO(@"TJPMemoryCache remove cache for key: %@", key); }); } // 清除所有缓存 - (void)clearAllCache { dispatch_barrier_async(self.cacheQueue, ^{ [self.cache removeAllObjects]; [self.cacheExpiryTimes removeAllObjects]; TJPLOG_INFO(@"TJPMemoryCache clear all cache"); }); } // 检查是否存在指定key的缓存 - (BOOL)hasCacheForKey:(NSString *)key { if (!key) return NO; __block BOOL hasCache = NO; dispatch_sync(self.cacheQueue, ^{ // 先检查是否过期 NSNumber *expiryTimestamp = self.cacheExpiryTimes[key]; if (expiryTimestamp) { NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; if (currentTimestamp > [expiryTimestamp doubleValue]) { // 已过期 [self.cache removeObjectForKey:key]; [self.cacheExpiryTimes removeObjectForKey:key]; hasCache = NO; return; } } hasCache = [self.cache objectForKey:key] != nil; }); return hasCache; } // 获取指定key的缓存剩余时间 - (NSTimeInterval)remainingTimeForKey:(NSString *)key { if (!key) return 0; __block NSTimeInterval remainingTime = 0; dispatch_sync(self.cacheQueue, ^{ NSNumber *expiryTimestamp = self.cacheExpiryTimes[key]; if (expiryTimestamp) { NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; remainingTime = [expiryTimestamp doubleValue] - currentTimestamp; if (remainingTime < 0) { remainingTime = 0; // 清理过期缓存 [self.cache removeObjectForKey:key]; [self.cacheExpiryTimes removeObjectForKey:key]; } } }); return remainingTime; } // 获取当前缓存大小(估算) - (NSUInteger)cacheSize { __block NSUInteger size = 0; dispatch_sync(self.cacheQueue, ^{ // NSCache没有直接获取大小的方法,这里返回缓存对象数量 // 可以根据实际需要估算或精确计算 size = self.cacheExpiryTimes.count; }); return size; } // 根据key前缀删除缓存 - (void)removeCacheWithKeyPrefix:(NSString *)keyPrefix { if (!keyPrefix || keyPrefix.length == 0) return; dispatch_barrier_async(self.cacheQueue, ^{ NSArray *allKeys = [self.cacheExpiryTimes.allKeys copy]; NSMutableArray *keysToRemove = [NSMutableArray array]; for (NSString *key in allKeys) { if ([key hasPrefix:keyPrefix]) { [keysToRemove addObject:key]; } } for (NSString *key in keysToRemove) { [self.cache removeObjectForKey:key]; [self.cacheExpiryTimes removeObjectForKey:key]; } TJPLOG_INFO(@"TJPMemoryCache remove %lu caches with prefix: %@", (unsigned long)keysToRemove.count, keyPrefix); }); } // 获取所有缓存的key - (NSArray *)allCacheKeys { __block NSArray *allKeys = nil; dispatch_sync(self.cacheQueue, ^{ // 清理过期缓存后返回有效的keys NSMutableArray *validKeys = [NSMutableArray array]; NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; NSArray *keys = [self.cacheExpiryTimes.allKeys copy]; for (NSString *key in keys) { NSNumber *expiryTimestamp = self.cacheExpiryTimes[key]; if (expiryTimestamp) { if (currentTimestamp <= [expiryTimestamp doubleValue]) { [validKeys addObject:key]; } else { // 清理过期缓存 [self.cache removeObjectForKey:key]; [self.cacheExpiryTimes removeObjectForKey:key]; } } else { [validKeys addObject:key]; } } allKeys = [validKeys copy]; }); return allKeys; } #pragma mark - Helper Methods // 清理过期缓存 - (void)cleanExpiredCache { dispatch_barrier_async(self.cacheQueue, ^{ NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; NSArray *allKeys = [self.cacheExpiryTimes.allKeys copy]; NSMutableArray *expiredKeys = [NSMutableArray array]; for (NSString *key in allKeys) { NSNumber *expiryTimestamp = self.cacheExpiryTimes[key]; if (expiryTimestamp && currentTimestamp > [expiryTimestamp doubleValue]) { [expiredKeys addObject:key]; } } for (NSString *key in expiredKeys) { [self.cache removeObjectForKey:key]; [self.cacheExpiryTimes removeObjectForKey:key]; } if (expiredKeys.count > 0) { TJPLOG_INFO(@"TJPMemoryCache cleaned %lu expired caches", (unsigned long)expiredKeys.count); } }); } // 获取缓存统计信息 - (NSDictionary *)cacheStatistics { __block NSDictionary *stats = nil; dispatch_sync(self.cacheQueue, ^{ NSUInteger totalCount = self.cacheExpiryTimes.count; NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; NSUInteger expiredCount = 0; for (NSString *key in self.cacheExpiryTimes.allKeys) { NSNumber *expiryTimestamp = self.cacheExpiryTimes[key]; if (expiryTimestamp && currentTimestamp > [expiryTimestamp doubleValue]) { expiredCount++; } } stats = @{ @"totalCount": @(totalCount), @"validCount": @(totalCount - expiredCount), @"expiredCount": @(expiredCount) }; }); return stats; } #pragma mark - Dealloc - (void)dealloc { [self clearAllCache]; } @end ================================================ FILE: iOS-Network-Stack-Dive/Tools/TJPUITools/TJPToast.h ================================================ // // TJPToast.h // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import NS_ASSUME_NONNULL_BEGIN @interface TJPToastLabel : UILabel - (void)setMessageText:(NSString *)text; @end @interface TJPToast : NSObject + (instancetype)shareInstance; + (void)show:(NSString *)title duration:(CGFloat)duration; + (void)show:(NSString *)title duration:(CGFloat)duration controller:(UIViewController *)controller; @end NS_ASSUME_NONNULL_END ================================================ FILE: iOS-Network-Stack-Dive/Tools/TJPUITools/TJPToast.m ================================================ // // TJPToast.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/31. // #import "TJPToast.h" #import "TJPNetworkDefine.h" static int changeCount; @interface TJPToast () @property (nonatomic, strong) TJPToastLabel *toastLabel; @property (nonatomic, strong) NSTimer *countTimer; @end @implementation TJPToast + (instancetype)shareInstance { static TJPToast *singleton = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ singleton = [[TJPToast alloc] init]; }); return singleton; } /** * 初始化方法 * * @return 自身 */ - (instancetype)init { self = [super init]; if (self) { self.toastLabel = [[TJPToastLabel alloc]init]; self.countTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(changeTime) userInfo:nil repeats:YES]; self.countTimer.fireDate = [NSDate distantFuture];//关闭定时器 } return self; } /** * 弹出并显示Toast * * @param title 显示的文本内容 * @param duration 显示时间 */ + (void)show:(NSString *)title duration:(CGFloat)duration { if ([title length] == 0) { return; } TJPToast *instance = [self shareInstance]; [instance.toastLabel setMessageText:title]; [[[UIApplication sharedApplication]keyWindow] addSubview:instance.toastLabel]; instance.toastLabel.alpha = 0.8; instance.countTimer.fireDate = [NSDate distantPast];//开启定时器 changeCount = duration; } + (void)show:(NSString *)title duration:(CGFloat)duration controller:(UIViewController *)controller { if ([title length] == 0) { return; } TJPToast *instance = [self shareInstance]; [instance.toastLabel setMessageText:title]; [controller.view.window addSubview:instance.toastLabel]; instance.toastLabel.alpha = 0.8; instance.countTimer.fireDate = [NSDate distantPast];//开启定时器 changeCount = duration; } /** * 定时器回调方法 */ - (void)changeTime { //NSLog(@"时间:%d",changeCount); if(changeCount-- <= 0){ self.countTimer.fireDate = [NSDate distantFuture];//关闭定时器 [UIView animateWithDuration:0.2f animations:^{ self.toastLabel.alpha = 0; } completion:^(BOOL finished) { [self.toastLabel removeFromSuperview]; }]; } } @end @implementation TJPToastLabel - (instancetype)init { self = [super init]; if (self) { self.layer.cornerRadius = 8; self.layer.masksToBounds = YES; self.backgroundColor = [UIColor blackColor]; self.numberOfLines = 0; self.textAlignment = NSTextAlignmentCenter; self.textColor = [UIColor whiteColor]; self.font = [UIFont systemFontOfSize:15]; } return self; } /** * 设置显示的文字 * * @param text 文字文本 */ - (void)setMessageText:(NSString *)text{ [self setText:text]; CGRect rect = [self.text boundingRectWithSize:CGSizeMake(TJPSCREEN_WIDTH-20, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading attributes:@{NSFontAttributeName:self.font} context:nil]; CGFloat width = rect.size.width + 20; CGFloat height = rect.size.height + 20; CGFloat x = (TJPSCREEN_WIDTH-width)/2; CGFloat y = (TJPSCREEN_HEIGHT-height)/2+30; self.frame = CGRectMake(x, y, width, height); } @end ================================================ FILE: iOS-Network-Stack-Dive/main.m ================================================ // // main.m // iOS-Network-Stack-Dive // // Created by 唐佳鹏 on 2025/3/17. // #import #import "AppDelegate.h" int main(int argc, char * argv[]) { NSString * appDelegateClassName; @autoreleasepool { // Setup code that might create autoreleased objects goes here. appDelegateClassName = NSStringFromClass([AppDelegate class]); } return UIApplicationMain(argc, argv, nil, appDelegateClassName); } ================================================ FILE: iOS-Network-Stack-Dive.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 77; objects = { /* Begin PBXBuildFile section */ 24C50012F14F83F293E426BE /* Pods_iOS_Network_Stack_Dive.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4F2EA2A7836F1CD8C4C3AC53 /* Pods_iOS_Network_Stack_Dive.framework */; }; 5578BAC2CA12A2D66DB80215 /* Pods_iOS_Network_Stack_DiveTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = AD9264FD75E624955CEBBE63 /* Pods_iOS_Network_Stack_DiveTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ 967117852D8C0B3E00EC1DDF /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 962A7EBA2D8830A6009FAD4E /* Project object */; proxyType = 1; remoteGlobalIDString = 962A7EC12D8830A6009FAD4E; remoteInfo = "iOS-Network-Stack-Dive"; }; /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ 4F2EA2A7836F1CD8C4C3AC53 /* Pods_iOS_Network_Stack_Dive.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS_Network_Stack_Dive.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 66780951CD89AF39D328AE34 /* Pods-iOS-Network-Stack-Dive.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-Network-Stack-Dive.release.xcconfig"; path = "Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive.release.xcconfig"; sourceTree = ""; }; 836E3947B7E85C6A6018B079 /* Pods-iOS-Network-Stack-DiveTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-Network-Stack-DiveTests.debug.xcconfig"; path = "Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests.debug.xcconfig"; sourceTree = ""; }; 962A7EC22D8830A6009FAD4E /* iOS-Network-Stack-Dive.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "iOS-Network-Stack-Dive.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 967117812D8C0B3E00EC1DDF /* iOS-Network-Stack-DiveTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "iOS-Network-Stack-DiveTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 9E50FF3D64A644566F3B466B /* Pods-iOS-Network-Stack-DiveTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-Network-Stack-DiveTests.release.xcconfig"; path = "Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests.release.xcconfig"; sourceTree = ""; }; 9F441CEC2666DAA60D9D506D /* Pods-iOS-Network-Stack-Dive.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iOS-Network-Stack-Dive.debug.xcconfig"; path = "Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive.debug.xcconfig"; sourceTree = ""; }; AD9264FD75E624955CEBBE63 /* Pods_iOS_Network_Stack_DiveTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iOS_Network_Stack_DiveTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ 962A7ED92D8830A7009FAD4E /* Exceptions for "iOS-Network-Stack-Dive" folder in "iOS-Network-Stack-Dive" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, ); target = 962A7EC12D8830A6009FAD4E /* iOS-Network-Stack-Dive */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ 962A7EC42D8830A6009FAD4E /* iOS-Network-Stack-Dive */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( 962A7ED92D8830A7009FAD4E /* Exceptions for "iOS-Network-Stack-Dive" folder in "iOS-Network-Stack-Dive" target */, ); path = "iOS-Network-Stack-Dive"; sourceTree = ""; }; 967117822D8C0B3E00EC1DDF /* iOS-Network-Stack-DiveTests */ = { isa = PBXFileSystemSynchronizedRootGroup; path = "iOS-Network-Stack-DiveTests"; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ 962A7EBF2D8830A6009FAD4E /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 24C50012F14F83F293E426BE /* Pods_iOS_Network_Stack_Dive.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; 9671177E2D8C0B3E00EC1DDF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( 5578BAC2CA12A2D66DB80215 /* Pods_iOS_Network_Stack_DiveTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ 435473B925D2EDC36316A6B6 /* Pods */ = { isa = PBXGroup; children = ( 9F441CEC2666DAA60D9D506D /* Pods-iOS-Network-Stack-Dive.debug.xcconfig */, 66780951CD89AF39D328AE34 /* Pods-iOS-Network-Stack-Dive.release.xcconfig */, 836E3947B7E85C6A6018B079 /* Pods-iOS-Network-Stack-DiveTests.debug.xcconfig */, 9E50FF3D64A644566F3B466B /* Pods-iOS-Network-Stack-DiveTests.release.xcconfig */, ); path = Pods; sourceTree = ""; }; 962A7EB92D8830A6009FAD4E = { isa = PBXGroup; children = ( 962A7EC42D8830A6009FAD4E /* iOS-Network-Stack-Dive */, 967117822D8C0B3E00EC1DDF /* iOS-Network-Stack-DiveTests */, 962A7EC32D8830A6009FAD4E /* Products */, 435473B925D2EDC36316A6B6 /* Pods */, BC0BA4C581DDCA5BDF41CE12 /* Frameworks */, ); sourceTree = ""; }; 962A7EC32D8830A6009FAD4E /* Products */ = { isa = PBXGroup; children = ( 962A7EC22D8830A6009FAD4E /* iOS-Network-Stack-Dive.app */, 967117812D8C0B3E00EC1DDF /* iOS-Network-Stack-DiveTests.xctest */, ); name = Products; sourceTree = ""; }; BC0BA4C581DDCA5BDF41CE12 /* Frameworks */ = { isa = PBXGroup; children = ( 4F2EA2A7836F1CD8C4C3AC53 /* Pods_iOS_Network_Stack_Dive.framework */, AD9264FD75E624955CEBBE63 /* Pods_iOS_Network_Stack_DiveTests.framework */, ); name = Frameworks; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ 962A7EC12D8830A6009FAD4E /* iOS-Network-Stack-Dive */ = { isa = PBXNativeTarget; buildConfigurationList = 962A7EDA2D8830A7009FAD4E /* Build configuration list for PBXNativeTarget "iOS-Network-Stack-Dive" */; buildPhases = ( E0E86FAE27DECE0BD8EC9700 /* [CP] Check Pods Manifest.lock */, 962A7EBE2D8830A6009FAD4E /* Sources */, 962A7EBF2D8830A6009FAD4E /* Frameworks */, 962A7EC02D8830A6009FAD4E /* Resources */, 3293DA1E5CBB6EDAA548E21D /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( 962A7EC42D8830A6009FAD4E /* iOS-Network-Stack-Dive */, ); name = "iOS-Network-Stack-Dive"; productName = "iOS-Network-Stack-Dive"; productReference = 962A7EC22D8830A6009FAD4E /* iOS-Network-Stack-Dive.app */; productType = "com.apple.product-type.application"; }; 967117802D8C0B3E00EC1DDF /* iOS-Network-Stack-DiveTests */ = { isa = PBXNativeTarget; buildConfigurationList = 967117872D8C0B3E00EC1DDF /* Build configuration list for PBXNativeTarget "iOS-Network-Stack-DiveTests" */; buildPhases = ( 8D23112F1C131CE88148489B /* [CP] Check Pods Manifest.lock */, 9671177D2D8C0B3E00EC1DDF /* Sources */, 9671177E2D8C0B3E00EC1DDF /* Frameworks */, 9671177F2D8C0B3E00EC1DDF /* Resources */, E4A809A2274B18549A1A67E9 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( 967117862D8C0B3E00EC1DDF /* PBXTargetDependency */, ); fileSystemSynchronizedGroups = ( 967117822D8C0B3E00EC1DDF /* iOS-Network-Stack-DiveTests */, ); name = "iOS-Network-Stack-DiveTests"; productName = "iOS-Network-Stack-DiveTests"; productReference = 967117812D8C0B3E00EC1DDF /* iOS-Network-Stack-DiveTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ 962A7EBA2D8830A6009FAD4E /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; CLASSPREFIX = TJP; LastUpgradeCheck = 1620; TargetAttributes = { 962A7EC12D8830A6009FAD4E = { CreatedOnToolsVersion = 16.2; }; 967117802D8C0B3E00EC1DDF = { CreatedOnToolsVersion = 16.2; TestTargetID = 962A7EC12D8830A6009FAD4E; }; }; }; buildConfigurationList = 962A7EBD2D8830A6009FAD4E /* Build configuration list for PBXProject "iOS-Network-Stack-Dive" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = 962A7EB92D8830A6009FAD4E; minimizedProjectReferenceProxies = 1; preferredProjectObjectVersion = 77; productRefGroup = 962A7EC32D8830A6009FAD4E /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 962A7EC12D8830A6009FAD4E /* iOS-Network-Stack-Dive */, 967117802D8C0B3E00EC1DDF /* iOS-Network-Stack-DiveTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ 962A7EC02D8830A6009FAD4E /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 9671177F2D8C0B3E00EC1DDF /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ 3293DA1E5CBB6EDAA548E21D /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-Dive/Pods-iOS-Network-Stack-Dive-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; 8D23112F1C131CE88148489B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-iOS-Network-Stack-DiveTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; E0E86FAE27DECE0BD8EC9700 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( ); inputPaths = ( "${PODS_PODFILE_DIR_PATH}/Podfile.lock", "${PODS_ROOT}/Manifest.lock", ); name = "[CP] Check Pods Manifest.lock"; outputFileListPaths = ( ); outputPaths = ( "$(DERIVED_FILE_DIR)/Pods-iOS-Network-Stack-Dive-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; E4A809A2274B18549A1A67E9 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iOS-Network-Stack-DiveTests/Pods-iOS-Network-Stack-DiveTests-frameworks.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ 962A7EBE2D8830A6009FAD4E /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; 9671177D2D8C0B3E00EC1DDF /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ 967117862D8C0B3E00EC1DDF /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 962A7EC12D8830A6009FAD4E /* iOS-Network-Stack-Dive */; targetProxy = 967117852D8C0B3E00EC1DDF /* PBXContainerItemProxy */; }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ 962A7EDB2D8830A7009FAD4E /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9F441CEC2666DAA60D9D506D /* Pods-iOS-Network-Stack-Dive.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4V7EL5BTH5; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS-Network-Stack-Dive/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.aaronT.www.iOS-Network-Stack-Dive"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; 962A7EDC2D8830A7009FAD4E /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 66780951CD89AF39D328AE34 /* Pods-iOS-Network-Stack-Dive.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4V7EL5BTH5; ENABLE_USER_SCRIPT_SANDBOXING = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "iOS-Network-Stack-Dive/Info.plist"; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); LIBRARY_SEARCH_PATHS = "$(inherited)"; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.aaronT.www.iOS-Network-Stack-Dive"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; 962A7EDD2D8830A7009FAD4E /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; }; name = Debug; }; 962A7EDE2D8830A7009FAD4E /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 18.2; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = iphoneos; VALIDATE_PRODUCT = YES; }; name = Release; }; 967117882D8C0B3E00EC1DDF /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 836E3947B7E85C6A6018B079 /* Pods-iOS-Network-Stack-DiveTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4V7EL5BTH5; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = ""; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.aaronT.www.iOS-Network-Stack-DiveTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOS-Network-Stack-Dive.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOS-Network-Stack-Dive"; }; name = Debug; }; 967117892D8C0B3E00EC1DDF /* Release */ = { isa = XCBuildConfiguration; baseConfigurationReference = 9E50FF3D64A644566F3B466B /* Pods-iOS-Network-Stack-DiveTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_TEAM = 4V7EL5BTH5; ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_PREFIX_HEADER = ""; GENERATE_INFOPLIST_FILE = YES; IPHONEOS_DEPLOYMENT_TARGET = 15.6; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.aaronT.www.iOS-Network-Stack-DiveTests"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/iOS-Network-Stack-Dive.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOS-Network-Stack-Dive"; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ 962A7EBD2D8830A6009FAD4E /* Build configuration list for PBXProject "iOS-Network-Stack-Dive" */ = { isa = XCConfigurationList; buildConfigurations = ( 962A7EDD2D8830A7009FAD4E /* Debug */, 962A7EDE2D8830A7009FAD4E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 962A7EDA2D8830A7009FAD4E /* Build configuration list for PBXNativeTarget "iOS-Network-Stack-Dive" */ = { isa = XCConfigurationList; buildConfigurations = ( 962A7EDB2D8830A7009FAD4E /* Debug */, 962A7EDC2D8830A7009FAD4E /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; 967117872D8C0B3E00EC1DDF /* Build configuration list for PBXNativeTarget "iOS-Network-Stack-DiveTests" */ = { isa = XCConfigurationList; buildConfigurations = ( 967117882D8C0B3E00EC1DDF /* Debug */, 967117892D8C0B3E00EC1DDF /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = 962A7EBA2D8830A6009FAD4E /* Project object */; } ================================================ FILE: iOS-Network-Stack-Dive.xcodeproj/project.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: iOS-Network-Stack-Dive.xcodeproj/xcshareddata/xcschemes/iOS-Network-Stack-Dive.xcscheme ================================================ ================================================ FILE: iOS-Network-Stack-Dive.xcodeproj/xcuserdata/aarongtang.xcuserdatad/xcschemes/xcschememanagement.plist ================================================ SchemeUserState iOS-Network-Stack-Dive.xcscheme_^#shared#^_ orderHint 12 SuppressBuildableAutocreation 962A7EC12D8830A6009FAD4E primary 967117802D8C0B3E00EC1DDF primary ================================================ FILE: iOS-Network-Stack-Dive.xcworkspace/contents.xcworkspacedata ================================================ ================================================ FILE: iOS-Network-Stack-Dive.xcworkspace/xcuserdata/aarongtang.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist ================================================ ================================================ FILE: iOS-Network-Stack-DiveTests/ArchitectureExtensions/AOP/TJPLoggerTests.m ================================================ // // TJPLoggerTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/26. // #import @interface TJPLoggerTests : XCTestCase @end @implementation TJPLoggerTests - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/ArchitectureExtensions/Network/TJPConnectStateMachineMetricsTest.m ================================================ // // TJPConnectStateMachineMetricsTest.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/4/9. // #import #import "TJPConnectStateMachine.h" #import "TJPMetricsCollector.h" @interface TJPConnectStateMachineMetricsTest : XCTestCase @end @implementation TJPConnectStateMachineMetricsTest - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testStateDurationTracking { // 初始化状态机并设置初始状态为 Disconnected TJPConnectStateMachine *machine = [[TJPConnectStateMachine alloc] initWithInitialState:TJPConnectStateDisconnected]; // 添加转换规则 [machine addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateConnecting forEvent:TJPConnectEventConnect]; [machine addTransitionFromState:TJPConnectStateConnecting toState:TJPConnectStateConnected forEvent:TJPConnectEventConnectSuccess]; [machine addTransitionFromState:TJPConnectStateConnecting toState:TJPConnectStateDisconnected forEvent:TJPConnectEventConnectFailure]; [machine addTransitionFromState:TJPConnectStateConnected toState:TJPConnectStateDisconnecting forEvent:TJPConnectEventDisconnect]; [machine addTransitionFromState:TJPConnectStateDisconnecting toState:TJPConnectStateDisconnected forEvent:TJPConnectEventDisconnectComplete]; XCTestExpectation *expectation = [self expectationWithDescription:@"State transition completed"]; // 触发从 Disconnected -> Connecting 状态转换 [machine sendEvent:TJPConnectEventConnect]; // -> Connecting NSLog(@"当前状态: %@", machine.currentState); // 延时 1 秒后触发 ConnectSuccess 事件,转到 Connected dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [machine sendEvent:TJPConnectEventConnectSuccess]; // -> Connected NSLog(@"当前状态: %@", machine.currentState); // 获取 Connecting 状态的持续时间 NSTimeInterval duration = [[TJPMetricsCollector sharedInstance] averageStateDuration:TJPConnectStateConnecting]; NSLog(@"Connecting 状态持续时间: %.2f 秒", duration); // 验证 Connecting 状态持续时间是否接近 1.0 秒 XCTAssertTrue(fabs(duration - 1.0) < 0.05, @"持续时间误差应小于 50ms"); // 触发期望,完成测试 [expectation fulfill]; // 完成测试 }); // 等待期望,最大等待时间 1.5 秒 [self waitForExpectationsWithTimeout:1.5 handler:^(NSError * _Nullable error) { if (error) { NSLog(@"Test failed: %@", error.localizedDescription); } }]; } - (void)testHighFrequencyStateChanges { TJPConnectStateMachine *machine = [[TJPConnectStateMachine alloc] initWithInitialState:TJPConnectStateDisconnected]; [machine addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateConnecting forEvent:TJPConnectEventConnect]; [machine addTransitionFromState:TJPConnectStateConnecting toState:TJPConnectStateConnected forEvent:TJPConnectEventConnectSuccess]; // 测量性能 [self measureBlock:^{ for (int i = 0; i < 10000; i++) { // 触发连接事件 [machine sendEvent:TJPConnectEventConnect]; // 立即触发连接成功事件,模拟连接过程中的状态变化 [machine sendEvent:TJPConnectEventConnectSuccess]; } }]; // 验证事件的平均持续时间 NSTimeInterval connectEventDuration = [[TJPMetricsCollector sharedInstance] averageEventDuration:TJPConnectEventConnect]; NSTimeInterval connectSuccessEventDuration = [[TJPMetricsCollector sharedInstance] averageEventDuration:TJPConnectEventConnectSuccess]; // 确保每个事件的持续时间大于零 XCTAssertTrue(connectEventDuration > 0, @"Connect event should have non-zero duration"); XCTAssertTrue(connectSuccessEventDuration > 0, @"ConnectSuccess event should have non-zero duration"); // 进一步验证事件处理时间在合理范围内,可以根据具体业务逻辑调整 XCTAssertTrue(connectEventDuration < 0.05, @"Connect event duration should be under 50ms"); XCTAssertTrue(connectSuccessEventDuration < 0.05, @"ConnectSuccess event duration should be under 50ms"); } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/ArchitectureExtensions/VIPER/TJPNavigationCoordinatorTests.m ================================================ // // TJPNavigationCoordinatorTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/4/1. // #import #import "TJPNavigationCoordinator.h" #import "TJPViewPushHandler.h" #import "OCMock/OCMock.h" @interface TJPNavigationCoordinatorTests : XCTestCase @end @implementation TJPNavigationCoordinatorTests - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testHandlerRegistration { // Given TJPNavigationCoordinator *coordinator = [TJPNavigationCoordinator sharedInstance]; id mockHandler = OCMProtocolMock(@protocol(TJPViperBaseRouterHandlerProtocol)); // 创建一个期望值,用于同步测试 XCTestExpectation *expectation = [self expectationWithDescription:@"Handler registration"]; // When [coordinator registerHandler:mockHandler forRouteType:TJPNavigationRouteTypeViewPush]; // 给异步队列一定的时间来完成注册 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // Then id handler = [coordinator.handlers objectForKey:@(TJPNavigationRouteTypeViewPush)]; XCTAssertEqualObjects(handler, mockHandler, @"处理器注册失败"); // 完成期望值 [expectation fulfill]; }); // 等待期望值完成 [self waitForExpectations:@[expectation] timeout:1.0]; } - (void)testSingleThreadRegistrationPerformance { TJPNavigationCoordinator *coordinator = [TJPNavigationCoordinator sharedInstance]; NSArray *routeTypes = @[@1, @2, @3]; // 实际业务路由类型 [self measureMetrics:@[XCTPerformanceMetric_WallClockTime] automaticallyStartMeasuring:NO forBlock:^{ // 预热缓存 [coordinator registerHandler:[TJPViewPushHandler new] forRouteType:1]; [self startMeasuring]; // 正式测试 for (int i = 0; i < 500; i++) { NSNumber *type = routeTypes[i % routeTypes.count]; [coordinator registerHandler:[TJPViewPushHandler new] forRouteType:type.integerValue]; } [self stopMeasuring]; // 清理 [routeTypes enumerateObjectsUsingBlock:^(NSNumber *t, NSUInteger idx, BOOL * _Nonnull stop) { [coordinator unregisterHandlerForRouteType:t.integerValue]; }]; }]; } - (void)testConcurrentRegistrationPerformance { TJPNavigationCoordinator *coordinator = [TJPNavigationCoordinator sharedInstance]; NSArray *routeTypes = @[@1, @2, @3]; [self measureBlock:^{ dispatch_group_t group = dispatch_group_create(); for (int i = 0; i < 1000; i++) { dispatch_group_enter(group); NSNumber *type = routeTypes[arc4random_uniform((uint32_t)routeTypes.count)]; TJPViewPushHandler *handler = [TJPViewPushHandler new]; dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ [coordinator registerHandler:handler forRouteType:type.integerValue]; dispatch_group_leave(group); }); } dispatch_group_wait(group, DISPATCH_TIME_FOREVER); }]; } - (void)testRegistrationMemorySafety { TJPNavigationCoordinator *coordinator = [TJPNavigationCoordinator sharedInstance]; NSArray *routeTypes = @[@1, @2, @3]; @autoreleasepool { for (int i = 0; i < 100; i++) { TJPViewPushHandler *handler = [TJPViewPushHandler new]; [coordinator registerHandler:handler forRouteType:routeTypes[i%3].integerValue]; } } // 验证handler是否被正确释放 XCTestExpectation *checkExpectation = [self expectationWithDescription:@"MemoryCheck"]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ __block BOOL hasLeak = NO; [routeTypes enumerateObjectsUsingBlock:^(NSNumber *t, NSUInteger idx, BOOL * _Nonnull stop) { id handler = [coordinator handlerForRouteType:t.integerValue]; if (handler) { hasLeak = YES; *stop = YES; } }]; XCTAssertFalse(hasLeak, @"检测到内存泄漏"); [checkExpectation fulfill]; }); [self waitForExpectations:@[checkExpectation] timeout:2]; } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V1_BasicFunction/TJPNetworkManagerTests.m ================================================ // // TJPNetworkManagerTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/20. // #import #import "TJPNetworkManagerV1.h" @interface TJPNetworkManagerTests : XCTestCase @property (nonatomic, strong) TJPNetworkManagerV1 *networkManager; @end @implementation TJPNetworkManagerTests - (void)setUp { self.networkManager = [TJPNetworkManagerV1 shared]; } - (void)tearDown { } #pragma mark - 基础功能测试 - (void)testNetworkConnection { XCTestExpectation *expectation = [self expectationWithDescription:@"等待连接"]; [self.networkManager connectToHost:@"tcpbin.com" port:4242]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ XCTAssertTrue(self.networkManager.isConnected, @"连接失败"); [expectation fulfill]; }); [self waitForExpectationsWithTimeout:5 handler:nil]; } - (void)testSendData { //测试数据发送 XCTestExpectation *expectation = [self expectationWithDescription:@"发送数据"]; NSData *testData = [@"Hello, TJPNetworkManager!" dataUsingEncoding:NSUTF8StringEncoding]; dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ [self.networkManager sendData:testData]; [expectation fulfill]; }); [self waitForExpectationsWithTimeout:3 handler:nil]; } #pragma mark - 并发问题测试 - (void)testConcurrentPendingMessagesAccess { //并发修改 pendingMessages XCTestExpectation *expectation = [self expectationWithDescription:@"并发访问 pendingMessages"]; dispatch_queue_t concurrentQueue = dispatch_queue_create("test.concurrentQueue", DISPATCH_QUEUE_CONCURRENT); //1000个线程同时对pendingMessages进行访问 导致崩溃 for (int i = 0; i < 1000; i++) { dispatch_async(concurrentQueue, ^{ NSNumber *key = @(i); NSData *data = [@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]; // 多线程会导致崩溃 self.networkManager.pendingMessages[key] = data; }); } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ XCTAssertGreaterThan(self.networkManager.pendingMessages.count, 0, @"pendingMessages 没有被正确修改"); [expectation fulfill]; }); [self waitForExpectationsWithTimeout:3 handler:nil]; } - (void)testConcurrentIsConnectedAccessWithDispatchApply { XCTestExpectation *expectation = [self expectationWithDescription:@"并发修改 isConnected"]; dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); dispatch_apply(10000, concurrentQueue, ^(size_t i) { self.networkManager.isConnected = (i % 2 == 0); }); dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ XCTAssertTrue(self.networkManager.isConnected == YES || self.networkManager.isConnected == NO, @"isConnected 状态异常"); [expectation fulfill]; }); [self waitForExpectationsWithTimeout:5 handler:nil]; } - (void)testConcurrentSequenceAccess { //并发修改 `_currentSequence` XCTestExpectation *expectation = [self expectationWithDescription:@"并发访问 _currentSequence"]; dispatch_queue_t concurrentQueue = dispatch_queue_create("test.concurrentQueue", DISPATCH_QUEUE_CONCURRENT); for (int i = 0; i < 1000; i++) { dispatch_async(concurrentQueue, ^{ (self.networkManager.currentSequence)++; // 可能出现竞争条件 }); } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ XCTAssertGreaterThan(self.networkManager.currentSequence, 0, @"_currentSequence 可能未正确递增"); [expectation fulfill]; }); [self waitForExpectationsWithTimeout:3 handler:nil]; } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V2_Concurrency/TJPNetworkManagerV2Tests.m ================================================ // // TJPNetworkManagerV2Tests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/21. // #import #import #import #import "TJPConcurrentNetworkManager.h" #import "TJPNetworkProtocol.h" #import "TJPMockTCPServer.h" @interface TJPNetworkManagerV2Tests : XCTestCase @property (nonatomic, strong) TJPConcurrentNetworkManager *networkManager; @property (nonatomic, strong) TJPMockTCPServer *server; @end @implementation TJPNetworkManagerV2Tests - (void)setUp { self.networkManager = [TJPConcurrentNetworkManager shared]; } #pragma mark - 基础功能测试 - (void)testNetworkConnection { XCTestExpectation *expectation = [self expectationWithDescription:@"等待连接"]; [self.networkManager connectToHost:@"tcpbin.com" port:4242]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ XCTAssertTrue(self.networkManager.isConnected, @"连接失败"); [expectation fulfill]; }); [self waitForExpectationsWithTimeout:5 handler:nil]; } - (void)testSendHeartbeat { XCTestExpectation *expectation = [self expectationWithDescription:@"心跳发送成功"]; self.server = [[TJPMockTCPServer alloc] init]; NSError *error = nil; XCTAssertTrue([self.server startOnPort:54321 error:&error]); [self.networkManager connectToHost:@"127.0.0.1" port:54321]; self.networkManager.onSocketWrite = ^(NSData *data, long tag) { const TJPAdavancedHeader *header = (const TJPAdavancedHeader *)data.bytes; if (ntohs(header->msgType) == TJPMessageTypeHeartbeat) { XCTAssertNotNil(self.networkManager.pendingHeartbeats[@(ntohl(header->sequence))]); [expectation fulfill]; } }; [self waitForExpectationsWithTimeout:35 handler:nil]; } - (void)testReceiveACK { XCTestExpectation *ackExpectation = [self expectationWithDescription:@"收到 ACK,消息已移除"]; self.server = [[TJPMockTCPServer alloc] init]; NSError *error = nil; XCTAssertTrue([self.server startOnPort:54321 error:&error]); [self.networkManager connectToHost:@"127.0.0.1" port:54321]; // 发送数据后等 ACK dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSString *message = @"test ack"; NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding]; NSUInteger sequenceBeforeSend = self.networkManager.currentSequence + 1; [self.networkManager sendData:data]; // 等待一会儿,查看是否 ACK 被移除 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ BOOL exists = self.networkManager.pendingMessages[@(sequenceBeforeSend)] != nil; XCTAssertFalse(exists, @"ACK 收到后,pendingMessages 应该移除该序列号"); [ackExpectation fulfill]; }); }); [self waitForExpectationsWithTimeout:5 handler:nil]; } - (void)testFullSocketChainWithMockServer { XCTestExpectation *expect = [self expectationWithDescription:@"全链路消息解析"]; self.server = [[TJPMockTCPServer alloc] init]; NSError *error = nil; XCTAssertTrue([self.server startOnPort:54321 error:&error]); TJPConcurrentNetworkManager *manager = [TJPConcurrentNetworkManager shared]; [manager resetParse]; [manager connectToHost:@"127.0.0.1" port:54321]; __block BOOL fulfilled = NO; self.networkManager.onMessageParsed = ^(NSString *payloadStr) { if (fulfilled) return; fulfilled = YES; XCTAssertEqualObjects(payloadStr, @"hello world"); [expect fulfill]; }; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ NSData *packet = [self.server buildPacketWithMessage:@"hello world"]; [self.server sendPacket:packet]; }); [self waitForExpectationsWithTimeout:5 handler:nil]; } // 构造模拟数据包(符合协议) - (NSData *)buildMockPacketWithMessage:(NSString *)message { TJPAdavancedHeader header = {0}; NSData *payload = [message dataUsingEncoding:NSUTF8StringEncoding]; header.magic = htonl(kProtocolMagic); header.msgType = htons(TJPMessageTypeNormalData); header.sequence = htonl(1); header.bodyLength = htonl((uint32_t)payload.length); uLong crc = crc32(0L, Z_NULL, 0); header.checksum = htonl((uint32_t)crc32(crc, [payload bytes], (uInt)[payload length])); NSMutableData *packet = [NSMutableData dataWithBytes:&header length:sizeof(header)]; [packet appendData:payload]; return packet; } #pragma mark - 并发问题测试 - (void)testConcurrentPendingMessagesAccess { //并发修改 pendingMessages XCTestExpectation *expectation = [self expectationWithDescription:@"并发访问 pendingMessages"]; dispatch_queue_t concurrentQueue = dispatch_queue_create("test.concurrentQueue", DISPATCH_QUEUE_CONCURRENT); //10000个线程同时对pendingMessages进行访问 for (int i = 0; i < 10000; i++) { dispatch_async(concurrentQueue, ^{ NSNumber *key = @(i); NSData *data = [@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]; //并发安全写入 [self.networkManager addPendingMessage:data forSequence:key.unsignedIntegerValue]; }); } dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ XCTAssertGreaterThan(self.networkManager.pendingMessages.count, 0, @"pendingMessages 没有被正确修改"); [expectation fulfill]; }); [self waitForExpectationsWithTimeout:3 handler:nil]; } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPConcreteSessionTests.m ================================================ // // TJPConcreteSessionTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/25. // #import #import "TJPConcreteSession.h" #import "TJPMockFinalVersionTCPServer.h" #import "TJPNetworkConfig.h" #import "TJPConnectStateMachine.h" #import "TJPSessionDelegate.h" @interface TJPConcreteSessionTests : XCTestCase @property (nonatomic, strong) TJPMockFinalVersionTCPServer *mockServer; @property (nonatomic, strong) TJPConcreteSession *session; @property (nonatomic, strong) XCTestExpectation *connectionExpectation; @property (nonatomic, strong) XCTestExpectation *dataExpectation; @property (nonatomic, strong) XCTestExpectation *stateChangeExpectation; @property (nonatomic, copy) NSArray *expectedStateSequence; @property (nonatomic, assign) NSUInteger currentStateIndex; @end @implementation TJPConcreteSessionTests - (void)setUp { self.mockServer = [[TJPMockFinalVersionTCPServer alloc] init]; [self.mockServer startWithPort:54321]; TJPNetworkConfig *config = [[TJPNetworkConfig alloc] init]; config.maxRetry = 3; config.baseDelay = 1.0; config.heartbeat = 5.0; self.session = [[TJPConcreteSession alloc] initWithConfiguration:config]; self.session.delegate = self; } - (void)tearDown { [self.mockServer stop]; self.mockServer = nil; self.session = nil; [super tearDown]; } #pragma mark - TJPSessionDelegate - (void)session:(id)session didReceiveData:(NSData *)data { if (self.dataExpectation) { NSString *receivedString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; if ([receivedString isEqualToString:@"test message"]) { [self.dataExpectation fulfill]; } } } - (void)session:(id)session stateChanged:(TJPConnectState)state { NSLog(@"State changed to: %@", state); // 状态顺序验证 if (self.expectedStateSequence) { if (self.currentStateIndex < self.expectedStateSequence.count) { TJPConnectState expectedState = self.expectedStateSequence[self.currentStateIndex]; XCTAssertEqualObjects(state, expectedState); self.currentStateIndex++; } } // 连接成功处理 if ([state isEqualToString:TJPConnectStateConnected] && self.connectionExpectation) { [self.connectionExpectation fulfill]; } } #pragma mark - 测试用例 - (void)testConnectionStateFlow { // 定义期望的状态变化顺序 self.expectedStateSequence = @[ TJPConnectStateConnecting, TJPConnectStateConnected ]; self.currentStateIndex = 0; self.stateChangeExpectation = [self expectationWithDescription:@"Should go through correct state sequence"]; self.connectionExpectation = [self expectationWithDescription:@"Connection should succeed"]; [self.session connectToHost:@"127.0.0.1" port:54321]; // 设置延迟检查状态顺序 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (self.currentStateIndex == self.expectedStateSequence.count) { [self.stateChangeExpectation fulfill]; } }); // 使用 XCTest 自带的 waitForExpectations 方法 [self waitForExpectationsWithTimeout:10 handler:nil]; } - (void)testAllConnectionState{ // 初始状态应为 Disconnected XCTAssertEqualObjects(self.session.stateMachine.currentState, TJPConnectStateDisconnected); // 1. 发送 Connect 事件 [self.session.stateMachine sendEvent:TJPConnectEventConnect]; XCTAssertEqualObjects(self.session.stateMachine.currentState, TJPConnectStateConnecting); // 2. 模拟连接成功 [self.session.stateMachine sendEvent:TJPConnectEventConnectSuccess]; XCTAssertEqualObjects(self.session.stateMachine.currentState, TJPConnectStateConnected); // 3. 正常断开流程 [self.session.stateMachine sendEvent:TJPConnectEventDisconnect]; XCTAssertEqualObjects(self.session.stateMachine.currentState, TJPConnectStateDisconnecting); // 4. 断开完成 [self.session.stateMachine sendEvent:TJPConnectEventDisconnectComplete]; XCTAssertEqualObjects(self.session.stateMachine.currentState, TJPConnectStateDisconnected); // 5. 测试强制断开 [self.session.stateMachine sendEvent:TJPConnectEventConnect]; // 进入 Connecting [self.session.stateMachine sendEvent:TJPConnectEventForceDisconnect]; XCTAssertEqualObjects(self.session.stateMachine.currentState, TJPConnectStateDisconnected); } - (void)testDataTransmissionWithACK { XCTestExpectation *connectionExpectation = [self expectationWithDescription:@"Connected"]; XCTestExpectation *dataExpectation = [self expectationWithDescription:@"Should receive data and ack"]; // 监听连接状态变化 [self.session.stateMachine onStateChange:^(TJPConnectState _Nonnull oldState, TJPConnectState _Nonnull newState) { if ([newState isEqualToString:TJPConnectStateConnected]) { [connectionExpectation fulfill]; } }]; // 开始连接 [self.session connectToHost:@"127.0.0.1" port:54321]; // 等待连接成功 [self waitForExpectations:@[connectionExpectation] timeout:5.0]; // 发送测试数据 [self.session sendData:[@"test message" dataUsingEncoding:NSUTF8StringEncoding]]; // Mock服务器处理 __block BOOL serverReceived = NO; __block BOOL didFulfill = NO; self.mockServer.didReceiveDataHandler = ^(NSData *data, uint32_t seq) { NSLog(@"Received data with sequence: %u", seq); // 调试日志,确保数据接收到了 serverReceived = YES; // 发送 ACK [self.mockServer sendACKForSequence:seq toSocket:self.mockServer.connectedSockets.firstObject]; NSLog(@"Sent ACK for sequence: %u", seq); // 调试日志,确保 ACK 被发送 // 触发期望 if (!didFulfill) { didFulfill = YES; NSLog(@"Fulfilling dataExpectation for sequence: %u", seq); [dataExpectation fulfill]; } }; // 等待数据 ACK [self waitForExpectations:@[dataExpectation] timeout:10.0]; XCTAssertTrue(serverReceived, "Server did not receive data"); } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPConnectStateMachineTests.m ================================================ // // TJPConnectStateMachineTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/25. // #import #import "TJPConnectStateMachine.h" @interface TJPConnectStateMachineTests : XCTestCase @end @implementation TJPConnectStateMachineTests - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testAddTransitionFromStateToStateForEvent { TJPConnectStateMachine *stateMachine = [[TJPConnectStateMachine alloc] initWithInitialState:TJPConnectStateDisconnected]; // 添加状态转换规则 [stateMachine addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateConnecting forEvent:TJPConnectEventConnect]; // 获取转换后的状态 TJPConnectState newState = [stateMachine valueForKey:@"_transitions"][@"Disconnected:Connect"]; // 验证是否添加成功 XCTAssertEqual(newState, TJPConnectStateConnecting, @"状态转换规则没有正确添加"); } - (void)testSendEvent { TJPConnectStateMachine *stateMachine = [[TJPConnectStateMachine alloc] initWithInitialState:TJPConnectStateDisconnected]; // 添加状态转换规则 [stateMachine addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateConnecting forEvent:TJPConnectEventConnect]; // 触发事件 [stateMachine sendEvent:TJPConnectEventConnect]; // 获取当前状态 TJPConnectState currentState = [stateMachine valueForKey:@"_currentState"]; // 验证当前状态是否已正确转换 XCTAssertEqual(currentState, TJPConnectStateConnecting, @"状态转换失败"); } - (void)testOnStateChange { TJPConnectStateMachine *stateMachine = [[TJPConnectStateMachine alloc] initWithInitialState:TJPConnectStateDisconnected]; // 设置期望 XCTestExpectation *expectation = [self expectationWithDescription:@"State change callback"]; // 添加状态变化回调 [stateMachine onStateChange:^(TJPConnectState oldState, TJPConnectState newState) { // 验证状态变化是否正确 XCTAssertEqual(oldState, TJPConnectStateDisconnected, @"旧状态错误"); XCTAssertEqual(newState, TJPConnectStateConnecting, @"新状态错误"); // 完成回调 [expectation fulfill]; }]; // 添加状态转换规则 [stateMachine addTransitionFromState:TJPConnectStateDisconnected toState:TJPConnectStateConnecting forEvent:TJPConnectEventConnect]; // 触发事件 [stateMachine sendEvent:TJPConnectEventConnect]; // 等待期望完成 [self waitForExpectationsWithTimeout:5 handler:nil]; } - (void)testInvalidStateTransition { TJPConnectStateMachine *stateMachine = [[TJPConnectStateMachine alloc] initWithInitialState:TJPConnectStateDisconnected]; // 触发无效事件(没有对应的状态转换) [stateMachine sendEvent:TJPConnectEventDisconnect]; // 验证是否没有发生状态转换 TJPConnectState currentState = [stateMachine valueForKey:@"_currentState"]; XCTAssertEqual(currentState, TJPConnectStateDisconnected, @"无效状态转换未正确处理"); } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPDynamicHeartbeatTests.m ================================================ // // TJPDynamicHeartbeatTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/24. // #import #import "TJPDynamicHeartbeat.h" #import "TJPConcreteSession.h" #import "TJPNetworkConfig.h" #import "TJPSequenceManager.h" #import "TJPNetworkCondition.h" @interface TJPDynamicHeartbeatTests : XCTestCase @property (nonatomic, strong) TJPSequenceManager *seqManager; @property (nonatomic, strong) TJPDynamicHeartbeat *heartbeatManager; @property (nonatomic, strong) TJPConcreteSession *mockSession; @end @implementation TJPDynamicHeartbeatTests - (void)setUp { TJPNetworkConfig *config = [[TJPNetworkConfig alloc] init]; config.heartbeat = 5.0; self.mockSession = [[TJPConcreteSession alloc] initWithConfiguration:config]; self.seqManager = [[TJPSequenceManager alloc] init]; self.heartbeatManager = [[TJPDynamicHeartbeat alloc] initWithBaseInterval:5 seqManager:self.seqManager session:self.mockSession]; } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testAdjustIntervalWithNetworkCondition { [self.heartbeatManager startMonitoring]; TJPNetworkCondition *excellentCondition = [[TJPNetworkCondition alloc] init]; // Excellent RTT excellentCondition.roundTripTime = 50; // Low packet loss excellentCondition.packetLossRate = 1.0; [self.heartbeatManager adjustIntervalWithNetworkCondition:excellentCondition]; XCTAssertEqual(self.heartbeatManager.currentInterval, 4.0, @"Interval should decrease for excellent network condition"); // Test with Poor network condition TJPNetworkCondition *poorCondition = [[TJPNetworkCondition alloc] init]; // High RTT poorCondition.roundTripTime = 900; // High packet loss poorCondition.packetLossRate = 20.0; [self.heartbeatManager adjustIntervalWithNetworkCondition:poorCondition]; XCTAssertEqual(self.heartbeatManager.currentInterval, 60.0, @"Interval should increase for poor network condition"); } - (void)testHeartbeatACKNowledgedForSequence { uint32_t sequence = 1234; [self.heartbeatManager.pendingHeartbeats setObject:[NSDate date] forKey:@(sequence)]; [self.heartbeatManager heartbeatACKNowledgedForSequence:sequence]; XCTAssertNil(self.heartbeatManager.pendingHeartbeats[@(sequence)], @"Heartbeat should be removed after ACK is received"); } - (void)testHeartbeatACKNowledgedForSequence_highConcurrency { const NSInteger concurrencyCount = 10000; // 使用异步队列来模拟并发 dispatch_queue_t queue = dispatch_queue_create("com.test.heartbeat.concurrent", DISPATCH_QUEUE_CONCURRENT); // 创建期望对象,确保所有操作完成后再进行验证 XCTestExpectation *expectation = [self expectationWithDescription:@"Concurrent operations completed"]; // 在并发情况下添加序列号到 pendingHeartbeats dispatch_apply(concurrencyCount, queue, ^(size_t i) { // 随机生成序列号并添加到 pendingHeartbeats uint32_t newSequence = (uint32_t)(i + 1); // 模拟发送心跳包 [self.heartbeatManager sendHeartbeat]; // 模拟心跳ACK操作 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 随机选择一个序列号,并模拟收到 ACK uint32_t ackSequence = arc4random_uniform((uint32_t)concurrencyCount) + 1; [self.heartbeatManager heartbeatACKNowledgedForSequence:ackSequence]; // 验证随机选择的序列号是否已被移除 XCTAssertNil(self.heartbeatManager.pendingHeartbeats[@(ackSequence)], @"Heartbeat should be removed after ACK is received in high concurrency"); // 完成期望 if (i == concurrencyCount - 1) { [expectation fulfill]; } }); }); // 等待期望的完成 [self waitForExpectationsWithTimeout:10.0 handler:nil]; } // 模拟网络波动场景 - (void)testNetworkFluctuation { TJPDynamicHeartbeat *heartbeat = [[TJPDynamicHeartbeat alloc] initWithBaseInterval:60 seqManager:self.seqManager session:self.mockSession]; // 第一阶段:优质网络(RTT=150ms, 丢包率0%) for (int i=0; i<10; i++) { [heartbeat.networkCondition updateRTTWithSample:150]; [heartbeat.networkCondition updateLostWithSample:NO]; } [heartbeat adjustIntervalWithNetworkCondition:heartbeat.networkCondition]; XCTAssertEqual(heartbeat.currentInterval, 60 * (150/200)); // 期望45秒 // 第二阶段:RTT恶化到800ms(触发Poor等级) for (int i=0; i<10; i++) { [heartbeat.networkCondition updateRTTWithSample:800]; } [heartbeat adjustIntervalWithNetworkCondition:heartbeat.networkCondition]; XCTAssertEqual(heartbeat.currentInterval, 60 * 2.5); // 期望150秒 // 第三阶段:高丢包率(20%) for (int i=0; i<10; i++) { [heartbeat.networkCondition updateLostWithSample:(i < 2)]; // 20%丢包 } [heartbeat adjustIntervalWithNetworkCondition:heartbeat.networkCondition]; XCTAssertEqual(heartbeat.currentInterval, 60 * 2.5); // 仍为Poor等级,保持150秒 } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPMessageContextTests.m ================================================ // // TJPMessageContextTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/24. // #import #import "TJPMessageContext.h" #import "TJPNetworkUtil.h" @interface TJPMessageContextTests : XCTestCase @end @implementation TJPMessageContextTests - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testContextWithData_initialization { // 创建测试数据 NSData *testData = [@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]; // 创建 TJPMessageContext 实例 TJPMessageContext *context = [TJPMessageContext contextWithData:testData seq:1 messageType:TJPMessageTypeNormalData encryptType:TJPEncryptTypeNone compressType:TJPCompressTypeNone sessionId:@""]; // 验证 sendTime 是否为当前时间(使用近似匹配) XCTAssertNotNil(context.sendTime, @"sendTime should not be nil"); // 验证 retryCount 是否为 0 XCTAssertEqual(context.retryCount, 0, @"retryCount should be initialized to 0"); // 验证 sequence 是否正确设置 XCTAssertGreaterThan(context.sequence, 0, @"sequence should be greater than 0"); } - (void)testBuildRetryPacket { // 创建测试数据 NSData *testData = [@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]; // 创建 TJPMessageContext 实例 TJPMessageContext *context = [TJPMessageContext contextWithData:testData seq:1 messageType:TJPMessageTypeNormalData encryptType:TJPEncryptTypeNone compressType:TJPCompressTypeNone sessionId:@""]; // 初始时的 retryCount NSInteger initialRetryCount = context.retryCount; // 调用 buildRetryPacket 方法 NSData *retryPacket = [context buildRetryPacket]; // 验证 retryCount 是否递增 XCTAssertEqual(context.retryCount, initialRetryCount + 1, @"retryCount should be incremented"); // 验证返回的数据包是否正确构建 XCTAssertNotNil(retryPacket, @"retryPacket should not be nil"); XCTAssertEqual(retryPacket.length, testData.length + sizeof(TJPFinalAdavancedHeader), @"retryPacket should have the correct length"); // 验证数据包的头部是否正确 TJPFinalAdavancedHeader *header = (TJPFinalAdavancedHeader *)retryPacket.bytes; XCTAssertEqual(ntohl(header->sequence), context.sequence, @"Sequence in retry packet should match context's sequence"); XCTAssertEqual(header->msgType, htons(TJPMessageTypeNormalData), @"Message type should be normal data in retry packet"); XCTAssertEqual(header->bodyLength, htonl((uint32_t)testData.length), @"Body length should match the original data's length"); XCTAssertEqual(header->checksum, [TJPNetworkUtil crc32ForData:testData], @"Checksum should match the original data's checksum"); } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPMessageParserTests.m ================================================ // // TJPMessageParserTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/25. // #import #import "TJPMessageParser.h" #import "TJPParsedPacket.h" #import "TJPNetworkUtil.h" #import static const NSUInteger kTestDataSize = 1024; // 1KB测试数据 static const NSUInteger kTestIterations = 10000; // 测试次数 static const NSUInteger kLargeDataSize = 1024 * 64; // 64KB大数据测试 @interface TJPMessageParserTests : XCTestCase @property (nonatomic, strong) TJPMessageParser *originalParser; //原始版本 @property (nonatomic, strong) TJPMessageParser *optimizedParser; //优化版本 @property (nonatomic, strong) NSMutableArray *testDataArray; @property (nonatomic, strong) NSData *sampleCompletePacket; @end @implementation TJPMessageParserTests - (void)setUp { _originalParser = [[TJPMessageParser alloc] initWithRingBufferEnabled:NO]; _optimizedParser = [[TJPMessageParser alloc] initWithRingBufferEnabled:YES]; [self generateTestData]; [self generateSamplePacket]; NSLog(@"\n=== 基准测试开始 ==="); NSLog(@"测试数据大小: %lu bytes", (unsigned long)kTestDataSize); NSLog(@"测试迭代次数: %lu", (unsigned long)kTestIterations); } - (void)generateSamplePacket { // 构造一个完整的测试包 TJPFinalAdavancedHeader header = {0}; header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(1001); header.sequence = htonl(12345); header.timestamp = htonl((uint32_t)[[NSDate date] timeIntervalSince1970]); header.encrypt_type = TJPEncryptTypeNone; header.compress_type = TJPCompressTypeNone; NSData *payloadData = [self generateValidTLVPayload]; // 关键修改点 header.bodyLength = htonl((uint32_t)payloadData.length); header.checksum = [TJPNetworkUtil crc32ForData:payloadData]; NSMutableData *packetData = [NSMutableData data]; [packetData appendBytes:&header length:sizeof(header)]; [packetData appendData:payloadData]; self.sampleCompletePacket = [packetData copy]; } - (NSData *)generatePacketWithSequence:(uint32_t)sequence { TJPFinalAdavancedHeader header = {0}; header.magic = htonl(kProtocolMagic); header.version_major = kProtocolVersionMajor; header.version_minor = kProtocolVersionMinor; header.msgType = htons(1001); header.sequence = htonl(sequence); header.timestamp = htonl((uint32_t)[[NSDate date] timeIntervalSince1970]); header.encrypt_type = TJPEncryptTypeNone; header.compress_type = TJPCompressTypeNone; NSData *payloadData = [self generateValidTLVPayload]; header.bodyLength = htonl((uint32_t)payloadData.length); header.checksum = [TJPNetworkUtil crc32ForData:payloadData]; NSMutableData *packetData = [NSMutableData data]; [packetData appendBytes:&header length:sizeof(header)]; [packetData appendData:payloadData]; return [packetData copy]; } - (NSData *)generateValidTLVPayload { NSMutableData *data = [NSMutableData data]; // 构造 TLV: Tag = 0x1001, Length = 15, Value = "性能测试数据" uint16_t tag = CFSwapInt16HostToBig(0x1001); // Tag 大端 NSData *value = [@"这是一个测试消息,用于性能基准测试" dataUsingEncoding:NSUTF8StringEncoding]; uint32_t length = CFSwapInt32HostToBig((uint32_t)value.length); // Length 大端 [data appendBytes:&tag length:sizeof(tag)]; [data appendBytes:&length length:sizeof(length)]; [data appendData:value]; return data; } - (void)generateTestData { self.testDataArray = [NSMutableArray array]; // 生成不同大小的测试数据 NSArray *sizes = @[@64, @256, @1024, @4096, @16384]; for (NSNumber *size in sizes) { NSMutableData *data = [NSMutableData dataWithLength:[size unsignedIntegerValue]]; // 填充随机数据 uint8_t *bytes = data.mutableBytes; for (NSUInteger i = 0; i < [size unsignedIntegerValue]; i++) { bytes[i] = arc4random() % 256; } [self.testDataArray addObject:data]; } } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testMaxNestedDepth { // 1. 测试有效嵌套深度(4层) NSData *validData = [self generateNestedTLVWithDepth:4]; TJPFinalAdavancedHeader header = {0}; header.magic = htonl(kProtocolMagic); header.bodyLength = htonl((uint32_t)validData.length); NSError *error = nil; TJPParsedPacket *packet = [TJPParsedPacket packetWithHeader:header payload:validData policy:TJPTLVTagPolicyRejectDuplicates maxNestedDepth:4 error:&error]; // 断言解析成功 XCTAssertNil(error, @"有效嵌套深度4应成功,错误: %@", error); XCTAssertNotNil(packet, @"packet不应为nil"); // 验证嵌套深度 __block NSUInteger nestedDepth = 0; id currentEntry = packet.tlvEntries[@0xFFFF]; while ([currentEntry isKindOfClass:[NSDictionary class]]) { nestedDepth++; currentEntry = ((NSDictionary *)currentEntry)[@0xFFFF]; } XCTAssertEqual(nestedDepth, 4, @"嵌套深度应为4,实际为%lu", nestedDepth); // 2. 测试无效嵌套深度(5层) NSData *invalidData = [self generateNestedTLVWithDepth:5]; header.bodyLength = htonl((uint32_t)invalidData.length); // 断言解析失败 XCTAssertNotNil(error, @"嵌套深度5应触发错误"); XCTAssertEqualObjects(error.domain, @"TLVError", @"错误域名不符"); XCTAssertEqual(error.code, TJPTLVParseErrorNestedTooDeep, @"错误码应为TJPTLVParseErrorNestedTooDeep"); } - (NSData *)generateNestedTLVWithDepth:(NSUInteger)depth { NSMutableData *data = [NSMutableData data]; // 递归生成嵌套TLV [self appendNestedTLVToData:data remainingDepth:depth]; return [data copy]; } - (void)appendNestedTLVToData:(NSMutableData *)data remainingDepth:(NSUInteger)remainingDepth { if (remainingDepth == 0) { // 最内层添加实际数据(例如用户ID) uint16_t tag = CFSwapInt16HostToBig(0x1001); // Tag=0x1001(用户ID) uint32_t length = CFSwapInt32HostToBig(5); // Length=5 [data appendBytes:&tag length:2]; [data appendBytes:&length length:4]; [data appendBytes:"Hello" length:5]; // Value="Hello" return; } // 外层添加嵌套TLV标记(例如0xFFFF) uint16_t tag = CFSwapInt16HostToBig(0xFFFF); // 嵌套保留Tag [data appendBytes:&tag length:2]; // 预留Length位置,后续填充 NSUInteger lengthOffset = data.length; [data appendBytes:&(uint32_t){0} length:4]; // 占位4字节 // 递归生成子TLV [self appendNestedTLVToData:data remainingDepth:remainingDepth - 1]; // 回填Length(整个子TLV的长度) uint32_t childLength = CFSwapInt32HostToBig((uint32_t)(data.length - lengthOffset - 4)); [data replaceBytesInRange:NSMakeRange(lengthOffset, 4) withBytes:&childLength]; } - (void)testFunctionalCorrectness { NSLog(@"\n--- 功能正确性对比测试 ---"); // 使用相同的测试数据测试两个版本 [self.originalParser feedData:self.sampleCompletePacket]; [self.optimizedParser feedData:self.sampleCompletePacket]; // 检查是否都能正确识别完整包 BOOL originalHasPacket = [self.originalParser hasCompletePacket]; BOOL optimizedHasPacket = [self.optimizedParser hasCompletePacket]; XCTAssertEqual(originalHasPacket, optimizedHasPacket, @"完整包检测结果应该一致"); // 解析包并对比结果 TJPParsedPacket *originalPacket = [self.originalParser nextPacket]; TJPParsedPacket *optimizedPacket = [self.optimizedParser nextPacket]; XCTAssertNotNil(originalPacket, @"原始版本应该能解析包"); XCTAssertNotNil(optimizedPacket, @"优化版本应该能解析包"); if (originalPacket && optimizedPacket) { XCTAssertEqual(originalPacket.messageType, optimizedPacket.messageType, @"消息类型应该一致"); XCTAssertEqual(originalPacket.sequence, optimizedPacket.sequence, @"序列号应该一致"); XCTAssertEqualObjects(originalPacket.payload, optimizedPacket.payload, @"载荷数据应该一致"); NSLog(@"✅ 功能正确性测试通过"); } } - (void)testMemoryUsage { NSLog(@"\n--- 内存使用对比测试 ---"); // 记录初始内存 size_t initialMemory = [self getCurrentMemoryUsage]; // 原始版本内存测试 size_t originalMemoryBefore = [self getCurrentMemoryUsage]; [self performMemoryStressTest:self.originalParser]; size_t originalMemoryAfter = [self getCurrentMemoryUsage]; // 重置并测试优化版本 [self.originalParser reset]; size_t optimizedMemoryBefore = [self getCurrentMemoryUsage]; [self performMemoryStressTest:self.optimizedParser]; size_t optimizedMemoryAfter = [self getCurrentMemoryUsage]; NSLog(@"原始版本内存增长: %zu KB", (originalMemoryAfter - originalMemoryBefore) / 1024); NSLog(@"优化版本内存增长: %zu KB", (optimizedMemoryAfter - optimizedMemoryBefore) / 1024); // 通常环形缓冲区版本的内存增长应该更稳定 } - (void)performMemoryStressTest:(TJPMessageParser *)parser { // 大量小数据写入,模拟内存碎片场景 for (NSUInteger i = 0; i < 1000; i++) { NSData *smallData = [self.testDataArray[0] subdataWithRange:NSMakeRange(0, 32)]; [parser feedData:smallData]; // 偶尔重置,模拟实际使用场景 if (i % 100 == 0) { [parser reset]; } } } - (size_t)getCurrentMemoryUsage { struct task_basic_info info; mach_msg_type_number_t size = sizeof(info); kern_return_t kerr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)&info, &size); return (kerr == KERN_SUCCESS) ? info.resident_size : 0; } - (void)testWriteAndParsePerformance { const NSUInteger iterationCount = 10000; const NSUInteger payloadSize = 512; // 可调大小 NSLog(@"\n=== 数据写入 + 解析 性能对比 ==="); NSArray *parsers = @[self.originalParser, self.optimizedParser]; NSArray *parserNames = @[@"原始版本", @"优化版本"]; for (NSUInteger p = 0; p < parsers.count; p++) { TJPMessageParser *parser = parsers[p]; NSString *name = parserNames[p]; CFTimeInterval start = CFAbsoluteTimeGetCurrent(); for (NSUInteger i = 0; i < iterationCount; i++) { NSData *packet = [self generatePacketWithSequence:(10000 + (uint32_t)i)]; [parser feedData:packet]; TJPParsedPacket *parsed = [parser nextPacket]; XCTAssertNotNil(parsed, @"解析结果不应为 nil"); } CFTimeInterval duration = CFAbsoluteTimeGetCurrent() - start; NSLog(@"%@:%.3f ms", name, duration * 1000); } } - (void)testParsePerformance { NSLog(@"\n--- 解析性能对比 ---"); // 测试原始版本解析性能 NSTimeInterval originalParseTime = [self measureParsePerformance:self.originalParser]; // 测试优化版本解析性能 NSTimeInterval optimizedParseTime = [self measureParsePerformance:self.optimizedParser]; CGFloat improvement = (originalParseTime - optimizedParseTime) / originalParseTime * 100; NSLog(@"原始版本解析时间: %.3f ms", originalParseTime * 1000); NSLog(@"优化版本解析时间: %.3f ms", optimizedParseTime * 1000); NSLog(@"解析性能提升: %.1f%%", improvement); XCTAssertLessThan(optimizedParseTime, originalParseTime, @"优化版本解析应该更快"); } - (NSTimeInterval)measureParsePerformance:(TJPMessageParser *)parser { [parser reset]; NSDate *startTime = [NSDate date]; NSUInteger parsedCount = 0; // 连续解析1000个包 for (NSUInteger i = 0; i < 1000; i++) { [parser feedData:self.sampleCompletePacket]; while ([parser hasCompletePacket]) { TJPParsedPacket *packet = [parser nextPacket]; if (packet) { parsedCount++; } } } NSTimeInterval totalTime = [[NSDate date] timeIntervalSinceDate:startTime]; NSLog(@"解析包数量: %lu", (unsigned long)parsedCount); return totalTime; } #pragma mark - 并发安全测试 - (void)testConcurrentSafety { NSLog(@"\n--- 并发安全性测试 ---"); dispatch_group_t group = dispatch_group_create(); dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); __block NSUInteger originalErrors = 0; __block NSUInteger optimizedErrors = 0; // 并发测试原始版本 for (NSUInteger i = 0; i < 10; i++) { dispatch_group_async(group, concurrentQueue, ^{ @try { for (NSUInteger j = 0; j < 100; j++) { [self.originalParser feedData:self.testDataArray[j % self.testDataArray.count]]; } } @catch (NSException *exception) { @synchronized (self) { originalErrors++; } } }); } // 并发测试优化版本 for (NSUInteger i = 0; i < 10; i++) { dispatch_group_async(group, concurrentQueue, ^{ @try { for (NSUInteger j = 0; j < 100; j++) { [self.optimizedParser feedData:self.testDataArray[j % self.testDataArray.count]]; } } @catch (NSException *exception) { @synchronized (self) { optimizedErrors++; } } }); } // 等待所有任务完成 dispatch_group_wait(group, DISPATCH_TIME_FOREVER); NSLog(@"原始版本并发错误: %lu", (unsigned long)originalErrors); NSLog(@"优化版本并发错误: %lu", (unsigned long)optimizedErrors); XCTAssertEqual(optimizedErrors, 0, @"优化版本应该没有并发错误"); } #pragma mark - 边界条件测试 - (void)testBoundaryConditions { NSLog(@"\n--- 边界条件测试 ---"); // 测试空数据 [self.originalParser feedData:[NSData data]]; [self.optimizedParser feedData:[NSData data]]; // 测试nil数据 [self.originalParser feedData:nil]; [self.optimizedParser feedData:nil]; // 测试超大数据 NSData *largeData = [NSMutableData dataWithLength:kLargeDataSize]; [self.originalParser feedData:largeData]; [self.optimizedParser feedData:largeData]; // 测试频繁重置 for (NSUInteger i = 0; i < 100; i++) { [self.originalParser feedData:self.testDataArray[0]]; [self.originalParser reset]; [self.optimizedParser feedData:self.testDataArray[0]]; [self.optimizedParser reset]; } NSLog(@"✅ 边界条件测试完成"); } #pragma mark - 综合报告 - (void)testGeneratePerformanceReport { NSLog(@"\n=== 性能测试综合报告 ==="); // 执行所有测试 [self testFunctionalCorrectness]; [self testWriteAndParsePerformance]; [self testParsePerformance]; [self testMemoryUsage]; [self testConcurrentSafety]; [self testBoundaryConditions]; NSLog(@"\n=== 测试结论 ==="); NSLog(@"1. 功能正确性: 优化版本与原始版本行为一致"); NSLog(@"2. 性能提升: 写入和解析性能都有显著提升"); NSLog(@"3. 内存使用: 优化版本内存碎片更少,使用更稳定"); NSLog(@"4. 并发安全: 优化版本提供了更好的线程安全保护"); NSLog(@"5. 边界处理: 两个版本都能正确处理各种边界条件"); } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPNetworkConditionTests.m ================================================ // // TJPNetworkConditionTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/25. // #import #import "TJPNetworkCondition.h" @interface TJPNetworkConditionTests : XCTestCase @property (nonatomic, strong) TJPNetworkCondition *condition; @end @implementation TJPNetworkConditionTests - (void)setUp { self.condition = [[TJPNetworkCondition alloc] init]; } - (void)testQualityLevel { TJPNetworkCondition *condition = [[TJPNetworkCondition alloc] init]; // 测试 RTT 和丢包率正常的情况 condition.roundTripTime = 50.0; condition.packetLossRate = 1.0; XCTAssertEqual(condition.qualityLevel, TJPNetworkQualityExcellent, @"网络质量评估错误(良好网络)"); // 测试较差的 RTT 和丢包率 condition.roundTripTime = 250.0; condition.packetLossRate = 5.0; XCTAssertEqual(condition.qualityLevel, TJPNetworkQualityGood, @"网络质量评估错误(普通网络)"); // 测试较差的网络 condition.roundTripTime = 500.0; condition.packetLossRate = 10.0; XCTAssertEqual(condition.qualityLevel, TJPNetworkQualityFair, @"网络质量评估错误(差网络)"); // 测试非常差的网络 condition.roundTripTime = 800.0; condition.packetLossRate = 20.0; XCTAssertEqual(condition.qualityLevel, TJPNetworkQualityPoor, @"网络质量评估错误(差网络)"); } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPNetworkCoordinatorTests.m ================================================ // // TJPNetworkCoordinatorTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/26. // #import #import #import "TJPNetworkCoordinator.h" #import "TJPNetworkConfig.h" #import "TJPSessionProtocol.h" @interface TJPNetworkCoordinatorTests : XCTestCase @property (nonatomic, strong) TJPNetworkConfig *mockConfig; @end @implementation TJPNetworkCoordinatorTests - (void)setUp { TJPNetworkConfig *config = [[TJPNetworkConfig alloc] init]; config.maxRetry = 3; config.baseDelay = 1.0; config.heartbeat = 5.0; } - (void)tearDown { self.mockConfig = nil; } - (void)testCreateSessionWithConfiguration { // 创建期望,确保异步操作完成后再进行断言 XCTestExpectation *expectation = [self expectationWithDescription:@"Session added to sessionMap"]; // 创建会话 id session = [[TJPNetworkCoordinator shared] createSessionWithConfiguration:self.mockConfig]; // 验证 session 不为空 XCTAssertNotNil(session, @"Session should not be nil after creation."); // 使用 dispatch_barrier_async 确保 sessionMap 更新后触发期望 dispatch_barrier_async([TJPNetworkCoordinator shared].sessionQueue, ^{ // 触发期望,表示 sessionMap 更新完成 [expectation fulfill]; }); // 等待期望 [self waitForExpectationsWithTimeout:5.0 handler:nil]; // 验证 sessionMap 中有一个会话 XCTAssertEqual([TJPNetworkCoordinator shared].sessionMap.count, 1, @"SessionMap should have 1 session."); } - (void)testUpdateAllSessionsState { // 创建期望,等待会话状态更新 XCTestExpectation *expectation = [self expectationWithDescription:@"Waiting for session state update"]; // 创建一个会话 __block id session = [[TJPNetworkCoordinator shared] createSessionWithConfiguration:self.mockConfig]; // 更新所有会话的状态 [[TJPNetworkCoordinator shared] updateAllSessionsState:TJPConnectStateDisconnected]; // 在更新状态完成后,触发期望 dispatch_async(dispatch_get_main_queue(), ^{ session = [[TJPNetworkCoordinator shared].sessionMap objectEnumerator].nextObject; // 验证会话的状态是否更新 XCTAssertEqualObjects(session.connectState, TJPConnectStateDisconnected, @"Session state should be updated to Disconnected."); // 完成期望 [expectation fulfill]; }); // 等待期望 [self waitForExpectationsWithTimeout:5.0 handler:nil]; } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPReconnectPolicyTests.m ================================================ // // TJPReconnectPolicyTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/25. // #import #import "TJPReconnectPolicy.h" @interface TJPReconnectPolicyTests : XCTestCase @end @implementation TJPReconnectPolicyTests - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testReconnectPolicyInitialization { TJPReconnectPolicy *reconnectPolicy = [[TJPReconnectPolicy alloc] initWithMaxAttempst:5 baseDelay:10.0 qos:TJPNetworkQoSUserInitiated delegate:nil]; // 验证初始化参数是否正确 XCTAssertEqual(reconnectPolicy.maxAttempts, 5, @"最大重试次数应为5"); XCTAssertEqual(reconnectPolicy.baseDelay, 10.0, @"基础延迟应为10.0"); XCTAssertEqual(reconnectPolicy.qosClass, QOS_CLASS_USER_INITIATED, @"QoS 应为 TJPNetworkQoSUserInitiated"); } - (void)testAttemptConnectionWithBlock { __block BOOL connectionAttempted = NO; TJPReconnectPolicy *reconnectPolicy = [[TJPReconnectPolicy alloc] initWithMaxAttempst:3 baseDelay:1.0 qos:TJPNetworkQoSUserInitiated delegate:nil]; // 使用 XCTestExpectation 来确保重试机制按预期工作 XCTestExpectation *expectation = [self expectationWithDescription:@"Retrying connection"]; // 尝试连接 [reconnectPolicy attemptConnectionWithBlock:^{ connectionAttempted = YES; [expectation fulfill]; }]; // 等待连接尝试 [self waitForExpectationsWithTimeout:5.0 handler:nil]; XCTAssertTrue(connectionAttempted, @"连接尝试应被调用"); } - (void)testCalculateDelay { TJPReconnectPolicy *reconnectPolicy = [[TJPReconnectPolicy alloc] initWithMaxAttempst:5 baseDelay:2.0 qos:TJPNetworkQoSUserInitiated delegate:nil]; // 设置 _currentAttempt 为 2,并测试延迟计算 reconnectPolicy.currentAttempt = 2; // 期望延迟计算应该是 pow(2, 2) = 4.0 NSTimeInterval calculatedDelay = [reconnectPolicy calculateDelay]; XCTAssertEqual(calculatedDelay, 4.0, @"The calculated delay should be 4.0 seconds."); // 测试当延迟超过最大延迟时,返回最大延迟值 reconnectPolicy.currentAttempt = 5; // 设置更高的尝试次数 NSTimeInterval maxDelay = [reconnectPolicy calculateDelay]; XCTAssertEqual(maxDelay, 30.0, @"The calculated delay should not exceed the maximum reconnect delay of 30 seconds."); } - (void)testNotifyReachMaxAttempts { TJPReconnectPolicy *reconnectPolicy = [[TJPReconnectPolicy alloc] initWithMaxAttempst:3 baseDelay:1.0 qos:TJPNetworkQoSUserInitiated delegate:nil]; // 使用 XCTestExpectation 来确保最大重试次数达到时的通知被触发 XCTestExpectation *expectation = [self expectationWithDescription:@"Max retry attempts reached"]; // 模拟尝试连接并重试 for (NSInteger i = 0; i < 3; i++) { [reconnectPolicy attemptConnectionWithBlock:^{ }]; } // 模拟最大重试次数达到 [reconnectPolicy notifyReachMaxAttempts]; // 确保通知到达 [expectation fulfill]; // 等待通知完成 [self waitForExpectationsWithTimeout:2.0 handler:nil]; } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPSequenceManagerTests.m ================================================ // // TJPSequenceManagerTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/25. // #import #import "TJPSequenceManager.h" @interface TJPSequenceManagerTests : XCTestCase @property (nonatomic, strong) TJPSequenceManager *seqManager; @end @implementation TJPSequenceManagerTests - (void)setUp { self.seqManager = [[TJPSequenceManager alloc] init]; } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testNextSeq { // 1. 测试初始值 uint32_t seq1 = [self.seqManager nextSequenceForCategory:TJPMessageCategoryNormal]; XCTAssertEqual(seq1, 1, @"序列号应从1开始"); // 2. 测试普通递增 uint32_t seq2 = [self.seqManager nextSequenceForCategory:TJPMessageCategoryNormal]; XCTAssertEqual(seq2, 2, @"序列号应递增"); // 3. 测试循环逻辑(直接设置到边界值) // 模拟 _sequence 已经达到 UINT32_MAX [self.seqManager resetSequence]; [self.seqManager setValue:@(UINT32_MAX) forKey:@"_sequence"]; uint32_t seqAfterMax = [self.seqManager nextSequenceForCategory:TJPMessageCategoryNormal]; XCTAssertEqual(seqAfterMax, 1, @"达到最大值后应循环回1"); } - (void)testResetSequence { TJPSequenceManager *manager = [[TJPSequenceManager alloc] init]; // 测试递增后 [manager nextSequenceForCategory:TJPMessageCategoryNormal]; [manager nextSequenceForCategory:TJPMessageCategoryNormal]; uint32_t seqBeforeReset = [manager nextSequenceForCategory:TJPMessageCategoryNormal]; XCTAssertEqual(seqBeforeReset, 3, @"序列号应该递增到3"); // 测试重置 [manager resetSequence]; uint32_t seqAfterReset = [manager nextSequenceForCategory:TJPMessageCategoryNormal]; XCTAssertEqual(seqAfterReset, 1, @"重置后序列号应该从1开始"); } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/CoreNetworkStack/TransportLayer/V3_FinalProduct/TJPTJPNetworkUtilTests.m ================================================ // // TJPTJPNetworkUtilTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/25. // #import #import "TJPCoreTypes.h" #import "TJPMessageBuilder.h" #import "TJPNetworkUtil.h" @interface TJPTJPNetworkUtilTests : XCTestCase @end @implementation TJPTJPNetworkUtilTests - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testBuildPacketWithData { NSData *data = [@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]; uint32_t sequence = 1234; TJPMessageType type = TJPMessageTypeNormalData; NSData *packet = [TJPMessageBuilder buildPacketWithMessageType:type sequence:sequence payload:data encryptType:TJPEncryptTypeNone compressType:TJPCompressTypeNone sessionID:@""]; // 验证包的长度(协议头 + 数据) XCTAssertEqual(packet.length, sizeof(TJPFinalAdavancedHeader) + data.length, @"数据包的长度应该是协议头长度 + 数据长度"); // 验证 CRC 校验 TJPFinalAdavancedHeader *header = (TJPFinalAdavancedHeader *)packet.bytes; // 使用crc32ForData方法计算expectedChecksum uint32_t expectedChecksum = [TJPNetworkUtil crc32ForData:data]; // 转换协议头的checksum为主机字节序,并进行比较 XCTAssertEqual(ntohl(header->checksum), expectedChecksum, @"CRC 校验失败"); } - (void)testCompressAndDecompressData { NSData *data = [@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]; // 压缩数据 NSData *compressedData = [TJPNetworkUtil compressData:data]; XCTAssertNotNil(compressedData, @"压缩后的数据不应为空"); // 解压数据 NSData *decompressedData = [TJPNetworkUtil decompressData:compressedData]; XCTAssertNotNil(decompressedData, @"解压后的数据不应为空"); // 验证解压后的数据是否与原始数据相同 XCTAssertEqualObjects(decompressedData, data, @"解压后的数据与原始数据不一致"); } - (void)testBase64EncodeDecode { NSData *data = [@"Test Data" dataUsingEncoding:NSUTF8StringEncoding]; // 编码数据 NSString *encodedString = [TJPNetworkUtil base64EncodeData:data]; XCTAssertNotNil(encodedString, @"Base64 编码后的字符串不应为空"); // 解码字符串 NSData *decodedData = [TJPNetworkUtil base64DecodeString:encodedString]; XCTAssertNotNil(decodedData, @"Base64 解码后的数据不应为空"); // 验证解码后的数据是否与原始数据相同 XCTAssertEqualObjects(decodedData, data, @"Base64 解码后的数据与原始数据不一致"); } - (void)testDeviceIPAddress { NSString *ipAddress = [TJPNetworkUtil deviceIPAddress]; XCTAssertNotNil(ipAddress, @"设备 IP 地址不应为空"); XCTAssertTrue([TJPNetworkUtil isValidIPAddress:ipAddress], @"设备 IP 地址无效"); // 测试有效的 IP 地址 NSString *validIP = @"192.168.1.1"; XCTAssertTrue([TJPNetworkUtil isValidIPAddress:validIP], @"有效的 IP 地址应该返回 YES"); // 测试无效的 IP 地址 NSString *invalidIP = @"999.999.999.999"; XCTAssertFalse([TJPNetworkUtil isValidIPAddress:invalidIP], @"无效的 IP 地址应该返回 NO"); } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end ================================================ FILE: iOS-Network-Stack-DiveTests/iOS_Network_Stack_DiveTests.m ================================================ // // iOS_Network_Stack_DiveTests.m // iOS-Network-Stack-DiveTests // // Created by 唐佳鹏 on 2025/3/20. // #import @interface iOS_Network_Stack_DiveTests : XCTestCase @end @implementation iOS_Network_Stack_DiveTests - (void)setUp { // Put setup code here. This method is called before the invocation of each test method in the class. } - (void)tearDown { // Put teardown code here. This method is called after the invocation of each test method in the class. } - (void)testExample { // This is an example of a functional test case. // Use XCTAssert and related functions to verify your tests produce the correct results. } - (void)testPerformanceExample { // This is an example of a performance test case. [self measureBlock:^{ // Put the code you want to measure the time of here. }]; } @end