From d96629ee4204b8bd184caa317feb9af468cec3e9 Mon Sep 17 00:00:00 2001 From: Daniel Barden Date: Tue, 20 Sep 2011 09:26:07 -0300 Subject: [PATCH] Added photos and location --- LocationSample.xcodeproj/project.pbxproj | 238 ++++++- LocationSample/DetailViewController.h | 10 +- LocationSample/DetailViewController.m | 46 +- LocationSample/MapView.h | 17 + LocationSample/MapView.m | 90 +++ LocationSample/MasterViewController.h | 5 +- LocationSample/MasterViewController.m | 45 +- LocationSample/PhotosController.h | 17 + LocationSample/PhotosController.m | 82 +++ .../en.lproj/DetailViewController.xib | 283 ++++++-- NIBlocks.h | 24 + NICommonMetrics.h | 138 ++++ NICommonMetrics.m | 77 +++ NIDataStructures.h | 486 ++++++++++++++ NIDataStructures.m | 491 ++++++++++++++ NIDebuggingTools.h | 186 ++++++ NIDebuggingTools.m | 61 ++ NIDeviceOrientation.h | 74 +++ NIDeviceOrientation.m | 74 +++ NIError.h | 56 ++ NIError.m | 21 + NIFoundationMethods.h | 135 ++++ NIFoundationMethods.m | 99 +++ NIInMemoryCache.h | 323 +++++++++ NIInMemoryCache.m | 500 ++++++++++++++ NINetworkActivity.h | 94 +++ NINetworkActivity.m | 145 ++++ NINonEmptyCollectionTesting.h | 53 ++ NINonEmptyCollectionTesting.m | 37 ++ NINonRetainingCollections.h | 60 ++ NINonRetainingCollections.m | 51 ++ NIOperations.h | 304 +++++++++ NIOperations.m | 241 +++++++ NIPaths.h | 50 ++ NIPaths.m | 39 ++ NIPhotoAlbumScrollView.h | 378 +++++++++++ NIPhotoAlbumScrollView.m | 627 ++++++++++++++++++ NIPhotoScrollView.h | 190 ++++++ NIPhotoScrollView.m | 505 ++++++++++++++ NIPhotoScrubberView.h | 187 ++++++ NIPhotoScrubberView.m | 506 ++++++++++++++ NIPreprocessorMacros.h | 70 ++ NIRuntimeClassModifications.h | 69 ++ NIRuntimeClassModifications.m | 37 ++ NISDKAvailability.h | 217 ++++++ NISDKAvailability.m | 87 +++ NIState.h | 88 +++ NIState.m | 67 ++ NIToolbarPhotoViewController.h | 192 ++++++ NIToolbarPhotoViewController.m | 573 ++++++++++++++++ NSData+NimbusCore.h | 28 + NSData+NimbusCore.m | 79 +++ NSString+NimbusCore.h | 50 ++ NSString+NimbusCore.m | 237 +++++++ NimbusCore+Additions.h | 34 + NimbusCore.h | 71 ++ NimbusPhotos.bundle/gfx/default.png | Bin 0 -> 4027 bytes NimbusPhotos.bundle/gfx/next.png | Bin 0 -> 241 bytes NimbusPhotos.bundle/gfx/next@2x.png | Bin 0 -> 3093 bytes NimbusPhotos.bundle/gfx/previous.png | Bin 0 -> 255 bytes NimbusPhotos.bundle/gfx/previous@2x.png | Bin 0 -> 3110 bytes NimbusPhotos.h | 134 ++++ Venue.h | 26 + Venue.m | 64 ++ 64 files changed, 9104 insertions(+), 64 deletions(-) create mode 100644 LocationSample/MapView.h create mode 100644 LocationSample/MapView.m create mode 100644 LocationSample/PhotosController.h create mode 100644 LocationSample/PhotosController.m create mode 100644 NIBlocks.h create mode 100644 NICommonMetrics.h create mode 100644 NICommonMetrics.m create mode 100644 NIDataStructures.h create mode 100644 NIDataStructures.m create mode 100644 NIDebuggingTools.h create mode 100644 NIDebuggingTools.m create mode 100644 NIDeviceOrientation.h create mode 100644 NIDeviceOrientation.m create mode 100644 NIError.h create mode 100644 NIError.m create mode 100644 NIFoundationMethods.h create mode 100644 NIFoundationMethods.m create mode 100644 NIInMemoryCache.h create mode 100644 NIInMemoryCache.m create mode 100644 NINetworkActivity.h create mode 100644 NINetworkActivity.m create mode 100644 NINonEmptyCollectionTesting.h create mode 100644 NINonEmptyCollectionTesting.m create mode 100644 NINonRetainingCollections.h create mode 100644 NINonRetainingCollections.m create mode 100644 NIOperations.h create mode 100644 NIOperations.m create mode 100644 NIPaths.h create mode 100644 NIPaths.m create mode 100644 NIPhotoAlbumScrollView.h create mode 100644 NIPhotoAlbumScrollView.m create mode 100644 NIPhotoScrollView.h create mode 100644 NIPhotoScrollView.m create mode 100644 NIPhotoScrubberView.h create mode 100644 NIPhotoScrubberView.m create mode 100644 NIPreprocessorMacros.h create mode 100644 NIRuntimeClassModifications.h create mode 100644 NIRuntimeClassModifications.m create mode 100644 NISDKAvailability.h create mode 100644 NISDKAvailability.m create mode 100644 NIState.h create mode 100644 NIState.m create mode 100644 NIToolbarPhotoViewController.h create mode 100644 NIToolbarPhotoViewController.m create mode 100644 NSData+NimbusCore.h create mode 100644 NSData+NimbusCore.m create mode 100644 NSString+NimbusCore.h create mode 100644 NSString+NimbusCore.m create mode 100644 NimbusCore+Additions.h create mode 100644 NimbusCore.h create mode 100644 NimbusPhotos.bundle/gfx/default.png create mode 100644 NimbusPhotos.bundle/gfx/next.png create mode 100644 NimbusPhotos.bundle/gfx/next@2x.png create mode 100644 NimbusPhotos.bundle/gfx/previous.png create mode 100644 NimbusPhotos.bundle/gfx/previous@2x.png create mode 100644 NimbusPhotos.h create mode 100644 Venue.h create mode 100644 Venue.m diff --git a/LocationSample.xcodeproj/project.pbxproj b/LocationSample.xcodeproj/project.pbxproj index a8c2e9f..2454b8f 100644 --- a/LocationSample.xcodeproj/project.pbxproj +++ b/LocationSample.xcodeproj/project.pbxproj @@ -16,24 +16,115 @@ D015DE601428829600235924 /* DetailViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE5F1428829600235924 /* DetailViewController.m */; }; D015DE631428829600235924 /* MasterViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D015DE611428829600235924 /* MasterViewController.xib */; }; D015DE661428829600235924 /* DetailViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D015DE641428829600235924 /* DetailViewController.xib */; }; + D015DE6F142882B900235924 /* Venue.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE6E142882B900235924 /* Venue.m */; }; + D015DE711428839C00235924 /* CoreLocation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D015DE701428839C00235924 /* CoreLocation.framework */; }; + D015DE741428995C00235924 /* MapView.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE731428995C00235924 /* MapView.m */; }; + D015DE76142899DA00235924 /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D015DE75142899DA00235924 /* MapKit.framework */; }; + D015DE7B1428A44D00235924 /* marinha1.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D015DE781428A44D00235924 /* marinha1.jpg */; }; + D015DE7C1428A44D00235924 /* marinha2.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D015DE791428A44D00235924 /* marinha2.jpg */; }; + D015DE7D1428A44D00235924 /* marinha3.jpg in Resources */ = {isa = PBXBuildFile; fileRef = D015DE7A1428A44D00235924 /* marinha3.jpg */; }; + D015DEA51428A47E00235924 /* NICommonMetrics.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE811428A47E00235924 /* NICommonMetrics.m */; }; + D015DEA61428A47E00235924 /* NIDataStructures.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE831428A47E00235924 /* NIDataStructures.m */; }; + D015DEA71428A47E00235924 /* NIDebuggingTools.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE851428A47E00235924 /* NIDebuggingTools.m */; }; + D015DEA81428A47E00235924 /* NIDeviceOrientation.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE871428A47E00235924 /* NIDeviceOrientation.m */; }; + D015DEA91428A47E00235924 /* NIError.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE891428A47E00235924 /* NIError.m */; }; + D015DEAA1428A47E00235924 /* NIFoundationMethods.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE8B1428A47E00235924 /* NIFoundationMethods.m */; }; + D015DEAB1428A47E00235924 /* NIInMemoryCache.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE8D1428A47E00235924 /* NIInMemoryCache.m */; }; + D015DEAC1428A47E00235924 /* NINetworkActivity.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE911428A47E00235924 /* NINetworkActivity.m */; }; + D015DEAD1428A47E00235924 /* NINonEmptyCollectionTesting.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE931428A47E00235924 /* NINonEmptyCollectionTesting.m */; }; + D015DEAE1428A47E00235924 /* NINonRetainingCollections.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE951428A47E00235924 /* NINonRetainingCollections.m */; }; + D015DEAF1428A47E00235924 /* NIOperations.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE971428A47E00235924 /* NIOperations.m */; }; + D015DEB01428A47E00235924 /* NIPaths.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE991428A47E00235924 /* NIPaths.m */; }; + D015DEB11428A47E00235924 /* NIRuntimeClassModifications.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE9C1428A47E00235924 /* NIRuntimeClassModifications.m */; }; + D015DEB21428A47E00235924 /* NISDKAvailability.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DE9E1428A47E00235924 /* NISDKAvailability.m */; }; + D015DEB31428A47E00235924 /* NIState.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DEA01428A47E00235924 /* NIState.m */; }; + D015DEB41428A47E00235924 /* NSData+NimbusCore.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DEA21428A47E00235924 /* NSData+NimbusCore.m */; }; + D015DEB51428A47E00235924 /* NSString+NimbusCore.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DEA41428A47E00235924 /* NSString+NimbusCore.m */; }; + D015DEC01428A4AC00235924 /* NIPhotoAlbumScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DEB91428A4AC00235924 /* NIPhotoAlbumScrollView.m */; }; + D015DEC11428A4AC00235924 /* NIPhotoScrollView.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DEBB1428A4AC00235924 /* NIPhotoScrollView.m */; }; + D015DEC21428A4AC00235924 /* NIPhotoScrubberView.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DEBD1428A4AC00235924 /* NIPhotoScrubberView.m */; }; + D015DEC31428A4AC00235924 /* NIToolbarPhotoViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DEBF1428A4AC00235924 /* NIToolbarPhotoViewController.m */; }; + D015DECB1428A73B00235924 /* PhotosController.m in Sources */ = {isa = PBXBuildFile; fileRef = D015DECA1428A73B00235924 /* PhotosController.m */; }; + D015DECD1428A9B600235924 /* CoreGraphics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D015DECC1428A9B500235924 /* CoreGraphics.framework */; }; + D015DECF1428AC8F00235924 /* NimbusPhotos.bundle in Resources */ = {isa = PBXBuildFile; fileRef = D015DECE1428AC8F00235924 /* NimbusPhotos.bundle */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ D015DE471428829600235924 /* LocationSample.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LocationSample.app; sourceTree = BUILT_PRODUCTS_DIR; }; D015DE4B1428829600235924 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; }; D015DE4D1428829600235924 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; - D015DE511428829600235924 /* LocationSample-Info.plist */ = {isa = PBXFileReference; path = "LocationSample-Info.plist"; sourceTree = ""; }; + D015DE511428829600235924 /* LocationSample-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "LocationSample-Info.plist"; sourceTree = ""; }; D015DE531428829600235924 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; D015DE551428829600235924 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; - D015DE571428829600235924 /* LocationSample-Prefix.pch */ = {isa = PBXFileReference; path = "LocationSample-Prefix.pch"; sourceTree = ""; }; - D015DE581428829600235924 /* AppDelegate.h */ = {isa = PBXFileReference; path = AppDelegate.h; sourceTree = ""; }; - D015DE591428829600235924 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; - D015DE5B1428829600235924 /* MasterViewController.h */ = {isa = PBXFileReference; path = MasterViewController.h; sourceTree = ""; }; - D015DE5C1428829600235924 /* MasterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MasterViewController.m; sourceTree = ""; }; - D015DE5E1428829600235924 /* DetailViewController.h */ = {isa = PBXFileReference; path = DetailViewController.h; sourceTree = ""; }; - D015DE5F1428829600235924 /* DetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetailViewController.m; sourceTree = ""; }; + D015DE571428829600235924 /* LocationSample-Prefix.pch */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "LocationSample-Prefix.pch"; sourceTree = ""; }; + D015DE581428829600235924 /* AppDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; usesTabs = 1; }; + D015DE591428829600235924 /* AppDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; usesTabs = 1; }; + D015DE5B1428829600235924 /* MasterViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MasterViewController.h; sourceTree = ""; usesTabs = 1; }; + D015DE5C1428829600235924 /* MasterViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MasterViewController.m; sourceTree = ""; usesTabs = 1; }; + D015DE5E1428829600235924 /* DetailViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = DetailViewController.h; sourceTree = ""; usesTabs = 1; }; + D015DE5F1428829600235924 /* DetailViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = DetailViewController.m; sourceTree = ""; usesTabs = 1; }; D015DE621428829600235924 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/MasterViewController.xib; sourceTree = ""; }; D015DE651428829600235924 /* en */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = en; path = en.lproj/DetailViewController.xib; sourceTree = ""; }; + D015DE6D142882B900235924 /* Venue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Venue.h; sourceTree = ""; usesTabs = 1; }; + D015DE6E142882B900235924 /* Venue.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Venue.m; sourceTree = ""; usesTabs = 1; }; + D015DE701428839C00235924 /* CoreLocation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreLocation.framework; path = System/Library/Frameworks/CoreLocation.framework; sourceTree = SDKROOT; }; + D015DE721428995C00235924 /* MapView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = MapView.h; sourceTree = ""; }; + D015DE731428995C00235924 /* MapView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = MapView.m; sourceTree = ""; }; + D015DE75142899DA00235924 /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; + D015DE781428A44D00235924 /* marinha1.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = marinha1.jpg; sourceTree = ""; }; + D015DE791428A44D00235924 /* marinha2.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = marinha2.jpg; sourceTree = ""; }; + D015DE7A1428A44D00235924 /* marinha3.jpg */ = {isa = PBXFileReference; lastKnownFileType = image.jpeg; path = marinha3.jpg; sourceTree = ""; }; + D015DE7F1428A47E00235924 /* NIBlocks.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIBlocks.h; sourceTree = ""; }; + D015DE801428A47E00235924 /* NICommonMetrics.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NICommonMetrics.h; sourceTree = ""; }; + D015DE811428A47E00235924 /* NICommonMetrics.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NICommonMetrics.m; sourceTree = ""; }; + D015DE821428A47E00235924 /* NIDataStructures.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIDataStructures.h; sourceTree = ""; }; + D015DE831428A47E00235924 /* NIDataStructures.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIDataStructures.m; sourceTree = ""; }; + D015DE841428A47E00235924 /* NIDebuggingTools.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIDebuggingTools.h; sourceTree = ""; }; + D015DE851428A47E00235924 /* NIDebuggingTools.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIDebuggingTools.m; sourceTree = ""; }; + D015DE861428A47E00235924 /* NIDeviceOrientation.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIDeviceOrientation.h; sourceTree = ""; }; + D015DE871428A47E00235924 /* NIDeviceOrientation.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIDeviceOrientation.m; sourceTree = ""; }; + D015DE881428A47E00235924 /* NIError.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIError.h; sourceTree = ""; }; + D015DE891428A47E00235924 /* NIError.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIError.m; sourceTree = ""; }; + D015DE8A1428A47E00235924 /* NIFoundationMethods.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIFoundationMethods.h; sourceTree = ""; }; + D015DE8B1428A47E00235924 /* NIFoundationMethods.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIFoundationMethods.m; sourceTree = ""; }; + D015DE8C1428A47E00235924 /* NIInMemoryCache.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIInMemoryCache.h; sourceTree = ""; }; + D015DE8D1428A47E00235924 /* NIInMemoryCache.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIInMemoryCache.m; sourceTree = ""; }; + D015DE8E1428A47E00235924 /* NimbusCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NimbusCore.h; sourceTree = ""; }; + D015DE8F1428A47E00235924 /* NimbusCore+Additions.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NimbusCore+Additions.h"; sourceTree = ""; }; + D015DE901428A47E00235924 /* NINetworkActivity.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NINetworkActivity.h; sourceTree = ""; }; + D015DE911428A47E00235924 /* NINetworkActivity.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NINetworkActivity.m; sourceTree = ""; }; + D015DE921428A47E00235924 /* NINonEmptyCollectionTesting.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NINonEmptyCollectionTesting.h; sourceTree = ""; }; + D015DE931428A47E00235924 /* NINonEmptyCollectionTesting.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NINonEmptyCollectionTesting.m; sourceTree = ""; }; + D015DE941428A47E00235924 /* NINonRetainingCollections.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NINonRetainingCollections.h; sourceTree = ""; }; + D015DE951428A47E00235924 /* NINonRetainingCollections.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NINonRetainingCollections.m; sourceTree = ""; }; + D015DE961428A47E00235924 /* NIOperations.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIOperations.h; sourceTree = ""; }; + D015DE971428A47E00235924 /* NIOperations.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIOperations.m; sourceTree = ""; }; + D015DE981428A47E00235924 /* NIPaths.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIPaths.h; sourceTree = ""; }; + D015DE991428A47E00235924 /* NIPaths.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIPaths.m; sourceTree = ""; }; + D015DE9A1428A47E00235924 /* NIPreprocessorMacros.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIPreprocessorMacros.h; sourceTree = ""; }; + D015DE9B1428A47E00235924 /* NIRuntimeClassModifications.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIRuntimeClassModifications.h; sourceTree = ""; }; + D015DE9C1428A47E00235924 /* NIRuntimeClassModifications.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIRuntimeClassModifications.m; sourceTree = ""; }; + D015DE9D1428A47E00235924 /* NISDKAvailability.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NISDKAvailability.h; sourceTree = ""; }; + D015DE9E1428A47E00235924 /* NISDKAvailability.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NISDKAvailability.m; sourceTree = ""; }; + D015DE9F1428A47E00235924 /* NIState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIState.h; sourceTree = ""; }; + D015DEA01428A47E00235924 /* NIState.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIState.m; sourceTree = ""; }; + D015DEA11428A47E00235924 /* NSData+NimbusCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+NimbusCore.h"; sourceTree = ""; }; + D015DEA21428A47E00235924 /* NSData+NimbusCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSData+NimbusCore.m"; sourceTree = ""; }; + D015DEA31428A47E00235924 /* NSString+NimbusCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSString+NimbusCore.h"; sourceTree = ""; }; + D015DEA41428A47E00235924 /* NSString+NimbusCore.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSString+NimbusCore.m"; sourceTree = ""; }; + D015DEB71428A4AC00235924 /* NimbusPhotos.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NimbusPhotos.h; sourceTree = ""; }; + D015DEB81428A4AC00235924 /* NIPhotoAlbumScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIPhotoAlbumScrollView.h; sourceTree = ""; }; + D015DEB91428A4AC00235924 /* NIPhotoAlbumScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIPhotoAlbumScrollView.m; sourceTree = ""; }; + D015DEBA1428A4AC00235924 /* NIPhotoScrollView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIPhotoScrollView.h; sourceTree = ""; }; + D015DEBB1428A4AC00235924 /* NIPhotoScrollView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIPhotoScrollView.m; sourceTree = ""; }; + D015DEBC1428A4AC00235924 /* NIPhotoScrubberView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIPhotoScrubberView.h; sourceTree = ""; }; + D015DEBD1428A4AC00235924 /* NIPhotoScrubberView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIPhotoScrubberView.m; sourceTree = ""; }; + D015DEBE1428A4AC00235924 /* NIToolbarPhotoViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NIToolbarPhotoViewController.h; sourceTree = ""; }; + D015DEBF1428A4AC00235924 /* NIToolbarPhotoViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NIToolbarPhotoViewController.m; sourceTree = ""; }; + D015DEC91428A73B00235924 /* PhotosController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = PhotosController.h; sourceTree = ""; }; + D015DECA1428A73B00235924 /* PhotosController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = PhotosController.m; sourceTree = ""; }; + D015DECC1428A9B500235924 /* CoreGraphics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreGraphics.framework; path = System/Library/Frameworks/CoreGraphics.framework; sourceTree = SDKROOT; }; + D015DECE1428AC8F00235924 /* NimbusPhotos.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = NimbusPhotos.bundle; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -41,6 +132,9 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + D015DECD1428A9B600235924 /* CoreGraphics.framework in Frameworks */, + D015DE76142899DA00235924 /* MapKit.framework in Frameworks */, + D015DE711428839C00235924 /* CoreLocation.framework in Frameworks */, D015DE4C1428829600235924 /* UIKit.framework in Frameworks */, D015DE4E1428829600235924 /* Foundation.framework in Frameworks */, ); @@ -52,6 +146,9 @@ D015DE3C1428829600235924 = { isa = PBXGroup; children = ( + D015DE7E1428A45B00235924 /* Nimbus */, + D015DE771428A44D00235924 /* images */, + D015DE6C142882A300235924 /* Models */, D015DE4F1428829600235924 /* LocationSample */, D015DE4A1428829600235924 /* Frameworks */, D015DE481428829600235924 /* Products */, @@ -69,6 +166,9 @@ D015DE4A1428829600235924 /* Frameworks */ = { isa = PBXGroup; children = ( + D015DECC1428A9B500235924 /* CoreGraphics.framework */, + D015DE75142899DA00235924 /* MapKit.framework */, + D015DE701428839C00235924 /* CoreLocation.framework */, D015DE4B1428829600235924 /* UIKit.framework */, D015DE4D1428829600235924 /* Foundation.framework */, ); @@ -87,6 +187,10 @@ D015DE611428829600235924 /* MasterViewController.xib */, D015DE641428829600235924 /* DetailViewController.xib */, D015DE501428829600235924 /* Supporting Files */, + D015DE721428995C00235924 /* MapView.h */, + D015DE731428995C00235924 /* MapView.m */, + D015DEC91428A73B00235924 /* PhotosController.h */, + D015DECA1428A73B00235924 /* PhotosController.m */, ); path = LocationSample; sourceTree = ""; @@ -102,6 +206,96 @@ name = "Supporting Files"; sourceTree = ""; }; + D015DE6C142882A300235924 /* Models */ = { + isa = PBXGroup; + children = ( + D015DE6D142882B900235924 /* Venue.h */, + D015DE6E142882B900235924 /* Venue.m */, + ); + name = Models; + sourceTree = ""; + }; + D015DE771428A44D00235924 /* images */ = { + isa = PBXGroup; + children = ( + D015DE781428A44D00235924 /* marinha1.jpg */, + D015DE791428A44D00235924 /* marinha2.jpg */, + D015DE7A1428A44D00235924 /* marinha3.jpg */, + ); + path = images; + sourceTree = ""; + }; + D015DE7E1428A45B00235924 /* Nimbus */ = { + isa = PBXGroup; + children = ( + D015DEC41428A4B300235924 /* Photos */, + D015DEB61428A48A00235924 /* Core */, + ); + name = Nimbus; + sourceTree = ""; + }; + D015DEB61428A48A00235924 /* Core */ = { + isa = PBXGroup; + children = ( + D015DE7F1428A47E00235924 /* NIBlocks.h */, + D015DE801428A47E00235924 /* NICommonMetrics.h */, + D015DE811428A47E00235924 /* NICommonMetrics.m */, + D015DE821428A47E00235924 /* NIDataStructures.h */, + D015DE831428A47E00235924 /* NIDataStructures.m */, + D015DE841428A47E00235924 /* NIDebuggingTools.h */, + D015DE851428A47E00235924 /* NIDebuggingTools.m */, + D015DE861428A47E00235924 /* NIDeviceOrientation.h */, + D015DE871428A47E00235924 /* NIDeviceOrientation.m */, + D015DE881428A47E00235924 /* NIError.h */, + D015DE891428A47E00235924 /* NIError.m */, + D015DE8A1428A47E00235924 /* NIFoundationMethods.h */, + D015DE8B1428A47E00235924 /* NIFoundationMethods.m */, + D015DE8C1428A47E00235924 /* NIInMemoryCache.h */, + D015DE8D1428A47E00235924 /* NIInMemoryCache.m */, + D015DE8E1428A47E00235924 /* NimbusCore.h */, + D015DE8F1428A47E00235924 /* NimbusCore+Additions.h */, + D015DE901428A47E00235924 /* NINetworkActivity.h */, + D015DE911428A47E00235924 /* NINetworkActivity.m */, + D015DE921428A47E00235924 /* NINonEmptyCollectionTesting.h */, + D015DE931428A47E00235924 /* NINonEmptyCollectionTesting.m */, + D015DE941428A47E00235924 /* NINonRetainingCollections.h */, + D015DE951428A47E00235924 /* NINonRetainingCollections.m */, + D015DE961428A47E00235924 /* NIOperations.h */, + D015DE971428A47E00235924 /* NIOperations.m */, + D015DE981428A47E00235924 /* NIPaths.h */, + D015DE991428A47E00235924 /* NIPaths.m */, + D015DE9A1428A47E00235924 /* NIPreprocessorMacros.h */, + D015DE9B1428A47E00235924 /* NIRuntimeClassModifications.h */, + D015DE9C1428A47E00235924 /* NIRuntimeClassModifications.m */, + D015DE9D1428A47E00235924 /* NISDKAvailability.h */, + D015DE9E1428A47E00235924 /* NISDKAvailability.m */, + D015DE9F1428A47E00235924 /* NIState.h */, + D015DEA01428A47E00235924 /* NIState.m */, + D015DEA11428A47E00235924 /* NSData+NimbusCore.h */, + D015DEA21428A47E00235924 /* NSData+NimbusCore.m */, + D015DEA31428A47E00235924 /* NSString+NimbusCore.h */, + D015DEA41428A47E00235924 /* NSString+NimbusCore.m */, + ); + name = Core; + sourceTree = ""; + }; + D015DEC41428A4B300235924 /* Photos */ = { + isa = PBXGroup; + children = ( + D015DECE1428AC8F00235924 /* NimbusPhotos.bundle */, + D015DEB71428A4AC00235924 /* NimbusPhotos.h */, + D015DEB81428A4AC00235924 /* NIPhotoAlbumScrollView.h */, + D015DEB91428A4AC00235924 /* NIPhotoAlbumScrollView.m */, + D015DEBA1428A4AC00235924 /* NIPhotoScrollView.h */, + D015DEBB1428A4AC00235924 /* NIPhotoScrollView.m */, + D015DEBC1428A4AC00235924 /* NIPhotoScrubberView.h */, + D015DEBD1428A4AC00235924 /* NIPhotoScrubberView.m */, + D015DEBE1428A4AC00235924 /* NIToolbarPhotoViewController.h */, + D015DEBF1428A4AC00235924 /* NIToolbarPhotoViewController.m */, + ); + name = Photos; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -156,6 +350,10 @@ D015DE541428829600235924 /* InfoPlist.strings in Resources */, D015DE631428829600235924 /* MasterViewController.xib in Resources */, D015DE661428829600235924 /* DetailViewController.xib in Resources */, + D015DE7B1428A44D00235924 /* marinha1.jpg in Resources */, + D015DE7C1428A44D00235924 /* marinha2.jpg in Resources */, + D015DE7D1428A44D00235924 /* marinha3.jpg in Resources */, + D015DECF1428AC8F00235924 /* NimbusPhotos.bundle in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -170,6 +368,30 @@ D015DE5A1428829600235924 /* AppDelegate.m in Sources */, D015DE5D1428829600235924 /* MasterViewController.m in Sources */, D015DE601428829600235924 /* DetailViewController.m in Sources */, + D015DE6F142882B900235924 /* Venue.m in Sources */, + D015DE741428995C00235924 /* MapView.m in Sources */, + D015DEA51428A47E00235924 /* NICommonMetrics.m in Sources */, + D015DEA61428A47E00235924 /* NIDataStructures.m in Sources */, + D015DEA71428A47E00235924 /* NIDebuggingTools.m in Sources */, + D015DEA81428A47E00235924 /* NIDeviceOrientation.m in Sources */, + D015DEA91428A47E00235924 /* NIError.m in Sources */, + D015DEAA1428A47E00235924 /* NIFoundationMethods.m in Sources */, + D015DEAB1428A47E00235924 /* NIInMemoryCache.m in Sources */, + D015DEAC1428A47E00235924 /* NINetworkActivity.m in Sources */, + D015DEAD1428A47E00235924 /* NINonEmptyCollectionTesting.m in Sources */, + D015DEAE1428A47E00235924 /* NINonRetainingCollections.m in Sources */, + D015DEAF1428A47E00235924 /* NIOperations.m in Sources */, + D015DEB01428A47E00235924 /* NIPaths.m in Sources */, + D015DEB11428A47E00235924 /* NIRuntimeClassModifications.m in Sources */, + D015DEB21428A47E00235924 /* NISDKAvailability.m in Sources */, + D015DEB31428A47E00235924 /* NIState.m in Sources */, + D015DEB41428A47E00235924 /* NSData+NimbusCore.m in Sources */, + D015DEB51428A47E00235924 /* NSString+NimbusCore.m in Sources */, + D015DEC01428A4AC00235924 /* NIPhotoAlbumScrollView.m in Sources */, + D015DEC11428A4AC00235924 /* NIPhotoScrollView.m in Sources */, + D015DEC21428A4AC00235924 /* NIPhotoScrubberView.m in Sources */, + D015DEC31428A4AC00235924 /* NIToolbarPhotoViewController.m in Sources */, + D015DECB1428A73B00235924 /* PhotosController.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/LocationSample/DetailViewController.h b/LocationSample/DetailViewController.h index 35afc92..8bcf8fd 100644 --- a/LocationSample/DetailViewController.h +++ b/LocationSample/DetailViewController.h @@ -7,11 +7,17 @@ // #import +#import "Venue.h" @interface DetailViewController : UIViewController -@property (strong, nonatomic) id detailItem; +@property (strong, nonatomic) Venue *venue; -@property (strong, nonatomic) IBOutlet UILabel *detailDescriptionLabel; +@property (strong, nonatomic) IBOutlet UILabel *name; +@property (nonatomic, retain) IBOutlet UITextView *description; + + +- (IBAction)openMap:(id)sender; +- (IBAction)openPhotos:(id)sender; @end diff --git a/LocationSample/DetailViewController.m b/LocationSample/DetailViewController.m index 53a997f..84804ec 100644 --- a/LocationSample/DetailViewController.m +++ b/LocationSample/DetailViewController.m @@ -7,6 +7,8 @@ // #import "DetailViewController.h" +#import "MapView.h" +#import "PhotosController.h" @interface DetailViewController () - (void)configureView; @@ -14,13 +16,14 @@ - (void)configureView; @implementation DetailViewController -@synthesize detailItem = _detailItem; -@synthesize detailDescriptionLabel = _detailDescriptionLabel; +@synthesize venue = _venue; +@synthesize name = _name; +@synthesize description = _description; - (void)dealloc { - [_detailItem release]; - [_detailDescriptionLabel release]; + [_name release]; + [_venue release]; [super dealloc]; } @@ -28,9 +31,9 @@ - (void)dealloc - (void)setDetailItem:(id)newDetailItem { - if (_detailItem != newDetailItem) { - [_detailItem release]; - _detailItem = [newDetailItem retain]; + if (_venue != newDetailItem) { + [_venue release]; + _venue = [newDetailItem retain]; // Update the view. [self configureView]; @@ -41,8 +44,9 @@ - (void)configureView { // Update the user interface for the detail item. - if (self.detailItem) { - self.detailDescriptionLabel.text = [self.detailItem description]; + if (self.venue) { + self.name.text = _venue.name; + self.description.text = _venue.description; } } @@ -70,12 +74,14 @@ - (void)viewDidUnload - (void)viewWillAppear:(BOOL)animated { + self.navigationController.navigationBar.barStyle = UIBarStyleDefault; + self.navigationController.navigationBar.translucent = NO; [super viewWillAppear:animated]; } - (void)viewDidAppear:(BOOL)animated { - [super viewDidAppear:animated]; + [super viewDidAppear:animated]; } - (void)viewWillDisappear:(BOOL)animated @@ -98,9 +104,25 @@ - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; if (self) { - self.title = NSLocalizedString(@"Detail", @"Detail"); + self.title = _venue.name; } return self; } - + +#pragma mark - IBActions +- (IBAction)openMap:(id)sender +{ + MapView *map = [[MapView alloc] init]; + map.venue = _venue; + [self.navigationController pushViewController:map animated:YES]; + [map release]; +} + +- (IBAction)openPhotos:(id)sender +{ + PhotosController *photos = [[PhotosController alloc] init]; + photos.venue = _venue; + [self.navigationController pushViewController:photos animated:YES]; + [photos release]; +} @end diff --git a/LocationSample/MapView.h b/LocationSample/MapView.h new file mode 100644 index 0000000..7510f65 --- /dev/null +++ b/LocationSample/MapView.h @@ -0,0 +1,17 @@ +// +// MapView.h +// LocationSample +// +// Created by Daniel Barden on 9/20/11. +// Copyright (c) 2011 None. All rights reserved. +// + +#import +#import +#import "Venue.h" + +@interface MapView : UIViewController + +@property (nonatomic, retain) MKMapView *mapView; +@property (nonatomic, retain) Venue *venue; +@end diff --git a/LocationSample/MapView.m b/LocationSample/MapView.m new file mode 100644 index 0000000..53ad663 --- /dev/null +++ b/LocationSample/MapView.m @@ -0,0 +1,90 @@ +// +// MapView.m +// LocationSample +// +// Created by Daniel Barden on 9/20/11. +// Copyright (c) 2011 None. All rights reserved. +// + +#import "MapView.h" + +@interface MyAnnotation : NSObject +@property (nonatomic) CLLocationCoordinate2D coordinate; +@property (nonatomic, copy) NSString *title; +@end + +@implementation MyAnnotation +@synthesize coordinate = _coordinate; +@synthesize title = _title; + +@end + +@implementation MapView +@synthesize mapView = _mapView; +@synthesize venue = _venue; + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Custom initialization + } + return self; +} + +- (void)didReceiveMemoryWarning +{ + // Releases the view if it doesn't have a superview. + [super didReceiveMemoryWarning]; + + // Release any cached data, images, etc that aren't in use. +} + +#pragma mark - View lifecycle + +/* +// Implement loadView to create a view hierarchy programmatically, without using a nib. +- (void)loadView +{ +} +*/ + + +// Implement viewDidLoad to do additional setup after loading the view, typically from a nib. +- (void)viewDidLoad +{ + self.title = _venue.name; + + _mapView = [[MKMapView alloc] initWithFrame:self.view.frame]; + MKCoordinateRegion region; + region.center.latitude = _venue.location.coordinate.latitude; + region.center.longitude = _venue.location.coordinate.longitude; + region.span.latitudeDelta = 0.1; + region.span.longitudeDelta = 0.1; + + MyAnnotation *annotation = [[MyAnnotation alloc] init]; + annotation.coordinate = _venue.location.coordinate; + annotation.title = _venue.name; + + [_mapView setRegion:region]; + [_mapView addAnnotation:annotation]; + [self.view addSubview:_mapView]; + + [super viewDidLoad]; +} + + +- (void)viewDidUnload +{ + [super viewDidUnload]; + // Release any retained subviews of the main view. + // e.g. self.myOutlet = nil; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return (interfaceOrientation == UIInterfaceOrientationPortrait); +} + +@end diff --git a/LocationSample/MasterViewController.h b/LocationSample/MasterViewController.h index bfb85aa..b37711d 100644 --- a/LocationSample/MasterViewController.h +++ b/LocationSample/MasterViewController.h @@ -7,11 +7,14 @@ // #import +#import @class DetailViewController; -@interface MasterViewController : UITableViewController +@interface MasterViewController : UITableViewController @property (strong, nonatomic) DetailViewController *detailViewController; +@property (nonatomic, retain) NSArray *venues; +@property (nonatomic, retain) CLLocationManager *clManager; @end diff --git a/LocationSample/MasterViewController.m b/LocationSample/MasterViewController.m index ebc8168..2621909 100644 --- a/LocationSample/MasterViewController.m +++ b/LocationSample/MasterViewController.m @@ -7,12 +7,16 @@ // #import "MasterViewController.h" +#import #import "DetailViewController.h" +#import "Venue.h" @implementation MasterViewController @synthesize detailViewController = _detailViewController; +@synthesize venues = _venues; +@synthesize clManager = _clManager; - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { @@ -40,7 +44,21 @@ - (void)didReceiveMemoryWarning - (void)viewDidLoad { [super viewDidLoad]; - // Do any additional setup after loading the view, typically from a nib. + + UIBarButtonItem *barbutton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(getLocation)]; + self.navigationItem.rightBarButtonItem = barbutton; + + _clManager = [[CLLocationManager alloc] init]; + _clManager.delegate = self; + + Venue *venue = [[Venue alloc] initWithName:@"Marinha" withLatitude:-30.05 withLongitude:-51.20]; + venue.description = @"Este parque esta jogado as tracasasdajsdlkajsdlja asdasasad asdaksjdasd asdlkasjdasd asdlkasdas dasdk asldk asdasldkasdklsad asdkalf asklf dfsdlkfsdlfksd fsdklf sdfklsdfksdlfksdlfsdkfsdlkf sdlfksdlfksdklf sdlfkdsf dsflksdkf dsflsdkf sdlkflsdkflsdkflsdkf sdfklsdflksdflksdlfksdfsdlkf sklfsdfk sdlfsdklfsdkflskd fslkdf sdlkf sldkfklsdfdslfksdlfksdflksdf lskdf sldkflsdkfsdklfksdlfsdlkfsdlk fdslkfsldkflsdkflsdkfsdklf dlskf lsdkfsdlfskdflsdlfksdfsdlkf sdklfdslkfklsd flksd flksdkflsdfkldskfdslfkdslfkdsfldskfsdlfksdlfksdlf sdflk dsf"; + NSArray *photos = [[NSArray alloc] initWithObjects:@"marinha1.jpg", @"marinha2.jpg", @"marinha3.jpg", nil]; + venue.photos = photos; + [photos release]; + + NSArray *tmpVenues = [[NSArray alloc] initWithObjects:venue, nil]; + self.venues = tmpVenues; } - (void)viewDidUnload @@ -94,12 +112,14 @@ - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(N UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { - cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease]; + cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease]; cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; } // Configure the cell. - cell.textLabel.text = NSLocalizedString(@"Detail", @"Detail"); + Venue *venue = [self.venues objectAtIndex:indexPath.row]; + cell.textLabel.text = venue.name; + cell.detailTextLabel.text = venue.distance; return cell; } @@ -146,7 +166,26 @@ - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath if (!self.detailViewController) { self.detailViewController = [[[DetailViewController alloc] initWithNibName:@"DetailViewController" bundle:nil] autorelease]; } + + self.detailViewController.venue = [_venues objectAtIndex:indexPath.row]; [self.navigationController pushViewController:self.detailViewController animated:YES]; } +#pragma mark - Location Methods +- (void)getLocation +{ + if (! [CLLocationManager locationServicesEnabled]) + return; + + [_clManager startUpdatingLocation]; +} + +- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation +{ + for (Venue *venue in _venues) { + [venue updateDistance:newLocation]; + } + [_clManager stopUpdatingLocation]; + [self.tableView reloadData]; +} @end diff --git a/LocationSample/PhotosController.h b/LocationSample/PhotosController.h new file mode 100644 index 0000000..31e4611 --- /dev/null +++ b/LocationSample/PhotosController.h @@ -0,0 +1,17 @@ +// +// PhotosController.h +// LocationSample +// +// Created by Daniel Barden on 9/20/11. +// Copyright (c) 2011 None. All rights reserved. +// + +#import +#import "NimbusPhotos.h" +#import "Venue.h" + +@interface PhotosController : NIToolbarPhotoViewController + +@property (nonatomic, retain) Venue *venue; +@property (nonatomic, retain) NIPhotoAlbumScrollView *photoview; +@end diff --git a/LocationSample/PhotosController.m b/LocationSample/PhotosController.m new file mode 100644 index 0000000..6c82582 --- /dev/null +++ b/LocationSample/PhotosController.m @@ -0,0 +1,82 @@ +// +// PhotosController.m +// LocationSample +// +// Created by Daniel Barden on 9/20/11. +// Copyright (c) 2011 None. All rights reserved. +// + +#import "PhotosController.h" + +@implementation PhotosController + +@synthesize venue = _venue; +@synthesize photoview = _photoview; + +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil +{ + self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]; + if (self) { + // Custom initialization + } + return self; +} + +- (void)didReceiveMemoryWarning +{ + // Releases the view if it doesn't have a superview. + [super didReceiveMemoryWarning]; + + // Release any cached data, images, etc that aren't in use. +} + +#pragma mark - View lifecycle + +// Implement loadView to create a view hierarchy programmatically, without using a nib. +- (void)loadView +{ + [super loadView]; + self.photoAlbumView.dataSource = self; + self.photoScrubberView.dataSource = self; +// self.hidesChromeWhenScrolling = NO; +// self.showPhotoAlbumBeneathToolbar = NO; + self.photoAlbumView.zoomingIsEnabled = NO; + [self.photoAlbumView reloadData]; + +} + +// Implement viewDidLoad to do additional setup after loading the view, typically from a nib. +- (void)viewDidLoad +{ + [super viewDidLoad]; +} + + +- (void)viewDidUnload +{ + [super viewDidUnload]; + // Release any retained subviews of the main view. + // e.g. self.myOutlet = nil; +} + +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation +{ + // Return YES for supported orientations + return (interfaceOrientation == UIInterfaceOrientationPortrait); +} + +#pragma mark - Photo View Controller Data Source + +- (UIImage *)photoAlbumScrollView:(NIPhotoAlbumScrollView *)photoAlbumScrollView photoAtIndex:(NSInteger)photoIndex photoSize:(NIPhotoScrollViewPhotoSize *)photoSize isLoading:(BOOL *)isLoading originalPhotoDimensions:(CGSize *)originalPhotoDimensions { + NSString *imageName = [_venue.photos objectAtIndex:photoIndex]; + NSLog(@"Fetching image %@", imageName); + UIImage *image = [[UIImage imageNamed:imageName] retain]; + *photoSize = NIPhotoScrollViewPhotoSizeOriginal; + return image; +} + +- (NSInteger)numberOfPhotosInPhotoScrollView:(NIPhotoAlbumScrollView *)photoScrollView { + NSLog(@"Number of photos %d", [_venue.photos count]); + return [_venue.photos count]; +} +@end diff --git a/LocationSample/en.lproj/DetailViewController.xib b/LocationSample/en.lproj/DetailViewController.xib index 516cc90..c0db4f4 100644 --- a/LocationSample/en.lproj/DetailViewController.xib +++ b/LocationSample/en.lproj/DetailViewController.xib @@ -2,30 +2,29 @@ 1280 - 10J869 - 1843 - 1038.35 - 461.00 + 11B26 + 1923 + 1138 + 566.00 com.apple.InterfaceBuilder.IBCocoaTouchPlugin - 843 + 919 YES - IBProxyObject + IBUITextView + IBUIButton IBUIView IBUILabel + IBProxyObject YES com.apple.InterfaceBuilder.IBCocoaTouchPlugin - YES - - YES - - + PluginDependencyRecalculationVersion + YES @@ -42,20 +41,113 @@ 274 YES - + - 298 - {{20, 221}, {280, 18}} + 292 + {{20, 247}, {128, 37}} - + + _NS:209 + NO + IBCocoaTouchFramework + 0 + 0 + 1 + View in Maps + 3 MQA + + 1 + MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA + + + 3 + MC41AA + + + 2 + 15 + + + Helvetica-Bold + 15 + 16 + + + + + 292 + {{186, 247}, {114, 37}} + + + + _NS:209 + NO + IBCocoaTouchFramework + 0 + 0 + 1 + View Photos + + + 1 + MC4xOTYwNzg0MzQ2IDAuMzA5ODAzOTMyOSAwLjUyMTU2ODY1NgA + + + + + + + + 292 + {{20, 111}, {280, 128}} + + + + _NS:623 + + 1 + MSAxIDEAA + YES + YES + + + + IBCocoaTouchFramework + NO + Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda. + + 2 + IBCocoaTouchFramework + + + 1 + 14 + + + Helvetica + 14 + 16 + + + + + 292 + {{20, 20}, {280, 21}} + + + + _NS:312 + NO + YES + 7 NO IBCocoaTouchFramework - Detail view content goes here + Label 1 MCAwIDAAA @@ -63,14 +155,13 @@ 1 10 - 1 1 - 4 + 17 - Helvetica-Light - 14 + Helvetica + 17 16 @@ -78,6 +169,7 @@ {{0, 20}, {320, 460}} + 3 MQA @@ -102,11 +194,45 @@ - detailDescriptionLabel + name + + + + 11 + + + + description - + + + 12 + + + + openMap: + + + 7 - 6 + 14 + + + + openPhotos: + + + 7 + + 15 + + + + delegate + + + + 13 @@ -114,7 +240,9 @@ YES 0 - + + YES + @@ -123,7 +251,10 @@ YES - + + + + @@ -139,8 +270,23 @@ - 4 - + 7 + + + + + 8 + + + + + 9 + + + + + 10 + @@ -150,16 +296,24 @@ YES -1.CustomClassName + -1.IBPluginDependency -2.CustomClassName - 1.IBEditorWindowLastContentRect + -2.IBPluginDependency 1.IBPluginDependency - 4.IBPluginDependency + 10.IBPluginDependency + 7.IBPluginDependency + 8.IBPluginDependency + 9.IBPluginDependency YES DetailViewController + com.apple.InterfaceBuilder.IBCocoaTouchPlugin UIResponder - {{354, 412}, {320, 480}} + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin + com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin com.apple.InterfaceBuilder.IBCocoaTouchPlugin @@ -176,7 +330,7 @@ - 6 + 15 @@ -184,15 +338,68 @@ DetailViewController UIViewController + + YES + + YES + openMap: + openPhotos: + + + YES + id + id + + + + YES + + YES + openMap: + openPhotos: + + + YES + + openMap: + id + + + openPhotos: + id + + + - detailDescriptionLabel - UILabel + YES + + YES + description + name + + + YES + UITextView + UILabel + - detailDescriptionLabel - - detailDescriptionLabel - UILabel + YES + + YES + description + name + + + YES + + description + UITextView + + + name + UILabel + @@ -210,6 +417,6 @@ YES 3 - 843 + 919 diff --git a/NIBlocks.h b/NIBlocks.h new file mode 100644 index 0000000..aaafc55 --- /dev/null +++ b/NIBlocks.h @@ -0,0 +1,24 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import + +#if NS_BLOCKS_AVAILABLE + +typedef void (^NIBasicBlock)(void); +typedef void (^NIErrorBlock)(NSError* error); + +#endif // #if NS_BLOCKS_AVAILABLE diff --git a/NICommonMetrics.h b/NICommonMetrics.h new file mode 100644 index 0000000..227dbf5 --- /dev/null +++ b/NICommonMetrics.h @@ -0,0 +1,138 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import +#import + +/** + * For common system metrics. + * + * If you ever need to work with system metrics in any way it can be a pain in the ass to try + * to figure out what the exact metrics are. Figuring out how long it takes the status bar + * to animate is not something you should be spending your time on. The metrics in this file + * are provided as a means of unifying a number of system metrics for use in your applications. + * + * + *

What Qualifies as a Common Metric

+ * + * Common metrics are related to system components, such as the dimensions of a toolbar in + * a particular orientation or the duration of a standard animation. This is + * not the place to put feature-specific metrics, such as the height of a photo scrubber + * view. + * + * + *

Examples

+ * + *

Positioning a Toolbar

+ * + * The following example updates the position and height of a toolbar when the device + * orientation is changing. This ensures that, in landscape mode on the iPhone, the toolbar + * is slightly shorter to accomodate the smaller height of the screen. + * + * @code + * - (void)willAnimateRotationToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation + * duration: (NSTimeInterval)duration { + * [super willAnimateRotationToInterfaceOrientation: toInterfaceOrientation + * duration: duration]; + * + * CGRect toolbarFrame = self.toolbar.frame; + * toolbarFrame.size.height = NIToolbarHeightForOrientation(toInterfaceOrientation); + * toolbarFrame.origin.y = self.view.bounds.size.height - toolbarFrame.size.height; + * self.toolbar.frame = toolbarFrame; + * } + * @endcode + * + * @ingroup NimbusCore + * @defgroup Common-Metrics Common Metrics + * @{ + */ + +/** + * Fetch the height of a toolbar in a given orientation. + * + * On the iPhone: + * - Portrait: 44 + * - Landscape: 33 + * + * On the iPad: always 44 + */ +CGFloat NIToolbarHeightForOrientation(UIInterfaceOrientation orientation); + +/** + * The animation curve used when changing the status bar's visibility. + * + * This is the curve of the animation used by + * -[[UIApplication sharedApplication] setStatusBarHidden:withAnimation:]. + * + * Value: UIViewAnimationCurveEaseIn + */ +UIViewAnimationCurve NIStatusBarAnimationCurve(void); + +/** + * The animation duration used when changing the status bar's visibility. + * + * This is the duration of the animation used by + * -[[UIApplication sharedApplication] setStatusBarHidden:withAnimation:]. + * + * Value: 0.3 seconds + */ +NSTimeInterval NIStatusBarAnimationDuration(void); + +/** + * The animation curve used when the status bar's bounds change (when a call is received, + * for example). + * + * Value: UIViewAnimationCurveEaseInOut + */ +UIViewAnimationCurve NIStatusBarBoundsChangeAnimationCurve(void); + +/** + * The animation duration used when the status bar's bounds change (when a call is received, + * for example). + * + * Value: 0.35 seconds + */ +NSTimeInterval NIStatusBarBoundsChangeAnimationDuration(void); + +/** + * Get the status bar's current height. + * + * If the status bar is hidden this will return 0. + * + * This is generally 20 when the status bar is its normal height. + */ +CGFloat NIStatusBarHeight(void); + +/** + * The animation duration when the device is rotating to a new orientation. + * + * Value: 0.4 seconds if the device is being rotated 90 degrees. + * 0.8 seconds if the device is being rotated 180 degrees. + * + * @param isFlippingUpsideDown YES if the device is being flipped upside down. + */ +NSTimeInterval NIDeviceRotationDuration(BOOL isFlippingUpsideDown); + +/** + * The padding around a standard cell in a table view. + * + * Value: 10 pixels on all sides. + */ +UIEdgeInsets NICellContentPadding(void); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Common Metrics /////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NICommonMetrics.m b/NICommonMetrics.m new file mode 100644 index 0000000..1331115 --- /dev/null +++ b/NICommonMetrics.m @@ -0,0 +1,77 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NICommonMetrics.h" + +#import "NISDKAvailability.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +CGFloat NIToolbarHeightForOrientation(UIInterfaceOrientation orientation) { + return (NIIsPad() + ? 44 + : (UIInterfaceOrientationIsPortrait(orientation) + ? 44 + : 33));; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +UIViewAnimationCurve NIStatusBarAnimationCurve(void) { + return UIViewAnimationCurveEaseIn; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSTimeInterval NIStatusBarAnimationDuration(void) { + return 0.3; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +UIViewAnimationCurve NIStatusBarBoundsChangeAnimationCurve(void) { + return UIViewAnimationCurveEaseInOut; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSTimeInterval NIStatusBarBoundsChangeAnimationDuration(void) { + return 0.35; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +CGFloat NIStatusBarHeight(void) { + CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; + + // We take advantage of the fact that the status bar will always be wider than it is tall + // in order to avoid having to check the status bar orientation. + CGFloat statusBarHeight = MIN(statusBarFrame.size.width, statusBarFrame.size.height); + + return statusBarHeight; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSTimeInterval NIDeviceRotationDuration(BOOL isFlippingUpsideDown) { + return isFlippingUpsideDown ? 0.8 : 0.4; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +UIEdgeInsets NICellContentPadding(void) { + return UIEdgeInsetsMake(10, 10, 10, 10); +} diff --git a/NIDataStructures.h b/NIDataStructures.h new file mode 100644 index 0000000..33f0827 --- /dev/null +++ b/NIDataStructures.h @@ -0,0 +1,486 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import + +/** + * For classic computer science data structures. + * + * iOS provides most of the important data structures such as arrays, dictionaries, and sets. + * However, it is missing some lesser-used data structures such as linked lists. Nimbus makes + * use of the linked list data structure to provide an efficient, least-recently-used cache + * removal policy for its in-memory cache, NIMemoryCache. + * + * + *

Comparison of Data Structures

+ * + *
+ *  Requirement           | NILinkedList | NSArray | NSSet | NSDictionary
+ *  =====================================================================
+ *  Instant arbitrary     |     YES      |   NO    |  YES  |     YES
+ *  insertion/deletion    |     [1]      |         |       |
+ *  ---------------------------------------------------------------------
+ *  Consistent object     |     YES      |   YES   |  NO   |     NO
+ *  ordering              |              |         |       |
+ *  ---------------------------------------------------------------------
+ *  Checking for object   |     NO       |   NO    |  YES  |     NO
+ *  existence quickly     |              |         |       |
+ *  ---------------------------------------------------------------------
+ *  Instant object access |     YES      |   NO    |  YES  |     YES
+ *                        |     [1]      |         |       |     [2]
+ *  ---------------------------------------------------------------------
+ * + * - [1] Note that being able to instantly remove and access objects in an NILinkedList + * requires additional overhead of maintaining NILinkedListLocation objects in your + * code. If this is your only requirement, then it's likely simpler to use an NSSet. + * A linked list is worth using if you also need consistent ordering, seeing + * as neither NSSet nor NSDictionary provide this. + * - [2] This assumes you are accessing the object with its key. + * + * + *

Why NILinkedList was Built

+ * + * NILinkedList was built to solve a specific need in Nimbus' in-memory caches of having + * a collection that guaranteed ordering, constant-time appending, and constant + * time removal of arbitrary objects. + * NSArray does not guarantee constant-time removal of objects, NSSet does not enforce ordering + * (though the new NSOrderedSet introduced in iOS 5 does), and NSDictionary also does not + * enforce ordering. + * + * + * @ingroup NimbusCore + * @defgroup Data-Structures Data Structures + * @{ + */ + +// This is not to be used externally. +struct NILinkedListNode { + id object; + struct NILinkedListNode* prev; + struct NILinkedListNode* next; +}; + +// A thin veil over NILinkedListNode pointers. This is the "public" interface to an object's +// location. Internally, this is cast to an NILinkedListNode*. +typedef void NILinkedListLocation; + +/** + * A singly linked list implementation. + * + * This data structure provides constant time insertion and deletion of objects + * in a collection. + * + * A linked list is different from an NSMutableArray solely in the runtime of adding and + * removing objects. It is always possible to remove objects from both the beginning and end of + * a linked list in constant time, contrasted with an NSMutableArray where removing an object + * from the beginning of the list could result in O(N) linear time, where + * N is the number of objects in the collection when the action is performed. + * If an object's location is known, it is possible to get O(1) constant time removal + * with a linked list, where an NSMutableArray would get at best O(N) linear time. + * + * This collection implements NSFastEnumeration which allows you to use foreach-style + * iteration on the linked list. If you would like more control over the iteration of the + * linked list you can use + * -[NILinkedList @link NILinkedList::objectEnumerator objectEnumerator@endlink] + * + * + *

When You Should Use a Linked List

+ * + * Linked lists should be used when you need guaranteed constant-time performance characteristics + * for adding arbitrary objects to and removing arbitrary objects from a collection that + * also enforces consistent ordering. + * + * Linked lists are used in NIMemoryCache to implement + * efficient least-recently used collections for in-memory caches. It is important + * that these caches use a collection with guaranteed constant-time properties because + * in-memory caches must operate as fast as possible in order to avoid locking up the UI. + * Specifically, in-memory caches could potentially have thousands of objects. Every time + * we access one of these objects we move its lru placement to the end of the lru list. If + * we were to use an NSArray for this data structure we could easily run into an + * O(N^2) exponential-time operation which is absolutely unacceptable. + */ +@interface NILinkedList : NSObject { +@private + struct NILinkedListNode* _head; + struct NILinkedListNode* _tail; + NSUInteger _count; + + // Used internally to track modifications to the linked list. + unsigned long _modificationNumber; +} + +- (NSUInteger)count; + +- (id)firstObject; +- (id)lastObject; + +#pragma mark Linked List Creation + ++ (NILinkedList *)linkedList; ++ (NILinkedList *)linkedListWithArray:(NSArray *)array; + +- (id)initWithArray:(NSArray *)anArray; + +#pragma mark Extended Methods + +- (NSArray *)allObjects; +- (NSEnumerator *)objectEnumerator; + +- (BOOL)containsObject:(id)anObject; + +- (NSString *)description; + +#pragma mark Methods for constant-time access. + +- (NILinkedListLocation *)locationOfObject:(id)object; +- (id)objectAtLocation:(NILinkedListLocation *)location; +- (void)removeObjectAtLocation:(NILinkedListLocation *)location; + +#pragma mark Mutable Operations +// TODO (jverkoey August 3, 2011): Consider creating an NIMutableLinkedList implementation. + +- (NILinkedListLocation *)addObject:(id)object; + +- (void)removeAllObjects; +- (void)removeObject:(id)object; +- (void)removeFirstObject; +- (void)removeLastObject; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Data Structures ////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + * @class NILinkedList + * + *

Modifying a linked list

+ * + * To add an object to a linked list, you may use @link NILinkedList::addObject: addObject:@endlink. + * + * @code + * [ll addObject:object]; + * @endcode + * + * To remove some arbitrary object in linear time (meaning we must perform a scan of the list), use + * @link NILinkedList::removeObject: removeObject:@endlink + * + * @code + * [ll removeObject:object]; + * @endcode + * + * Note that using a linked list in this way is way is identical to using an + * NSMutableArray in performance time. + * + * + *

Accessing a Linked List

+ * + * You can access the first and last objects with constant time by using + * @link NILinkedList::firstObject firstObject@endlink and + * @link NILinkedList::lastObject lastObject@endlink. + * + * @code + * id firstObject = ll.firstObject; + * id lastObject = ll.lastObject; + * @endcode + * + * + *

Traversing a Linked List

+ * + * NILinkedList implements the NSFastEnumeration protocol, allowing you to use foreach-style + * iteration over the objects of a linked list. Note that you cannot modify a linked list + * during fast iteration and doing so will fire an assertion. + * + * @code + * for (id object in ll) { + * // perform operations on the object + * } + * @endcode + * + * If you need to modify the linked list while traversing it, an acceptable algorithm is to + * successively remove either the head or tail object, depending on the order in which you wish + * to traverse the linked list. + * + *

Traversing Forward by Removing Objects from the List

+ * + * @code + * while (nil != ll.firstObject) { + * id object = [ll firstObject]; + * + * // Remove the head of the linked list in constant time. + * [ll removeFirstObject]; + * } + * @endcode + * + *

Traversing Backward by Removing Objects from the List

+ * + * @code + * while (nil != ll.lastObject) { + * id object = [ll lastObject]; + * + * // Remove the tail of the linked list in constant time. + * [ll removeLastObject]; + * } + * @endcode + * + * + *

Examples

+ * + * + *

Building a first-in-first-out list of operations

+ * + * @code + * NILinkedList* ll = [NILinkedList linkedList]; + * + * // Add the operations to the linked list like you would an array. + * [ll addObject:operation1]; + * + * // Each addObject call appends the object to the end of the linked list. + * [ll addObject:operation2]; + * + * while (nil != ll.firstObject) { + * NSOperation* op1 = [ll firstObject]; + * // Process the operation... + * + * // Remove the head of the linked list in constant time. + * [ll removeFirstObject]; + * } + * @endcode + * + * + *

Removing an item from the middle of the list

+ * + * @code + * NILinkedList* ll = [NILinkedList linkedList]; + * + * [ll addObject:obj1]; + * [ll addObject:obj2]; + * [ll addObject:obj3]; + * [ll addObject:obj4]; + * + * // Find an object in the linked list in linear time. + * NILinkedListLocation* location = [ll locationOfObject:obj3]; + * + * // Remove the object in constant time. + * [ll removeObjectAtLocation:location]; + * + * // Location is no longer valid at this point. + * location = nil; + * + * // Remove an object in linear time. This is simply a more concise version of the above. + * [ll removeObject:obj4]; + * + * // We would use the NILinkedListLocation to remove the object if we were storing the location + * // in an external data structure and wanted constant time removal, for example. See + * // NIMemoryCache for an example of this in action. + * @endcode + * + * @sa NIMemoryCache + * + * + *

Using the location object for constant time operations

+ * + * @code + * NILinkedList* ll = [NILinkedList linkedList]; + * + * [ll addObject:obj1]; + * NILinkedListLocation* location = [ll addObject:obj2]; + * [ll addObject:obj3]; + * [ll addObject:obj4]; + * + * // Remove the second object in constant time. + * [ll removeObjectAtLocation:location]; + * + * // Location is no longer valid at this point. + * location = nil; + * @endcode + */ + + +/** @name Creating a Linked List */ + +/** + * Convenience method for creating an autoreleased linked list. + * + * Identical to [[[NILinkedList alloc] init] autorelease]; + * + * @fn NILinkedList::linkedList + */ + +/** + * Convenience method for creating an autoreleased linked list with an array. + * + * Identical to [[[NILinkedList alloc] initWithArray:array] autorelease]; + * + * @fn NILinkedList::linkedListWithArray: + */ + +/** + * Initializes a newly allocated linked list by placing in it the objects contained + * in a given array. + * + * @fn NILinkedList::initWithArray: + * @param anArray An array. + * @returns A linked list initialized to contain the objects in anArray. + */ + + +/** @name Querying a Linked List */ + +/** + * Returns the number of objects currently in the linked list. + * + * @fn NILinkedList::count + * @returns The number of objects currently in the linked list. + */ + +/** + * Returns the first object in the linked list. + * + * @fn NILinkedList::firstObject + * @returns The first object in the linked list. If the linked list is empty, returns nil. + */ + +/** + * Returns the last object in the linked list. + * + * @fn NILinkedList::lastObject + * @returns The last object in the linked list. If the linked list is empty, returns nil. + */ + +/** + * Returns an array containing the linked list's objects, or an empty array if the linked list + * has no objects. + * + * @fn NILinkedList::allObjects + * @returns An array containing the linked list's objects, or an empty array if the linked + * list has no objects. The objects will be in the same order as the linked list. + */ + +/** + * Returns an enumerator object that lets you access each object in the linked list. + * + * @fn NILinkedList::objectEnumerator + * @returns An enumerator object that lets you access each object in the linked list, in + * order, from the first object to the last. + * @attention When you use this method you must not modify the linked list during enumeration. + */ + +/** + * Returns a Boolean value that indicates whether a given object is present in the linked list. + * + * Run-time: O(count) linear + * + * @fn NILinkedList::containsObject: + * @returns YES if anObject is present in the linked list, otherwise NO. + */ + +/** + * Returns a string that represents the contents of the linked list, formatted as a property list. + * + * @fn NILinkedList::description + * @returns A string that represents the contents of the linked list, + * formatted as a property list. + */ + + +/** @name Adding Objects */ + +/** + * Append an object to the linked list. + * + * Run-time: O(1) constant + * + * @fn NILinkedList::addObject: + * @returns A location within the linked list. + */ + + +/** @name Removing Objects */ + +/** + * Remove all objects from the linked list. + * + * Run-time: Theta(count) linear + * + * @fn NILinkedList::removeAllObjects + */ + +/** + * Remove an object from the linked list. + * + * Run-time: O(count) linear + * + * @fn NILinkedList::removeObject: + */ + +/** + * Remove the first object from the linked list. + * + * Run-time: O(1) constant + * + * @fn NILinkedList::removeFirstObject + */ + +/** + * Remove the last object from the linked list. + * + * Run-time: O(1) constant + * + * @fn NILinkedList::removeLastObject + */ + + +/** @name Constant-Time Access */ + +/** + * Search for an object in the linked list. + * + * The NILinkedListLocation object will remain valid as long as the object is still in the + * linked list. Once the object is removed from the linked list, however, the location object + * is released from memory and should no longer be used. + * + * TODO (jverkoey July 1, 2011): Consider creating a wrapper object for the location so that + * we can deal with incorrect usage more safely. + * + * Run-time: O(count) linear + * + * @fn NILinkedList::locationOfObject: + * @returns A location within the linked list. + */ + +/** + * Retrieve the object at a specific location. + * + * Run-time: O(1) constant + * + * @fn NILinkedList::objectAtLocation: + */ + +/** + * Remove an object at a predetermined location. + * + * It is assumed that this location still exists in the linked list. If the object this + * location refers to has since been removed then this method will have undefined results. + * + * This is provided as an optimization over the O(n) removal method but should be used with care. + * + * Run-time: O(1) constant + * + * @fn NILinkedList::removeObjectAtLocation: + */ diff --git a/NIDataStructures.m b/NIDataStructures.m new file mode 100644 index 0000000..174cd2e --- /dev/null +++ b/NIDataStructures.m @@ -0,0 +1,491 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIDataStructures.h" + +#import "NIDebuggingTools.h" +#import "NIPreprocessorMacros.h" + +@interface NILinkedList() + +/** + * @internal + * + * Exposed so that the linked list enumerator can iterate over the nodes directly. + */ +@property (nonatomic, readonly, assign) struct NILinkedListNode* head; + +@end + + +#pragma mark - + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * @internal + * + * A implementation of NSEnumerator for NILinkedList. + * + * This class simply implements the nextObject NSEnumerator method and traverses a linked list. + * The linked list is retained when this enumerator is created and released once the enumerator + * is either released or deallocated. + */ +@interface NILinkedListEnumerator : NSEnumerator { +@private + NILinkedList* _ll; + struct NILinkedListNode* _iterator; +} + +/** + * Designated initializer. Retains the linked list. + */ +- (id)initWithLinkedList:(NILinkedList *)ll; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NILinkedListEnumerator + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + NI_RELEASE_SAFELY(_ll); + _iterator = nil; + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)initWithLinkedList:(NILinkedList *)ll { + if ((self = [super init])) { + _ll = [ll retain]; + _iterator = ll.head; + } + return self; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)nextObject { + id object = nil; + + // Iteration step. + if (nil != _iterator) { + object = _iterator->object; + _iterator = _iterator->next; + + // Completion step. + } else { + // As per the guidelines in the Objective-C docs for enumerators, we release the linked + // list when we are finished enumerating. + NI_RELEASE_SAFELY(_ll); + + // We don't have to set _iterator to nil here because is already is. + } + return object; +} + + +@end + + +#pragma mark - + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NILinkedList + +@synthesize head = _head; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + [self removeAllObjects]; + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Linked List Creation + + +/////////////////////////////////////////////////////////////////////////////////////////////////// ++ (NILinkedList *)linkedList { + return [[[[self class] alloc] init] autorelease]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// ++ (NILinkedList *)linkedListWithArray:(NSArray *)array { + return [[[[self class] alloc] initWithArray:array] autorelease]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)initWithArray:(NSArray *)anArray { + if ((self = [self init])) { + for (id object in anArray) { + [self addObject:object]; + } + } + return self; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Private Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)_eraseNode:(struct NILinkedListNode *)node { + [node->object release]; + free(node); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)_setHead:(struct NILinkedListNode *)head { + _head = head; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)_setTail:(struct NILinkedListNode *)tail { + _tail = tail; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)_setCount:(NSUInteger)count { + _count = count; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSCopying + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)copyWithZone:(NSZone *)zone { + NILinkedList* copy = [[[self class] allocWithZone:zone] init]; + + struct NILinkedListNode* node = _head; + + while (0 != node) { + [copy addObject:node->object]; + node = node->next; + } + + [copy _setCount:_count]; + + return copy; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSCoding + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)encodeWithCoder:(NSCoder *)coder { + [coder encodeValueOfObjCType:@encode(NSUInteger) at:&_count]; + + struct NILinkedListNode* node = _head; + while (0 != node) { + [coder encodeObject:node->object]; + node = node->next; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)initWithCoder:(NSCoder *)decoder { + if ((self = [super init])) { + // We'll let addObject modify the count, so create a local count here so that we don't + // double count every object. + NSUInteger count = 0; + [decoder decodeValueOfObjCType:@encode(NSUInteger) at:&count]; + + for (NSUInteger ix = 0; ix < count; ++ix) { + id object = [decoder decodeObject]; + + [self addObject:object]; + } + + // Sanity check. + NIDASSERT(count == _count); + } + return self; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark NSFastEnumeration + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSUInteger)countByEnumeratingWithState: (NSFastEnumerationState *)state + objects: (id *)stackbuf + count: (NSUInteger)len { + // Initialization condition. + if (0 == state->state) { + // Whenever the linked list is modified, the modification number increases. This allows + // enumeration to bail out if the linked list is modified mid-flight. + state->mutationsPtr = &_modificationNumber; + } + + NSUInteger numberOfItemsReturned = 0; + + // If there is no _tail (i.e. this is an empty list) then this will end immediately. + if ((struct NILinkedListNode *)state->state != _tail) { + state->itemsPtr = stackbuf; + + if (0 == state->state) { + // Initialize the state here instead of above when we check 0 == state->state because + // for single item linked lists head == tail. If we initialized it in the initialization + // condition, state->state != _tail check would fail and we wouldn't return the single + // object. + state->state = (unsigned long)_head; + } + + // Return *at most* the number of request objects. + while ((0 != state->state) && (numberOfItemsReturned < len)) { + struct NILinkedListNode* node = (struct NILinkedListNode *)state->state; + stackbuf[numberOfItemsReturned] = node->object; + state->state = (unsigned long)node->next; + ++numberOfItemsReturned; + } + + if (0 == state->state) { + // Final step condition. We allow the above loop to overstep the end one iteration, + // because we rewind it one step here (to ensure that the next time enumeration occurs, + // state == _tail. + state->state = (unsigned long)_tail; + } + + } // else we've returned all of the items that we can; leave numberOfItemsReturned as 0 to + // signal that there is nothing left to be done. + + return numberOfItemsReturned; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Public Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)firstObject { + return (nil != _head) ? _head->object : nil; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)lastObject { + return (nil != _tail) ? _tail->object : nil; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSUInteger)count { + return _count; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Extended Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSArray *)allObjects { + NSMutableArray* mutableArrayOfObjects = [[NSMutableArray alloc] initWithCapacity:self.count]; + + for (id object in self) { + [mutableArrayOfObjects addObject:object]; + } + + NSArray* arrayOfObjects = [mutableArrayOfObjects copy]; + NI_RELEASE_SAFELY(mutableArrayOfObjects); + return [arrayOfObjects autorelease]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (BOOL)containsObject:(id)anObject { + for (id object in self) { + if (object == anObject) { + return YES; + } + } + + return NO; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSString *)description { + // In general we should try to avoid cheating by using allObjects for memory performance reasons, + // but the description method is complex enough that it's not worth reinventing the wheel here. + return [[self allObjects] description]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)objectAtLocation:(NILinkedListLocation *)location { + return ((struct NILinkedListNode *)location)->object; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSEnumerator *)objectEnumerator { + return [[[NILinkedListEnumerator alloc] initWithLinkedList:self] autorelease]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NILinkedListLocation *)locationOfObject:(id)object { + struct NILinkedListNode* node = _head; + while (0 != node) { + if (node->object == object) { + return (NILinkedListLocation *)node; + } + node = node->next; + } + return 0; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)removeObjectAtLocation:(NILinkedListLocation *)location { + if (0 == location) { + return; + } + + struct NILinkedListNode* node = (struct NILinkedListNode *)location; + + if (0 != node->prev) { + node->prev->next = node->next; + + } else { + _head = node->next; + } + + if (0 != node->next) { + node->next->prev = node->prev; + + } else { + _tail = node->prev; + } + + [self _eraseNode:node]; + + --_count; + ++_modificationNumber; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NILinkedListLocation *)addObject:(id)object { + // nil objects can not be added to a linked list. + NIDASSERT(nil != object); + if (nil == object) { + return nil; + } + + struct NILinkedListNode* node = malloc(sizeof(struct NILinkedListNode)); + memset(node, 0, sizeof(struct NILinkedListNode)); + + node->object = [object retain]; + + // Empty condition. + if (nil == _tail) { + _head = node; + _tail = node; + + } else { + // Non-empty condition. + _tail->next = (struct NILinkedListNode*)node; + node->prev = (struct NILinkedListNode*)_tail; + _tail = node; + } + + ++_count; + ++_modificationNumber; + + return (NILinkedListLocation *)node; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Mutable Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)removeAllObjects { + struct NILinkedListNode* node = _head; + while (nil != node) { + struct NILinkedListNode* next = (struct NILinkedListNode *)node->next; + [self _eraseNode:node]; + node = next; + } + + _head = nil; + _tail = nil; + + _count = 0; + ++_modificationNumber; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)removeObject:(id)object { + NILinkedListLocation* location = [self locationOfObject:object]; + if (0 != location) { + [self removeObjectAtLocation:location]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)removeFirstObject { + [self removeObjectAtLocation:_head]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)removeLastObject { + [self removeObjectAtLocation:_tail]; +} + +@end diff --git a/NIDebuggingTools.h b/NIDebuggingTools.h new file mode 100644 index 0000000..9fffe61 --- /dev/null +++ b/NIDebuggingTools.h @@ -0,0 +1,186 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import + +/** + * For inspecting code and writing to logs in debug builds. + * + * Nearly all of the following macros will only do anything if the DEBUG macro is defined. + * The recommended way to enable the debug tools is to specify DEBUG in the "Preprocessor Macros" + * field in your application's Debug target settings. Be careful not to set this for your release + * or app store builds because this will enable code that may cause your app to be rejected. + * + * + *

Debug Assertions

+ * + * Debug assertions are a lightweight "sanity check". They won't crash the app, nor will they + * be included in release builds. They will halt the app's execution when debugging so + * that you can inspect the values that caused the failure. + * + * @code + * NIDASSERT(statement); + * @endcode + * + * If statement is false, the statement will be written to the log and if you are running in + * the simulator with a debugger attached, the app will break on the assertion line. + * + * + *

Debug Logging

+ * + * @code + * NIDPRINT(@"formatted log text %d", param1); + * @endcode + * + * Print the given formatted text to the log. + * + * @code + * NIDPRINTMETHODNAME(); + * @endcode + * + * Print the current method name to the log. + * + * @code + * NIDCONDITIONLOG(statement, @"formatted log text %d", param1); + * @endcode + * + * If statement is true, then the formatted text will be written to the log. + * + * @code + * NIDINFO/NIDWARNING/NIDERROR(@"formatted log text %d", param1); + * @endcode + * + * Will only write the formatted text to the log if NIMaxLogLevel is greater than the respective + * NID* method's log level. See below for log levels. + * + * The default maximum log level is NILOGLEVEL_WARNING. + * + *

Turning up the log level while the app is running

+ * + * NIMaxLogLevel is declared a non-const extern so that you can modify it at runtime. This can + * be helpful for turning logging on while the application is running. + * + * @code + * NIMaxLogLevel = NILOGLEVEL_INFO; + * @endcode + * + * @ingroup NimbusCore + * @defgroup Debugging-Tools Debugging Tools + * @{ + */ + +#ifdef DEBUG + +/** + * Assertions that only fire when DEBUG is defined. + * + * An assertion is like a programmatic breakpoint. Use it for sanity checks to save headache while + * writing your code. + */ +#import + +#if TARGET_IPHONE_SIMULATOR +int NIIsInDebugger(void); +// We leave the __asm__ in this macro so that when a break occurs, we don't have to step out of +// a "breakInDebugger" function. +#define NIDASSERT(xx) { if (!(xx)) { NIDPRINT(@"NIDASSERT failed: %s", #xx); \ +if (NIDebugAssertionsShouldBreak && NIIsInDebugger()) { __asm__("int $3\n" : : ); }; } \ +} ((void)0) +#else +#define NIDASSERT(xx) { if (!(xx)) { NIDPRINT(@"NIDASSERT failed: %s", #xx); } } ((void)0) +#endif // #if TARGET_IPHONE_SIMULATOR + +#else +#define NIDASSERT(xx) ((void)0) +#endif // #ifdef DEBUG + + +#define NILOGLEVEL_INFO 5 +#define NILOGLEVEL_WARNING 3 +#define NILOGLEVEL_ERROR 1 + +/** + * The maximum log level to output for Nimbus debug logs. + * + * This value may be changed at run-time. + * + * The default value is NILOGLEVEL_WARNING. + */ +extern NSInteger NIMaxLogLevel; + +/** + * Whether or not debug assertions should halt program execution like a breakpoint when they fail. + * + * An example of when this is used is in unit tests, when failure cases are tested that will + * fire debug assertions. + * + * The default value is YES. + */ +extern BOOL NIDebugAssertionsShouldBreak; + +/** + * Only writes to the log when DEBUG is defined. + * + * This log method will always write to the log, regardless of log levels. It is used by all + * of the other logging methods in Nimbus' debugging library. + */ +#ifdef DEBUG +#define NIDPRINT(xx, ...) NSLog(@"%s(%d): " xx, __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) +#else +#define NIDPRINT(xx, ...) ((void)0) +#endif // #ifdef DEBUG + +/** + * Write the containing method's name to the log using NIDPRINT. + */ +#define NIDPRINTMETHODNAME() NIDPRINT(@"%s", __PRETTY_FUNCTION__) + +#ifdef DEBUG +/** + * Only writes to the log if condition is satisified. + * + * This macro powers the level-based loggers. It can also be used for conditionally enabling + * families of logs. + */ +#define NIDCONDITIONLOG(condition, xx, ...) { if ((condition)) { NIDPRINT(xx, ##__VA_ARGS__); } \ +} ((void)0) +#else +#define NIDCONDITIONLOG(condition, xx, ...) ((void)0) +#endif // #ifdef DEBUG + + +/** + * Only writes to the log if NIMaxLogLevel >= NILOGLEVEL_ERROR. + */ +#define NIDERROR(xx, ...) NIDCONDITIONLOG((NILOGLEVEL_ERROR <= NIMaxLogLevel), xx, ##__VA_ARGS__) + +/** + * Only writes to the log if NIMaxLogLevel >= NILOGLEVEL_WARNING. + */ +#define NIDWARNING(xx, ...) NIDCONDITIONLOG((NILOGLEVEL_WARNING <= NIMaxLogLevel), \ +xx, ##__VA_ARGS__) + +/** + * Only writes to the log if NIMaxLogLevel >= NILOGLEVEL_INFO. + */ +#define NIDINFO(xx, ...) NIDCONDITIONLOG((NILOGLEVEL_INFO <= NIMaxLogLevel), xx, ##__VA_ARGS__) + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Debugging Tools ////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NIDebuggingTools.m b/NIDebuggingTools.m new file mode 100644 index 0000000..e800843 --- /dev/null +++ b/NIDebuggingTools.m @@ -0,0 +1,61 @@ +// +// Copyright 2011 Jeff Verkoeyen +// Copyright 2009-2011 Facebook +// +// 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. +// + +#import "NIDebuggingTools.h" + +NSInteger NIMaxLogLevel = NILOGLEVEL_WARNING; +BOOL NIDebugAssertionsShouldBreak = YES; + +#ifdef DEBUG + +#if TARGET_IPHONE_SIMULATOR + +#import +#import + +// From: http://developer.apple.com/mac/library/qa/qa2004/qa1361.html +int NIIsInDebugger(void) { + int mib[4]; + struct kinfo_proc info; + size_t size; + + // Initialize the flags so that, if sysctl fails for some bizarre + // reason, we get a predictable result. + + info.kp_proc.p_flag = 0; + + // Initialize mib, which tells sysctl the info we want, in this case + // we're looking for information about a specific process ID. + + mib[0] = CTL_KERN; + mib[1] = KERN_PROC; + mib[2] = KERN_PROC_PID; + mib[3] = getpid(); + + // Call sysctl. + + size = sizeof(info); + sysctl(mib, sizeof(mib) / sizeof(*mib), &info, &size, NULL, 0); + + // We're being debugged if the P_TRACED flag is set. + + return (info.kp_proc.p_flag & P_TRACED) != 0; +} + +#endif // #ifdef TARGET_IPHONE_SIMULATOR + +#endif // #ifdef DEBUG diff --git a/NIDeviceOrientation.h b/NIDeviceOrientation.h new file mode 100644 index 0000000..0d7fd27 --- /dev/null +++ b/NIDeviceOrientation.h @@ -0,0 +1,74 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import +#import + +/** + * For dealing with device orientations. + * + *

Examples

+ * + *

Use NIIsSupportedOrientation to Enable Autorotation

+ * + * @code + * - (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { + * return NIIsSupportedOrientation(toInterfaceOrientation); + * } + * @endcode + * + * @ingroup NimbusCore + * @defgroup Device-Orientation Device Orientation + * @{ + */ + +/** + * For use in shouldAutorotateToInterfaceOrientation: + * + * On iPhone/iPod touch: + * + * Returns YES if the orientation is portrait, landscape left, or landscape right. + * This helps to ignore upside down and flat orientations. + * + * On iPad: + * + * Always returns YES. + */ +BOOL NIIsSupportedOrientation(UIInterfaceOrientation orientation); + +/** + * Returns the application's current interface orientation. + * + * This is simply a convenience method for [UIApplication sharedApplication].statusBarOrientation. + * + * @returns The current interface orientation. + */ +UIInterfaceOrientation NIInterfaceOrientation(void); + +/** + * Creates an affine transform for the given device orientation. + * + * This is useful for creating a transformation matrix for a view that has been added + * directly to the window and doesn't automatically have its transformation modified. + */ +CGAffineTransform NIRotateTransformForOrientation(UIInterfaceOrientation orientation); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Device Orientation /////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + diff --git a/NIDeviceOrientation.m b/NIDeviceOrientation.m new file mode 100644 index 0000000..95d8fde --- /dev/null +++ b/NIDeviceOrientation.m @@ -0,0 +1,74 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import "NIDeviceOrientation.h" + +#import + +#import "NIDebuggingTools.h" +#import "NISDKAvailability.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +BOOL NIIsSupportedOrientation(UIInterfaceOrientation orientation) { + if (NIIsPad()) { + return YES; + + } else { + switch (orientation) { + case UIInterfaceOrientationPortrait: + case UIInterfaceOrientationLandscapeLeft: + case UIInterfaceOrientationLandscapeRight: + return YES; + default: + return NO; + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +UIInterfaceOrientation NIInterfaceOrientation(void) { + UIInterfaceOrientation orient = [UIApplication sharedApplication].statusBarOrientation; + + // This code used to use the navigator to find the currently visible view controller and + // fall back to checking its orientation if we didn't know the status bar's orientation. + // It's unclear when this was actually necessary, though, so this assertion is here to try + // to find that case. If this assertion fails then the repro case needs to be analyzed and + // this method made more robust to handle that case. + NIDASSERT(UIDeviceOrientationUnknown != orient); + + return orient; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +CGAffineTransform NIRotateTransformForOrientation(UIInterfaceOrientation orientation) { + if (orientation == UIInterfaceOrientationLandscapeLeft) { + return CGAffineTransformMakeRotation((CGFloat)(M_PI * 1.5)); + + } else if (orientation == UIInterfaceOrientationLandscapeRight) { + return CGAffineTransformMakeRotation((CGFloat)(M_PI / 2.0)); + + } else if (orientation == UIInterfaceOrientationPortraitUpsideDown) { + return CGAffineTransformMakeRotation((CGFloat)(-M_PI)); + + } else { + return CGAffineTransformIdentity; + } +} diff --git a/NIError.h b/NIError.h new file mode 100644 index 0000000..b39fd0f --- /dev/null +++ b/NIError.h @@ -0,0 +1,56 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import +#import + +/** + * For defining various error types used throughout the Nimbus framework. + * + * @ingroup NimbusCore + * @defgroup Errors Errors + * @{ + */ + +/** The Nimbus error domain. */ +extern NSString* const NINimbusErrorDomain; + +/** The key used for images in the error's userInfo. */ +extern NSString* const NIImageErrorKey; + +/** NSError codes in NINimbusErrorDomain. */ +typedef enum { + /** The image is too small to be used. */ + NIImageTooSmall = 1, +} NINimbusErrorDomainCode; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Errors /////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + +/** + *

Example

+ * + * @code + * error = [NSError errorWithDomain: NINimbusErrorDomain + * code: NIImageTooSmall + * userInfo: [NSDictionary dictionaryWithObject: image + * forKey: NIImageErrorKey]]; + * @endcode + * + * @enum NINimbusErrorDomainCode + */ diff --git a/NIError.m b/NIError.m new file mode 100644 index 0000000..5c16dd4 --- /dev/null +++ b/NIError.m @@ -0,0 +1,21 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIError.h" + +NSString* const NINimbusErrorDomain = @"com.nimbus.error"; + +NSString* const NIImageErrorKey = @"image"; diff --git a/NIFoundationMethods.h b/NIFoundationMethods.h new file mode 100644 index 0000000..9990e1c --- /dev/null +++ b/NIFoundationMethods.h @@ -0,0 +1,135 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import +#import + +/** + * For filling in gaps in Apple's Foundation framework. + * + * @ingroup NimbusCore + * @defgroup Foundation-Methods Foundation Methods + * @{ + * + * Utility methods save time and headache. You've probably written dozens of your own. Nimbus + * hopes to provide an ever-growing set of convenience methods that compliment the Foundation + * framework's functionality. + */ + +#pragma mark - +#pragma mark CGRect Methods + +/** + * For manipulating CGRects. + * + * @defgroup CGRect-Methods CGRect Methods + * @{ + * + * These methods provide additional means of modifying the edges of CGRects beyond the basics + * included in CoreGraphics. + */ + +/** + * Modifies only the right and bottom edges of a CGRect. + * + * @return a CGRect with dx and dy subtracted from the width and height. + * + * Example result: CGRectMake(x, y, w - dx, h - dy) + */ +CGRect NIRectContract(CGRect rect, CGFloat dx, CGFloat dy); + +/** + * Modifies only the top and left edges of a CGRect. + * + * @return a CGRect whose origin has been offset by dx, dy, and whose size has been + * contracted by dx, dy. + * + * Example result: CGRectMake(x + dx, y + dy, w - dx, h - dy) + */ +CGRect NIRectShift(CGRect rect, CGFloat dx, CGFloat dy); + +/** + * Add the insets to a CGRect - equivalent to padding in CSS. + * + * @return a CGRect whose edges have been inset. + * + * Example result: CGRectMake(x + left, y + top, w - (left + right), h - (top + bottom)) + */ +CGRect NIRectInset(CGRect rect, UIEdgeInsets insets); + +/**@}*/ + + +#pragma mark - +#pragma mark NSRange Methods + +/** + * For manipulating NSRange. + * + * @defgroup NSRange-Methods NSRange Methods + * @{ + */ + +/** + * Create an NSRange object from a CFRange object. + * + * @return an NSRange object with the same values as the CFRange object. + * + * @attention This has the potential to behave unexpectedly because it converts the + * CFRange's long values to unsigned integers. Nimbus will fire off a debug + * assertion at runtime if the value will be chopped or the sign will change. + * Even though the assertion will fire, the method will still chop or change + * the sign of the values so you should take care to fix this. + */ +NSRange NIMakeNSRangeFromCFRange(CFRange range); + +/**@}*/ + + +#pragma mark - +#pragma mark General Purpose Methods + +/** + * For general purpose foundation type manipulation. + * + * @defgroup General-Purpose-Methods General Purpose Methods + * @{ + */ + +/** + * Bounds a given value within the min and max values. + * + * If max < min then value will be min. + * + * @returns min <= result <= max + */ +CGFloat boundf(CGFloat value, CGFloat min, CGFloat max); + +/** + * Bounds a given value within the min and max values. + * + * If max < min then value will be min. + * + * @returns min <= result <= max + */ +NSInteger boundi(NSInteger value, NSInteger min, NSInteger max); + +/**@}*/ + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Foundation Methods /////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NIFoundationMethods.m b/NIFoundationMethods.m new file mode 100644 index 0000000..a618d29 --- /dev/null +++ b/NIFoundationMethods.m @@ -0,0 +1,99 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIFoundationMethods.h" + +#import "NIDebuggingTools.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark CGRect Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +CGRect NIRectContract(CGRect rect, CGFloat dx, CGFloat dy) { + return CGRectMake(rect.origin.x, rect.origin.y, rect.size.width - dx, rect.size.height - dy); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +CGRect NIRectShift(CGRect rect, CGFloat dx, CGFloat dy) { + return CGRectOffset(NIRectContract(rect, dx, dy), dx, dy); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +CGRect NIRectInset(CGRect rect, UIEdgeInsets insets) { + return CGRectMake(rect.origin.x + insets.left, rect.origin.y + insets.top, + rect.size.width - (insets.left + insets.right), + rect.size.height - (insets.top + insets.bottom)); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSRange Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSRange NIMakeNSRangeFromCFRange(CFRange range) { + // CFRange stores its values in signed longs, but we're about to copy the values into + // unsigned integers, let's check whether we're about to lose any information. + NIDASSERT(range.location >= 0 && range.location <= NSIntegerMax); + NIDASSERT(range.length >= 0 && range.length <= NSIntegerMax); + return NSMakeRange(range.location, range.length); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark General Purpose Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +CGFloat boundf(CGFloat value, CGFloat min, CGFloat max) { + if (max < min) { + max = min; + } + CGFloat bounded = value; + if (bounded > max) { + bounded = max; + } + if (bounded < min) { + bounded = min; + } + return bounded; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSInteger boundi(NSInteger value, NSInteger min, NSInteger max) { + if (max < min) { + max = min; + } + NSInteger bounded = value; + if (bounded > max) { + bounded = max; + } + if (bounded < min) { + bounded = min; + } + return bounded; +} diff --git a/NIInMemoryCache.h b/NIInMemoryCache.h new file mode 100644 index 0000000..2545669 --- /dev/null +++ b/NIInMemoryCache.h @@ -0,0 +1,323 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import + +/** + * For storing and accessing objects in memory. + * + * The base class, NIMemoryCache, is a generic object store that may be used for anything that + * requires support for expiration. + * + * @ingroup NimbusCore + * @defgroup In-Memory-Caches In-Memory Caches + * @{ + */ + +@class NILinkedList; + +/** + * An in-memory cache for storing objects with expiration support. + * + * The Nimbus in-memory object cache allows you to store objects in memory with an expiration + * date attached. Objects with expiration dates drop out of the cache when they have expired. + */ +@interface NIMemoryCache : NSObject { +@private + // Mapping from a name (usually a URL) to an internal object. + NSMutableDictionary* _cacheMap; + + // A linked list of least recently used cache objects. Most recently used is the tail. + NILinkedList* _lruCacheObjects; +} + +// Designated initializer. +- (id)initWithCapacity:(NSUInteger)capacity; + +- (NSUInteger)count; + +- (void)storeObject:(id)object withName:(NSString *)name; +- (void)storeObject:(id)object withName:(NSString *)name expiresAfter:(NSDate *)expirationDate; + +- (void)removeObjectWithName:(NSString *)name; +- (void)removeAllObjects; + +- (id)objectWithName:(NSString *)name; +- (BOOL)containsObjectWithName:(NSString *)name; +- (NSDate *)dateOfLastAccessWithName:(NSString *)name; + +- (NSString *)nameOfLeastRecentlyUsedObject; +- (NSString *)nameOfMostRecentlyUsedObject; + +- (void)reduceMemoryUsage; + + +// Subclassing + +- (void)willSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject; +- (void)didSetObject:(id)object withName:(NSString *)name; +- (void)willRemoveObject:(id)object withName:(NSString *)name; + +@end + + +/** + * An in-memory cache for storing images with caps on the total number of pixels. + * + * When reduceMemoryUsage is called, the least recently used images are removed from the cache + * until the numberOfPixels is below maxNumberOfPixelsUnderStress. + * + * When an image is added to the cache that causes the memory usage to pass the max, the + * least recently used images are removed from the cache until the numberOfPixels is below + * maxNumberOfPixels. + * + * By default the image memory cache has no limit to its pixel count. You must explicitly + * set this value in your application. + * + * @attention If the cache is too small to fit the newly added image, then all images + * will end up being removed including the one being added. + * + * @see Nimbus::imageMemoryCache + * @see Nimbus::setImageMemoryCache: + */ +@interface NIImageMemoryCache : NIMemoryCache { +@private + NSUInteger _numberOfPixels; + + NSUInteger _maxNumberOfPixels; + NSUInteger _maxNumberOfPixelsUnderStress; +} + +@property (nonatomic, readonly, assign) NSUInteger numberOfPixels; +@property (nonatomic, readwrite, assign) NSUInteger maxNumberOfPixels; +@property (nonatomic, readwrite, assign) NSUInteger maxNumberOfPixelsUnderStress; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of In-Memory Cache ////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// + + +/** @name Creating an In-Memory Cache */ + +/** + * Initializes a newly allocated cache with the given capacity. + * + * @returns An in-memory cache initialized with the given capacity. + * @fn NIMemoryCache::initWithCapacity: + */ + + +/** @name Storing Objects in the Cache */ + +/** + * Stores an object in the cache. + * + * The object will be stored without an expiration date. The object will stay in the cache until + * it's bumped out due to the cache's memory limit. + * + * @param object The object being stored in the cache. + * @param name The name used as a key to store this object. + * @fn NIMemoryCache::storeObject:withName: + */ + +/** + * Stores an object in the cache with an expiration date. + * + * If an object is stored with an expiration date that has already passed then the object will + * not be stored in the cache and any existing object will be removed. The rationale behind this + * is that the object would be removed from the cache the next time it was accessed anyway. + * + * @param object The object being stored in the cache. + * @param name The name used as a key to store this object. + * @param expirationDate A date after which this object is no longer valid in the cache. + * @fn NIMemoryCache::storeObject:withName:expiresAfter: + */ + + +/** @name Removing Objects from the Cache */ + +/** + * Removes an object from the cache. + * + * @param name The name used as a key to store this object. + * @fn NIMemoryCache::removeObjectWithName: + */ + +/** + * Removes all objects from the cache, regardless of expiration dates. + * + * This will completely clear out the cache and all objects in the cache will be released. + * + * @fn NIMemoryCache::removeAllObjects + */ + + +/** @name Accessing Objects in the Cache */ + +/** + * Retrieves an object from the cache. + * + * If the object has expired then the object will be removed from the cache and nil will be + * returned. + * + * @returns The object stored in the cache. The object is retained and autoreleased to + * ensure that it survives this run loop if you then remove it from the cache. + * @fn NIMemoryCache::objectWithName: + */ + +/** + * Returns a Boolean value that indicates whether an object with the given name is present + * in the cache. + * + * Does not update the access time of the object. + * + * If the object has expired then the object will be removed from the cache and NO will be + * returned. + * + * @returns YES if an object with the given name is present in the cache and has not expired, + * otherwise NO. + * @fn NIMemoryCache::containsObjectWithName: + */ + +/** + * Returns the date that the object with the given name was last accessed. + * + * Does not update the access time of the object. + * + * If the object has expired then the object will be removed from the cache and nil will be + * returned. + * + * @returns The last access date of the object if it exists and has not expired, nil + * otherwise. + * @fn NIMemoryCache::dateOfLastAccessWithName: + */ + +/** + * Retrieve the name of the object that was least recently used. + * + * This will not update the access time of the object. + * + * If the cache is empty, returns nil. + * + * @fn NIMemoryCache::nameOfLeastRecentlyUsedObject + */ + +/** + * Retrieve the key with the most fresh access. + * + * This will not update the access time of the object. + * + * If the cache is empty, returns nil. + * + * @fn NIMemoryCache::nameOfMostRecentlyUsedObject + */ + + +/** @name Reducing Memory Usage Explicitly */ + +/** + * Removes all expired objects from the cache. + * + * Subclasses may add additional functionality to this implementation. + * Subclasses should call super in order to prune expired objects. + * + * This will be called when UIApplicationDidReceiveMemoryWarningNotification + * is posted. + * + * @fn NIMemoryCache::reduceMemoryUsage + */ + + +/** @name Querying an In-Memory Cache */ + +/** + * Returns the number of objects currently in the cache. + * + * @returns The number of objects currently in the cache. + * @fn NIMemoryCache::count + */ + + +/** + * @name Subclassing + * + * The following methods are provided to aid in subclassing and are not meant to be + * used externally. + */ + +/** + * An object is about to be stored in the cache. + * + * @param object The object that is about to be stored in the cache. + * @param name The cache name for the object. + * @param previousObject The object previously stored in the cache. This may be the + * same as object. + * @fn NIMemoryCache::willSetObject:withName:previousObject: + */ + +/** + * An object has been stored in the cache. + * + * @param object The object that was stored in the cache. + * @param name The cache name for the object. + * @fn NIMemoryCache::didSetObject:withName: + */ + +/** + * An object is about to be removed from the cache. + * + * @param object The object about to removed from the cache. + * @param name The cache name for the object about to be removed. + * @fn NIMemoryCache::willRemoveObject:withName: + */ + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// NIImageMemoryCache + +/** @name Querying an In-Memory Image Cache */ + +/** + * Returns the total number of pixels being stored in the cache. + * + * @returns The total number of pixels being stored in the cache. + * @fn NIImageMemoryCache::numberOfPixels + */ + + +/** @name Setting the Maximum Number of Pixels */ + +/** + * The maximum number of pixels this cache may ever store. + * + * Defaults to 0, which is special cased to represent an unlimited number of pixels. + * + * @returns The maximum number of pixels this cache may ever store. + * @fn NIImageMemoryCache::maxNumberOfPixels + */ + +/** + * The maximum number of pixels this cache may store after a call to reduceMemoryUsage. + * + * Defaults to 0, which is special cased to represent an unlimited number of pixels. + * + * @returns The maximum number of pixels this cache may store after a call + * to reduceMemoryUsage. + * @fn NIImageMemoryCache::maxNumberOfPixelsUnderStress + */ diff --git a/NIInMemoryCache.m b/NIInMemoryCache.m new file mode 100644 index 0000000..2aa5843 --- /dev/null +++ b/NIInMemoryCache.m @@ -0,0 +1,500 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIInMemoryCache.h" + +#import "NIDataStructures.h" +#import "NIDebuggingTools.h" +#import "NIPreprocessorMacros.h" + +@interface NIMemoryCache() + +@property (nonatomic, readwrite, retain) NSMutableDictionary* cacheMap; +@property (nonatomic, readwrite, retain) NILinkedList* lruCacheObjects; + +@end + + +/** + * @brief A single cache item's information. + * + * Used in expiration calculations and for storing the actual cache object. + */ +@interface NIMemoryCacheInfo : NSObject { +@private + NSString* _name; + id _object; + NSDate* _expirationDate; + NSDate* _lastAccessTime; + + // Keep tabs on the location of the lru object so that we can move it quickly. + NILinkedListLocation* _lruLocation; +} + +/** + * @brief The name used to store this object in the cache. + */ +@property (nonatomic, readwrite, copy) NSString* name; + +/** + * @brief The object stored in the cache. + */ +@property (nonatomic, readwrite, retain) id object; + +/** + * @brief The date after which the image is no longer valid and should be removed from the cache. + */ +@property (nonatomic, readwrite, retain) NSDate* expirationDate; + +/** + * @brief The last time this image was accessed. + * + * This property is updated every time the image is fetched from or stored into the cache. It + * is used when the memory peak has been reached as a fast means of removing least-recently-used + * images. When the memory limit is reached, we sort the cache based on the last access times and + * then prune images until we're under the memory limit again. + */ +@property (nonatomic, readwrite, retain) NSDate* lastAccessTime; + +/** + * @brief The location of this object in the least-recently used linked list. + */ +@property (nonatomic, readwrite, assign) NILinkedListLocation* lruLocation; + +/** + * @brief Determine whether this cache entry has past its expiration date. + * + * @returns YES if an expiration date has been specified and the expiration date has been passed. + * NO in all other cases. Notably if there is no expiration date then this object will + * never expire. + */ +- (BOOL)hasExpired; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NIMemoryCache + +@synthesize cacheMap = _cacheMap; +@synthesize lruCacheObjects = _lruCacheObjects; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + [[NSNotificationCenter defaultCenter] removeObserver:self]; + + NI_RELEASE_SAFELY(_cacheMap); + NI_RELEASE_SAFELY(_lruCacheObjects); + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)init { + return [self initWithCapacity:0]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)initWithCapacity:(NSUInteger)capacity { + if ((self = [super init])) { + _cacheMap = [[NSMutableDictionary alloc] initWithCapacity:capacity]; + _lruCacheObjects = [[NILinkedList alloc] init]; + + // Automatically reduce memory usage when we get a memory warning. + [[NSNotificationCenter defaultCenter] addObserver: self + selector: @selector(reduceMemoryUsage) + name: UIApplicationDidReceiveMemoryWarningNotification + object: nil]; + } + return self; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Internal Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)updateAccessTimeForInfo:(NIMemoryCacheInfo *)info { + NIDASSERT(nil != info); + if (nil == info) { + return; + } + info.lastAccessTime = [NSDate date]; + + [_lruCacheObjects removeObjectAtLocation:info.lruLocation]; + info.lruLocation = [_lruCacheObjects addObject:info]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NIMemoryCacheInfo *)cacheInfoForName:(NSString *)name { + return [_cacheMap objectForKey:name]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setCacheInfo:(NIMemoryCacheInfo *)info forName:(NSString *)name { + NIDASSERT(nil != name); + if (nil == name) { + return; + } + + // Storing in the cache counts as an access of the object, so we update the access time. + [self updateAccessTimeForInfo:info]; + + [self willSetObject: info.object + withName: name + previousObject: [self cacheInfoForName:name].object]; + + [_cacheMap setObject:info forKey:name]; + + [self didSetObject: info.object + withName: name]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)removeCacheInfoForName:(NSString *)name { + NIDASSERT(nil != name); + if (nil == name) { + return; + } + + [self willRemoveObject:[self cacheInfoForName:name].object withName:name]; + + [_cacheMap removeObjectForKey:name]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Subclassing + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)willSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject { + // No-op +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didSetObject:(id)object withName:(NSString *)name { + // No-op +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)willRemoveObject:(id)object withName:(NSString *)name { + // No-op +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Public Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)storeObject:(id)object withName:(NSString *)name { + [self storeObject:object withName:name expiresAfter:nil]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)storeObject:(id)object withName:(NSString *)name expiresAfter:(NSDate *)expirationDate { + // Don't store nil objects in the cache. + if (nil == object) { + return; + } + + if (nil != expirationDate && [[NSDate date] timeIntervalSinceDate:expirationDate] >= 0) { + // The object being stored is already expired so remove the object from the cache altogether. + [self removeObjectWithName:name]; + + // We're done here. + return; + } + NIMemoryCacheInfo* info = [self cacheInfoForName:name]; + + // Create a new cache entry. + if (nil == info) { + info = [[[NIMemoryCacheInfo alloc] init] autorelease]; + info.name = name; + } + + // Store the object in the cache item. + info.object = object; + + // Override any existing expiration date. + info.expirationDate = expirationDate; + + // Commit the changes to the cache. + [self setCacheInfo:info forName:name]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)objectWithName:(NSString *)name { + NIMemoryCacheInfo* info = [self cacheInfoForName:name]; + + id object = nil; + + if (nil != info) { + if ([info hasExpired]) { + [self removeObjectWithName:name]; + + } else { + // Update the access time whenever we fetch an object from the cache. + [self updateAccessTimeForInfo:info]; + + object = info.object; + } + } + + return [[object retain] autorelease]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (BOOL)containsObjectWithName:(NSString *)name { + NIMemoryCacheInfo* info = [self cacheInfoForName:name]; + + if ([info hasExpired]) { + [self removeObjectWithName:name]; + return NO; + } + + return (nil != info); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSDate *)dateOfLastAccessWithName:(NSString *)name { + NIMemoryCacheInfo* info = [self cacheInfoForName:name]; + + if ([info hasExpired]) { + [self removeObjectWithName:name]; + return nil; + } + + return [info lastAccessTime]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSString *)nameOfLeastRecentlyUsedObject { + NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject]; + + if ([info hasExpired]) { + [self removeObjectWithName:info.name]; + return nil; + } + + return info.name; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSString *)nameOfMostRecentlyUsedObject { + NIMemoryCacheInfo* info = [self.lruCacheObjects lastObject]; + + if ([info hasExpired]) { + [self removeObjectWithName:info.name]; + return nil; + } + + return info.name; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)removeObjectWithName:(NSString *)name { + [self removeCacheInfoForName:name]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)removeAllObjects { + NI_RELEASE_SAFELY(_cacheMap); + _cacheMap = [[NSMutableDictionary alloc] init]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)reduceMemoryUsage { + // Copy the cache map because it's likely that we're going to modify it. + NSDictionary* cacheMap = [_cacheMap copy]; + + // Iterate over the copied cache map (which will not be modified). + for (id name in cacheMap) { + NIMemoryCacheInfo* info = [self cacheInfoForName:name]; + + if ([info hasExpired]) { + [self removeCacheInfoForName:name]; + } + } + NI_RELEASE_SAFELY(cacheMap); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSUInteger)count { + return [_cacheMap count]; +} + + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NIMemoryCacheInfo + +@synthesize name = _name; +@synthesize object = _object; +@synthesize expirationDate = _expirationDate; +@synthesize lastAccessTime = _lastAccessTime; +@synthesize lruLocation = _lruLocation; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + NI_RELEASE_SAFELY(_name); + NI_RELEASE_SAFELY(_object); + NI_RELEASE_SAFELY(_expirationDate); + NI_RELEASE_SAFELY(_lastAccessTime); + _lruLocation = nil; + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (BOOL)hasExpired { + return (nil != _expirationDate + && [[NSDate date] timeIntervalSinceDate:_expirationDate] >= 0); +} + + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@interface NIImageMemoryCache() + +// Internally only. +@property (nonatomic, readwrite, assign) NSUInteger numberOfPixels; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NIImageMemoryCache + +@synthesize numberOfPixels = _numberOfPixels; +@synthesize maxNumberOfPixels = _maxNumberOfPixels; +@synthesize maxNumberOfPixelsUnderStress = _maxNumberOfPixelsUnderStress; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSUInteger)numberOfPixelsUsedByImage:(UIImage *)image { + if (nil == image) { + return 0; + } + + NSUInteger numberOfPixels = (NSUInteger)(image.size.width * image.size.height); + if ([image respondsToSelector:@selector(scale)]) { + numberOfPixels *= [image scale]; + } + return numberOfPixels; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)reduceMemoryUsage { + // Remove all expired images first. + [super reduceMemoryUsage]; + + if (self.maxNumberOfPixelsUnderStress > 0) { + // Remove the least recently used images by iterating over the linked list. + while (self.numberOfPixels > self.maxNumberOfPixelsUnderStress) { + NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject]; + [self removeCacheInfoForName:info.name]; + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)willSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject { + NIDASSERT(nil == object || [object isKindOfClass:[UIImage class]]); + if (![object isKindOfClass:[UIImage class]]) { + return; + } + + self.numberOfPixels -= [self numberOfPixelsUsedByImage:previousObject]; + self.numberOfPixels += [self numberOfPixelsUsedByImage:object]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didSetObject:(id)object withName:(NSString *)name { + // Reduce the cache size after the object has been set in case the cache size is smaller + // than the object that's being added and we need to remove this object right away. If we + // try to reduce the cache size before the object's been set, we won't have anything to remove + // and we'll get stuck in an infinite loop. + if (self.maxNumberOfPixels > 0) { + // Remove least recently used images until we satisfy our memory constraints. + while (self.numberOfPixels > self.maxNumberOfPixels + && [self.lruCacheObjects count] > 0) { + NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject]; + [self removeCacheInfoForName:info.name]; + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)willRemoveObject:(id)object withName:(NSString *)name { + NIDASSERT(nil == object || [object isKindOfClass:[UIImage class]]); + if (nil == object || ![object isKindOfClass:[UIImage class]]) { + return; + } + + NIMemoryCacheInfo* info = [self cacheInfoForName:name]; + [self.lruCacheObjects removeObjectAtLocation:info.lruLocation]; + + self.numberOfPixels -= [self numberOfPixelsUsedByImage:object]; +} + + +@end + diff --git a/NINetworkActivity.h b/NINetworkActivity.h new file mode 100644 index 0000000..4b0d9cf --- /dev/null +++ b/NINetworkActivity.h @@ -0,0 +1,94 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 July 2, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import + +/** + * For showing network activity in the device's status bar. + * + * @ingroup NimbusCore + * @defgroup Network-Activity Network Activity + * @{ + * + * Two methods for keeping track of all active network tasks. These methods are threadsafe + * and act as a simple counter. When the counter is positive, the network activity indicator + * is displayed. + */ + +/** + * Increment the number of active network tasks. + * + * The status bar activity indicator will be spinning while there are active tasks. + * + * This method is threadsafe. + */ +void NINetworkActivityTaskDidStart(void); + +/** + * Decrement the number of active network tasks. + * + * The status bar activity indicator will be spinning while there are active tasks. + * + * This method is threadsafe. + */ +void NINetworkActivityTaskDidFinish(void); + +/** + * @name For Debugging Only + * @{ + * + * Methods that will only do anything interesting if the DEBUG preprocessor macro is defined. + */ + +/** + * Enable network activity debugging. + * + * @attention This won't do anything unless the DEBUG preprocessor macro is defined. + * + * The Nimbus network activity methods will only work correctly if they are the only methods to + * touch networkActivityIndicatorVisible. If you are using another library that touches + * networkActivityIndicatorVisible then the network activity indicator might not accurately + * represent its state. + * + * When enabled, the networkActivityIndicatorVisible method on UIApplication will be swizzled + * with a debugging method that checks the global network task count and verifies that state + * is maintained correctly. If it is found that networkActivityIndicatorVisible is being accessed + * directly, then an assertion will be fired. + * + * If debugging was previously enabled, this does nothing. + */ +void NIEnableNetworkActivityDebugging(void); + +/** + * Disable network activity debugging. + * + * @attention This won't do anything unless the DEBUG preprocessor macro is defined. + * + * When disabled, the networkActivityIndicatorVisible will be restored if this was previously + * enabled, otherwise this method does nothing. + * + * If debugging wasn't previously enabled, this does nothing. + */ +void NIDisableNetworkActivityDebugging(void); + +/**@}*/// End of For Debugging Only + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Network Activity ///////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NINetworkActivity.m b/NINetworkActivity.m new file mode 100644 index 0000000..8d26828 --- /dev/null +++ b/NINetworkActivity.m @@ -0,0 +1,145 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NINetworkActivity.h" + +#import "NIDebuggingTools.h" + +#ifdef DEBUG +#import "NIRuntimeClassModifications.h" +#endif + +#import + +static int gNetworkTaskCount = 0; +static pthread_mutex_t gMutex = PTHREAD_MUTEX_INITIALIZER; + +/////////////////////////////////////////////////////////////////////////////////////////////////// +void NINetworkActivityTaskDidStart(void) { + pthread_mutex_lock(&gMutex); + + BOOL enableNetworkActivityIndicator = (0 == gNetworkTaskCount); + + ++gNetworkTaskCount; + + if (enableNetworkActivityIndicator) { + [UIApplication sharedApplication].networkActivityIndicatorVisible = YES; + } + + pthread_mutex_unlock(&gMutex); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +void NINetworkActivityTaskDidFinish(void) { + pthread_mutex_lock(&gMutex); + + --gNetworkTaskCount; + // If this asserts, you don't have enough stop requests to match your start requests. + NIDASSERT(gNetworkTaskCount >= 0); + gNetworkTaskCount = MAX(0, gNetworkTaskCount); + + if (gNetworkTaskCount == 0) { + [UIApplication sharedApplication].networkActivityIndicatorVisible = NO; + } + + pthread_mutex_unlock(&gMutex); +} + + +#pragma mark - +#pragma mark Network Activity Debugging + +#ifdef DEBUG + +static BOOL gNetworkActivityDebuggingEnabled = NO; + +void NISwizzleMethodsForNetworkActivityDebugging(void); + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation UIApplication (NimbusNetworkActivityDebugging) + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)nimbusDebugSetNetworkActivityIndicatorVisible:(BOOL)visible { + // This method will only be used when swizzled, so this will actually call + // setNetworkActivityIndicatorVisible: + [self nimbusDebugSetNetworkActivityIndicatorVisible:visible]; + + // Sanity check that this method isn't being called directly when debugging isn't enabled. + NIDASSERT(gNetworkActivityDebuggingEnabled); + + // If either of the following assertions fail then you should look at the call stack to + // determine what code is erroneously calling setNetworkActivityIndicatorVisible: directly. + if (visible) { + // The only time we should be enabling the network activity indicator is when the task + // count is one. + NIDASSERT(1 == gNetworkTaskCount); + + } else { + // The only time we should be disabling the network activity indicator is when the task + // count is zero. + NIDASSERT(0 == gNetworkTaskCount); + } +} + + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +void NISwizzleMethodsForNetworkActivityDebugging(void) { + NISwapInstanceMethods([UIApplication class], + @selector(setNetworkActivityIndicatorVisible:), + @selector(nimbusDebugSetNetworkActivityIndicatorVisible:)); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +void NIEnableNetworkActivityDebugging(void) { + if (!gNetworkActivityDebuggingEnabled) { + gNetworkActivityDebuggingEnabled = YES; + NISwizzleMethodsForNetworkActivityDebugging(); + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +void NIDisableNetworkActivityDebugging(void) { + if (gNetworkActivityDebuggingEnabled) { + gNetworkActivityDebuggingEnabled = NO; + NISwizzleMethodsForNetworkActivityDebugging(); + } +} + + +#else // #ifndef DEBUG + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +void NIEnableNetworkActivityDebugging(void) { + // No-op +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +void NIDisableNetworkActivityDebugging(void) { + // No-op +} + + +#endif // #ifdef DEBUG diff --git a/NINonEmptyCollectionTesting.h b/NINonEmptyCollectionTesting.h new file mode 100644 index 0000000..423823b --- /dev/null +++ b/NINonEmptyCollectionTesting.h @@ -0,0 +1,53 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import + +/** + * For testing whether a collection is of a certain type and is non-empty. + * + * @ingroup NimbusCore + * @defgroup Non-Empty-Collection-Testing Non-Empty Collection Testing + * @{ + * + * Simply calling -count on an object may not yield the expected results when enumerating it if + * certain assumptions are also made about the object's type. For example, if a JSON response + * returns a dictionary when you expected an array, casting the result to an NSArray and + * calling count will yield a positive value, but objectAtIndex: will crash the application. + * These methods provide a safer check for non-emptiness of collections. + */ + +/** + * Tests if an object is a non-nil array which is not empty. + */ +BOOL NIIsArrayWithObjects(id object); + +/** + * Tests if an object is a non-nil set which is not empty. + */ +BOOL NIIsSetWithObjects(id object); + +/** + * Tests if an object is a non-nil string which is not empty. + */ +BOOL NIIsStringWithAnyText(id object); + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Non-Empty Collection Testing ///////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NINonEmptyCollectionTesting.m b/NINonEmptyCollectionTesting.m new file mode 100644 index 0000000..2b74840 --- /dev/null +++ b/NINonEmptyCollectionTesting.m @@ -0,0 +1,37 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import "NINonEmptyCollectionTesting.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +BOOL NIIsArrayWithObjects(id object) { + return [object isKindOfClass:[NSArray class]] && [(NSArray*)object count] > 0; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +BOOL NIIsSetWithObjects(id object) { + return [object isKindOfClass:[NSSet class]] && [(NSSet*)object count] > 0; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +BOOL NIIsStringWithAnyText(id object) { + return [object isKindOfClass:[NSString class]] && [(NSString*)object length] > 0; +} diff --git a/NINonRetainingCollections.h b/NINonRetainingCollections.h new file mode 100644 index 0000000..809d46a --- /dev/null +++ b/NINonRetainingCollections.h @@ -0,0 +1,60 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import + +/** + * For collections that don't retain their objects. + * + * @ingroup NimbusCore + * @defgroup Non-Retaining-Collections Non-Retaining Collections + * @{ + * + * Non-retaining collections have historically been used when we needed more than one delegate + * in an object. However, NSNotificationCenter is a much better solution for n > 1 delegates. + * Using a non-retaining collection is dangerous, so if you must use one, use it with extreme care. + * The danger primarily lies in the fact that by all appearances the collection should still + * operate like a regular collection, so this might lead to a lot of developer error if the + * developer assumes that the collection does, in fact, retain the object. + */ + +/** + * Creates a mutable array which does not retain references to the objects it contains. + * + * Typically used with arrays of delegates. + */ +NSMutableArray* NICreateNonRetainingMutableArray(void); + +/** + * Creates a mutable dictionary which does not retain references to the values it contains. + * + * Typically used with dictionaries of delegates. + */ +NSMutableDictionary* NICreateNonRetainingMutableDictionary(void); + +/** + * Creates a mutable set which does not retain references to the values it contains. + * + * Typically used with sets of delegates. + */ +NSMutableSet* NICreateNonRetainingMutableSet(void); + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Non-Retaining Collections //////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NINonRetainingCollections.m b/NINonRetainingCollections.m new file mode 100644 index 0000000..e19fc7a --- /dev/null +++ b/NINonRetainingCollections.m @@ -0,0 +1,51 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 9, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import "NINonRetainingCollections.h" + +// No-ops for non-retaining objects. +static const void* NIRetainNoOp(CFAllocatorRef allocator, const void *value) { return value; } +static void NIReleaseNoOp(CFAllocatorRef allocator, const void *value) { } + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSMutableArray* NICreateNonRetainingMutableArray(void) { + CFArrayCallBacks callbacks = kCFTypeArrayCallBacks; + callbacks.retain = NIRetainNoOp; + callbacks.release = NIReleaseNoOp; + return (NSMutableArray *)CFArrayCreateMutable(nil, 0, &callbacks); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSMutableDictionary* NICreateNonRetainingMutableDictionary(void) { + CFDictionaryKeyCallBacks keyCallbacks = kCFTypeDictionaryKeyCallBacks; + CFDictionaryValueCallBacks callbacks = kCFTypeDictionaryValueCallBacks; + callbacks.retain = NIRetainNoOp; + callbacks.release = NIReleaseNoOp; + return (NSMutableDictionary *)CFDictionaryCreateMutable(nil, 0, &keyCallbacks, &callbacks); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSMutableSet* NICreateNonRetainingMutableSet(void) { + CFSetCallBacks callbacks = kCFTypeSetCallBacks; + callbacks.retain = NIRetainNoOp; + callbacks.release = NIReleaseNoOp; + return (NSMutableSet *)CFSetCreateMutable(nil, 0, &callbacks); +} diff --git a/NIOperations.h b/NIOperations.h new file mode 100644 index 0000000..27c3eb8 --- /dev/null +++ b/NIOperations.h @@ -0,0 +1,304 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import +#import + +#import "NIBlocks.h" + +/** + * For writing code that runs concurrently. + * + * @ingroup NimbusCore + * @defgroup Operations Operations + * + * This collection of NSOperation implementations is meant to provide a set of common + * operations that might be used in an application to offload complex processing to a separate + * thread. + */ + +@protocol NIOperationDelegate; + +/** + * A base implementation of an NSOperation that supports traditional delegation and blocks. + * + * + *

Subclassing

+ * + * A subclass should call the operationDid* methods to notify the delegate on the main thread + * of changes in the operation's state. Calling these methods will notify the delegate and the + * blocks if provided. + * + * @ingroup Operations + */ +@interface NIOperation : NSOperation { +@private + id _delegate; + + NSInteger _tag; + + NSError* _lastError; + +#if NS_BLOCKS_AVAILABLE + // Performed on the main thread. + NIBasicBlock _didStartBlock; + NIBasicBlock _didFinishBlock; + NIErrorBlock _didFailWithErrorBlock; + + // Performed in the operation's thread. + NIBasicBlock _willFinishBlock; +#endif // #if NS_BLOCKS_AVAILABLE +} + +@property (readwrite, assign) id delegate; +@property (readonly, retain) NSError* lastError; +@property (readwrite, assign) NSInteger tag; + + +#if NS_BLOCKS_AVAILABLE + +@property (readwrite, copy) NIBasicBlock didStartBlock; +@property (readwrite, copy) NIBasicBlock didFinishBlock; +@property (readwrite, copy) NIErrorBlock didFailWithErrorBlock; +@property (readwrite, copy) NIBasicBlock willFinishBlock; + +#endif // #if NS_BLOCKS_AVAILABLE + +- (void)operationDidStart; +- (void)operationDidFinish; +- (void)operationDidFailWithError:(NSError *)error; +- (void)operationWillFinish; + +@end + + +/** + * An operation that reads a file from disk. + * + * Provides asynchronous file reading support when added to an NSOperationQueue. + * + * It is recommended to add this operation to a serial NSOperationQueue to avoid overlapping + * disk read attempts. This will noticeably improve performance when loading many files + * from disk at once. + * + * @ingroup Operations + */ +@interface NIReadFileFromDiskOperation : NIOperation { +@private + // [in] + NSString* _pathToFile; + + // [out] + NSData* _data; + id _processedObject; +} + +// Designated initializer. +- (id)initWithPathToFile:(NSString *)pathToFile; + +@property (readwrite, copy) NSString* pathToFile; +@property (readonly, retain) NSData* data; +@property (readwrite, retain) id processedObject; + +@end + + +/** + * The delegate protocol for an NSOperation. + * + * @ingroup Operations + */ +@protocol NIOperationDelegate +@optional + +/** @name [NIOperationDelegate] State Changes */ + +/** The operation has started executing. */ +- (void)operationDidStart:(NSOperation *)operation; + +/** + * The operation is about to complete successfully. + * + * This will not be called if the operation fails. + * + * This will be called from within the operation's runloop and must be thread safe. + */ +- (void)operationWillFinish:(NSOperation *)operation; + +/** + * The operation has completed successfully. + * + * This will not be called if the operation fails. + */ +- (void)operationDidFinish:(NSOperation *)operation; + +/** + * The operation failed in some way and has completed. + * + * operationDidFinish: will not be called. + */ +- (void)operationDidFail:(NSOperation *)operation withError:(NSError *)error; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// NIOperation + +/** @name Delegation */ + +/** + * The delegate through which changes are notified for this operation. + * + * All delegate methods are performed on the main thread. + * + * @fn NIOperation::delegate + */ + + +/** @name Post-Operation Properties */ + +/** + * The error last passed to the didFailWithError notification. + * + * @fn NIOperation::lastError + */ + + +/** @name Identification */ + +/** + * A simple tagging mechanism for identifying operations. + * + * @fn NIOperation::tag + */ + + +#if NS_BLOCKS_AVAILABLE +/** @name Blocks */ + +/** + * The operation has started executing. + * + * Performed on the main thread. + * + * @fn NIOperation::didStartBlock + */ + +/** + * The operation has completed successfully. + * + * This will not be called if the operation fails. + * + * Performed on the main thread. + * + * @fn NIOperation::didFinishBlock + */ + +/** + * The operation failed in some way and has completed. + * + * didFinishBlock will not be executed. + * + * Performed on the main thread. + * + * @fn NIOperation::didFailWithErrorBlock + */ + +/** + * The operation is about to complete successfully. + * + * This will not be called if the operation fails. + * + * Performed in the operation's thread. + * + * @fn NIOperation::willFinishBlock + */ +#endif // #if NS_BLOCKS_AVAILABLE + + +/** + * @name Subclassing + * + * The following methods are provided to aid in subclassing and are not meant to be + * used externally. + */ + +/** + * On the main thread, notify the delegate that the operation has begun. + * + * @fn NIOperation::operationDidStart + */ + +/** + * On the main thread, notify the delegate that the operation has finished. + * + * @fn NIOperation::operationDidFinish + */ + +/** + * On the main thread, notify the delegate that the operation has failed. + * + * @fn NIOperation::operationDidFailWithError: + */ + +/** + * In the operation's thread, notify the delegate that the operation will finish successfully. + * + * @fn NIOperation::operationWillFinish + */ + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// NIReadFileFromDiskOperation + +/** @name Creating an Operation */ + +/** + * Initializes a newly allocated "read from disk" operation with a given path to a file to be read. + * + * @fn NIReadFileFromDiskOperation::initWithPathToFile: + */ + + +/** @name Configuring the Operation */ + +/** + * The path to the file that should be read from disk. + * + * @fn NIReadFileFromDiskOperation::pathToFile + */ + + +/** @name Operation Results */ + +/** + * The data that was read from disk. + * + * Will be nil if the data couldn't be read. + * + * @sa NIOperation::lastError + * @fn NIReadFileFromDiskOperation::data + */ + +/** + * An object created from the data that was read from disk. + * + * Will be nil if the data couldn't be read. + * + * @sa NIOperation::lastError + * @fn NIReadFileFromDiskOperation::processedObject + */ diff --git a/NIOperations.m b/NIOperations.m new file mode 100644 index 0000000..56d41a6 --- /dev/null +++ b/NIOperations.m @@ -0,0 +1,241 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIOperations.h" + +#import "NIDebuggingTools.h" +#import "NIPreprocessorMacros.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@interface NIReadFileFromDiskOperation() + +@property (readwrite, retain) NSData* data; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NIReadFileFromDiskOperation + +@synthesize pathToFile = _pathToFile; +@synthesize data = _data; +@synthesize processedObject = _processedObject; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + NI_RELEASE_SAFELY(_pathToFile); + NI_RELEASE_SAFELY(_data); + NI_RELEASE_SAFELY(_processedObject); + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)initWithPathToFile:(NSString *)pathToFile { + if ((self = [super init])) { + self.pathToFile = pathToFile; + } + return self; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NSOperation + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)main { + NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init]; + + [self operationDidStart]; + + NSError* error = nil; + + self.data = [NSData dataWithContentsOfFile: self.pathToFile + options: 0 + error: &error]; + + + if (nil != error) { + [self operationDidFailWithError:error]; + + } else { + [self operationWillFinish]; + [self operationDidFinish]; + } + + NI_RELEASE_SAFELY(pool); +} + + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@interface NIOperation() + +@property (readwrite, retain) NSError* lastError; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NIOperation + +@synthesize delegate = _delegate; +@synthesize tag = _tag; +@synthesize lastError = _lastError; + +#if NS_BLOCKS_AVAILABLE +@synthesize didStartBlock = _didStartBlock; +@synthesize didFinishBlock = _didFinishBlock; +@synthesize didFailWithErrorBlock = _didFailWithErrorBlock; +@synthesize willFinishBlock = _willFinishBlock; +#endif // #if NS_BLOCKS_AVAILABLE + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + NI_RELEASE_SAFELY(_lastError); + +#if NS_BLOCKS_AVAILABLE + NI_RELEASE_SAFELY(_didStartBlock); + NI_RELEASE_SAFELY(_didFinishBlock); + NI_RELEASE_SAFELY(_didFailWithErrorBlock); + NI_RELEASE_SAFELY(_willFinishBlock); +#endif // #if NS_BLOCKS_AVAILABLE + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Initiate delegate notification from the NSOperation + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)operationDidStart { + [self performSelectorOnMainThread: @selector(onMainThreadOperationDidStart) + withObject: nil + waitUntilDone: [NSThread isMainThread]]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)operationDidFinish { + [self performSelectorOnMainThread: @selector(onMainThreadOperationDidFinish) + withObject: nil + waitUntilDone: [NSThread isMainThread]]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)operationDidFailWithError:(NSError *)error { + self.lastError = error; + + [self performSelectorOnMainThread: @selector(onMainThreadOperationDidFailWithError:) + withObject: error + waitUntilDone: [NSThread isMainThread]]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)operationWillFinish { + if ([self.delegate respondsToSelector:@selector(operationWillFinish:)]) { + [self.delegate operationWillFinish:self]; + } + +#if NS_BLOCKS_AVAILABLE + if (nil != self.willFinishBlock) { + self.willFinishBlock(); + } +#endif // #if NS_BLOCKS_AVAILABLE +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Main Thread + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)onMainThreadOperationDidStart { + // This method should only be called on the main thread. + NIDASSERT([NSThread isMainThread]); + + if ([self.delegate respondsToSelector:@selector(operationDidStart:)]) { + [self.delegate operationDidStart:self]; + } + +#if NS_BLOCKS_AVAILABLE + if (nil != self.didStartBlock) { + self.didStartBlock(); + } +#endif // #if NS_BLOCKS_AVAILABLE +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)onMainThreadOperationDidFinish { + // This method should only be called on the main thread. + NIDASSERT([NSThread isMainThread]); + + if ([self.delegate respondsToSelector:@selector(operationDidFinish:)]) { + [self.delegate operationDidFinish:self]; + } + +#if NS_BLOCKS_AVAILABLE + if (nil != self.didFinishBlock) { + self.didFinishBlock(); + } +#endif // #if NS_BLOCKS_AVAILABLE +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)onMainThreadOperationDidFailWithError:(NSError *)error { + // This method should only be called on the main thread. + NIDASSERT([NSThread isMainThread]); + + if ([self.delegate respondsToSelector:@selector(operationDidFail:withError:)]) { + [self.delegate operationDidFail:self withError:error]; + } + +#if NS_BLOCKS_AVAILABLE + if (nil != self.didFailWithErrorBlock) { + self.didFailWithErrorBlock(error); + } +#endif // #if NS_BLOCKS_AVAILABLE +} + + +@end diff --git a/NIPaths.h b/NIPaths.h new file mode 100644 index 0000000..0a5640c --- /dev/null +++ b/NIPaths.h @@ -0,0 +1,50 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import + +/** + * For creating standard system paths. + * + * @ingroup NimbusCore + * @defgroup Paths Paths + * @{ + */ + +/** + * Create a path with the given bundle and the relative path appended. + * + * @param bundle The bundle to append relativePath to. If nil, [NSBundle mainBundle] + * will be used. + * @param relativePath The relative path to append to the bundle's path. + * + * @returns The bundle path concatenated with the given relative path. + */ +NSString* NIPathForBundleResource(NSBundle* bundle, NSString* relativePath); + +/** + * Create a path with the documents directory and the relative path appended. + * + * @returns The documents path concatenated with the given relative path. + */ +NSString* NIPathForDocumentsResource(NSString* relativePath); + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Paths //////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NIPaths.m b/NIPaths.m new file mode 100644 index 0000000..074e0f6 --- /dev/null +++ b/NIPaths.m @@ -0,0 +1,39 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import "NIPaths.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSString* NIPathForBundleResource(NSBundle* bundle, NSString* relativePath) { + NSString* resourcePath = [(nil == bundle ? [NSBundle mainBundle] : bundle) resourcePath]; + return [resourcePath stringByAppendingPathComponent:relativePath]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +NSString* NIPathForDocumentsResource(NSString* relativePath) { + static NSString* documentsPath = nil; + if (nil == documentsPath) { + NSArray* dirs = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, + NSUserDomainMask, + YES); + documentsPath = [[dirs objectAtIndex:0] retain]; + } + return [documentsPath stringByAppendingPathComponent:relativePath]; +} diff --git a/NIPhotoAlbumScrollView.h b/NIPhotoAlbumScrollView.h new file mode 100644 index 0000000..626e32d --- /dev/null +++ b/NIPhotoAlbumScrollView.h @@ -0,0 +1,378 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import +#import + +#import "NIPhotoScrollView.h" + +/** + * numberOfPhotos will be this value until reloadData is called. + */ +extern const NSInteger NIPhotoAlbumScrollViewUnknownNumberOfPhotos; + +/** + * The default number of pixels on the side of each photo. + */ +extern const CGFloat NIPhotoAlbumScrollViewDefaultPageHorizontalMargin; + +@protocol NIPhotoAlbumScrollViewDataSource; +@protocol NIPhotoAlbumScrollViewDelegate; + +/** + * A paged scroll view that shows a collection of photos. + * + * @ingroup Photos-Views + * + * This view provides a light-weight implementation of a photo viewer, complete with + * pinch-to-zoom and swiping to change photos. It is designed to perform well with + * large sets of photos and large images that are loaded from either the network or + * disk. + * + * It is intended for this view to be used in conjunction with a view controller that + * implements the data source protocol and presents any required chrome. + * + * @see NIToolbarPhotoViewController + */ +@interface NIPhotoAlbumScrollView : UIView < + UIScrollViewDelegate, + NIPhotoScrollViewDelegate> { +@private + UIScrollView* _pagingScrollView; + + // Sets of NIPhotoScrollViews + NSMutableSet* _visiblePages; + NSMutableSet* _recycledPages; + + // Configurable Properties + UIImage* _loadingImage; + CGFloat _pageHorizontalMargin; + BOOL _zoomingIsEnabled; + BOOL _zoomingAboveOriginalSizeIsEnabled; + + // State Information + NSInteger _firstVisiblePageIndexBeforeRotation; + CGFloat _percentScrolledIntoFirstVisiblePage; + BOOL _isModifyingContentOffset; + BOOL _isAnimatingToPhoto; + NSInteger _centerPhotoIndex; + + // Cached Data Source Information + NSInteger _numberOfPages; + + id _dataSource; + id _delegate; +} + + +#pragma mark Configuring Functionality /** @name Configuring Functionality */ + +/** + * Whether zooming is enabled or not. + * + * Regardless of whether this is enabled, only original-sized images will be zoomable. + * This is because we often don't know how large the final image is so we can't + * calculate min and max zoom amounts correctly. + * + * By default this is YES. + */ +@property (nonatomic, readwrite, assign, getter=isZoomingEnabled) BOOL zoomingIsEnabled; + +/** + * Whether small photos can be zoomed at least until they fit the screen. + * + * @see NIPhotoScrollView::zoomingAboveOriginalSizeIsEnabled + * + * By default this is YES. + */ +@property (nonatomic, readwrite, assign, getter=isZoomingAboveOriginalSizeEnabled) BOOL zoomingAboveOriginalSizeIsEnabled; + + +#pragma mark Data Source /** @name Data Source */ + +/** + * The data source for this photo album view. + * + * This is the only means by which this photo album view acquires any information about the + * album to be displayed. + */ +@property (nonatomic, readwrite, assign) id dataSource; + +/** + * Force the view to reload its data by asking the data source for information. + * + * This must be called at least once after dataSource has been set in order for the view + * to gather any presentable information. + * + * This method is expensive. It will reset the state of the view and remove all existing + * pages before requesting the new information from the data source. + */ +- (void)reloadData; + + +#pragma mark Delegate /** @name Delegate */ + +/** + * The delegate for this photo album view. + * + * Any user interactions or state changes are sent to the delegate through this property. + */ +@property (nonatomic, readwrite, assign) id delegate; + + +#pragma mark Configuring Presentation /** @name Configuring Presentation */ + +/** + * An image that is displayed while the photo is loading. + * + * This photo will be presented if no image is returned in the data source's implementation + * of photoAlbumScrollView:photoAtIndex:photoSize:isLoading:. + * + * Zooming is disabled when showing a loading image, regardless of the state of zoomingIsEnabled. + * + * By default this is nil. + */ +@property (nonatomic, readwrite, retain) UIImage* loadingImage; + +/** + * The number of pixels on either side of each photo page. + * + * The space between each photo will be 2x this value. + * + * By default this is NIPhotoAlbumScrollViewDefaultPageHorizontalMargin. + */ +@property (nonatomic, readwrite, assign) CGFloat pageHorizontalMargin; + + +#pragma mark State /** @name State */ + +/** + * The current center photo index. + * + * This is a zero-based value. If you intend to use this in a label such as "photo ## of n" be + * sure to add one to this value. + * + * Setting this value directly will do so without animation. + */ +@property (nonatomic, readwrite, assign) NSInteger centerPhotoIndex; + +/** + * Change the center photo index with optional animation. + */ +- (void)setCenterPhotoIndex:(NSInteger)centerPhotoIndex animated:(BOOL)animated; + +/** + * The total number of photos in this photo album view, as gathered from the data source. + * + * This value is cached after reloadData has been called. + * + * Until reloadData is called the first time, numberOfPhotos will be + * NIPhotoAlbumScrollViewUnknownNumberOfPhotos. + */ +@property (nonatomic, readonly, assign) NSInteger numberOfPhotos; + + +#pragma mark Changing the Visible Photo /** @name Changing the Visible Photo */ + +/** + * Returns YES if there is a next photo. + */ +- (BOOL)hasNext; + +/** + * Returns YES if there is a previous photo. + */ +- (BOOL)hasPrevious; + +/** + * Move to the next photo if there is one. + */ +- (void)moveToNextAnimated:(BOOL)animated; + +/** + * Move to the previous photo if there is one. + */ +- (void)moveToPreviousAnimated:(BOOL)animated; + + +#pragma mark Notifying the View of Loaded Photos /** @name Notifying the View of Loaded Photos */ + +/** + * Notify the scroll view that a photo has been loaded at a given index. + * + * You should notify the completed loading of thumbnails as well. Calling this method + * is fairly lightweight and will only update the images of the visible pages. Err on the + * side of calling this method too much rather than too little. + * + * The photo at the given index will only be replaced with the given image if photoSize + * is of a higher quality than the currently-displayed photo's size. + */ +- (void)didLoadPhoto: (UIImage *)image + atIndex: (NSInteger)photoIndex + photoSize: (NIPhotoScrollViewPhotoSize)photoSize; + + +#pragma mark Rotating the Scroll View /** @name Rotating the Scroll View */ + +/** + * Stores the current state of the scroll view in preparation for rotation. + * + * This must be called in conjunction with willAnimateRotationToInterfaceOrientation:duration: + * in the methods by the same name from the view controller containing this view. + */ +- (void)willRotateToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation + duration: (NSTimeInterval)duration; + +/** + * Updates the frame of the scroll view while maintaining the current visible page's state. + */ +- (void)willAnimateRotationToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation + duration: (NSTimeInterval)duration; + + +@end + + +/** + * The photo album scroll data source. + * + * @ingroup Photos-Protocols + * + * This data source emphasizes speed and memory efficiency by requesting images only when + * they're needed and encouraging immediate responses from the data source implementation. + * + * @see NIPhotoAlbumScrollView + */ +@protocol NIPhotoAlbumScrollViewDataSource + +@required + + +#pragma mark Fetching Required Album Information /** @name [NIPhotoAlbumScrollViewDataSource] Fetching Required Album Information */ + +/** + * Fetches the total number of photos in the scroll view. + * + * The value returned in this method will be cached by the scroll view until reloadData + * is called again. + */ +- (NSInteger)numberOfPhotosInPhotoScrollView:(NIPhotoAlbumScrollView *)photoScrollView; + +/** + * Fetches the highest-quality image available for the photo at the given index. + * + * Your goal should be to make this implementation return as fast as possible. Avoid + * hitting the disk or blocking on a network request. Aim to load images asynchronously. + * + * If you already have the highest-quality image in memory (like in an NIImageMemoryCache), + * then you can simply return the image and set photoSize to be + * NIPhotoScrollViewPhotoSizeOriginal. + * + * If the highest-quality image is not available when this method is called then you should + * spin off an asynchronous operation to load the image and set isLoading to YES. + * + * If you have a thumbnail in memory but not the full-size image yet, then you should return + * the thumbnail, set isLoading to YES, and set photoSize to NIPhotoScrollViewPhotoSizeThumbnail. + * + * Once the high-quality image finishes loading, call didLoadPhoto:atIndex:photoSize: with + * the image. + * + * This method will be called to prefetch the next and previous photos in the scroll view. + * The currently displayed photo will always be requested first. + * + * @attention The photo scroll view does not hold onto the UIImages for very long at all. + * It is up to the controller to decide on an adequate caching policy to ensure + * that images are kept in memory through the life of the photo album. + * In your implementation of the data source you should prioritize thumbnails + * being kept in memory over full-size images. When a memory warning is received, + * the original photos should be relinquished from memory first. + */ +- (UIImage *)photoAlbumScrollView: (NIPhotoAlbumScrollView *)photoAlbumScrollView + photoAtIndex: (NSInteger)photoIndex + photoSize: (NIPhotoScrollViewPhotoSize *)photoSize + isLoading: (BOOL *)isLoading + originalPhotoDimensions: (CGSize *)originalPhotoDimensions; + + +@optional + + +#pragma mark Optimizing Data Retrieval /** @name [NIPhotoAlbumScrollViewDataSource] Optimizing Data Retrieval */ + +/** + * Called when you should cancel any asynchronous loading requests for the given photo. + * + * When a photo is not immediately visible this method is called to allow the data + * source to minimize the number of active asynchronous operations in place. + * + * This method is optional, though recommended because it focuses the device's processing + * power on the most immediately accessible photos. + */ +- (void)photoAlbumScrollView: (NIPhotoAlbumScrollView *)photoAlbumScrollView + stopLoadingPhotoAtIndex: (NSInteger)photoIndex; + + +@end + + +/** + * The photo album scroll view delegate. + * + * @ingroup Photos-Protocols + * @see NIPhotoAlbumScrollView + */ +@protocol NIPhotoAlbumScrollViewDelegate + +@optional + +#pragma mark Scrolling and Zooming /** @name [NIPhotoAlbumScrollViewDelegate] Scrolling and Zooming */ + +/** + * The user is scrolling between two photos. + */ +- (void)photoAlbumScrollViewDidScroll:(NIPhotoAlbumScrollView *)photoAlbumScrollView; + +/** + * The user double-tapped to zoom in or out. + */ +- (void)photoAlbumScrollView: (NIPhotoAlbumScrollView *)photoAlbumScrollView + didZoomIn: (BOOL)didZoomIn; + + +#pragma mark Changing Pages /** @name [NIPhotoAlbumScrollViewDelegate] Changing Pages */ + +/** + * The current page has changed. + * + * photoAlbumScrollView.currentCenterPhotoIndex will reflect the changed photo index. + */ +- (void)photoAlbumScrollViewDidChangePages:(NIPhotoAlbumScrollView *)photoAlbumScrollView; + + +#pragma mark Data Availability /** @name [NIPhotoAlbumScrollViewDelegate] Data Availability */ + +/** + * The next photo in the album has been loaded and is ready to be displayed. + */ +- (void)photoAlbumScrollViewDidLoadNextPhoto:(NIPhotoAlbumScrollView *)photoAlbumScrollView; + +/** + * The previous photo in the album has been loaded and is ready to be displayed. + */ +- (void)photoAlbumScrollViewDidLoadPreviousPhoto:(NIPhotoAlbumScrollView *)photoAlbumScrollView; + + +@end diff --git a/NIPhotoAlbumScrollView.m b/NIPhotoAlbumScrollView.m new file mode 100644 index 0000000..c2688df --- /dev/null +++ b/NIPhotoAlbumScrollView.m @@ -0,0 +1,627 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIPhotoAlbumScrollView.h" + +#import "NIPhotoScrollView.h" +#import "NimbusCore.h" + +const NSInteger NIPhotoAlbumScrollViewUnknownNumberOfPhotos = -1; +const CGFloat NIPhotoAlbumScrollViewDefaultPageHorizontalMargin = 10; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NIPhotoAlbumScrollView + +@synthesize loadingImage = _loadingImage; +@synthesize pageHorizontalMargin = _pageHorizontalMargin; +@synthesize zoomingIsEnabled = _zoomingIsEnabled; +@synthesize zoomingAboveOriginalSizeIsEnabled = _zoomingAboveOriginalSizeIsEnabled; +@synthesize dataSource = _dataSource; +@synthesize delegate = _delegate; +@synthesize centerPhotoIndex = _centerPhotoIndex; +@synthesize numberOfPhotos = _numberOfPages; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + _pagingScrollView = nil; + + NI_RELEASE_SAFELY(_loadingImage); + + NI_RELEASE_SAFELY(_visiblePages); + NI_RELEASE_SAFELY(_recycledPages); + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)initWithFrame:(CGRect)frame { + if ((self = [super initWithFrame:frame])) { + // Default state. + self.pageHorizontalMargin = NIPhotoAlbumScrollViewDefaultPageHorizontalMargin; + self.zoomingIsEnabled = YES; + self.zoomingAboveOriginalSizeIsEnabled = YES; + + _firstVisiblePageIndexBeforeRotation = -1; + _percentScrolledIntoFirstVisiblePage = -1; + _centerPhotoIndex = -1; + _numberOfPages = NIPhotoAlbumScrollViewUnknownNumberOfPhotos; + + _pagingScrollView = [[[UIScrollView alloc] initWithFrame:frame] autorelease]; + _pagingScrollView.pagingEnabled = YES; + + _pagingScrollView.autoresizingMask = (UIViewAutoresizingFlexibleWidth + | UIViewAutoresizingFlexibleHeight); + + _pagingScrollView.delegate = self; + + // Ensure that empty areas of the scroll view are draggable. + _pagingScrollView.backgroundColor = [UIColor blackColor]; + + _pagingScrollView.showsVerticalScrollIndicator = NO; + _pagingScrollView.showsHorizontalScrollIndicator = NO; + + [self addSubview:_pagingScrollView]; + } + return self; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)notifyDelegatePhotoDidLoadAtIndex:(NSInteger)photoIndex { + if (photoIndex == (self.centerPhotoIndex + 1) + && [self.delegate respondsToSelector:@selector(photoAlbumScrollViewDidLoadNextPhoto:)]) { + [self.delegate photoAlbumScrollViewDidLoadNextPhoto:self]; + + } else if (photoIndex == (self.centerPhotoIndex - 1) + && [self.delegate respondsToSelector:@selector(photoAlbumScrollViewDidLoadPreviousPhoto:)]) { + [self.delegate photoAlbumScrollViewDidLoadPreviousPhoto:self]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Page Layout + + +// The following three methods are from Apple's ImageScrollView example application and have +// been used here because they are well-documented and concise. + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGRect)frameForPagingScrollView { + CGRect frame = self.bounds; + + // We make the paging scroll view a little bit wider on the side edges so that there + // there is space between the pages when flipping through them. + frame.origin.x -= self.pageHorizontalMargin; + frame.size.width += (2 * self.pageHorizontalMargin); + + return frame; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGRect)frameForPageAtIndex:(NSInteger)pageIndex { + // We have to use our paging scroll view's bounds, not frame, to calculate the page + // placement. When the device is in landscape orientation, the frame will still be in + // portrait because the pagingScrollView is the root view controller's view, so its + // frame is in window coordinate space, which is never rotated. Its bounds, however, + // will be in landscape because it has a rotation transform applied. + CGRect bounds = _pagingScrollView.bounds; + CGRect pageFrame = bounds; + + // We need to counter the extra spacing added to the paging scroll view in + // frameForPagingScrollView: + pageFrame.size.width -= self.pageHorizontalMargin * 2; + pageFrame.origin.x = (bounds.size.width * pageIndex) + self.pageHorizontalMargin; + + return pageFrame; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGSize)contentSizeForPagingScrollView { + // We have to use the paging scroll view's bounds to calculate the contentSize, for the + // same reason outlined above. + CGRect bounds = _pagingScrollView.bounds; + return CGSizeMake(bounds.size.width * _numberOfPages, bounds.size.height); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Visible Page Management + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NIPhotoScrollView *)dequeueRecycledPage { + NIPhotoScrollView* page = [_recycledPages anyObject]; + + if (nil != page) { + // Ensure that this object sticks around for this runloop. + [[page retain] autorelease]; + + [_recycledPages removeObject:page]; + + // Reset this page to a blank slate state. + [page prepareForReuse]; + } + + return page; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (BOOL)isDisplayingPageForIndex:(NSInteger)pageIndex { + BOOL foundPage = NO; + + // There will never be more than 3 visible pages in this array, so this lookup is + // effectively O(C) constant time. + for (NIPhotoScrollView* page in _visiblePages) { + if (page.photoIndex == pageIndex) { + foundPage = YES; + break; + } + } + + return foundPage; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSInteger)currentVisiblePageIndex { + CGPoint contentOffset = _pagingScrollView.contentOffset; + CGSize boundsSize = _pagingScrollView.bounds.size; + + // Whatever image is currently displayed in the center of the screen is the currently + // visible image. + return boundi((NSInteger)(floorf((contentOffset.x + boundsSize.width / 2) / boundsSize.width) + + 0.5f), + 0, self.numberOfPhotos); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSRange)visiblePageRange { + if (0 >= _numberOfPages) { + return NSMakeRange(0, 0); + } + + NSInteger currentVisiblePageIndex = [self currentVisiblePageIndex]; + + int firstVisiblePageIndex = boundi(currentVisiblePageIndex - 1, 0, _numberOfPages - 1); + int lastVisiblePageIndex = boundi(currentVisiblePageIndex + 1, 0, _numberOfPages - 1); + + return NSMakeRange(firstVisiblePageIndex, lastVisiblePageIndex - firstVisiblePageIndex + 1); +} + + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)configurePage:(NIPhotoScrollView *)page forIndex:(NSInteger)pageIndex { + page.photoIndex = pageIndex; + page.frame = [self frameForPageAtIndex:pageIndex]; + + // When we ask the data source for the image we expect the following to happen: + // 1) If the data source has any image at this index, it should return it and set the + // photoSize accordingly. + // 2) If the returned photo is not the highest quality available, the data source should + // start loading it and set isLoading to YES. + // 3) If no photo was available, then the data source should start loading the photo + // at its highest quality and nil should be returned. loadingImage will be used in + // this case. + NIPhotoScrollViewPhotoSize photoSize = NIPhotoScrollViewPhotoSizeUnknown; + BOOL isLoading = NO; + CGSize originalPhotoDimensions = CGSizeZero; + UIImage* image = [_dataSource photoAlbumScrollView: self + photoAtIndex: pageIndex + photoSize: &photoSize + isLoading: &isLoading + originalPhotoDimensions: &originalPhotoDimensions]; + + page.photoDimensions = originalPhotoDimensions; + + if (nil == image) { + page.zoomingIsEnabled = NO; + [page setImage:self.loadingImage photoSize:NIPhotoScrollViewPhotoSizeUnknown]; + + } else { + page.zoomingIsEnabled = ([self isZoomingEnabled] + && (NIPhotoScrollViewPhotoSizeOriginal == photoSize)); + if (photoSize > page.photoSize) { + [page setImage:image photoSize:photoSize]; + + if (NIPhotoScrollViewPhotoSizeOriginal == photoSize) { + [self notifyDelegatePhotoDidLoadAtIndex:pageIndex]; + } + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)resetPage:(NIPhotoScrollView *)page { + page.zoomScale = page.minimumZoomScale; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)resetSurroundingPages { + for (NIPhotoScrollView* page in _visiblePages) { + if (page.photoIndex != self.centerPhotoIndex) { + [self resetPage:page]; + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)displayPageAtIndex:(NSInteger)pageIndex { + NIPhotoScrollView* page = [self dequeueRecycledPage]; + + if (nil == page) { + page = [[[NIPhotoScrollView alloc] init] autorelease]; + page.photoScrollViewDelegate = self; + page.zoomingAboveOriginalSizeIsEnabled = [self isZoomingAboveOriginalSizeEnabled]; + } + + // This will only be called once each time the page is shown. + [self configurePage:page forIndex:pageIndex]; + + [_pagingScrollView addSubview:page]; + [_visiblePages addObject:page]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)updateVisiblePages { + NSInteger oldCenterPhotoIndex = self.centerPhotoIndex; + + NSRange visiblePageRange = [self visiblePageRange]; + + _centerPhotoIndex = [self currentVisiblePageIndex]; + + // Recycle no-longer-visible pages. + for (NIPhotoScrollView* page in _visiblePages) { + if (!NSLocationInRange(page.photoIndex, visiblePageRange)) { + [_recycledPages addObject:page]; + [page removeFromSuperview]; + + // Give the data source the opportunity to kill any asynchronous operations for this + // now-recycled page. + if ([_dataSource respondsToSelector: + @selector(photoAlbumScrollView:stopLoadingPhotoAtIndex:)]) { + [_dataSource photoAlbumScrollView: self + stopLoadingPhotoAtIndex: page.photoIndex]; + } + } + } + [_visiblePages minusSet:_recycledPages]; + + // Prioritize displaying the currently visible page. + if (![self isDisplayingPageForIndex:_centerPhotoIndex]) { + [self displayPageAtIndex:_centerPhotoIndex]; + } + + // Add missing pages. + for (int pageIndex = visiblePageRange.location; + pageIndex < NSMaxRange(visiblePageRange); ++pageIndex) { + if (![self isDisplayingPageForIndex:pageIndex]) { + [self displayPageAtIndex:pageIndex]; + } + } + + if (oldCenterPhotoIndex != _centerPhotoIndex + && [self.delegate respondsToSelector:@selector(photoAlbumScrollViewDidChangePages:)]) { + [self.delegate photoAlbumScrollViewDidChangePages:self]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UIScrollViewDelegate + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setFrame:(CGRect)frame { + // We have to modify this method because it eventually leads to changing the content offset + // programmatically. When this happens we end up getting a scrollViewDidScroll: message + // during which we do not want to modify the visible pages because this is handled elsewhere. + + + // Don't lose the previous modification state if an animation is occurring when the + // frame changes, like when the device changes orientation. + BOOL wasModifyingContentOffset = _isModifyingContentOffset; + _isModifyingContentOffset = YES; + [super setFrame:frame]; + _isModifyingContentOffset = wasModifyingContentOffset; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + if (!_isModifyingContentOffset) { + // This method is called repeatedly as the user scrolls so updateVisiblePages must be + // leight-weight enough not to noticeably impact performance. + [self updateVisiblePages]; + + if ([self.delegate respondsToSelector:@selector(photoAlbumScrollViewDidScroll:)]) { + [self.delegate photoAlbumScrollViewDidScroll:self]; + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { + if (!decelerate) { + [self resetSurroundingPages]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + [self resetSurroundingPages]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NIPhotoScrollViewDelegate + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)photoScrollViewDidDoubleTapToZoom: (NIPhotoScrollView *)photoScrollView + didZoomIn: (BOOL)didZoomIn { + if ([self.delegate respondsToSelector:@selector(photoAlbumScrollView:didZoomIn:)]) { + [self.delegate photoAlbumScrollView:self didZoomIn:didZoomIn]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Public Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)reloadData { + NIDASSERT(nil != _dataSource); + + // Remove any visible pages from the view before we release the sets. + for (NIPhotoScrollView* page in _visiblePages) { + [page removeFromSuperview]; + } + + NI_RELEASE_SAFELY(_visiblePages); + NI_RELEASE_SAFELY(_recycledPages); + + // Reset the state of the scroll view. + _isModifyingContentOffset = YES; + _pagingScrollView.contentSize = self.bounds.size; + _pagingScrollView.contentOffset = CGPointZero; + _isModifyingContentOffset = NO; + + // If there is no data source then we can't do anything particularly interesting. + if (nil == _dataSource) { + return; + } + + _visiblePages = [[NSMutableSet alloc] init]; + _recycledPages = [[NSMutableSet alloc] init]; + + // Cache the number of pages. + _numberOfPages = [_dataSource numberOfPhotosInPhotoScrollView:self]; + + _pagingScrollView.frame = [self frameForPagingScrollView]; + + // The content size is calculated based on the number of pages and the scroll view frame. + _pagingScrollView.contentSize = [self contentSizeForPagingScrollView]; + + // Begin requesting the photo information from the data source. + [self updateVisiblePages]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didLoadPhoto: (UIImage *)image + atIndex: (NSInteger)photoIndex + photoSize: (NIPhotoScrollViewPhotoSize)photoSize { + for (NIPhotoScrollView* page in _visiblePages) { + if (page.photoIndex == photoIndex) { + + // Only replace the photo if it's of a higher quality than one we're already showing. + if (photoSize > page.photoSize) { + [page setImage:image photoSize:photoSize]; + + page.zoomingIsEnabled = ([self isZoomingEnabled] + && (NIPhotoScrollViewPhotoSizeOriginal == photoSize)); + + // Notify the delegate that the photo has been loaded. + if (NIPhotoScrollViewPhotoSizeOriginal == photoSize) { + [self notifyDelegatePhotoDidLoadAtIndex:photoIndex]; + } + } + break; + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)willRotateToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation + duration: (NSTimeInterval)duration { + // Here, our pagingScrollView bounds have not yet been updated for the new interface + // orientation. This is a good place to calculate the content offset that we will + // need in the new orientation. + CGFloat offset = _pagingScrollView.contentOffset.x; + CGFloat pageWidth = _pagingScrollView.bounds.size.width; + + if (offset >= 0) { + _firstVisiblePageIndexBeforeRotation = floorf(offset / pageWidth); + _percentScrolledIntoFirstVisiblePage = ((offset + - (_firstVisiblePageIndexBeforeRotation * pageWidth)) + / pageWidth); + + } else { + _firstVisiblePageIndexBeforeRotation = 0; + _percentScrolledIntoFirstVisiblePage = offset / pageWidth; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)willAnimateRotationToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation + duration: (NSTimeInterval)duration { + BOOL wasModifyingContentOffset = _isModifyingContentOffset; + + // Recalculate contentSize based on current orientation. + _isModifyingContentOffset = YES; + _pagingScrollView.contentSize = [self contentSizeForPagingScrollView]; + _isModifyingContentOffset = wasModifyingContentOffset; + + // adjust frames and configuration of each visible page. + for (NIPhotoScrollView* page in _visiblePages) { + [page setFrameAndMaintainZoomAndCenter:[self frameForPageAtIndex:page.photoIndex]]; + } + + // Adjust contentOffset to preserve page location based on values collected prior to location. + CGFloat pageWidth = _pagingScrollView.bounds.size.width; + CGFloat newOffset = ((_firstVisiblePageIndexBeforeRotation * pageWidth) + + (_percentScrolledIntoFirstVisiblePage * pageWidth)); + _isModifyingContentOffset = YES; + _pagingScrollView.contentOffset = CGPointMake(newOffset, 0); + _isModifyingContentOffset = wasModifyingContentOffset; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setZoomingAboveOriginalSizeIsEnabled:(BOOL)enabled { + _zoomingAboveOriginalSizeIsEnabled = enabled; + + for (NIPhotoScrollView* page in _visiblePages) { + page.zoomingAboveOriginalSizeIsEnabled = enabled; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (BOOL)hasNext { + return (self.centerPhotoIndex < self.numberOfPhotos - 1); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (BOOL)hasPrevious { + return self.centerPhotoIndex > 0; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didAnimateToPage:(NSNumber *)photoIndex { + _isAnimatingToPhoto = NO; + + // Reset the content offset once the animation completes, just to be sure that the + // viewer sits on a page bounds even if we rotate the device while animating. + CGPoint offset = [self frameForPageAtIndex:[photoIndex intValue]].origin; + offset.x -= self.pageHorizontalMargin; + + _isModifyingContentOffset = YES; + _pagingScrollView.contentOffset = offset; + _isModifyingContentOffset = NO; + + [self updateVisiblePages]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)moveToPageAtIndex:(NSInteger)photoIndex animated:(BOOL)animated { + if (_isAnimatingToPhoto) { + // Don't allow re-entry for sliding animations. + return; + } + + CGPoint offset = [self frameForPageAtIndex:photoIndex].origin; + offset.x -= self.pageHorizontalMargin; + + _isModifyingContentOffset = YES; + [_pagingScrollView setContentOffset:offset animated:animated]; + + NSNumber* photoIndexNumber = [NSNumber numberWithInt:photoIndex]; + if (animated) { + _isAnimatingToPhoto = YES; + SEL selector = @selector(didAnimateToPage:); + [NSObject cancelPreviousPerformRequestsWithTarget: self]; + + // When the animation is finished we reset the content offset just in case the frame + // changes while we're animating (like when rotating the device). To do this we need + // to know the destination index for the animation. + [self performSelector: selector + withObject: photoIndexNumber + afterDelay: 0.4]; + + } else { + [self didAnimateToPage:photoIndexNumber]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)moveToNextAnimated:(BOOL)animated { + if ([self hasNext]) { + NSInteger pageIndex = self.centerPhotoIndex + 1; + + [self moveToPageAtIndex:pageIndex animated:animated]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)moveToPreviousAnimated:(BOOL)animated { + if ([self hasPrevious]) { + NSInteger pageIndex = self.centerPhotoIndex - 1; + + [self moveToPageAtIndex:pageIndex animated:animated]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setCenterPhotoIndex:(NSInteger)centerPhotoIndex animated:(BOOL)animated { + [self moveToPageAtIndex:centerPhotoIndex animated:animated]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setCenterPhotoIndex:(NSInteger)centerPhotoIndex { + [self setCenterPhotoIndex:centerPhotoIndex animated:NO]; +} + + +@end diff --git a/NIPhotoScrollView.h b/NIPhotoScrollView.h new file mode 100644 index 0000000..712b57e --- /dev/null +++ b/NIPhotoScrollView.h @@ -0,0 +1,190 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import +#import + +/** + * Contextual information about the size of the photo. + */ +typedef enum { + // Unknown photo size. + NIPhotoScrollViewPhotoSizeUnknown, + + // A smaller version of the image. + NIPhotoScrollViewPhotoSizeThumbnail, + + // The full-size image. + NIPhotoScrollViewPhotoSizeOriginal, +} NIPhotoScrollViewPhotoSize; + +@protocol NIPhotoScrollViewDelegate; + +/** + * A single photo view that supports zooming and rotation. + * + * @ingroup Photos-Views + */ +@interface NIPhotoScrollView : UIScrollView < + UIScrollViewDelegate> { +@private + // The photo view to be zoomed. + UIImageView* _imageView; + + // Photo Album State + NSInteger _photoIndex; + + // Photo Information + NIPhotoScrollViewPhotoSize _photoSize; + CGSize _photoDimensions; + + // Configurable State + BOOL _zoomingIsEnabled; + BOOL _zoomingAboveOriginalSizeIsEnabled; + + UITapGestureRecognizer* _doubleTapGestureRecognizer; + + id _photoScrollViewDelegate; +} + +#pragma mark Configuring Functionality /** @name Configuring Functionality */ + +/** + * Whether the photo is allowed to be zoomed. + * + * By default this is YES. + */ +@property (nonatomic, readwrite, assign, getter=isZoomingEnabled) BOOL zoomingIsEnabled; + +/** + * Whether small photos can be zoomed at least until they fit the screen. + * + * If this is disabled, images smaller than the view size can not be zoomed in beyond + * their original dimensions. + * + * If this is enabled, images smaller than the view size can be zoomed in only until + * they fit the view bounds. + * + * The default behavior in Photos.app allows small photos to be zoomed in. + * + * @attention This will allow photos to be zoomed in even if they don't have any more + * pixels to show, causing the photo to blur. This can look ok for photographs, + * but might not look ok for software design mockups. + * + * By default this is YES. + */ +@property (nonatomic, readwrite, assign, getter=isZoomingAboveOriginalSizeEnabled) BOOL zoomingAboveOriginalSizeIsEnabled; + +/** + * Whether double-tapping zooms in and out of the image. + * + * Available on iOS 3.2 and later. + * + * By default this is YES. + */ +@property (nonatomic, readwrite, assign, getter=isDoubleTapToZoomEnabled) BOOL doubleTapToZoomIsEnabled; + + +#pragma mark State /** @name State */ + +/** + * The currently-displayed photo. + */ +- (UIImage *)image; + +/** + * The index of this photo within a photo album. + */ +@property (nonatomic, readwrite, assign) NSInteger photoIndex; + +/** + * The current size of the photo. + * + * This is used to replace the photo only with successively higher-quality versions. + */ +@property (nonatomic, readwrite, assign) NIPhotoScrollViewPhotoSize photoSize; + +/** + * The largest dimensions of the photo. + * + * This is used to show the thumbnail at the final image size in case the final image size + * is smaller than the album's frame. Without this value we have to assume that the thumbnail + * will take up the full screen. If the final image doesn't take up the full screen, then + * the photo view will appear to "snap" to the smaller full-size image when the final image + * does load. + * + * CGSizeZero is used to signify an unknown final photo dimension. + */ +@property (nonatomic, readwrite, assign) CGSize photoDimensions; + + +#pragma mark Modifying State /** @name Modifying State */ + +/** + * Remove image and reset the zoom scale. + */ +- (void)prepareForReuse; + +/** + * Set a new photo with a specific size. + * + * If image is nil then the photoSize will be overridden as NIPhotoScrollViewPhotoSizeUnknown. + * + * Resets the current zoom levels and zooms to fit the image. + */ +- (void)setImage:(UIImage *)image photoSize:(NIPhotoScrollViewPhotoSize)photoSize; + + +#pragma mark Saving/Restoring Offset and Scale /** @name Saving/Restoring Offset and Scale */ + +/** + * Set the frame of the view while maintaining the zoom and center of the scroll view. + */ +- (void)setFrameAndMaintainZoomAndCenter:(CGRect)frame; + + +#pragma mark Photo Scroll View Delegate /** @name Photo Scroll View Delegate */ + +/** + * The photo scroll view delegate. + */ +@property (nonatomic, readwrite, assign) id photoScrollViewDelegate; + + +@end + + +/** + * The photo scroll view delegate. + * + * @ingroup Photos-Protocols + */ +@protocol NIPhotoScrollViewDelegate + +@optional + +#pragma mark Zooming /** @name [NIPhotoScrollViewDelegate] Zooming */ + +/** + * The user has double-tapped the photo to zoom either in or out. + * + * @param photoScrollView The photo scroll view that was tapped. + * @param didZoomIn YES if the photo was zoomed in. NO if the photo was zoomed out. + */ +- (void)photoScrollViewDidDoubleTapToZoom: (NIPhotoScrollView *)photoScrollView + didZoomIn: (BOOL)didZoomIn; + +@end diff --git a/NIPhotoScrollView.m b/NIPhotoScrollView.m new file mode 100644 index 0000000..5d183dc --- /dev/null +++ b/NIPhotoScrollView.m @@ -0,0 +1,505 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIPhotoScrollView.h" + +#import "NimbusCore.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NIPhotoScrollView + +@synthesize photoIndex = _photoIndex; +@synthesize photoSize = _photoSize; +@synthesize photoDimensions = _photoDimensions; +@synthesize zoomingIsEnabled = _zoomingIsEnabled; +@synthesize zoomingAboveOriginalSizeIsEnabled = _zoomingAboveOriginalSizeIsEnabled; +@synthesize photoScrollViewDelegate = _photoScrollViewDelegate; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + NI_RELEASE_SAFELY(_doubleTapGestureRecognizer); + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)initWithFrame:(CGRect)frame { + if ((self = [super initWithFrame:frame])) { + // Disable the scroll indicators. + self.showsVerticalScrollIndicator = NO; + self.showsHorizontalScrollIndicator = NO; + + // Photo viewers should feel sticky when you're panning around, not smooth and slippery + // like a UITableView. + self.decelerationRate = UIScrollViewDecelerationRateFast; + + // Ensure that empty areas of the scroll view are draggable. + self.backgroundColor = [UIColor blackColor]; + + // We implement viewForZoomingInScrollView: and return the image view for zooming. + self.delegate = self; + + + // Default configuration. + self.zoomingIsEnabled = YES; + self.zoomingAboveOriginalSizeIsEnabled = YES; + self.doubleTapToZoomIsEnabled = YES; + + + // Autorelease so that we don't have to worry about releasing it in dealloc. + _imageView = [[[UIImageView alloc] initWithFrame:CGRectZero] autorelease]; + + // Increases the retain count to 1. The image view will be released when this view + // is released. + [self addSubview:_imageView]; + } + return self; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGFloat)scaleForSize:(CGSize)size boundsSize:(CGSize)boundsSize useMinimalScale:(BOOL)minimalScale { + CGFloat xScale = boundsSize.width / size.width; // The scale needed to perfectly fit the image width-wise. + CGFloat yScale = boundsSize.height / size.height; // The scale needed to perfectly fit the image height-wise. + CGFloat minScale = minimalScale ? MIN(xScale, yScale) : MAX(xScale, yScale); // Use the minimum of these to allow the image to become fully visible, or the maximum to get fullscreen size + + return minScale; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Calculate the min and max scale for the given dimensions and photo size. + * + * minScale will fit the photo to the bounds, unless it is too small in which case it will + * show the image at a 1-to-1 resolution. + * + * maxScale will be whatever value shows the image at a 1-to-1 resolution, UNLESS + * isZoomingAboveOriginalSizeEnabled is enabled, in which case maxScale will be calculated + * such that the image completely fills the bounds. + * + * Exception: If the photo size is unknown (this is a loading image, for example) then + * the minimum scale will be set without considering the screen scale. This allows the + * loading image to draw with its own image scale if it's a high-res @2x image. + */ +- (void)minAndMaxScaleForDimensions: (CGSize)dimensions + boundsSize: (CGSize)boundsSize + photoSize: (NIPhotoScrollViewPhotoSize)photoSize + minScale: (CGFloat *)pMinScale + maxScale: (CGFloat *)pMaxScale { + NIDASSERT(nil != pMinScale); + NIDASSERT(nil != pMaxScale); + if (nil == pMinScale + || nil == pMaxScale) { + return; + } + + CGFloat minScale = [self scaleForSize: dimensions + boundsSize: boundsSize + useMinimalScale: YES]; + + // On high resolution screens we have double the pixel density, so we will be seeing + // every pixel if we limit the maximum zoom scale to 0.5. + // If the photo size is unknown, it's likely that we're showing the loading image and + // don't want to shrink it down with the zoom because it should be a scaled image. + CGFloat maxScale = ((NIPhotoScrollViewPhotoSizeUnknown == photoSize) + ? 1 + : (1.0f / NIScreenScale())); + + if (NIPhotoScrollViewPhotoSizeThumbnail != photoSize) { + // Don't let minScale exceed maxScale. (If the image is smaller than the screen, we + // don't want to force it to be zoomed.) + minScale = MIN(minScale, maxScale); + } + + // At this point if the image is small, then minScale and maxScale will be the same because + // we don't want to allow the photo to be zoomed. + + // If zooming above the original size IS enabled, however, expand the max zoom to + // whatever value would make the image fit the view perfectly. + if ([self isZoomingAboveOriginalSizeEnabled]) { + CGFloat idealMaxScale = [self scaleForSize: dimensions + boundsSize: boundsSize + useMinimalScale: NO]; + maxScale = MAX(maxScale, idealMaxScale); + } + + *pMinScale = minScale; + *pMaxScale = maxScale; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setMaxMinZoomScalesForCurrentBounds { + CGSize imageSize = _imageView.bounds.size; + + // Avoid crashing if the image has no dimensions. + NIDASSERT(imageSize.width > 0 && imageSize.height > 0); + if (imageSize.width <= 0 || imageSize.height <= 0) { + self.maximumZoomScale = 1; + self.minimumZoomScale = 1; + return; + } + + // The following code is from Apple's ImageScrollView example application and has been used + // here because it is well-documented and concise. + + CGSize boundsSize = self.bounds.size; + + CGFloat minScale = 0; + CGFloat maxScale = 0; + + // Calculate the min/max scale for the image to be presented. + [self minAndMaxScaleForDimensions: imageSize + boundsSize: boundsSize + photoSize: self.photoSize + minScale: &minScale + maxScale: &maxScale]; + + // When we show thumbnails for images that are too small for the bounds, we try to use + // the known photo dimensions to scale the minimum scale to match what the final image + // would be. This avoids any "snapping" effects from stretching the thumbnail too large. + if ((NIPhotoScrollViewPhotoSizeThumbnail == self.photoSize) + && !CGSizeEqualToSize(self.photoDimensions, CGSizeZero)) { + CGFloat scaleToFitOriginal = 0; + CGFloat originalMaxScale = 0; + // Calculate the original-sized image's min/max scale. + [self minAndMaxScaleForDimensions: self.photoDimensions + boundsSize: boundsSize + photoSize: NIPhotoScrollViewPhotoSizeOriginal + minScale: &scaleToFitOriginal + maxScale: &originalMaxScale]; + + if (scaleToFitOriginal + FLT_EPSILON >= (1.0 / NIScreenScale())) { + // If the final image will be smaller than the view then we want to use that + // scale as the "true" scale and adjust it relatively to the thumbnail's dimensions. + // This ensures that the thumbnail will always be the same visual size as the original + // image, giving us that sexy "crisping" effect when the thumbnail is loaded. + CGFloat relativeSize = self.photoDimensions.width / imageSize.width; + minScale = scaleToFitOriginal * relativeSize; + } + } + + // If zooming is disabled then we flatten the range for zooming to only allow the min zoom. + self.maximumZoomScale = [self isZoomingEnabled] ? maxScale : minScale; + self.minimumZoomScale = minScale; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UIView + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)layoutSubviews { + [super layoutSubviews]; + + // Center the image as it becomes smaller than the size of the screen. + + CGSize boundsSize = self.bounds.size; + CGRect frameToCenter = _imageView.frame; + + // Center horizontally. + if (frameToCenter.size.width < boundsSize.width) { + frameToCenter.origin.x = floorf((boundsSize.width - frameToCenter.size.width) / 2); + + } else { + frameToCenter.origin.x = 0; + } + + // Center vertically. + if (frameToCenter.size.height < boundsSize.height) { + frameToCenter.origin.y = floorf((boundsSize.height - frameToCenter.size.height) / 2); + + } else { + frameToCenter.origin.y = 0; + } + + _imageView.frame = frameToCenter; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UIScrollView + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { + return _imageView; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Gesture Recognizers + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGRect)rectAroundPoint:(CGPoint)point atZoomScale:(CGFloat)zoomScale { + NIDASSERT(zoomScale > 0); + + // Define the shape of the zoom rect. + CGSize boundsSize = self.bounds.size; + + // Modify the size according to the requested zoom level. + // For example, if we're zooming in to 0.5 zoom, then this will increase the bounds size + // by a factor of two. + CGSize scaledBoundsSize = CGSizeMake(boundsSize.width / zoomScale, + boundsSize.height / zoomScale); + + CGRect rect = CGRectMake(point.x - scaledBoundsSize.width / 2, + point.y - scaledBoundsSize.height / 2, + scaledBoundsSize.width, + scaledBoundsSize.height); + + // When the image is zoomed out there is a bit of empty space around the image due + // to the fact that it's centered on the screen. When we created the rect around the + // point we need to take this "space" into account. + + // 1: get the frame of the image in this view's coordinates. + CGRect imageScaledFrame = [self convertRect:_imageView.frame toView:self]; + + // 2: Offset the frame by the excess amount. This will ensure that the zoomed location + // is always centered on the tap location. We only allow positive values because a + // negative value implies that there isn't actually any offset. + rect = CGRectOffset(rect, -MAX(0, imageScaledFrame.origin.x), -MAX(0, imageScaledFrame.origin.y)); + + return rect; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didDoubleTap:(UITapGestureRecognizer *)tapGesture { + BOOL isCompletelyZoomedIn = (self.maximumZoomScale <= self.zoomScale + FLT_EPSILON); + + BOOL didZoomIn; + + if (isCompletelyZoomedIn) { + // Zoom the photo back out. + [self setZoomScale:self.minimumZoomScale animated:YES]; + + didZoomIn = NO; + + } else { + // Zoom into the tap point. + CGPoint tapCenter = [tapGesture locationInView:_imageView]; + + CGRect maxZoomRect = [self rectAroundPoint:tapCenter atZoomScale:self.maximumZoomScale]; + [self zoomToRect:maxZoomRect animated:YES]; + + didZoomIn = YES; + } + + if ([self.photoScrollViewDelegate respondsToSelector: + @selector(photoScrollViewDidDoubleTapToZoom:didZoomIn:)]) { + [self.photoScrollViewDelegate photoScrollViewDidDoubleTapToZoom:self didZoomIn:didZoomIn]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Public Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setImage:(UIImage *)image photoSize:(NIPhotoScrollViewPhotoSize)photoSize { + _imageView.image = image; + [_imageView sizeToFit]; + + if (nil == image) { + self.photoSize = NIPhotoScrollViewPhotoSizeUnknown; + + } else { + self.photoSize = photoSize; + } + + // The min/max zoom values assume that the content size is the image size. The max zoom will + // be a value that allows the image to be seen at a 1-to-1 pixel resolution, while the min + // zoom will be small enough to fit the image on the screen perfectly. + if (nil != image) { + self.contentSize = image.size; + } + + [self setMaxMinZoomScalesForCurrentBounds]; + + // Start off with the image fully-visible on the screen. + self.zoomScale = self.minimumZoomScale; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (UIImage *)image { + return _imageView.image; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)prepareForReuse { + _imageView.image = nil; + + self.photoSize = NIPhotoScrollViewPhotoSizeUnknown; + + self.zoomScale = 1; + + self.contentSize = self.bounds.size; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setZoomingIsEnabled:(BOOL)enabled { + _zoomingIsEnabled = enabled; + + if (nil != _imageView.image) { + [self setMaxMinZoomScalesForCurrentBounds]; + + // Fit the image on screen. + self.zoomScale = self.minimumZoomScale; + + // Disable zoom bouncing if zooming is disabled, otherwise the view will allow pinching. + self.bouncesZoom = enabled; + + } else { + // Reset to the defaults if there is no set image yet. + self.zoomScale = 1; + self.minimumZoomScale = 1; + self.maximumZoomScale = 1; + self.bouncesZoom = NO; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setDoubleTapToZoomIsEnabled:(BOOL)enabled { + // Only enable double-tap to zoom if the SDK supports it. This feature only works on + // iOS 3.2 and above. + if (enabled && nil == _doubleTapGestureRecognizer + && (nil != NIUITapGestureRecognizerClass() + && [self respondsToSelector:@selector(addGestureRecognizer:)])) { + _doubleTapGestureRecognizer = + [[NIUITapGestureRecognizerClass() alloc] initWithTarget: self + action: @selector(didDoubleTap:)]; + + // I freaking love gesture recognizers. + [_doubleTapGestureRecognizer setNumberOfTapsRequired:2]; + + [self addGestureRecognizer:_doubleTapGestureRecognizer]; + } + + // If the recognizer hasn't been initialized then this will fire on nil and do nothing. + [_doubleTapGestureRecognizer setEnabled:enabled]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (BOOL)isDoubleTapToZoomIsEnabled { + // If the gesture recognizer hasn't been created, then _doubleTapGestureRecognizer will be + // nil and so calling isEnabled will return 0. + return [_doubleTapGestureRecognizer isEnabled]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark Saving/Restoring Offset and Scale + +// The following code is from Apple's ImageScrollView example application and has been used +// here because it is well-documented and concise. + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Fetch the visual center point of this view in the image view's coordinate space. +- (CGPoint)pointToCenterAfterRotation { + CGPoint boundsCenter = CGPointMake(CGRectGetMidX(self.bounds), CGRectGetMidY(self.bounds)); + return [self convertPoint:boundsCenter toView:_imageView]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGFloat)scaleToRestoreAfterRotation { + CGFloat contentScale = self.zoomScale; + + // If we're at the minimum zoom scale, preserve that by returning 0, which + // will be converted to the minimum allowable scale when the scale is restored. + if (contentScale <= self.minimumZoomScale + FLT_EPSILON) { + contentScale = 0; + } + + return contentScale; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGPoint)maximumContentOffset { + CGSize contentSize = self.contentSize; + CGSize boundsSize = self.bounds.size; + return CGPointMake(contentSize.width - boundsSize.width, + contentSize.height - boundsSize.height); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGPoint)minimumContentOffset { + return CGPointZero; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)restoreCenterPoint:(CGPoint)oldCenter scale:(CGFloat)oldScale { + // Step 1: restore zoom scale, making sure it is within the allowable range. + self.zoomScale = boundf(oldScale, self.minimumZoomScale, self.maximumZoomScale); + + // Step 2: restore center point, making sure it is within the allowable range. + + // 2a: convert our desired center point back to the scroll view's coordinate space from the + // image's coordinate space. + CGPoint boundsCenter = [self convertPoint:oldCenter fromView:_imageView]; + + // 2b: calculate the content offset that would yield that center point + CGPoint offset = CGPointMake(boundsCenter.x - self.bounds.size.width / 2.0f, + boundsCenter.y - self.bounds.size.height / 2.0f); + + // 2c: restore offset, adjusted to be within the allowable range + CGPoint maxOffset = [self maximumContentOffset]; + CGPoint minOffset = [self minimumContentOffset]; + offset.x = boundf(offset.x, minOffset.x, maxOffset.x); + offset.y = boundf(offset.y, minOffset.y, maxOffset.y); + self.contentOffset = offset; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setFrameAndMaintainZoomAndCenter:(CGRect)frame { + CGPoint restorePoint = [self pointToCenterAfterRotation]; + CGFloat restoreScale = [self scaleToRestoreAfterRotation]; + self.frame = frame; + [self setMaxMinZoomScalesForCurrentBounds]; + [self restoreCenterPoint:restorePoint scale:restoreScale]; +} + + +@end diff --git a/NIPhotoScrubberView.h b/NIPhotoScrubberView.h new file mode 100644 index 0000000..31df766 --- /dev/null +++ b/NIPhotoScrubberView.h @@ -0,0 +1,187 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import +#import + +@protocol NIPhotoScrubberViewDataSource; +@protocol NIPhotoScrubberViewDelegate; + +/** + * A control built for quickly skimming through a collection of images. + * + * @ingroup Photos-Views + * + * The user interacts with the scrubber by "scrubbing" their finger along the control, + * or more simply, touching the control and moving their finger along a single axis. + * Scrubbers can be seen in the Photos.app on the iPad. + * + * The thumbnails displayed in a scrubber will be a subset of the overall set of photos. + * The wider the scrubber, the more thumbnails will be shown. The displayed thumbnails will + * be chosen at constant intervals in the album, with a larger "selected" thumbnail image + * that will show whatever image is currently selected. This larger thumbnail will be + * positioned relatively within the scrubber to show the user what the current selection + * is in a physically intuitive way. + * + * This view is a completely independent view from the photo scroll view so you can choose + * to use this in your already built photo viewer. + * + * @image html scrubber1.png "Screenshot of NIPhotoScrubberView on the iPad." + * + * @see NIPhotoScrubberViewDataSource + * @see NIPhotoScrubberViewDelegate + */ +@interface NIPhotoScrubberView : UIView { +@private + NSMutableArray* _visiblePhotoViews; + NSMutableSet* _recycledPhotoViews; + + UIView* _containerView; + UIImageView* _selectionView; + + // State + NSInteger _selectedPhotoIndex; + + // Cached data source values + NSInteger _numberOfPhotos; + + // Cached display values + CGFloat _numberOfVisiblePhotos; + + id _dataSource; + id _delegate; +} + +#pragma mark Data Source /** @name Data Source */ + +/** + * The data source for this scrubber view. + */ +@property (nonatomic, readwrite, assign) id dataSource; + +/** + * Forces the scrubber view to reload all of its data. + * + * This must be called at least once after dataSource has been set in order for the view + * to gather any presentable information. + * + * This method is expensive. It will reset the state of the view and remove all existing + * thumbnails before requesting the new information from the data source. + */ +- (void)reloadData; + +/** + * Notify the scrubber view that a thumbnail has been loaded at a given index. + * + * This method is cheap, so do not be afraid to call it whenever a thumbnail loads. + * It will only modify visible thumbnails. + */ +- (void)didLoadThumbnail: (UIImage *)image + atIndex: (NSInteger)photoIndex; + + +#pragma mark Delegate /** @name Delegate */ + +/** + * The delegate for this scrubber view. + */ +@property (nonatomic, readwrite, assign) id delegate; + + +#pragma mark Accessing Selection /** @name Accessing Selection */ + +/** + * The selected photo index. + */ +@property (nonatomic, readwrite, assign) NSInteger selectedPhotoIndex; + +/** + * Set the selected photo with animation. + */ +- (void)setSelectedPhotoIndex:(NSInteger)photoIndex animated:(BOOL)animated; + +@end + +/** + * The data source for the photo scrubber. + * + * @ingroup Photos-Protocols + * + *

Performance Considerations

+ * + * A scrubber view's purpose is for instantly flipping through an album of photos. As such, + * it's crucial that your implementation of the data source performs blazingly fast. When + * the scrubber requests a thumbnail from you you should *not* be hitting the disk or blocking + * on a network call. If you don't have the thumbnail available at that exact moment, fire + * off an asynchronous load request (using NIReadFileFromDiskOperation or NIHTTPRequest) + * and return nil. Once the thumbnail is loaded, call didLoadThumbnail:atIndex: to notify + * the scrubber that it can display the thumbnail now. + * + * It is not recommended to use high-res images for your scrubber thumbnails. This is because + * the scrubber will keep a large set of images in memory and if you're giving it + * high-resolution images then you'll find that your app quickly burns through memory. + * If you don't have access to thumbnails from whatever API you're using then you should consider + * not using a scrubber. + * + * @see NIPhotoScrubberView + */ +@protocol NIPhotoScrubberViewDataSource + +@required + +#pragma mark Fetching Required Information /** @name Fetching Required Information */ + +/** + * Fetches the total number of photos in the scroll view. + * + * The value returned in this method will be cached by the scroll view until reloadData + * is called again. + */ +- (NSInteger)numberOfPhotosInScrubberView:(NIPhotoScrubberView *)photoScrubberView; + +/** + * Fetch the thumbnail image for the given photo index. + * + * Please read and understand the performance considerations for this data source. + */ +- (UIImage *)photoScrubberView: (NIPhotoScrubberView *)photoScrubberView + thumbnailAtIndex: (NSInteger)thumbnailIndex; + +@end + +/** + * The delegate for the photo scrubber. + * + * @ingroup Photos-Protocols + * + * Sends notifications of state changes. + * + * @see NIPhotoScrubberView + */ +@protocol NIPhotoScrubberViewDelegate + +@optional + +#pragma mark Selection Changes /** @name Selection Changes */ + +/** + * The photo scrubber changed its selection. + * + * Use photoScrubberView.selectedPhotoIndex to access the current selection. + */ +- (void)photoScrubberViewDidChangeSelection:(NIPhotoScrubberView *)photoScrubberView; + +@end diff --git a/NIPhotoScrubberView.m b/NIPhotoScrubberView.m new file mode 100644 index 0000000..d5d7391 --- /dev/null +++ b/NIPhotoScrubberView.m @@ -0,0 +1,506 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIPhotoScrubberView.h" + +#import "NimbusCore.h" + +#import + +static const NSInteger NIPhotoScrubberViewUnknownTag = -1; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@interface NIPhotoScrubberView() + +/** + * @internal + * + * A lightweight method for updating all of the visible thumbnails in the scrubber. + * + * This method will force the scrubber to lay itself out, calculate how many thumbnails might + * be visible, and then lay out the thumbnails and fetch any thumbnail images it can find. + * + * This method should never take much time to run, so it can safely be used in layoutSubviews. + */ +- (void)updateVisiblePhotos; + +/** + * @internal + * + * Returns a new, autoreleased image view in the style of this photo scrubber. + * + * This implementation returns an image with a 1px solid white border and a black background. + */ +- (UIImageView *)photoView; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NIPhotoScrubberView + +@synthesize dataSource = _dataSource; +@synthesize delegate = _delegate; +@synthesize selectedPhotoIndex = _selectedPhotoIndex; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + NI_RELEASE_SAFELY(_visiblePhotoViews); + NI_RELEASE_SAFELY(_recycledPhotoViews); + + NI_RELEASE_SAFELY(_containerView); + NI_RELEASE_SAFELY(_selectionView); + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)initWithFrame:(CGRect)frame { + if ((self = [super initWithFrame:frame])) { + // Only one finger should be allowed to interact with the scrubber at a time. + self.multipleTouchEnabled = NO; + + _containerView = [[UIView alloc] init]; + _containerView.layer.borderColor = [UIColor colorWithWhite:1 alpha:0.1f].CGColor; + _containerView.layer.borderWidth = 1; + _containerView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.3f]; + _containerView.userInteractionEnabled = NO; + [self addSubview:_containerView]; + + _selectionView = [[self photoView] retain]; + [self addSubview:_selectionView]; + + _selectedPhotoIndex = -1; + } + + return self; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark View Creation + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (UIImageView *)photoView { + UIImageView* imageView = [[[UIImageView alloc] init] autorelease]; + + imageView.layer.borderColor = [UIColor whiteColor].CGColor; + imageView.layer.borderWidth = 1; + imageView.backgroundColor = [UIColor blackColor]; + imageView.clipsToBounds = YES; + + imageView.userInteractionEnabled = NO; + + imageView.contentMode = UIViewContentModeScaleAspectFill; + + imageView.tag = NIPhotoScrubberViewUnknownTag; + + return imageView; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Layout + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGSize)photoSize { + CGSize boundsSize = self.bounds.size; + + // These numbers are roughly estimated from the Photos.app's scrubber. + CGFloat photoWidth = floorf(boundsSize.height / 2.4f); + CGFloat photoHeight = floorf(photoWidth * 0.75f); + + return CGSizeMake(photoWidth, photoHeight); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGSize)selectionSize { + CGSize boundsSize = self.bounds.size; + + // These numbers are roughly estimated from the Photos.app's scrubber. + CGFloat selectionWidth = floorf(boundsSize.height / 1.2f); + CGFloat selectionHeight = floorf(selectionWidth * 0.75f); + + return CGSizeMake(selectionWidth, selectionHeight); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// The amount of space on either side of the scrubber's left and right edges. +- (CGFloat)horizontalMargins { + CGSize photoSize = [self photoSize]; + return floorf(photoSize.width / 2); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGFloat)spaceBetweenPhotos { + return 1; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// The maximum number of pixels that the scrubber can utilize. The scrubber layer's border +// is contained within this width and must be considered when laying out the thumbnails. +- (CGFloat)maxContentWidth { + CGSize boundsSize = self.bounds.size; + CGFloat horizontalMargins = [self horizontalMargins]; + + CGFloat maxContentWidth = (boundsSize.width + - horizontalMargins * 2); + return maxContentWidth; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSInteger)numberOfVisiblePhotos { + CGSize photoSize = [self photoSize]; + CGFloat spaceBetweenPhotos = [self spaceBetweenPhotos]; + + // Here's where we take into account the container layer's border because we don't want to + // display thumbnails on top of the border. + CGFloat maxContentWidth = ([self maxContentWidth] + - _containerView.layer.borderWidth * 2); + + NSInteger numberOfPhotosThatFit = (NSInteger)floor((maxContentWidth + spaceBetweenPhotos) + / (photoSize.width + spaceBetweenPhotos)); + return MIN(_numberOfPhotos, numberOfPhotosThatFit); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGRect)frameForSelectionAtIndex:(NSInteger)photoIndex { + CGSize photoSize = [self photoSize]; + CGSize selectionSize = [self selectionSize]; + + CGFloat containerWidth = _containerView.bounds.size.width; + // TODO (jverkoey July 21, 2011): I need to figure out why this is necessary. + // Basically, when there are a lot of photos it seems like the selection frame + // slowly gets offset from the thumbnail frame it's supposed to be representing until by the end + // it's off the right edge by a noticeable amount. Trimming off some fat from the right + // edge seems to fix this. + if (_numberOfVisiblePhotos < _numberOfPhotos) { + containerWidth -= photoSize.width / 2; + } + + // Calculate the offset into the container view based on index/numberOfPhotos. + CGFloat relativeOffset = floorf((((CGFloat)photoIndex * containerWidth) + / (CGFloat)MAX(1, _numberOfPhotos))); + + return CGRectMake(floorf(_containerView.frame.origin.x + + relativeOffset + + photoSize.width / 2 - selectionSize.width / 2), + floorf(_containerView.center.y - selectionSize.height / 2), + selectionSize.width, selectionSize.height); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (CGRect)frameForThumbAtIndex:(NSInteger)thumbIndex { + CGSize photoSize = [self photoSize]; + CGFloat spaceBetweenPhotos = [self spaceBetweenPhotos]; + return CGRectMake(_containerView.layer.borderWidth + + (photoSize.width + spaceBetweenPhotos) * thumbIndex, + _containerView.layer.borderWidth, + photoSize.width, photoSize.height); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)layoutSubviews { + [super layoutSubviews]; + + CGSize boundsSize = self.bounds.size; + + CGSize photoSize = [self photoSize]; + CGFloat spaceBetweenPhotos = [self spaceBetweenPhotos]; + CGFloat maxContentWidth = [self maxContentWidth]; + + // Update the total number of visible photos. + _numberOfVisiblePhotos = [self numberOfVisiblePhotos]; + + // Hide views if there isn't any interesting information to show. + _containerView.hidden = (0 == _numberOfVisiblePhotos); + _selectionView.hidden = (_selectedPhotoIndex < 0 || _containerView.hidden); + + // Calculate the container width using the number of visible photos. + CGFloat containerWidth = ((_numberOfVisiblePhotos * photoSize.width) + + (MAX(0, _numberOfVisiblePhotos - 1) * spaceBetweenPhotos) + + _containerView.layer.borderWidth * 2); + + // Then we center the container in the content area. + CGFloat containerMargins = MAX(0, floorf((maxContentWidth - containerWidth) / 2)); + CGFloat horizontalMargins = [self horizontalMargins]; + CGFloat containerHeight = photoSize.height + _containerView.layer.borderWidth * 2; + + CGFloat containerLeftMargin = horizontalMargins + containerMargins; + CGFloat containerTopMargin = floorf((boundsSize.height - containerHeight) / 2); + + _containerView.frame = CGRectMake(containerLeftMargin, + containerTopMargin, + containerWidth, + containerHeight); + + // Don't bother updating the selected photo index if there isn't a selection; the + // selection view will be hidden anyway. + if (_selectedPhotoIndex >= 0) { + _selectionView.frame = [self frameForSelectionAtIndex:_selectedPhotoIndex]; + } + + // Update the frames for all of the thumbnails. + [self updateVisiblePhotos]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +// Transforms an index into the number of visible photos into an index into the total +// number of photos. +- (NSInteger)photoIndexAtScrubberIndex:(NSInteger)scrubberIndex { + return (NSInteger)(ceilf((CGFloat)(scrubberIndex * _numberOfPhotos) + / (CGFloat)_numberOfVisiblePhotos) + + 0.5f); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)updateVisiblePhotos { + if (nil == self.dataSource) { + return; + } + + // This will update the number of visible photos if the layout did indeed change. + [self layoutIfNeeded]; + + // Recycle any views that we no longer need. + while ([_visiblePhotoViews count] > _numberOfVisiblePhotos) { + UIView* photoView = [_visiblePhotoViews lastObject]; + [photoView removeFromSuperview]; + + [_recycledPhotoViews addObject:photoView]; + + [_visiblePhotoViews removeLastObject]; + } + + // Lay out the visible photos. + for (NSInteger ix = 0; ix < _numberOfVisiblePhotos; ++ix) { + UIImageView* photoView = nil; + + // We must first get the photo view at this index. + + // If there aren't enough visible photo views then try to recycle another view. + if (ix >= [_visiblePhotoViews count]) { + photoView = [[[_recycledPhotoViews anyObject] retain] autorelease]; + if (nil == photoView) { + // Couldn't recycle the view, so create a new one. + photoView = [self photoView]; + + } else { + [_recycledPhotoViews removeObject:photoView]; + } + [_containerView addSubview:photoView]; + [_visiblePhotoViews addObject:photoView]; + + } else { + photoView = [_visiblePhotoViews objectAtIndex:ix]; + } + + NSInteger photoIndex = [self photoIndexAtScrubberIndex:ix]; + + // Only request the thumbnail if this thumbnail's photo index has changed. Otherwise + // we assume that this photo either already has the thumbnail or it's still loading. + if (photoView.tag != photoIndex) { + photoView.tag = photoIndex; + + photoView.image = [self.dataSource photoScrubberView:self thumbnailAtIndex:photoIndex]; + } + + photoView.frame = [self frameForThumbAtIndex:ix]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Changing Selection + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (NSInteger)photoIndexAtPoint:(CGPoint)point { + NSInteger photoIndex; + + if (point.x <= 0) { + // Beyond the left edge + photoIndex = 0; + + } else if (point.x >= _containerView.bounds.size.width) { + // Beyond the right edge + photoIndex = (_numberOfPhotos - 1); + + } else { + // Somewhere in between + photoIndex = (NSInteger)(floorf((point.x / _containerView.bounds.size.width) * _numberOfPhotos) + + 0.5f); + } + + return photoIndex; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)updateSelectionWithPoint:(CGPoint)point { + NSInteger photoIndex = [self photoIndexAtPoint:point]; + + if (photoIndex != _selectedPhotoIndex) { + [self setSelectedPhotoIndex:photoIndex]; + + if ([self.delegate respondsToSelector:@selector(photoScrubberViewDidChangeSelection:)]) { + [self.delegate photoScrubberViewDidChangeSelection:self]; + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UIResponder + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { + [super touchesBegan:touches withEvent:event]; + + UITouch* touch = [touches anyObject]; + CGPoint touchPoint = [touch locationInView:_containerView]; + + [self updateSelectionWithPoint:touchPoint]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event { + [super touchesMoved:touches withEvent:event]; + + UITouch* touch = [touches anyObject]; + CGPoint touchPoint = [touch locationInView:_containerView]; + + [self updateSelectionWithPoint:touchPoint]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Public Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didLoadThumbnail: (UIImage *)image + atIndex: (NSInteger)photoIndex { + for (UIImageView* thumbView in _visiblePhotoViews) { + if (thumbView.tag == photoIndex) { + thumbView.image = image; + break; + } + } + + // Update the selected thumbnail if it's the one that just received a photo. + if (_selectedPhotoIndex == photoIndex) { + _selectionView.image = image; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)reloadData { + NIDASSERT(nil != _dataSource); + + // Remove any visible photos from the view before we release the sets. + for (UIView* photoView in _visiblePhotoViews) { + [photoView removeFromSuperview]; + } + + NI_RELEASE_SAFELY(_visiblePhotoViews); + NI_RELEASE_SAFELY(_recycledPhotoViews); + + // If there is no data source then we can't do anything particularly interesting. + if (nil == _dataSource) { + return; + } + + _visiblePhotoViews = [[NSMutableArray alloc] init]; + _recycledPhotoViews = [[NSMutableSet alloc] init]; + + // Cache the number of photos. + _numberOfPhotos = [_dataSource numberOfPhotosInScrubberView:self]; + + [self setNeedsLayout]; + + // This will call layoutIfNeeded and layoutSubviews will then be called because we + // set the needsLayout flag. + [self updateVisiblePhotos]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setSelectedPhotoIndex:(NSInteger)photoIndex animated:(BOOL)animated { + if (_selectedPhotoIndex != photoIndex) { + // Don't animate the selection if it was previously invalid. + animated = animated && (_selectedPhotoIndex >= 0); + + _selectedPhotoIndex = photoIndex; + + if (animated) { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDuration:0.2]; + [UIView setAnimationCurve:UIViewAnimationCurveEaseOut]; + [UIView setAnimationBeginsFromCurrentState:YES]; + } + + _selectionView.frame = [self frameForSelectionAtIndex:_selectedPhotoIndex]; + + if (animated) { + [UIView commitAnimations]; + } + + _selectionView.image = [self.dataSource photoScrubberView: self + thumbnailAtIndex: _selectedPhotoIndex]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setSelectedPhotoIndex:(NSInteger)photoIndex { + [self setSelectedPhotoIndex:photoIndex animated:NO]; +} + + +@end diff --git a/NIPreprocessorMacros.h b/NIPreprocessorMacros.h new file mode 100644 index 0000000..357f76b --- /dev/null +++ b/NIPreprocessorMacros.h @@ -0,0 +1,70 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import + + +#pragma mark - +#pragma mark Preprocessor Macros + +/** + * For generating code where methods can't be used. + * + * @ingroup NimbusCore + * @defgroup Preprocessor-Macros Preprocessor Macros + * @{ + */ + +/** + * Mark a method or property as deprecated to the compiler. + * + * Any use of a deprecated method or property will flag a warning when compiling. + * + * Borrowed from Apple's AvailabiltyInternal.h header. + * + * @htmlonly + *
+ *   __AVAILABILITY_INTERNAL_DEPRECATED         __attribute__((deprecated))
+ * 
+ * @endhtmlonly + */ +#define __NI_DEPRECATED_METHOD __attribute__((deprecated)) + +/** + * Force a category to be loaded when an app starts up. + * + * 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. + */ +#define NI_FIX_CATEGORY_BUG(name) @interface NI_FIX_CATEGORY_BUG_##name @end \ +@implementation NI_FIX_CATEGORY_BUG_##name @end + +/** + * Release and assign nil to an object. + * + * This macro is preferred to simply releasing an object to avoid accidentally using the + * object later on in a method. + */ +#define NI_RELEASE_SAFELY(__POINTER) { [__POINTER release]; __POINTER = nil; } + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Preprocessor Macros ////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NIRuntimeClassModifications.h b/NIRuntimeClassModifications.h new file mode 100644 index 0000000..210d9e6 --- /dev/null +++ b/NIRuntimeClassModifications.h @@ -0,0 +1,69 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import + +/** + * For modifying class implementations at runtime. + * + * @ingroup NimbusCore + * @defgroup Runtime-Class-Modifications Runtime Class Modifications + * @{ + * + * @attention Please use caution when modifying class implementations at runtime. + * Apple is prone to rejecting apps for gratuitous use of method swapping. + * In particular, avoid swapping any NSObject methods such as dealloc, init, + * and retain/release on UIKit classes. + * + * See example: @link ExampleRuntimeDebugging.m Runtime Debugging with Method Swizzling@endlink + */ + +/** + * Swap two class instance method implementations. + * + * Use this method when you would like to replace an existing method implementation in a class + * with your own implementation at runtime. In practice this is often used to replace the + * implementations of UIKit classes where subclassing isn't an adequate solution. + * + * This will only work for methods declared with a -. + * + * After calling this method, any calls to originalSel will actually call newSel and vice versa. + * + * Uses method_exchangeImplementations to accomplish this. + */ +void NISwapInstanceMethods(Class cls, SEL originalSel, SEL newSel); + +/** + * Swap two class method implementations. + * + * Use this method when you would like to replace an existing method implementation in a class + * with your own implementation at runtime. In practice this is often used to replace the + * implementations of UIKit classes where subclassing isn't an adequate solution. + * + * This will only work for methods declared with a +. + * + * After calling this method, any calls to originalSel will actually call newSel and vice versa. + * + * Uses method_exchangeImplementations to accomplish this. + */ +void NISwapClassMethods(Class cls, SEL originalSel, SEL newSel); + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of Runtime Class Modifications ////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NIRuntimeClassModifications.m b/NIRuntimeClassModifications.m new file mode 100644 index 0000000..0ef9c3d --- /dev/null +++ b/NIRuntimeClassModifications.m @@ -0,0 +1,37 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import "NIRuntimeClassModifications.h" + +#import + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +void NISwapInstanceMethods(Class cls, SEL originalSel, SEL newSel) { + Method originalMethod = class_getInstanceMethod(cls, originalSel); + Method newMethod = class_getInstanceMethod(cls, newSel); + method_exchangeImplementations(originalMethod, newMethod); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +void NISwapClassMethods(Class cls, SEL originalSel, SEL newSel) { + Method originalMethod = class_getClassMethod(cls, originalSel); + Method newMethod = class_getClassMethod(cls, newSel); + method_exchangeImplementations(originalMethod, newMethod); +} diff --git a/NISDKAvailability.h b/NISDKAvailability.h new file mode 100644 index 0000000..1e48895 --- /dev/null +++ b/NISDKAvailability.h @@ -0,0 +1,217 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import +#import + +/** + * For checking SDK feature availibility. + * + * @ingroup NimbusCore + * @defgroup SDK-Availability SDK Availability + * @{ + * + * NIIOS macros are defined in parallel to their __IPHONE_ counterparts as a consistently-defined + * means of checking __IPHONE_OS_VERSION_MAX_ALLOWED. + * + * For example: + * + * @htmlonly + *
+ *     #if __IPHONE_OS_VERSION_MAX_ALLOWED >= NIIOS_3_2
+ *       // This code will only compile on versions >= iOS 3.2
+ *     #endif
+ * 
+ * @endhtmlonly + */ + +/** + * Released on July 11, 2008 + */ +#define NIIOS_2_0 20000 + +/** + * Released on September 9, 2008 + */ +#define NIIOS_2_1 20100 + +/** + * Released on November 21, 2008 + */ +#define NIIOS_2_2 20200 + +/** + * Released on June 17, 2009 + */ +#define NIIOS_3_0 30000 + +/** + * Released on September 9, 2009 + */ +#define NIIOS_3_1 30100 + +/** + * Released on April 3, 2010 + */ +#define NIIOS_3_2 30200 + +/** + * Released on June 21, 2010 + */ +#define NIIOS_4_0 40000 + +/** + * Released on September 8, 2010 + */ +#define NIIOS_4_1 40100 + +/** + * Released on November 22, 2010 + */ +#define NIIOS_4_2 40200 + +/** + * Released on March 9, 2011 + */ +#define NIIOS_4_3 40300 + +/** + * Release TBD. + */ +#define NIIOS_5_0 50000 + +#ifndef kCFCoreFoundationVersionNumber_iPhoneOS_2_0 +#define kCFCoreFoundationVersionNumber_iPhoneOS_2_0 478.23 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iPhoneOS_2_1 +#define kCFCoreFoundationVersionNumber_iPhoneOS_2_1 478.26 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iPhoneOS_2_2 +#define kCFCoreFoundationVersionNumber_iPhoneOS_2_2 478.29 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iPhoneOS_3_0 +#define kCFCoreFoundationVersionNumber_iPhoneOS_3_0 478.47 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iPhoneOS_3_1 +#define kCFCoreFoundationVersionNumber_iPhoneOS_3_1 478.52 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iPhoneOS_3_2 +#define kCFCoreFoundationVersionNumber_iPhoneOS_3_2 478.61 +#endif + +#ifndef kCFCoreFoundationVersionNumber_iOS_4_0 +#define kCFCoreFoundationVersionNumber_iOS_4_0 550.32 +#endif + +/** + * Checks whether the device the app is currently running on is an iPad or not. + * + * @returns YES if the device is an iPad. + */ +BOOL NIIsPad(void); + +/** + * Checks whether the device's OS version is at least the given version number. + * + * Useful for runtime checks of the device's version number. + * + * @param versionNumber Any value of kCFCoreFoundationVersionNumber. + * + * @attention Apple recommends using respondsToSelector where possible to check for + * feature support. Use this method as a last resort. + */ +BOOL NIDeviceOSVersionIsAtLeast(double versionNumber); + +/** + * Fetch the screen's scale in an SDK-agnostic way. This will work on any pre-iOS 4.0 SDK. + * + * Pre-iOS 4.0: will always return 1. + * iOS 4.0: returns the device's screen scale. + */ +CGFloat NIScreenScale(void); + +/** + * Safely fetch the UIPopoverController class if it is available. + * + * The class is cached to avoid repeated lookups. + * + * Uses NSClassFromString to fetch the popover controller class. + * + * This class was first introduced in iOS 3.2 April 3, 2010. + * + * @attention If you wish to maintain pre-iOS 3.2 support then you must use this method + * instead of directly referring to UIPopoverController anywhere within your code. + * Failure to do so will cause your app to crash on startup on pre-iOS 3.2 devices. + */ +Class NIUIPopoverControllerClass(void); + +/** + * Safely fetch the UITapGestureRecognizer class if it is available. + * + * The class is cached to avoid repeated lookups. + * + * Uses NSClassFromString to fetch the tap gesture recognizer class. + * + * This class was first introduced in iOS 3.2 April 3, 2010. + * + * @attention If you wish to maintain pre-iOS 3.2 support then you must use this method + * instead of directly referring to UIPopoverController anywhere within your code. + * Failure to do so will cause your app to crash on startup on pre-iOS 3.2 devices. + */ +Class NIUITapGestureRecognizerClass(void); + + +#pragma mark Building with Old SDKs + +// Define classes that were introduced in iOS 3.2. +#if __IPHONE_OS_VERSION_MAX_ALLOWED < NIIOS_3_2 + +@class UIPopoverController; +@class UITapGestureRecognizer; + +#endif + + +// Define methods that were introduced in iOS 4.0. +#if __IPHONE_OS_VERSION_MAX_ALLOWED < NIIOS_4_0 + +@interface UIImage (NimbusSDKAvailability) + ++ (UIImage *)imageWithCGImage:(CGImageRef)imageRef scale:(CGFloat)scale orientation:(UIImageOrientation)orientation; + +- (CGFloat)scale; + +@end + +@interface UIScreen (NimbusSDKAvailability) + +- (CGFloat)scale; + +@end + +#endif + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of SDK Availability ///////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NISDKAvailability.m b/NISDKAvailability.m new file mode 100644 index 0000000..7177577 --- /dev/null +++ b/NISDKAvailability.m @@ -0,0 +1,87 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import "NimbusCore.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +BOOL NIIsPad(void) { +#ifdef UI_USER_INTERFACE_IDIOM + static NSInteger isPad = -1; + if (isPad < 0) { + isPad = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad); + } + return isPad > 0; +#else + return NO; +#endif +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +BOOL NIDeviceOSVersionIsAtLeast(double versionNumber) { + return kCFCoreFoundationVersionNumber >= versionNumber; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +CGFloat NIScreenScale(void) { + static int respondsToScale = -1; + if (respondsToScale == -1) { + // Avoid calling this anymore than we need to. + respondsToScale = !!([[UIScreen mainScreen] respondsToSelector:@selector(scale)]); + } + + if (respondsToScale) { + return [[UIScreen mainScreen] scale]; + + } else { + return 1; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +Class NIUIPopoverControllerClass(void) { + static Class sClass = nil; + static BOOL hasChecked = NO; + if (!hasChecked) { + hasChecked = YES; + sClass = NSClassFromString(@"UIPopoverController"); + } + return sClass; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +Class NIUITapGestureRecognizerClass(void) { + static Class sClass = nil; + static BOOL hasChecked = NO; + if (!hasChecked) { + hasChecked = YES; + + // An interesting gotcha: UITapGestureRecognizer actually *does* exist in iOS 3.0, but does + // not conform to all of the same methods that the 3.2 implementation does. This can be + // really confusing, so instead of returning the class, we'll always return nil on + // pre-iOS 3.2 devices. + if (NIDeviceOSVersionIsAtLeast(kCFCoreFoundationVersionNumber_iPhoneOS_3_2)) { + sClass = NSClassFromString(@"UITapGestureRecognizer"); + } + } + return sClass; +} diff --git a/NIState.h b/NIState.h new file mode 100644 index 0000000..a5bd6b7 --- /dev/null +++ b/NIState.h @@ -0,0 +1,88 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import + +#import "NIInMemoryCache.h" + +/** + * For modifying Nimbus state information. + * + * @ingroup NimbusCore + * @defgroup Core-State State + * @{ + * + * The Nimbus core provides a common layer of features used by nearly all of the libraries in + * the Nimbus ecosystem. Here you will find methods for accessing and setting the global image + * cache amongst other things. + */ + +/** + * The Nimbus state interface. + * + * @ingroup Core-State + */ +@interface Nimbus : NSObject + +#pragma mark Accessing Global State /** @name Accessing Global State */ + +/** + * Access the global image memory cache. + * + * If a cache hasn't been assigned via Nimbus::setGlobalImageMemoryCache: then one will be created + * automatically. + * + * @remarks The default image cache has no upper limit on its memory consumption. It is + * up to you to specify an upper limit in your application. + */ ++ (NIImageMemoryCache *)imageMemoryCache; + +/** + * Access the global network operation queue. + * + * The global network operation queue exists to be used for asynchronous network requests if + * you choose. By defining a global operation queue in the core of Nimbus, we can ensure that + * all libraries that depend on core will use the same network operation queue unless configured + * otherwise. + * + * If an operation queue hasn't been assigned via Nimbus::setGlobalNetworkOperationQueue: then + * one will be created automatically with the default iOS settings. + */ ++ (NSOperationQueue *)networkOperationQueue; + + +#pragma mark Modifying Global State /** @name Modifying Global State */ + +/** + * Set the global image memory cache. + * + * The cache will be retained and the old cache released. + */ ++ (void)setImageMemoryCache:(NIImageMemoryCache *)imageMemoryCache; + +/** + * Set the global network operation queue. + * + * The queue will be retained and the old queue released. + */ ++ (void)setNetworkOperationQueue:(NSOperationQueue *)queue; + +@end + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/**@}*/// End of State //////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/NIState.m b/NIState.m new file mode 100644 index 0000000..95f4fea --- /dev/null +++ b/NIState.m @@ -0,0 +1,67 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIState.h" + +#import "NIInMemoryCache.h" + +static NIImageMemoryCache* sNimbusGlobalMemoryCache = nil; +static NSOperationQueue* sNimbusGlobalOperationQueue = nil; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation Nimbus + + +/////////////////////////////////////////////////////////////////////////////////////////////////// ++ (void)setImageMemoryCache:(NIImageMemoryCache *)imageMemoryCache { + if (sNimbusGlobalMemoryCache != imageMemoryCache) { + [sNimbusGlobalMemoryCache release]; + sNimbusGlobalMemoryCache = [imageMemoryCache retain]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// ++ (NIImageMemoryCache *)imageMemoryCache { + if (nil == sNimbusGlobalMemoryCache) { + sNimbusGlobalMemoryCache = [[NIImageMemoryCache alloc] init]; + } + return sNimbusGlobalMemoryCache; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// ++ (void)setNetworkOperationQueue:(NSOperationQueue *)queue { + if (sNimbusGlobalOperationQueue != queue) { + [sNimbusGlobalOperationQueue release]; + sNimbusGlobalOperationQueue = [queue retain]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// ++ (NSOperationQueue *)networkOperationQueue { + if (nil == sNimbusGlobalOperationQueue) { + sNimbusGlobalOperationQueue = [[NSOperationQueue alloc] init]; + } + return sNimbusGlobalOperationQueue; +} + + +@end diff --git a/NIToolbarPhotoViewController.h b/NIToolbarPhotoViewController.h new file mode 100644 index 0000000..d00fcb1 --- /dev/null +++ b/NIToolbarPhotoViewController.h @@ -0,0 +1,192 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import +#import + +#import "NIPhotoAlbumScrollView.h" +#import "NIPhotoScrubberView.h" + +@class NIPhotoAlbumScrollView; + +/** + * A simple photo album view controller implementation with a toolbar. + * + * @ingroup Photos-Controllers + * + * This controller does not implement the photo album data source, it simply implements + * some of the most common UI elements that are associated with a photo viewer. + * + * For an example of implementing the data source, see the photos examples in the + * examples directory. + * + *

Implementing Delegate Methods

+ * + * This view controller already implements NIPhotoAlbumScrollViewDelegate. If you want to + * implement methods of this delegate you should take care to call the super implementation + * if necessary. The following methods have implementations in this class: + * + * - photoAlbumScrollViewDidScroll: + * - photoAlbumScrollView:didZoomIn: + * - photoAlbumScrollViewDidChangePages: + * + * + *

Recommended Configurations

+ * + *

Default: Zooming enabled with translucent toolbar

+ * + * The default settings are good for showing a photo album that takes up the entire screen. + * The photos will be visible beneath the toolbar because it is translucent. The chrome will + * be hidden whenever the user starts interacting with the photos. + * + * @code + * showPhotoAlbumBeneathToolbar = YES; + * hidesChromeWhenScrolling = YES; + * chromeCanBeHidden = YES; + * @endcode + * + *

Zooming disabled with opaque toolbar

+ * + * The following settings are good for viewing photo albums when you want to keep the chrome + * visible at all times without zooming enabled. + * + * @code + * showPhotoAlbumBeneathToolbar = NO; + * chromeCanBeHidden = NO; + * photoAlbumView.zoomingIsEnabled = NO; + * @endcode + */ +@interface NIToolbarPhotoViewController : UIViewController < + NIPhotoAlbumScrollViewDelegate, + NIPhotoScrubberViewDelegate > { +@private + // Views + UIToolbar* _toolbar; + NIPhotoAlbumScrollView* _photoAlbumView; + + // Toolbar Buttons + UIBarButtonItem* _nextButton; + UIBarButtonItem* _previousButton; + + // Scrubber View + NIPhotoScrubberView* _photoScrubberView; + + // Gestures + UITapGestureRecognizer* _tapGesture; + + // State + BOOL _isAnimatingChrome; + + // Configuration + BOOL _showPhotoAlbumBeneathToolbar; + BOOL _hidesChromeWhenScrolling; + BOOL _chromeCanBeHidden; + BOOL _animateMovingToNextAndPreviousPhotos; + BOOL _scrubberIsEnabled; +} + +#pragma mark Configuring Functionality /** @name Configuring Functionality */ + +/** + * Whether to show the photo album view beneath the toolbar or not. + * + * If this is enabled, the toolbar will be translucent and the photo view will + * take up the entire view controller's bounds with the toolbar shown on top. + * + * If this is disabled, the photo will only occupy the remaining space above the + * toolbar. + * + * By default this is YES. + */ +@property (nonatomic, readwrite, assign) BOOL showPhotoAlbumBeneathToolbar; + +/** + * Whether or not to hide the chrome when the user begins interacting with the photo. + * + * If this is enabled, then the chrome will be hidden when the user starts swiping from + * one photo to another. + * + * The chrome is the toolbar and the system status bar. + * + * By default this is YES. + * + * @attention This will be set to NO if toolbarCanBeHidden is set to NO. + */ +@property (nonatomic, readwrite, assign) BOOL hidesChromeWhenScrolling; + +/** + * Whether or not to allow hiding the chrome. + * + * If this is enabled then the user will be able to single-tap to dismiss or show the + * toolbar. + * + * The chrome is the toolbar and the system status bar. + * + * If this is disabled then the chrome will always be visible. + * + * By default this is YES. + * + * @attention Setting this to NO will also disable hidesToolbarWhenScrolling. + */ +@property (nonatomic, readwrite, assign) BOOL chromeCanBeHidden; + +/** + * Whether to animate moving to a next or previous photo when the user taps the button. + * + * By default this is NO. + */ +@property (nonatomic, readwrite, assign) BOOL animateMovingToNextAndPreviousPhotos; + +/** + * Whether to show a scrubber in the toolbar instead of next/previous buttons. + * + * By default this is YES on the iPad and NO on the iPhone. + */ +@property (nonatomic, readwrite, assign, getter=isScrubberEnabled) BOOL scrubberIsEnabled; + + +#pragma mark Views /** @name Views */ + +/** + * The toolbar view. + */ +@property (nonatomic, readonly, retain) UIToolbar* toolbar; + +/** + * The photo album view. + */ +@property (nonatomic, readonly, retain) NIPhotoAlbumScrollView* photoAlbumView; + +/** + * The photo scrubber view. + */ +@property (nonatomic, readonly, retain) NIPhotoScrubberView* photoScrubberView; + + +#pragma mark Toolbar Buttons /** @name Toolbar Buttons */ + +/** + * The 'next' button. + */ +@property (nonatomic, readonly, retain) UIBarButtonItem* nextButton; + +/** + * The 'previous' button. + */ +@property (nonatomic, readonly, retain) UIBarButtonItem* previousButton; + + +@end diff --git a/NIToolbarPhotoViewController.m b/NIToolbarPhotoViewController.m new file mode 100644 index 0000000..aabffaa --- /dev/null +++ b/NIToolbarPhotoViewController.m @@ -0,0 +1,573 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +#import "NIToolbarPhotoViewController.h" + +#import "NIPhotoAlbumScrollView.h" + +#import "NimbusCore.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +@implementation NIToolbarPhotoViewController + +@synthesize showPhotoAlbumBeneathToolbar = _showPhotoAlbumBeneathToolbar; +@synthesize hidesChromeWhenScrolling = _hidesChromeWhenScrolling; +@synthesize chromeCanBeHidden = _chromeCanBeHidden; +@synthesize animateMovingToNextAndPreviousPhotos = _animateMovingToNextAndPreviousPhotos; +@synthesize scrubberIsEnabled = _scrubberIsEnabled; +@synthesize toolbar = _toolbar; +@synthesize photoAlbumView = _photoAlbumView; +@synthesize photoScrubberView = _photoScrubberView; +@synthesize nextButton = _nextButton; +@synthesize previousButton = _previousButton; + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)shutdown { + _toolbar = nil; + _photoAlbumView = nil; + + NI_RELEASE_SAFELY(_nextButton); + NI_RELEASE_SAFELY(_previousButton); + + NI_RELEASE_SAFELY(_photoScrubberView); + + NI_RELEASE_SAFELY(_tapGesture); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)dealloc { + [self shutdown]; + + [super dealloc]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { + if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) { + // Default Configuration Settings + self.showPhotoAlbumBeneathToolbar = YES; + self.hidesChromeWhenScrolling = YES; + self.chromeCanBeHidden = YES; + self.animateMovingToNextAndPreviousPhotos = NO; + + // The scrubber is better use of the extra real estate on the iPad. + // If you ask me, though, the scrubber works pretty well on the iPhone too. It's up + // to you if you want to use it in your own implementations. + self.scrubberIsEnabled = NIIsPad(); + + // Allow the photos to display beneath the status bar. + self.wantsFullScreenLayout = YES; + } + return self; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)addTapGestureToView { + if ([self isViewLoaded] + && nil != NIUITapGestureRecognizerClass() + && [self.photoAlbumView respondsToSelector:@selector(addGestureRecognizer:)]) { + if (nil == _tapGesture) { + _tapGesture = + [[NIUITapGestureRecognizerClass() alloc] initWithTarget: self + action: @selector(didTap)]; + + [self.photoAlbumView addGestureRecognizer:_tapGesture]; + } + } + + [_tapGesture setEnabled:YES]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)updateToolbarItems { + UIBarItem* flexibleSpace = + [[[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemFlexibleSpace + target: nil + action: nil] autorelease]; + + if ([self isScrubberEnabled]) { + NI_RELEASE_SAFELY(_nextButton); + NI_RELEASE_SAFELY(_previousButton); + + if (nil == _photoScrubberView) { + CGRect scrubberFrame = CGRectMake(0, 0, + self.toolbar.bounds.size.width, + self.toolbar.bounds.size.height); + _photoScrubberView = [[NIPhotoScrubberView alloc] initWithFrame:scrubberFrame]; + _photoScrubberView.autoresizingMask = (UIViewAutoresizingFlexibleWidth + | UIViewAutoresizingFlexibleHeight); + _photoScrubberView.delegate = self; + } + + UIBarButtonItem* scrubberItem = + [[[UIBarButtonItem alloc] initWithCustomView:self.photoScrubberView] autorelease]; + self.toolbar.items = [NSArray arrayWithObjects: + flexibleSpace, scrubberItem, flexibleSpace, + nil]; + + [_photoScrubberView setSelectedPhotoIndex:self.photoAlbumView.centerPhotoIndex]; + + } else { + NI_RELEASE_SAFELY(_photoScrubberView); + + if (nil == _nextButton) { + UIImage* nextIcon = [UIImage imageWithContentsOfFile: + NIPathForBundleResource(nil, @"NimbusPhotos.bundle/gfx/next.png")]; + + // We weren't able to find the next or previous icons in your application's resources. + // Ensure that you've dragged the NimbusPhotos.bundle from src/photos/resources into your + // application with the "Create Folder References" option selected. You can verify that + // you've done this correctly by expanding the NimbusPhotos.bundle file in your project + // and verifying that the 'gfx' directory is blue. Also verify that the bundle is being + // copied in the Copy Bundle Resources phase. + NIDASSERT(nil != nextIcon); + + _nextButton = [[UIBarButtonItem alloc] initWithImage: nextIcon + style: UIBarButtonItemStylePlain + target: self + action: @selector(didTapNextButton)]; + + } + + if (nil == _previousButton) { + UIImage* previousIcon = [UIImage imageWithContentsOfFile: + NIPathForBundleResource(nil, @"NimbusPhotos.bundle/gfx/previous.png")]; + + // We weren't able to find the next or previous icons in your application's resources. + // Ensure that you've dragged the NimbusPhotos.bundle from src/photos/resources into your + // application with the "Create Folder References" option selected. You can verify that + // you've done this correctly by expanding the NimbusPhotos.bundle file in your project + // and verifying that the 'gfx' directory is blue. Also verify that the bundle is being + // copied in the Copy Bundle Resources phase. + NIDASSERT(nil != previousIcon); + + _previousButton = [[UIBarButtonItem alloc] initWithImage: previousIcon + style: UIBarButtonItemStylePlain + target: self + action: @selector(didTapPreviousButton)]; + } + + self.toolbar.items = [NSArray arrayWithObjects: + flexibleSpace, self.previousButton, + flexibleSpace, self.nextButton, + flexibleSpace, + nil]; + } + +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)loadView { + [super loadView]; + + CGRect bounds = self.view.bounds; + + // Toolbar Setup + + CGFloat toolbarHeight = NIToolbarHeightForOrientation(NIInterfaceOrientation()); + CGRect toolbarFrame = CGRectMake(0, bounds.size.height - toolbarHeight, + bounds.size.width, toolbarHeight); + + _toolbar = [[[UIToolbar alloc] initWithFrame:toolbarFrame] autorelease]; + _toolbar.barStyle = UIBarStyleBlack; + _toolbar.translucent = self.showPhotoAlbumBeneathToolbar; + _toolbar.autoresizingMask = (UIViewAutoresizingFlexibleWidth + | UIViewAutoresizingFlexibleTopMargin); + + [self updateToolbarItems]; + + // Photo Album View Setup + + CGRect photoAlbumFrame = bounds; + if (!self.showPhotoAlbumBeneathToolbar) { + photoAlbumFrame = NIRectContract(bounds, 0, toolbarHeight); + } + _photoAlbumView = [[[NIPhotoAlbumScrollView alloc] initWithFrame:photoAlbumFrame] autorelease]; + _photoAlbumView.autoresizingMask = (UIViewAutoresizingFlexibleWidth + | UIViewAutoresizingFlexibleHeight); + _photoAlbumView.delegate = self; + + [self.view addSubview:_photoAlbumView]; + [self.view addSubview:_toolbar]; + + + if (self.hidesChromeWhenScrolling) { + [self addTapGestureToView]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)viewDidUnload { + [self shutdown]; + + [super viewDidUnload]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)viewWillAppear:(BOOL)animated { + [super viewWillAppear:animated]; + + [[UIApplication sharedApplication] setStatusBarStyle: (NIIsPad() + ? UIStatusBarStyleBlackOpaque + : UIStatusBarStyleBlackTranslucent) + animated: animated]; + + UINavigationBar* navBar = self.navigationController.navigationBar; + navBar.barStyle = UIBarStyleBlack; + navBar.translucent = YES; + + _previousButton.enabled = [self.photoAlbumView hasPrevious]; + _nextButton.enabled = [self.photoAlbumView hasNext]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { + return NIIsSupportedOrientation(toInterfaceOrientation); +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)willRotateToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation + duration: (NSTimeInterval)duration { + [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; + + [self.photoAlbumView willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)willAnimateRotationToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation + duration: (NSTimeInterval)duration { + [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation + duration:duration]; + + CGRect toolbarFrame = self.toolbar.frame; + toolbarFrame.size.height = NIToolbarHeightForOrientation(toInterfaceOrientation); + toolbarFrame.origin.y = self.view.bounds.size.height - toolbarFrame.size.height; + self.toolbar.frame = toolbarFrame; + + if (!self.showPhotoAlbumBeneathToolbar) { + CGRect photoAlbumFrame = self.photoAlbumView.frame; + photoAlbumFrame.size.height = self.view.bounds.size.height - toolbarFrame.size.height; + self.photoAlbumView.frame = photoAlbumFrame; + } + + [self.photoAlbumView willAnimateRotationToInterfaceOrientation: toInterfaceOrientation + duration: duration]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didHideChrome { + _isAnimatingChrome = NO; + self.toolbar.hidden = YES; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didShowChrome { + _isAnimatingChrome = NO; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setChromeVisibility:(BOOL)isVisible animated:(BOOL)animated { + if (_isAnimatingChrome + || (!isVisible && self.toolbar.hidden) + || (isVisible && !self.toolbar.hidden) + || !self.chromeCanBeHidden) { + // Nothing to do here. + return; + } + + CGRect toolbarFrame = self.toolbar.frame; + CGRect bounds = self.view.bounds; + + // Reset the toolbar's initial position. + if (!isVisible) { + toolbarFrame.origin.y = bounds.size.height - toolbarFrame.size.height; + + } else { + // Ensure that the toolbar is visible through the animation. + self.toolbar.hidden = NO; + + toolbarFrame.origin.y = bounds.size.height; + } + self.toolbar.frame = toolbarFrame; + + // Show/hide the system chrome. + if ([[UIApplication sharedApplication] respondsToSelector: + @selector(setStatusBarHidden:withAnimation:)]) { + // On 3.2 and higher we can slide the status bar out. + [[UIApplication sharedApplication] setStatusBarHidden: !isVisible + withAnimation: (animated + ? UIStatusBarAnimationSlide + : UIStatusBarAnimationNone)]; + + } else { +#if __IPHONE_OS_VERSION_MIN_REQUIRED < NIIOS_3_2 + // On 3.0 devices we use the boring fade animation. + [[UIApplication sharedApplication] setStatusBarHidden: !isVisible + animated: animated]; +#endif + } + + // Place the toolbar at its final location. + if (isVisible) { + // Slide up. + toolbarFrame.origin.y = bounds.size.height - toolbarFrame.size.height; + + } else { + // Slide down. + toolbarFrame.origin.y = bounds.size.height; + } + + // If there is a navigation bar, place it at its final location. + CGRect navigationBarFrame = CGRectZero; + if (nil != self.navigationController.navigationBar) { + navigationBarFrame = self.navigationController.navigationBar.frame; + CGRect statusBarFrame = [[UIApplication sharedApplication] statusBarFrame]; + CGFloat statusBarHeight = MIN(statusBarFrame.size.width, statusBarFrame.size.height); + + if (isVisible) { + navigationBarFrame.origin.y = statusBarHeight; + + } else { + navigationBarFrame.origin.y = 0; + } + } + + if (animated) { + [UIView beginAnimations:nil context:nil]; + [UIView setAnimationDelegate:self]; + [UIView setAnimationDidStopSelector:(isVisible + ? @selector(didShowChrome) + : @selector(didHideChrome))]; + + // Ensure that the animation matches the status bar's. + [UIView setAnimationDuration:NIStatusBarAnimationDuration()]; + [UIView setAnimationCurve:NIStatusBarAnimationCurve()]; + } + + self.toolbar.frame = toolbarFrame; + if (nil != self.navigationController.navigationBar) { + self.navigationController.navigationBar.frame = navigationBarFrame; + self.navigationController.navigationBar.alpha = (isVisible ? 1 : 0); + } + + if (animated) { + _isAnimatingChrome = YES; + [UIView commitAnimations]; + + } else if (!isVisible) { + [self didHideChrome]; + + } else if (isVisible) { + [self didShowChrome]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)toggleChromeVisibility { + [self setChromeVisibility:(self.toolbar.hidden || _isAnimatingChrome) animated:YES]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark UIGestureRecognizer + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didTap { + SEL selector = @selector(toggleChromeVisibility); + if (self.photoAlbumView.zoomingIsEnabled) { + // Cancel any previous delayed performs so that we don't stack them. + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:selector object:nil]; + + // We need to delay taking action on the first tap in case a second tap comes in, causing + // a double-tap gesture to be recognized and the photo to be zoomed. + [self performSelector: selector + withObject: nil + afterDelay: 0.3]; + + } else { + // When zooming is disabled, double-tap-to-zoom is also disabled so we don't have to + // be as careful; just toggle the chrome immediately. + [self toggleChromeVisibility]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)refreshChromeState { + self.previousButton.enabled = [self.photoAlbumView hasPrevious]; + self.nextButton.enabled = [self.photoAlbumView hasNext]; + + self.title = [NSString stringWithFormat:@"%d of %d", + (self.photoAlbumView.centerPhotoIndex + 1), + self.photoAlbumView.numberOfPhotos]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NIPhotoAlbumScrollViewDelegate + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)photoAlbumScrollViewDidScroll:(NIPhotoAlbumScrollView *)photoAlbumScrollView { + if (self.hidesChromeWhenScrolling) { + [self setChromeVisibility:NO animated:YES]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)photoAlbumScrollView: (NIPhotoAlbumScrollView *)photoAlbumScrollView + didZoomIn: (BOOL)didZoomIn { + // This delegate method is called after a double-tap gesture, so cancel any pending + // single-tap gestures. + [NSObject cancelPreviousPerformRequestsWithTarget: self + selector: @selector(toggleChromeVisibility) + object: nil]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)photoAlbumScrollViewDidChangePages:(NIPhotoAlbumScrollView *)photoAlbumScrollView { + // We animate the scrubber when the chrome won't disappear as a nice touch. + // We don't bother animating if the chrome disappears when scrolling because the user + // will barely see the animation happen. + [self.photoScrubberView setSelectedPhotoIndex: [photoAlbumScrollView centerPhotoIndex] + animated: !self.hidesChromeWhenScrolling]; + + [self refreshChromeState]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark NIPhotoScrubberViewDelegate + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)photoScrubberViewDidChangeSelection:(NIPhotoScrubberView *)photoScrubberView { + [self.photoAlbumView setCenterPhotoIndex:photoScrubberView.selectedPhotoIndex animated:NO]; + + [self refreshChromeState]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Actions + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didTapNextButton { + [self.photoAlbumView moveToNextAnimated:self.animateMovingToNextAndPreviousPhotos]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)didTapPreviousButton { + [self.photoAlbumView moveToPreviousAnimated:self.animateMovingToNextAndPreviousPhotos]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +#pragma mark - +#pragma mark Public Methods + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setShowPhotoAlbumBeneathToolbar:(BOOL)enabled { + _showPhotoAlbumBeneathToolbar = enabled; + + self.toolbar.translucent = enabled; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setHidesChromeWhenScrolling:(BOOL)hidesToolbar { + _hidesChromeWhenScrolling = hidesToolbar; + + if (hidesToolbar) { + [self addTapGestureToView]; + + } else { + [_tapGesture setEnabled:NO]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setChromeCanBeHidden:(BOOL)canBeHidden { + if (nil == NIUITapGestureRecognizerClass()) { + // Don't allow the chrome to be hidden if we can't tap to make it visible again. + canBeHidden = NO; + } + + _chromeCanBeHidden = canBeHidden; + + if (!canBeHidden) { + self.hidesChromeWhenScrolling = NO; + + if ([self isViewLoaded]) { + // Ensure that the toolbar is visible. + self.toolbar.hidden = NO; + + CGRect toolbarFrame = self.toolbar.frame; + CGRect bounds = self.view.bounds; + toolbarFrame.origin.y = bounds.size.height - toolbarFrame.size.height; + self.toolbar.frame = toolbarFrame; + } + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +- (void)setScrubberIsEnabled:(BOOL)enabled { + if (_scrubberIsEnabled != enabled) { + _scrubberIsEnabled = enabled; + + if ([self isViewLoaded]) { + [self updateToolbarItems]; + } + } +} + + +@end diff --git a/NSData+NimbusCore.h b/NSData+NimbusCore.h new file mode 100644 index 0000000..9d20814 --- /dev/null +++ b/NSData+NimbusCore.h @@ -0,0 +1,28 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import + +// Documentation for these additions is found in the .m file. +@interface NSData (NimbusCore) + +@property (nonatomic, readonly) NSString* md5Hash; + +@property (nonatomic, readonly) NSString* sha1Hash; + +@end diff --git a/NSData+NimbusCore.m b/NSData+NimbusCore.m new file mode 100644 index 0000000..916e4b1 --- /dev/null +++ b/NSData+NimbusCore.m @@ -0,0 +1,79 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import "NSData+NimbusCore.h" + +#import "NIPreprocessorMacros.h" + +#import + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +NI_FIX_CATEGORY_BUG(NSDataNimbusCore) +/** + * For hashing raw data. + * + * Turning NSData objects into hashes is a common operation when verifying downloaded + * data and sending data over the wire. + */ +@implementation NSData (NimbusCore) + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Calculate an md5 hash using CC_MD5. + * + * @returns The md5 hash of this data. + */ +- (NSString *)md5Hash { + unsigned char result[CC_MD5_DIGEST_LENGTH]; + CC_MD5([self bytes], [self length], result); + + return [NSString stringWithFormat: + @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7], + result[8], result[9], result[10], result[11], result[12], result[13], result[14], + result[15] + ]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Calculate the SHA1 hash using CC_SHA1. + * + * @returns The SHA1 hash of this data. + */ +- (NSString *)sha1Hash { + unsigned char result[CC_SHA1_DIGEST_LENGTH]; + CC_SHA1([self bytes], [self length], result); + + return [NSString stringWithFormat: + @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + result[0], result[1], result[2], result[3], result[4], result[5], result[6], result[7], + result[8], result[9], result[10], result[11], result[12], result[13], result[14], + result[15], result[16], result[17], result[18], result[19] + ]; +} + + +@end + +/**@}*/ diff --git a/NSString+NimbusCore.h b/NSString+NimbusCore.h new file mode 100644 index 0000000..d93f9ae --- /dev/null +++ b/NSString+NimbusCore.h @@ -0,0 +1,50 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import + +// Documentation for these additions is found in the .m file. +@interface NSString (NimbusCore) + +#pragma mark Checking String Contents + +- (BOOL)isWhitespaceAndNewlines; + +#pragma mark Display + +- (CGFloat)heightWithFont: (UIFont*)font + constrainedToWidth: (CGFloat)width + lineBreakMode: (UILineBreakMode)lineBreakMode; + +#pragma mark URL queries + +- (NSDictionary*)queryContentsUsingEncoding:(NSStringEncoding)encoding; +- (NSString *)stringByAddingPercentEscapesForURLParameter; +- (NSString*)stringByAddingQueryDictionary:(NSDictionary*)query; + +#pragma mark Versions + +- (NSComparisonResult)versionStringCompare:(NSString *)other; + +#pragma mark Hashing + +@property (nonatomic, readonly) NSString* md5Hash; +@property (nonatomic, readonly) NSString* sha1Hash; + + +@end diff --git a/NSString+NimbusCore.m b/NSString+NimbusCore.m new file mode 100644 index 0000000..05f3cec --- /dev/null +++ b/NSString+NimbusCore.m @@ -0,0 +1,237 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// Forked from Three20 June 10, 2011 - Copyright 2009-2011 Facebook +// +// 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. +// + +#import "NSString+NimbusCore.h" + +#import "NSData+NimbusCore.h" +#import "NIPreprocessorMacros.h" + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////////////////////////// +NI_FIX_CATEGORY_BUG(NSStringNimbusCore) +/** + * For manipulating NSStrings. + */ +@implementation NSString (NimbusCore) + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Determines if the string contains only whitespace and newlines. + */ +- (BOOL)isWhitespaceAndNewlines { + NSCharacterSet* whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet]; + for (NSInteger i = 0; i < self.length; ++i) { + unichar c = [self characterAtIndex:i]; + if (![whitespace characterIsMember:c]) { + return NO; + } + } + return YES; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Calculates the height of this text given the font, max width, and line break mode. + * + * A convenience wrapper for sizeWithFont:constrainedToSize:lineBreakMode: + */ +- (CGFloat)heightWithFont: (UIFont*)font + constrainedToWidth: (CGFloat)width + lineBreakMode: (UILineBreakMode)lineBreakMode { + return [self sizeWithFont: font + constrainedToSize: CGSizeMake(width, CGFLOAT_MAX) + lineBreakMode: lineBreakMode].height; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Parses a URL query string into a dictionary where the values are arrays. + * + * A query string is one that looks like ¶m1=value1¶m2=value2... + * + * The resulting NSDictionary will contain keys for each parameter name present in the query. + * The value for each key will be an NSArray which may be empty if the key is simply present + * in the query. Otherwise each object in the array with be an NSString corresponding to a value + * in the query for that parameter. + */ +- (NSDictionary*)queryContentsUsingEncoding:(NSStringEncoding)encoding { + NSCharacterSet* delimiterSet = [NSCharacterSet characterSetWithCharactersInString:@"&;"]; + NSMutableDictionary* pairs = [NSMutableDictionary dictionary]; + NSScanner* scanner = [[[NSScanner alloc] initWithString:self] autorelease]; + while (![scanner isAtEnd]) { + NSString* pairString = nil; + [scanner scanUpToCharactersFromSet:delimiterSet intoString:&pairString]; + [scanner scanCharactersFromSet:delimiterSet intoString:NULL]; + NSArray* kvPair = [pairString componentsSeparatedByString:@"="]; + if (kvPair.count == 1 || kvPair.count == 2) { + NSString* key = [[kvPair objectAtIndex:0] + stringByReplacingPercentEscapesUsingEncoding:encoding]; + NSMutableArray* values = [pairs objectForKey:key]; + if (nil == values) { + values = [NSMutableArray array]; + [pairs setObject:values forKey:key]; + } + if (kvPair.count == 1) { + [values addObject:[NSNull null]]; + + } else if (kvPair.count == 2) { + NSString* value = [[kvPair objectAtIndex:1] + stringByReplacingPercentEscapesUsingEncoding:encoding]; + [values addObject:value]; + } + } + } + return [NSDictionary dictionaryWithDictionary:pairs]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Returns a string that has been escaped for use as a URL parameter. + */ +- (NSString *)stringByAddingPercentEscapesForURLParameter { + return [(NSString *)CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, + (CFStringRef)self, + NULL, + (CFStringRef)@";/?:@&=+$,", + kCFStringEncodingUTF8) + autorelease]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Parses a URL, adds query parameters to its query, and re-encodes it as a new URL. + */ +- (NSString*)stringByAddingQueryDictionary:(NSDictionary*)query { + NSMutableArray* pairs = [NSMutableArray array]; + for (NSString* key in [query keyEnumerator]) { + NSString* value = [[query objectForKey:key] stringByAddingPercentEscapesForURLParameter]; + NSString* pair = [NSString stringWithFormat:@"%@=%@", key, value]; + [pairs addObject:pair]; + } + + NSString* params = [pairs componentsJoinedByString:@"&"]; + if ([self rangeOfString:@"?"].location == NSNotFound) { + return [self stringByAppendingFormat:@"?%@", params]; + + } else { + return [self stringByAppendingFormat:@"&%@", params]; + } +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Compares two strings expressing software versions. + * + * The comparison is (except for the development version provisions noted below) lexicographic + * string comparison. So as long as the strings being compared use consistent version formats, + * a variety of schemes are supported. For example "3.02" < "3.03" and "3.0.2" < "3.0.3". If you + * mix such schemes, like trying to compare "3.02" and "3.0.3", the result may not be what you + * expect. + * + * Development versions are also supported by adding an "a" character and more version info after + * it. For example "3.0a1" or "3.01a4". The way these are handled is as follows: if the parts + * before the "a" are different, the parts after the "a" are ignored. If the parts before the "a" + * are identical, the result of the comparison is the result of NUMERICALLY comparing the parts + * after the "a". If the part after the "a" is empty, it is treated as if it were "0". If one + * string has an "a" and the other does not (e.g. "3.0" and "3.0a1") the one without the "a" + * is newer. + * + * Examples (?? means undefined): + * @htmlonly + *
+ *   "3.0" = "3.0"
+ *   "3.0a2" = "3.0a2"
+ *   "3.0" > "2.5"
+ *   "3.1" > "3.0"
+ *   "3.0a1" < "3.0"
+ *   "3.0a1" < "3.0a4"
+ *   "3.0a2" < "3.0a19"  <-- numeric, not lexicographic
+ *   "3.0a" < "3.0a1"
+ *   "3.02" < "3.03"
+ *   "3.0.2" < "3.0.3"
+ *   "3.00" ?? "3.0"
+ *   "3.02" ?? "3.0.3"
+ *   "3.02" ?? "3.0.2"
+ * 
+ * @endhtmlonly + */ +- (NSComparisonResult)versionStringCompare:(NSString *)other { + NSArray *oneComponents = [self componentsSeparatedByString:@"a"]; + NSArray *twoComponents = [other componentsSeparatedByString:@"a"]; + + // The parts before the "a" + NSString *oneMain = [oneComponents objectAtIndex:0]; + NSString *twoMain = [twoComponents objectAtIndex:0]; + + // If main parts are different, return that result, regardless of alpha part + NSComparisonResult mainDiff; + if ((mainDiff = [oneMain compare:twoMain]) != NSOrderedSame) { + return mainDiff; + } + + // At this point the main parts are the same; just deal with alpha stuff + // If one has an alpha part and the other doesn't, the one without is newer + if ([oneComponents count] < [twoComponents count]) { + return NSOrderedDescending; + + } else if ([oneComponents count] > [twoComponents count]) { + return NSOrderedAscending; + + } else if ([oneComponents count] == 1) { + // Neither has an alpha part, and we know the main parts are the same + return NSOrderedSame; + } + + // At this point the main parts are the same and both have alpha parts. Compare the alpha parts + // numerically. If it's not a valid number (including empty string) it's treated as zero. + NSNumber *oneAlpha = [NSNumber numberWithInt:[[oneComponents objectAtIndex:1] intValue]]; + NSNumber *twoAlpha = [NSNumber numberWithInt:[[twoComponents objectAtIndex:1] intValue]]; + return [oneAlpha compare:twoAlpha]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Calculate the md5 hash using CC_MD5. + * + * @returns md5 hash of this string. + */ +- (NSString*)md5Hash { + return [[self dataUsingEncoding:NSUTF8StringEncoding] md5Hash]; +} + + +/////////////////////////////////////////////////////////////////////////////////////////////////// +/** + * Calculate the SHA1 hash using CommonCrypto CC_SHA1. + * + * @returns SHA1 hash of this string. + */ +- (NSString*)sha1Hash { + return [[self dataUsingEncoding:NSUTF8StringEncoding] sha1Hash]; +} + +@end diff --git a/NimbusCore+Additions.h b/NimbusCore+Additions.h new file mode 100644 index 0000000..a08aa56 --- /dev/null +++ b/NimbusCore+Additions.h @@ -0,0 +1,34 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +/** + * @ingroup NimbusCore + * @{ + * + * All category documentation is found in the source files due to limitations of Doxygen. + * Look for the documentation in the Classes tab of the documentation. + */ + +#import +#import + +#import "NimbusCore.h" + +// Additions +#import "NSData+NimbusCore.h" +#import "NSString+NimbusCore.h" + +/**@}*/ diff --git a/NimbusCore.h b/NimbusCore.h new file mode 100644 index 0000000..c4d4154 --- /dev/null +++ b/NimbusCore.h @@ -0,0 +1,71 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +/** + * @defgroup NimbusCore Nimbus Core + * + * Nimbus' Core defines the foundation upon which all other Nimbus features are built. + * Within the core you will find common elements used to build iOS applications + * including in-memory caches, path manipulation, and SDK availability. These features form + * the foundation upon which all other Nimbus libraries are built. + * + *

How Features are Added to the Core

+ * + * As a general rule of thumb, if something is used between multiple independent libraries or + * applications with little variation, it likely qualifies to be added to the Core. + * + *

Exceptions

+ * + * Standalone user interface components are rarely acceptable features to add to the Core. + * For example: photo viewers, pull to refresh, launchers, attributed labels. + * + * Nimbus is not UIKit: we don't have the privilege of being an assumed cost on every iOS + * device. Developers must carefully weigh whether it is worth adding a Nimbus feature - along + * with its dependencies - over building the feature themselves or using another library. This + * means that an incredible amount of care must be placed into deciding what gets added to the + * Core. + * + *

How Features are Removed from the Core

+ * + * It is inevitable that certain aspects of the Core will grow and develop over time. If a + * feature gets to the point where the value of being a separate library is greater than the + * overhead of managing such a library, then the feature should be considered for removal + * from the Core. + * + * Great care must be taken to ensure that Nimbus doesn't become a framework composed of + * hundreds of miniscule libraries. + */ + +#import +#import + +#import "NIBlocks.h" +#import "NICommonMetrics.h" +#import "NIDataStructures.h" +#import "NIDebuggingTools.h" +#import "NIDeviceOrientation.h" +#import "NIError.h" +#import "NIFoundationMethods.h" +#import "NIInMemoryCache.h" +#import "NINetworkActivity.h" +#import "NINonEmptyCollectionTesting.h" +#import "NINonRetainingCollections.h" +#import "NIOperations.h" +#import "NIPaths.h" +#import "NIPreprocessorMacros.h" +#import "NIRuntimeClassModifications.h" +#import "NISDKAvailability.h" +#import "NIState.h" diff --git a/NimbusPhotos.bundle/gfx/default.png b/NimbusPhotos.bundle/gfx/default.png new file mode 100644 index 0000000000000000000000000000000000000000..35d940fb830da23151338dac316f7b39d8306f01 GIT binary patch literal 4027 zcmV;s4@B^ZP)T}ebiRCwC#oojDgR~3fW*mJQR=VGT# z+N6-SF1?6~wg@N%mA2eP)qX+cBKW{B;K%S&5Ep^OMFJEGe4t3EDrzM_Ep21s6xXrG zb35$iImdIx89SN1_qp!piB=p%nTdVQ*=yFj_S$PTetY%xA4VcF8jb`2!eej0FPqo5 z(~E#@aJZ?+n1f)u5^u40D$n=@9xRo?#{uV z7q}b%kbpw~fJ2%GzelkFKzO9qKM?`|AQ=E)hGdAMND>2p02M=O{SzSoKq^8Ia*6;! zM+m9)PlNyfhu{zZU?LerlL?6dK!BPdN;w4U_egdpMlu?4QWBuBFn?je*rGHV4G$55 zgG1=;cct6il|jFUQvfK!87wYJr?VuTr4?y5TL;d;$syS8G7LvU+1uNd54Uf;-Rte& zh;Rr1`D^}GTcR@}t@hFbCPY}Z5&n^{2w`NLg8BNlW~=q4dM5zLmv*}&tE*>aW#!bF zg@wlIU^sFrG8`ur#UiO_{=I4z^9>o68cWM7k%lh-kRPp9M^2wwHeVMP`~9H|heHd8 zFj@cB5rR4cHPtK{wzQ3oSzK&#hX8Vo5Sr5IXzrjP0}pKwoiY#;PsElbKrM0-;~E5+ zJIqKQisFD2KrtMyyGX3pVfr=RWBgW&b(_(=GQUIPl@h$#+{A{ks05hN7>JSqmu*N{{M z0RIufVI8wmQc*}M0(c1#Y!iYJNkstgokMUeccgR~z-NGv|2kwY0|3kfhX4RL1OV`! zLlANZ00rj|bcew$halt-0AN;xAXF*>lqN!OScf81DguDd2*IhkDe)^De@-}U?23## zQLP(m8#`xA+ffhWe={()W$c=2~NR{}aX@Hg?|ey|fAC zqp|J9jzR5xE;AG$8N_Q=vF1^NhW?*0_H^tNe6wuq$=Fm@T#cg#2q#4FvJS1@g9u?R z9y0d1SR&+s<{(})_8()vBVvGDB7|{1g2PQGaM9TF@z)~Ie8ab68S$#I4|r^A93a@P zLupJE3fjB}5yI{Htg+9OD}j#6GT_D5l70y2vbq?m$;60>CNYck$KEL#ralg1t} z_6r$rT>>ct=K{gxHO<0*qVl1;1}_;qQ{wM&&Dbl({$%VgLXjx=iV&RJF4b!;gNEf_ zs)&@*XyKVSiut9nZ64=ZBoXY_p?GMa7M&1w*jPo;#r1_)CR8o2qDjZ^$+(UIY9JYk zvIB>}Es#d+2%fC)&!ELkUp4kz{PO@5C>f;ElHtoT&feF7C*mB)&y00>yaSUA9=a|! z{r^0bathBHd#q-U)P2UjW$ed7%>v9DA-Hr*l4=(w>Gs`IHNCG|P5Iq8QsBT;r2>Q^ zIwsL|Sx+T?zUDup<`rI+YUf5E_Z)%**D*fksQd=xWONHTm{!rrIX7H_%(vB9HXUdd2YQBr6TLelCibTrxcV!)9SoIuk} zyHx5t!&5>PUm9)pqKBkNHda1|5RD&gAB9QjdV=}hBW}%jWP(#kS&{k`Ao<|2ICA=_ zkZdf8KVbTE9&8BBV2$;3M&scigu>>wbx2tjIJx_m#kd2Vz7jg~wA#Mlik zM~)Ff0qbzOblHE*Q?_e-iO0+e-IDx{v0oYc2bUv|2%+XZ6<1}PFa!I%g35<`jQv5V zSI8rYkpDWIZjj2X2wGOJ$*6%cP%~Uej4r)C*dU^h}tkgZJcsGxj zH?&?so4G#DrKq)J5WmA-%OxAuxW(Qj#1~~;=%u-Z9WIIY5W()>`x^G@IuEYoE!58P zgk^$m<$mATZ)7}k61=4lY}etDngomY2thKO;}Oe@wv5}?>&eMwE=NrygYPhZY;-YiL$WL)-t; zf2Cr1WCQtwVp%u#%{Z@c)!0^GKmXnFHAw7qO3E%B^P0`|wKZKpTKx54e9bnEUEkW; z8qT#2E#-fmCBrIfvJ`B@_IfNO>R9Qh(WoX?^_aaDaGiH>BFcqZyzBb?70k5j-QR0|!#`(SMP5dAxL5bqm%Hv~leV7(_|m0r51sNj$e&z&XtPku%$dX1SA6lA{i#bmYpUy)jUF=^*Vr6gphcNc|DVqx@?(V2oOeu zAig^$rMC~~i!){#y9@xiYKD}FTh(!mB?E;g0Ctiga-8;3%E?W1TZTX+gZl)vC?y*F z4{9k#bigQc2rr2s?n#8EL{Z%awJ@y-0!kr7w`@65sW{C#j|qTv4#7Ukn57&_aKPyz zw-rKC3^UiEonnZOKl|*uTs-&jm_R_9VX~Y(oOZ06WYGKpH3$GjNQT)~Xq0f7TMD5` z2LF|c^^pu8X&w65c66M}5M?))LAxKlAhbjSxXnrw=eZ1#WYiDA!{H`%08a?z$N)u11}Q^U zQJsKxJ9>yb0f6N?6mcKaGTth*+p3Tm%^7Zkb~QQ^%Yh3iW6~0UWkl{eFzK+;QL;JQ z^o)!v3DXWh(VhiV76O!7GNhsgPm1B;*a>g~5g;eUAmXj-vP{i~pCitgZpcusVJ!fm zXa@JK|J1OT1+FLn97+ZsVMa-stO5ip8C*-3Q#k~e>L$5GF5naZ3XlvEG20OUd`kwG zy-O*yV-f%XLNpHg-RlrM9)A{c2mqDNA=GV93jp|#3^kkn3=p1X@O%8ZHnSZ8f|d*- z?vjCB1_1CZ83f4yP@H5CN9mrD46ef+T#sG?fQvtfX0{@*dJiN608C2;-yVx=3Q%2# z5Q*b#M-k)#K*5s1WgVsmEFl>H;7EiZZbv4k-h(IsAioGfd>?Y-7yv4*7(7pIq76GhrCoyS@n_nw1Heql;C+*U90EY@lEGKk zWg-Lsm_!Iu`eR<*F=O&G@c}@dA%dsr&y%jplL<`oy+^X=Nk0UD;`~80bBfyh?}20h zfTNNjy@zR&h#;v5P+){0enu*iR0M!qBZR4hgjFKqNK%mq0U*yLf}c@tLSYgB%*r9S z7BkG!G60ZI4&mtfqo*Q3$dW;3)FN2P|4ce2;S>O>D;az=ND&H?0E&tbd^SiSsR#h6 z9D;9$#We+}EJO$Zkcto-X3EdfFC-NK@=PH_lZ^%;hX4RRBLtU%qL~Mrh!6nsix9+b z$E2q7fJh3H0E*NMvzrxJy$6y30H!5_k72J7!(IUjkqqwZF#Q@PHvzyTLSWb{K&3+j zuUSQM2mp}EAy^-XNN+XDr28`06d=Cpwga`ouZix`wWd~iC0SZhZi1_Q6 zM1%kUlL*0g3pKI`068ZSBw`#q0Avv%i0^UmTvUKOa|knYd6w@%ga8235rSVyLx=!S zU5CJ227n4{22Z2hvYN|)O8}^FgphPOa+=F9UH;rQ*5J|tWH0v*%a!9#de@$cSxj!) zG{2wXegR~Le&B9r7a>@L`lGl6X-3K9Rg?ojrs(cqS0t03incDw{Ax60VPQcQ7n{;< zx1?k2+O_xI=Y9d?!}4rU>Ec~0>uaYEzBvHMQ9CS*YYY4R{@G|` zWc;Y0ZO4NMVR2E~&F02nIC${pZZ|UD9J>nuc@Zz-;b;_nxU<{b>+U-j209ID8H`4< zv)5hR9}J9h*yaWS6u`)=!@+PUz1~1Z7G2A{Bo&jr%YI*Nz+C|-JYaC&+-sKMn*e}v z(BvEfK=7dj4N?GLMud>GU?%hj2sA*D$yo*>1OPY$hX4RL1OUJx000gF0PcjtIs|qZ z0K$Y}uK<8#0D#*_hU+)B<;Dm9lR>}7)c^>P;cy_sL4WKJu5E70-py?p3~&elA(GA? hHb^D{K*Ik87y$G1`fFw&+erWb002ovPDHLkV1kPEB)I?p literal 0 HcmV?d00001 diff --git a/NimbusPhotos.bundle/gfx/next.png b/NimbusPhotos.bundle/gfx/next.png new file mode 100644 index 0000000000000000000000000000000000000000..feacfa4654a4ab97bf99266022588075ae2232f8 GIT binary patch literal 241 zcmeAS@N?(olHy`uVBq!ia0vp^Vn8g!!3HGbRrPd%lw^r(L`iUdT1k0gQ7VIDN`6wR zf@f}GdTLN=VoGJ<$y6JlqRE~vjv*Ddl79UEZ_nImF-4-3Cq;sXX{*B-C+=p)1WAG7 zi-!^fEZB}5+F~HVlE<{sW|!5m{|S#Z+2k8;@OJngW|<+ef|;e^hlD`;A=iJw@(sZ} zTbr0KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0003%NklZ6`SeZsJ zZQ4|+Ql$z60doO|co-J&c@kyKg29uHXiuf>WboN$R2&M=As z3|$VqW!ho!6oO!#3V<5Wy0&`oMKX zFja$aj?w_oiU8bC!M_`!=sMV$!sZU##NKQV&KdYVd3Kz8(M?jIZ$#)43&qPSibw%I z2M@PXC+IEm@~VLw*k5#>TQ%@4E`KvIud3+ADVR4SWQjovW@$fM|vRXB}XUexj4L{6H4@=yX6IQtAJ9SE<>h%-M zA9;29BxE9I1t@NrBF1*u$X`~M$IZ`BMUqD#f#GSa+-f1#Js>YKc)I$ztaD0e0suLO BR4B7LEP)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z0003|NklA6RD{9O($bQ3b%JFkSuoB%yB|P5fp#rRid7K=K{06& zTwOJpOeR6FT+3OJ4R+af?>YFC+YS$h=Xu}%J(VoWq@!LKl@~ z8eUt#8BN@wl|m%icrD88yoP&tLL-IJ9fr(Si=GmBGrW!g`elu^JcjGIL8r_)Jgeai z+=s8&4R^xL)@M93E3XbOfc_>;mniPpDF2kISPJazIa2qz5*bTP=6Esu6 z%PwAGH#;_b5N@{i-vGW4jRC$JV5XLe;p9pOBWz-rQieaUjq8+3_l-TA#Gc`4@H2>s zCuaH0&x@B)yi4Q(_)5_ko}{-hL7r3Ebf4J4MM|sr2m3gRJ;RgK3!q;uGQ;ye2DSt6 zH)1Q@Gz~);M=9auJGStU5)Xf27iTe-t`z+a0OL_eVpgx_Q~&?~07*qoM6N<$g0ZN@ Ap8x;= literal 0 HcmV?d00001 diff --git a/NimbusPhotos.h b/NimbusPhotos.h new file mode 100644 index 0000000..023ea1b --- /dev/null +++ b/NimbusPhotos.h @@ -0,0 +1,134 @@ +// +// Copyright 2011 Jeff Verkoeyen +// +// 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. +// + +/** + * @defgroup NimbusPhotos Nimbus Photos + * @{ + * + * Photo viewers are a common, non-trivial feature in many types of iOS apps ranging from + * simple photo viewers to apps that fetch photos from an API. The Nimbus photo album viewer + * is designed to consume minimal amounts of memory and encourage the use of threads to provide + * a high quality user experience that doesn't include any blocking of the UI while images + * are loaded from disk or the network. The photo viewer pre-caches images in an album to either + * side of the current image so that the user will ideally always have a high-quality + * photo experience. + * + *

Adding the Photos Feature to Your Application

+ * + * The Nimbus Photos feature uses a small number of custom photos that are stored in the + * NimbusPhotos bundle. You must add this bundle to your application, ensuring that you select + * the "Create Folder References" option and that the bundle is copied in the + * "Copy Bundle Resources" phase. + * + * The bundle can be found at src/photos/resources/NimbusPhotos.bundle. + * + * + *

Feature Breakdown

+ * + * NIPhotoAlbumScrollView - A paged scroll view that implements a data source similar to that + * of UITableView. This scroll view consumes minimal amounts of memory and is built to be fast + * and responsive. + * + * NIPhotoScrollView - A single page within the NIPhotoAlbumScrollView. This view implements + * the zooming and rotation functionality for a photo. + * + * NIPhotoScrubberView - A scrubber view for skimming through a set of photos. This view + * made its debut by Apple on the iPad in Photos.app. Nimbus' implementation of this view + * is built to be responsive and consume little memory. + * + * NIToolbarPhotoViewController - A skeleton implementation of a view controller that includes + * multiple configurable properties. This controller will show a scrubber on the iPad and + * next/previous arrows on the iPhone. It also provides support for hiding and showing the + * app's chrome. If you wish to use this controller you must simply implement the data source + * for the photo album. NetworkPhotoAlbum in the examples/photos directory demos building + * a data source that fetches its information from the network. + * + * + *

Architecture

+ * + * The architectural design of the photo album view takes inspiration from UITableView. Images + * are requested only when they might become visible and are released when they become + * inaccessible again. Each page of the photo album view is a recycled NIPhotoScrollView. + * These page views handle zooming and panning within a given photo. The photo album view + * NIPhotoAlbumScrollView contains a paging scroll view of these page views and provides + * interfaces for maintaining the orientation during rotations. + * + * The view controller NIToolbarPhotoViewController is provided as a basic implementation of + * functionality that is expected from a photo viewer. This includes: a toolbar with next and + * previous arrows; auto-rotation support; and toggling the chrome. + * + * + *

Example Applications

+ * + *

Network Photo Albums

+ * + *
View the README on GitHub + * + * This sample application demos the use of the multiple photo APIs to fetch photos from public + * photo album and display them in high-definition on the iPad and iPhone. + * + * The following APIs are currently demoed: + * + * - Facebook Graph API + * - Dribbble Shots + * + * Sample location: examples/photos/NetworkPhotoAlbums + * + * + *

Screenshots

+ * + * @image html photos-iphone-example1.png "Screenshot of a basic photo album on the iPhone." + * + * Image source: flickr.com/photos/janekm/360669001 + */ + +/** + * The views used to display photos. + * + * @defgroup Photos-Views Photo Views + * + * NIPhotoAlbumScrollView is the meat of the Nimbus photo viewer's functionality. Contained + * within this view are pages of NIPhotoScrollView views. In your view controller you are + * expected to implement the NIPhotoAlbumScrollViewDataSource in order to provide the photo + * album view with the necessary information for presenting an album. + */ + +/** + * The protocols used to interact with the photo views. + * + * @defgroup Photos-Protocols Photo Protocols + */ + +/** + * Basic photo album view controller implementations. + * + * @defgroup Photos-Controllers Photo View Controllers + * + * The view controllers provided here are not meant to be fully functional view controllers + * on their own. It's up to you to build the data source, whether that be from disk or from + * a network API. + */ + +/**@}*/ + +#import +#import + +#import "NimbusCore.h" +#import "NIToolbarPhotoViewController.h" +#import "NIPhotoAlbumScrollView.h" +#import "NIPhotoScrollView.h" +#import "NIPhotoScrubberView.h" diff --git a/Venue.h b/Venue.h new file mode 100644 index 0000000..0c11a13 --- /dev/null +++ b/Venue.h @@ -0,0 +1,26 @@ +// +// Venue.h +// LocationSample +// +// Created by Daniel Barden on 9/20/11. +// Copyright (c) 2011 None. All rights reserved. +// + +#import +#import + +@interface Venue : NSObject { +@private + double _distance; +} + +@property (nonatomic, retain) NSString *name; +@property (nonatomic, retain) NSArray *photos; +@property (nonatomic, retain) CLLocation *location; +@property (nonatomic, retain) NSString *description; + +- (id)initWithName:(NSString *)name withLatitude:(double)latitude withLongitude:(double)longitude; +- (void)updateDistance:(CLLocation *)coordinate; +- (NSString *)distance; + +@end diff --git a/Venue.m b/Venue.m new file mode 100644 index 0000000..f18d47e --- /dev/null +++ b/Venue.m @@ -0,0 +1,64 @@ +// +// Venue.m +// LocationSample +// +// Created by Daniel Barden on 9/20/11. +// Copyright (c) 2011 None. All rights reserved. +// + +#import "Venue.h" + +@implementation Venue +@synthesize name = _name; +@synthesize photos = _photos; +@synthesize location = _location; +@synthesize description = _description; + +#pragma mark - Initializers + +- (id)init +{ + if ((self =[super init])) { + + } + return self; +} + +- (id)initWithName:(NSString *)name +{ + if ((self = [self init])) { + self.name = name; + } + return self; +} + +- (id)initWithName:(NSString *)name withLatitude:(double)latitude withLongitude:(double)longitude +{ + if ((self = [super init])) { + self.name = name; + _location = [[CLLocation alloc] initWithLatitude:latitude longitude:longitude]; + } + return self; +} + +#pragma mark - Location Methods +- (void)updateDistance:(CLLocation *)coordinate { + double distance = [_location distanceFromLocation:coordinate]; + _distance = distance; + NSLog(@"distance %g", _distance); +} + +- (NSString *)distance +{ + if (_distance == 0) + return nil; + return [NSString stringWithFormat:@"%g metros", _distance]; +} + +- (void)dealloc +{ + [_location release]; + [_name release]; + [super dealloc]; +} +@end