DetailViewController.m 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731
  1. //
  2. // DetailViewController.m
  3. // iOSDemo
  4. //
  5. // Created by adam on 29/09/2012.
  6. // Copyright (c) 2012 na. All rights reserved.
  7. //
  8. #import "DetailViewController.h"
  9. @interface ImageLoadingOptions : NSObject
  10. @property(nonatomic) CGSize overrideImageSize;
  11. @property(nonatomic) float overrideImageRenderScale;
  12. @property(nonatomic,strong) SVGKSourceLocalFile* localFileSource;
  13. - (id)initWithSource:(SVGKSource*) source;
  14. @end
  15. static NSString * const kTimeIntervalForLastReRenderOfSVGFromMemory = @"timeIntervalForLastReRenderOfSVGFromMemory";
  16. @implementation ImageLoadingOptions
  17. - (id)initWithSource:(SVGKSource*) source
  18. {
  19. self = [super init];
  20. if (self) {
  21. if( [source isKindOfClass:[SVGKSourceLocalFile class]])
  22. self.localFileSource = (SVGKSourceLocalFile*) source;
  23. else
  24. ; // cannot auto-select loading options for anything except local files / bundle files
  25. self.overrideImageRenderScale = 1.0;
  26. self.overrideImageSize = CGSizeZero;
  27. }
  28. return self;
  29. }
  30. @end
  31. @interface DetailViewController ()
  32. @property (nonatomic, strong) UIPopoverController *popoverController;
  33. @property (nonatomic, strong) NSDate* startParseTime, * endParseTime;
  34. - (void)loadSVGFrom:(SVGKSource *) svgSource;
  35. - (void)shakeHead;
  36. /** Apple's NSTimer class is an old OS X class that doesn't place nicely with ObjC blocks
  37. (Apple needs to upgrade it). It sets up some nasty retain cycles and you can't set
  38. the .userInfo after its started (a side-effect of the internal retain'ing Apple does)
  39. By storing this reference here, we can give a block a reference in advance, while also
  40. giving the block's output to the timer as .userinfo at creation time
  41. */
  42. @property (nonatomic, strong) NSTimer* tickerLoadingApplesNSTimerSucks;
  43. @end
  44. @implementation DetailViewController
  45. @synthesize scrollViewForSVG;
  46. @synthesize toolbar, popoverController, contentView, detailItem;
  47. @synthesize viewActivityIndicator;
  48. @synthesize exportText = _exportText;
  49. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
  50. {
  51. self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
  52. if (self) {
  53. }
  54. return self;
  55. }
  56. - (void)dealloc {
  57. self.detailItem = nil;
  58. self.contentView = nil;
  59. }
  60. -(void)viewDidLoad
  61. {
  62. [super viewDidLoad];
  63. self.scrollViewForSVG.delegate = self;
  64. }
  65. - (void)viewWillAppear:(BOOL)animated {
  66. [super viewWillAppear:animated];
  67. [self setupNavigationBar];
  68. }
  69. CALayer* lastTappedLayer;
  70. CGFloat lastTappedLayerOriginalBorderWidth;
  71. CGColorRef lastTappedLayerOriginalBorderColor;
  72. CATextLayer *textLayerForLastTappedLayer;
  73. -(void) deselectTappedLayer
  74. {
  75. if( lastTappedLayer != nil )
  76. {
  77. #if ALLOW_SVGKFASTIMAGEVIEW_TO_DO_HIT_TESTING
  78. if( [self.contentView isKindOfClass:[SVGKFastImageView class]])
  79. {
  80. [lastTappedLayer removeFromSuperlayer]; // nothing else needed
  81. }
  82. else
  83. #endif
  84. {
  85. lastTappedLayer.borderWidth = lastTappedLayerOriginalBorderWidth;
  86. lastTappedLayer.borderColor = lastTappedLayerOriginalBorderColor;
  87. }
  88. [textLayerForLastTappedLayer removeFromSuperlayer];
  89. textLayerForLastTappedLayer = nil;
  90. lastTappedLayer = nil;
  91. }
  92. }
  93. - (void)setupNavigationBar {
  94. NSString *renderTitle = self.requiresLayeredImageView ? @"LayeredImageView" : @"FastImageView";
  95. UIBarButtonItem *renderItem = [[UIBarButtonItem alloc] initWithTitle:renderTitle style:UIBarButtonItemStylePlain target:self action:@selector(toggleRender:)];
  96. UIBarButtonItem *debugItem = [[UIBarButtonItem alloc] initWithTitle:@"Debug" style:UIBarButtonItemStylePlain target:self action:@selector(showHideBorder:)];
  97. UIBarButtonItem *animateItem = [[UIBarButtonItem alloc] initWithTitle:@"Animate" style:UIBarButtonItemStylePlain target:self action:@selector(animate:)];
  98. self.navigationItem.rightBarButtonItems = @[renderItem, debugItem, animateItem];
  99. }
  100. -(NSString*) layerInfo:(CALayer*) l
  101. {
  102. return [NSString stringWithFormat:@"%@:%@", [l class], NSStringFromCGRect(l.frame)];
  103. }
  104. /**
  105. Example of how to handle gaps on an SVG
  106. */
  107. -(void) handleTapGesture:(UITapGestureRecognizer*) recognizer
  108. {
  109. CGPoint p = [recognizer locationInView:self.scrollViewForSVG];
  110. #if ALLOW_SVGKFASTIMAGEVIEW_TO_DO_HIT_TESTING // look how much code this requires! It's insane! Use SVGKLayeredImageView instead if you need hit-testing!
  111. SVGKImage* svgImage = nil; // ONLY used for the hacky code below that demonstrates how complex hit-testing is on an SVGKFastImageView
  112. /**
  113. WARNING:
  114. Whenever you're using SVGKFastImageView, it "hides" the raw CALayers from you, and Apple
  115. doesn't provide an easy way around this (we do it this way because there are missing methods
  116. and bugs in Apple's UIScrollView, which SVGKFastImageView fixes).
  117. So, you cannot do a hittest on "SVGKFastImageView.layer" - that will always return the root,
  118. empty, full-size layer.
  119. Instead, you have to hit-test the layer INSIDE the fast imageview.
  120. --------
  121. HOWEVER: YOU SHOULD NOT BE DOING THIS!
  122. IF YOU NEED TO DO HIT-TESTING, USE SVGKLayeredImageView (as per the docs!)
  123. THE EXAMPLE CODE HERE SHOWS YOU HOW YOU COULD, IN THEORY, DO HIT-TESTING WITH EITHER, BUT IT
  124. IS HIGHLY RECOMMENDED NEVER TO USE HIT-TESTING WITH SVGKFastImageView!
  125. */
  126. #endif
  127. CALayer* layerForHitTesting;
  128. #if ALLOW_SVGKFASTIMAGEVIEW_TO_DO_HIT_TESTING // look how much code this requires! It's insane! Use SVGKLayeredImageView instead if you need hit-testing!
  129. if( [self.contentView isKindOfClass:[SVGKFastImageView class]])
  130. {
  131. layerForHitTesting = ((SVGKFastImageView*)self.contentView).image.CALayerTree;
  132. svgImage = ((SVGKFastImageView*)self.contentView).image;
  133. /**
  134. ALSO, because SVGKFastImageView DOES NOT ALTER the underlying layers when it zooms
  135. (the zoom is handled "fast" and done internally at 100% accuracy),
  136. any zoom will be ignored for the hit-test - we have to MANUALLY apply the zoom
  137. */
  138. CGSize scaleConvertImageToViewForHitTest = CGSizeMake( self.contentView.bounds.size.width / svgImage.size.width, self.contentView.bounds.size.height / svgImage.size.height ); // this is a copy/paste of the internal "SCALING" logic used in SVGKFastImageView
  139. p = CGPointApplyAffineTransform( p, CGAffineTransformInvert( CGAffineTransformMakeScale( scaleConvertImageToViewForHitTest.width, scaleConvertImageToViewForHitTest.height)) ); // must do the OPPOSITE of the zoom (to convert the 'seeming' point to the 'actual' point
  140. }
  141. else
  142. #endif
  143. layerForHitTesting = self.contentView.layer;
  144. CALayer* hitLayer = [layerForHitTesting hitTest:p];
  145. if( hitLayer == lastTappedLayer )
  146. [self deselectTappedLayer]; // do this both ways, but have to do it AFTER the if-test because it nil's one of the if-phrases!
  147. else {
  148. [self deselectTappedLayer]; // do this both ways, but have to do it AFTER the if-test because it nil's one of the if-phrases!
  149. #if ALLOW_SVGKFASTIMAGEVIEW_TO_DO_HIT_TESTING // look how much code this requires! It's insane! Use SVGKLayeredImageView instead if you need hit-testing!
  150. if( [self.contentView isKindOfClass:[SVGKFastImageView class]])
  151. {
  152. /** NEVER DO THIS - this is a proof-of-concept, but instead you should ALWAYS
  153. use SVGKLayeredImageView if you want to do hit-testing!
  154. */
  155. NSLog(@"WARNING: don't use SVGKFastImageView for hit-testing");
  156. /**
  157. Because SVGKFastImageView "hides" the layers, any visual changes we make
  158. will NOT be reflected on-screen.
  159. Instead, we have to put a NEW layer over the top
  160. */
  161. CALayer* absolutePositionedCloneLayer = [svgImage newCopyPositionedAbsoluteOfLayer:hitLayer];
  162. lastTappedLayer = [[CALayer alloc] init];
  163. lastTappedLayer.frame = absolutePositionedCloneLayer.frame;
  164. /**
  165. ALSO, because SVGKFastImageView DOES NOT ALTER the underlying layers when it zooms
  166. (the zoom is handled "fast" and done internally at 100% accuracy),
  167. any zoom will be ignored for the new layer - we have to MANUALLY apply the zoom
  168. */
  169. CGSize scaleConvertImageToView = CGSizeMake( self.contentView.bounds.size.width / svgImage.size.width, self.contentView.bounds.size.height / svgImage.size.height ); // this is a copy/paste of the internal "SCALING" logic used in SVGKFastImageView
  170. lastTappedLayer.frame = CGRectApplyAffineTransform( lastTappedLayer.frame, CGAffineTransformMakeScale(scaleConvertImageToView.width, scaleConvertImageToView.height));
  171. [self.contentView.layer addSublayer:lastTappedLayer];
  172. }
  173. else
  174. #endif
  175. lastTappedLayer = hitLayer;
  176. if( lastTappedLayer != nil )
  177. {
  178. lastTappedLayerOriginalBorderColor = lastTappedLayer.borderColor;
  179. lastTappedLayerOriginalBorderWidth = lastTappedLayer.borderWidth;
  180. lastTappedLayer.borderColor = [UIColor greenColor].CGColor;
  181. lastTappedLayer.borderWidth = 3.0;
  182. #if SHOW_DEBUG_INFO_ON_EACH_TAPPED_LAYER
  183. /** mtrubnikov's code for adding a text overlay showing exactly what you tapped
  184. */
  185. NSString* textToDraw = [NSString stringWithFormat:@"%@ (%@): {%.1f, %.1f} {%.1f, %.1f}", hitLayer.name, [hitLayer class], lastTappedLayer.frame.origin.x, lastTappedLayer.frame.origin.y, lastTappedLayer.frame.size.width, lastTappedLayer.frame.size.height];
  186. UIFont* fontToDraw = [UIFont fontWithName:@"Helvetica"
  187. size:14.0f];
  188. CGSize sizeOfTextRect = [textToDraw sizeWithAttributes:@{NSFontAttributeName: fontToDraw}];
  189. textLayerForLastTappedLayer = [[CATextLayer alloc] init];
  190. [textLayerForLastTappedLayer setFont:@"Helvetica"];
  191. [textLayerForLastTappedLayer setFontSize:14.0f];
  192. [textLayerForLastTappedLayer setFrame:CGRectMake(0, 0, sizeOfTextRect.width, sizeOfTextRect.height)];
  193. [textLayerForLastTappedLayer setString:textToDraw];
  194. [textLayerForLastTappedLayer setAlignmentMode:kCAAlignmentLeft];
  195. [textLayerForLastTappedLayer setForegroundColor:[UIColor redColor].CGColor];
  196. [textLayerForLastTappedLayer setContentsScale:[[UIScreen mainScreen] scale]];
  197. [textLayerForLastTappedLayer setShouldRasterize:NO];
  198. [self.contentView.layer addSublayer:textLayerForLastTappedLayer];
  199. /*
  200. * mtrubnikov's code for adding a text overlay showing exactly what you tapped*/
  201. #endif
  202. }
  203. }
  204. }
  205. #pragma mark - CRITICAL: this method makes Apple render SVGs in sharp focus
  206. -(void)scrollViewDidEndZooming:(UIScrollView *)scrollView withView:(UIView *)view atScale:(CGFloat)finalScale
  207. {
  208. /** NB: very important! The "finalScale" parameter to this method is SLIGHTLY DIFFERENT from the scale that Apple reports in the other delegate methods
  209. This is very confusing, clearly it's bit of a hack - the other methods get called
  210. at slightly the wrong time, and so their data is slightly wrong (out-by-one animation step).
  211. ONLY the values passed as params to this method are correct!
  212. */
  213. /**
  214. * If we use the SVGKLayeredImageView then there is no need to redraw the image - only update scale on text elements
  215. */
  216. if ([self.contentView isKindOfClass:[SVGKLayeredImageView class]]) {
  217. finalScale *= [UIScreen mainScreen].scale;
  218. NSMutableDictionary
  219. * newActions = [@{kCAOnOrderIn : [NSNull null], kCAOnOrderOut : [NSNull null], @"sublayers" : [NSNull null], @"contents" : [NSNull null], @"bounds" : [NSNull null]} mutableCopy];
  220. [CATransaction begin];
  221. [CATransaction setDisableActions:YES];
  222. // Override actions to prevent implicit CALayer animations that happen when setting scale
  223. [self setActions:newActions andUpdateScale:finalScale onTextSublayersOf:((SVGKLayeredImageView*)view).image.CALayerTree];
  224. [CATransaction commit];
  225. }
  226. else
  227. {
  228. /**
  229. Apple's implementation of zooming is EXTREMELY poorly designed; it's a hack onto a class
  230. that was only designed to do panning (hence the name: uiSCROLLview)
  231. So ... "zooming" via a UIScrollView is NOT integrated with UIView
  232. rendering - in a UIView subclass, you CANNOT KNOW whether you have been "zoomed"
  233. (i.e.: had your view contents ruined horribly by Apple's class)
  234. The three lines that follow are - allegedly - Apple's preferred way of handling
  235. the situation. Note that we DO NOT SET view.frame! According to official docs,
  236. view.frame is UNDEFINED (this is very worrying, breaks a huge amount of UIKit-related code,
  237. but that's how Apple has documented / implemented it!)
  238. */
  239. view.transform = CGAffineTransformIdentity; // this alters view.frame! But *not* view.bounds
  240. view.bounds = CGRectApplyAffineTransform(view.bounds, CGAffineTransformMakeScale(finalScale, finalScale));
  241. [view setNeedsDisplay];
  242. /**
  243. Workaround for another bug in Apple's hacks for UIScrollView:
  244. - when you reset the transform, as advised by Apple, you "break" Apple's memory of the scroll factor.
  245. ... because they "forgot" to store it anywhere (they read your view.transform as if it were a private
  246. variable inside UIScrollView! This causes MANY bugs in applications :( )
  247. */
  248. self.scrollViewForSVG.minimumZoomScale /= finalScale;
  249. self.scrollViewForSVG.maximumZoomScale /= finalScale;
  250. }
  251. }
  252. - (void) setActions:(NSMutableDictionary*)actions andUpdateScale:(float)scale onTextSublayersOf:(CALayer*)layer {
  253. if ([layer isKindOfClass:[CATextLayer class]]){
  254. layer.actions = actions;
  255. layer.contentsScale = scale;
  256. }
  257. for (CALayer* sublayer in layer.sublayers) {
  258. [self setActions:actions andUpdateScale:scale onTextSublayersOf:sublayer];
  259. }
  260. }
  261. #pragma mark - rest of class
  262. - (void)setDetailItem:(id)newDetailItem {
  263. if (detailItem != newDetailItem) {
  264. detailItem = newDetailItem;
  265. [self reload];
  266. }
  267. if (self.popoverController) {
  268. [self.popoverController dismissPopoverAnimated:YES];
  269. }
  270. }
  271. - (void)setContentView:(SVGKImageView *)newContentView {
  272. if (contentView == newContentView) {
  273. return;
  274. }
  275. if (contentView) {
  276. [contentView removeObserver:self forKeyPath:kTimeIntervalForLastReRenderOfSVGFromMemory];
  277. }
  278. if (newContentView) {
  279. /** Fast image view renders asynchronously, so we have to wait for a callback that its finished a render... */
  280. [newContentView addObserver:self forKeyPath:kTimeIntervalForLastReRenderOfSVGFromMemory options:0 context:nil];
  281. }
  282. contentView = newContentView;
  283. }
  284. -(void)reload {
  285. [self deselectTappedLayer]; // do this first because it DEPENDS UPON the type of self.contentView BEFORE the change in value
  286. // FIXME: re-write this class so that this method does NOT require self.view to exist
  287. [self view]; // Apple's design to trigger the creation of view. Original design of THIS class is that it breaks if view isn't already existing
  288. if (self.detailItem) {
  289. [self loadSVGFrom:self.detailItem];
  290. }
  291. }
  292. -(void) willLoadNewResource
  293. {
  294. // update the view
  295. self.subViewLoadingPopup.hidden = FALSE;
  296. self.progressLoading.progress = 0;
  297. [self.viewActivityIndicator startAnimating];
  298. /** Move the gesture recognizer off the old view */
  299. if( self.contentView != nil
  300. && self.tapGestureRecognizer != nil )
  301. [self.contentView removeGestureRecognizer:self.tapGestureRecognizer];
  302. [self.labelParseTime removeFromSuperview]; // we'll re-add to the new one
  303. [self.contentView removeFromSuperview];
  304. }
  305. /**
  306. If you want to emulate Apple's @2x naming system for UIImage, you can...
  307. */
  308. -(void) preProcessImageFor2X:(ImageLoadingOptions*) options
  309. {
  310. #if ALLOW_2X_STYLE_SCALING_OF_SVGS_AS_AN_EXAMPLE
  311. if( [options.localFileSource.filePath hasSuffix:@"@2x"])
  312. {
  313. SVGKSourceLocalFile* modifiedSource = [options.localFileSource copy];
  314. modifiedSource.filePath = [modifiedSource.filePath substringToIndex:modifiedSource.filePath.length - @"@2x".length];
  315. options.overrideImageRenderScale = 2.0;
  316. options.localFileSource = modifiedSource;
  317. }
  318. #endif
  319. }
  320. /**
  321. If you want to emulate Apple's @WxH naming system for UIImage, you can...
  322. */
  323. -(void) preProcessImageForAt160x240:(ImageLoadingOptions*) options
  324. {
  325. if( [options.localFileSource.filePath hasSuffix:@"@160x240"]) // could be any 999x999 you want, up to you to implement!
  326. {
  327. SVGKSourceLocalFile* modifiedSource = [options.localFileSource copy];
  328. modifiedSource.filePath = [modifiedSource.filePath substringToIndex:modifiedSource.filePath.length - @"@160x240".length];
  329. options.overrideImageSize = CGSizeMake( 160, 240 );
  330. options.localFileSource = modifiedSource;
  331. }
  332. }
  333. - (void)loadSVGFrom:(SVGKSource *) svgSource
  334. {
  335. [self willLoadNewResource];
  336. [[NSRunLoop mainRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.01]]; // makes the animation appear
  337. self.startParseTime = self.endParseTime = [NSDate date]; // reset them
  338. SVGKImage *document = nil;
  339. ImageLoadingOptions* loadingOptions = [[ImageLoadingOptions alloc] initWithSource:svgSource];
  340. /** Detect URL vs file */
  341. self.startParseTime = [NSDate date];
  342. if( [svgSource isKindOfClass:[SVGKSourceURL class]])
  343. {
  344. @try
  345. {
  346. /**
  347. This would work, but won't let us read any errors:
  348. document = [SVGKImage imageWithContentsOfURL:[NSURL URLWithString:name]];
  349. so, instead, we create an SVGKSource explicitly (as this demo app is taking
  350. user-input, and we have no idea how valid it is!)
  351. */
  352. document = [SVGKImage imageWithSource:svgSource];
  353. [self internalLoadedResource:svgSource withOptions:loadingOptions parserOutput:(document==nil)?nil:document.parseErrorsAndWarnings createImageViewFromDocument:document];
  354. }
  355. @catch( NSException* e )
  356. {
  357. [[[UIAlertView alloc] initWithTitle:@"SVG load failed" message:[NSString stringWithFormat:@"Error = %@", e] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
  358. [self internalLoadedResource:svgSource withOptions:loadingOptions parserOutput:nil createImageViewFromDocument:nil];
  359. }
  360. }
  361. else if( [svgSource isKindOfClass:[SVGKSourceLocalFile class]])
  362. {
  363. /** This demo shows different images being used in different ways.
  364. Here we setup special conditions based on the filename etc:
  365. */
  366. [self preProcessImageFor2X:loadingOptions];
  367. [self preProcessImageForAt160x240:loadingOptions];
  368. /** Detect the magic name(s) for the nil-demos */
  369. if( svgSource == nil )
  370. {
  371. /** This demonstrates / tests what happens if you create an SVGKLayeredImageView with a nil SVGKImage
  372. */
  373. [self didLoadNewResourceCreatingImageView:[[SVGKLayeredImageView alloc] init]];
  374. }
  375. else
  376. {
  377. /**
  378. the actual loading of the SVG file
  379. */
  380. #if LOAD_SYNCHRONOUSLY
  381. document = [SVGKImage imageNamed:[name stringByAppendingPathExtension:@"svg"]];
  382. [self internalLoadedResource:name withOptions:loadingOptions createImageViewFromDocument:document];
  383. #else
  384. SVGKParser* parser = [SVGKImage imageWithSource:svgSource
  385. onCompletion:^(SVGKImage *loadedImage, SVGKParseResult* parseResult)
  386. {
  387. [self.tickerLoadingApplesNSTimerSucks invalidate];
  388. dispatch_async(dispatch_get_main_queue(), ^{
  389. // must be on main queue since this affects the UIKit GUI!
  390. [self internalLoadedResource:svgSource withOptions:loadingOptions parserOutput:parseResult createImageViewFromDocument:loadedImage];
  391. });
  392. }];
  393. self.tickerLoadingApplesNSTimerSucks = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(tickLoadingSVG:) userInfo:parser repeats:TRUE];
  394. #endif
  395. }
  396. }
  397. else
  398. {
  399. [[[UIAlertView alloc] initWithTitle:@"SVG load failed" message:[NSString stringWithFormat:@"Unknown kind of source. Should be a recognized SVGKSource subclass. Was actually : %@", [svgSource class]] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
  400. [self internalLoadedResource:nil withOptions:loadingOptions parserOutput:nil createImageViewFromDocument:nil];
  401. }
  402. }
  403. /** Updates the on-screen loading bar */
  404. -(void) tickLoadingSVG:(NSTimer*) timer
  405. {
  406. SVGKParser* parser = (SVGKParser*) timer.userInfo;
  407. dispatch_async(dispatch_get_main_queue(),
  408. ^{
  409. // must be on main queue since this affects the UIKit GUI!
  410. self.progressLoading.progress = parser.currentParseRun.parseProgressFractionApproximate;
  411. });
  412. }
  413. /**
  414. Creates an appropriate SVGKImageView to display the loaded SVGKImage, and triggers the post-processing
  415. of on-screen displays
  416. */
  417. -(void) internalLoadedResource:(SVGKSource*) source withOptions:(ImageLoadingOptions*) loadingOptions parserOutput:(SVGKParseResult*) parseResult createImageViewFromDocument:(SVGKImage*) document
  418. {
  419. self.endParseTime = [NSDate date];
  420. SVGKImageView* newContentView = nil;
  421. if( loadingOptions.overrideImageRenderScale != 1.0 )
  422. document.scale = loadingOptions.overrideImageRenderScale;
  423. if( document == nil )
  424. {
  425. if( parseResult == nil )
  426. {
  427. [[[UIAlertView alloc] initWithTitle:@"SVG parse failed" message:@"Total failure. See console log" delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
  428. }
  429. else
  430. {
  431. [[[UIAlertView alloc] initWithTitle:@"SVG parse failed" message:[NSString stringWithFormat:@"Summary: %@",parseResult] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
  432. }
  433. newContentView = nil; // signals to the rest of this method: the load failed
  434. }
  435. else
  436. {
  437. if( document.parseErrorsAndWarnings.rootOfSVGTree != nil )
  438. {
  439. //NSLog(@"[%@] Freshly loaded document (name = %@) has size = %@", [self class], name, NSStringFromCGSize(document.size) );
  440. /** NB: the SVG Spec says that the "correct" way to upscale or downscale an SVG is by changing the
  441. SVG Viewport. SVGKit automagically does this for you if you ever set a value to image.scale */
  442. if( ! CGSizeEqualToSize( CGSizeZero, loadingOptions.overrideImageSize ) )
  443. document.size = loadingOptions.overrideImageSize; // preferred way to scale an SVG! (standards compliant!)
  444. if( self.requiresLayeredImageView )
  445. {
  446. newContentView = [[SVGKLayeredImageView alloc] initWithSVGKImage:document];
  447. }
  448. else
  449. {
  450. newContentView = [[SVGKFastImageView alloc] initWithSVGKImage:document];
  451. NSLog(@"[%@] WARNING: workaround for Apple bugs: UIScrollView spams tiny changes to the transform to the content view; currently, we have NO WAY of efficiently measuring whether or not to re-draw the SVGKImageView. As a temporary solution, we are DISABLING the SVGKImageView's auto-redraw-at-higher-resolution code - in general, you do NOT want to do this", [self class]);
  452. ((SVGKFastImageView*)newContentView).disableAutoRedrawAtHighestResolution = TRUE;
  453. }
  454. }
  455. if( parseResult.errorsFatal.count > 0 )
  456. {
  457. [[[UIAlertView alloc] initWithTitle:@"SVG parse failed" message:[NSString stringWithFormat:@"%@",parseResult] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
  458. // [[[[UIAlertView alloc] initWithTitle:@"SVG parse failed" message:[NSString stringWithFormat:@"%i fatal errors, %i warnings. First fatal = %@",[document.parseErrorsAndWarnings.errorsFatal count],[document.parseErrorsAndWarnings.errorsRecoverable count]+[document.parseErrorsAndWarnings.warnings count], ((NSError*)[document.parseErrorsAndWarnings.errorsFatal objectAtIndex:0]).localizedDescription] delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] autorelease] show];
  459. newContentView = nil; // signals to the rest of this method: the load failed
  460. }
  461. }
  462. self.sourceOfCurrentDocument = source;
  463. [self didLoadNewResourceCreatingImageView:newContentView];
  464. }
  465. /**
  466. Reconfigures the view to display the newly-loaded image, and display meta info
  467. about how long it took to parse, etc
  468. */
  469. -(void) didLoadNewResourceCreatingImageView:(SVGKImageView*) newContentView
  470. {
  471. if( newContentView != nil )
  472. {
  473. /**
  474. * NB: at this point we're guaranteed to have a "new" replacemtent ready for self.contentView
  475. */
  476. /******* swap the new contentview in ************/
  477. self.contentView = newContentView;
  478. if( self.labelParseTime == nil )
  479. {
  480. self.labelParseTime = [[UILabel alloc] init];
  481. self.labelParseTime.autoresizingMask = UIViewAutoresizingFlexibleWidth;
  482. self.labelParseTime.backgroundColor = [UIColor colorWithWhite:1 alpha:0.5];
  483. self.labelParseTime.textColor = [UIColor blackColor];
  484. self.labelParseTime.text = @"(parsing)";
  485. }
  486. self.labelParseTime.frame = CGRectMake(0, 0, self.view.bounds.size.width, 20);
  487. [self.contentView addSubview:self.labelParseTime];
  488. /** set the border for new item */
  489. self.contentView.showBorder = FALSE;
  490. /** Move the gesture recognizer onto the new one */
  491. if( self.tapGestureRecognizer == nil )
  492. {
  493. self.tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapGesture:)];
  494. }
  495. [self.contentView addGestureRecognizer:self.tapGestureRecognizer];
  496. [self.scrollViewForSVG addSubview:self.contentView];
  497. [self.scrollViewForSVG setContentSize: self.contentView.frame.size];
  498. float screenToDocumentSizeRatio = self.scrollViewForSVG.frame.size.width / self.contentView.frame.size.width;
  499. self.scrollViewForSVG.minimumZoomScale = MIN( 1, screenToDocumentSizeRatio );
  500. self.scrollViewForSVG.maximumZoomScale = MAX( 1, screenToDocumentSizeRatio );
  501. self.labelParseTime.text = [NSString stringWithFormat:@"(parsed: %.2f secs, rendered: %.2f secs)", [self.endParseTime timeIntervalSinceDate:self.startParseTime], self.contentView.timeIntervalForLastReRenderOfSVGFromMemory ];
  502. /**
  503. EXAMPLE:
  504. How to find particular nodes in the tree, after parsing.
  505. In this case, we search for all SVG <g> tags, which usually mean grouped-objects in Inkscape etc:
  506. NodeList* elementsUsingTagG = [document.DOMDocument getElementsByTagName:@"g"];
  507. NSLog( @"[%@] checking for SVG standard set of elements with XML tag/node of <g>: %@", [self class], elementsUsingTagG.internalArray );
  508. */
  509. }
  510. [self.viewActivityIndicator stopAnimating];
  511. self.subViewLoadingPopup.hidden = TRUE;
  512. }
  513. -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
  514. {
  515. if( [keyPath isEqualToString:kTimeIntervalForLastReRenderOfSVGFromMemory ] )
  516. {
  517. self.labelParseTime.text = [NSString stringWithFormat:@"(parsed: %.2f secs, rendered: %.2f secs)", [self.endParseTime timeIntervalSinceDate:self.startParseTime], self.contentView.timeIntervalForLastReRenderOfSVGFromMemory ];
  518. }
  519. }
  520. - (IBAction)toggleRender:(id)sender {
  521. if (self.requiresLayeredImageView) {
  522. self.requiresLayeredImageView = NO;
  523. } else {
  524. self.requiresLayeredImageView = YES;
  525. }
  526. [self setupNavigationBar];
  527. [self reload];
  528. }
  529. - (IBAction)animate:(id)sender {
  530. if ([self.sourceOfCurrentDocument.keyForAppleDictionaries rangeOfString:@"/Monkey.svg"].location != NSNotFound) {
  531. [self shakeHead];
  532. } else if ([self.sourceOfCurrentDocument.keyForAppleDictionaries rangeOfString:@"/parent-clip.svg"].location != NSNotFound) {
  533. [self moveGreenSquare];
  534. }
  535. }
  536. - (void)shakeHead {
  537. CALayer *layer = [self.contentView.image layerWithIdentifier:@"head"];
  538. CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
  539. animation.duration = 0.25f;
  540. animation.autoreverses = YES;
  541. animation.repeatCount = 100000;
  542. animation.fromValue = [NSNumber numberWithFloat:0.1f];
  543. animation.toValue = [NSNumber numberWithFloat:-0.1f];
  544. [layer addAnimation:animation forKey:@"shakingHead"];
  545. }
  546. - (void)moveGreenSquare {
  547. CALayer *layer = [self.contentView.image layerWithIdentifier:@"greensquare"];
  548. CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"position"];
  549. animation.duration = 1;
  550. animation.autoreverses = YES;
  551. animation.repeatCount = 100000;
  552. animation.fromValue = [NSValue valueWithCGPoint:CGPointMake(50, 50)];
  553. animation.toValue = [NSValue valueWithCGPoint:CGPointMake(-50, -50)];
  554. [layer addAnimation:animation forKey:@"moveGreenSquare"];
  555. }
  556. - (IBAction) showHideBorder:(id)sender
  557. {
  558. self.contentView.showBorder = ! self.contentView.showBorder;
  559. /**
  560. NB: normally, the following would NOT be needed - the SVGKImageView would automatically
  561. detect it needs to be re-drawn.
  562. But ... because we're doing zooming in this class, and Apple's zooming causes huge performance problems,
  563. we disabled the auto-redraw in the loadResource: method above.
  564. So, now, we have to manually tell the SVGKImageView to redraw
  565. */
  566. [self.contentView setNeedsDisplay];
  567. }
  568. #pragma mark - Split view
  569. - (void)splitViewController:(UISplitViewController *)splitController willHideViewController:(UIViewController *)viewController withBarButtonItem:(UIBarButtonItem *)barButtonItem forPopoverController:(UIPopoverController *)argumentPopoverController
  570. {
  571. barButtonItem.title = NSLocalizedString(@"Master", @"Master");
  572. [self.navigationItem setLeftBarButtonItem:barButtonItem animated:YES];
  573. self.popoverController = argumentPopoverController;
  574. }
  575. - (void)splitViewController:(UISplitViewController *)splitController willShowViewController:(UIViewController *)viewController invalidatingBarButtonItem:(UIBarButtonItem *)barButtonItem
  576. {
  577. // Called when the view is shown again in the split view, invalidating the button and popover controller.
  578. [self.navigationItem setLeftBarButtonItem:nil animated:YES];
  579. self.popoverController = nil;
  580. }
  581. - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
  582. return YES;
  583. }
  584. - (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
  585. {
  586. return self.contentView;
  587. }
  588. @end