Bgerstle has uploaded a new change for review. https://gerrit.wikimedia.org/r/220527
Change subject: centralized image caching & retrieval ...................................................................... centralized image caching & retrieval Project changes: - Added SDWebImage fork as a submodule, which means you now need to run `git submodule update --init --recursive` to grab the files needed by the project (which CocoaPods integrates into the project like any other Pod)... - Luckily `make pod` will now do this for you Code changes: - Wrote WMFImageController to manage fetching, caching, and downloading images (on top of SDWebImageManager) - Rewrote WMFArticleImageProtocol to use WMFImageController What works right now: - Lead & article images in the webview (face detection should also work, but can't search to find an article with a face) - Pull to refresh loading from the cache What doesn't work: - Native images (not using old MWKDataStore image storage) - As a result of this, saved pages is also broken until migration is implemented & WMFImageController is integrated - Keeping images for saved pages: need to add lookup during disk cleanup, pending extension of SDWebImage in next ticket (T101527) Bug: T95350 Change-Id: I98037e1637fd8d6534a570480edc0ab1556fb44b --- M Wikipedia.xcodeproj/project.pbxproj A Wikipedia/Images/SDWebImageManager+PromiseKit.swift A Wikipedia/Images/WMFImageController.swift A Wikipedia/Networking/Cancellable.swift M Wikipedia/Protocols/WMFArticleImageProtocol.m M Wikipedia/Wikipedia-Bridging-Header.h A Wikipedia/mw-utils/SwiftUtilities.swift 7 files changed, 386 insertions(+), 148 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/apps/ios/wikipedia refs/changes/27/220527/10 diff --git a/Wikipedia.xcodeproj/project.pbxproj b/Wikipedia.xcodeproj/project.pbxproj index 30633d4..64df850 100644 --- a/Wikipedia.xcodeproj/project.pbxproj +++ b/Wikipedia.xcodeproj/project.pbxproj @@ -321,6 +321,10 @@ BCB848781AAAABF80077EC24 /* WMFRoundingUtilities.c in Sources */ = {isa = PBXBuildFile; fileRef = BCB848771AAAABF80077EC24 /* WMFRoundingUtilities.c */; }; BCB8487B1AAAADF90077EC24 /* WMFRoundingUtilitiesTests.m in Sources */ = {isa = PBXBuildFile; fileRef = BCB8487A1AAAADF90077EC24 /* WMFRoundingUtilitiesTests.m */; }; BCB848831AAE0C5C0077EC24 /* WMFImageGalleryCollectionViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 08D631F91A69B8CD00D87AD0 /* WMFImageGalleryCollectionViewCell.m */; }; + BCBDC8821B38E414003A6D17 /* SDWebImageManager+PromiseKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCBDC8811B38E414003A6D17 /* SDWebImageManager+PromiseKit.swift */; }; + BCBDC8841B38E441003A6D17 /* WMFImageController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCBDC8831B38E441003A6D17 /* WMFImageController.swift */; }; + BCBDC88C1B3A0715003A6D17 /* Cancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCBDC88B1B3A0715003A6D17 /* Cancellable.swift */; }; + BCBDC88E1B3A42E7003A6D17 /* SwiftUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCBDC88D1B3A42E7003A6D17 /* SwiftUtilities.swift */; }; BCC185D81A9E5628005378F8 /* UILabel+WMFStyling.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC185D71A9E5628005378F8 /* UILabel+WMFStyling.m */; }; BCC185E01A9EC836005378F8 /* UIButton+FrameUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC185DF1A9EC836005378F8 /* UIButton+FrameUtils.m */; }; BCC185E81A9FA498005378F8 /* UICollectionViewFlowLayout+AttributeUtils.m in Sources */ = {isa = PBXBuildFile; fileRef = BCC185E71A9FA498005378F8 /* UICollectionViewFlowLayout+AttributeUtils.m */; }; @@ -968,6 +972,10 @@ BCB848771AAAABF80077EC24 /* WMFRoundingUtilities.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = WMFRoundingUtilities.c; sourceTree = "<group>"; }; BCB8487A1AAAADF90077EC24 /* WMFRoundingUtilitiesTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WMFRoundingUtilitiesTests.m; sourceTree = "<group>"; }; BCB848811AAE06420077EC24 /* ImageGallerySpecs */ = {isa = PBXFileReference; explicitFileType = text.script.sh; fileEncoding = 4; name = ImageGallerySpecs; path = "Image Gallery/ImageGallerySpecs"; sourceTree = "<group>"; }; + BCBDC8811B38E414003A6D17 /* SDWebImageManager+PromiseKit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = "SDWebImageManager+PromiseKit.swift"; path = "Images/SDWebImageManager+PromiseKit.swift"; sourceTree = "<group>"; }; + BCBDC8831B38E441003A6D17 /* WMFImageController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = WMFImageController.swift; path = Images/WMFImageController.swift; sourceTree = "<group>"; }; + BCBDC88B1B3A0715003A6D17 /* Cancellable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Cancellable.swift; sourceTree = "<group>"; }; + BCBDC88D1B3A42E7003A6D17 /* SwiftUtilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftUtilities.swift; sourceTree = "<group>"; }; BCBDE0AB1AA76EAC006BD29A /* WMFImageURLParsingTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = WMFImageURLParsingTests.m; sourceTree = "<group>"; }; BCC185D61A9E5628005378F8 /* UILabel+WMFStyling.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UILabel+WMFStyling.h"; sourceTree = "<group>"; }; BCC185D71A9E5628005378F8 /* UILabel+WMFStyling.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UILabel+WMFStyling.m"; sourceTree = "<group>"; }; @@ -1529,6 +1537,7 @@ BC955BCE1A82C2FA000EF9E4 /* AFHTTPRequestOperationManager+WMFConfig.m */, BC50C37D1A83C784006DC7AF /* WMFNetworkUtilities.h */, BC50C37E1A83C784006DC7AF /* WMFNetworkUtilities.m */, + BCBDC88B1B3A0715003A6D17 /* Cancellable.swift */, ); path = Networking; sourceTree = "<group>"; @@ -2129,6 +2138,15 @@ path = Categories; sourceTree = "<group>"; }; + BC628C791B389E2B00B3F85C /* Images */ = { + isa = PBXGroup; + children = ( + BCBDC8831B38E441003A6D17 /* WMFImageController.swift */, + BCBDC8811B38E414003A6D17 /* SDWebImageManager+PromiseKit.swift */, + ); + name = Images; + sourceTree = "<group>"; + }; BC69C3101AB0C16B0090B039 /* View Model */ = { isa = PBXGroup; children = ( @@ -2462,6 +2480,7 @@ BC092B971B18E8AF00093C59 /* NSString+WMFPageUtilities.h */, BC092B951B18E89200093C59 /* NSString+WMFPageUtilities.m */, BC7E4A491B34A26A00EECD8B /* WMFLogging.h */, + BCBDC88D1B3A42E7003A6D17 /* SwiftUtilities.swift */, ); path = "mw-utils"; sourceTree = "<group>"; @@ -2564,6 +2583,7 @@ D4B0ADFF19365F4600F0AC90 /* EventLogging */, 0442F57C1900718600F55DF9 /* Fonts */, 0493C2C91952373100EBB973 /* Housekeeping */, + BC628C791B389E2B00B3F85C /* Images */, 0463639518A844380049EE4F /* Keychain */, 0487041519F824D700B7D307 /* Networking */, 04616DF71AE7060C00815BCE /* Protocols */, @@ -3160,6 +3180,7 @@ BC23759A1AB78D8A00B0BAA8 /* NSParagraphStyle+WMFNaturalAlignmentStyle.m in Sources */, 04090A3B187FB7D000577EDF /* UIView+Debugging.m in Sources */, BCC185D81A9E5628005378F8 /* UILabel+WMFStyling.m in Sources */, + BCBDC8841B38E441003A6D17 /* WMFImageController.swift in Sources */, 0EFB0F241B31EE2D00D05C08 /* Saved.m in Sources */, BCB669B11A83F6C400C7B1FE /* MWKRecentSearchList.m in Sources */, 04DD89B118BFE63A00DD5DAD /* PreviewAndSaveViewController.m in Sources */, @@ -3236,6 +3257,7 @@ BCB58F671A8AA22200465627 /* MWKLicense+ToGlyph.m in Sources */, 0EFB0F191B31EE2D00D05C08 /* Article.m in Sources */, 04C43AAE18344131006C643B /* CommunicationBridge.m in Sources */, + BCBDC8821B38E414003A6D17 /* SDWebImageManager+PromiseKit.swift in Sources */, 045AB8C31B1E15D9002839D7 /* NSURL+Extras.m in Sources */, 04224501197F5E09005DD0BF /* BulletedLabel.m in Sources */, C979727D1A731F2D00C6ED7A /* WMFShareOptionsView.m in Sources */, @@ -3272,6 +3294,7 @@ 04F39590186CF80100B0D6FC /* TOCViewController.m in Sources */, BCE24FDF1B0CF0C7003F054B /* LegacyPhoneGapDataMigrator.m in Sources */, 042950D41A9D3BA7009BE784 /* UIColor+WMFHexColor.m in Sources */, + BCBDC88C1B3A0715003A6D17 /* Cancellable.swift in Sources */, 0439317619FB092600386E8F /* UIWebView+LoadAssetsHtml.m in Sources */, 04B91AA718E34BBC00FFAA1C /* UIView+TemporaryAnimatedXF.m in Sources */, 04224500197F5E09005DD0BF /* AbuseFilterAlert.m in Sources */, @@ -3301,6 +3324,7 @@ BCE24FDD1B0CF0C7003F054B /* LegacyCoreDataMigrator.m in Sources */, 04478633185145090050563B /* HistoryViewController.m in Sources */, BCB669B21A83F6C400C7B1FE /* MWKImageInfo.m in Sources */, + BCBDC88E1B3A42E7003A6D17 /* SwiftUtilities.swift in Sources */, BCB848781AAAABF80077EC24 /* WMFRoundingUtilities.c in Sources */, D4B0AE051936604700F0AC90 /* EditFunnel.m in Sources */, 0487048319F8262600B7D307 /* FetcherBase.m in Sources */, diff --git a/Wikipedia/Images/SDWebImageManager+PromiseKit.swift b/Wikipedia/Images/SDWebImageManager+PromiseKit.swift new file mode 100644 index 0000000..09e60ed --- /dev/null +++ b/Wikipedia/Images/SDWebImageManager+PromiseKit.swift @@ -0,0 +1,49 @@ +// +// SDWebImageManager+PromiseKit.swift +// Wikipedia +// +// Created by Brian Gerstle on 6/22/15. +// Copyright (c) 2015 Wikimedia Foundation. All rights reserved. +// + +import Foundation + +public typealias ImageDownload = (image: UIImage, data: NSData?) + +public class ImageOperationWrapper<T> : Promise<T>, Cancellable { + var operation: SDWebImageOperation! + + // need our own special version of "defer" because Promise is a class, not a protocol + public class func wrapper() -> (ImageOperationWrapper<T>, (T)->Void, (NSError)->Void) { + var sealant: Sealant<T>! + var wrapper = ImageOperationWrapper<T>(sealant: { sealant = $0 }) + return (wrapper, sealant.resolve, sealant.resolve) + } + + // `wrapper()` is provided as a convenience (similar to `Promise.defer()`) + public init(sealant: (Sealant<T>)->Void) { + super.init(sealant: sealant) + } + + func cancel() -> Void { + // cancel underlying operation (idempotent) + operation.cancel() + } +} + +extension SDWebImageManager { + func promisedImageWithURL(URL: NSURL, options: SDWebImageOptions) -> ImageOperationWrapper<ImageDownload> { + let (wrapper, fulfill, reject) = ImageOperationWrapper<ImageDownload>.wrapper() + wrapper.operation = self.downloadImageAndDataWithURL(URL, + options: options, + progress: nil) + { img, data, err, cacheType, finished, imageURL in + if finished && err == nil { + fulfill((img, data)) + } else { + reject(err) + } + } + return wrapper + } +} diff --git a/Wikipedia/Images/WMFImageController.swift b/Wikipedia/Images/WMFImageController.swift new file mode 100644 index 0000000..5316e5c --- /dev/null +++ b/Wikipedia/Images/WMFImageController.swift @@ -0,0 +1,110 @@ +// +// WMFImageController.swift +// Wikipedia +// +// Created by Brian Gerstle on 6/22/15. +// Copyright (c) 2015 Wikimedia Foundation. All rights reserved. +// + +import Foundation + +@objc +public class WMFImageController : NSObject { + + /// MARK: Initialization + + private static let _sharedInstance = { + return WMFImageController(manager: SDWebImageManager.sharedManager()) + }() + + class func sharedInstance() -> WMFImageController { + return _sharedInstance + } + + private let imageManager: SDWebImageManager + + private lazy var cancellingQueue: dispatch_queue_t = { + dispatch_queue_create("org.wikimedia.wikipedia.wmfimagecontroller.\(address(self))", + DISPATCH_QUEUE_CONCURRENT) + }() + + private lazy var cancellables: NSMapTable = { + NSMapTable.strongToWeakObjectsMapTable() + }() + + public init(manager: SDWebImageManager) { + self.imageManager = manager; + } + + public convenience init(sharedCacheFromController otherController: WMFImageController) { + self.init(manager: SDWebImageManager(downloader: SDWebImageDownloader(), + cache: otherController.imageManager.imageCache)) + } + + /// MARK: Fetching + + /// Retrieve the data and uncompressed image for `url`. + public func fetchImageWithURL(url: NSURL) -> Promise<ImageDownload> { + let promise = imageManager.promisedImageWithURL(url, options: SDWebImageOptions.allZeros) + self.addCancellableForURL(promise, url: url) + return promise + } + + public func isDownloadingImageWithURL(url: NSURL) -> Bool { + return imageManager.imageDownloader.isDownloadingImageAtURL(url) + } + + // MARK: Caching + + public func dataForImageWithURL(url: NSURL) -> NSData? { + return imageManager.imageCache.dataFromDiskCacheForKey(url.absoluteString!) + } + + public func cachedImageWithURL(url: NSURL) -> UIImage? { + return imageManager.imageCache.imageFromDiskCacheForKey(url.absoluteString!) + } + + public func cancelFetchForURL(url: NSURL) { + weak var wself = self; + dispatch_barrier_async(self.cancellingQueue) { + let sself = wself + if let cancellable = sself?.cancellables.objectForKey(url.absoluteString!) as? WrapperObject<Cancellable> { + sself?.cancellables.removeObjectForKey(url.absoluteString!) + cancellable.value.cancel() + } + } + } + + /// MARK: Private + + private func addCancellableForURL(cancellable: Cancellable, url: NSURL) { + weak var wself = self; + dispatch_async(self.cancellingQueue) { + let sself = wself + sself?.cancellables.setObject(WrapperObject(value: cancellable), forKey: url.absoluteString!) + } + } +} + +/// MARK: Objective-C Bridge + +/// Class which packages up the Swift tuple response from `fetchImageWithURL(url:)` +@objc class WMFImageDownload : NSObject { + var image: UIImage + var data: NSData? + + init(image: UIImage, data: NSData?) { + self.image = image + self.data = data + } +} + +extension WMFImageController { + /// Objective-C compatible `AnyPromise` which resolves to `WMFImageDownload`. + public func fetchImageWithURL(url: NSURL) -> AnyPromise { + let adaptedPromise: Promise<WMFImageDownload> = fetchImageWithURL(url).thenInBackground() { img, data in + return WMFImageDownload(image: img, data: data) + } + return AnyPromise(bound: adaptedPromise) + } +} diff --git a/Wikipedia/Networking/Cancellable.swift b/Wikipedia/Networking/Cancellable.swift new file mode 100644 index 0000000..eefac05 --- /dev/null +++ b/Wikipedia/Networking/Cancellable.swift @@ -0,0 +1,13 @@ +// +// Cancellable.swift +// Wikipedia +// +// Created by Brian Gerstle on 6/23/15. +// Copyright (c) 2015 Wikimedia Foundation. All rights reserved. +// + +import Foundation + +protocol Cancellable { + func cancel() -> Void +} diff --git a/Wikipedia/Protocols/WMFArticleImageProtocol.m b/Wikipedia/Protocols/WMFArticleImageProtocol.m index fd8582b..83e2b0e 100644 --- a/Wikipedia/Protocols/WMFArticleImageProtocol.m +++ b/Wikipedia/Protocols/WMFArticleImageProtocol.m @@ -5,52 +5,112 @@ #import "NSURL+WMFRest.h" #import "SessionSingleton.h" #import "NSString+Extras.h" +#import "Wikipedia-Swift.h" +#import "PromiseKit.h" + +#pragma mark - Logging Config + +// Set the level for logs in this file +#undef LOG_LEVEL_DEF +#define LOG_LEVEL_DEF WMFArticleImageProtocolLogLevel +static const int WMFArticleImageProtocolLogLevel = DDLogLevelInfo; + +#pragma mark - Typedefs + +typedef void (^ WMFImageDownloadBlock)(WMFImageDownload* download); + +#pragma mark - Constants NSString* const WMFArticleImageSectionImageRetrievedNotification = @"WMFSectionImageRetrieved"; +static NSString* const WMFArticleImageProtocolHost = @"upload.wikimedia.org"; -// We need to set a property on the request to prevent infinite loops due to handling "http(s)" requests. -// See: http://www.raywenderlich.com/59982/nsurlprotocol-tutorial -static NSString* const WMFArticleImageProtocolAlreadyHandled = @"WMFArticleImageProtocolAlreadyHandled"; +#pragma mark - Handling Checks -static NSString* const WMFArticleImageProtocolHost = @"upload.wikimedia.org"; - -__attribute__((constructor)) static void WMFRegisterArticleImageProtocol() { - [NSURLProtocol registerClass:[WMFArticleImageProtocol class]]; +static inline BOOL isSchemeHandled(NSURLRequest* request) { + if ([[[request URL] scheme] hasPrefix:@"http"]) { + return YES; + } else { + DDLogVerbose(@"Skipping non-HTTP request: %@", request); + return NO; + } } -@interface WMFArticleImageProtocol () <NSURLConnectionDelegate> -@property (nonatomic, strong) NSURLConnection* connection; -@property (nonatomic, strong) NSMutableData* mutableImageData; -@property (nonatomic, strong) NSURLResponse* response; -@end +static NSArray* mimeTypesToCache() { + static NSArray* types = nil; + static dispatch_once_t once; + dispatch_once(&once, ^{ + types = @[@"image/jpeg", @"image/png", @"image/gif"]; + }); + return types; +} + +static inline BOOL isImageMIMEType(NSURLRequest* request) { + NSString* extension = request.URL.pathExtension; + // HAX: the path extension of a request URL is not always a reliable way to detect its (expected) response MIME type + // maybe we should check request content-type header? would need to set Accept: application/json for API requests + if ([mimeTypesToCache() containsObject:[extension wmf_mimeTypeForExtension]]) { + return YES; + } else { + DDLogVerbose(@"Skipping request %@ with MIME type: %@", request, extension); + return NO; + } +} + +static BOOL imagePlaceholderExists(NSURLRequest* request) { + NSArray* variants = + [[SessionSingleton sharedInstance].currentArticle.images imageSizeVariants:request.URL.absoluteString]; + if (variants.count != 0) { + return YES; + } else { + DDLogVerbose(@"Skipping request without image variant placeholder: %@", request); + return NO; + } +} + +static inline BOOL isNotDownloadingImageFromURL(NSURLRequest* request) { + if ([[WMFImageController sharedInstance] isDownloadingImageWithURL:request.URL]) { + DDLogVerbose(@"Skipping redundant download for image at %@", request); + return NO; + } else { + return YES; + } +} + +static inline BOOL isFromWikipediaImageHost(NSURLRequest* request) { + if ([request.URL.host wmf_caseInsensitiveContainsString:WMFArticleImageProtocolHost]) { + return YES; + } else { + DDLogVerbose(@"Skipping request with host not matching %@: %@", WMFArticleImageProtocolHost, request); + return NO; + } +} @implementation WMFArticleImageProtocol +#pragma mark - Registration & Initialization + ++ (void)load { + [NSURLProtocol registerClass:self]; +} + + (BOOL)canInitWithRequest:(NSURLRequest*)request { - if ( - // Has 'http' or 'https' scheme and 'upload.wikimedia.org' host. - ![[request URL] wmf_conformsToAnyOfSchemes:[self schemesToExamine] andHasHost:WMFArticleImageProtocolHost] || - - // Prevent multiple 'startLoading' calls for a given resource. - [NSURLProtocol propertyForKey:WMFArticleImageProtocolAlreadyHandled inRequest:request] || - - // Check that extension is one we are interested in. - ![self isFileExtensionRerouted:request.URL.pathExtension] || - - // Only interested if image (or a size variant of image) has a data store record. - // (we make placeholder records when the html is received and parsed) - ![self imageVariantPlaceHolderRecordFoundForRequest:request] - ) { - return NO; - } - - return YES; + return isSchemeHandled(request) + && isImageMIMEType(request) + && isFromWikipediaImageHost(request) + // BG: Do these checks last, as they're the most expensive + && imagePlaceholderExists(request) + && isNotDownloadingImageFromURL(request); } -+ (BOOL)imageVariantPlaceHolderRecordFoundForRequest:(NSURLRequest*)request { - NSArray* variants = [self.article.images imageSizeVariants:request.URL.absoluteString]; - return (!variants || (variants.count == 0)) ? NO : YES; ++ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request { + return [[NSURLRequest alloc] initWithURL:request.URL]; } + ++ (BOOL)requestIsCacheEquivalent:(NSURLRequest*)a toRequest:(NSURLRequest*)b { + return [super requestIsCacheEquivalent:a toRequest:b]; +} + +#pragma mark - Getters + (MWKArticle*)article { return [SessionSingleton sharedInstance].currentArticle; @@ -60,132 +120,71 @@ return [WMFArticleImageProtocol article]; } -+ (NSArray*)schemesToExamine { - static NSArray* schemes = nil; - static dispatch_once_t once; - dispatch_once(&once, ^{ - schemes = @[@"https", @"http"]; - }); - return schemes; -} +#pragma mark - NSURLProtocol -+ (NSArray*)mimeTypesToCache { - static NSArray* types = nil; - static dispatch_once_t once; - dispatch_once(&once, ^{ - types = @[@"image/jpeg", @"image/png", @"image/gif"]; - }); - return types; -} - -+ (BOOL)isFileExtensionRerouted:(NSString*)extension { - return [self.mimeTypesToCache containsObject:[extension wmf_mimeTypeForExtension]]; -} - -+ (NSURLRequest*)canonicalRequestForRequest:(NSURLRequest*)request { - return request; -} - -+ (BOOL)requestIsCacheEquivalent:(NSURLRequest*)a toRequest:(NSURLRequest*)b { - return [super requestIsCacheEquivalent:a toRequest:b]; +- (void)stopLoading { + [[WMFImageController sharedInstance] cancelFetchForURL:self.request.URL]; } - (void)startLoading { - MWKImage* image = [self.article existingImageWithURL:[self.request.URL absoluteString]]; - if ([image isCached]) { - [self respondWithImageData:[image asNSData]]; - } else { - [self sendImageRequest:self.request]; - } + DDLogVerbose(@"Fetching image %@", self.request.URL); + [[WMFImageController sharedInstance] fetchImageWithURL:self.request.URL] + .thenInBackground([self respondWithDataFromDownload]) + .then([self sendImageDownloadedNotification]) + .catch([self respondWithError]); } -- (void)respondWithImageData:(NSData*)imageData { - NSURLResponse* response = - [[NSURLResponse alloc] initWithURL:self.request.URL - MIMEType:[self.request.URL.pathExtension wmf_mimeTypeForExtension] - expectedContentLength:imageData.length - textEncodingName:nil]; +#pragma mark - Callbacks - [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; - [[self client] URLProtocol:self didLoadData:imageData]; - [[self client] URLProtocolDidFinishLoading:self]; +- (WMFImageDownloadBlock)respondWithDataFromDownload { + return ^(WMFImageDownload* download) { + NSData* data = download.data; + if (!data) { + // HAX: manually read the data in case the image was returned directly from memory + data = [[WMFImageController sharedInstance] dataForImageWithURL:self.request.URL]; + } + if (!data) { + DDLogWarn(@"Disk cache miss for request: %@", self.request); + data = [NSData data]; + } + DDLogVerbose(@"Sending image response for %@", self.request.URL); + NSURLResponse* response = + [[NSURLResponse alloc] initWithURL:self.request.URL + MIMEType:[self.request.URL.pathExtension wmf_mimeTypeForExtension] + expectedContentLength:data.length + textEncodingName:nil]; + + // prevent browser from caching images (hopefully?) + [[self client] URLProtocol:self + didReceiveResponse:response + cacheStoragePolicy:NSURLCacheStorageNotAllowed]; + + [[self client] URLProtocol:self didLoadData:data]; + [[self client] URLProtocolDidFinishLoading:self]; + }; } -- (void)sendImageRequest:(NSURLRequest*)request { - NSMutableURLRequest* mutableRequest = [request mutableCopy]; - [NSURLProtocol setProperty:@YES forKey:WMFArticleImageProtocolAlreadyHandled inRequest:mutableRequest]; - self.connection = [NSURLConnection connectionWithRequest:mutableRequest delegate:self]; +- (WMFImageDownloadBlock)sendImageDownloadedNotification { + return ^(WMFImageDownload* download) { + MWKImage* image = [[WMFArticleImageProtocol article] existingImageWithURL:self.request.URL.absoluteString]; + // If this is a size variant, MWKImage placeholder record won't exist, so we'll need to create one. + if (!image) { + // Use kMWKArticleSectionNone (section Images.plist's should be just the orig image urls, not + // all the variants from the src set). + image = [[WMFArticleImageProtocol article] importImageURL:self.request.URL.absoluteString + sectionId:kMWKArticleSectionNone]; + } + [[NSNotificationCenter defaultCenter] postNotificationName:WMFArticleImageSectionImageRetrievedNotification + object:image + userInfo:nil]; + }; } -- (void)stopLoading { - [self.connection cancel]; - self.connection = nil; -} - -- (void)connection:(NSURLConnection*)connection didReceiveResponse:(NSURLResponse*)response { - [self.client URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; - self.response = response; - self.mutableImageData = [[NSMutableData alloc] init]; -} - -- (void)connection:(NSURLConnection*)connection didReceiveData:(NSData*)data { - [self.client URLProtocol:self didLoadData:data]; - [self.mutableImageData appendData:data]; -} - -- (void)connectionDidFinishLoading:(NSURLConnection*)connection { - [self.client URLProtocolDidFinishLoading:self]; - - MWKImage* image = [self saveImageToDataCache:self.mutableImageData]; - if (image) { - dispatch_async(dispatch_get_main_queue(), ^(){ - [self broadcastInfoForImage:image]; - }); - } -} - -- (void)connection:(NSURLConnection*)connection didFailWithError:(NSError*)error { - [self.client URLProtocol:self didFailWithError:error]; -} - -/** - * Saves image to the data store record for the current article. - * - * @return Returns image on success, nil on fail. - */ -- (MWKImage*)saveImageToDataCache:(NSData*)imageData { - NSAssert(self.request, @"No request found."); - NSAssert(self.request.URL, @"No request URL found."); - NSAssert(imageData, @"Attempt to save Nil image data to data store for URL: %@", self.request.URL.absoluteString); - - if (!imageData) { - return nil; - } - - // "canInitWithRequest:" already determined that this image, or a size variant of it, has a placeholder MWKImage record. - MWKImage* image = [self.article existingImageWithURL:self.request.URL.absoluteString]; - // If this is a size variant, MWKImage placeholder record won't exist, so we'll need to create one. - if (!image) { - // Use kMWKArticleSectionNone (section Images.plist's should be just the orig image urls, not - // all the variants from the src set). - image = [self.article importImageURL:self.request.URL.absoluteString sectionId:kMWKArticleSectionNone]; - } - - @try { - //NSLog(@"Rerouting cached response to WMF data store for %@", self.request); - [self.article importImageData:imageData image:image]; - }@catch (NSException* e) { - NSAssert(false, @"Failure to save cached image data: %@ \n %@", e, self.request.URL.absoluteString); - return nil; - } - - return image; -} - -- (void)broadcastInfoForImage:(MWKImage*)image { - [[NSNotificationCenter defaultCenter] postNotificationName:WMFArticleImageSectionImageRetrievedNotification - object:image - userInfo:nil]; +- (PMKRejecter)respondWithError { + return ^(NSError* error) { + DDLogWarn(@"Failed to fetch image at %@ due to %@", self.request.URL, error); + [self.client URLProtocol:self didFailWithError:error]; + }; } @end diff --git a/Wikipedia/Wikipedia-Bridging-Header.h b/Wikipedia/Wikipedia-Bridging-Header.h index 3e43ef0..d3db1d9 100644 --- a/Wikipedia/Wikipedia-Bridging-Header.h +++ b/Wikipedia/Wikipedia-Bridging-Header.h @@ -1 +1,5 @@ #import "WikipediaAppUtils.h" +#import <SDWebImage/SDWebImageManager.h> +#import <SDWebImage/UIImage+MultiFormat.h> +#import "MediaWikiKit.h" +#import "WMFGCDHelpers.h" diff --git a/Wikipedia/mw-utils/SwiftUtilities.swift b/Wikipedia/mw-utils/SwiftUtilities.swift new file mode 100644 index 0000000..1905a89 --- /dev/null +++ b/Wikipedia/mw-utils/SwiftUtilities.swift @@ -0,0 +1,39 @@ +// +// SwiftUtilities.swift +// Wikipedia +// +// Created by Brian Gerstle on 6/23/15. +// Copyright (c) 2015 Wikimedia Foundation. All rights reserved. +// + +import Foundation + +/// MARK: WrapperObject + +/// Allows boxing of `Any` objects (including functions) for storage in `Any`-incompatible Foundation collections. +public class WrapperObject<T: Any> { + var value: T + + public required init(value: T) { + self.value = value + } +} + +/// MARK: Addressable + +/// Protocol that allows you to generically get the pointer address of an object. +public protocol Addressable { + func address() -> Int +} + +func address<A>(a: A) -> Int { + return unsafeBitCast(a, Int.self) +} + +func address<A: Addressable>(a: A) -> Int { + return a.address() +} + +public func addressString<T>(a: T) -> String { + return String(format: "%p", address(a)) +} -- To view, visit https://gerrit.wikimedia.org/r/220527 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I98037e1637fd8d6534a570480edc0ab1556fb44b Gerrit-PatchSet: 10 Gerrit-Project: apps/ios/wikipedia Gerrit-Branch: 5.0 Gerrit-Owner: Bgerstle <bgers...@wikimedia.org> Gerrit-Reviewer: Fjalapeno <cfl...@wikimedia.org> Gerrit-Reviewer: Mhurd <mh...@wikimedia.org> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits