66 changed files with 10248 additions and 0 deletions
-
202Pods/Nimbus/LICENSE
-
13Pods/Nimbus/README.mdown
-
37Pods/Nimbus/src/core/src/NIActions+Subclassing.h
-
422Pods/Nimbus/src/core/src/NIActions.h
-
247Pods/Nimbus/src/core/src/NIActions.m
-
92Pods/Nimbus/src/core/src/NIButtonUtilities.h
-
83Pods/Nimbus/src/core/src/NIButtonUtilities.m
-
172Pods/Nimbus/src/core/src/NICommonMetrics.h
-
69Pods/Nimbus/src/core/src/NICommonMetrics.m
-
194Pods/Nimbus/src/core/src/NIDebuggingTools.h
-
61Pods/Nimbus/src/core/src/NIDebuggingTools.m
-
92Pods/Nimbus/src/core/src/NIDeviceOrientation.h
-
76Pods/Nimbus/src/core/src/NIDeviceOrientation.m
-
54Pods/Nimbus/src/core/src/NIError.h
-
24Pods/Nimbus/src/core/src/NIError.m
-
421Pods/Nimbus/src/core/src/NIFoundationMethods.h
-
314Pods/Nimbus/src/core/src/NIFoundationMethods.m
-
48Pods/Nimbus/src/core/src/NIImageUtilities.h
-
24Pods/Nimbus/src/core/src/NIImageUtilities.m
-
316Pods/Nimbus/src/core/src/NIInMemoryCache.h
-
453Pods/Nimbus/src/core/src/NIInMemoryCache.m
-
101Pods/Nimbus/src/core/src/NINetworkActivity.h
-
171Pods/Nimbus/src/core/src/NINetworkActivity.m
-
58Pods/Nimbus/src/core/src/NINonEmptyCollectionTesting.h
-
35Pods/Nimbus/src/core/src/NINonEmptyCollectionTesting.m
-
65Pods/Nimbus/src/core/src/NINonRetainingCollections.h
-
36Pods/Nimbus/src/core/src/NINonRetainingCollections.m
-
20Pods/Nimbus/src/core/src/NIOperations+Subclassing.h
-
209Pods/Nimbus/src/core/src/NIOperations.h
-
111Pods/Nimbus/src/core/src/NIOperations.m
-
69Pods/Nimbus/src/core/src/NIPaths.h
-
61Pods/Nimbus/src/core/src/NIPaths.m
-
141Pods/Nimbus/src/core/src/NIPreprocessorMacros.h
-
74Pods/Nimbus/src/core/src/NIRuntimeClassModifications.h
-
37Pods/Nimbus/src/core/src/NIRuntimeClassModifications.m
-
257Pods/Nimbus/src/core/src/NISDKAvailability.h
-
72Pods/Nimbus/src/core/src/NISDKAvailability.m
-
224Pods/Nimbus/src/core/src/NISnapshotRotation.h
-
299Pods/Nimbus/src/core/src/NISnapshotRotation.m
-
84Pods/Nimbus/src/core/src/NIState.h
-
58Pods/Nimbus/src/core/src/NIState.m
-
164Pods/Nimbus/src/core/src/NIViewRecycler.h
-
111Pods/Nimbus/src/core/src/NIViewRecycler.m
-
26Pods/Nimbus/src/core/src/NimbusCore+Additions.h
-
120Pods/Nimbus/src/core/src/NimbusCore.h
-
26Pods/Nimbus/src/core/src/UIResponder+NimbusCore.h
-
49Pods/Nimbus/src/core/src/UIResponder+NimbusCore.m
-
123Pods/Nimbus/src/pagingscrollview/src/NIPagingScrollView+Subclassing.h
-
386Pods/Nimbus/src/pagingscrollview/src/NIPagingScrollView.h
-
760Pods/Nimbus/src/pagingscrollview/src/NIPagingScrollView.m
-
36Pods/Nimbus/src/pagingscrollview/src/NIPagingScrollViewPage.h
-
26Pods/Nimbus/src/pagingscrollview/src/NIPagingScrollViewPage.m
-
53Pods/Nimbus/src/pagingscrollview/src/NimbusPagingScrollView.h
-
170Pods/Nimbus/src/photos/src/NIPhotoAlbumScrollView.h
-
221Pods/Nimbus/src/photos/src/NIPhotoAlbumScrollView.m
-
93Pods/Nimbus/src/photos/src/NIPhotoAlbumScrollViewDataSource.h
-
55Pods/Nimbus/src/photos/src/NIPhotoAlbumScrollViewDelegate.h
-
162Pods/Nimbus/src/photos/src/NIPhotoScrollView.h
-
510Pods/Nimbus/src/photos/src/NIPhotoScrollView.m
-
41Pods/Nimbus/src/photos/src/NIPhotoScrollViewDelegate.h
-
31Pods/Nimbus/src/photos/src/NIPhotoScrollViewPhotoSize.h
-
168Pods/Nimbus/src/photos/src/NIPhotoScrubberView.h
-
465Pods/Nimbus/src/photos/src/NIPhotoScrubberView.m
-
201Pods/Nimbus/src/photos/src/NIToolbarPhotoViewController.h
-
533Pods/Nimbus/src/photos/src/NIToolbarPhotoViewController.m
-
122Pods/Nimbus/src/photos/src/NimbusPhotos.h
@ -0,0 +1,202 @@ |
|||||
|
|
||||
|
Apache License |
||||
|
Version 2.0, January 2004 |
||||
|
http://www.apache.org/licenses/ |
||||
|
|
||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION |
||||
|
|
||||
|
1. Definitions. |
||||
|
|
||||
|
"License" shall mean the terms and conditions for use, reproduction, |
||||
|
and distribution as defined by Sections 1 through 9 of this document. |
||||
|
|
||||
|
"Licensor" shall mean the copyright owner or entity authorized by |
||||
|
the copyright owner that is granting the License. |
||||
|
|
||||
|
"Legal Entity" shall mean the union of the acting entity and all |
||||
|
other entities that control, are controlled by, or are under common |
||||
|
control with that entity. For the purposes of this definition, |
||||
|
"control" means (i) the power, direct or indirect, to cause the |
||||
|
direction or management of such entity, whether by contract or |
||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the |
||||
|
outstanding shares, or (iii) beneficial ownership of such entity. |
||||
|
|
||||
|
"You" (or "Your") shall mean an individual or Legal Entity |
||||
|
exercising permissions granted by this License. |
||||
|
|
||||
|
"Source" form shall mean the preferred form for making modifications, |
||||
|
including but not limited to software source code, documentation |
||||
|
source, and configuration files. |
||||
|
|
||||
|
"Object" form shall mean any form resulting from mechanical |
||||
|
transformation or translation of a Source form, including but |
||||
|
not limited to compiled object code, generated documentation, |
||||
|
and conversions to other media types. |
||||
|
|
||||
|
"Work" shall mean the work of authorship, whether in Source or |
||||
|
Object form, made available under the License, as indicated by a |
||||
|
copyright notice that is included in or attached to the work |
||||
|
(an example is provided in the Appendix below). |
||||
|
|
||||
|
"Derivative Works" shall mean any work, whether in Source or Object |
||||
|
form, that is based on (or derived from) the Work and for which the |
||||
|
editorial revisions, annotations, elaborations, or other modifications |
||||
|
represent, as a whole, an original work of authorship. For the purposes |
||||
|
of this License, Derivative Works shall not include works that remain |
||||
|
separable from, or merely link (or bind by name) to the interfaces of, |
||||
|
the Work and Derivative Works thereof. |
||||
|
|
||||
|
"Contribution" shall mean any work of authorship, including |
||||
|
the original version of the Work and any modifications or additions |
||||
|
to that Work or Derivative Works thereof, that is intentionally |
||||
|
submitted to Licensor for inclusion in the Work by the copyright owner |
||||
|
or by an individual or Legal Entity authorized to submit on behalf of |
||||
|
the copyright owner. For the purposes of this definition, "submitted" |
||||
|
means any form of electronic, verbal, or written communication sent |
||||
|
to the Licensor or its representatives, including but not limited to |
||||
|
communication on electronic mailing lists, source code control systems, |
||||
|
and issue tracking systems that are managed by, or on behalf of, the |
||||
|
Licensor for the purpose of discussing and improving the Work, but |
||||
|
excluding communication that is conspicuously marked or otherwise |
||||
|
designated in writing by the copyright owner as "Not a Contribution." |
||||
|
|
||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity |
||||
|
on behalf of whom a Contribution has been received by Licensor and |
||||
|
subsequently incorporated within the Work. |
||||
|
|
||||
|
2. Grant of Copyright License. Subject to the terms and conditions of |
||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
|
copyright license to reproduce, prepare Derivative Works of, |
||||
|
publicly display, publicly perform, sublicense, and distribute the |
||||
|
Work and such Derivative Works in Source or Object form. |
||||
|
|
||||
|
3. Grant of Patent License. Subject to the terms and conditions of |
||||
|
this License, each Contributor hereby grants to You a perpetual, |
||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable |
||||
|
(except as stated in this section) patent license to make, have made, |
||||
|
use, offer to sell, sell, import, and otherwise transfer the Work, |
||||
|
where such license applies only to those patent claims licensable |
||||
|
by such Contributor that are necessarily infringed by their |
||||
|
Contribution(s) alone or by combination of their Contribution(s) |
||||
|
with the Work to which such Contribution(s) was submitted. If You |
||||
|
institute patent litigation against any entity (including a |
||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work |
||||
|
or a Contribution incorporated within the Work constitutes direct |
||||
|
or contributory patent infringement, then any patent licenses |
||||
|
granted to You under this License for that Work shall terminate |
||||
|
as of the date such litigation is filed. |
||||
|
|
||||
|
4. Redistribution. You may reproduce and distribute copies of the |
||||
|
Work or Derivative Works thereof in any medium, with or without |
||||
|
modifications, and in Source or Object form, provided that You |
||||
|
meet the following conditions: |
||||
|
|
||||
|
(a) You must give any other recipients of the Work or |
||||
|
Derivative Works a copy of this License; and |
||||
|
|
||||
|
(b) You must cause any modified files to carry prominent notices |
||||
|
stating that You changed the files; and |
||||
|
|
||||
|
(c) You must retain, in the Source form of any Derivative Works |
||||
|
that You distribute, all copyright, patent, trademark, and |
||||
|
attribution notices from the Source form of the Work, |
||||
|
excluding those notices that do not pertain to any part of |
||||
|
the Derivative Works; and |
||||
|
|
||||
|
(d) If the Work includes a "NOTICE" text file as part of its |
||||
|
distribution, then any Derivative Works that You distribute must |
||||
|
include a readable copy of the attribution notices contained |
||||
|
within such NOTICE file, excluding those notices that do not |
||||
|
pertain to any part of the Derivative Works, in at least one |
||||
|
of the following places: within a NOTICE text file distributed |
||||
|
as part of the Derivative Works; within the Source form or |
||||
|
documentation, if provided along with the Derivative Works; or, |
||||
|
within a display generated by the Derivative Works, if and |
||||
|
wherever such third-party notices normally appear. The contents |
||||
|
of the NOTICE file are for informational purposes only and |
||||
|
do not modify the License. You may add Your own attribution |
||||
|
notices within Derivative Works that You distribute, alongside |
||||
|
or as an addendum to the NOTICE text from the Work, provided |
||||
|
that such additional attribution notices cannot be construed |
||||
|
as modifying the License. |
||||
|
|
||||
|
You may add Your own copyright statement to Your modifications and |
||||
|
may provide additional or different license terms and conditions |
||||
|
for use, reproduction, or distribution of Your modifications, or |
||||
|
for any such Derivative Works as a whole, provided Your use, |
||||
|
reproduction, and distribution of the Work otherwise complies with |
||||
|
the conditions stated in this License. |
||||
|
|
||||
|
5. Submission of Contributions. Unless You explicitly state otherwise, |
||||
|
any Contribution intentionally submitted for inclusion in the Work |
||||
|
by You to the Licensor shall be under the terms and conditions of |
||||
|
this License, without any additional terms or conditions. |
||||
|
Notwithstanding the above, nothing herein shall supersede or modify |
||||
|
the terms of any separate license agreement you may have executed |
||||
|
with Licensor regarding such Contributions. |
||||
|
|
||||
|
6. Trademarks. This License does not grant permission to use the trade |
||||
|
names, trademarks, service marks, or product names of the Licensor, |
||||
|
except as required for reasonable and customary use in describing the |
||||
|
origin of the Work and reproducing the content of the NOTICE file. |
||||
|
|
||||
|
7. Disclaimer of Warranty. Unless required by applicable law or |
||||
|
agreed to in writing, Licensor provides the Work (and each |
||||
|
Contributor provides its Contributions) on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or |
||||
|
implied, including, without limitation, any warranties or conditions |
||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A |
||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the |
||||
|
appropriateness of using or redistributing the Work and assume any |
||||
|
risks associated with Your exercise of permissions under this License. |
||||
|
|
||||
|
8. Limitation of Liability. In no event and under no legal theory, |
||||
|
whether in tort (including negligence), contract, or otherwise, |
||||
|
unless required by applicable law (such as deliberate and grossly |
||||
|
negligent acts) or agreed to in writing, shall any Contributor be |
||||
|
liable to You for damages, including any direct, indirect, special, |
||||
|
incidental, or consequential damages of any character arising as a |
||||
|
result of this License or out of the use or inability to use the |
||||
|
Work (including but not limited to damages for loss of goodwill, |
||||
|
work stoppage, computer failure or malfunction, or any and all |
||||
|
other commercial damages or losses), even if such Contributor |
||||
|
has been advised of the possibility of such damages. |
||||
|
|
||||
|
9. Accepting Warranty or Additional Liability. While redistributing |
||||
|
the Work or Derivative Works thereof, You may choose to offer, |
||||
|
and charge a fee for, acceptance of support, warranty, indemnity, |
||||
|
or other liability obligations and/or rights consistent with this |
||||
|
License. However, in accepting such obligations, You may act only |
||||
|
on Your own behalf and on Your sole responsibility, not on behalf |
||||
|
of any other Contributor, and only if You agree to indemnify, |
||||
|
defend, and hold each Contributor harmless for any liability |
||||
|
incurred by, or claims asserted against, such Contributor by reason |
||||
|
of your accepting any such warranty or additional liability. |
||||
|
|
||||
|
END OF TERMS AND CONDITIONS |
||||
|
|
||||
|
APPENDIX: How to apply the Apache License to your work. |
||||
|
|
||||
|
To apply the Apache License to your work, attach the following |
||||
|
boilerplate notice, with the fields enclosed by brackets "[]" |
||||
|
replaced with your own identifying information. (Don't include |
||||
|
the brackets!) The text should be enclosed in the appropriate |
||||
|
comment syntax for the file format. We also recommend that a |
||||
|
file or class name and description of purpose be included on the |
||||
|
same "printed page" as the copyright notice for easier |
||||
|
identification within third-party archives. |
||||
|
|
||||
|
Copyright [yyyy] [name of copyright owner] |
||||
|
|
||||
|
Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
you may not use this file except in compliance with the License. |
||||
|
You may obtain a copy of the License at |
||||
|
|
||||
|
http://www.apache.org/licenses/LICENSE-2.0 |
||||
|
|
||||
|
Unless required by applicable law or agreed to in writing, software |
||||
|
distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
See the License for the specific language governing permissions and |
||||
|
limitations under the License. |
||||
@ -0,0 +1,13 @@ |
|||||
|
Nimbus is an iOS framework whose feature set grows only as fast as its documentation. |
||||
|
|
||||
|
[](https://travis-ci.org/jverkoey/nimbus) |
||||
|
|
||||
|
Getting Started |
||||
|
--------------- |
||||
|
|
||||
|
- Visit the Nimbus website at [nimbuskit.info](http://nimbuskit.info). |
||||
|
- [Add Nimbus to your project](http://wiki.nimbuskit.info/Add-Nimbus-to-your-project). |
||||
|
- Follow Nimbus' development through its [version history](http://docs.nimbuskit.info/group___version-_history.html). |
||||
|
- See the [latest API diffs](http://docs.nimbuskit.info/group___version-9-3.html). |
||||
|
- Read the [Three20 Migration Guide](http://docs.nimbuskit.info/group___three20-_migration-_guide.html). |
||||
|
- Ask questions and get updates via the [Nimbus mailing list](http://groups.google.com/group/nimbusios). |
||||
@ -0,0 +1,37 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIActions.h" |
||||
|
|
||||
|
@interface NIObjectActions : NSObject |
||||
|
|
||||
|
@property (nonatomic, copy) NIActionBlock tapAction; |
||||
|
@property (nonatomic, copy) NIActionBlock detailAction; |
||||
|
@property (nonatomic, copy) NIActionBlock navigateAction; |
||||
|
|
||||
|
@property (nonatomic) SEL tapSelector; |
||||
|
@property (nonatomic) SEL detailSelector; |
||||
|
@property (nonatomic) SEL navigateSelector; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@interface NIActions () |
||||
|
|
||||
|
@property (nonatomic, weak) id target; |
||||
|
|
||||
|
- (NIObjectActions *)actionForObjectOrClassOfObject:(id<NSObject>)object; |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,422 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
/** |
||||
|
* For attaching actions to objects. |
||||
|
* |
||||
|
* @ingroup NimbusCore |
||||
|
* @defgroup Actions Actions |
||||
|
* @{ |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* @param object An action was performed on this object. |
||||
|
* @param target The target that was attached to the NIActions instance. |
||||
|
* @param indexPath The index path of the object. |
||||
|
*/ |
||||
|
typedef BOOL (^NIActionBlock)(id object, id target, NSIndexPath* indexPath); |
||||
|
|
||||
|
/** |
||||
|
* The attachable types of actions for NIAction. |
||||
|
*/ |
||||
|
typedef NS_OPTIONS(NSUInteger, NIActionType) { |
||||
|
NIActionTypeNone = 0, |
||||
|
NIActionTypeTap = 1 << 0, |
||||
|
NIActionTypeDetail = 1 << 1, |
||||
|
NIActionTypeNavigate = 1 << 2, |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* The NIActions class provides a generic interface for attaching actions to objects. |
||||
|
* |
||||
|
* NIActions are used to implement user interaction in UITableViews and UICollectionViews via the |
||||
|
* corresponding classes (NITableViewActions and NICollectionViewActions) in the respective |
||||
|
* feature. NIActions separates the necessity |
||||
|
* |
||||
|
* <h3>Types of Actions</h3> |
||||
|
* |
||||
|
* The three primary types of actions are: |
||||
|
* |
||||
|
* - buttons, |
||||
|
* - detail views, |
||||
|
* - and pushing a new controller onto the navigation controller. |
||||
|
* |
||||
|
* <h3>Attaching Actions</h3> |
||||
|
* |
||||
|
* Actions may be attached to specific instances of objects or to entire classes of objects. When |
||||
|
* an action is attached to both a class of object and an instance of that class, only the instance |
||||
|
* action should be executed. |
||||
|
* |
||||
|
* All attachment methods return the object that was provided. This makes it simple to attach |
||||
|
* actions within an array creation statement. |
||||
|
* |
||||
|
* Actions come in two forms: blocks and selector invocations. Both can be attached to an object |
||||
|
* for each type of action and both will be executed, with the block being executed first. Blocks |
||||
|
* should be used for simple executions while selectors should be used when the action is complex. |
||||
|
* |
||||
|
* The following is an example of using NITableViewActions: |
||||
|
* |
||||
|
@code |
||||
|
NSArray *objects = @[ |
||||
|
[NITitleCellObject objectWithTitle:@"Implicit tap handler"], |
||||
|
[self.actions attachToObject:[NITitleCellObject objectWithTitle:@"Explicit tap handler"] |
||||
|
tapBlock: |
||||
|
^BOOL(id object, id target) { |
||||
|
NSLog(@"Object was tapped with an explicit action: %@", object); |
||||
|
}] |
||||
|
]; |
||||
|
|
||||
|
[self.actions attachToClass:[NITitleCellObject class] |
||||
|
tapBlock: |
||||
|
^BOOL(id object, id target) { |
||||
|
NSLog(@"Object was tapped: %@", object); |
||||
|
}]; |
||||
|
@endcode |
||||
|
* |
||||
|
*/ |
||||
|
@interface NIActions : NSObject |
||||
|
|
||||
|
// Designated initializer. |
||||
|
- (id)initWithTarget:(id)target; |
||||
|
|
||||
|
#pragma mark Mapping Objects |
||||
|
|
||||
|
- (id)attachToObject:(id<NSObject>)object tapBlock:(NIActionBlock)action; |
||||
|
- (id)attachToObject:(id<NSObject>)object detailBlock:(NIActionBlock)action; |
||||
|
- (id)attachToObject:(id<NSObject>)object navigationBlock:(NIActionBlock)action; |
||||
|
|
||||
|
- (id)attachToObject:(id<NSObject>)object tapSelector:(SEL)selector; |
||||
|
- (id)attachToObject:(id<NSObject>)object detailSelector:(SEL)selector; |
||||
|
- (id)attachToObject:(id<NSObject>)object navigationSelector:(SEL)selector; |
||||
|
|
||||
|
#pragma mark Mapping Classes |
||||
|
|
||||
|
- (void)attachToClass:(Class)aClass tapBlock:(NIActionBlock)action; |
||||
|
- (void)attachToClass:(Class)aClass detailBlock:(NIActionBlock)action; |
||||
|
- (void)attachToClass:(Class)aClass navigationBlock:(NIActionBlock)action; |
||||
|
|
||||
|
- (void)attachToClass:(Class)aClass tapSelector:(SEL)selector; |
||||
|
- (void)attachToClass:(Class)aClass detailSelector:(SEL)selector; |
||||
|
- (void)attachToClass:(Class)aClass navigationSelector:(SEL)selector; |
||||
|
|
||||
|
#pragma mark Object State |
||||
|
|
||||
|
- (BOOL)isObjectActionable:(id<NSObject>)object; |
||||
|
- (NIActionType)attachedActionTypesForObject:(id<NSObject>)object; |
||||
|
|
||||
|
+ (id)objectFromKeyClass:(Class)keyClass map:(NSMutableDictionary *)map; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* Returns a block that pushes an instance of the controllerClass onto the navigation stack. |
||||
|
* |
||||
|
* Allocates an instance of the controller class and calls the init selector. |
||||
|
* |
||||
|
* The target property of the NIActions instance must be an instance of UIViewController |
||||
|
* with an attached navigationController. |
||||
|
* |
||||
|
* @param controllerClass The class of controller to instantiate. |
||||
|
*/ |
||||
|
NIActionBlock NIPushControllerAction(Class controllerClass); |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
|
|
||||
|
/** The protocol for a data source that can be used with NIActions. */ |
||||
|
@protocol NIActionsDataSource <NSObject> |
||||
|
|
||||
|
/** |
||||
|
* The object located at the given indexPath. |
||||
|
* |
||||
|
* @param indexPath The index path of the requested object. |
||||
|
*/ |
||||
|
- (id)objectAtIndexPath:(NSIndexPath *)indexPath; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** @name Creating Table View Actions */ |
||||
|
|
||||
|
/** |
||||
|
* Initializes a newly allocated table view actions object with the given controller. |
||||
|
* |
||||
|
* @attention This method is deprecated. Use the new method |
||||
|
* @link NIActions::initWithTarget: initWithTarget:@endlink. |
||||
|
* |
||||
|
* The controller is stored as a weak reference internally. |
||||
|
* |
||||
|
* @param controller The controller that will be used in action blocks. |
||||
|
* @fn NIActions::initWithController: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Initializes a newly allocated table view actions object with the given target. |
||||
|
* |
||||
|
* This is the designated initializer. |
||||
|
* |
||||
|
* The target is stored as a weak reference internally. |
||||
|
* |
||||
|
* @param target The target that will be provided to action blocks and on which selectors will |
||||
|
* be performed. |
||||
|
* @fn NIActions::initWithTarget: |
||||
|
*/ |
||||
|
|
||||
|
/** @name Mapping Objects */ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a tap action to the given object. |
||||
|
* |
||||
|
* A cell with an attached tap action will have its selectionStyle set to |
||||
|
* @c tableViewCellSelectionStyle when the cell is displayed. |
||||
|
* |
||||
|
* The action will be executed when the object's corresponding cell is tapped. The object argument |
||||
|
* of the block will be the object to which this action was attached. The target argument of the |
||||
|
* block will be this receiver's @c target. |
||||
|
* |
||||
|
* Return NO if the tap action is used to present a modal view controller. This provides a visual |
||||
|
* reminder to the user when the modal controller is dismissed as to which cell was tapped to invoke |
||||
|
* the modal controller. |
||||
|
* |
||||
|
* The tap action will be invoked first, followed by the navigation action if one is attached. |
||||
|
* |
||||
|
* @param object The object to attach the action to. This object must be contained within |
||||
|
* an NITableViewModel. |
||||
|
* @param action The tap action block. |
||||
|
* @returns The object that you attached this action to. |
||||
|
* @fn NIActions::attachToObject:tapBlock: |
||||
|
* @sa NIActions::attachToObject:tapSelector: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a detail action to the given object. |
||||
|
* |
||||
|
* When a cell with a detail action is displayed, its accessoryType will be set to |
||||
|
* UITableViewCellAccessoryDetailDisclosureButton. |
||||
|
* |
||||
|
* When a cell's detail button is tapped, the detail action block will be executed. The return |
||||
|
* value of the block is ignored. |
||||
|
* |
||||
|
* @param object The object to attach the action to. This object must be contained within |
||||
|
* an NITableViewModel. |
||||
|
* @param action The detail action block. |
||||
|
* @returns The object that you attached this action to. |
||||
|
* @fn NIActions::attachToObject:detailBlock: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a navigation action to the given object. |
||||
|
* |
||||
|
* When a cell with a navigation action is displayed, its accessoryType will be set to |
||||
|
* UITableViewCellAccessoryDisclosureIndicator if there is no detail action, otherwise the |
||||
|
* detail disclosure indicator takes precedence. |
||||
|
* |
||||
|
* When a cell with a navigation action is tapped the navigation block will be executed. |
||||
|
* |
||||
|
* If a tap action also exists for this object then the tap action will be executed first, followed |
||||
|
* by the navigation action. |
||||
|
* |
||||
|
* @param object The object to attach the action to. This object must be contained within |
||||
|
* an NITableViewModel. |
||||
|
* @param action The navigation action block. |
||||
|
* @returns The object that you attached this action to. |
||||
|
* @fn NIActions::attachToObject:navigationBlock: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a tap selector to the given object. |
||||
|
* |
||||
|
* The method signature for the selector is: |
||||
|
@code |
||||
|
- (BOOL)didTapObject:(id)object; |
||||
|
@endcode |
||||
|
* |
||||
|
* A cell with an attached tap action will have its selectionStyle set to |
||||
|
* @c tableViewCellSelectionStyle when the cell is displayed. |
||||
|
* |
||||
|
* The selector will be performed on the action object's target when a cell with a tap selector is |
||||
|
* tapped, unless that selector does not exist on the @c target in which case nothing happens. |
||||
|
* |
||||
|
* If the selector invocation returns YES then the cell will be deselected immediately after the |
||||
|
* invocation completes its execution. If NO is returned then the cell's selection will remain. |
||||
|
* |
||||
|
* Return NO if the tap action is used to present a modal view controller. This provides a visual |
||||
|
* reminder to the user when the modal controller is dismissed as to which cell was tapped to invoke |
||||
|
* the modal controller. |
||||
|
* |
||||
|
* The tap action will be invoked first, followed by the navigation action if one is attached. |
||||
|
* |
||||
|
* @param object The object to attach the selector to. This object must be contained within |
||||
|
* an NITableViewModel. |
||||
|
* @param selector The selector that will be invoked by this action. |
||||
|
* @returns The object that you attached this action to. |
||||
|
* @fn NIActions::attachToObject:tapSelector: |
||||
|
* @sa NIActions::attachToObject:tapBlock: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a detail selector to the given object. |
||||
|
* |
||||
|
* The method signature for the selector is: |
||||
|
@code |
||||
|
- (void)didTapObject:(id)object; |
||||
|
@endcode |
||||
|
* |
||||
|
* A cell with an attached tap action will have its selectionStyle set to |
||||
|
* @c tableViewCellSelectionStyle and its accessoryType set to |
||||
|
* @c UITableViewCellAccessoryDetailDisclosureButton when the cell is displayed. |
||||
|
* |
||||
|
* The selector will be performed on the action object's target when a cell with a detail selector's |
||||
|
* accessory indicator is tapped, unless that selector does not exist on the @c target in which |
||||
|
* case nothing happens. |
||||
|
* |
||||
|
* @param object The object to attach the selector to. This object must be contained within |
||||
|
* an NITableViewModel. |
||||
|
* @param selector The selector that will be invoked by this action. |
||||
|
* @returns The object that you attached this action to. |
||||
|
* @fn NIActions::attachToObject:detailSelector: |
||||
|
* @sa NIActions::attachToObject:detailBlock: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a navigation selector to the given object. |
||||
|
* |
||||
|
* The method signature for the selector is: |
||||
|
@code |
||||
|
- (void)didTapObject:(id)object; |
||||
|
@endcode |
||||
|
* |
||||
|
* A cell with an attached navigation action will have its selectionStyle set to |
||||
|
* @c tableViewCellSelectionStyle and its accessoryType set to |
||||
|
* @c UITableViewCellAccessoryDetailDisclosureButton, unless it also has an attached detail action, |
||||
|
* in which case its accessoryType will be set to @c UITableViewCellAccessoryDisclosureIndicator |
||||
|
* when the cell is displayed. |
||||
|
* |
||||
|
* The selector will be performed on the action object's target when a cell with a navigation |
||||
|
* selector is tapped, unless that selector does not exist on the @c target in which case nothing |
||||
|
* happens. |
||||
|
* |
||||
|
* @param object The object to attach the selector to. This object must be contained within |
||||
|
* an NITableViewModel. |
||||
|
* @param selector The selector that will be invoked by this action. |
||||
|
* @returns The object that you attached this action to. |
||||
|
* @fn NIActions::attachToObject:navigationSelector: |
||||
|
* @sa NIActions::attachToObject:navigationBlock: |
||||
|
*/ |
||||
|
|
||||
|
/** @name Mapping Classes */ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a tap block to a class. |
||||
|
* |
||||
|
* This method behaves similarly to attachToObject:tapBlock: except it attaches a tap action to |
||||
|
* all instances and subclassed instances of a given class. |
||||
|
* |
||||
|
* @param aClass The class to attach the action to. |
||||
|
* @param action The tap action block. |
||||
|
* @fn NIActions::attachToClass:tapBlock: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a detail block to a class. |
||||
|
* |
||||
|
* This method behaves similarly to attachToObject:detailBlock: except it attaches a detail action |
||||
|
* to all instances and subclassed instances of a given class. |
||||
|
* |
||||
|
* @param aClass The class to attach the action to. |
||||
|
* @param action The detail action block. |
||||
|
* @fn NIActions::attachToClass:detailBlock: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a navigation block to a class. |
||||
|
* |
||||
|
* This method behaves similarly to attachToObject:navigationBlock: except it attaches a navigation |
||||
|
* action to all instances and subclassed instances of a given class. |
||||
|
* |
||||
|
* @param aClass The class to attach the action to. |
||||
|
* @param action The navigation action block. |
||||
|
* @fn NIActions::attachToClass:navigationBlock: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a tap selector to a class. |
||||
|
* |
||||
|
* This method behaves similarly to attachToObject:tapBlock: except it attaches a tap action to |
||||
|
* all instances and subclassed instances of a given class. |
||||
|
* |
||||
|
* @param aClass The class to attach the action to. |
||||
|
* @param selector The tap selector. |
||||
|
* @fn NIActions::attachToClass:tapSelector: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a detail selector to a class. |
||||
|
* |
||||
|
* This method behaves similarly to attachToObject:detailBlock: except it attaches a detail action |
||||
|
* to all instances and subclassed instances of a given class. |
||||
|
* |
||||
|
* @param aClass The class to attach the action to. |
||||
|
* @param selector The tap selector. |
||||
|
* @fn NIActions::attachToClass:detailSelector: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Attaches a navigation selector to a class. |
||||
|
* |
||||
|
* This method behaves similarly to attachToObject:navigationBlock: except it attaches a navigation |
||||
|
* action to all instances and subclassed instances of a given class. |
||||
|
* |
||||
|
* @param aClass The class to attach the action to. |
||||
|
* @param selector The tap selector. |
||||
|
* @fn NIActions::attachToClass:navigationSelector: |
||||
|
*/ |
||||
|
|
||||
|
/** @name Object State */ |
||||
|
|
||||
|
/** |
||||
|
* Returns whether or not the object has any actions attached to it. |
||||
|
* |
||||
|
* @fn NIActions::isObjectActionable: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Returns a bitmask of flags indicating the types of actions attached to the provided object. |
||||
|
* |
||||
|
* @fn NIActions::attachedActionTypesForObject: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Returns a mapped object from the given key class. |
||||
|
* |
||||
|
* If the key class is a subclass of any mapped key classes, the nearest ancestor class's mapped |
||||
|
* object will be returned and keyClass will be added to the map for future accesses. |
||||
|
* |
||||
|
* @param keyClass The key class that will be used to find the mapping in map. |
||||
|
* @param map A map of key classes to classes. May be modified if keyClass is a subclass of |
||||
|
* any existing key classes. |
||||
|
* @returns The mapped object if a match for keyClass was found in map. nil is returned |
||||
|
* otherwise. |
||||
|
* @fn NIActions::objectFromKeyClass:map: |
||||
|
*/ |
||||
|
|
||||
|
/**@}*/// End of Actions ////////////////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,247 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIActions.h" |
||||
|
#import "NIActions+Subclassing.h" |
||||
|
|
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIDebuggingTools.h" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
@interface NIActions () |
||||
|
|
||||
|
@property (nonatomic, strong) NSMutableDictionary* objectToAction; |
||||
|
@property (nonatomic, strong) NSMutableDictionary* classToAction; |
||||
|
@property (nonatomic, strong) NSMutableSet* objectSet; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation NIActions |
||||
|
|
||||
|
- (id)initWithTarget:(id)target { |
||||
|
if ((self = [super init])) { |
||||
|
_target = target; |
||||
|
|
||||
|
_objectToAction = [[NSMutableDictionary alloc] init]; |
||||
|
_classToAction = [[NSMutableDictionary alloc] init]; |
||||
|
_objectSet = [[NSMutableSet alloc] init]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (id)init { |
||||
|
return [self initWithTarget:nil]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Private |
||||
|
|
||||
|
- (id)keyForObject:(id<NSObject>)object { |
||||
|
return @(object.hash); |
||||
|
} |
||||
|
|
||||
|
// |
||||
|
// actionForObject: and actionForClass: are used when attaching actions to objects and classes and |
||||
|
// will always return an NIObjectActions object. These methods should not be used for determining |
||||
|
// whether an action is attached to a given object or class. |
||||
|
// |
||||
|
// actionForObjectOrClassOfObject: determines whether an action has been attached to an object |
||||
|
// or class of object and then returns the NIObjectActions or nil if no actions have been attached. |
||||
|
// |
||||
|
|
||||
|
// Retrieves an NIObjectActions object for the given object or creates one if it doesn't yet exist |
||||
|
// so that actions may be attached. |
||||
|
- (NIObjectActions *)actionForObject:(id<NSObject>)object { |
||||
|
id key = [self keyForObject:object]; |
||||
|
NIObjectActions* action = [self.objectToAction objectForKey:key]; |
||||
|
if (nil == action) { |
||||
|
action = [[NIObjectActions alloc] init]; |
||||
|
[self.objectToAction setObject:action forKey:key]; |
||||
|
} |
||||
|
return action; |
||||
|
} |
||||
|
|
||||
|
// Retrieves an NIObjectActions object for the given class or creates one if it doesn't yet exist |
||||
|
// so that actions may be attached. |
||||
|
- (NIObjectActions *)actionForClass:(Class)class { |
||||
|
NIObjectActions* action = [self.classToAction objectForKey:class]; |
||||
|
if (nil == action) { |
||||
|
action = [[NIObjectActions alloc] init]; |
||||
|
[self.classToAction setObject:action forKey:(id<NSCopying>)class]; |
||||
|
} |
||||
|
return action; |
||||
|
} |
||||
|
|
||||
|
// Fetches any attached actions for a given object. |
||||
|
- (NIObjectActions *)actionForObjectOrClassOfObject:(id<NSObject>)object { |
||||
|
id key = [self keyForObject:object]; |
||||
|
NIObjectActions* action = [self.objectToAction objectForKey:key]; |
||||
|
if (nil == action) { |
||||
|
action = [self.class objectFromKeyClass:object.class map:self.classToAction]; |
||||
|
} |
||||
|
return action; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Public |
||||
|
|
||||
|
- (id)attachToObject:(id<NSObject>)object tapBlock:(NIActionBlock)action { |
||||
|
[self.objectSet addObject:object]; |
||||
|
[self actionForObject:object].tapAction = action; |
||||
|
return object; |
||||
|
} |
||||
|
|
||||
|
- (id)attachToObject:(id<NSObject>)object detailBlock:(NIActionBlock)action { |
||||
|
[self.objectSet addObject:object]; |
||||
|
[self actionForObject:object].detailAction = action; |
||||
|
return object; |
||||
|
} |
||||
|
|
||||
|
- (id)attachToObject:(id<NSObject>)object navigationBlock:(NIActionBlock)action { |
||||
|
[self.objectSet addObject:object]; |
||||
|
[self actionForObject:object].navigateAction = action; |
||||
|
return object; |
||||
|
} |
||||
|
|
||||
|
- (id)attachToObject:(id<NSObject>)object tapSelector:(SEL)selector { |
||||
|
[self.objectSet addObject:object]; |
||||
|
[self actionForObject:object].tapSelector = selector; |
||||
|
return object; |
||||
|
} |
||||
|
|
||||
|
- (id)attachToObject:(id<NSObject>)object detailSelector:(SEL)selector { |
||||
|
[self.objectSet addObject:object]; |
||||
|
[self actionForObject:object].detailSelector = selector; |
||||
|
return object; |
||||
|
} |
||||
|
|
||||
|
- (id)attachToObject:(id<NSObject>)object navigationSelector:(SEL)selector { |
||||
|
[self.objectSet addObject:object]; |
||||
|
[self actionForObject:object].navigateSelector = selector; |
||||
|
return object; |
||||
|
} |
||||
|
|
||||
|
- (void)attachToClass:(Class)aClass tapBlock:(NIActionBlock)action { |
||||
|
[self actionForClass:aClass].tapAction = action; |
||||
|
} |
||||
|
|
||||
|
- (void)attachToClass:(Class)aClass detailBlock:(NIActionBlock)action { |
||||
|
[self actionForClass:aClass].detailAction = action; |
||||
|
} |
||||
|
|
||||
|
- (void)attachToClass:(Class)aClass navigationBlock:(NIActionBlock)action { |
||||
|
[self actionForClass:aClass].navigateAction = action; |
||||
|
} |
||||
|
|
||||
|
- (void)attachToClass:(Class)aClass tapSelector:(SEL)selector { |
||||
|
[self actionForClass:aClass].tapSelector = selector; |
||||
|
} |
||||
|
|
||||
|
- (void)attachToClass:(Class)aClass detailSelector:(SEL)selector { |
||||
|
[self actionForClass:aClass].detailSelector = selector; |
||||
|
} |
||||
|
|
||||
|
- (void)attachToClass:(Class)aClass navigationSelector:(SEL)selector { |
||||
|
[self actionForClass:aClass].navigateSelector = selector; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)isObjectActionable:(id<NSObject>)object { |
||||
|
return NIActionTypeNone != [self attachedActionTypesForObject:object]; |
||||
|
} |
||||
|
|
||||
|
- (NIActionType)attachedActionTypesForObject:(id<NSObject>)object { |
||||
|
if (nil == object) { |
||||
|
return NIActionTypeNone; |
||||
|
} |
||||
|
|
||||
|
NIObjectActions* actions = [self actionForObjectOrClassOfObject:object]; |
||||
|
NIActionType attachedActionTypes = 0; |
||||
|
if (actions.tapAction || actions.tapSelector) { |
||||
|
attachedActionTypes |= NIActionTypeTap; |
||||
|
} |
||||
|
if (actions.detailAction || actions.detailSelector) { |
||||
|
attachedActionTypes |= NIActionTypeDetail; |
||||
|
} |
||||
|
if (actions.navigateAction || actions.navigateSelector) { |
||||
|
attachedActionTypes |= NIActionTypeNavigate; |
||||
|
} |
||||
|
return attachedActionTypes; |
||||
|
} |
||||
|
|
||||
|
+ (id)objectFromKeyClass:(Class)keyClass map:(NSMutableDictionary *)map { |
||||
|
id object = [map objectForKey:keyClass]; |
||||
|
|
||||
|
if (nil == object) { |
||||
|
// No mapping found for this key class, but it may be a subclass of another object that does |
||||
|
// have a mapping, so let's see what we can find. |
||||
|
Class superClass = nil; |
||||
|
for (Class class in map.allKeys) { |
||||
|
// We want to find the lowest node in the class hierarchy so that we pick the lowest ancestor |
||||
|
// in the hierarchy tree. |
||||
|
if ([keyClass isSubclassOfClass:class] |
||||
|
&& (nil == superClass || [class isSubclassOfClass:superClass])) { |
||||
|
superClass = class; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (nil != superClass) { |
||||
|
object = [map objectForKey:superClass]; |
||||
|
|
||||
|
// Add this subclass to the map so that next time this result is instant. |
||||
|
[map setObject:object forKey:(id<NSCopying>)keyClass]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (nil == object) { |
||||
|
// We couldn't find a mapping at all so let's add an empty mapping. |
||||
|
[map setObject:[NSNull class] forKey:(id<NSCopying>)keyClass]; |
||||
|
|
||||
|
} else if (object == [NSNull class]) { |
||||
|
// Don't return null mappings. |
||||
|
object = nil; |
||||
|
} |
||||
|
|
||||
|
return object; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation NIObjectActions |
||||
|
@end |
||||
|
|
||||
|
NIActionBlock NIPushControllerAction(Class controllerClass) { |
||||
|
return [^(id object, id target, NSIndexPath* indexPath) { |
||||
|
// You must initialize the actions object with initWithTarget: and pass a valid |
||||
|
// controller. |
||||
|
NIDASSERT(nil != target); |
||||
|
NIDASSERT([target isKindOfClass:[UIViewController class]]); |
||||
|
UIViewController *controller = target; |
||||
|
|
||||
|
if (nil != controller && [controller isKindOfClass:[UIViewController class]]) { |
||||
|
// No navigation controller to push this new controller; this controller |
||||
|
// is going to be lost. |
||||
|
NIDASSERT(nil != controller.navigationController); |
||||
|
|
||||
|
UIViewController* controllerToPush = [[controllerClass alloc] init]; |
||||
|
[controller.navigationController pushViewController:controllerToPush |
||||
|
animated:YES]; |
||||
|
} |
||||
|
|
||||
|
return NO; |
||||
|
} copy]; |
||||
|
} |
||||
@ -0,0 +1,92 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* For manipulating UIButton objects. |
||||
|
* |
||||
|
* @ingroup NimbusCore |
||||
|
* @defgroup Button-Utilities Button Utilities |
||||
|
* @{ |
||||
|
* |
||||
|
* The methods provided here make it possible to specify different properties for different button |
||||
|
* states in a scalable way. For example, you can define a stylesheet class that has a number of |
||||
|
* class methods that return the various properties for buttons. |
||||
|
* |
||||
|
@code |
||||
|
@implementation Stylesheet |
||||
|
|
||||
|
+ (UIImage *)backgroundImageForButtonWithState:(UIControlState)state { |
||||
|
if (state & UIControlStateHighlighted) { |
||||
|
return [UIImage imageNamed:@"button_highlighted"]; |
||||
|
|
||||
|
} else if (state == UIControlStateNormal) { |
||||
|
return [UIImage imageNamed:@"button"]; |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
// The result of the implementation above will set the background images for the button's |
||||
|
// highlighted and default states. |
||||
|
NIApplyBackgroundImageSelectorToButton(@selector(backgroundImageForButtonWithState:), |
||||
|
[Stylesheet class], |
||||
|
button); |
||||
|
@endcode |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Sets the images for a button's states. |
||||
|
* |
||||
|
* @param selector A selector of the form: |
||||
|
* (UIImage *)imageWithControlState:(UIControlState)controlState |
||||
|
* @param target The target upon which the selector will be invoked. |
||||
|
* @param button The button object whose properties should be modified. |
||||
|
*/ |
||||
|
void NIApplyImageSelectorToButton(SEL selector, id target, UIButton* button); |
||||
|
|
||||
|
/** |
||||
|
* Sets the background images for a button's states. |
||||
|
* |
||||
|
* @param selector A selector of the form: |
||||
|
* (UIImage *)backgroundImageWithControlState:(UIControlState)controlState |
||||
|
* @param target The target upon which the selector will be invoked. |
||||
|
* @param button The button object whose properties should be modified. |
||||
|
*/ |
||||
|
void NIApplyBackgroundImageSelectorToButton(SEL selector, id target, UIButton* button); |
||||
|
|
||||
|
/** |
||||
|
* Sets the title colors for a button's states. |
||||
|
* |
||||
|
* @param selector A selector of the form: |
||||
|
* (UIColor *)colorWithControlState:(UIControlState)controlState |
||||
|
* @param target The target upon which the selector will be invoked. |
||||
|
* @param button The button object whose properties should be modified. |
||||
|
*/ |
||||
|
void NIApplyTitleColorSelectorToButton(SEL selector, id target, UIButton* button); |
||||
|
|
||||
|
/**@}*/// End of Button Utilities ///////////////////////////////////////////////////////////////// |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
@ -0,0 +1,83 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIButtonUtilities.h" |
||||
|
|
||||
|
void NIApplyImageSelectorToButton(SEL selector, id target, UIButton* button) { |
||||
|
typedef UIImage* (*ImageMethod)(id, SEL, UIControlState); |
||||
|
ImageMethod method = (ImageMethod)[target methodForSelector:selector]; |
||||
|
UIImage* image = nil; |
||||
|
|
||||
|
image = method(target, selector, UIControlStateNormal); |
||||
|
[button setImage:image forState:UIControlStateNormal]; |
||||
|
|
||||
|
image = method(target, selector, UIControlStateHighlighted); |
||||
|
[button setImage:image forState:UIControlStateHighlighted]; |
||||
|
|
||||
|
image = method(target, selector, UIControlStateDisabled); |
||||
|
[button setImage:image forState:UIControlStateDisabled]; |
||||
|
|
||||
|
image = method(target, selector, UIControlStateSelected); |
||||
|
[button setImage:image forState:UIControlStateSelected]; |
||||
|
|
||||
|
UIControlState selectedHighlightState = UIControlStateSelected | UIControlStateHighlighted; |
||||
|
image = method(target, selector, selectedHighlightState); |
||||
|
[button setImage:image forState:selectedHighlightState]; |
||||
|
} |
||||
|
|
||||
|
void NIApplyBackgroundImageSelectorToButton(SEL selector, id target, UIButton* button) { |
||||
|
typedef UIImage* (*ImageMethod)(id, SEL, UIControlState); |
||||
|
ImageMethod method = (ImageMethod)[target methodForSelector:selector]; |
||||
|
UIImage* image = nil; |
||||
|
|
||||
|
image = method(target, selector, UIControlStateNormal); |
||||
|
[button setBackgroundImage:image forState:UIControlStateNormal]; |
||||
|
|
||||
|
image = method(target, selector, UIControlStateHighlighted); |
||||
|
[button setBackgroundImage:image forState:UIControlStateHighlighted]; |
||||
|
|
||||
|
image = method(target, selector, UIControlStateDisabled); |
||||
|
[button setBackgroundImage:image forState:UIControlStateDisabled]; |
||||
|
|
||||
|
image = method(target, selector, UIControlStateSelected); |
||||
|
[button setBackgroundImage:image forState:UIControlStateSelected]; |
||||
|
|
||||
|
UIControlState selectedHighlightState = UIControlStateSelected | UIControlStateHighlighted; |
||||
|
image = method(target, selector, selectedHighlightState); |
||||
|
[button setBackgroundImage:image forState:selectedHighlightState]; |
||||
|
} |
||||
|
|
||||
|
void NIApplyTitleColorSelectorToButton(SEL selector, id target, UIButton* button) { |
||||
|
typedef UIColor* (*ColorMethod)(id, SEL, UIControlState); |
||||
|
ColorMethod method = (ColorMethod)[target methodForSelector:selector]; |
||||
|
UIColor* color = nil; |
||||
|
|
||||
|
color = method(target, selector, UIControlStateNormal); |
||||
|
[button setTitleColor:color forState:UIControlStateNormal]; |
||||
|
|
||||
|
color = method(target, selector, UIControlStateHighlighted); |
||||
|
[button setTitleColor:color forState:UIControlStateHighlighted]; |
||||
|
|
||||
|
color = method(target, selector, UIControlStateDisabled); |
||||
|
[button setTitleColor:color forState:UIControlStateDisabled]; |
||||
|
|
||||
|
color = method(target, selector, UIControlStateSelected); |
||||
|
[button setTitleColor:color forState:UIControlStateSelected]; |
||||
|
|
||||
|
UIControlState selectedHighlightState = UIControlStateSelected | UIControlStateHighlighted; |
||||
|
color = method(target, selector, selectedHighlightState); |
||||
|
[button setTitleColor:color forState:selectedHighlightState]; |
||||
|
} |
||||
@ -0,0 +1,172 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIPreprocessorMacros.h" |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* For common system metrics. |
||||
|
* |
||||
|
* If you work with system metrics in any way it can be a pain in the ass 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. |
||||
|
* |
||||
|
* <h2>What Qualifies as a Common Metric</h2> |
||||
|
* |
||||
|
* Common metrics are 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. |
||||
|
* |
||||
|
* <h2>Examples</h2> |
||||
|
* |
||||
|
* <h3>Positioning a Toolbar</h3> |
||||
|
* |
||||
|
* 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 |
||||
|
* @{ |
||||
|
*/ |
||||
|
|
||||
|
#ifndef UIViewAutoresizingFlexibleMargins |
||||
|
#define UIViewAutoresizingFlexibleMargins (UIViewAutoresizingFlexibleLeftMargin \ |
||||
|
| UIViewAutoresizingFlexibleTopMargin \ |
||||
|
| UIViewAutoresizingFlexibleRightMargin \ |
||||
|
| UIViewAutoresizingFlexibleBottomMargin) |
||||
|
#endif |
||||
|
|
||||
|
#ifndef UIViewAutoresizingFlexibleDimensions |
||||
|
#define UIViewAutoresizingFlexibleDimensions (UIViewAutoresizingFlexibleWidth \ |
||||
|
| UIViewAutoresizingFlexibleHeight) |
||||
|
#endif |
||||
|
|
||||
|
#ifndef UIViewAutoresizingNavigationBar |
||||
|
#define UIViewAutoresizingNavigationBar (UIViewAutoresizingFlexibleWidth \ |
||||
|
| UIViewAutoresizingFlexibleBottomMargin) |
||||
|
#endif |
||||
|
|
||||
|
#ifndef UIViewAutoresizingToolbar |
||||
|
#define UIViewAutoresizingToolbar (UIViewAutoresizingFlexibleWidth \ |
||||
|
| UIViewAutoresizingFlexibleTopMargin) |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* The recommended number of points for a minimum tappable area. |
||||
|
* |
||||
|
* Value: 44 |
||||
|
*/ |
||||
|
CGFloat NIMinimumTapDimension(void); |
||||
|
|
||||
|
/** |
||||
|
* 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 |
||||
|
* <code>-[[UIApplication sharedApplication] setStatusBarHidden:withAnimation:].</code> |
||||
|
* |
||||
|
* 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 |
||||
|
* <code>-[[UIApplication sharedApplication] setStatusBarHidden:withAnimation:].</code> |
||||
|
* |
||||
|
* 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) NI_EXTENSION_UNAVAILABLE_IOS(""); |
||||
|
|
||||
|
/** |
||||
|
* 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); |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
|
|
||||
|
/**@}*/// End of Common Metrics /////////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,69 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
CGFloat NIMinimumTapDimension(void) { |
||||
|
return 44; |
||||
|
} |
||||
|
|
||||
|
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); |
||||
|
} |
||||
@ -0,0 +1,194 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
/** |
||||
|
* 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. |
||||
|
* |
||||
|
* |
||||
|
* <h2>Debug Assertions</h2> |
||||
|
* |
||||
|
* Debug assertions are a lightweight "sanity check". They won't crash the app, nor will they |
||||
|
* be included in release builds. They <i>will</i> halt the app's execution when debugging so |
||||
|
* that you can inspect the values that caused the failure. |
||||
|
* |
||||
|
* @code |
||||
|
* NIDASSERT(statement); |
||||
|
* @endcode |
||||
|
* |
||||
|
* If <i>statement</i> is false, the statement will be written to the log and if a debugger is |
||||
|
* attached, the app will break on the assertion line. |
||||
|
* |
||||
|
* |
||||
|
* <h2>Debug Logging</h2> |
||||
|
* |
||||
|
* @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. |
||||
|
* |
||||
|
* <h3>Turning up the log level while the app is running</h3> |
||||
|
* |
||||
|
* 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 |
||||
|
* @{ |
||||
|
*/ |
||||
|
|
||||
|
#if defined(DEBUG) || defined(NI_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 <TargetConditionals.h> |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
int NIIsInDebugger(void); |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
#if TARGET_IPHONE_SIMULATOR |
||||
|
// 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); \ |
||||
|
if (NIDebugAssertionsShouldBreak && NIIsInDebugger()) { raise(SIGTRAP); } } \ |
||||
|
} ((void)0) |
||||
|
#endif // #if TARGET_IPHONE_SIMULATOR |
||||
|
|
||||
|
#else |
||||
|
#define NIDASSERT(xx) ((void)0) |
||||
|
#endif // #if defined(DEBUG) || defined(NI_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. |
||||
|
*/ |
||||
|
#if defined(DEBUG) || defined(NI_DEBUG) |
||||
|
#define NIDPRINT(xx, ...) NSLog(@"%s(%d): " xx, __PRETTY_FUNCTION__, __LINE__, ##__VA_ARGS__) |
||||
|
#else |
||||
|
#define NIDPRINT(xx, ...) ((void)0) |
||||
|
#endif // #if defined(DEBUG) || defined(NI_DEBUG) |
||||
|
|
||||
|
/** |
||||
|
* Write the containing method's name to the log using NIDPRINT. |
||||
|
*/ |
||||
|
#define NIDPRINTMETHODNAME() NIDPRINT(@"%s", __PRETTY_FUNCTION__) |
||||
|
|
||||
|
#if defined(DEBUG) || defined(NI_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 // #if defined(DEBUG) || defined(NI_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 ////////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,61 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// 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" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
NSInteger NIMaxLogLevel = NILOGLEVEL_WARNING; |
||||
|
BOOL NIDebugAssertionsShouldBreak = YES; |
||||
|
|
||||
|
#if defined(DEBUG) || defined(NI_DEBUG) |
||||
|
|
||||
|
#import <unistd.h> |
||||
|
#import <sys/sysctl.h> |
||||
|
|
||||
|
// 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 // #if defined(DEBUG) || defined(NI_DEBUG) |
||||
@ -0,0 +1,92 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIPreprocessorMacros.h" |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* For dealing with device orientations. |
||||
|
* |
||||
|
* <h2>Examples</h2> |
||||
|
* |
||||
|
* <h3>Use NIIsSupportedOrientation to Enable Autorotation</h3> |
||||
|
* |
||||
|
* @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) NI_EXTENSION_UNAVAILABLE_IOS(""); |
||||
|
|
||||
|
/** |
||||
|
* Returns YES if the device is a phone and the orientation is landscape. |
||||
|
* |
||||
|
* This is a useful check for phone landscape mode which often requires |
||||
|
* additional logic to handle the smaller vertical real estate. |
||||
|
* |
||||
|
* @returns YES if the device is a phone and orientation is landscape. |
||||
|
*/ |
||||
|
BOOL NIIsLandscapePhoneOrientation(UIInterfaceOrientation orientation); |
||||
|
|
||||
|
/** |
||||
|
* 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); |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
|
|
||||
|
/**@}*/// End of Device Orientation /////////////////////////////////////////////////////////////// |
||||
|
|
||||
@ -0,0 +1,76 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <QuartzCore/QuartzCore.h> |
||||
|
|
||||
|
#import "NIDebuggingTools.h" |
||||
|
#import "NISDKAvailability.h" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
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 Three20 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; |
||||
|
} |
||||
|
|
||||
|
BOOL NIIsLandscapePhoneOrientation(UIInterfaceOrientation orientation) { |
||||
|
return NIIsPhone() && UIInterfaceOrientationIsLandscape(orientation); |
||||
|
} |
||||
|
|
||||
|
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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,54 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
/** |
||||
|
* 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 /////////////////////////////////////////////////////////////////////////// |
||||
|
|
||||
|
/** |
||||
|
* <h3>Example</h3> |
||||
|
* |
||||
|
* @code |
||||
|
* error = [NSError errorWithDomain: NINimbusErrorDomain |
||||
|
* code: NIImageTooSmall |
||||
|
* userInfo: [NSDictionary dictionaryWithObject: image |
||||
|
* forKey: NIImageErrorKey]]; |
||||
|
* @endcode |
||||
|
* |
||||
|
* @enum NINimbusErrorDomainCode |
||||
|
*/ |
||||
@ -0,0 +1,24 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
NSString* const NINimbusErrorDomain = @"com.nimbus.error"; |
||||
|
NSString* const NIImageErrorKey = @"image"; |
||||
@ -0,0 +1,421 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIPreprocessorMacros.h" |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* 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 - NSInvocation Methods |
||||
|
|
||||
|
/** |
||||
|
* Construct an NSInvocation with an instance of an object and a selector |
||||
|
* |
||||
|
* @return an NSInvocation that will call the given selector on the given target |
||||
|
*/ |
||||
|
NSInvocation* NIInvocationWithInstanceTarget(NSObject* target, SEL selector); |
||||
|
|
||||
|
/** |
||||
|
* This method is deprecated. Please use NIInvocationWithInstanceTarget([object class], selector) |
||||
|
* instead. |
||||
|
*/ |
||||
|
NSInvocation* NIInvocationWithClassTarget(Class targetClass, SEL selector) __NI_DEPRECATED_METHOD; |
||||
|
|
||||
|
#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 right and bottom edges of a CGRect. |
||||
|
* |
||||
|
* @return a CGRect with dx and dy added to the width and height. |
||||
|
* |
||||
|
* Example result: CGRectMake(x, y, w + dx, h + dy) |
||||
|
*/ |
||||
|
CGRect NIRectExpand(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); |
||||
|
|
||||
|
/** |
||||
|
* Inverse of UIEdgeInsetsInsetRect. |
||||
|
* |
||||
|
* Example result: CGRectMake(x - left, y - top, |
||||
|
* w + left + right, h + top + bottom) |
||||
|
*/ |
||||
|
CGRect NIEdgeInsetsOutsetRect(CGRect rect, UIEdgeInsets outsets); |
||||
|
|
||||
|
/** |
||||
|
* Returns the x position that will center size within containerSize. |
||||
|
* |
||||
|
* Example result: floorf((containerSize.width - size.width) / 2.f) |
||||
|
*/ |
||||
|
CGFloat NICenterX(CGSize containerSize, CGSize size); |
||||
|
|
||||
|
/** |
||||
|
* Returns the y position that will center size within containerSize. |
||||
|
* |
||||
|
* Example result: floorf((containerSize.height - size.height) / 2.f) |
||||
|
*/ |
||||
|
CGFloat NICenterY(CGSize containerSize, CGSize size); |
||||
|
|
||||
|
/** |
||||
|
* Returns a rect that will center viewToCenter within containerView. |
||||
|
* |
||||
|
* @return a CGPoint that will center viewToCenter within containerView. |
||||
|
*/ |
||||
|
CGRect NIFrameOfCenteredViewWithinView(UIView* viewToCenter, UIView* containerView); |
||||
|
|
||||
|
/** |
||||
|
* Returns the size of the string with given UILabel properties. |
||||
|
*/ |
||||
|
CGSize NISizeOfStringWithLabelProperties(NSString *string, CGSize constrainedToSize, UIFont *font, NSLineBreakMode lineBreakMode, NSInteger numberOfLines); |
||||
|
|
||||
|
/**@}*/ |
||||
|
|
||||
|
|
||||
|
#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 - NSData Methods |
||||
|
|
||||
|
/** |
||||
|
* For manipulating NSData. |
||||
|
* |
||||
|
* @defgroup NSData-Methods NSData Methods |
||||
|
* @{ |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Calculates an md5 hash of the data using CC_MD5. |
||||
|
*/ |
||||
|
NSString* NIMD5HashFromData(NSData* data); |
||||
|
|
||||
|
/** |
||||
|
* Calculates a sha1 hash of the data using CC_SHA1. |
||||
|
*/ |
||||
|
NSString* NISHA1HashFromData(NSData* data); |
||||
|
|
||||
|
/**@}*/ |
||||
|
|
||||
|
|
||||
|
#pragma mark - NSString Methods |
||||
|
|
||||
|
/** |
||||
|
* For manipulating NSStrings. |
||||
|
* |
||||
|
* @defgroup NSString-Methods NSString Methods |
||||
|
* @{ |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Calculates an md5 hash of the string using CC_MD5. |
||||
|
* |
||||
|
* Treats the string as UTF8. |
||||
|
*/ |
||||
|
NSString* NIMD5HashFromString(NSString* string); |
||||
|
|
||||
|
/** |
||||
|
* Calculates a sha1 hash of the string using CC_SHA1. |
||||
|
* |
||||
|
* Treats the string as UTF8. |
||||
|
*/ |
||||
|
NSString* NISHA1HashFromString(NSString* string); |
||||
|
|
||||
|
/** |
||||
|
* Returns a Boolean value indicating whether the string is a NSString object that contains only |
||||
|
* whitespace and newlines. |
||||
|
*/ |
||||
|
BOOL NIIsStringWithWhitespaceAndNewlines(NSString* string); |
||||
|
|
||||
|
/** |
||||
|
* 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 |
||||
|
* <pre> |
||||
|
* "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" |
||||
|
* </pre> |
||||
|
* @endhtmlonly |
||||
|
*/ |
||||
|
NSComparisonResult NICompareVersionStrings(NSString* string1, NSString* string2); |
||||
|
|
||||
|
/** |
||||
|
* 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* NIQueryDictionaryFromStringUsingEncoding(NSString* string, NSStringEncoding encoding); |
||||
|
|
||||
|
/** |
||||
|
* Returns a string that has been escaped for use as a URL parameter. |
||||
|
*/ |
||||
|
NSString* NIStringByAddingPercentEscapesForURLParameterString(NSString* parameter); |
||||
|
|
||||
|
/** |
||||
|
* Appends a dictionary of query parameters to a string, adding the ? character if necessary. |
||||
|
*/ |
||||
|
NSString* NIStringByAddingQueryDictionaryToString(NSString* string, NSDictionary* query); |
||||
|
|
||||
|
/**@}*/ |
||||
|
|
||||
|
|
||||
|
#pragma mark - CGFloat Methods |
||||
|
|
||||
|
/** |
||||
|
* For manipulating CGFloat. |
||||
|
* |
||||
|
* @defgroup CGFloat-Methods CGFloat Methods |
||||
|
* @{ |
||||
|
* |
||||
|
* These methods provide math functions on CGFloats. They could easily be replaced with <tgmath.h> |
||||
|
* but that is currently (Xcode 5.0) incompatible with CLANG_ENABLE_MODULES (on by default for |
||||
|
* many projects/targets). We'll use CG_INLINE because this really should be completely inline. |
||||
|
*/ |
||||
|
|
||||
|
#if CGFLOAT_IS_DOUBLE |
||||
|
#define NI_CGFLOAT_EPSILON DBL_EPSILON |
||||
|
#else |
||||
|
#define NI_CGFLOAT_EPSILON FLT_EPSILON |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* fabs()/fabsf() sized for CGFloat |
||||
|
*/ |
||||
|
CG_INLINE CGFloat NICGFloatAbs(CGFloat x) { |
||||
|
#if CGFLOAT_IS_DOUBLE |
||||
|
return (CGFloat)fabs(x); |
||||
|
#else |
||||
|
return (CGFloat)fabsf(x); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* floor()/floorf() sized for CGFloat |
||||
|
*/ |
||||
|
CG_INLINE CGFloat NICGFloatFloor(CGFloat x) { |
||||
|
#if CGFLOAT_IS_DOUBLE |
||||
|
return (CGFloat)floor(x); |
||||
|
#else |
||||
|
return (CGFloat)floorf(x); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* ceil()/ceilf() sized for CGFloat |
||||
|
*/ |
||||
|
CG_INLINE CGFloat NICGFloatCeil(CGFloat x) { |
||||
|
#if CGFLOAT_IS_DOUBLE |
||||
|
return (CGFloat)ceil(x); |
||||
|
#else |
||||
|
return (CGFloat)ceilf(x); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* round()/roundf() sized for CGFloat |
||||
|
*/ |
||||
|
CG_INLINE CGFloat NICGFloatRound(CGFloat x) { |
||||
|
#if CGFLOAT_IS_DOUBLE |
||||
|
return (CGFloat)round(x); |
||||
|
#else |
||||
|
return (CGFloat)roundf(x); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* sqrt()/sqrtf() sized for CGFloat |
||||
|
*/ |
||||
|
CG_INLINE CGFloat NICGFloatSqRt(CGFloat x) { |
||||
|
#if CGFLOAT_IS_DOUBLE |
||||
|
return (CGFloat)sqrt(x); |
||||
|
#else |
||||
|
return (CGFloat)sqrtf(x); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* copysign()/copysignf() sized for CGFloat |
||||
|
*/ |
||||
|
CG_INLINE CGFloat NICGFloatCopySign(CGFloat x, CGFloat y) { |
||||
|
#if CGFLOAT_IS_DOUBLE |
||||
|
return (CGFloat)copysign(x, y); |
||||
|
#else |
||||
|
return (CGFloat)copysignf(x, y); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* pow()/powf() sized for CGFloat |
||||
|
*/ |
||||
|
CG_INLINE CGFloat NICGFloatPow(CGFloat x, CGFloat y) { |
||||
|
#if CGFLOAT_IS_DOUBLE |
||||
|
return (CGFloat)pow(x, y); |
||||
|
#else |
||||
|
return (CGFloat)powf(x, y); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* cos()/cosf() sized for CGFloat |
||||
|
*/ |
||||
|
CG_INLINE CGFloat NICGFloatCos(CGFloat x) { |
||||
|
#if CGFLOAT_IS_DOUBLE |
||||
|
return (CGFloat)cos(x); |
||||
|
#else |
||||
|
return (CGFloat)cosf(x); |
||||
|
#endif |
||||
|
} |
||||
|
|
||||
|
/**@}*/ |
||||
|
|
||||
|
#pragma mark - General Purpose Methods |
||||
|
|
||||
|
/** |
||||
|
* For general purpose foundation type manipulation. |
||||
|
* |
||||
|
* @defgroup General-Purpose-Methods General Purpose Methods |
||||
|
* @{ |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Deprecated method. Use NIBoundf instead. |
||||
|
*/ |
||||
|
CGFloat boundf(CGFloat value, CGFloat min, CGFloat max) __NI_DEPRECATED_METHOD; // Use NIBoundf instead. MAINTENANCE: Remove by Feb 28, 2014. |
||||
|
|
||||
|
/** |
||||
|
* Deprecated method. Use NIBoundi instead. |
||||
|
*/ |
||||
|
NSInteger boundi(NSInteger value, NSInteger min, NSInteger max) __NI_DEPRECATED_METHOD; // Use NIBoundi instead. MAINTENANCE: Remove by Feb 28, 2014. |
||||
|
|
||||
|
/** |
||||
|
* Bounds a given value within the min and max values. |
||||
|
* |
||||
|
* If max < min then value will be min. |
||||
|
* |
||||
|
* @returns min <= result <= max |
||||
|
*/ |
||||
|
CGFloat NIBoundf(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 NIBoundi(NSInteger value, NSInteger min, NSInteger max); |
||||
|
|
||||
|
/**@}*/ |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
|
|
||||
|
/**@}*/// End of Foundation Methods /////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,314 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
#import <CommonCrypto/CommonDigest.h> |
||||
|
#import <objc/runtime.h> |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
#pragma mark - NSInvocation |
||||
|
|
||||
|
NSInvocation* NIInvocationWithInstanceTarget(NSObject *targetObject, SEL selector) { |
||||
|
NSMethodSignature* sig = [targetObject methodSignatureForSelector:selector]; |
||||
|
NSInvocation* inv = [NSInvocation invocationWithMethodSignature:sig]; |
||||
|
[inv setTarget:targetObject]; |
||||
|
[inv setSelector:selector]; |
||||
|
return inv; |
||||
|
} |
||||
|
|
||||
|
// Deprecated. Please delete on the next minor version upgrade. |
||||
|
NSInvocation* NIInvocationWithClassTarget(Class targetClass, SEL selector) { |
||||
|
return NIInvocationWithInstanceTarget((NSObject *)targetClass, selector); |
||||
|
} |
||||
|
|
||||
|
#pragma mark - CGRect |
||||
|
|
||||
|
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 NIRectExpand(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 NIEdgeInsetsOutsetRect(CGRect rect, UIEdgeInsets outsets) { |
||||
|
return CGRectMake(rect.origin.x - outsets.left, |
||||
|
rect.origin.y - outsets.top, |
||||
|
rect.size.width + outsets.left + outsets.right, |
||||
|
rect.size.height + outsets.top + outsets.bottom); |
||||
|
} |
||||
|
|
||||
|
CGFloat NICenterX(CGSize containerSize, CGSize size) { |
||||
|
return NICGFloatFloor((containerSize.width - size.width) / 2.f); |
||||
|
} |
||||
|
|
||||
|
CGFloat NICenterY(CGSize containerSize, CGSize size) { |
||||
|
return NICGFloatFloor((containerSize.height - size.height) / 2.f); |
||||
|
} |
||||
|
|
||||
|
CGRect NIFrameOfCenteredViewWithinView(UIView* viewToCenter, UIView* containerView) { |
||||
|
CGPoint origin; |
||||
|
CGSize containerViewSize = containerView.bounds.size; |
||||
|
CGSize viewSize = viewToCenter.frame.size; |
||||
|
origin.x = NICenterX(containerViewSize, viewSize); |
||||
|
origin.y = NICenterY(containerViewSize, viewSize); |
||||
|
return CGRectMake(origin.x, origin.y, viewSize.width, viewSize.height); |
||||
|
} |
||||
|
|
||||
|
CGSize NISizeOfStringWithLabelProperties(NSString *string, CGSize constrainedToSize, UIFont *font, NSLineBreakMode lineBreakMode, NSInteger numberOfLines) { |
||||
|
if (string.length == 0) { |
||||
|
return CGSizeZero; |
||||
|
} |
||||
|
|
||||
|
CGFloat lineHeight = font.lineHeight; |
||||
|
CGSize size = CGSizeZero; |
||||
|
|
||||
|
if (numberOfLines == 1) { |
||||
|
#pragma clang diagnostic push |
||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations" |
||||
|
size = [string sizeWithFont:font forWidth:constrainedToSize.width lineBreakMode:lineBreakMode]; |
||||
|
#pragma clang diagnostic pop |
||||
|
} else { |
||||
|
#pragma clang diagnostic push |
||||
|
#pragma clang diagnostic ignored "-Wdeprecated-declarations" |
||||
|
size = [string sizeWithFont:font constrainedToSize:constrainedToSize lineBreakMode:lineBreakMode]; |
||||
|
#pragma clang diagnostic pop |
||||
|
if (numberOfLines > 0) { |
||||
|
size.height = MIN(size.height, numberOfLines * lineHeight); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return size; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - NSRange |
||||
|
|
||||
|
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 - NSData |
||||
|
|
||||
|
NSString* NIMD5HashFromData(NSData* data) { |
||||
|
unsigned char result[CC_MD5_DIGEST_LENGTH]; |
||||
|
bzero(result, sizeof(result)); |
||||
|
CC_MD5_CTX md5Context; |
||||
|
CC_MD5_Init(&md5Context); |
||||
|
size_t bytesHashed = 0; |
||||
|
while (bytesHashed < [data length]) { |
||||
|
CC_LONG updateSize = 1024 * 1024; |
||||
|
if (([data length] - bytesHashed) < (size_t)updateSize) { |
||||
|
updateSize = (CC_LONG)([data length] - bytesHashed); |
||||
|
} |
||||
|
CC_MD5_Update(&md5Context, (char *)[data bytes] + bytesHashed, updateSize); |
||||
|
bytesHashed += updateSize; |
||||
|
} |
||||
|
CC_MD5_Final(result, &md5Context); |
||||
|
|
||||
|
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] |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
NSString* NISHA1HashFromData(NSData* data) { |
||||
|
unsigned char result[CC_SHA1_DIGEST_LENGTH]; |
||||
|
bzero(result, sizeof(result)); |
||||
|
CC_SHA1_CTX sha1Context; |
||||
|
CC_SHA1_Init(&sha1Context); |
||||
|
size_t bytesHashed = 0; |
||||
|
while (bytesHashed < [data length]) { |
||||
|
CC_LONG updateSize = 1024 * 1024; |
||||
|
if (([data length] - bytesHashed) < (size_t)updateSize) { |
||||
|
updateSize = (CC_LONG)([data length] - bytesHashed); |
||||
|
} |
||||
|
CC_SHA1_Update(&sha1Context, (char *)[data bytes] + bytesHashed, updateSize); |
||||
|
bytesHashed += updateSize; |
||||
|
} |
||||
|
CC_SHA1_Final(result, &sha1Context); |
||||
|
|
||||
|
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] |
||||
|
]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - NSString |
||||
|
|
||||
|
NSString* NIMD5HashFromString(NSString* string) { |
||||
|
return NIMD5HashFromData([string dataUsingEncoding:NSUTF8StringEncoding]); |
||||
|
} |
||||
|
|
||||
|
NSString* NISHA1HashFromString(NSString* string) { |
||||
|
return NISHA1HashFromData([string dataUsingEncoding:NSUTF8StringEncoding]); |
||||
|
} |
||||
|
|
||||
|
BOOL NIIsStringWithWhitespaceAndNewlines(NSString* string) { |
||||
|
NSCharacterSet* notWhitespaceAndNewlines = [[NSCharacterSet whitespaceAndNewlineCharacterSet] invertedSet]; |
||||
|
return [string isKindOfClass:[NSString class]] && [string rangeOfCharacterFromSet:notWhitespaceAndNewlines].length == 0; |
||||
|
} |
||||
|
|
||||
|
NSComparisonResult NICompareVersionStrings(NSString* string1, NSString* string2) { |
||||
|
NSArray *oneComponents = [string1 componentsSeparatedByString:@"a"]; |
||||
|
NSArray *twoComponents = [string2 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]; |
||||
|
} |
||||
|
|
||||
|
NSDictionary* NIQueryDictionaryFromStringUsingEncoding(NSString* string, NSStringEncoding encoding) { |
||||
|
NSCharacterSet* delimiterSet = [NSCharacterSet characterSetWithCharactersInString:@"&;"]; |
||||
|
NSMutableDictionary* pairs = [NSMutableDictionary dictionary]; |
||||
|
NSScanner* scanner = [[NSScanner alloc] initWithString:string]; |
||||
|
|
||||
|
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[0] stringByReplacingPercentEscapesUsingEncoding:encoding]; |
||||
|
|
||||
|
NSMutableArray* values = pairs[key]; |
||||
|
if (nil == values) { |
||||
|
values = [NSMutableArray array]; |
||||
|
pairs[key] = values; |
||||
|
} |
||||
|
|
||||
|
if (kvPair.count == 1) { |
||||
|
[values addObject:[NSNull null]]; |
||||
|
|
||||
|
} else if (kvPair.count == 2) { |
||||
|
NSString* value = [kvPair[1] stringByReplacingPercentEscapesUsingEncoding:encoding]; |
||||
|
[values addObject:value]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return [pairs copy]; |
||||
|
} |
||||
|
|
||||
|
NSString* NIStringByAddingPercentEscapesForURLParameterString(NSString* parameter) { |
||||
|
CFStringRef buffer = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault, |
||||
|
(__bridge CFStringRef)parameter, |
||||
|
NULL, |
||||
|
(__bridge CFStringRef)@"!*'();:@&=+$,/?%#[]", |
||||
|
kCFStringEncodingUTF8); |
||||
|
|
||||
|
NSString* result = [NSString stringWithString:(__bridge NSString *)buffer]; |
||||
|
CFRelease(buffer); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
NSString* NIStringByAddingQueryDictionaryToString(NSString* string, NSDictionary* query) { |
||||
|
NSMutableArray* pairs = [NSMutableArray array]; |
||||
|
for (NSString* key in [query keyEnumerator]) { |
||||
|
NSString* value = NIStringByAddingPercentEscapesForURLParameterString([query objectForKey:key]); |
||||
|
NSString* pair = [NSString stringWithFormat:@"%@=%@", key, value]; |
||||
|
[pairs addObject:pair]; |
||||
|
} |
||||
|
|
||||
|
NSString* params = [pairs componentsJoinedByString:@"&"]; |
||||
|
if ([string rangeOfString:@"?"].location == NSNotFound) { |
||||
|
return [string stringByAppendingFormat:@"?%@", params]; |
||||
|
|
||||
|
} else { |
||||
|
return [string stringByAppendingFormat:@"&%@", params]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - General Purpose |
||||
|
|
||||
|
// Deprecated. |
||||
|
CGFloat boundf(CGFloat value, CGFloat min, CGFloat max) { |
||||
|
return NIBoundf(value, min, max); |
||||
|
} |
||||
|
|
||||
|
// Deprecated. |
||||
|
NSInteger boundi(NSInteger value, NSInteger min, NSInteger max) { |
||||
|
return NIBoundi(value, min, max); |
||||
|
} |
||||
|
|
||||
|
CGFloat NIBoundf(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 NIBoundi(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; |
||||
|
} |
||||
@ -0,0 +1,48 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* For manipulating UIImage objects. |
||||
|
* |
||||
|
* @ingroup NimbusCore |
||||
|
* @defgroup Image-Utilities Image Utilities |
||||
|
* @{ |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Returns an image that is stretchable from the center. |
||||
|
* |
||||
|
* A common use of this method is to create an image that has rounded corners for a button |
||||
|
* and then assign a stretchable version of that image to a UIButton. |
||||
|
* |
||||
|
* This stretches the middle vertical and horizontal line of pixels, so use care when |
||||
|
* stretching images that have gradients. For example, an image with a vertical gradient |
||||
|
* can be stretched horizontally, but will look odd if stretched vertically. |
||||
|
*/ |
||||
|
UIImage* NIStretchableImageFromImage(UIImage* image); |
||||
|
|
||||
|
/**@}*/// End of Image Utilities ////////////////////////////////////////////////////////////////// |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
@ -0,0 +1,24 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIImageUtilities.h" |
||||
|
|
||||
|
UIImage* NIStretchableImageFromImage(UIImage* image) { |
||||
|
const CGSize size = image.size; |
||||
|
NSInteger midX = (NSInteger)(size.width / 2.f); |
||||
|
NSInteger midY = (NSInteger)(size.height / 2.f); |
||||
|
return [image stretchableImageWithLeftCapWidth:midX topCapHeight:midY]; |
||||
|
} |
||||
@ -0,0 +1,316 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
#import "NIPreprocessorMacros.h" |
||||
|
|
||||
|
/** |
||||
|
* 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 |
||||
|
* @{ |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* 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 |
||||
|
|
||||
|
// 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)removeAllObjectsWithPrefix:(NSString *)prefix; |
||||
|
- (void)removeAllObjects; |
||||
|
|
||||
|
- (id)objectWithName:(NSString *)name; |
||||
|
- (BOOL)containsObjectWithName:(NSString *)name; |
||||
|
- (NSDate *)dateOfLastAccessWithName:(NSString *)name; |
||||
|
|
||||
|
- (NSString *)nameOfLeastRecentlyUsedObject; |
||||
|
- (NSString *)nameOfMostRecentlyUsedObject; |
||||
|
|
||||
|
- (void)reduceMemoryUsage; |
||||
|
|
||||
|
// Subclassing |
||||
|
|
||||
|
- (BOOL)shouldSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject; |
||||
|
- (void)didSetObject:(id)object withName:(NSString *)name; |
||||
|
- (void)willRemoveObject:(id)object withName:(NSString *)name; |
||||
|
|
||||
|
// Deprecated method. Use shouldSetObject:withName:previousObject: instead. |
||||
|
- (BOOL)willSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject __NI_DEPRECATED_METHOD; |
||||
|
|
||||
|
@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 |
||||
|
|
||||
|
@property (nonatomic, readonly) unsigned long long numberOfPixels; |
||||
|
|
||||
|
@property (nonatomic) unsigned long long maxNumberOfPixels; // Default: 0 (unlimited) |
||||
|
@property (nonatomic) unsigned long long maxNumberOfPixelsUnderStress; // Default: 0 (unlimited) |
||||
|
|
||||
|
@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 with the given name. |
||||
|
* |
||||
|
* @param name The name used as a key to store this object. |
||||
|
* @fn NIMemoryCache::removeObjectWithName: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Removes all objects from the cache with a given prefix. |
||||
|
* |
||||
|
* This method requires a scan of the cache entries. |
||||
|
* |
||||
|
* @param prefix Any object name that has this prefix will be removed from the cache. |
||||
|
* @fn NIMemoryCache::removeAllObjectsWithPrefix: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* 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 <code>UIApplicationDidReceiveMemoryWarningNotification</code> |
||||
|
* 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. |
||||
|
* @returns YES If object is allowed to be stored in the cache. |
||||
|
* @fn NIMemoryCache::shouldSetObject:withName:previousObject: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* This method is deprecated. Please use shouldSetObject:withName:previousObject: instead. |
||||
|
* |
||||
|
* @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 |
||||
|
*/ |
||||
@ -0,0 +1,453 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIDebuggingTools.h" |
||||
|
#import "NIPreprocessorMacros.h" |
||||
|
|
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
@interface NIMemoryCache() |
||||
|
// Mapping from a name (usually a URL) to an internal object. |
||||
|
@property (nonatomic, strong) NSMutableDictionary* cacheMap; |
||||
|
// A linked list of least recently used cache objects. Most recently used is the tail. |
||||
|
@property (nonatomic, strong) NSMutableOrderedSet* lruCacheObjects; |
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* @brief A single cache item's information. |
||||
|
* |
||||
|
* Used in expiration calculations and for storing the actual cache object. |
||||
|
*/ |
||||
|
@interface NIMemoryCacheInfo : NSObject |
||||
|
|
||||
|
/** |
||||
|
* @brief The name used to store this object in the cache. |
||||
|
*/ |
||||
|
@property (nonatomic, copy) NSString* name; |
||||
|
|
||||
|
/** |
||||
|
* @brief The object stored in the cache. |
||||
|
*/ |
||||
|
@property (nonatomic, strong) id object; |
||||
|
|
||||
|
/** |
||||
|
* @brief The date after which the image is no longer valid and should be removed from the cache. |
||||
|
*/ |
||||
|
@property (nonatomic, strong) 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, strong) NSDate* lastAccessTime; |
||||
|
|
||||
|
/** |
||||
|
* @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 |
||||
|
|
||||
|
- (void)dealloc { |
||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
||||
|
} |
||||
|
|
||||
|
- (id)init { |
||||
|
return [self initWithCapacity:0]; |
||||
|
} |
||||
|
|
||||
|
- (id)initWithCapacity:(NSUInteger)capacity { |
||||
|
if ((self = [super init])) { |
||||
|
_cacheMap = [[NSMutableDictionary alloc] initWithCapacity:capacity]; |
||||
|
_lruCacheObjects = [NSMutableOrderedSet orderedSet]; |
||||
|
|
||||
|
// Automatically reduce memory usage when we get a memory warning. |
||||
|
[[NSNotificationCenter defaultCenter] addObserver:self |
||||
|
selector:@selector(reduceMemoryUsage) |
||||
|
name:UIApplicationDidReceiveMemoryWarningNotification |
||||
|
object:nil]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (NSString *)description { |
||||
|
return [NSString stringWithFormat: |
||||
|
@"<%@" |
||||
|
@" lruObjects: %@" |
||||
|
@" cache map: %@" |
||||
|
@">", |
||||
|
[super description], |
||||
|
self.lruCacheObjects, |
||||
|
self.cacheMap]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Internal |
||||
|
|
||||
|
- (void)updateAccessTimeForInfo:(NIMemoryCacheInfo *)info { |
||||
|
@synchronized(self) { |
||||
|
NIDASSERT(nil != info); |
||||
|
if (nil == info) { |
||||
|
return; // COV_NF_LINE |
||||
|
} |
||||
|
info.lastAccessTime = [NSDate date]; |
||||
|
|
||||
|
[self.lruCacheObjects removeObject:info]; |
||||
|
[self.lruCacheObjects addObject:info]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (NIMemoryCacheInfo *)cacheInfoForName:(NSString *)name { |
||||
|
NIMemoryCacheInfo* info; |
||||
|
@synchronized(self) { |
||||
|
info = self.cacheMap[name]; |
||||
|
} |
||||
|
return info; |
||||
|
} |
||||
|
|
||||
|
- (void)setCacheInfo:(NIMemoryCacheInfo *)info forName:(NSString *)name { |
||||
|
@synchronized(self) { |
||||
|
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]; |
||||
|
|
||||
|
id previousObject = [self cacheInfoForName:name].object; |
||||
|
if ([self shouldSetObject:info.object withName:name previousObject:previousObject]) { |
||||
|
self.cacheMap[name] = info; |
||||
|
[self didSetObject:info.object withName:name]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)removeCacheInfoForName:(NSString *)name { |
||||
|
@synchronized(self) { |
||||
|
NIDASSERT(nil != name); |
||||
|
if (nil == name) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
NIMemoryCacheInfo* cacheInfo = [self cacheInfoForName:name]; |
||||
|
[self willRemoveObject:cacheInfo.object withName:name]; |
||||
|
|
||||
|
[self.lruCacheObjects removeObject:cacheInfo]; |
||||
|
[self.cacheMap removeObjectForKey:name]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Subclassing |
||||
|
|
||||
|
// Deprecated method. |
||||
|
- (BOOL)willSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject { |
||||
|
return [self shouldSetObject:object withName:name previousObject:previousObject]; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)shouldSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject { |
||||
|
// Allow anything to be stored. |
||||
|
return YES; |
||||
|
} |
||||
|
|
||||
|
- (void)didSetObject:(id)object withName:(NSString *)name { |
||||
|
// No-op |
||||
|
} |
||||
|
|
||||
|
- (void)willRemoveObject:(id)object withName:(NSString *)name { |
||||
|
// No-op |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Public |
||||
|
|
||||
|
- (void)storeObject:(id)object withName:(NSString *)name { |
||||
|
@synchronized(self) { |
||||
|
[self storeObject:object withName:name expiresAfter:nil]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)storeObject:(id)object withName:(NSString *)name expiresAfter:(NSDate *)expirationDate { |
||||
|
@synchronized(self) { |
||||
|
// 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]; |
||||
|
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 { |
||||
|
@synchronized(self) { |
||||
|
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; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (BOOL)containsObjectWithName:(NSString *)name { |
||||
|
@synchronized(self) { |
||||
|
NIMemoryCacheInfo* info = [self cacheInfoForName:name]; |
||||
|
|
||||
|
if ([info hasExpired]) { |
||||
|
[self removeObjectWithName:name]; |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
return (nil != info); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (NSDate *)dateOfLastAccessWithName:(NSString *)name { |
||||
|
@synchronized(self) { |
||||
|
NIMemoryCacheInfo* info = [self cacheInfoForName:name]; |
||||
|
|
||||
|
if ([info hasExpired]) { |
||||
|
[self removeObjectWithName:name]; |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
return [info lastAccessTime]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (NSString *)nameOfLeastRecentlyUsedObject { |
||||
|
@synchronized(self) { |
||||
|
NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject]; |
||||
|
|
||||
|
if ([info hasExpired]) { |
||||
|
[self removeObjectWithName:info.name]; |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
return info.name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (NSString *)nameOfMostRecentlyUsedObject { |
||||
|
@synchronized(self) { |
||||
|
NIMemoryCacheInfo* info = [self.lruCacheObjects lastObject]; |
||||
|
|
||||
|
if ([info hasExpired]) { |
||||
|
[self removeObjectWithName:info.name]; |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
return info.name; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)removeObjectWithName:(NSString *)name { |
||||
|
@synchronized(self) { |
||||
|
[self removeCacheInfoForName:name]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)removeAllObjectsWithPrefix:(NSString *)prefix { |
||||
|
@synchronized(self) { |
||||
|
// Assertions fire if you try to modify the object you're iterating over, so we make a copy. |
||||
|
for (NSString* name in [self.cacheMap copy]) { |
||||
|
if ([name hasPrefix:prefix]) { |
||||
|
[self removeObjectWithName:name]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)removeAllObjects { |
||||
|
@synchronized(self) { |
||||
|
[self.cacheMap removeAllObjects]; |
||||
|
[self.lruCacheObjects removeAllObjects]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)reduceMemoryUsage { |
||||
|
@synchronized(self) { |
||||
|
// Assertions fire if you try to modify the object you're iterating over, so we make a copy. |
||||
|
for (id name in [self.cacheMap copy]) { |
||||
|
NIMemoryCacheInfo* info = [self cacheInfoForName:name]; |
||||
|
|
||||
|
if ([info hasExpired]) { |
||||
|
[self removeCacheInfoForName:name]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (NSUInteger)count { |
||||
|
@synchronized(self) { |
||||
|
return self.cacheMap.count; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation NIMemoryCacheInfo |
||||
|
|
||||
|
- (BOOL)hasExpired { |
||||
|
return (nil != _expirationDate |
||||
|
&& [[NSDate date] timeIntervalSinceDate:_expirationDate] >= 0); |
||||
|
} |
||||
|
|
||||
|
- (NSString *)description { |
||||
|
return [NSString stringWithFormat: |
||||
|
@"<%@" |
||||
|
@" name: %@" |
||||
|
@" object: %@" |
||||
|
@" expiration date: %@" |
||||
|
@" last access time: %@" |
||||
|
@">", |
||||
|
[super description], |
||||
|
self.name, |
||||
|
self.object, |
||||
|
self.expirationDate, |
||||
|
self.lastAccessTime]; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@interface NIImageMemoryCache() |
||||
|
@property (nonatomic, assign) unsigned long long numberOfPixels; |
||||
|
@end |
||||
|
|
||||
|
@implementation NIImageMemoryCache |
||||
|
|
||||
|
- (unsigned long long)numberOfPixelsUsedByImage:(UIImage *)image { |
||||
|
@synchronized(self) { |
||||
|
if (nil == image) { |
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
return (unsigned long long)(image.size.width * image.size.height * [image scale] * [image scale]); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)removeAllObjects { |
||||
|
@synchronized(self) { |
||||
|
[super removeAllObjects]; |
||||
|
|
||||
|
self.numberOfPixels = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)reduceMemoryUsage { |
||||
|
@synchronized(self) { |
||||
|
// 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]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (BOOL)shouldSetObject:(id)object withName:(NSString *)name previousObject:(id)previousObject { |
||||
|
@synchronized(self) { |
||||
|
NIDASSERT(nil == object || [object isKindOfClass:[UIImage class]]); |
||||
|
if (![object isKindOfClass:[UIImage class]]) { |
||||
|
return NO; |
||||
|
} |
||||
|
|
||||
|
_numberOfPixels -= [self numberOfPixelsUsedByImage:previousObject]; |
||||
|
_numberOfPixels += [self numberOfPixelsUsedByImage:object]; |
||||
|
|
||||
|
return YES; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)didSetObject:(id)object withName:(NSString *)name { |
||||
|
@synchronized(self) { |
||||
|
// 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 (self.maxNumberOfPixels > 0) { |
||||
|
// Remove least recently used images until we satisfy our memory constraints. |
||||
|
while (self.numberOfPixels > self.maxNumberOfPixels) { |
||||
|
NIMemoryCacheInfo* info = [self.lruCacheObjects firstObject]; |
||||
|
[self removeCacheInfoForName:info.name]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)willRemoveObject:(id)object withName:(NSString *)name { |
||||
|
@synchronized(self) { |
||||
|
NIDASSERT(nil == object || [object isKindOfClass:[UIImage class]]); |
||||
|
if (nil == object || ![object isKindOfClass:[UIImage class]]) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
self.numberOfPixels -= [self numberOfPixelsUsedByImage:object]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
@ -0,0 +1,101 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
#import "NIPreprocessorMacros.h" |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* 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) NI_EXTENSION_UNAVAILABLE_IOS(""); |
||||
|
|
||||
|
/** |
||||
|
* 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 |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
|
|
||||
|
/**@}*/// End of Network Activity ///////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,171 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
#import "NIPreprocessorMacros.h" |
||||
|
|
||||
|
#if defined(DEBUG) || defined(NI_DEBUG) |
||||
|
#import "NIRuntimeClassModifications.h" |
||||
|
#endif |
||||
|
|
||||
|
#import <pthread.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
static int gNetworkTaskCount = 0; |
||||
|
static pthread_mutex_t gMutex = PTHREAD_MUTEX_INITIALIZER; |
||||
|
static const NSTimeInterval kDelayBeforeDisablingActivity = 0.1; |
||||
|
static NSTimer* gScheduledDelayTimer = nil; |
||||
|
|
||||
|
@interface NINetworkActivity : NSObject |
||||
|
@end |
||||
|
|
||||
|
|
||||
|
@implementation NINetworkActivity |
||||
|
|
||||
|
|
||||
|
// Called after a certain amount of time has passed since all network activity has stopped. |
||||
|
// By delaying the turnoff of the network activity we avoid "flickering" effects when network |
||||
|
// activity is starting and stopping rapidly. |
||||
|
+ (void)disableNetworkActivity NI_EXTENSION_UNAVAILABLE_IOS("") { |
||||
|
pthread_mutex_lock(&gMutex); |
||||
|
if (nil != gScheduledDelayTimer) { |
||||
|
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO; |
||||
|
gScheduledDelayTimer = nil; |
||||
|
} |
||||
|
pthread_mutex_unlock(&gMutex); |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
|
||||
|
void NINetworkActivityTaskDidStart(void) { |
||||
|
pthread_mutex_lock(&gMutex); |
||||
|
|
||||
|
BOOL enableNetworkActivityIndicator = (0 == gNetworkTaskCount); |
||||
|
|
||||
|
++gNetworkTaskCount; |
||||
|
[gScheduledDelayTimer invalidate]; |
||||
|
gScheduledDelayTimer = nil; |
||||
|
|
||||
|
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) { |
||||
|
[gScheduledDelayTimer invalidate]; |
||||
|
gScheduledDelayTimer = nil; |
||||
|
|
||||
|
// Ensure that the timer is scheduled on the main loop, otherwise it will die when the thread |
||||
|
// dies. |
||||
|
dispatch_async(dispatch_get_main_queue(), ^{ |
||||
|
pthread_mutex_lock(&gMutex); |
||||
|
gScheduledDelayTimer = [NSTimer scheduledTimerWithTimeInterval:kDelayBeforeDisablingActivity |
||||
|
target:[NINetworkActivity class] |
||||
|
selector:@selector(disableNetworkActivity) |
||||
|
userInfo:nil |
||||
|
repeats:NO]; |
||||
|
pthread_mutex_unlock(&gMutex); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
pthread_mutex_unlock(&gMutex); |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Network Activity Debugging |
||||
|
|
||||
|
#if defined(DEBUG) || defined(NI_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 // #if defined(DEBUG) || defined(NI_DEBUG) |
||||
@ -0,0 +1,58 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* 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); |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
|
|
||||
|
/**@}*/// End of Non-Empty Collection Testing ///////////////////////////////////////////////////// |
||||
@ -0,0 +1,35 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
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; |
||||
|
} |
||||
@ -0,0 +1,65 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* 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); |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
|
|
||||
|
/**@}*/// End of Non-Retaining Collections //////////////////////////////////////////////////////// |
||||
@ -0,0 +1,36 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
NSMutableArray* NICreateNonRetainingMutableArray(void) { |
||||
|
return (__bridge_transfer NSMutableArray *)CFArrayCreateMutable(nil, 0, nil); |
||||
|
} |
||||
|
|
||||
|
NSMutableDictionary* NICreateNonRetainingMutableDictionary(void) { |
||||
|
return (__bridge_transfer NSMutableDictionary *)CFDictionaryCreateMutable(nil, 0, nil, nil); |
||||
|
} |
||||
|
|
||||
|
NSMutableSet* NICreateNonRetainingMutableSet(void) { |
||||
|
return (__bridge_transfer NSMutableSet *)CFSetCreateMutable(nil, 0, nil); |
||||
|
} |
||||
|
|
||||
@ -0,0 +1,20 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
|
||||
|
@interface NIOperation() |
||||
|
@property (strong) NSError* lastError; |
||||
|
@end |
||||
@ -0,0 +1,209 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIPreprocessorMacros.h" /* for weak */ |
||||
|
|
||||
|
@class NIOperation; |
||||
|
|
||||
|
typedef void (^NIOperationBlock)(NIOperation* operation); |
||||
|
typedef void (^NIOperationDidFailBlock)(NIOperation* operation, NSError* error); |
||||
|
|
||||
|
/** |
||||
|
* 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. |
||||
|
* |
||||
|
* <h2>Subclassing</h2> |
||||
|
* |
||||
|
* 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 |
||||
|
|
||||
|
@property (weak) id<NIOperationDelegate> delegate; |
||||
|
@property (readonly, strong) NSError* lastError; |
||||
|
@property (assign) NSInteger tag; |
||||
|
|
||||
|
@property (copy) NIOperationBlock didStartBlock; |
||||
|
@property (copy) NIOperationBlock didFinishBlock; |
||||
|
@property (copy) NIOperationDidFailBlock didFailWithErrorBlock; |
||||
|
@property (copy) NIOperationBlock willFinishBlock; |
||||
|
|
||||
|
- (void)didStart; |
||||
|
- (void)didFinish; |
||||
|
- (void)didFailWithError:(NSError *)error; |
||||
|
- (void)willFinish; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* The delegate protocol for an NIOperation. |
||||
|
* |
||||
|
* @ingroup Operations |
||||
|
*/ |
||||
|
@protocol NIOperationDelegate <NSObject> |
||||
|
@optional |
||||
|
|
||||
|
/** @name [NIOperationDelegate] State Changes */ |
||||
|
|
||||
|
/** The operation has started executing. */ |
||||
|
- (void)nimbusOperationDidStart:(NIOperation *)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)nimbusOperationWillFinish:(NIOperation *)operation; |
||||
|
|
||||
|
/** |
||||
|
* The operation has completed successfully. |
||||
|
* |
||||
|
* This will not be called if the operation fails. |
||||
|
*/ |
||||
|
- (void)nimbusOperationDidFinish:(NIOperation *)operation; |
||||
|
|
||||
|
/** |
||||
|
* The operation failed in some way and has completed. |
||||
|
* |
||||
|
* operationDidFinish: will not be called. |
||||
|
*/ |
||||
|
- (void)nimbusOperationDidFail:(NIOperation *)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 |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
/** @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 |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* @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::didStart |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* On the main thread, notify the delegate that the operation has finished. |
||||
|
* |
||||
|
* @fn NIOperation::didFinish |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* On the main thread, notify the delegate that the operation has failed. |
||||
|
* |
||||
|
* @fn NIOperation::didFailWithError: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* In the operation's thread, notify the delegate that the operation will finish successfully. |
||||
|
* |
||||
|
* @fn NIOperation::willFinish |
||||
|
*/ |
||||
@ -0,0 +1,111 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
#import "NIOperations+Subclassing.h" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
@implementation NIOperation |
||||
|
|
||||
|
- (void)dealloc { |
||||
|
// For an unknown reason these block objects are not released when the NIOperation is deallocated |
||||
|
// with ARC enabled. |
||||
|
_didStartBlock = nil; |
||||
|
_didFinishBlock = nil; |
||||
|
_didFailWithErrorBlock = nil; |
||||
|
_willFinishBlock = nil; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Initiate delegate notification from the NSOperation |
||||
|
|
||||
|
- (void)didStart { |
||||
|
[self performSelectorOnMainThread:@selector(onMainThreadOperationDidStart) |
||||
|
withObject:nil |
||||
|
waitUntilDone:[NSThread isMainThread]]; |
||||
|
} |
||||
|
|
||||
|
- (void)didFinish { |
||||
|
[self performSelectorOnMainThread:@selector(onMainThreadOperationDidFinish) |
||||
|
withObject:nil |
||||
|
waitUntilDone:[NSThread isMainThread]]; |
||||
|
} |
||||
|
|
||||
|
- (void)didFailWithError:(NSError *)error { |
||||
|
self.lastError = error; |
||||
|
|
||||
|
[self performSelectorOnMainThread:@selector(onMainThreadOperationDidFailWithError:) |
||||
|
withObject:error |
||||
|
waitUntilDone:[NSThread isMainThread]]; |
||||
|
} |
||||
|
|
||||
|
- (void)willFinish { |
||||
|
if ([self.delegate respondsToSelector:@selector(nimbusOperationWillFinish:)]) { |
||||
|
[self.delegate nimbusOperationWillFinish:self]; |
||||
|
} |
||||
|
|
||||
|
if (nil != self.willFinishBlock) { |
||||
|
self.willFinishBlock(self); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Main Thread |
||||
|
|
||||
|
- (void)onMainThreadOperationDidStart { |
||||
|
// This method should only be called on the main thread. |
||||
|
NIDASSERT([NSThread isMainThread]); |
||||
|
|
||||
|
if ([self.delegate respondsToSelector:@selector(nimbusOperationDidStart:)]) { |
||||
|
[self.delegate nimbusOperationDidStart:self]; |
||||
|
} |
||||
|
|
||||
|
if (nil != self.didStartBlock) { |
||||
|
self.didStartBlock(self); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)onMainThreadOperationDidFinish { |
||||
|
// This method should only be called on the main thread. |
||||
|
NIDASSERT([NSThread isMainThread]); |
||||
|
|
||||
|
if ([self.delegate respondsToSelector:@selector(nimbusOperationDidFinish:)]) { |
||||
|
[self.delegate nimbusOperationDidFinish:self]; |
||||
|
} |
||||
|
|
||||
|
if (nil != self.didFinishBlock) { |
||||
|
self.didFinishBlock(self); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)onMainThreadOperationDidFailWithError:(NSError *)error { |
||||
|
// This method should only be called on the main thread. |
||||
|
NIDASSERT([NSThread isMainThread]); |
||||
|
|
||||
|
if ([self.delegate respondsToSelector:@selector(nimbusOperationDidFail:withError:)]) { |
||||
|
[self.delegate nimbusOperationDidFail:self withError:error]; |
||||
|
} |
||||
|
|
||||
|
if (nil != self.didFailWithErrorBlock) { |
||||
|
self.didFailWithErrorBlock(self, error); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,69 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* 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); |
||||
|
|
||||
|
/** |
||||
|
* Create a path with the Library directory and the relative path appended. |
||||
|
* |
||||
|
* @returns The Library path concatenated with the given relative path. |
||||
|
*/ |
||||
|
NSString* NIPathForLibraryResource(NSString* relativePath); |
||||
|
|
||||
|
/** |
||||
|
* Create a path with the caches directory and the relative path appended. |
||||
|
* |
||||
|
* @returns The caches path concatenated with the given relative path. |
||||
|
*/ |
||||
|
NSString* NIPathForCachesResource(NSString* relativePath); |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
|
|
||||
|
/**@}*/// End of Paths //////////////////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,61 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
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]; |
||||
|
} |
||||
|
return [documentsPath stringByAppendingPathComponent:relativePath]; |
||||
|
} |
||||
|
|
||||
|
NSString* NIPathForLibraryResource(NSString* relativePath) { |
||||
|
static NSString* libraryPath = nil; |
||||
|
if (nil == libraryPath) { |
||||
|
NSArray* dirs = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, |
||||
|
NSUserDomainMask, |
||||
|
YES); |
||||
|
libraryPath = [dirs objectAtIndex:0]; |
||||
|
} |
||||
|
return [libraryPath stringByAppendingPathComponent:relativePath]; |
||||
|
} |
||||
|
|
||||
|
NSString* NIPathForCachesResource(NSString* relativePath) { |
||||
|
static NSString* cachesPath = nil; |
||||
|
if (nil == cachesPath) { |
||||
|
NSArray* dirs = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, |
||||
|
NSUserDomainMask, |
||||
|
YES); |
||||
|
cachesPath = [dirs objectAtIndex:0]; |
||||
|
} |
||||
|
return [cachesPath stringByAppendingPathComponent:relativePath]; |
||||
|
} |
||||
@ -0,0 +1,141 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
|
||||
|
#pragma mark - Preprocessor Macros |
||||
|
|
||||
|
/** |
||||
|
* Preprocessor macros are added to Nimbus with care. Macros hide functionality and are difficult |
||||
|
* to debug, so most macros found in Nimbus are one-liners or compiler utilities. |
||||
|
* |
||||
|
* <h2>Creating Byte- and Hex-based Colors</h2> |
||||
|
* |
||||
|
* Nimbus provides the RGBCOLOR and RGBACOLOR macros for easily creating UIColor objects |
||||
|
* with byte and hex values. |
||||
|
* |
||||
|
* <h3>Examples</h3> |
||||
|
* |
||||
|
@code |
||||
|
UIColor* color = RGBCOLOR(255, 128, 64); // Fully opaque orange |
||||
|
UIColor* color = RGBACOLOR(255, 128, 64, 0.5); // Orange with 50% transparency |
||||
|
UIColor* color = RGBCOLOR(0xFF, 0x7A, 0x64); // Hexadecimal color |
||||
|
@endcode |
||||
|
* |
||||
|
* <h3>Why it exists</h3> |
||||
|
* |
||||
|
* There is no easy way to create UIColor objects using 0 - 255 range values or hexadecimal. This |
||||
|
* leads to code like this being written: |
||||
|
* |
||||
|
@code |
||||
|
UIColor* color = [UIColor colorWithRed:128.f/255.0f green:64.f/255.0f blue:32.f/255.0f alpha:1] |
||||
|
@endcode |
||||
|
* |
||||
|
* <h2>Avoid requiring the -all_load and -force_load flags</h2> |
||||
|
* |
||||
|
* Categories can introduce the need for the -all_load and -force_load because of the fact that |
||||
|
* the application will not load these categories on startup without them. This is due to the way |
||||
|
* Xcode deals with .m files that only contain categories: it doesn't load them without the |
||||
|
* -all_load or -force_load flag specified. |
||||
|
* |
||||
|
* There is, however, a way to force Xcode into loading the category .m file. If you provide an |
||||
|
* empty class implementation in the .m file then your app will pick up the category |
||||
|
* implementation. |
||||
|
* |
||||
|
* Example in plain UIKit: |
||||
|
* |
||||
|
@code |
||||
|
@interface BogusClass |
||||
|
@end |
||||
|
@implementation BogusClass |
||||
|
@end |
||||
|
|
||||
|
@implementation UIViewController (MyCustomCategory) |
||||
|
... |
||||
|
@end |
||||
|
@endcode |
||||
|
* |
||||
|
* NI_FIX_CATEGORY_BUG is a Nimbus macro that you include in your category `.m` file to save you |
||||
|
* the trouble of having to write a bogus class for every category. Just be sure that the name you |
||||
|
* provide to the macro is unique across your project or you will encounter duplicate symbol errors |
||||
|
* when linking. |
||||
|
* |
||||
|
@code |
||||
|
NI_FIX_CATEGORY_BUG(UIViewController_MyCustomCategory); |
||||
|
|
||||
|
@implementation UIViewController (MyCustomCategory) |
||||
|
... |
||||
|
@end |
||||
|
@endcode |
||||
|
* |
||||
|
* @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 |
||||
|
* <pre> |
||||
|
* __AVAILABILITY_INTERNAL_DEPRECATED __attribute__((deprecated)) |
||||
|
* </pre> |
||||
|
* @endhtmlonly |
||||
|
*/ |
||||
|
#define __NI_DEPRECATED_METHOD __attribute__((deprecated)) |
||||
|
|
||||
|
/** |
||||
|
* Mark APIs as unavailable in app extensions. |
||||
|
* |
||||
|
* Use of unavailable methods, classes, or functions produces a compile error when built as part |
||||
|
* of an app extension target. If the method, class or function using the unavailable API has also |
||||
|
* been marked as unavailable in app extensions, the error will be suppressed. |
||||
|
*/ |
||||
|
#ifdef NS_EXTENSION_UNAVAILABLE_IOS |
||||
|
#define NI_EXTENSION_UNAVAILABLE_IOS(msg) NS_EXTENSION_UNAVAILABLE_IOS(msg) |
||||
|
#else |
||||
|
#define NI_EXTENSION_UNAVAILABLE_IOS(msg) |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* 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 : NSObject @end \ |
||||
|
@implementation NI_FIX_CATEGORY_BUG_##name @end |
||||
|
|
||||
|
/** |
||||
|
* Creates an opaque UIColor object from a byte-value color definition. |
||||
|
*/ |
||||
|
#define RGBCOLOR(r,g,b) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:1] |
||||
|
|
||||
|
/** |
||||
|
* Creates a UIColor object from a byte-value color definition and alpha transparency. |
||||
|
*/ |
||||
|
#define RGBACOLOR(r,g,b,a) [UIColor colorWithRed:(r)/255.0f green:(g)/255.0f blue:(b)/255.0f alpha:(a)] |
||||
|
|
||||
|
/**@}*/// End of Preprocessor Macros ////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,74 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* 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); |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
}; |
||||
|
#endif |
||||
|
|
||||
|
/**@}*/// End of Runtime Class Modifications ////////////////////////////////////////////////////// |
||||
@ -0,0 +1,37 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <objc/runtime.h> |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
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); |
||||
|
} |
||||
@ -0,0 +1,257 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIPreprocessorMacros.h" |
||||
|
|
||||
|
/** |
||||
|
* 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 |
||||
|
* <pre> |
||||
|
* #if __IPHONE_OS_VERSION_MAX_ALLOWED >= NIIOS_3_2 |
||||
|
* // This code will only compile on versions >= iOS 3.2 |
||||
|
* #endif |
||||
|
* </pre> |
||||
|
* @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 |
||||
|
|
||||
|
/** |
||||
|
* Released on October 12, 2011. |
||||
|
*/ |
||||
|
#define NIIOS_5_0 50000 |
||||
|
|
||||
|
/** |
||||
|
* Released on March 7, 2012. |
||||
|
*/ |
||||
|
#define NIIOS_5_1 50100 |
||||
|
|
||||
|
/** |
||||
|
* Released on September 19, 2012. |
||||
|
*/ |
||||
|
#define NIIOS_6_0 60000 |
||||
|
|
||||
|
/** |
||||
|
* Released on January 28, 2013. |
||||
|
*/ |
||||
|
#define NIIOS_6_1 60100 |
||||
|
|
||||
|
/** |
||||
|
* Released on September 18, 2013 |
||||
|
*/ |
||||
|
#define NIIOS_7_0 70000 |
||||
|
|
||||
|
#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 |
||||
|
|
||||
|
#ifndef kCFCoreFoundationVersionNumber_iOS_4_1 |
||||
|
#define kCFCoreFoundationVersionNumber_iOS_4_1 550.38 |
||||
|
#endif |
||||
|
|
||||
|
#ifndef kCFCoreFoundationVersionNumber_iOS_4_2 |
||||
|
#define kCFCoreFoundationVersionNumber_iOS_4_2 550.52 |
||||
|
#endif |
||||
|
|
||||
|
#ifndef kCFCoreFoundationVersionNumber_iOS_4_3 |
||||
|
#define kCFCoreFoundationVersionNumber_iOS_4_3 550.52 |
||||
|
#endif |
||||
|
|
||||
|
#ifndef kCFCoreFoundationVersionNumber_iOS_5_0 |
||||
|
#define kCFCoreFoundationVersionNumber_iOS_5_0 675.00 |
||||
|
#endif |
||||
|
|
||||
|
#ifndef kCFCoreFoundationVersionNumber_iOS_5_1 |
||||
|
#define kCFCoreFoundationVersionNumber_iOS_5_1 690.10 |
||||
|
#endif |
||||
|
|
||||
|
#ifndef kCFCoreFoundationVersionNumber_iOS_6_0 |
||||
|
#define kCFCoreFoundationVersionNumber_iOS_6_0 793.00 |
||||
|
#endif |
||||
|
|
||||
|
#ifndef kCFCoreFoundationVersionNumber_iOS_6_1 |
||||
|
#define kCFCoreFoundationVersionNumber_iOS_6_1 793.00 |
||||
|
#endif |
||||
|
|
||||
|
#if defined(__cplusplus) |
||||
|
extern "C" { |
||||
|
#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 the app is currently running on is an |
||||
|
* iPhone/iPod touch or not. |
||||
|
* |
||||
|
* @returns YES if the device is an iPhone or iPod touch. |
||||
|
*/ |
||||
|
BOOL NIIsPhone(void); |
||||
|
|
||||
|
/** |
||||
|
* Checks whether the device supports tint colors on all UIViews. |
||||
|
* |
||||
|
* @returns YES if all UIView instances on the device respond to tintColor. |
||||
|
*/ |
||||
|
BOOL NIIsTintColorGloballySupported(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. |
||||
|
*/ |
||||
|
CGFloat NIScreenScale(void); |
||||
|
|
||||
|
/** |
||||
|
* Returns YES if the screen is a retina display, NO otherwise. |
||||
|
*/ |
||||
|
BOOL NIIsRetina(void); |
||||
|
|
||||
|
/** |
||||
|
* This method is now deprecated. Use [UIPopoverController class] instead. |
||||
|
* |
||||
|
* MAINTENANCE: Remove by Feb 28, 2014. |
||||
|
*/ |
||||
|
Class NIUIPopoverControllerClass(void) __NI_DEPRECATED_METHOD; |
||||
|
|
||||
|
/** |
||||
|
* This method is now deprecated. Use [UITapGestureRecognizer class] instead. |
||||
|
* |
||||
|
* MAINTENANCE: Remove by Feb 28, 2014. |
||||
|
*/ |
||||
|
Class NIUITapGestureRecognizerClass(void) __NI_DEPRECATED_METHOD; |
||||
|
|
||||
|
#if defined(__cplusplus) |
||||
|
} // extern "C" |
||||
|
#endif |
||||
|
|
||||
|
#pragma mark Building with Old SDKs |
||||
|
|
||||
|
// Define methods that were introduced in iOS 7.0. |
||||
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED < NIIOS_7_0 |
||||
|
|
||||
|
@interface UIViewController (Nimbus7SDKAvailability) |
||||
|
|
||||
|
@property (nonatomic, assign) UIRectEdge edgesForExtendedLayout; |
||||
|
- (void)setNeedsStatusBarAppearanceUpdate; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
#endif |
||||
|
|
||||
|
|
||||
|
/**@}*/// End of SDK Availability ///////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,72 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED < NIIOS_6_0 |
||||
|
const UIImageResizingMode UIImageResizingModeStretch = -1; |
||||
|
#endif |
||||
|
|
||||
|
BOOL NIIsPad(void) { |
||||
|
static NSInteger isPad = -1; |
||||
|
if (isPad < 0) { |
||||
|
isPad = ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? 1 : 0; |
||||
|
} |
||||
|
return isPad > 0; |
||||
|
} |
||||
|
|
||||
|
BOOL NIIsPhone(void) { |
||||
|
static NSInteger isPhone = -1; |
||||
|
if (isPhone < 0) { |
||||
|
isPhone = ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) ? 1 : 0; |
||||
|
} |
||||
|
return isPhone > 0; |
||||
|
} |
||||
|
|
||||
|
BOOL NIIsTintColorGloballySupported(void) { |
||||
|
static NSInteger isTintColorGloballySupported = -1; |
||||
|
if (isTintColorGloballySupported < 0) { |
||||
|
UIView* view = [[UIView alloc] init]; |
||||
|
isTintColorGloballySupported = [view respondsToSelector:@selector(tintColor)]; |
||||
|
} |
||||
|
return isTintColorGloballySupported > 0; |
||||
|
} |
||||
|
|
||||
|
BOOL NIDeviceOSVersionIsAtLeast(double versionNumber) { |
||||
|
return kCFCoreFoundationVersionNumber >= versionNumber; |
||||
|
} |
||||
|
|
||||
|
CGFloat NIScreenScale(void) { |
||||
|
return [[UIScreen mainScreen] scale]; |
||||
|
} |
||||
|
|
||||
|
BOOL NIIsRetina(void) { |
||||
|
return NIScreenScale() > 1.f; |
||||
|
} |
||||
|
|
||||
|
Class NIUIPopoverControllerClass(void) { |
||||
|
return [UIPopoverController class]; |
||||
|
} |
||||
|
|
||||
|
Class NIUITapGestureRecognizerClass(void) { |
||||
|
return [UITapGestureRecognizer class]; |
||||
|
} |
||||
@ -0,0 +1,224 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
/** |
||||
|
* An object designed to easily implement snapshot rotation. |
||||
|
* |
||||
|
* Snapshot rotation involves taking two screenshots of a UIView: the "before" and the "after" |
||||
|
* state of the rotation. These two images are then cross-faded during the rotation, creating an |
||||
|
* animation that minimizes visual artifacts that would otherwise be noticed when rotation occurs. |
||||
|
* |
||||
|
* This feature will only function on iOS 6.0 and higher. On older iOS versions the view will rotate |
||||
|
* just as it always has. |
||||
|
* |
||||
|
* This functionality has been adopted from WWDC 2012 session 240 "Polishing Your Interface |
||||
|
* Rotations". |
||||
|
* |
||||
|
* @ingroup NimbusCore |
||||
|
* @defgroup Snapshot-Rotation Snapshot Rotation |
||||
|
* @{ |
||||
|
*/ |
||||
|
|
||||
|
@protocol NISnapshotRotationDelegate; |
||||
|
|
||||
|
/** |
||||
|
* The NISnapshotRotation class provides support for implementing snapshot-based rotations on views. |
||||
|
* |
||||
|
* You must call this object's rotation methods from your controller in order for the rotation |
||||
|
* object to implement the rotation animations correctly. |
||||
|
*/ |
||||
|
@interface NISnapshotRotation : NSObject |
||||
|
|
||||
|
// Designated initializer. |
||||
|
- (id)initWithDelegate:(id<NISnapshotRotationDelegate>)delegate; |
||||
|
|
||||
|
@property (nonatomic, weak) id<NISnapshotRotationDelegate> delegate; |
||||
|
|
||||
|
@property (nonatomic, readonly, assign) CGRect frameBeforeRotation; |
||||
|
@property (nonatomic, readonly, assign) CGRect frameAfterRotation; |
||||
|
|
||||
|
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; |
||||
|
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; |
||||
|
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* The NITableViewSnapshotRotation class implements the fixedInsetsForSnapshotRotation: delegate |
||||
|
* method and forwards all other delegate methods along. |
||||
|
* |
||||
|
* If you are rotating a UITableView you can instantiate a NITableViewSnapshotRotation object and |
||||
|
* use it just like you would a snapshot rotation object. The NITableViewSnapshotRotation class |
||||
|
* intercepts the fixedInsetsForSnapshotRotation: method and returns insets that map to the |
||||
|
* dimensions of the content view for the first visible cell in the table view. |
||||
|
* |
||||
|
* The assigned delegate only needs to implement containerViewForSnapshotRotation: and |
||||
|
* rotatingViewForSnapshotRotation:. |
||||
|
*/ |
||||
|
@interface NITableViewSnapshotRotation : NISnapshotRotation |
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* The methods declared by the NISnapshotRotation protocol allow the adopting delegate to respond to |
||||
|
* messages from the NISnapshotRotation class and thus implement snapshot rotations. |
||||
|
*/ |
||||
|
@protocol NISnapshotRotationDelegate <NSObject> |
||||
|
@required |
||||
|
|
||||
|
/** @name Accessing Rotation Views */ |
||||
|
|
||||
|
/** |
||||
|
* Tells the delegate to return the container view of the rotating view. |
||||
|
* |
||||
|
* This is often the controller's self.view. This view must not be the same as the rotatingView and |
||||
|
* rotatingView must be in the subview tree of containerView. |
||||
|
* |
||||
|
* @sa NISnapshotRotation::rotatingViewForSnapshotRotation: |
||||
|
*/ |
||||
|
- (UIView *)containerViewForSnapshotRotation:(NISnapshotRotation *)snapshotRotation; |
||||
|
|
||||
|
/** |
||||
|
* Tells the delegate to return the rotating view. |
||||
|
* |
||||
|
* The rotating view is the view that will be snapshotted during the rotation. |
||||
|
* |
||||
|
* This view must not be the same as the containerView and must be in the subview tree of |
||||
|
* containerView. |
||||
|
* |
||||
|
* @sa NISnapshotRotation::containerViewForSnapshotRotation: |
||||
|
*/ |
||||
|
- (UIView *)rotatingViewForSnapshotRotation:(NISnapshotRotation *)snapshotRotation; |
||||
|
|
||||
|
@optional |
||||
|
|
||||
|
/** @name Configuring Fixed Insets */ |
||||
|
|
||||
|
/** |
||||
|
* Asks the delegate to return the insets of the rotating view that should be fixed during rotation. |
||||
|
* |
||||
|
* This method will only be called on iOS 6.0 and higher. |
||||
|
* |
||||
|
* The returned insets will denote which parts of the snapshotted images will not stretch during |
||||
|
* the rotation animation. |
||||
|
*/ |
||||
|
- (UIEdgeInsets)fixedInsetsForSnapshotRotation:(NISnapshotRotation *)snapshotRotation; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
extern "C" { |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* Returns an opaque UIImage snapshot of the given view. |
||||
|
* |
||||
|
* This method takes into account the offset of scrollable views and captures whatever is currently |
||||
|
* in the frame of the view. |
||||
|
* |
||||
|
* @param view A snapshot will be taken of this view. |
||||
|
* @returns A UIImage with the snapshot of @c view. |
||||
|
*/ |
||||
|
UIImage* NISnapshotOfView(UIView* view); |
||||
|
|
||||
|
/** |
||||
|
* Returns a UIImageView with an image snapshot of the given view. |
||||
|
* |
||||
|
* The frame of the returned view is set to match the frame of @c view. |
||||
|
* |
||||
|
* @param view A snapshot will be taken of this view. |
||||
|
* @returns A UIImageView with the snapshot of @c view and matching frame. |
||||
|
*/ |
||||
|
UIImageView* NISnapshotViewOfView(UIView* view); |
||||
|
|
||||
|
/** |
||||
|
* Returns a UIImage snapshot of the given view with transparency. |
||||
|
* |
||||
|
* This method takes into account the offset of scrollable views and captures whatever is currently |
||||
|
* in the frame of the view. |
||||
|
* |
||||
|
* @param view A snapshot will be taken of this view. |
||||
|
* @returns A UIImage with the snapshot of @c view. |
||||
|
*/ |
||||
|
UIImage* NISnapshotOfViewWithTransparency(UIView* view); |
||||
|
|
||||
|
/** |
||||
|
* Returns a UIImageView with an image snapshot with transparency of the given view. |
||||
|
* |
||||
|
* The frame of the returned view is set to match the frame of @c view. |
||||
|
* |
||||
|
* @param view A snapshot will be taken of this view. |
||||
|
* @returns A UIImageView with the snapshot of @c view and matching frame. |
||||
|
*/ |
||||
|
UIImageView* NISnapshotViewOfViewWithTransparency(UIView* view); |
||||
|
|
||||
|
#if defined __cplusplus |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* @} |
||||
|
*/ |
||||
|
|
||||
|
/** @name Creating a Snapshot Rotation Object */ |
||||
|
|
||||
|
/** |
||||
|
* Initializes a newly allocated rotation object with a given delegate. |
||||
|
* |
||||
|
* @param delegate A delegate that implements the NISnapshotRotation protocol. |
||||
|
* @returns A NISnapshotRotation object initialized with @c delegate. |
||||
|
* @fn NISnapshotRotation::initWithDelegate: |
||||
|
*/ |
||||
|
|
||||
|
/** @name Accessing the Delegate */ |
||||
|
|
||||
|
/** |
||||
|
* The delegate of the snapshot rotation object. |
||||
|
* |
||||
|
* The delegate must adopt the NISnapshotRotation protocol. The NISnapshotRotation class, which does |
||||
|
* not retain the delegate, invokes each protocol method the delegate implements. |
||||
|
* |
||||
|
* @fn NISnapshotRotation::delegate |
||||
|
*/ |
||||
|
|
||||
|
/** @name Implementing UIViewController Autorotation */ |
||||
|
|
||||
|
/** |
||||
|
* Prepares the animation for a rotation by taking a snapshot of the rotatingView in its current |
||||
|
* state. |
||||
|
* |
||||
|
* This method must be called from your UIViewController implementation. |
||||
|
* |
||||
|
* @fn NISnapshotRotation::willRotateToInterfaceOrientation:duration: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Crossfades between the initial and final snapshots. |
||||
|
* |
||||
|
* This method must be called from your UIViewController implementation. |
||||
|
* |
||||
|
* @fn NISnapshotRotation::willAnimateRotationToInterfaceOrientation:duration: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Finalizes the rotation animation by removing the snapshot views from the container view. |
||||
|
* |
||||
|
* This method must be called from your UIViewController implementation. |
||||
|
* |
||||
|
* @fn NISnapshotRotation::didRotateFromInterfaceOrientation: |
||||
|
*/ |
||||
@ -0,0 +1,299 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// This code was originally found in Apple's WWDC Session 240 on |
||||
|
// "Polishing Your Interface Rotations" and has been repurposed into a reusable class. |
||||
|
// |
||||
|
// 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 "NISnapshotRotation.h" |
||||
|
|
||||
|
#import "NIDebuggingTools.h" |
||||
|
#import "NISDKAvailability.h" |
||||
|
#import <QuartzCore/QuartzCore.h> |
||||
|
#import <objc/runtime.h> |
||||
|
|
||||
|
#if __IPHONE_OS_VERSION_MIN_REQUIRED < NIIOS_6_0 |
||||
|
#error "Nimbus Snapshot Rotation requires iOS 6 or higher." |
||||
|
#endif |
||||
|
|
||||
|
UIImage* NISnapshotOfViewWithTransparencyOption(UIView* view, BOOL transparency); |
||||
|
|
||||
|
UIImage* NISnapshotOfViewWithTransparencyOption(UIView* view, BOOL transparency) { |
||||
|
// Passing 0 as the last argument ensures that the image context will match the current device's |
||||
|
// scaling mode. |
||||
|
UIGraphicsBeginImageContextWithOptions(view.bounds.size, !transparency, 0); |
||||
|
|
||||
|
CGContextRef cx = UIGraphicsGetCurrentContext(); |
||||
|
|
||||
|
// Views that can scroll do so by modifying their bounds. We want to capture the part of the view |
||||
|
// that is currently in the frame, so we offset by the bounds of the view accordingly. |
||||
|
CGContextTranslateCTM(cx, -view.bounds.origin.x, -view.bounds.origin.y); |
||||
|
|
||||
|
BOOL didDraw = NO; |
||||
|
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= NIIOS_7_0 |
||||
|
if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) { |
||||
|
didDraw = [view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES]; |
||||
|
} |
||||
|
#endif |
||||
|
if (!didDraw) { |
||||
|
[view.layer renderInContext:cx]; |
||||
|
} |
||||
|
|
||||
|
UIImage* image = UIGraphicsGetImageFromCurrentImageContext(); |
||||
|
UIGraphicsEndImageContext(); |
||||
|
|
||||
|
return image; |
||||
|
} |
||||
|
|
||||
|
UIImage* NISnapshotOfView(UIView* view) { |
||||
|
return NISnapshotOfViewWithTransparencyOption(view, NO); |
||||
|
} |
||||
|
|
||||
|
UIImageView* NISnapshotViewOfView(UIView* view) { |
||||
|
UIImage* image = NISnapshotOfView(view); |
||||
|
|
||||
|
UIImageView* snapshotView = [[UIImageView alloc] initWithImage:image]; |
||||
|
snapshotView.frame = view.frame; |
||||
|
|
||||
|
return snapshotView; |
||||
|
} |
||||
|
|
||||
|
UIImage* NISnapshotOfViewWithTransparency(UIView* view) { |
||||
|
return NISnapshotOfViewWithTransparencyOption(view, YES); |
||||
|
} |
||||
|
|
||||
|
UIImageView* NISnapshotViewOfViewWithTransparency(UIView* view) { |
||||
|
UIImage* image = NISnapshotOfViewWithTransparency(view); |
||||
|
|
||||
|
UIImageView* snapshotView = [[UIImageView alloc] initWithImage:image]; |
||||
|
snapshotView.frame = view.frame; |
||||
|
|
||||
|
return snapshotView; |
||||
|
} |
||||
|
|
||||
|
@interface NISnapshotRotation() |
||||
|
@property (nonatomic, assign) BOOL isSupportedOS; |
||||
|
@property (nonatomic, assign) CGRect frameBeforeRotation; |
||||
|
@property (nonatomic, assign) CGRect frameAfterRotation; |
||||
|
|
||||
|
@property (nonatomic, strong) UIImageView* snapshotViewBeforeRotation; |
||||
|
@property (nonatomic, strong) UIImageView* snapshotViewAfterRotation; |
||||
|
@end |
||||
|
|
||||
|
@implementation NISnapshotRotation |
||||
|
|
||||
|
- (id)initWithDelegate:(id<NISnapshotRotationDelegate>)delegate { |
||||
|
if ((self = [super init])) { |
||||
|
_delegate = delegate; |
||||
|
|
||||
|
// Check whether this feature is supported or not. |
||||
|
UIImage* image = [[UIImage alloc] init]; |
||||
|
_isSupportedOS = [image respondsToSelector:@selector(resizableImageWithCapInsets:resizingMode:)]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (id)init { |
||||
|
return [self initWithDelegate:nil]; |
||||
|
} |
||||
|
|
||||
|
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { |
||||
|
if (!self.isSupportedOS) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
UIView* containerView = [self.delegate containerViewForSnapshotRotation:self]; |
||||
|
UIView* rotationView = [self.delegate rotatingViewForSnapshotRotation:self]; |
||||
|
|
||||
|
// The container view must not be the same as the rotation view. |
||||
|
NIDASSERT(containerView != rotationView); |
||||
|
if (containerView == rotationView) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
self.frameBeforeRotation = rotationView.frame; |
||||
|
self.snapshotViewBeforeRotation = NISnapshotViewOfViewWithTransparency(rotationView); |
||||
|
[containerView insertSubview:self.snapshotViewBeforeRotation aboveSubview:rotationView]; |
||||
|
} |
||||
|
|
||||
|
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration { |
||||
|
if (!self.isSupportedOS) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
UIView* containerView = [self.delegate containerViewForSnapshotRotation:self]; |
||||
|
UIView* rotationView = [self.delegate rotatingViewForSnapshotRotation:self]; |
||||
|
|
||||
|
// The container view must not be the same as the rotation view. |
||||
|
NIDASSERT(containerView != rotationView); |
||||
|
if (containerView == rotationView) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
self.frameAfterRotation = rotationView.frame; |
||||
|
|
||||
|
[UIView setAnimationsEnabled:NO]; |
||||
|
|
||||
|
self.snapshotViewAfterRotation = NISnapshotViewOfViewWithTransparency(rotationView); |
||||
|
// Set the new frame while maintaining the old frame's height. |
||||
|
self.snapshotViewAfterRotation.frame = CGRectMake(self.frameBeforeRotation.origin.x, |
||||
|
self.frameBeforeRotation.origin.y, |
||||
|
self.frameBeforeRotation.size.width, |
||||
|
self.snapshotViewAfterRotation.frame.size.height); |
||||
|
|
||||
|
UIImage* imageBeforeRotation = self.snapshotViewBeforeRotation.image; |
||||
|
UIImage* imageAfterRotation = self.snapshotViewAfterRotation.image; |
||||
|
|
||||
|
if ([self.delegate respondsToSelector:@selector(fixedInsetsForSnapshotRotation:)]) { |
||||
|
UIEdgeInsets fixedInsets = [self.delegate fixedInsetsForSnapshotRotation:self]; |
||||
|
|
||||
|
imageBeforeRotation = [imageBeforeRotation resizableImageWithCapInsets:fixedInsets resizingMode:UIImageResizingModeStretch]; |
||||
|
imageAfterRotation = [imageAfterRotation resizableImageWithCapInsets:fixedInsets resizingMode:UIImageResizingModeStretch]; |
||||
|
} |
||||
|
|
||||
|
self.snapshotViewBeforeRotation.image = imageBeforeRotation; |
||||
|
self.snapshotViewAfterRotation.image = imageAfterRotation; |
||||
|
|
||||
|
[UIView setAnimationsEnabled:YES]; |
||||
|
|
||||
|
if (imageAfterRotation.size.height < imageBeforeRotation.size.height) { |
||||
|
self.snapshotViewAfterRotation.alpha = 0; |
||||
|
|
||||
|
[containerView insertSubview:self.snapshotViewAfterRotation aboveSubview:self.snapshotViewBeforeRotation]; |
||||
|
|
||||
|
self.snapshotViewAfterRotation.alpha = 1; |
||||
|
|
||||
|
} else { |
||||
|
[containerView insertSubview:self.snapshotViewAfterRotation belowSubview:self.snapshotViewBeforeRotation]; |
||||
|
self.snapshotViewBeforeRotation.alpha = 0; |
||||
|
} |
||||
|
|
||||
|
self.snapshotViewAfterRotation.frame = self.frameAfterRotation; |
||||
|
self.snapshotViewBeforeRotation.frame = CGRectMake(self.frameAfterRotation.origin.x, |
||||
|
self.frameAfterRotation.origin.y, |
||||
|
self.frameAfterRotation.size.width, |
||||
|
self.snapshotViewBeforeRotation.frame.size.height); |
||||
|
|
||||
|
rotationView.hidden = YES; |
||||
|
} |
||||
|
|
||||
|
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation { |
||||
|
if (!self.isSupportedOS) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
UIView* containerView = [self.delegate containerViewForSnapshotRotation:self]; |
||||
|
UIView* rotationView = [self.delegate rotatingViewForSnapshotRotation:self]; |
||||
|
|
||||
|
// The container view must not be the same as the rotation view. |
||||
|
NIDASSERT(containerView != rotationView); |
||||
|
if (containerView == rotationView) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
[self.snapshotViewBeforeRotation removeFromSuperview]; |
||||
|
[self.snapshotViewAfterRotation removeFromSuperview]; |
||||
|
self.snapshotViewBeforeRotation = nil; |
||||
|
self.snapshotViewAfterRotation = nil; |
||||
|
|
||||
|
rotationView.hidden = NO; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@interface NITableViewSnapshotRotation() <NISnapshotRotationDelegate> |
||||
|
@property (nonatomic, weak) id<NISnapshotRotationDelegate> forwardingDelegate; |
||||
|
@end |
||||
|
|
||||
|
@implementation NITableViewSnapshotRotation |
||||
|
|
||||
|
- (void)setDelegate:(id<NISnapshotRotationDelegate>)delegate { |
||||
|
if (delegate == self) { |
||||
|
[super setDelegate:delegate]; |
||||
|
|
||||
|
} else { |
||||
|
self.forwardingDelegate = delegate; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (id)init { |
||||
|
if ((self = [super init])) { |
||||
|
self.delegate = self; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Forward Invocations |
||||
|
|
||||
|
- (BOOL)shouldForwardSelectorToDelegate:(SEL)selector { |
||||
|
struct objc_method_description description; |
||||
|
// Only forward the selector if it's part of the protocol. |
||||
|
description = protocol_getMethodDescription(@protocol(NISnapshotRotationDelegate), selector, NO, YES); |
||||
|
|
||||
|
BOOL isSelectorInProtocol = (description.name != NULL && description.types != NULL); |
||||
|
return (isSelectorInProtocol && [self.forwardingDelegate respondsToSelector:selector]); |
||||
|
} |
||||
|
|
||||
|
- (BOOL)respondsToSelector:(SEL)selector { |
||||
|
if ([super respondsToSelector:selector] == YES) { |
||||
|
return YES; |
||||
|
|
||||
|
} else { |
||||
|
return [self shouldForwardSelectorToDelegate:selector]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (id)forwardingTargetForSelector:(SEL)selector { |
||||
|
if ([self shouldForwardSelectorToDelegate:selector]) { |
||||
|
return self.forwardingDelegate; |
||||
|
|
||||
|
} else { |
||||
|
return nil; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - NISnapshotRotation |
||||
|
|
||||
|
- (UIView *)containerViewForSnapshotRotation:(NISnapshotRotation *)snapshotRotation { |
||||
|
return [self.forwardingDelegate containerViewForSnapshotRotation:snapshotRotation]; |
||||
|
} |
||||
|
|
||||
|
- (UIView *)rotatingViewForSnapshotRotation:(NISnapshotRotation *)snapshotRotation { |
||||
|
return [self.forwardingDelegate rotatingViewForSnapshotRotation:snapshotRotation]; |
||||
|
} |
||||
|
|
||||
|
- (UIEdgeInsets)fixedInsetsForSnapshotRotation:(NISnapshotRotation *)snapshotRotation { |
||||
|
UIEdgeInsets insets = UIEdgeInsetsZero; |
||||
|
|
||||
|
// Find the right edge of the content view in the coordinate space of the UITableView. |
||||
|
UIView* rotatingView = [self.forwardingDelegate rotatingViewForSnapshotRotation:snapshotRotation]; |
||||
|
NIDASSERT([rotatingView isKindOfClass:[UITableView class]]); |
||||
|
if ([rotatingView isKindOfClass:[UITableView class]]) { |
||||
|
UITableView* tableView = (UITableView *)rotatingView; |
||||
|
|
||||
|
NSArray* visibleCells = tableView.visibleCells; |
||||
|
if (visibleCells.count > 0) { |
||||
|
UIView* contentView = [[visibleCells objectAtIndex:0] contentView]; |
||||
|
CGFloat contentViewRightEdge = [tableView convertPoint:CGPointMake(contentView.bounds.size.width, 0) fromView:contentView].x; |
||||
|
|
||||
|
CGFloat fixedRightWidth = tableView.bounds.size.width - contentViewRightEdge; |
||||
|
CGFloat fixedLeftWidth = MIN(snapshotRotation.frameAfterRotation.size.width, snapshotRotation.frameBeforeRotation.size.width) - fixedRightWidth - 1; |
||||
|
insets = UIEdgeInsetsMake(0, fixedLeftWidth, 0, fixedRightWidth); |
||||
|
} |
||||
|
} |
||||
|
return insets; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,84 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
@class NIImageMemoryCache; |
||||
|
|
||||
|
/** |
||||
|
* 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 //////////////////////////////////////////////////////////////////////////// |
||||
@ -0,0 +1,58 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
static NIImageMemoryCache* sNimbusGlobalMemoryCache = nil; |
||||
|
static NSOperationQueue* sNimbusGlobalOperationQueue = nil; |
||||
|
|
||||
|
@implementation Nimbus |
||||
|
|
||||
|
+ (void)setImageMemoryCache:(NIImageMemoryCache *)imageMemoryCache { |
||||
|
if (sNimbusGlobalMemoryCache != imageMemoryCache) { |
||||
|
sNimbusGlobalMemoryCache = nil; |
||||
|
sNimbusGlobalMemoryCache = imageMemoryCache; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
+ (NIImageMemoryCache *)imageMemoryCache { |
||||
|
if (nil == sNimbusGlobalMemoryCache) { |
||||
|
sNimbusGlobalMemoryCache = [[NIImageMemoryCache alloc] init]; |
||||
|
} |
||||
|
return sNimbusGlobalMemoryCache; |
||||
|
} |
||||
|
|
||||
|
+ (void)setNetworkOperationQueue:(NSOperationQueue *)queue { |
||||
|
if (sNimbusGlobalOperationQueue != queue) { |
||||
|
sNimbusGlobalOperationQueue = nil; |
||||
|
sNimbusGlobalOperationQueue = queue; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
+ (NSOperationQueue *)networkOperationQueue { |
||||
|
if (nil == sNimbusGlobalOperationQueue) { |
||||
|
sNimbusGlobalOperationQueue = [[NSOperationQueue alloc] init]; |
||||
|
} |
||||
|
return sNimbusGlobalOperationQueue; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,164 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
/** |
||||
|
* For recycling views in scroll views. |
||||
|
* |
||||
|
* @ingroup NimbusCore |
||||
|
* @defgroup Core-View-Recycling View Recyling |
||||
|
* @{ |
||||
|
* |
||||
|
* View recycling is an important aspect of iOS memory management and performance when building |
||||
|
* scroll views. UITableView uses view recycling via the table cell dequeue mechanism. |
||||
|
* NIViewRecycler implements this recycling functionality, allowing you to implement recycling |
||||
|
* mechanisms in your own views and controllers. |
||||
|
* |
||||
|
* |
||||
|
* <h2>Example Use</h2> |
||||
|
* |
||||
|
* Imagine building a UITableView. We'll assume that a viewRecycler object exists in the view. |
||||
|
* |
||||
|
* Views are usually recycled once they are no longer on screen, so within a did scroll event |
||||
|
* we might have code like the following: |
||||
|
* |
||||
|
@code |
||||
|
for (UIView<NIRecyclableView>* view in visibleViews) { |
||||
|
if (![self isVisible:view]) { |
||||
|
[viewRecycler recycleView:view]; |
||||
|
[view removeFromSuperview]; |
||||
|
} |
||||
|
} |
||||
|
@endcode |
||||
|
* |
||||
|
* This will take the views that are no longer visible and add them to the recycler. At a later |
||||
|
* point in that same didScroll code we will check if there are any new views that are visible. |
||||
|
* This is when we try to dequeue a recycled view from the recycler. |
||||
|
* |
||||
|
@code |
||||
|
UIView<NIRecyclableView>* view = [viewRecycler dequeueReusableViewWithIdentifier:reuseIdentifier]; |
||||
|
if (nil == view) { |
||||
|
// Allocate a new view that conforms to the NIRecyclableView protocol. |
||||
|
view = [[[...]] autorelease]; |
||||
|
} |
||||
|
[self addSubview:view]; |
||||
|
@endcode |
||||
|
* |
||||
|
*/ |
||||
|
|
||||
|
@protocol NIRecyclableView; |
||||
|
|
||||
|
/** |
||||
|
* An object for efficiently reusing views by recycling and dequeuing them from a pool of views. |
||||
|
* |
||||
|
* This sort of object is likely what UITableView and NIPagingScrollView use to recycle their views. |
||||
|
*/ |
||||
|
@interface NIViewRecycler : NSObject |
||||
|
|
||||
|
- (UIView<NIRecyclableView> *)dequeueReusableViewWithIdentifier:(NSString *)reuseIdentifier; |
||||
|
|
||||
|
- (void)recycleView:(UIView<NIRecyclableView> *)view; |
||||
|
|
||||
|
- (void)removeAllViews; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* The NIRecyclableView protocol defines a set of optional methods that a view may implement to |
||||
|
* handle being added to a NIViewRecycler. |
||||
|
*/ |
||||
|
@protocol NIRecyclableView <NSObject> |
||||
|
|
||||
|
@optional |
||||
|
|
||||
|
/** |
||||
|
* The identifier used to categorize views into buckets for reuse. |
||||
|
* |
||||
|
* Views will be reused when a new view is requested with a matching identifier. |
||||
|
* |
||||
|
* If the reuseIdentifier is nil then the class name will be used. |
||||
|
*/ |
||||
|
@property (nonatomic, copy) NSString* reuseIdentifier; |
||||
|
|
||||
|
/** |
||||
|
* Called immediately after the view has been dequeued from the recycled view pool. |
||||
|
*/ |
||||
|
- (void)prepareForReuse; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* A simple implementation of the NIRecyclableView protocol as a UIView. |
||||
|
* |
||||
|
* This class can be used as a base class for building recyclable views if specific reuse |
||||
|
* identifiers are necessary, e.g. when the same class might have different implementations |
||||
|
* depending on the reuse identifier. |
||||
|
* |
||||
|
* Assuming functionality is consistent for a given class it is simpler not to have a |
||||
|
* reuseIdentifier, making the view recycler use the class name as the reuseIdentifier. In this case |
||||
|
* subclassing this class is overkill. |
||||
|
*/ |
||||
|
@interface NIRecyclableView : UIView <NIRecyclableView> |
||||
|
|
||||
|
// Designated initializer. |
||||
|
- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier; |
||||
|
|
||||
|
@property (nonatomic, copy) NSString* reuseIdentifier; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/**@}*/ // End of View Recyling |
||||
|
|
||||
|
/** |
||||
|
* Dequeues a reusable view from the recycled views pool if one exists, otherwise returns nil. |
||||
|
* |
||||
|
* @fn NIViewRecycler::dequeueReusableViewWithIdentifier: |
||||
|
* @param reuseIdentifier Often the name of the class of view you wish to fetch. |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Adds a given view to the recycled views pool. |
||||
|
* |
||||
|
* @fn NIViewRecycler::recycleView: |
||||
|
* @param view The view to recycle. The reuse identifier will be retrieved from the view |
||||
|
* via the NIRecyclableView protocol. |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Removes all of the views from the recycled views pool. |
||||
|
* |
||||
|
* @fn NIViewRecycler::removeAllViews |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Initializes a newly allocated view with the given reuse identifier. |
||||
|
* |
||||
|
* This is the designated initializer. |
||||
|
* |
||||
|
* @fn NIRecyclableView::initWithReuseIdentifier: |
||||
|
* @param reuseIdentifier The identifier that will be used to group this view in the view |
||||
|
* recycler. |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* This view's reuse identifier. |
||||
|
* |
||||
|
* Used by NIViewRecycler to pool this view into a group of similar recycled views. |
||||
|
* |
||||
|
* @fn NIRecyclableView::reuseIdentifier |
||||
|
*/ |
||||
@ -0,0 +1,111 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIViewRecycler.h" |
||||
|
|
||||
|
#import "NimbusCore.h" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
@interface NIViewRecycler() |
||||
|
@property (nonatomic, strong) NSMutableDictionary* reuseIdentifiersToRecycledViews; |
||||
|
@end |
||||
|
|
||||
|
@implementation NIViewRecycler |
||||
|
|
||||
|
- (void)dealloc { |
||||
|
[[NSNotificationCenter defaultCenter] removeObserver:self]; |
||||
|
} |
||||
|
|
||||
|
- (id)init { |
||||
|
if ((self = [super init])) { |
||||
|
_reuseIdentifiersToRecycledViews = [[NSMutableDictionary alloc] init]; |
||||
|
|
||||
|
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter]; |
||||
|
[nc addObserver:self |
||||
|
selector:@selector(reduceMemoryUsage) |
||||
|
name:UIApplicationDidReceiveMemoryWarningNotification |
||||
|
object:nil]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Memory Warnings |
||||
|
|
||||
|
- (void)reduceMemoryUsage { |
||||
|
[self removeAllViews]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Public |
||||
|
|
||||
|
- (UIView<NIRecyclableView> *)dequeueReusableViewWithIdentifier:(NSString *)reuseIdentifier { |
||||
|
NSMutableArray* views = [_reuseIdentifiersToRecycledViews objectForKey:reuseIdentifier]; |
||||
|
UIView<NIRecyclableView>* view = [views lastObject]; |
||||
|
if (nil != view) { |
||||
|
[views removeLastObject]; |
||||
|
if ([view respondsToSelector:@selector(prepareForReuse)]) { |
||||
|
[view prepareForReuse]; |
||||
|
} |
||||
|
} |
||||
|
return view; |
||||
|
} |
||||
|
|
||||
|
- (void)recycleView:(UIView<NIRecyclableView> *)view { |
||||
|
NIDASSERT([view isKindOfClass:[UIView class]]); |
||||
|
|
||||
|
NSString* reuseIdentifier = nil; |
||||
|
if ([view respondsToSelector:@selector(reuseIdentifier)]) { |
||||
|
reuseIdentifier = [view reuseIdentifier];; |
||||
|
} |
||||
|
if (nil == reuseIdentifier) { |
||||
|
reuseIdentifier = NSStringFromClass([view class]); |
||||
|
} |
||||
|
|
||||
|
NIDASSERT(nil != reuseIdentifier); |
||||
|
if (nil == reuseIdentifier) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
NSMutableArray* views = [_reuseIdentifiersToRecycledViews objectForKey:reuseIdentifier]; |
||||
|
if (nil == views) { |
||||
|
views = [[NSMutableArray alloc] init]; |
||||
|
[_reuseIdentifiersToRecycledViews setObject:views forKey:reuseIdentifier]; |
||||
|
} |
||||
|
[views addObject:view]; |
||||
|
} |
||||
|
|
||||
|
- (void)removeAllViews { |
||||
|
[_reuseIdentifiersToRecycledViews removeAllObjects]; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@implementation NIRecyclableView |
||||
|
|
||||
|
- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier { |
||||
|
if ((self = [super initWithFrame:CGRectZero])) { |
||||
|
_reuseIdentifier = reuseIdentifier; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (id)initWithFrame:(CGRect)frame { |
||||
|
return [self initWithReuseIdentifier:nil]; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,26 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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. |
||||
|
// |
||||
|
|
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NimbusCore.h" |
||||
|
|
||||
|
// Additions |
||||
|
#import "UIResponder+NimbusCore.h" |
||||
@ -0,0 +1,120 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 |
||||
|
* |
||||
|
* <div id="github" feature="core"></div> |
||||
|
* |
||||
|
* 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. |
||||
|
* |
||||
|
* <h2>How Features are Added to the Core</h2> |
||||
|
* |
||||
|
* 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. |
||||
|
* |
||||
|
* <h3>Exceptions</h3> |
||||
|
* |
||||
|
* Standalone user interface components are <i>rarely</i> 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. |
||||
|
* |
||||
|
* <h2>How Features are Removed from the Core</h2> |
||||
|
* |
||||
|
* 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. |
||||
|
* |
||||
|
* <h2>Common autoresizing masks</h2> |
||||
|
* |
||||
|
* Nimbus provides the following macros: UIViewAutoresizingFlexibleMargins, |
||||
|
* UIViewAutoresizingFlexibleDimensions, UIViewAutoresizingNavigationBar, and |
||||
|
* UIViewAutoresizingToolbarBar. |
||||
|
* |
||||
|
@code |
||||
|
// Create a view that fills its superview's bounds. |
||||
|
UIView* contentView = [[UIView alloc] initWithFrame:self.view.bounds]; |
||||
|
contentView.autoresizingMask = UIViewAutoresizingFlexibleDimensions; |
||||
|
[self.view addSubview:contentView]; |
||||
|
|
||||
|
// Create a view that is always centered in the superview's bounds. |
||||
|
UIView* centeredView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)]; |
||||
|
centeredView.autoresizingMask = UIViewAutoresizingFlexibleMargins; |
||||
|
// Center the view within the superview however you choose. |
||||
|
[self.view addSubview:centeredView]; |
||||
|
|
||||
|
// Create a navigation bar that stays fixed to the top. |
||||
|
UINavigationBar* navBar = [[UINavigationBar alloc] initWithFrame:CGRectZero]; |
||||
|
[navBar sizeToFit]; |
||||
|
navBar.autoresizingMask = UIViewAutoresizingNavigationBar; |
||||
|
[self.view addSubview:navBar]; |
||||
|
|
||||
|
// Create a toolbar that stays fixed to the bottom. |
||||
|
UIToolbar* toolBar = [[UIToolbar alloc] initWithFrame:CGRectZero]; |
||||
|
[toolBar sizeToFit]; |
||||
|
toolBar.autoresizingMask = UIViewAutoresizingToolbarBar; |
||||
|
[self.view addSubview:toolBar]; |
||||
|
@endcode |
||||
|
* |
||||
|
* <h3>Why they exist</h3> |
||||
|
* |
||||
|
* Using the existing UIViewAutoresizing flags can be tedious for common flags. |
||||
|
* |
||||
|
* For example, to make a view have flexible margins you would need to write four flags: |
||||
|
* |
||||
|
@code |
||||
|
view.autoresizingMask = (UIViewAutoresizingFlexibleLeftMargin |
||||
|
| UIViewAutoresizingFlexibleTopMargin |
||||
|
| UIViewAutoresizingFlexibleRightMargin |
||||
|
| UIViewAutoresizingFlexibleBottomMargin); |
||||
|
@endcode |
||||
|
*/ |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIActions.h" |
||||
|
#import "NIButtonUtilities.h" |
||||
|
#import "NICommonMetrics.h" |
||||
|
#import "NIDebuggingTools.h" |
||||
|
#import "NIDeviceOrientation.h" |
||||
|
#import "NIError.h" |
||||
|
#import "NIFoundationMethods.h" |
||||
|
#import "NIImageUtilities.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 "NISnapshotRotation.h" |
||||
|
#import "NIState.h" |
||||
|
#import "NIViewRecycler.h" |
||||
@ -0,0 +1,26 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// +currentFirstResponder originally written by Jakob Egger, adapted by 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 <UIKit/UIKit.h> |
||||
|
|
||||
|
// Documentation for these additions is found in the .m file. |
||||
|
@interface UIResponder (NimbusCore) |
||||
|
|
||||
|
+ (instancetype)nimbus_currentFirstResponder; |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,49 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// +currentFirstResponder originally written by Jakob Egger, adapted by 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 "UIResponder+NimbusCore.h" |
||||
|
|
||||
|
#import "NIPreprocessorMacros.h" |
||||
|
|
||||
|
// Adapted from http://stackoverflow.com/questions/5029267/is-there-any-way-of-asking-an-ios-view-which-of-its-children-has-first-responder/14135456#14135456 |
||||
|
|
||||
|
static __weak id sCurrentFirstResponder = nil; |
||||
|
|
||||
|
NI_FIX_CATEGORY_BUG(UIResponderNimbusCore) |
||||
|
/** |
||||
|
* For working with UIResponders. |
||||
|
*/ |
||||
|
@implementation UIResponder (NimbusCore) |
||||
|
|
||||
|
/** |
||||
|
* Returns the current first responder by sending an action from the UIApplication. |
||||
|
* |
||||
|
* The implementation was adapted from http://stackoverflow.com/questions/5029267/is-there-any-way-of-asking-an-ios-view-which-of-its-children-has-first-responder/14135456#14135456 |
||||
|
*/ |
||||
|
+ (instancetype)nimbus_currentFirstResponder { |
||||
|
sCurrentFirstResponder = nil; |
||||
|
[[UIApplication sharedApplication] sendAction:@selector(nimbus_findFirstResponder:) |
||||
|
to:nil from:nil forEvent:nil]; |
||||
|
return sCurrentFirstResponder; |
||||
|
} |
||||
|
|
||||
|
- (void)nimbus_findFirstResponder:(id)sender { |
||||
|
sCurrentFirstResponder = self; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,123 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIPagingScrollView.h" |
||||
|
|
||||
|
// Methods that are meant to be subclassed. |
||||
|
@interface NIPagingScrollView (Subclassing) |
||||
|
|
||||
|
// Meant to be subclassed. Default implementations are stubs. |
||||
|
- (void)willDisplayPage:(UIView<NIPagingScrollViewPage> *)pageView; |
||||
|
- (void)didRecyclePage:(UIView<NIPagingScrollViewPage> *)pageView; |
||||
|
- (void)didReloadNumberOfPages; |
||||
|
- (void)didChangeCenterPageIndexFrom:(NSInteger)from to:(NSInteger)to; |
||||
|
|
||||
|
// Meant to be subclassed. |
||||
|
- (UIView<NIPagingScrollViewPage> *)loadPageAtIndex:(NSInteger)pageIndex; |
||||
|
|
||||
|
#pragma mark Accessing Child Views |
||||
|
|
||||
|
- (UIScrollView *)scrollView; |
||||
|
- (NSMutableSet *)visiblePages; // Set of UIView<NIPagingScrollViewPage>* |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
// Methods that are not meant to be subclassed. |
||||
|
@interface NIPagingScrollView (ProtectedMethods) |
||||
|
|
||||
|
- (void)setCenterPageIndexIvar:(NSInteger)centerPageIndex; |
||||
|
- (void)recyclePageAtIndex:(NSInteger)pageIndex; |
||||
|
- (void)displayPageAtIndex:(NSInteger)pageIndex; |
||||
|
- (CGFloat)pageScrollableDimension; |
||||
|
- (void)layoutVisiblePages; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* Called before the page is about to be shown and after its frame has been set. |
||||
|
* |
||||
|
* Meant to be subclassed. By default this method does nothing. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::willDisplayPage: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Called immediately after the page is removed from the paging scroll view. |
||||
|
* |
||||
|
* Meant to be subclassed. By default this method does nothing. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::didRecyclePage: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Called immediately after the data source has been queried for its number of |
||||
|
* pages. |
||||
|
* |
||||
|
* Meant to be subclassed. By default this method does nothing. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::didReloadNumberOfPages |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Called when the visible page has changed. |
||||
|
* |
||||
|
* Meant to be subclassed. By default this method does nothing. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::didChangeCenterPageIndexFrom:to: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Called when a page needs to be loaded before it is displayed. |
||||
|
* |
||||
|
* By default this method asks the data source for the page at the given index. |
||||
|
* A subclass may chose to modify the page index using a transformation method |
||||
|
* before calling super. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::loadPageAtIndex: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Sets the centerPageIndex ivar without side effects. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::setCenterPageIndexIvar: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Recycles the page at the given index. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::recyclePageAtIndex: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Displays the page at the given index. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::displayPageAtIndex: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Returns the page's scrollable dimension. |
||||
|
* |
||||
|
* This is the width of the paging scroll view for horizontal scroll views, or |
||||
|
* the height of the paging scroll view for vertical scroll views. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::pageScrollableDimension |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Updates the frames of all visible pages based on their page indices. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::layoutVisiblePages |
||||
|
*/ |
||||
@ -0,0 +1,386 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// Copyright 2012 Manu Cornet (vertical layouts) |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NimbusCore.h" |
||||
|
|
||||
|
/** |
||||
|
* numberOfPages will be this value until reloadData is called. |
||||
|
*/ |
||||
|
extern const NSInteger NIPagingScrollViewUnknownNumberOfPages; |
||||
|
|
||||
|
/** |
||||
|
* The default number of pixels on the side of each page. |
||||
|
* |
||||
|
* Value: 10 |
||||
|
*/ |
||||
|
extern const CGFloat NIPagingScrollViewDefaultPageMargin; |
||||
|
|
||||
|
typedef enum { |
||||
|
NIPagingScrollViewHorizontal = 0, |
||||
|
NIPagingScrollViewVertical, |
||||
|
} NIPagingScrollViewType; |
||||
|
|
||||
|
@protocol NIPagingScrollViewDataSource; |
||||
|
@protocol NIPagingScrollViewDelegate; |
||||
|
@protocol NIPagingScrollViewPage; |
||||
|
@class NIViewRecycler; |
||||
|
|
||||
|
/** |
||||
|
* The NIPagingScrollView class provides a UITableView-like interface for loading pages via a data |
||||
|
* source. |
||||
|
* |
||||
|
* @ingroup NimbusPagingScrollView |
||||
|
*/ |
||||
|
@interface NIPagingScrollView : UIView <UIScrollViewDelegate> |
||||
|
|
||||
|
#pragma mark Data Source |
||||
|
|
||||
|
- (void)reloadData; |
||||
|
@property (nonatomic, weak) id<NIPagingScrollViewDataSource> dataSource; |
||||
|
@property (nonatomic, weak) id<NIPagingScrollViewDelegate> delegate; |
||||
|
|
||||
|
// It is highly recommended that you use this method to manage view recycling. |
||||
|
- (UIView<NIPagingScrollViewPage> *)dequeueReusablePageWithIdentifier:(NSString *)identifier; |
||||
|
|
||||
|
#pragma mark State |
||||
|
|
||||
|
- (UIView<NIPagingScrollViewPage> *)centerPageView; |
||||
|
@property (nonatomic) NSInteger centerPageIndex; // Use moveToPageAtIndex:animated: to animate to a given page. |
||||
|
|
||||
|
@property (nonatomic, readonly) NSInteger numberOfPages; |
||||
|
|
||||
|
#pragma mark Configuring Presentation |
||||
|
|
||||
|
// Controls the border between pages. |
||||
|
@property (nonatomic) CGFloat pageMargin; |
||||
|
// Used to make the view smaller than the frame of the paging scroll view, thus showing |
||||
|
// neighboring pages, either horizontally or vertically depending on the configuration |
||||
|
// of the view. |
||||
|
@property (nonatomic) CGFloat pageInset; |
||||
|
@property (nonatomic) NIPagingScrollViewType type; // Default: NIPagingScrollViewHorizontal |
||||
|
|
||||
|
#pragma mark Visible Pages |
||||
|
|
||||
|
- (BOOL)hasNext; |
||||
|
- (BOOL)hasPrevious; |
||||
|
- (void)moveToNextAnimated:(BOOL)animated; |
||||
|
- (void)moveToPreviousAnimated:(BOOL)animated; |
||||
|
- (BOOL)moveToPageAtIndex:(NSInteger)pageIndex animated:(BOOL)animated updateVisiblePagesWhileScrolling:(BOOL)updateVisiblePagesWhileScrolling; |
||||
|
|
||||
|
// Short form for moveToPageAtIndex:pageIndex animated:animated updateVisiblePagesWhileScrolling:NO |
||||
|
- (BOOL)moveToPageAtIndex:(NSInteger)pageIndex animated:(BOOL)animated; |
||||
|
|
||||
|
#pragma mark Rotating the Scroll View |
||||
|
|
||||
|
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; |
||||
|
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* The delegate for NIPagingScrollView. |
||||
|
* |
||||
|
* @ingroup NimbusPagingScrollView |
||||
|
*/ |
||||
|
@protocol NIPagingScrollViewDelegate <UIScrollViewDelegate> |
||||
|
@optional |
||||
|
|
||||
|
#pragma mark Scrolling and Zooming /** @name [NIPhotoAlbumScrollViewDelegate] Scrolling and Zooming */ |
||||
|
|
||||
|
/** |
||||
|
* The user is scrolling between two photos. |
||||
|
*/ |
||||
|
- (void)pagingScrollViewDidScroll:(NIPagingScrollView *)pagingScrollView; |
||||
|
|
||||
|
#pragma mark Changing Pages /** @name [NIPagingScrollViewDelegate] Changing Pages */ |
||||
|
|
||||
|
/** |
||||
|
* The current page will change. |
||||
|
* |
||||
|
* pagingScrollView.centerPageIndex will reflect the old page index, not the new |
||||
|
* page index. |
||||
|
*/ |
||||
|
- (void)pagingScrollViewWillChangePages:(NIPagingScrollView *)pagingScrollView; |
||||
|
|
||||
|
/** |
||||
|
* The current page has changed. |
||||
|
* |
||||
|
* pagingScrollView.centerPageIndex will reflect the changed page index. |
||||
|
*/ |
||||
|
- (void)pagingScrollViewDidChangePages:(NIPagingScrollView *)pagingScrollView; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* The data source for NIPagingScrollView. |
||||
|
* |
||||
|
* @ingroup NimbusPagingScrollView |
||||
|
*/ |
||||
|
@protocol NIPagingScrollViewDataSource <NSObject> |
||||
|
@required |
||||
|
|
||||
|
#pragma mark Fetching Required Album Information /** @name [NIPagingScrollViewDataSource] Fetching Required Album Information */ |
||||
|
|
||||
|
/** |
||||
|
* Fetches the total number of pages in the scroll view. |
||||
|
* |
||||
|
* The value returned in this method will be cached by the scroll view until reloadData |
||||
|
* is called again. |
||||
|
*/ |
||||
|
- (NSInteger)numberOfPagesInPagingScrollView:(NIPagingScrollView *)pagingScrollView; |
||||
|
|
||||
|
/** |
||||
|
* Fetches a page that will be displayed at the given page index. |
||||
|
* |
||||
|
* You should always try to reuse pages by calling dequeueReusablePageWithIdentifier: on the |
||||
|
* paging scroll view before allocating a new page. |
||||
|
*/ |
||||
|
- (UIView<NIPagingScrollViewPage> *)pagingScrollView:(NIPagingScrollView *)pagingScrollView pageViewForIndex:(NSInteger)pageIndex; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* The protocol that a paging scroll view page should implement. |
||||
|
* |
||||
|
* By providing a protocol instead of a UIView base class we allow more flexibility when |
||||
|
* building pages. |
||||
|
* |
||||
|
* @ingroup NimbusPagingScrollView |
||||
|
*/ |
||||
|
@protocol NIPagingScrollViewPage <NIRecyclableView> |
||||
|
@required |
||||
|
|
||||
|
/** |
||||
|
* The index of this page view. |
||||
|
*/ |
||||
|
@property (nonatomic, assign) NSInteger pageIndex; |
||||
|
|
||||
|
@optional |
||||
|
|
||||
|
/** |
||||
|
* Called after the page has gone off-screen. |
||||
|
* |
||||
|
* This method should be used to reset any state information after a page goes off-screen. |
||||
|
* For example, in the Nimbus photo viewer we reset the zoom scale so that if the photo |
||||
|
* was zoomed in it will fit on the screen again when the user flips back and forth between |
||||
|
* two pages. |
||||
|
*/ |
||||
|
- (void)pageDidDisappear; |
||||
|
|
||||
|
/** |
||||
|
* Called when the frame of the page is going to change. |
||||
|
* |
||||
|
* Use this method to maintain any state that may be affected by the frame changing. |
||||
|
* The Nimbus photo viewer uses this method to save and restore the zoom and center |
||||
|
* point. This makes the photo always appear to rotate around the center point of the screen |
||||
|
* rather than the center of the photo. |
||||
|
*/ |
||||
|
- (void)setFrameAndMaintainState:(CGRect)frame; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** @name Data Source */ |
||||
|
|
||||
|
/** |
||||
|
* The data source for this page album view. |
||||
|
* |
||||
|
* This is the only means by which this paging view acquires any information about the |
||||
|
* album to be displayed. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::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 cheap because we only fetch new information about the currently displayed |
||||
|
* pages. If the number of pages shrinks then the current center page index will be decreased |
||||
|
* accordingly. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::reloadData |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Dequeues a reusable page from the set of recycled pages. |
||||
|
* |
||||
|
* If no pages have been recycled for the given identifier then this will return nil. |
||||
|
* In this case it is your responsibility to create a new page. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::dequeueReusablePageWithIdentifier: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The delegate for this paging view. |
||||
|
* |
||||
|
* Any user interactions or state changes are sent to the delegate through this property. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::delegate |
||||
|
*/ |
||||
|
|
||||
|
/** @name Configuring Presentation */ |
||||
|
|
||||
|
/** |
||||
|
* The number of pixels on either side of each page. |
||||
|
* |
||||
|
* The space between each page will be 2x this value. |
||||
|
* |
||||
|
* By default this is NIPagingScrollViewDefaultPageMargin. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::pageMargin |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The type of paging scroll view to display. |
||||
|
* |
||||
|
* This property allows you to configure whether you want a horizontal or vertical paging scroll |
||||
|
* view. You should set this property before you present the scroll view and not modify it after. |
||||
|
* |
||||
|
* By default this is NIPagingScrollViewHorizontal. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::type |
||||
|
*/ |
||||
|
|
||||
|
/** @name State */ |
||||
|
|
||||
|
/** |
||||
|
* The current center page view. |
||||
|
* |
||||
|
* If no pages exist then this will return nil. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::centerPageView |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The current center page index. |
||||
|
* |
||||
|
* This is a zero-based value. If you intend to use this in a label such as "page ## of n" be |
||||
|
* sure to add one to this value. |
||||
|
* |
||||
|
* Setting this value directly will center the new page without any animation. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::centerPageIndex |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Change the center page index with optional animation. |
||||
|
* |
||||
|
* This method is deprecated in favor of |
||||
|
* @link NIPagingScrollView::moveToPageAtIndex:animated: moveToPageAtIndex:animated:@endlink |
||||
|
* |
||||
|
* @fn NIPagingScrollView::setCenterPageIndex:animated: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The total number of pages in this paging view, as gathered from the data source. |
||||
|
* |
||||
|
* This value is cached after reloadData has been called. |
||||
|
* |
||||
|
* Until reloadData is called the first time, numberOfPages will be |
||||
|
* NIPagingScrollViewUnknownNumberOfPages. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::numberOfPages |
||||
|
*/ |
||||
|
|
||||
|
/** @name Changing the Visible Page */ |
||||
|
|
||||
|
/** |
||||
|
* Returns YES if there is a next page. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::hasNext |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Returns YES if there is a previous page. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::hasPrevious |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Move to the next page if there is one. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::moveToNextAnimated: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Move to the previous page if there is one. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::moveToPreviousAnimated: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Move to the given page index with optional animation. |
||||
|
* |
||||
|
* @returns NO if a page change animation is already in effect and we couldn't change the page |
||||
|
* again. |
||||
|
* @fn NIPagingScrollView::moveToPageAtIndex:animated: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Move to the given page index with optional animation and option to enable page updates while |
||||
|
* scrolling. |
||||
|
* |
||||
|
* NOTE: Passing YES for moveToPageAtIndex:animated:updateVisiblePagesWhileScrolling will cause |
||||
|
* every page from the present page to the destination page to be loaded. This has the potential to |
||||
|
* cause choppy animations. |
||||
|
* |
||||
|
* @param updateVisiblePagesWhileScrolling If YES, will query the data source for any pages |
||||
|
* that become visible while the animation occurs. |
||||
|
* @returns NO if a page change animation is already in effect and we couldn't change the page |
||||
|
* again. |
||||
|
* @fn NIPagingScrollView::moveToPageAtIndex:animated:updateVisiblePagesWhileScrolling: |
||||
|
*/ |
||||
|
|
||||
|
/** @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. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::willRotateToInterfaceOrientation:duration: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Updates the frame of the scroll view while maintaining the current visible page's state. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::willAnimateRotationToInterfaceOrientation:duration: |
||||
|
*/ |
||||
|
|
||||
|
/** @name Subclassing */ |
||||
|
|
||||
|
/** |
||||
|
* The internal scroll view. |
||||
|
* |
||||
|
* Meant to be used by subclasses only. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::pagingScrollView |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The set of currently visible pages. |
||||
|
* |
||||
|
* Meant to be used by subclasses only. |
||||
|
* |
||||
|
* @fn NIPagingScrollView::visiblePages |
||||
|
*/ |
||||
@ -0,0 +1,760 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// Copyright 2012 Manu Cornet (vertical layouts) |
||||
|
// |
||||
|
// 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 "NIPagingScrollView.h" |
||||
|
#import "NIPagingScrollView+Subclassing.h" |
||||
|
|
||||
|
#import "NimbusCore.h" |
||||
|
|
||||
|
#import <objc/runtime.h> |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
const NSInteger NIPagingScrollViewUnknownNumberOfPages = -1; |
||||
|
const CGFloat NIPagingScrollViewDefaultPageMargin = 10; |
||||
|
const CGFloat NIPagingScrollViewDefaultPageInset = 0; |
||||
|
|
||||
|
@implementation NIPagingScrollView { |
||||
|
NIViewRecycler* _viewRecycler; |
||||
|
UIScrollView* _scrollView; |
||||
|
|
||||
|
NSMutableSet* _visiblePages; |
||||
|
|
||||
|
// Animating to Pages |
||||
|
NSInteger _animatingToPageIndex; |
||||
|
BOOL _isKillingAnimation; |
||||
|
NSInteger _queuedAnimationPageIndex; |
||||
|
BOOL _shouldUpdateVisiblePagesWhileScrolling; |
||||
|
|
||||
|
// Rotation State |
||||
|
NSInteger _firstVisiblePageIndexBeforeRotation; |
||||
|
CGFloat _percentScrolledIntoFirstVisiblePage; |
||||
|
} |
||||
|
|
||||
|
- (void)commonInit { |
||||
|
// Default state. |
||||
|
self.pageMargin = NIPagingScrollViewDefaultPageMargin; |
||||
|
self.pageInset = NIPagingScrollViewDefaultPageInset; |
||||
|
self.type = NIPagingScrollViewHorizontal; |
||||
|
|
||||
|
// Internal state |
||||
|
_animatingToPageIndex = -1; |
||||
|
_firstVisiblePageIndexBeforeRotation = -1; |
||||
|
_percentScrolledIntoFirstVisiblePage = -1; |
||||
|
_centerPageIndex = -1; |
||||
|
_numberOfPages = NIPagingScrollViewUnknownNumberOfPages; |
||||
|
|
||||
|
_viewRecycler = [[NIViewRecycler alloc] init]; |
||||
|
|
||||
|
// The internal scroll view that powers this paging scroll view. |
||||
|
_scrollView = [[UIScrollView alloc] initWithFrame:self.bounds]; |
||||
|
_scrollView.pagingEnabled = YES; |
||||
|
_scrollView.scrollsToTop = NO; |
||||
|
|
||||
|
// Allows the scroll view to show adjacent pages... |
||||
|
_scrollView.clipsToBounds = NO; |
||||
|
// ...while still clipping contents to the bounds of the paging scroll view. |
||||
|
self.clipsToBounds = YES; |
||||
|
|
||||
|
_scrollView.autoresizingMask = UIViewAutoresizingFlexibleDimensions; |
||||
|
|
||||
|
_scrollView.delegate = self; |
||||
|
|
||||
|
_scrollView.showsVerticalScrollIndicator = NO; |
||||
|
_scrollView.showsHorizontalScrollIndicator = NO; |
||||
|
|
||||
|
[self addSubview:_scrollView]; |
||||
|
} |
||||
|
|
||||
|
- (id)initWithFrame:(CGRect)frame { |
||||
|
if ((self = [super initWithFrame:frame])) { |
||||
|
[self commonInit]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (id)initWithCoder:(NSCoder *)aDecoder { |
||||
|
if ((self = [super initWithCoder:aDecoder])) { |
||||
|
[self commonInit]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Page Layout |
||||
|
|
||||
|
- (void)layoutSubviews { |
||||
|
[super layoutSubviews]; |
||||
|
_scrollView.frame = [self frameForPagingScrollView]; |
||||
|
|
||||
|
// Retain the current position. |
||||
|
CGPoint offset = [self frameForPageAtIndex:_centerPageIndex].origin; |
||||
|
_scrollView.contentOffset = [self contentOffsetFromPageOffset:offset]; |
||||
|
|
||||
|
_scrollView.contentSize = [self contentSizeForPagingScrollView]; |
||||
|
[self layoutVisiblePages]; |
||||
|
} |
||||
|
|
||||
|
// 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; |
||||
|
|
||||
|
if (NIPagingScrollViewHorizontal == self.type) { |
||||
|
// 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 = CGRectInset(frame, self.pageInset - self.pageMargin, 0); |
||||
|
|
||||
|
} else if (NIPagingScrollViewVertical == self.type) { |
||||
|
frame = CGRectInset(frame, 0, self.pageInset - self.pageMargin); |
||||
|
} |
||||
|
|
||||
|
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 = _scrollView.bounds; |
||||
|
CGRect pageFrame = bounds; |
||||
|
|
||||
|
if (NIPagingScrollViewHorizontal == self.type) { |
||||
|
pageFrame.origin.x = (bounds.size.width * pageIndex); |
||||
|
// We need to counter the extra spacing added to the paging scroll view in |
||||
|
// frameForPagingScrollView. |
||||
|
pageFrame = CGRectInset(pageFrame, self.pageMargin, 0); |
||||
|
|
||||
|
} else if (NIPagingScrollViewVertical == self.type) { |
||||
|
pageFrame.origin.y = (bounds.size.height * pageIndex); |
||||
|
pageFrame = CGRectInset(pageFrame, 0, self.pageMargin); |
||||
|
} |
||||
|
|
||||
|
return pageFrame; |
||||
|
} |
||||
|
|
||||
|
- (CGSize)contentSizeForPagingScrollView { |
||||
|
// We use the paging scroll view's bounds to calculate the contentSize, for the same reason |
||||
|
// outlined above. |
||||
|
CGRect bounds = _scrollView.bounds; |
||||
|
if (NIPagingScrollViewHorizontal == self.type) { |
||||
|
return CGSizeMake(bounds.size.width * self.numberOfPages, bounds.size.height); |
||||
|
|
||||
|
} else if (NIPagingScrollViewVertical == self.type) { |
||||
|
return CGSizeMake(bounds.size.width, bounds.size.height * self.numberOfPages); |
||||
|
} |
||||
|
|
||||
|
return CGSizeZero; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)contentOffsetFromPageOffset:(CGPoint)offset { |
||||
|
if (NIPagingScrollViewHorizontal == self.type) { |
||||
|
offset.x -= self.pageMargin; |
||||
|
|
||||
|
} else if (NIPagingScrollViewVertical == self.type) { |
||||
|
offset.y -= self.pageMargin; |
||||
|
} |
||||
|
|
||||
|
return offset; |
||||
|
} |
||||
|
|
||||
|
- (CGFloat)pageScrollableDimension { |
||||
|
if (NIPagingScrollViewHorizontal == self.type) { |
||||
|
return _scrollView.bounds.size.width; |
||||
|
|
||||
|
} else if (NIPagingScrollViewVertical == self.type) { |
||||
|
return _scrollView.bounds.size.height; |
||||
|
} |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)contentOffsetFromOffset:(CGFloat)offset { |
||||
|
if (NIPagingScrollViewHorizontal == self.type) { |
||||
|
return CGPointMake(offset, 0); |
||||
|
|
||||
|
} else if (NIPagingScrollViewVertical == self.type) { |
||||
|
return CGPointMake(0, offset); |
||||
|
} |
||||
|
|
||||
|
return CGPointMake(0, 0); |
||||
|
} |
||||
|
|
||||
|
- (CGFloat)scrolledPageOffset { |
||||
|
if (NIPagingScrollViewHorizontal == self.type) { |
||||
|
return _scrollView.contentOffset.x; |
||||
|
|
||||
|
} else if (NIPagingScrollViewVertical == self.type) { |
||||
|
return _scrollView.contentOffset.y; |
||||
|
} |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Visible Page Management |
||||
|
|
||||
|
- (BOOL)isDisplayingPageForIndex:(NSInteger)pageIndex { |
||||
|
BOOL foundPage = NO; |
||||
|
|
||||
|
// There will never be more than a handful (3 without insets) of visible pages in this array, so this lookup is |
||||
|
// effectively O(C) constant time. |
||||
|
for (UIView <NIPagingScrollViewPage>* page in _visiblePages) { |
||||
|
if (page.pageIndex == pageIndex) { |
||||
|
foundPage = YES; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return foundPage; |
||||
|
} |
||||
|
|
||||
|
- (NSInteger)currentVisiblePageIndex { |
||||
|
CGPoint contentOffset = _scrollView.contentOffset; |
||||
|
CGSize boundsSize = _scrollView.bounds.size; |
||||
|
|
||||
|
if (NIPagingScrollViewHorizontal == self.type) { |
||||
|
// Whatever image is currently displayed in the center of the screen is the currently |
||||
|
// visible image. |
||||
|
return NIBoundi((NSInteger)(NICGFloatFloor((contentOffset.x + boundsSize.width / 2) / boundsSize.width) |
||||
|
+ 0.5f), |
||||
|
0, self.numberOfPages - 1); |
||||
|
|
||||
|
} else if (NIPagingScrollViewVertical == self.type) { |
||||
|
return NIBoundi((NSInteger)(NICGFloatFloor((contentOffset.y + boundsSize.height / 2) / boundsSize.height) |
||||
|
+ 0.5f), |
||||
|
0, self.numberOfPages - 1); |
||||
|
} |
||||
|
|
||||
|
return 0; |
||||
|
} |
||||
|
|
||||
|
- (NSRange)rangeOfVisiblePages { |
||||
|
if (0 >= self.numberOfPages) { |
||||
|
return NSMakeRange(0, 0); |
||||
|
} |
||||
|
|
||||
|
NSInteger visibleRange = 1; |
||||
|
if (_pageInset != 0) { |
||||
|
CGSize boundsSize = _scrollView.bounds.size; |
||||
|
CGSize frameSize = self.frame.size; |
||||
|
visibleRange = (NSInteger)ceil(frameSize.width / (boundsSize.width + _pageMargin)); |
||||
|
} |
||||
|
|
||||
|
NSInteger currentVisiblePageIndex = [self currentVisiblePageIndex]; |
||||
|
|
||||
|
NSInteger firstVisiblePageIndex = NIBoundi(currentVisiblePageIndex - visibleRange, 0, self.numberOfPages - 1); |
||||
|
NSInteger lastVisiblePageIndex = NIBoundi(currentVisiblePageIndex + visibleRange, 0, self.numberOfPages - 1); |
||||
|
|
||||
|
return NSMakeRange(firstVisiblePageIndex, lastVisiblePageIndex - firstVisiblePageIndex + 1); |
||||
|
} |
||||
|
|
||||
|
- (void)willDisplayPage:(UIView<NIPagingScrollViewPage> *)pageView atIndex:(NSInteger)pageIndex { |
||||
|
pageView.pageIndex = pageIndex; |
||||
|
pageView.frame = [self frameForPageAtIndex:pageIndex]; |
||||
|
|
||||
|
[self willDisplayPage:pageView]; |
||||
|
} |
||||
|
|
||||
|
- (void)resetPage:(id<NIPagingScrollViewPage>)page { |
||||
|
if ([page respondsToSelector:@selector(pageDidDisappear)]) { |
||||
|
[page pageDidDisappear]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)resetSurroundingPages { |
||||
|
for (id<NIPagingScrollViewPage> page in _visiblePages) { |
||||
|
if (page.pageIndex != self.centerPageIndex) { |
||||
|
[self resetPage:page]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (UIView<NIPagingScrollViewPage> *)dequeueReusablePageWithIdentifier:(NSString *)identifier { |
||||
|
NIDASSERT(nil != identifier); |
||||
|
if (nil == identifier) { |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
return (UIView<NIPagingScrollViewPage> *)[_viewRecycler dequeueReusableViewWithIdentifier:identifier]; |
||||
|
} |
||||
|
|
||||
|
- (UIView<NIPagingScrollViewPage> *)loadPageAtIndex:(NSInteger)pageIndex { |
||||
|
UIView<NIPagingScrollViewPage>* page = [self.dataSource pagingScrollView:self pageViewForIndex:pageIndex]; |
||||
|
|
||||
|
NIDASSERT([page isKindOfClass:[UIView class]]); |
||||
|
NIDASSERT([page conformsToProtocol:@protocol(NIPagingScrollViewPage)]); |
||||
|
|
||||
|
if (nil == page || ![page isKindOfClass:[UIView class]] |
||||
|
|| ![page conformsToProtocol:@protocol(NIPagingScrollViewPage)]) { |
||||
|
// Bail out! This page is malformed. |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
return page; |
||||
|
} |
||||
|
|
||||
|
- (void)displayPageAtIndex:(NSInteger)pageIndex { |
||||
|
UIView<NIPagingScrollViewPage>* page = [self loadPageAtIndex:pageIndex]; |
||||
|
if (nil == page) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// This will only be called once, before the page is shown. |
||||
|
[self willDisplayPage:page atIndex:pageIndex]; |
||||
|
|
||||
|
[_scrollView addSubview:page]; |
||||
|
[_visiblePages addObject:page]; |
||||
|
} |
||||
|
|
||||
|
- (void)recyclePageAtIndex:(NSInteger)pageIndex { |
||||
|
for (UIView<NIPagingScrollViewPage>* page in [_visiblePages copy]) { |
||||
|
if (page.pageIndex == pageIndex) { |
||||
|
[_viewRecycler recycleView:page]; |
||||
|
[page removeFromSuperview]; |
||||
|
|
||||
|
[self didRecyclePage:page]; |
||||
|
|
||||
|
[_visiblePages removeObject:page]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)preloadOffscreenPages { |
||||
|
NSRange rangeOfVisiblePages = [self rangeOfVisiblePages]; |
||||
|
for (NSUInteger pageIndex = rangeOfVisiblePages.location; |
||||
|
pageIndex < NSMaxRange(rangeOfVisiblePages); ++pageIndex) { |
||||
|
if (![self isDisplayingPageForIndex:pageIndex]) { |
||||
|
[self displayPageAtIndex:pageIndex]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)updateVisiblePagesShouldNotifyDelegate:(BOOL)shouldNotifyDelegate { |
||||
|
// Before updating _centerPageIndex, notify delegate |
||||
|
if (shouldNotifyDelegate && (self.numberOfPages > 0) && |
||||
|
([self currentVisiblePageIndex] != self.centerPageIndex) && |
||||
|
[self.delegate respondsToSelector:@selector(pagingScrollViewWillChangePages:)]) { |
||||
|
[self.delegate pagingScrollViewWillChangePages:self]; |
||||
|
} |
||||
|
|
||||
|
NSRange rangeOfVisiblePages = [self rangeOfVisiblePages]; |
||||
|
// Recycle no-longer-visible pages. We copy _visiblePages because we may modify it while we're |
||||
|
// iterating over it. |
||||
|
for (UIView<NIPagingScrollViewPage>* page in [_visiblePages copy]) { |
||||
|
if (!NSLocationInRange(page.pageIndex, rangeOfVisiblePages)) { |
||||
|
[_viewRecycler recycleView:page]; |
||||
|
[page removeFromSuperview]; |
||||
|
|
||||
|
[self didRecyclePage:page]; |
||||
|
|
||||
|
[_visiblePages removeObject:page]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
NSInteger oldCenterPageIndex = self.centerPageIndex; |
||||
|
|
||||
|
if (self.numberOfPages > 0) { |
||||
|
_centerPageIndex = [self currentVisiblePageIndex]; |
||||
|
|
||||
|
[self didChangeCenterPageIndexFrom:oldCenterPageIndex to:_centerPageIndex]; |
||||
|
|
||||
|
if (_pageInset != 0) { |
||||
|
// Load all visible insetted pages immediately. |
||||
|
[self preloadOffscreenPages]; |
||||
|
} else { |
||||
|
// Prioritize displaying the currently visible page. |
||||
|
if (![self isDisplayingPageForIndex:_centerPageIndex]) { |
||||
|
[self displayPageAtIndex:_centerPageIndex]; |
||||
|
} |
||||
|
|
||||
|
// Add missing pages after displaying the current page. |
||||
|
[self performSelector:@selector(preloadOffscreenPages) |
||||
|
withObject:nil |
||||
|
afterDelay:0]; |
||||
|
} |
||||
|
} else { |
||||
|
_centerPageIndex = -1; |
||||
|
} |
||||
|
|
||||
|
if (shouldNotifyDelegate && oldCenterPageIndex != _centerPageIndex |
||||
|
&& [self.delegate respondsToSelector:@selector(pagingScrollViewDidChangePages:)]) { |
||||
|
[self.delegate pagingScrollViewDidChangePages:self]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)layoutVisiblePages { |
||||
|
for (UIView<NIPagingScrollViewPage>* page in _visiblePages) { |
||||
|
CGRect pageFrame = [self frameForPageAtIndex:page.pageIndex]; |
||||
|
if ([page respondsToSelector:@selector(setFrameAndMaintainState:)]) { |
||||
|
[page setFrameAndMaintainState:pageFrame]; |
||||
|
|
||||
|
} else { |
||||
|
[page setFrame:pageFrame]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - UIView |
||||
|
|
||||
|
- (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. |
||||
|
[super setFrame:frame]; |
||||
|
|
||||
|
_scrollView.contentSize = [self contentSizeForPagingScrollView]; |
||||
|
[self layoutVisiblePages]; |
||||
|
} |
||||
|
|
||||
|
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event { |
||||
|
UIView *view = [super hitTest:point withEvent:event]; |
||||
|
// We must forward hits for the scrollView or else the smaller frame when |
||||
|
// it is inset will prevent touches outside the scrollView bounds. |
||||
|
if (view == self) { |
||||
|
return _scrollView; |
||||
|
} |
||||
|
return view; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - UIScrollViewDelegate |
||||
|
|
||||
|
- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { |
||||
|
[self updateVisiblePagesShouldNotifyDelegate:YES]; |
||||
|
_isKillingAnimation = NO; |
||||
|
|
||||
|
if ([self.delegate respondsToSelector:_cmd]) { |
||||
|
[self.delegate scrollViewWillBeginDragging:scrollView]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)scrollViewDidScroll:(UIScrollView *)scrollView { |
||||
|
if ([scrollView isTracking] && [scrollView isDragging]) { |
||||
|
if ([self.delegate respondsToSelector:@selector(pagingScrollViewDidScroll:)]) { |
||||
|
[self.delegate pagingScrollViewDidScroll:self]; |
||||
|
} |
||||
|
} |
||||
|
if (_shouldUpdateVisiblePagesWhileScrolling |
||||
|
&& ![scrollView isTracking] && ![scrollView isDragging]) { |
||||
|
[self updateVisiblePagesShouldNotifyDelegate:YES]; |
||||
|
} |
||||
|
|
||||
|
if ([self.delegate respondsToSelector:_cmd]) { |
||||
|
[self.delegate scrollViewDidScroll:scrollView]; |
||||
|
} |
||||
|
|
||||
|
if (_isKillingAnimation) { |
||||
|
// The content size is calculated based on the number of pages and the scroll view frame. |
||||
|
CGPoint offset = [self frameForPageAtIndex:_centerPageIndex].origin; |
||||
|
offset = [self contentOffsetFromPageOffset:offset]; |
||||
|
_scrollView.contentOffset = offset; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { |
||||
|
_isKillingAnimation = NO; |
||||
|
|
||||
|
if (!decelerate) { |
||||
|
[self updateVisiblePagesShouldNotifyDelegate:YES]; |
||||
|
[self resetSurroundingPages]; |
||||
|
} |
||||
|
|
||||
|
if ([self.delegate respondsToSelector:_cmd]) { |
||||
|
[self.delegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { |
||||
|
[self updateVisiblePagesShouldNotifyDelegate:YES]; |
||||
|
[self resetSurroundingPages]; |
||||
|
|
||||
|
if ([self.delegate respondsToSelector:_cmd]) { |
||||
|
[self.delegate scrollViewDidEndDecelerating:scrollView]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)scrollViewDidEndScrollingAnimation:(UIScrollView *)scrollView { |
||||
|
if (_animatingToPageIndex >= 0) { |
||||
|
[self didAnimateToPage:_animatingToPageIndex]; |
||||
|
|
||||
|
if ([self.delegate respondsToSelector:_cmd]) { |
||||
|
[self.delegate scrollViewDidEndScrollingAnimation:scrollView]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Forward UIScrollViewDelegate Methods |
||||
|
|
||||
|
|
||||
|
- (BOOL)shouldForwardSelectorToDelegate:(SEL)aSelector { |
||||
|
struct objc_method_description description; |
||||
|
// Only forward the selector if it's part of the UIScrollViewDelegate protocol. |
||||
|
description = protocol_getMethodDescription(@protocol(UIScrollViewDelegate), |
||||
|
aSelector, |
||||
|
NO, |
||||
|
YES); |
||||
|
|
||||
|
BOOL isSelectorInScrollViewDelegate = (description.name != NULL && description.types != NULL); |
||||
|
return (isSelectorInScrollViewDelegate |
||||
|
&& [self.delegate respondsToSelector:aSelector]); |
||||
|
} |
||||
|
|
||||
|
- (BOOL)respondsToSelector:(SEL)aSelector { |
||||
|
if ([super respondsToSelector:aSelector] == YES) { |
||||
|
return YES; |
||||
|
|
||||
|
} else { |
||||
|
return [self shouldForwardSelectorToDelegate:aSelector]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (id)forwardingTargetForSelector:(SEL)aSelector { |
||||
|
if ([self shouldForwardSelectorToDelegate:aSelector]) { |
||||
|
return self.delegate; |
||||
|
|
||||
|
} else { |
||||
|
return nil; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Subclassing |
||||
|
|
||||
|
|
||||
|
- (void)willDisplayPage:(UIView<NIPagingScrollViewPage> *)pageView { |
||||
|
// No-op. |
||||
|
} |
||||
|
|
||||
|
- (void)didRecyclePage:(UIView<NIPagingScrollViewPage> *)pageView { |
||||
|
// No-op |
||||
|
} |
||||
|
|
||||
|
- (void)didReloadNumberOfPages { |
||||
|
// No-op |
||||
|
} |
||||
|
|
||||
|
- (void)didChangeCenterPageIndexFrom:(NSInteger)from to:(NSInteger)to { |
||||
|
// No-op |
||||
|
} |
||||
|
|
||||
|
- (void)setCenterPageIndexIvar:(NSInteger)centerPageIndex { |
||||
|
_centerPageIndex = centerPageIndex; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Public |
||||
|
|
||||
|
|
||||
|
- (void)reloadData { |
||||
|
_animatingToPageIndex = -1; |
||||
|
NIDASSERT(nil != _dataSource); |
||||
|
|
||||
|
// Remove any visible pages from the view before we release the sets. |
||||
|
for (UIView<NIPagingScrollViewPage>* page in _visiblePages) { |
||||
|
[_viewRecycler recycleView:page]; |
||||
|
[(UIView *)page removeFromSuperview]; |
||||
|
|
||||
|
[self didRecyclePage:page]; |
||||
|
} |
||||
|
|
||||
|
_visiblePages = nil; |
||||
|
|
||||
|
// If there is no data source then we can't do anything particularly interesting. |
||||
|
if (nil == _dataSource) { |
||||
|
_scrollView.contentSize = self.bounds.size; |
||||
|
_scrollView.contentOffset = CGPointZero; |
||||
|
|
||||
|
// May as well just get rid of all the views then. |
||||
|
[_viewRecycler removeAllViews]; |
||||
|
|
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
_visiblePages = [[NSMutableSet alloc] init]; |
||||
|
|
||||
|
// Cache the number of pages. |
||||
|
_numberOfPages = [_dataSource numberOfPagesInPagingScrollView:self]; |
||||
|
_scrollView.frame = [self frameForPagingScrollView]; |
||||
|
_scrollView.contentSize = [self contentSizeForPagingScrollView]; |
||||
|
|
||||
|
[self didReloadNumberOfPages]; |
||||
|
|
||||
|
NSInteger oldCenterPageIndex = _centerPageIndex; |
||||
|
if (oldCenterPageIndex >= 0) { |
||||
|
_centerPageIndex = NIBoundi(_centerPageIndex, 0, self.numberOfPages - 1); |
||||
|
|
||||
|
if (![_scrollView isTracking] && ![_scrollView isDragging]) { |
||||
|
// The content size is calculated based on the number of pages and the scroll view frame. |
||||
|
CGPoint offset = [self frameForPageAtIndex:_centerPageIndex].origin; |
||||
|
offset = [self contentOffsetFromPageOffset:offset]; |
||||
|
_scrollView.contentOffset = offset; |
||||
|
|
||||
|
_isKillingAnimation = YES; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Begin requesting the page information from the data source. |
||||
|
[self updateVisiblePagesShouldNotifyDelegate:NO]; |
||||
|
} |
||||
|
|
||||
|
- (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 = [self scrolledPageOffset]; |
||||
|
CGFloat pageScrollableDimension = [self pageScrollableDimension]; |
||||
|
|
||||
|
if (offset >= 0) { |
||||
|
_firstVisiblePageIndexBeforeRotation = (NSInteger)NICGFloatFloor(offset / pageScrollableDimension); |
||||
|
_percentScrolledIntoFirstVisiblePage = ((offset |
||||
|
- (_firstVisiblePageIndexBeforeRotation * pageScrollableDimension)) |
||||
|
/ pageScrollableDimension); |
||||
|
|
||||
|
} else { |
||||
|
_firstVisiblePageIndexBeforeRotation = 0; |
||||
|
_percentScrolledIntoFirstVisiblePage = offset / pageScrollableDimension; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)willAnimateRotationToInterfaceOrientation: (UIInterfaceOrientation)toInterfaceOrientation |
||||
|
duration: (NSTimeInterval)duration { |
||||
|
// Recalculate contentSize based on current orientation. |
||||
|
_scrollView.contentSize = [self contentSizeForPagingScrollView]; |
||||
|
|
||||
|
[self layoutVisiblePages]; |
||||
|
|
||||
|
// Adjust contentOffset to preserve page location based on values collected prior to location. |
||||
|
CGFloat pageScrollableDimension = [self pageScrollableDimension]; |
||||
|
CGFloat newOffset = ((_firstVisiblePageIndexBeforeRotation * pageScrollableDimension) |
||||
|
+ (_percentScrolledIntoFirstVisiblePage * pageScrollableDimension)); |
||||
|
_scrollView.contentOffset = [self contentOffsetFromOffset:newOffset]; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)hasNext { |
||||
|
return (self.centerPageIndex < self.numberOfPages - 1); |
||||
|
} |
||||
|
|
||||
|
- (BOOL)hasPrevious { |
||||
|
return self.centerPageIndex > 0; |
||||
|
} |
||||
|
|
||||
|
- (void)didAnimateToPage:(NSInteger)pageIndex { |
||||
|
_shouldUpdateVisiblePagesWhileScrolling = NO; |
||||
|
_animatingToPageIndex = -1; |
||||
|
if (_queuedAnimationPageIndex >= 0 && _queuedAnimationPageIndex != pageIndex) { |
||||
|
[self moveToPageAtIndex:_queuedAnimationPageIndex animated:YES]; |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
// 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:pageIndex].origin; |
||||
|
offset = [self contentOffsetFromPageOffset:offset]; |
||||
|
|
||||
|
_scrollView.contentOffset = offset; |
||||
|
|
||||
|
[self updateVisiblePagesShouldNotifyDelegate:YES]; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)moveToPageAtIndex:(NSInteger)pageIndex animated:(BOOL)animated { |
||||
|
return [self moveToPageAtIndex:pageIndex animated:animated updateVisiblePagesWhileScrolling:NO]; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)moveToPageAtIndex:(NSInteger)pageIndex animated:(BOOL)animated updateVisiblePagesWhileScrolling:(BOOL)updateVisiblePagesWhileScrolling { |
||||
|
if (_animatingToPageIndex >= 0) { |
||||
|
// Don't allow re-entry for sliding animations. |
||||
|
_queuedAnimationPageIndex = pageIndex; |
||||
|
return NO; |
||||
|
} |
||||
|
_shouldUpdateVisiblePagesWhileScrolling = updateVisiblePagesWhileScrolling; |
||||
|
_isKillingAnimation = NO; |
||||
|
_queuedAnimationPageIndex = -1; |
||||
|
|
||||
|
CGPoint offset = [self frameForPageAtIndex:pageIndex].origin; |
||||
|
offset = [self contentOffsetFromPageOffset:offset]; |
||||
|
|
||||
|
// The paging scroll view won't actually animate if the offsets are identical. |
||||
|
animated = animated && !CGPointEqualToPoint(offset, _scrollView.contentOffset); |
||||
|
|
||||
|
if (animated) { |
||||
|
_animatingToPageIndex = pageIndex; |
||||
|
} |
||||
|
[_scrollView setContentOffset:offset animated:animated]; |
||||
|
if (!animated) { |
||||
|
[self resetSurroundingPages]; |
||||
|
[self didAnimateToPage:pageIndex]; |
||||
|
} |
||||
|
return YES; |
||||
|
} |
||||
|
|
||||
|
- (void)moveToNextAnimated:(BOOL)animated { |
||||
|
if ([self hasNext]) { |
||||
|
NSInteger pageIndex = self.centerPageIndex + 1; |
||||
|
|
||||
|
[self moveToPageAtIndex:pageIndex animated:animated]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)moveToPreviousAnimated:(BOOL)animated { |
||||
|
if ([self hasPrevious]) { |
||||
|
NSInteger pageIndex = self.centerPageIndex - 1; |
||||
|
|
||||
|
[self moveToPageAtIndex:pageIndex animated:animated]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (UIView<NIPagingScrollViewPage> *)centerPageView { |
||||
|
for (UIView<NIPagingScrollViewPage>* page in _visiblePages) { |
||||
|
if (page.pageIndex == self.centerPageIndex) { |
||||
|
return page; |
||||
|
} |
||||
|
} |
||||
|
return nil; |
||||
|
} |
||||
|
|
||||
|
- (void)setCenterPageIndex:(NSInteger)centerPageIndex { |
||||
|
[self moveToPageAtIndex:centerPageIndex animated:NO]; |
||||
|
} |
||||
|
|
||||
|
- (void)setPageMargin:(CGFloat)pageMargin { |
||||
|
_pageMargin = pageMargin; |
||||
|
[self setNeedsLayout]; |
||||
|
} |
||||
|
|
||||
|
- (void)setPageInset:(CGFloat)pageInset { |
||||
|
_pageInset = pageInset; |
||||
|
[self setNeedsLayout]; |
||||
|
} |
||||
|
|
||||
|
- (void)setType:(NIPagingScrollViewType)type { |
||||
|
if (_type != type) { |
||||
|
_type = type; |
||||
|
_scrollView.scrollsToTop = (type == NIPagingScrollViewVertical); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (UIScrollView *)scrollView { |
||||
|
return _scrollView; |
||||
|
} |
||||
|
|
||||
|
- (NSMutableSet *)visiblePages { |
||||
|
return _visiblePages; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,36 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIPagingScrollView.h" |
||||
|
|
||||
|
/** |
||||
|
* A skeleton implementation of a page view. |
||||
|
* |
||||
|
* This view simply implements the required properties of NIPagingScrollViewPage. |
||||
|
* |
||||
|
* @ingroup NimbusPagingScrollView |
||||
|
*/ |
||||
|
@interface NIPagingScrollViewPage : NIRecyclableView <NIPagingScrollViewPage> |
||||
|
@property (nonatomic) NSInteger pageIndex; |
||||
|
@end |
||||
|
|
||||
|
/** |
||||
|
* The page index. |
||||
|
* |
||||
|
* @fn NIPagingScrollViewPage::pageIndex |
||||
|
*/ |
||||
@ -0,0 +1,26 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIPagingScrollViewPage.h" |
||||
|
|
||||
|
#import "NimbusCore.h" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
@implementation NIPagingScrollViewPage |
||||
|
@end |
||||
@ -0,0 +1,53 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
/** |
||||
|
* @defgroup NimbusPagingScrollView Nimbus Paging Scroll View |
||||
|
* @{ |
||||
|
* |
||||
|
* <div id="github" feature="pagingscrollview"></div> |
||||
|
* |
||||
|
* A paging scroll view is a UIScrollView that scrolls horizontally and shows a series of |
||||
|
* pages that are efficiently recycled. |
||||
|
* |
||||
|
* The Nimbus paging scroll view is powered by a datasource that allows you to separate the |
||||
|
* data from the view. This makes it easy to efficiently recycle pages and only create as many |
||||
|
* pages of content as may be visible at any given point in time. Nimbus' implementation also |
||||
|
* provides helpful features such as keeping the center page centered when the device changes |
||||
|
* orientation. |
||||
|
* |
||||
|
* Paging scroll views are commonly used in many iOS applications. For example, Nimbus' Photos |
||||
|
* feature uses a paging scroll view to power its NIPhotoAlbumScrollView. |
||||
|
* |
||||
|
* <h2>Building a Component with NIPagingScrollView</h2> |
||||
|
* |
||||
|
* NIPagingScrollView works much like a UITableView in that you must implement a data source |
||||
|
* and optionally a delegate. The data source fetches information about the contents of the |
||||
|
* paging scroll view, such as the total number of pages and the view for a given page when it |
||||
|
* is required. The views that you return for pages must conform to the NIPagingScrollViewPage |
||||
|
* protocol. This is similar to UITableViewCell, but rather than subclass a view you can simply |
||||
|
* implement a protocol. If you would prefer not to implement the protocol, you can subclass |
||||
|
* NIPageView which implements the required methods of NIPagingScrollViewPage. |
||||
|
*/ |
||||
|
|
||||
|
/**@}*/ |
||||
|
|
||||
|
#import "NIPagingScrollView.h" |
||||
|
#import "NIPagingScrollViewPage.h" |
||||
|
|
||||
|
#import "NimbusCore.h" |
||||
@ -0,0 +1,170 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIPhotoScrollViewDelegate.h" |
||||
|
#import "NIPhotoAlbumScrollViewDataSource.h" |
||||
|
#import "NIPhotoAlbumScrollViewDelegate.h" |
||||
|
|
||||
|
#import "NimbusPagingScrollView.h" |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
/** |
||||
|
* A paged scroll view that shows a collection of photos. |
||||
|
* |
||||
|
* @ingroup NimbusPhotos |
||||
|
* |
||||
|
* 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 : NIPagingScrollView <NIPhotoScrollViewDelegate> |
||||
|
|
||||
|
#pragma mark Data Source |
||||
|
|
||||
|
// For use in your pagingScrollView:pageForIndex: data source implementation. |
||||
|
- (UIView<NIPagingScrollViewPage> *)pagingScrollView:(NIPagingScrollView *)pagingScrollView pageViewForIndex:(NSInteger)pageIndex; |
||||
|
|
||||
|
@property (nonatomic, weak) id<NIPhotoAlbumScrollViewDataSource> dataSource; |
||||
|
@property (nonatomic, weak) id<NIPhotoAlbumScrollViewDelegate> delegate; |
||||
|
|
||||
|
#pragma mark Configuring Functionality |
||||
|
|
||||
|
@property (nonatomic, assign, getter=isZoomingEnabled) BOOL zoomingIsEnabled; |
||||
|
@property (nonatomic, assign, getter=isZoomingAboveOriginalSizeEnabled) BOOL zoomingAboveOriginalSizeIsEnabled; |
||||
|
@property (nonatomic, strong) UIColor* photoViewBackgroundColor; |
||||
|
|
||||
|
#pragma mark Configuring Presentation |
||||
|
|
||||
|
@property (nonatomic, strong) UIImage* loadingImage; |
||||
|
|
||||
|
#pragma mark Notifying the View of Loaded Photos |
||||
|
|
||||
|
- (void)didLoadPhoto: (UIImage *)image |
||||
|
atIndex: (NSInteger)photoIndex |
||||
|
photoSize: (NIPhotoScrollViewPhotoSize)photoSize; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
|
||||
|
/** @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. |
||||
|
* |
||||
|
* @fn NIPhotoAlbumScrollView::dataSource |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Use this method in your implementation of NIPhotoAlbumScrollViewDataSource's |
||||
|
* pagingScrollView:pageForIndex:. |
||||
|
* |
||||
|
* Example: |
||||
|
* |
||||
|
@code |
||||
|
- (id<NIPagingScrollViewPage>)pagingScrollView:(NIPagingScrollView *)pagingScrollView pageForIndex:(NSInteger)pageIndex { |
||||
|
return [self.photoAlbumView pagingScrollView:pagingScrollView pageForIndex:pageIndex]; |
||||
|
} |
||||
|
@endcode |
||||
|
* |
||||
|
* Automatically uses the paging scroll view's page recycling methods and creates |
||||
|
* NIPhotoScrollViews as needed. |
||||
|
* |
||||
|
* @fn NIPhotoAlbumScrollView::pagingScrollView:pageForIndex: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The delegate for this photo album view. |
||||
|
* |
||||
|
* Any user interactions or state changes are sent to the delegate through this property. |
||||
|
* |
||||
|
* @fn NIPhotoAlbumScrollView::delegate |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
/** @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. |
||||
|
* |
||||
|
* @fn NIPhotoAlbumScrollView::zoomingIsEnabled |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Whether small photos can be zoomed at least until they fit the screen. |
||||
|
* |
||||
|
* @see NIPhotoScrollView::zoomingAboveOriginalSizeIsEnabled |
||||
|
* |
||||
|
* By default this is YES. |
||||
|
* |
||||
|
* @fn NIPhotoAlbumScrollView::zoomingAboveOriginalSizeIsEnabled |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The background color of each photo's view. |
||||
|
* |
||||
|
* By default this is [UIColor blackColor]. |
||||
|
* |
||||
|
* @fn NIPhotoAlbumScrollView::photoViewBackgroundColor |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
/** @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. |
||||
|
* |
||||
|
* @fn NIPhotoAlbumScrollView::loadingImage |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
/** @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. |
||||
|
* |
||||
|
* @fn NIPhotoAlbumScrollView::didLoadPhoto:atIndex:photoSize: |
||||
|
*/ |
||||
@ -0,0 +1,221 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIPagingScrollView+Subclassing.h" |
||||
|
#import "NIPhotoScrollView.h" |
||||
|
#import "NIPhotoAlbumScrollViewDataSource.h" |
||||
|
#import "NimbusCore.h" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
@implementation NIPhotoAlbumScrollView { |
||||
|
// Configurable Properties |
||||
|
UIImage* _loadingImage; |
||||
|
BOOL _zoomingIsEnabled; |
||||
|
BOOL _zoomingAboveOriginalSizeIsEnabled; |
||||
|
} |
||||
|
|
||||
|
- (id)initWithFrame:(CGRect)frame { |
||||
|
if ((self = [super initWithFrame:frame])) { |
||||
|
// Default state. |
||||
|
self.zoomingIsEnabled = YES; |
||||
|
self.zoomingAboveOriginalSizeIsEnabled = YES; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)setBackgroundColor:(UIColor *)backgroundColor { |
||||
|
[super setBackgroundColor:backgroundColor]; |
||||
|
|
||||
|
self.scrollView.backgroundColor = backgroundColor; |
||||
|
} |
||||
|
|
||||
|
- (void)notifyDelegatePhotoDidLoadAtIndex:(NSInteger)photoIndex { |
||||
|
if (photoIndex == (self.centerPageIndex + 1) |
||||
|
&& [self.delegate respondsToSelector:@selector(photoAlbumScrollViewDidLoadNextPhoto:)]) { |
||||
|
[self.delegate photoAlbumScrollViewDidLoadNextPhoto:self]; |
||||
|
|
||||
|
} else if (photoIndex == (self.centerPageIndex - 1) |
||||
|
&& [self.delegate respondsToSelector:@selector(photoAlbumScrollViewDidLoadPreviousPhoto:)]) { |
||||
|
[self.delegate photoAlbumScrollViewDidLoadPreviousPhoto:self]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Visible Page Management |
||||
|
|
||||
|
|
||||
|
- (void)willDisplayPage:(NIPhotoScrollView *)page { |
||||
|
// 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 the high quality photo and set isLoading to YES. |
||||
|
// 3) If no photo was available, then the data source should start loading the photo |
||||
|
// at its highest available quality and nil should be returned. The loadingImage property |
||||
|
// will be displayed until the image is loaded. isLoading should be set to YES. |
||||
|
NIPhotoScrollViewPhotoSize photoSize = NIPhotoScrollViewPhotoSizeUnknown; |
||||
|
BOOL isLoading = NO; |
||||
|
CGSize originalPhotoDimensions = CGSizeZero; |
||||
|
UIImage* image = [self.dataSource photoAlbumScrollView: self |
||||
|
photoAtIndex: page.pageIndex |
||||
|
photoSize: &photoSize |
||||
|
isLoading: &isLoading |
||||
|
originalPhotoDimensions: &originalPhotoDimensions]; |
||||
|
|
||||
|
page.photoDimensions = originalPhotoDimensions; |
||||
|
// Only mark the view as loading if the center image is loading. |
||||
|
page.loading = (page.pageIndex == self.centerPageIndex) && isLoading; |
||||
|
|
||||
|
if (nil == image) { |
||||
|
page.zoomingIsEnabled = NO; |
||||
|
[page setImage:self.loadingImage photoSize:NIPhotoScrollViewPhotoSizeUnknown]; |
||||
|
|
||||
|
} else { |
||||
|
BOOL updateImage = photoSize > page.photoSize; |
||||
|
if (updateImage) { |
||||
|
[page setImage:image photoSize:photoSize]; |
||||
|
} |
||||
|
|
||||
|
// Configure this after the image is set otherwise if the page's image isn't there |
||||
|
// e.g. (after prepareForReuse), zooming will always be disabled |
||||
|
page.zoomingIsEnabled = ([self isZoomingEnabled] |
||||
|
&& (NIPhotoScrollViewPhotoSizeOriginal == photoSize)); |
||||
|
|
||||
|
if (updateImage && NIPhotoScrollViewPhotoSizeOriginal == photoSize) { |
||||
|
[self notifyDelegatePhotoDidLoadAtIndex:page.pageIndex]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)didRecyclePage:(UIView<NIPagingScrollViewPage> *)page { |
||||
|
// Give the data source the opportunity to kill any asynchronous operations for this |
||||
|
// now-recycled page. |
||||
|
if ([self.dataSource respondsToSelector: |
||||
|
@selector(photoAlbumScrollView:stopLoadingPhotoAtIndex:)]) { |
||||
|
[self.dataSource photoAlbumScrollView: self |
||||
|
stopLoadingPhotoAtIndex: page.pageIndex]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#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 - Public |
||||
|
|
||||
|
|
||||
|
- (UIView<NIPagingScrollViewPage> *)pagingScrollView:(NIPagingScrollView *)pagingScrollView |
||||
|
pageViewForIndex:(NSInteger)pageIndex { |
||||
|
UIView<NIPagingScrollViewPage>* pageView = nil; |
||||
|
NSString* reuseIdentifier = @"photo"; |
||||
|
pageView = [pagingScrollView dequeueReusablePageWithIdentifier:reuseIdentifier]; |
||||
|
if (nil == pageView) { |
||||
|
pageView = [[NIPhotoScrollView alloc] init]; |
||||
|
pageView.reuseIdentifier = reuseIdentifier; |
||||
|
pageView.backgroundColor = self.photoViewBackgroundColor; |
||||
|
} |
||||
|
|
||||
|
NIPhotoScrollView* photoScrollView = (NIPhotoScrollView *)pageView; |
||||
|
photoScrollView.photoScrollViewDelegate = self; |
||||
|
photoScrollView.zoomingAboveOriginalSizeIsEnabled = [self isZoomingAboveOriginalSizeEnabled]; |
||||
|
|
||||
|
return pageView; |
||||
|
} |
||||
|
|
||||
|
- (void)didLoadPhoto: (UIImage *)image |
||||
|
atIndex: (NSInteger)pageIndex |
||||
|
photoSize: (NIPhotoScrollViewPhotoSize)photoSize { |
||||
|
// This modifies the UI and therefor MUST be executed on the main thread. |
||||
|
NIDASSERT([NSThread isMainThread]); |
||||
|
|
||||
|
for (NIPhotoScrollView* page in self.visiblePages) { |
||||
|
if (page.pageIndex == pageIndex) { |
||||
|
|
||||
|
// Only replace the photo if it's of a higher quality than one we're already showing. |
||||
|
if (photoSize > page.photoSize) { |
||||
|
page.loading = NO; |
||||
|
[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:pageIndex]; |
||||
|
} |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setZoomingAboveOriginalSizeIsEnabled:(BOOL)enabled { |
||||
|
_zoomingAboveOriginalSizeIsEnabled = enabled; |
||||
|
|
||||
|
for (NIPhotoScrollView* page in self.visiblePages) { |
||||
|
page.zoomingAboveOriginalSizeIsEnabled = enabled; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setPhotoViewBackgroundColor:(UIColor *)photoViewBackgroundColor { |
||||
|
if (_photoViewBackgroundColor != photoViewBackgroundColor) { |
||||
|
_photoViewBackgroundColor = photoViewBackgroundColor; |
||||
|
|
||||
|
for (UIView<NIPagingScrollViewPage>* page in self.visiblePages) { |
||||
|
page.backgroundColor = photoViewBackgroundColor; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (BOOL)hasNext { |
||||
|
return (self.centerPageIndex < self.numberOfPages - 1); |
||||
|
} |
||||
|
|
||||
|
- (BOOL)hasPrevious { |
||||
|
return self.centerPageIndex > 0; |
||||
|
} |
||||
|
|
||||
|
- (id<NIPhotoAlbumScrollViewDataSource>)dataSource { |
||||
|
NIDASSERT([[super dataSource] conformsToProtocol:@protocol(NIPhotoAlbumScrollViewDataSource)]); |
||||
|
return (id<NIPhotoAlbumScrollViewDataSource>)[super dataSource]; |
||||
|
} |
||||
|
|
||||
|
- (void)setDataSource:(id<NIPhotoAlbumScrollViewDataSource>)dataSource { |
||||
|
[super setDataSource:(id<NIPagingScrollViewDataSource>)dataSource]; |
||||
|
} |
||||
|
|
||||
|
- (id<NIPhotoAlbumScrollViewDelegate>)delegate { |
||||
|
id<NIPagingScrollViewDelegate> superDelegate = [super delegate]; |
||||
|
NIDASSERT(nil == superDelegate |
||||
|
|| [superDelegate conformsToProtocol:@protocol(NIPhotoAlbumScrollViewDelegate)]); |
||||
|
return (id<NIPhotoAlbumScrollViewDelegate>)superDelegate; |
||||
|
} |
||||
|
|
||||
|
- (void)setDelegate:(id<NIPhotoAlbumScrollViewDelegate>)delegate { |
||||
|
[super setDelegate:(id<NIPhotoAlbumScrollViewDelegate>)delegate]; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,93 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIPhotoScrollViewPhotoSize.h" |
||||
|
|
||||
|
#import "NimbusPagingScrollView.h" |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
|
||||
|
@class NIPhotoAlbumScrollView; |
||||
|
|
||||
|
/** |
||||
|
* The photo album scroll data source. |
||||
|
* |
||||
|
* @ingroup NimbusPhotos |
||||
|
* |
||||
|
* 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 <NIPagingScrollViewDataSource> |
||||
|
|
||||
|
@required |
||||
|
|
||||
|
#pragma mark Fetching Required Album Information /** @name [NIPhotoAlbumScrollViewDataSource] Fetching Required Album Information */ |
||||
|
|
||||
|
/** |
||||
|
* 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 |
||||
|
|
||||
@ -0,0 +1,55 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
#import "NimbusPagingScrollView.h" |
||||
|
|
||||
|
@class NIPhotoAlbumScrollView; |
||||
|
|
||||
|
/** |
||||
|
* The photo album scroll view delegate. |
||||
|
* |
||||
|
* @ingroup Photos-Protocols |
||||
|
* @see NIPhotoAlbumScrollView |
||||
|
*/ |
||||
|
@protocol NIPhotoAlbumScrollViewDelegate <NIPagingScrollViewDelegate> |
||||
|
|
||||
|
@optional |
||||
|
|
||||
|
#pragma mark Scrolling and Zooming /** @name [NIPhotoAlbumScrollViewDelegate] Scrolling and Zooming */ |
||||
|
|
||||
|
/** |
||||
|
* The user double-tapped to zoom in or out. |
||||
|
*/ |
||||
|
- (void)photoAlbumScrollView: (NIPhotoAlbumScrollView *)photoAlbumScrollView |
||||
|
didZoomIn: (BOOL)didZoomIn; |
||||
|
|
||||
|
|
||||
|
#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 |
||||
|
|
||||
@ -0,0 +1,162 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIPagingScrollViewPage.h" |
||||
|
#import "NIPhotoScrollViewPhotoSize.h" |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
@protocol NIPhotoScrollViewDelegate; |
||||
|
@class NICenteringScrollView; |
||||
|
|
||||
|
/** |
||||
|
* A single photo view that supports zooming and rotation. |
||||
|
* |
||||
|
* @ingroup NimbusPhotos |
||||
|
*/ |
||||
|
@interface NIPhotoScrollView : UIView <UIScrollViewDelegate, NIPagingScrollViewPage> |
||||
|
|
||||
|
#pragma mark Configuring Functionality |
||||
|
|
||||
|
@property (nonatomic, assign, getter=isZoomingEnabled) BOOL zoomingIsEnabled; // default: yes |
||||
|
@property (nonatomic, assign, getter=isZoomingAboveOriginalSizeEnabled) BOOL zoomingAboveOriginalSizeIsEnabled; // default: yes |
||||
|
@property (nonatomic, assign, getter=isDoubleTapToZoomEnabled) BOOL doubleTapToZoomIsEnabled; // default: yes |
||||
|
@property (nonatomic, assign) CGFloat maximumScale; // default: 0 (autocalculate) |
||||
|
@property (nonatomic, weak) id<NIPhotoScrollViewDelegate> photoScrollViewDelegate; |
||||
|
|
||||
|
#pragma mark State |
||||
|
|
||||
|
- (UIImage *)image; |
||||
|
- (NIPhotoScrollViewPhotoSize)photoSize; |
||||
|
- (void)setImage:(UIImage *)image photoSize:(NIPhotoScrollViewPhotoSize)photoSize; |
||||
|
@property (nonatomic, assign, getter = isLoading) BOOL loading; |
||||
|
|
||||
|
@property (nonatomic, assign) NSInteger pageIndex; |
||||
|
@property (nonatomic, assign) CGSize photoDimensions; |
||||
|
@property (nonatomic, readonly, strong) UITapGestureRecognizer* doubleTapGestureRecognizer; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** @name Configuring Functionality */ |
||||
|
|
||||
|
/** |
||||
|
* Whether the photo is allowed to be zoomed. |
||||
|
* |
||||
|
* By default this is YES. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::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. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::zoomingAboveOriginalSizeIsEnabled |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Whether double-tapping zooms in and out of the image. |
||||
|
* |
||||
|
* Available on iOS 3.2 and later. |
||||
|
* |
||||
|
* By default this is YES. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::doubleTapToZoomIsEnabled |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The maximum scale of the image. |
||||
|
* |
||||
|
* By default this is 0, meaning the view will automatically determine the maximum scale. |
||||
|
* Setting this to a non-zero value will override the automatically-calculated maximum scale. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::maximumScale |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The photo scroll view delegate. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::photoScrollViewDelegate |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
/** @name State */ |
||||
|
|
||||
|
/** |
||||
|
* The currently-displayed photo. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::image |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* 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. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::setImage:photoSize: |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The index of this photo within a photo album. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::pageIndex |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The current size of the photo. |
||||
|
* |
||||
|
* This is used to replace the photo only with successively higher-quality versions. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::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. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::photoDimensions |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The gesture recognizer for double-tapping zooms in and out of the image. |
||||
|
* |
||||
|
* This is used mainly for setting up dependencies between gesture recognizers. |
||||
|
* |
||||
|
* @fn NIPhotoScrollView::doubleTapGestureRecognizer |
||||
|
*/ |
||||
@ -0,0 +1,510 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 "NIPhotoScrollViewDelegate.h" |
||||
|
|
||||
|
#import "NimbusCore.h" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
/** |
||||
|
* A UIScrollView that centers the zooming view's frame as the user zooms. |
||||
|
* |
||||
|
* We must update the zooming view's frame within the scroll view's layoutSubviews, |
||||
|
* thus why we've subclassed UIScrollView. |
||||
|
*/ |
||||
|
@interface NICenteringScrollView : UIScrollView |
||||
|
@end |
||||
|
|
||||
|
|
||||
|
@implementation NICenteringScrollView |
||||
|
|
||||
|
|
||||
|
#pragma mark - UIView |
||||
|
|
||||
|
|
||||
|
- (void)layoutSubviews { |
||||
|
[super layoutSubviews]; |
||||
|
|
||||
|
// Center the image as it becomes smaller than the size of the screen. |
||||
|
|
||||
|
UIView* zoomingSubview = [self.delegate viewForZoomingInScrollView:self]; |
||||
|
CGSize boundsSize = self.bounds.size; |
||||
|
CGRect frameToCenter = zoomingSubview.frame; |
||||
|
|
||||
|
// Center horizontally. |
||||
|
if (frameToCenter.size.width < boundsSize.width) { |
||||
|
frameToCenter.origin.x = NICGFloatFloor((boundsSize.width - frameToCenter.size.width) / 2); |
||||
|
|
||||
|
} else { |
||||
|
frameToCenter.origin.x = 0; |
||||
|
} |
||||
|
|
||||
|
// Center vertically. |
||||
|
if (frameToCenter.size.height < boundsSize.height) { |
||||
|
frameToCenter.origin.y = NICGFloatFloor((boundsSize.height - frameToCenter.size.height) / 2); |
||||
|
|
||||
|
} else { |
||||
|
frameToCenter.origin.y = 0; |
||||
|
} |
||||
|
|
||||
|
zoomingSubview.frame = frameToCenter; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
@interface NIPhotoScrollView () |
||||
|
@property (nonatomic, assign) NIPhotoScrollViewPhotoSize photoSize; |
||||
|
- (void)setMaxMinZoomScalesForCurrentBounds; |
||||
|
@end |
||||
|
|
||||
|
@implementation NIPhotoScrollView { |
||||
|
// The photo view to be zoomed. |
||||
|
UIImageView* _imageView; |
||||
|
// The scroll view. |
||||
|
NICenteringScrollView* _scrollView; |
||||
|
UIActivityIndicatorView* _loadingView; |
||||
|
|
||||
|
// Photo Information |
||||
|
NIPhotoScrollViewPhotoSize _photoSize; |
||||
|
CGSize _photoDimensions; |
||||
|
|
||||
|
// Configurable State |
||||
|
BOOL _zoomingIsEnabled; |
||||
|
BOOL _zoomingAboveOriginalSizeIsEnabled; |
||||
|
|
||||
|
UITapGestureRecognizer* _doubleTapGestureRecognizer; |
||||
|
} |
||||
|
|
||||
|
@synthesize reuseIdentifier; |
||||
|
|
||||
|
- (id)initWithFrame:(CGRect)frame { |
||||
|
if ((self = [super initWithFrame:frame])) { |
||||
|
// Default configuration. |
||||
|
self.zoomingIsEnabled = YES; |
||||
|
self.zoomingAboveOriginalSizeIsEnabled = YES; |
||||
|
self.doubleTapToZoomIsEnabled = YES; |
||||
|
|
||||
|
// Autorelease so that we don't have to worry about releasing the subviews in dealloc. |
||||
|
_scrollView = [[NICenteringScrollView alloc] initWithFrame:self.bounds]; |
||||
|
_scrollView.autoresizingMask = (UIViewAutoresizingFlexibleWidth |
||||
|
| UIViewAutoresizingFlexibleHeight); |
||||
|
|
||||
|
_loadingView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhite]; |
||||
|
[_loadingView sizeToFit]; |
||||
|
_loadingView.frame = NIFrameOfCenteredViewWithinView(_loadingView, self); |
||||
|
_loadingView.autoresizingMask = UIViewAutoresizingFlexibleMargins; |
||||
|
|
||||
|
// We implement viewForZoomingInScrollView: and return the image view for zooming. |
||||
|
_scrollView.delegate = self; |
||||
|
|
||||
|
// Disable the scroll indicators. |
||||
|
_scrollView.showsVerticalScrollIndicator = NO; |
||||
|
_scrollView.showsHorizontalScrollIndicator = NO; |
||||
|
|
||||
|
// Photo viewers should feel sticky when you're panning around, not smooth and slippery |
||||
|
// like a UITableView. |
||||
|
_scrollView.decelerationRate = UIScrollViewDecelerationRateFast; |
||||
|
|
||||
|
// Ensure that empty areas of the scroll view are draggable. |
||||
|
self.backgroundColor = [UIColor blackColor]; |
||||
|
_scrollView.backgroundColor = self.backgroundColor; |
||||
|
|
||||
|
_imageView = [[UIImageView alloc] initWithFrame:CGRectZero]; |
||||
|
|
||||
|
[_scrollView addSubview:_imageView]; |
||||
|
[self addSubview:_scrollView]; |
||||
|
[self addSubview:_loadingView]; |
||||
|
} |
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)setBackgroundColor:(UIColor *)backgroundColor { |
||||
|
[super setBackgroundColor:backgroundColor]; |
||||
|
|
||||
|
_scrollView.backgroundColor = backgroundColor; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - UIScrollView |
||||
|
|
||||
|
|
||||
|
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView { |
||||
|
return _imageView; |
||||
|
} |
||||
|
|
||||
|
#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 = (_scrollView.maximumZoomScale <= _scrollView.zoomScale + FLT_EPSILON); |
||||
|
|
||||
|
BOOL didZoomIn; |
||||
|
|
||||
|
if (isCompletelyZoomedIn) { |
||||
|
// Zoom the photo back out. |
||||
|
[_scrollView setZoomScale:_scrollView.minimumZoomScale animated:YES]; |
||||
|
|
||||
|
didZoomIn = NO; |
||||
|
|
||||
|
} else { |
||||
|
// Zoom into the tap point. |
||||
|
CGPoint tapCenter = [tapGesture locationInView:_imageView]; |
||||
|
|
||||
|
CGRect maxZoomRect = [self rectAroundPoint:tapCenter atZoomScale:_scrollView.maximumZoomScale]; |
||||
|
[_scrollView zoomToRect:maxZoomRect animated:YES]; |
||||
|
|
||||
|
didZoomIn = YES; |
||||
|
} |
||||
|
|
||||
|
if ([self.photoScrollViewDelegate respondsToSelector: |
||||
|
@selector(photoScrollViewDidDoubleTapToZoom:didZoomIn:)]) { |
||||
|
[self.photoScrollViewDelegate photoScrollViewDidDoubleTapToZoom:self didZoomIn:didZoomIn]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#pragma mark - NIPagingScrollViewPage |
||||
|
|
||||
|
|
||||
|
- (void)prepareForReuse { |
||||
|
_imageView.image = nil; |
||||
|
self.photoSize = NIPhotoScrollViewPhotoSizeUnknown; |
||||
|
_scrollView.zoomScale = 1; |
||||
|
_scrollView.contentSize = self.bounds.size; |
||||
|
} |
||||
|
|
||||
|
- (void)pageDidDisappear { |
||||
|
_scrollView.zoomScale = _scrollView.minimumZoomScale; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Public |
||||
|
|
||||
|
|
||||
|
- (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) { |
||||
|
_scrollView.contentSize = image.size; |
||||
|
|
||||
|
} else { |
||||
|
_scrollView.contentSize = self.bounds.size; |
||||
|
} |
||||
|
|
||||
|
[self setMaxMinZoomScalesForCurrentBounds]; |
||||
|
|
||||
|
// Start off with the image fully-visible on the screen. |
||||
|
_scrollView.zoomScale = _scrollView.minimumZoomScale; |
||||
|
|
||||
|
[self setNeedsLayout]; |
||||
|
} |
||||
|
|
||||
|
- (void)setLoading:(BOOL)loading { |
||||
|
_loading = loading; |
||||
|
|
||||
|
if (loading) { |
||||
|
[_loadingView startAnimating]; |
||||
|
} else { |
||||
|
[_loadingView stopAnimating]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (UIImage *)image { |
||||
|
return _imageView.image; |
||||
|
} |
||||
|
|
||||
|
- (void)setZoomingIsEnabled:(BOOL)enabled { |
||||
|
_zoomingIsEnabled = enabled; |
||||
|
|
||||
|
if (nil != _imageView.image) { |
||||
|
[self setMaxMinZoomScalesForCurrentBounds]; |
||||
|
|
||||
|
// Fit the image on screen. |
||||
|
_scrollView.zoomScale = _scrollView.minimumZoomScale; |
||||
|
|
||||
|
// Disable zoom bouncing if zooming is disabled, otherwise the view will allow pinching. |
||||
|
_scrollView.bouncesZoom = enabled; |
||||
|
|
||||
|
} else { |
||||
|
// Reset to the defaults if there is no set image yet. |
||||
|
_scrollView.zoomScale = 1; |
||||
|
_scrollView.minimumZoomScale = 1; |
||||
|
_scrollView.maximumZoomScale = 1; |
||||
|
_scrollView.bouncesZoom = NO; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setDoubleTapToZoomIsEnabled:(BOOL)enabled { |
||||
|
if (enabled && nil == _doubleTapGestureRecognizer) { |
||||
|
_doubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didDoubleTap:)]; |
||||
|
_doubleTapGestureRecognizer.numberOfTapsRequired = 2; |
||||
|
[self addGestureRecognizer:_doubleTapGestureRecognizer]; |
||||
|
} |
||||
|
|
||||
|
_doubleTapGestureRecognizer.enabled = enabled; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)isDoubleTapToZoomEnabled { |
||||
|
return [_doubleTapGestureRecognizer isEnabled]; |
||||
|
} |
||||
|
|
||||
|
- (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 |
||||
|
photoScale: (CGFloat)photoScale |
||||
|
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 |
||||
|
: (photoScale / 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.) |
||||
|
// todo marco 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. |
||||
|
if (imageSize.width <= 0 || imageSize.height <= 0) { |
||||
|
_scrollView.maximumZoomScale = 1; |
||||
|
_scrollView.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 = _scrollView.bounds.size; |
||||
|
|
||||
|
CGFloat minScale = 0; |
||||
|
CGFloat maxScale = 0; |
||||
|
|
||||
|
// Calculate the min/max scale for the image to be presented. |
||||
|
[self minAndMaxScaleForDimensions: imageSize |
||||
|
boundsSize: boundsSize |
||||
|
photoScale: _imageView.image.scale |
||||
|
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 |
||||
|
photoScale: _imageView.image.scale |
||||
|
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. |
||||
|
if (self.isZoomingEnabled && NIPhotoScrollViewPhotoSizeOriginal == self.photoSize && self.maximumScale > 0) { |
||||
|
_scrollView.maximumZoomScale = self.maximumScale; |
||||
|
} else { |
||||
|
_scrollView.maximumZoomScale = self.isZoomingEnabled ? maxScale : minScale; |
||||
|
} |
||||
|
_scrollView.minimumZoomScale = minScale; |
||||
|
} |
||||
|
|
||||
|
#pragma mark Saving/Restoring Offset and Scale |
||||
|
|
||||
|
// Parts of the following code are from Apple's ImageScrollView example application and |
||||
|
// have been used here because they are well-documented and concise. |
||||
|
|
||||
|
|
||||
|
// Fetch the visual center point of this view in the image view's coordinate space. |
||||
|
- (CGPoint)pointToCenterAfterRotation { |
||||
|
CGRect bounds = _scrollView.bounds; |
||||
|
CGPoint boundsCenter = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds)); |
||||
|
return [self convertPoint:boundsCenter toView:_imageView]; |
||||
|
} |
||||
|
|
||||
|
- (CGFloat)scaleToRestoreAfterRotation { |
||||
|
CGFloat contentScale = _scrollView.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 <= _scrollView.minimumZoomScale + FLT_EPSILON) { |
||||
|
contentScale = 0; |
||||
|
} |
||||
|
|
||||
|
return contentScale; |
||||
|
} |
||||
|
|
||||
|
- (CGPoint)maximumContentOffset { |
||||
|
CGSize contentSize = _scrollView.contentSize; |
||||
|
CGSize boundsSize = _scrollView.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. |
||||
|
_scrollView.zoomScale = NIBoundf(oldScale, |
||||
|
_scrollView.minimumZoomScale, _scrollView.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 - _scrollView.bounds.size.width / 2.0f, |
||||
|
boundsCenter.y - _scrollView.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 = NIBoundf(offset.x, minOffset.x, maxOffset.x); |
||||
|
offset.y = NIBoundf(offset.y, minOffset.y, maxOffset.y); |
||||
|
_scrollView.contentOffset = offset; |
||||
|
} |
||||
|
|
||||
|
#pragma mark Saving/Restoring Offset and Scale |
||||
|
|
||||
|
|
||||
|
- (void)setFrameAndMaintainState:(CGRect)frame { |
||||
|
CGPoint restorePoint = [self pointToCenterAfterRotation]; |
||||
|
CGFloat restoreScale = [self scaleToRestoreAfterRotation]; |
||||
|
self.frame = frame; |
||||
|
[self setMaxMinZoomScalesForCurrentBounds]; |
||||
|
[self restoreCenterPoint:restorePoint scale:restoreScale]; |
||||
|
|
||||
|
[_scrollView setNeedsLayout]; |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,41 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
@class NIPhotoScrollView; |
||||
|
|
||||
|
/** |
||||
|
* The photo scroll view delegate. |
||||
|
* |
||||
|
* @ingroup NimbusPhotos |
||||
|
*/ |
||||
|
@protocol NIPhotoScrollViewDelegate <NSObject> |
||||
|
|
||||
|
@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 |
||||
@ -0,0 +1,31 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
|
||||
|
/** |
||||
|
* 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; |
||||
@ -0,0 +1,168 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIPreprocessorMacros.h" /* for weak */ |
||||
|
|
||||
|
@protocol NIPhotoScrubberViewDataSource; |
||||
|
@protocol NIPhotoScrubberViewDelegate; |
||||
|
|
||||
|
/** |
||||
|
* A control built for quickly skimming through a collection of images. |
||||
|
* |
||||
|
* @ingroup NimbusPhotos |
||||
|
* |
||||
|
* 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 |
||||
|
|
||||
|
#pragma mark Data Source /** @name Data Source */ |
||||
|
|
||||
|
/** |
||||
|
* The data source for this scrubber view. |
||||
|
*/ |
||||
|
@property (nonatomic, weak) id<NIPhotoScrubberViewDataSource> 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, weak) id<NIPhotoScrubberViewDelegate> delegate; |
||||
|
|
||||
|
#pragma mark Accessing Selection /** @name Accessing Selection */ |
||||
|
|
||||
|
/** |
||||
|
* The selected photo index. |
||||
|
*/ |
||||
|
@property (nonatomic, 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 NimbusPhotos |
||||
|
* |
||||
|
* <h2>Performance Considerations</h2> |
||||
|
* |
||||
|
* 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 <NSObject> |
||||
|
|
||||
|
@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 NimbusPhotos |
||||
|
* |
||||
|
* Sends notifications of state changes. |
||||
|
* |
||||
|
* @see NIPhotoScrubberView |
||||
|
*/ |
||||
|
@protocol NIPhotoScrubberViewDelegate <NSObject> |
||||
|
|
||||
|
@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 |
||||
@ -0,0 +1,465 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <QuartzCore/QuartzCore.h> |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
static const NSInteger NIPhotoScrubberViewUnknownTag = -1; |
||||
|
|
||||
|
@interface NIPhotoScrubberView() |
||||
|
|
||||
|
/** |
||||
|
* @internal |
||||
|
* |
||||
|
* A method to encapsulate initilization logic that can be shared by different init methods. |
||||
|
*/ |
||||
|
- (void)initializeScrubber; |
||||
|
|
||||
|
/** |
||||
|
* @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 { |
||||
|
NSMutableArray* _visiblePhotoViews; |
||||
|
NSMutableSet* _recycledPhotoViews; |
||||
|
|
||||
|
UIView* _containerView; |
||||
|
UIImageView* _selectionView; |
||||
|
|
||||
|
// State |
||||
|
NSInteger _selectedPhotoIndex; |
||||
|
|
||||
|
// Cached data source values |
||||
|
NSInteger _numberOfPhotos; |
||||
|
|
||||
|
// Cached display values |
||||
|
NSInteger _numberOfVisiblePhotos; |
||||
|
} |
||||
|
|
||||
|
- (id)initWithFrame:(CGRect)frame { |
||||
|
if ((self = [super initWithFrame:frame])) { |
||||
|
[self initializeScrubber]; |
||||
|
} |
||||
|
|
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (id)initWithCoder:(NSCoder *)aDecoder { |
||||
|
if ((self = [super initWithCoder:aDecoder])) { |
||||
|
[self initializeScrubber]; |
||||
|
} |
||||
|
|
||||
|
return self; |
||||
|
} |
||||
|
|
||||
|
- (void)initializeScrubber { |
||||
|
// 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]; |
||||
|
[self addSubview:_selectionView]; |
||||
|
|
||||
|
_selectedPhotoIndex = -1; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - View Creation |
||||
|
|
||||
|
|
||||
|
- (UIImageView *)photoView { |
||||
|
UIImageView* imageView = [[UIImageView alloc] init]; |
||||
|
|
||||
|
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 - Layout |
||||
|
|
||||
|
|
||||
|
- (CGSize)photoSize { |
||||
|
CGSize boundsSize = self.bounds.size; |
||||
|
|
||||
|
// These numbers are roughly estimated from the Photos.app's scrubber. |
||||
|
CGFloat photoWidth = NICGFloatFloor(boundsSize.height / 2.4f); |
||||
|
CGFloat photoHeight = NICGFloatFloor(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 = NICGFloatFloor(boundsSize.height / 1.2f); |
||||
|
CGFloat selectionHeight = NICGFloatFloor(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 NICGFloatFloor(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 = NICGFloatFloor((((CGFloat)photoIndex * containerWidth) |
||||
|
/ (CGFloat)MAX(1, _numberOfPhotos))); |
||||
|
|
||||
|
return CGRectMake(NICGFloatFloor(_containerView.frame.origin.x |
||||
|
+ relativeOffset |
||||
|
+ photoSize.width / 2 - selectionSize.width / 2), |
||||
|
NICGFloatFloor(_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, NICGFloatFloor((maxContentWidth - containerWidth) / 2)); |
||||
|
CGFloat horizontalMargins = [self horizontalMargins]; |
||||
|
CGFloat containerHeight = photoSize.height + _containerView.layer.borderWidth * 2; |
||||
|
|
||||
|
CGFloat containerLeftMargin = horizontalMargins + containerMargins; |
||||
|
CGFloat containerTopMargin = NICGFloatFloor((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)(NICGFloatCeil((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] > (NSUInteger)_numberOfVisiblePhotos) { |
||||
|
UIView* photoView = [_visiblePhotoViews lastObject]; |
||||
|
[photoView removeFromSuperview]; |
||||
|
|
||||
|
[_recycledPhotoViews addObject:photoView]; |
||||
|
|
||||
|
[_visiblePhotoViews removeLastObject]; |
||||
|
} |
||||
|
|
||||
|
// Lay out the visible photos. |
||||
|
for (NSUInteger ix = 0; ix < (NSUInteger)_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]; |
||||
|
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; |
||||
|
|
||||
|
UIImage* image = [self.dataSource photoScrubberView:self thumbnailAtIndex:photoIndex]; |
||||
|
photoView.image = image; |
||||
|
|
||||
|
if (_selectedPhotoIndex == photoIndex) { |
||||
|
_selectionView.image = image; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
photoView.frame = [self frameForThumbAtIndex:ix]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
#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)(NICGFloatFloor((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 - 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 - Public |
||||
|
|
||||
|
|
||||
|
- (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]; |
||||
|
} |
||||
|
|
||||
|
// 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 |
||||
@ -0,0 +1,201 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIPhotoAlbumScrollView.h" |
||||
|
#import "NIPhotoScrubberView.h" |
||||
|
|
||||
|
@class NIPhotoAlbumScrollView; |
||||
|
|
||||
|
/** |
||||
|
* A simple photo album view controller implementation with a toolbar. |
||||
|
* |
||||
|
* @ingroup NimbusPhotos |
||||
|
* |
||||
|
* 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. |
||||
|
* |
||||
|
* <h2>Implementing Delegate Methods</h2> |
||||
|
* |
||||
|
* 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: |
||||
|
* |
||||
|
* |
||||
|
* <h2>Recommended Configurations</h2> |
||||
|
* |
||||
|
* <h3>Default: Zooming enabled with translucent toolbar</h3> |
||||
|
* |
||||
|
* 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 |
||||
|
* toolbarIsTranslucent = YES; |
||||
|
* hidesChromeWhenScrolling = YES; |
||||
|
* chromeCanBeHidden = YES; |
||||
|
* @endcode |
||||
|
* |
||||
|
* <h3>Zooming disabled with opaque toolbar</h3> |
||||
|
* |
||||
|
* The following settings are good for viewing photo albums when you want to keep the chrome |
||||
|
* visible at all times without zooming enabled. |
||||
|
* |
||||
|
* @code |
||||
|
* toolbarIsTranslucent = NO; |
||||
|
* chromeCanBeHidden = NO; |
||||
|
* photoAlbumView.zoomingIsEnabled = NO; |
||||
|
* @endcode |
||||
|
*/ |
||||
|
@interface NIToolbarPhotoViewController : UIViewController <NIPhotoAlbumScrollViewDelegate, NIPhotoScrubberViewDelegate> |
||||
|
|
||||
|
#pragma mark Configuring Functionality |
||||
|
|
||||
|
@property (nonatomic, assign, getter=isToolbarTranslucent) BOOL toolbarIsTranslucent; // default: yes |
||||
|
@property (nonatomic, assign) BOOL hidesChromeWhenScrolling; // default: yes |
||||
|
@property (nonatomic, assign) BOOL chromeCanBeHidden; // default: yes |
||||
|
@property (nonatomic, assign) BOOL animateMovingToNextAndPreviousPhotos; // default: no |
||||
|
@property (nonatomic, assign, getter=isScrubberEnabled) BOOL scrubberIsEnabled; // default: ipad yes - iphone no |
||||
|
|
||||
|
#pragma mark Views |
||||
|
|
||||
|
@property (nonatomic, readonly, strong) UIToolbar* toolbar; |
||||
|
@property (nonatomic, readonly, strong) NIPhotoAlbumScrollView* photoAlbumView; |
||||
|
@property (nonatomic, readonly, strong) NIPhotoScrubberView* photoScrubberView; |
||||
|
- (void)refreshChromeState; |
||||
|
|
||||
|
#pragma mark Toolbar Buttons |
||||
|
|
||||
|
@property (nonatomic, readonly, strong) UIBarButtonItem* nextButton; |
||||
|
@property (nonatomic, readonly, strong) UIBarButtonItem* previousButton; |
||||
|
|
||||
|
#pragma mark Subclassing |
||||
|
|
||||
|
- (void)setChromeVisibility:(BOOL)isVisible animated:(BOOL)animated; |
||||
|
- (void)setChromeTitle; |
||||
|
|
||||
|
@end |
||||
|
|
||||
|
/** @name Configuring Functionality */ |
||||
|
|
||||
|
/** |
||||
|
* Whether the toolbar is translucent and shows photos beneath it or not. |
||||
|
* |
||||
|
* If this is enabled, the toolbar will be translucent and the photo view will |
||||
|
* take up the entire view controller's bounds. |
||||
|
* |
||||
|
* If this is disabled, the photo will only occupy the remaining space above the |
||||
|
* toolbar. The toolbar will also not be hidden when the chrome is dismissed. This is by design |
||||
|
* because dismissing the toolbar when photos can't be displayed beneath it would leave |
||||
|
* an empty space below the album. |
||||
|
* |
||||
|
* By default this is YES. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::toolbarIsTranslucent |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* 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. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::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. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::chromeCanBeHidden |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* Whether to animate moving to a next or previous photo when the user taps the button. |
||||
|
* |
||||
|
* By default this is NO. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::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. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::scrubberIsEnabled |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
/** @name Views */ |
||||
|
|
||||
|
/** |
||||
|
* The toolbar view. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::toolbar |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The photo album view. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::photoAlbumView |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The photo scrubber view. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::photoScrubberView |
||||
|
*/ |
||||
|
|
||||
|
|
||||
|
/** @name Toolbar Buttons */ |
||||
|
|
||||
|
/** |
||||
|
* The 'next' button. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::nextButton |
||||
|
*/ |
||||
|
|
||||
|
/** |
||||
|
* The 'previous' button. |
||||
|
* |
||||
|
* @fn NIToolbarPhotoViewController::previousButton |
||||
|
*/ |
||||
@ -0,0 +1,533 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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" |
||||
|
|
||||
|
#if !defined(__has_feature) || !__has_feature(objc_arc) |
||||
|
#error "Nimbus requires ARC support." |
||||
|
#endif |
||||
|
|
||||
|
@implementation NIToolbarPhotoViewController { |
||||
|
// Views |
||||
|
UIToolbar* _toolbar; |
||||
|
NIPhotoAlbumScrollView* _photoAlbumView; |
||||
|
|
||||
|
// Scrubber View |
||||
|
NIPhotoScrubberView* _photoScrubberView; |
||||
|
|
||||
|
// Toolbar Buttons |
||||
|
UIBarButtonItem* _nextButton; |
||||
|
UIBarButtonItem* _previousButton; |
||||
|
|
||||
|
// Gestures |
||||
|
UITapGestureRecognizer* _tapGesture; |
||||
|
|
||||
|
// State |
||||
|
BOOL _isAnimatingChrome; |
||||
|
BOOL _isChromeHidden; |
||||
|
BOOL _prefersStatusBarHidden; |
||||
|
|
||||
|
// Configuration |
||||
|
BOOL _toolbarIsTranslucent; |
||||
|
BOOL _hidesChromeWhenScrolling; |
||||
|
BOOL _chromeCanBeHidden; |
||||
|
BOOL _animateMovingToNextAndPreviousPhotos; |
||||
|
BOOL _scrubberIsEnabled; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
- (void)shutdown_NIToolbarPhotoViewController { |
||||
|
_toolbar = nil; |
||||
|
_photoAlbumView = nil; |
||||
|
_nextButton = nil; |
||||
|
_previousButton = nil; |
||||
|
_photoScrubberView = nil; |
||||
|
_tapGesture = nil; |
||||
|
} |
||||
|
|
||||
|
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { |
||||
|
if ((self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil])) { |
||||
|
// Default Configuration Settings |
||||
|
self.toolbarIsTranslucent = YES; |
||||
|
self.hidesChromeWhenScrolling = YES; |
||||
|
self.chromeCanBeHidden = YES; |
||||
|
self.animateMovingToNextAndPreviousPhotos = NO; |
||||
|
if ([self respondsToSelector:@selector(setAutomaticallyAdjustsScrollViewInsets:)]) { |
||||
|
self.automaticallyAdjustsScrollViewInsets = 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]) { |
||||
|
if (nil == _tapGesture) { |
||||
|
_tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(didTap)]; |
||||
|
[self.photoAlbumView addGestureRecognizer:_tapGesture]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
_tapGesture.enabled = YES; |
||||
|
} |
||||
|
|
||||
|
- (void)updateToolbarItems { |
||||
|
UIBarItem* flexibleSpace = |
||||
|
[[UIBarButtonItem alloc] initWithBarButtonSystemItem: UIBarButtonSystemItemFlexibleSpace |
||||
|
target: nil |
||||
|
action: nil]; |
||||
|
|
||||
|
if ([self isScrubberEnabled]) { |
||||
|
_nextButton = nil; |
||||
|
_previousButton = nil; |
||||
|
|
||||
|
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]; |
||||
|
self.toolbar.items = [NSArray arrayWithObjects: |
||||
|
flexibleSpace, scrubberItem, flexibleSpace, |
||||
|
nil]; |
||||
|
|
||||
|
[_photoScrubberView setSelectedPhotoIndex:self.photoAlbumView.centerPageIndex]; |
||||
|
|
||||
|
} else { |
||||
|
_photoScrubberView = nil; |
||||
|
|
||||
|
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]; |
||||
|
|
||||
|
self.view.backgroundColor = [UIColor blackColor]; |
||||
|
|
||||
|
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]; |
||||
|
_toolbar.barStyle = UIBarStyleBlack; |
||||
|
_toolbar.translucent = self.toolbarIsTranslucent; |
||||
|
_toolbar.autoresizingMask = (UIViewAutoresizingFlexibleWidth |
||||
|
| UIViewAutoresizingFlexibleTopMargin); |
||||
|
|
||||
|
[self updateToolbarItems]; |
||||
|
|
||||
|
// Photo Album View Setup |
||||
|
|
||||
|
CGRect photoAlbumFrame = bounds; |
||||
|
if (!self.toolbarIsTranslucent) { |
||||
|
photoAlbumFrame = NIRectContract(bounds, 0, toolbarHeight); |
||||
|
} |
||||
|
_photoAlbumView = [[NIPhotoAlbumScrollView alloc] initWithFrame:photoAlbumFrame]; |
||||
|
_photoAlbumView.autoresizingMask = (UIViewAutoresizingFlexibleWidth |
||||
|
| UIViewAutoresizingFlexibleHeight); |
||||
|
_photoAlbumView.delegate = self; |
||||
|
|
||||
|
[self.view addSubview:_photoAlbumView]; |
||||
|
[self.view addSubview:_toolbar]; |
||||
|
|
||||
|
if (self.hidesChromeWhenScrolling || self.chromeCanBeHidden) { |
||||
|
[self addTapGestureToView]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (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 = self.toolbarIsTranslucent; |
||||
|
|
||||
|
_previousButton.enabled = [self.photoAlbumView hasPrevious]; |
||||
|
_nextButton.enabled = [self.photoAlbumView hasNext]; |
||||
|
} |
||||
|
|
||||
|
#if __IPHONE_OS_VERSION_MIN_REQUIRED < NIIOS_6_0 |
||||
|
- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation { |
||||
|
return NIIsSupportedOrientation(toInterfaceOrientation); |
||||
|
} |
||||
|
#endif |
||||
|
|
||||
|
|
||||
|
- (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 { |
||||
|
[self.photoAlbumView willAnimateRotationToInterfaceOrientation: toInterfaceOrientation |
||||
|
duration: 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.toolbarIsTranslucent) { |
||||
|
CGRect photoAlbumFrame = self.photoAlbumView.frame; |
||||
|
photoAlbumFrame.size.height = self.view.bounds.size.height - toolbarFrame.size.height; |
||||
|
self.photoAlbumView.frame = photoAlbumFrame; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (UIView *)rotatingFooterView { |
||||
|
return self.toolbar.hidden ? nil : self.toolbar; |
||||
|
} |
||||
|
|
||||
|
- (void)didHideChrome { |
||||
|
_isAnimatingChrome = NO; |
||||
|
if (self.toolbarIsTranslucent) { |
||||
|
self.toolbar.hidden = YES; |
||||
|
} |
||||
|
|
||||
|
[self.navigationController setNavigationBarHidden:YES animated:NO]; |
||||
|
_isChromeHidden = YES; |
||||
|
} |
||||
|
|
||||
|
- (void)didShowChrome { |
||||
|
_isAnimatingChrome = NO; |
||||
|
|
||||
|
_isChromeHidden = NO; |
||||
|
} |
||||
|
|
||||
|
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation { |
||||
|
return UIStatusBarAnimationSlide; |
||||
|
} |
||||
|
|
||||
|
- (BOOL)prefersStatusBarHidden { |
||||
|
return _prefersStatusBarHidden; |
||||
|
} |
||||
|
|
||||
|
- (void)setChromeVisibility:(BOOL)isVisible animated:(BOOL)animated { |
||||
|
if (_isAnimatingChrome |
||||
|
|| (!isVisible && _isChromeHidden) |
||||
|
|| (isVisible && !_isChromeHidden) |
||||
|
|| !self.chromeCanBeHidden) { |
||||
|
// Nothing to do here. |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
CGRect toolbarFrame = self.toolbar.frame; |
||||
|
CGRect bounds = self.view.bounds; |
||||
|
|
||||
|
if (self.toolbarIsTranslucent) { |
||||
|
// 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. |
||||
|
BOOL isStatusBarAppearanceSupported = [self respondsToSelector:@selector(setNeedsStatusBarAppearanceUpdate)]; |
||||
|
if (!isStatusBarAppearanceSupported) { |
||||
|
[[UIApplication sharedApplication] setStatusBarHidden:!isVisible |
||||
|
withAnimation:(animated |
||||
|
? UIStatusBarAnimationSlide |
||||
|
: UIStatusBarAnimationNone)]; |
||||
|
} |
||||
|
|
||||
|
if (self.toolbarIsTranslucent) { |
||||
|
// 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 = self.navigationController.navigationBar.frame; |
||||
|
|
||||
|
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()]; |
||||
|
} |
||||
|
|
||||
|
if (isStatusBarAppearanceSupported) { |
||||
|
_prefersStatusBarHidden = !isVisible; |
||||
|
[self setNeedsStatusBarAppearanceUpdate]; |
||||
|
} |
||||
|
|
||||
|
if (nil != self.navigationController.navigationBar) { |
||||
|
if (isVisible) { |
||||
|
[UIView setAnimationsEnabled:NO]; |
||||
|
[self.navigationController setNavigationBarHidden:NO animated:NO]; |
||||
|
navigationBarFrame.origin.y = 0; |
||||
|
self.navigationController.navigationBar.frame = navigationBarFrame; |
||||
|
self.navigationController.navigationBar.alpha = 0; |
||||
|
[UIView setAnimationsEnabled:YES]; |
||||
|
|
||||
|
navigationBarFrame.origin.y = NIStatusBarHeight(); |
||||
|
|
||||
|
} else { |
||||
|
navigationBarFrame.origin.y = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if (self.toolbarIsTranslucent) { |
||||
|
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:(_isChromeHidden || _isAnimatingChrome) animated:YES]; |
||||
|
} |
||||
|
|
||||
|
#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 setChromeTitle]; |
||||
|
} |
||||
|
|
||||
|
- (void)setChromeTitle { |
||||
|
self.title = [NSString stringWithFormat:@"%zd of %zd", |
||||
|
(self.photoAlbumView.centerPageIndex + 1), |
||||
|
self.photoAlbumView.numberOfPages]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - NIPhotoAlbumScrollViewDelegate |
||||
|
|
||||
|
|
||||
|
- (void)pagingScrollViewDidScroll:(NIPagingScrollView *)pagingScrollView { |
||||
|
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)pagingScrollViewDidChangePages:(NIPagingScrollView *)pagingScrollView { |
||||
|
// 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: [pagingScrollView centerPageIndex] |
||||
|
animated: !self.hidesChromeWhenScrolling]; |
||||
|
|
||||
|
[self refreshChromeState]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - NIPhotoScrubberViewDelegate |
||||
|
|
||||
|
|
||||
|
- (void)photoScrubberViewDidChangeSelection:(NIPhotoScrubberView *)photoScrubberView { |
||||
|
[self.photoAlbumView moveToPageAtIndex:photoScrubberView.selectedPhotoIndex animated:NO]; |
||||
|
|
||||
|
[self refreshChromeState]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Actions |
||||
|
|
||||
|
|
||||
|
- (void)didTapNextButton { |
||||
|
[self.photoAlbumView moveToNextAnimated:self.animateMovingToNextAndPreviousPhotos]; |
||||
|
|
||||
|
[self refreshChromeState]; |
||||
|
} |
||||
|
|
||||
|
- (void)didTapPreviousButton { |
||||
|
[self.photoAlbumView moveToPreviousAnimated:self.animateMovingToNextAndPreviousPhotos]; |
||||
|
|
||||
|
[self refreshChromeState]; |
||||
|
} |
||||
|
|
||||
|
#pragma mark - Public |
||||
|
|
||||
|
|
||||
|
- (void)settoolbarIsTranslucent:(BOOL)enabled { |
||||
|
_toolbarIsTranslucent = enabled; |
||||
|
|
||||
|
self.toolbar.translucent = enabled; |
||||
|
} |
||||
|
|
||||
|
- (void)setHidesChromeWhenScrolling:(BOOL)hidesToolbar { |
||||
|
_hidesChromeWhenScrolling = hidesToolbar; |
||||
|
|
||||
|
if (hidesToolbar) { |
||||
|
[self addTapGestureToView]; |
||||
|
|
||||
|
} else { |
||||
|
[_tapGesture setEnabled:_chromeCanBeHidden]; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setChromeCanBeHidden:(BOOL)canBeHidden { |
||||
|
_chromeCanBeHidden = canBeHidden; |
||||
|
|
||||
|
if (canBeHidden) { |
||||
|
[self addTapGestureToView]; |
||||
|
|
||||
|
} else { |
||||
|
self.hidesChromeWhenScrolling = NO; |
||||
|
|
||||
|
if ([self isViewLoaded]) { |
||||
|
// Ensure that the chrome is visible. |
||||
|
[self setChromeVisibility:YES animated:NO]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
- (void)setScrubberIsEnabled:(BOOL)enabled { |
||||
|
if (_scrubberIsEnabled != enabled) { |
||||
|
_scrubberIsEnabled = enabled; |
||||
|
|
||||
|
if ([self isViewLoaded]) { |
||||
|
[self updateToolbarItems]; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@end |
||||
@ -0,0 +1,122 @@ |
|||||
|
// |
||||
|
// Copyright 2011-2014 NimbusKit |
||||
|
// |
||||
|
// 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 |
||||
|
* @{ |
||||
|
* |
||||
|
* <div id="github" feature="photos"></div> |
||||
|
* |
||||
|
* 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. |
||||
|
* |
||||
|
* <h2>Adding the Photos Feature to Your Application</h2> |
||||
|
* |
||||
|
* 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 <code>src/photos/resources/NimbusPhotos.bundle</code>. |
||||
|
* |
||||
|
* |
||||
|
* <h2>Feature Breakdown</h2> |
||||
|
* |
||||
|
* 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. |
||||
|
* |
||||
|
* |
||||
|
* <h2>Architecture</h2> |
||||
|
* |
||||
|
* 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. |
||||
|
* |
||||
|
* <h3>NIPhotoAlbumScrollView</h3> |
||||
|
* |
||||
|
* 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. |
||||
|
* |
||||
|
* |
||||
|
* <h2>Example Applications</h2> |
||||
|
* |
||||
|
* <h3>Network Photo Albums</h3> |
||||
|
* |
||||
|
* <a href="https://github.com/jverkoey/nimbus/tree/master/examples/photos/NetworkPhotoAlbums">View the README on GitHub</a> |
||||
|
* |
||||
|
* 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: <code>examples/photos/NetworkPhotoAlbums</code> |
||||
|
* |
||||
|
* |
||||
|
* <h2>Screenshots</h2> |
||||
|
* |
||||
|
* @image html photos-iphone-example1.png "Screenshot of a basic photo album on the iPhone." |
||||
|
* |
||||
|
* Image source: <a href="http://www.flickr.com/photos/janekm/360669001/">flickr.com/photos/janekm/360669001</a> |
||||
|
*/ |
||||
|
|
||||
|
/**@}*/ |
||||
|
|
||||
|
#import <Foundation/Foundation.h> |
||||
|
#import <UIKit/UIKit.h> |
||||
|
|
||||
|
#import "NIPhotoAlbumScrollView.h" |
||||
|
#import "NIPhotoAlbumScrollViewDataSource.h" |
||||
|
#import "NIPhotoAlbumScrollViewDelegate.h" |
||||
|
#import "NIPhotoScrollView.h" |
||||
|
#import "NIPhotoScrollViewDelegate.h" |
||||
|
#import "NIPhotoScrollViewPhotoSize.h" |
||||
|
#import "NIPhotoScrubberView.h" |
||||
|
#import "NIToolbarPhotoViewController.h" |
||||
|
|
||||
|
#import "NimbusPagingScrollView.h" |
||||
|
#import "NimbusCore.h" |
||||
Write
Preview
Loading…
Cancel
Save
Reference in new issue