-
13Podfile
-
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
-
512Pods/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
-
21Pods/WebBrowser/LICENSE.md
-
90Pods/WebBrowser/README.md
-
44Pods/WebBrowser/WebBrowser/InternationalControl.swift
-
39Pods/WebBrowser/WebBrowser/NavigationBarAppearance.swift
-
14Pods/WebBrowser/WebBrowser/Resources/LocalizedStrings/en.lproj/WebBrowser.strings
-
14Pods/WebBrowser/WebBrowser/Resources/LocalizedStrings/ja.lproj/WebBrowser.strings
-
14Pods/WebBrowser/WebBrowser/Resources/LocalizedStrings/ko.lproj/WebBrowser.strings
-
14Pods/WebBrowser/WebBrowser/Resources/LocalizedStrings/zh-Hans.lproj/WebBrowser.strings
-
14Pods/WebBrowser/WebBrowser/Resources/LocalizedStrings/zh-Hant.lproj/WebBrowser.strings
-
6Pods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/Contents.json
-
23Pods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/backIcon.imageset/Contents.json
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/backIcon.imageset/backIcon.png
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/backIcon.imageset/backIcon@2x.png
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/backIcon.imageset/backIcon@3x.png
-
23Pods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/forwardIcon.imageset/Contents.json
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/forwardIcon.imageset/forwardIcon.png
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/forwardIcon.imageset/forwardIcon@2x.png
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/forwardIcon.imageset/forwardIcon@3x.png
-
33Pods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/safariIcon.imageset/Contents.json
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/safariIcon.imageset/safariIcon.png
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/safariIcon.imageset/safariIcon@2x.png
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/safariIcon.imageset/safariIcon@2x~iPad.png
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/safariIcon.imageset/safariIcon@3x.png
-
BINPods/WebBrowser/WebBrowser/Resources/WebBrowser.xcassets/safariIcon.imageset/safariIcon~iPad.png
-
49Pods/WebBrowser/WebBrowser/SafariActivity.swift
-
30Pods/WebBrowser/WebBrowser/ToolbarAppearance.swift
-
38Pods/WebBrowser/WebBrowser/WebBrowser.swift
-
46Pods/WebBrowser/WebBrowser/WebBrowserDelegate.swift
-
412Pods/WebBrowser/WebBrowser/WebBrowserViewController.swift
-
46kplayer.xcodeproj/project.pbxproj
-
44kplayer.xcodeproj/xcuserdata/marcoschmickler.xcuserdatad/xcschemes/kplayer.xcscheme
-
37kplayer/AppDelegate.swift
-
25kplayer/PlayerApp.swift
@ -1,202 +0,0 @@ |
|||
|
|||
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. |
|||
@ -1,13 +0,0 @@ |
|||
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). |
|||
@ -1,37 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,422 +0,0 @@ |
|||
// |
|||
// 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 ////////////////////////////////////////////////////////////////////////// |
|||
@ -1,247 +0,0 @@ |
|||
// |
|||
// 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]; |
|||
} |
|||
@ -1,92 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,83 +0,0 @@ |
|||
// |
|||
// 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]; |
|||
} |
|||
@ -1,172 +0,0 @@ |
|||
// |
|||
// 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 /////////////////////////////////////////////////////////////////// |
|||
@ -1,69 +0,0 @@ |
|||
// |
|||
// 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); |
|||
} |
|||
@ -1,194 +0,0 @@ |
|||
// |
|||
// 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 ////////////////////////////////////////////////////////////////// |
|||
@ -1,61 +0,0 @@ |
|||
// |
|||
// 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) |
|||
@ -1,92 +0,0 @@ |
|||
// |
|||
// 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 /////////////////////////////////////////////////////////////// |
|||
|
|||
@ -1,76 +0,0 @@ |
|||
// |
|||
// 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; |
|||
} |
|||
} |
|||
@ -1,54 +0,0 @@ |
|||
// |
|||
// 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 |
|||
*/ |
|||
@ -1,24 +0,0 @@ |
|||
// |
|||
// 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"; |
|||
@ -1,421 +0,0 @@ |
|||
// |
|||
// 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 /////////////////////////////////////////////////////////////// |
|||
@ -1,314 +0,0 @@ |
|||
// |
|||
// 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; |
|||
} |
|||
@ -1,48 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,24 +0,0 @@ |
|||
// |
|||
// 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]; |
|||
} |
|||
@ -1,316 +0,0 @@ |
|||
// |
|||
// 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 |
|||
*/ |
|||
@ -1,453 +0,0 @@ |
|||
// |
|||
// 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 |
|||
|
|||
@ -1,101 +0,0 @@ |
|||
// |
|||
// 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 ///////////////////////////////////////////////////////////////// |
|||
@ -1,171 +0,0 @@ |
|||
// |
|||
// 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) |
|||
@ -1,58 +0,0 @@ |
|||
// |
|||
// 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 ///////////////////////////////////////////////////// |
|||
@ -1,35 +0,0 @@ |
|||
// |
|||
// 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; |
|||
} |
|||
@ -1,65 +0,0 @@ |
|||
// |
|||
// 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 //////////////////////////////////////////////////////// |
|||
@ -1,36 +0,0 @@ |
|||
// |
|||
// 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); |
|||
} |
|||
|
|||
@ -1,20 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,209 +0,0 @@ |
|||
// |
|||
// 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 |
|||
*/ |
|||
@ -1,111 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,69 +0,0 @@ |
|||
// |
|||
// 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 //////////////////////////////////////////////////////////////////////////// |
|||
@ -1,61 +0,0 @@ |
|||
// |
|||
// 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]; |
|||
} |
|||
@ -1,141 +0,0 @@ |
|||
// |
|||
// 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 ////////////////////////////////////////////////////////////// |
|||
@ -1,74 +0,0 @@ |
|||
// |
|||
// 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 ////////////////////////////////////////////////////// |
|||
@ -1,37 +0,0 @@ |
|||
// |
|||
// 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); |
|||
} |
|||
@ -1,257 +0,0 @@ |
|||
// |
|||
// 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 ///////////////////////////////////////////////////////////////// |
|||
@ -1,72 +0,0 @@ |
|||
// |
|||
// 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]; |
|||
} |
|||
@ -1,224 +0,0 @@ |
|||
// |
|||
// 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: |
|||
*/ |
|||
@ -1,299 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,84 +0,0 @@ |
|||
// |
|||
// 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 //////////////////////////////////////////////////////////////////////////// |
|||
@ -1,58 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,164 +0,0 @@ |
|||
// |
|||
// 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 |
|||
*/ |
|||
@ -1,111 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,26 +0,0 @@ |
|||
// |
|||
// 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" |
|||
@ -1,120 +0,0 @@ |
|||
// |
|||
// 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" |
|||
@ -1,26 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,49 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,123 +0,0 @@ |
|||
// |
|||
// 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 |
|||
*/ |
|||
@ -1,386 +0,0 @@ |
|||
// |
|||
// 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 |
|||
*/ |
|||
@ -1,760 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,36 +0,0 @@ |
|||
// |
|||
// 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 |
|||
*/ |
|||
@ -1,26 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,53 +0,0 @@ |
|||
// |
|||
// 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" |
|||
@ -1,170 +0,0 @@ |
|||
// |
|||
// 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: |
|||
*/ |
|||
@ -1,221 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,93 +0,0 @@ |
|||
// |
|||
// 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 |
|||
|
|||
@ -1,55 +0,0 @@ |
|||
// |
|||
// 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 |
|||
|
|||
@ -1,162 +0,0 @@ |
|||
// |
|||
// 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 |
|||
*/ |
|||
@ -1,512 +0,0 @@ |
|||
// |
|||
// 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; |
|||
|
|||
_scrollView.scrollEnabled = true; |
|||
|
|||
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 |
|||
@ -1,41 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,31 +0,0 @@ |
|||
// |
|||
// 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; |
|||
@ -1,168 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,465 +0,0 @@ |
|||
// |
|||
// 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 / 0.6f); |
|||
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 / 0.3f); |
|||
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 |
|||
@ -1,201 +0,0 @@ |
|||
// |
|||
// 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 |
|||
*/ |
|||
@ -1,533 +0,0 @@ |
|||
// |
|||
// 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 |
|||
@ -1,122 +0,0 @@ |
|||
// |
|||
// 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" |
|||
@ -1,21 +0,0 @@ |
|||
The MIT License (MIT) |
|||
|
|||
Copyright (c) 2016 Teambition |
|||
|
|||
Permission is hereby granted, free of charge, to any person obtaining a copy |
|||
of this software and associated documentation files (the "Software"), to deal |
|||
in the Software without restriction, including without limitation the rights |
|||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
|||
copies of the Software, and to permit persons to whom the Software is |
|||
furnished to do so, subject to the following conditions: |
|||
|
|||
The above copyright notice and this permission notice shall be included in all |
|||
copies or substantial portions of the Software. |
|||
|
|||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
|||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
|||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
|||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
|||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
|||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
|||
SOFTWARE. |
|||
@ -1,90 +0,0 @@ |
|||
# WebBrowser |
|||
A web browser using WebKit and written in Swift for iOS apps. |
|||
|
|||
 |
|||
|
|||
## How To Get Started |
|||
### Carthage |
|||
Specify "WebBrowser" in your ```Cartfile```: |
|||
```ogdl |
|||
github "teambition/WebBrowser" |
|||
``` |
|||
|
|||
### CocoaPods |
|||
Specify "WebBrowser" in your ```Podfile```: |
|||
```ruby |
|||
source 'https://github.com/CocoaPods/Specs.git' |
|||
platform :ios, '8.0' |
|||
use_frameworks! |
|||
|
|||
pod 'WebBrowser' |
|||
``` |
|||
|
|||
### Usage |
|||
#### Initialization |
|||
```swift |
|||
let webBrowserViewController = WebBrowserViewController() |
|||
// assign delegate |
|||
webBrowserViewController.delegate = self |
|||
|
|||
webBrowserViewController.language = .english |
|||
webBrowserViewController.tintColor = ... |
|||
webBrowserViewController.barTintColor = ... |
|||
webBrowserViewController.isToolbarHidden = false |
|||
webBrowserViewController.isShowActionBarButton = true |
|||
webBrowserViewController.toolbarItemSpace = 50 |
|||
webBrowserViewController.isShowURLInNavigationBarWhenLoading = true |
|||
webBrowserViewController.isShowPageTitleInNavigationBar = true |
|||
webBrowserViewController.customApplicationActivities = ... |
|||
|
|||
webBrowserViewController.loadURLString("https://www.apple.com/cn/") |
|||
``` |
|||
|
|||
#### Pushing to the navigation stack |
|||
```swift |
|||
navigationController?.pushViewController(webBrowserViewController, animated: true) |
|||
``` |
|||
|
|||
#### Presenting modally |
|||
```swift |
|||
let navigationWebBrowser = WebBrowserViewController.rootNavigationWebBrowser(webBrowser: webBrowserViewController) |
|||
present(navigationWebBrowser, animated: true, completion: nil) |
|||
``` |
|||
|
|||
#### Implement the delegate |
|||
```swift |
|||
func webBrowser(_ webBrowser: WebBrowserViewController, didStartLoad url: URL?) { |
|||
// do something |
|||
} |
|||
|
|||
func webBrowser(_ webBrowser: WebBrowserViewController, didFinishLoad url: URL?) { |
|||
// do something |
|||
} |
|||
|
|||
func webBrowser(_ webBrowser: WebBrowserViewController, didFailLoad url: URL?, withError error: Error) { |
|||
// do something |
|||
} |
|||
|
|||
func webBrowserWillDismiss(_ webBrowser: WebBrowserViewController) { |
|||
// do something |
|||
} |
|||
|
|||
func webBrowserDidDismiss(_ webBrowser: WebBrowserViewController) { |
|||
// do something |
|||
} |
|||
``` |
|||
|
|||
## Minimum Requirement |
|||
iOS 8.0 |
|||
|
|||
## Localization |
|||
WebBrowser supports 5 languages: English, Simplified Chinese, Traditional Chinese, Korean, Japanese. You can set the language when initialization. |
|||
|
|||
## Release Notes |
|||
* [Release Notes](https://github.com/teambition/WebBrowser/releases) |
|||
|
|||
## License |
|||
WebBrowser is released under the MIT license. See [LICENSE](https://github.com/teambition/WebBrowser/blob/master/LICENSE.md) for details. |
|||
|
|||
## More Info |
|||
Have a question? Please [open an issue](https://github.com/teambition/WebBrowser/issues/new)! |
|||
@ -1,44 +0,0 @@ |
|||
// |
|||
// InternationalControl.swift |
|||
// WebBrowser |
|||
// |
|||
// Created by Xin Hong on 16/4/27. |
|||
// Copyright © 2016年 Teambition. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
|
|||
public enum WebBrowserLanguage { |
|||
case english |
|||
case simplifiedChinese |
|||
case traditionalChinese |
|||
case korean |
|||
case japanese |
|||
|
|||
internal var identifier: String { |
|||
switch self { |
|||
case .english: return "en" |
|||
case .simplifiedChinese: return "zh-Hans" |
|||
case .traditionalChinese: return "zh-Hant" |
|||
case .korean: return "ko" |
|||
case .japanese: return "ja" |
|||
} |
|||
} |
|||
} |
|||
|
|||
internal func LocalizedString(key: String, comment: String? = nil) -> String { |
|||
return InternationalControl.sharedControl.localizedString(key: key, comment: comment) |
|||
} |
|||
|
|||
internal struct InternationalControl { |
|||
internal static var sharedControl = InternationalControl() |
|||
internal var language: WebBrowserLanguage = .english |
|||
|
|||
internal func localizedString(key: String, comment: String? = nil) -> String { |
|||
guard let localizationPath = WebBrowser.localizationPath(forIdentifier: language.identifier) else { |
|||
return key |
|||
} |
|||
let bundle = Bundle(path: localizationPath) |
|||
return bundle?.localizedString(forKey: key, value: nil, table: "WebBrowser") ?? key |
|||
} |
|||
} |
|||
@ -1,39 +0,0 @@ |
|||
// |
|||
// NavigationBarAppearance.swift |
|||
// WebBrowser |
|||
// |
|||
// Created by Xin Hong on 16/4/30. |
|||
// Copyright © 2016年 Teambition. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
|
|||
internal struct NavigationBarAppearance { |
|||
var isHidden = false |
|||
var tintColor = UIColor.blue |
|||
var barTintColor: UIColor? |
|||
var isTranslucent = true |
|||
var shadowImage: UIImage? |
|||
var backgroundImageForBarMetricsDefault: UIImage? |
|||
var backgroundImageForBarMetricsCompact: UIImage? |
|||
|
|||
init() { } |
|||
|
|||
init(navigationBar: UINavigationBar) { |
|||
tintColor = navigationBar.tintColor |
|||
barTintColor = navigationBar.barTintColor |
|||
isTranslucent = navigationBar.isTranslucent |
|||
shadowImage = navigationBar.shadowImage |
|||
backgroundImageForBarMetricsDefault = navigationBar.backgroundImage(for: .default) |
|||
backgroundImageForBarMetricsCompact = navigationBar.backgroundImage(for: .compact) |
|||
} |
|||
|
|||
func apply(to navigationBar: UINavigationBar) { |
|||
navigationBar.tintColor = tintColor |
|||
navigationBar.barTintColor = barTintColor |
|||
navigationBar.isTranslucent = isTranslucent |
|||
navigationBar.shadowImage = shadowImage |
|||
navigationBar.setBackgroundImage(backgroundImageForBarMetricsDefault, for: .default) |
|||
navigationBar.setBackgroundImage(backgroundImageForBarMetricsCompact, for: .compact) |
|||
} |
|||
} |
|||
@ -1,14 +0,0 @@ |
|||
/* |
|||
WebBrowser.strings |
|||
WebBrowser |
|||
|
|||
Created by Xin Hong on 16/4/27. |
|||
Copyright © 2016年 Teambition. All rights reserved. |
|||
*/ |
|||
|
|||
"Done" = "Done"; |
|||
"Cancel" = "Cancel"; |
|||
"Open" = "Open"; |
|||
"OpenExternalAppAlert.title" = "Leave this app?"; |
|||
"OpenExternalAppAlert.message" = "This web page is trying to open an outside app. Are you sure to open it?"; |
|||
"Open in Safari" = "Open in Safari"; |
|||
@ -1,14 +0,0 @@ |
|||
/* |
|||
WebBrowser.strings |
|||
WebBrowser |
|||
|
|||
Created by Xin Hong on 16/4/27. |
|||
Copyright © 2016年 Teambition. All rights reserved. |
|||
*/ |
|||
|
|||
"Done" = "完了"; |
|||
"Cancel" = "キャンセル"; |
|||
"Open" = "開く"; |
|||
"OpenExternalAppAlert.title" = "離れるこの App?"; |
|||
"OpenExternalAppAlert.message" = "このページこのページしようとして開くもうひとつApp, 確定開く?"; |
|||
"Open in Safari" = "Safariで開く"; |
|||
@ -1,14 +0,0 @@ |
|||
/* |
|||
WebBrowser.strings |
|||
WebBrowser |
|||
|
|||
Created by Xin Hong on 16/4/27. |
|||
Copyright © 2016年 Teambition. All rights reserved. |
|||
*/ |
|||
|
|||
"Done" = "완료"; |
|||
"Cancel" = "취소"; |
|||
"Open" = "열기"; |
|||
"OpenExternalAppAlert.title" = "떠나다이 App?"; |
|||
"OpenExternalAppAlert.message" = "이 페이지 애쓰고 있다 열기 다른 App, 확정 열기?"; |
|||
"Open in Safari" = "Safari로 열기"; |
|||
@ -1,14 +0,0 @@ |
|||
/* |
|||
WebBrowser.strings |
|||
WebBrowser |
|||
|
|||
Created by Xin Hong on 16/4/27. |
|||
Copyright © 2016年 Teambition. All rights reserved. |
|||
*/ |
|||
|
|||
"Done" = "完成"; |
|||
"Cancel" = "取消"; |
|||
"Open" = "打开"; |
|||
"OpenExternalAppAlert.title" = "离开此应用?"; |
|||
"OpenExternalAppAlert.message" = "此页面正试图打开另一个应用,确定要打开吗?"; |
|||
"Open in Safari" = "在 Safari 中打开"; |
|||
@ -1,14 +0,0 @@ |
|||
/* |
|||
WebBrowser.strings |
|||
WebBrowser |
|||
|
|||
Created by Xin Hong on 16/4/27. |
|||
Copyright © 2016年 Teambition. All rights reserved. |
|||
*/ |
|||
|
|||
"Done" = "完成"; |
|||
"Cancel" = "取消"; |
|||
"Open" = "打開"; |
|||
"OpenExternalAppAlert.title" = "離開此App?"; |
|||
"OpenExternalAppAlert.message" = "此頁面正試圖打開另一App,確定要打開嗎?"; |
|||
"Open in Safari" = "在 Safari 中打開"; |
|||
@ -1,6 +0,0 @@ |
|||
{ |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
|||
@ -1,23 +0,0 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "backIcon.png", |
|||
"scale" : "1x" |
|||
}, |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "backIcon@2x.png", |
|||
"scale" : "2x" |
|||
}, |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "backIcon@3x.png", |
|||
"scale" : "3x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
|||
|
Before Width: 22 | Height: 22 | Size: 1.2 KiB |
|
Before Width: 44 | Height: 44 | Size: 1.6 KiB |
|
Before Width: 66 | Height: 66 | Size: 1.9 KiB |
@ -1,23 +0,0 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "forwardIcon.png", |
|||
"scale" : "1x" |
|||
}, |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "forwardIcon@2x.png", |
|||
"scale" : "2x" |
|||
}, |
|||
{ |
|||
"idiom" : "universal", |
|||
"filename" : "forwardIcon@3x.png", |
|||
"scale" : "3x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
|||
|
Before Width: 22 | Height: 22 | Size: 1.3 KiB |
|
Before Width: 44 | Height: 44 | Size: 1.6 KiB |
|
Before Width: 66 | Height: 66 | Size: 1.9 KiB |
@ -1,33 +0,0 @@ |
|||
{ |
|||
"images" : [ |
|||
{ |
|||
"idiom" : "iphone", |
|||
"filename" : "safariIcon.png", |
|||
"scale" : "1x" |
|||
}, |
|||
{ |
|||
"idiom" : "iphone", |
|||
"filename" : "safariIcon@2x.png", |
|||
"scale" : "2x" |
|||
}, |
|||
{ |
|||
"idiom" : "iphone", |
|||
"filename" : "safariIcon@3x.png", |
|||
"scale" : "3x" |
|||
}, |
|||
{ |
|||
"idiom" : "ipad", |
|||
"filename" : "safariIcon~iPad.png", |
|||
"scale" : "1x" |
|||
}, |
|||
{ |
|||
"idiom" : "ipad", |
|||
"filename" : "safariIcon@2x~iPad.png", |
|||
"scale" : "2x" |
|||
} |
|||
], |
|||
"info" : { |
|||
"version" : 1, |
|||
"author" : "xcode" |
|||
} |
|||
} |
|||
|
Before Width: 60 | Height: 60 | Size: 1.5 KiB |
|
Before Width: 120 | Height: 120 | Size: 3.6 KiB |
|
Before Width: 152 | Height: 152 | Size: 4.9 KiB |
|
Before Width: 180 | Height: 180 | Size: 6.0 KiB |
|
Before Width: 76 | Height: 76 | Size: 2.1 KiB |
@ -1,49 +0,0 @@ |
|||
// |
|||
// SafariActivity.swift |
|||
// WebBrowser |
|||
// |
|||
// Created by Xin Hong on 16/4/27. |
|||
// Copyright © 2016年 Teambition. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
|
|||
open class SafariActivity: UIActivity { |
|||
open var url: URL? |
|||
|
|||
open override var activityType: UIActivity.ActivityType? { |
|||
return ActivityType(String(describing: self)) |
|||
} |
|||
|
|||
open override var activityTitle : String? { |
|||
return LocalizedString(key: "Open in Safari") |
|||
} |
|||
|
|||
open override var activityImage : UIImage? { |
|||
return WebBrowser.image(named: "safariIcon") |
|||
} |
|||
|
|||
open override func canPerform(withActivityItems activityItems: [Any]) -> Bool { |
|||
for activityItem in activityItems { |
|||
if let activityURL = activityItem as? URL { |
|||
return UIApplication.shared.canOpenURL(activityURL) |
|||
} |
|||
} |
|||
return false |
|||
} |
|||
|
|||
open override func prepare(withActivityItems activityItems: [Any]) { |
|||
for activityItem in activityItems { |
|||
if let activityURL = activityItem as? URL { |
|||
url = activityURL |
|||
} |
|||
} |
|||
} |
|||
|
|||
open override func perform() { |
|||
if let url = url { |
|||
let completed = UIApplication.shared.openURL(url) |
|||
activityDidFinish(completed) |
|||
} |
|||
} |
|||
} |
|||
@ -1,30 +0,0 @@ |
|||
// |
|||
// ToolbarAppearance.swift |
|||
// WebBrowser |
|||
// |
|||
// Created by Xin Hong on 16/4/30. |
|||
// Copyright © 2016年 Teambition. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
|
|||
internal struct ToolbarAppearance { |
|||
var isHidden = true |
|||
var tintColor = UIColor.blue |
|||
var barTintColor: UIColor? |
|||
var isTranslucent = true |
|||
|
|||
init() { } |
|||
|
|||
init(toolbar: UIToolbar) { |
|||
tintColor = toolbar.tintColor |
|||
barTintColor = toolbar.barTintColor |
|||
isTranslucent = toolbar.isTranslucent |
|||
} |
|||
|
|||
func apply(to toolbar: UIToolbar) { |
|||
toolbar.tintColor = tintColor |
|||
toolbar.barTintColor = barTintColor |
|||
toolbar.isTranslucent = isTranslucent |
|||
} |
|||
} |
|||
@ -1,38 +0,0 @@ |
|||
// |
|||
// WebBrowser.swift |
|||
// WebBrowser |
|||
// |
|||
// Created by Xin Hong on 16/4/27. |
|||
// Copyright © 2016年 Teambition. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
|
|||
internal struct WebBrowser { |
|||
static let estimatedProgressKeyPath = "estimatedProgress" |
|||
static var estimatedProgressContext = 0 |
|||
static let defaultToolbarItemSpace: CGFloat = 50 |
|||
|
|||
static var resourceBundleURL: URL? { |
|||
let resourceBundleURL = Bundle(for: WebBrowserViewController.self).url(forResource: "WebBrowser", withExtension: "bundle") |
|||
return resourceBundleURL |
|||
} |
|||
|
|||
static func localizationPath(forIdentifier identifier: String) -> String? { |
|||
if let path = Bundle(identifier: "Teambition.WebBrowser")?.path(forResource: identifier, ofType: "lproj") { |
|||
return path |
|||
} else if let resourceBundleURL = resourceBundleURL, let resourceBundle = Bundle(url: resourceBundleURL) { |
|||
return resourceBundle.path(forResource: identifier, ofType: "lproj") |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
static func image(named name: String) -> UIImage? { |
|||
if let image = UIImage(named: name, in: Bundle(for: WebBrowserViewController.self), compatibleWith: nil) { |
|||
return image |
|||
} else if let resourceBundleURL = resourceBundleURL, let resourceBundle = Bundle(url: resourceBundleURL) { |
|||
return UIImage(named: name, in: resourceBundle, compatibleWith: nil) |
|||
} |
|||
return nil |
|||
} |
|||
} |
|||
@ -1,46 +0,0 @@ |
|||
// |
|||
// WebBrowserDelegate.swift |
|||
// WebBrowser |
|||
// |
|||
// Created by Xin Hong on 16/4/26. |
|||
// Copyright © 2016年 Teambition. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
import WebKit |
|||
|
|||
public protocol WebBrowserDelegate: class { |
|||
func webBrowser(_ webBrowser: WebBrowserViewController, didStartLoad url: URL?) |
|||
func webBrowser(_ webBrowser: WebBrowserViewController, didFinishLoad url: URL?) |
|||
func webBrowser(_ webBrowser: WebBrowserViewController, didFailLoad url: URL?, withError error: Error) |
|||
|
|||
func webBrowserWillDismiss(_ webBrowser: WebBrowserViewController) |
|||
func webBrowserDidDismiss(_ webBrowser: WebBrowserViewController) |
|||
func webBrowser(_ webBrowser: WebBrowserViewController, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) -> Bool |
|||
} |
|||
|
|||
public extension WebBrowserDelegate { |
|||
func webBrowser(_ webBrowser: WebBrowserViewController, didStartLoad url: URL?) { |
|||
|
|||
} |
|||
|
|||
func webBrowser(_ webBrowser: WebBrowserViewController, didFinishLoad url: URL?) { |
|||
|
|||
} |
|||
|
|||
func webBrowser(_ webBrowser: WebBrowserViewController, didFailLoad url: URL?, withError error: Error) { |
|||
|
|||
} |
|||
|
|||
func webBrowserWillDismiss(_ webBrowser: WebBrowserViewController) { |
|||
|
|||
} |
|||
|
|||
func webBrowserDidDismiss(_ webBrowser: WebBrowserViewController) { |
|||
|
|||
} |
|||
|
|||
func webBrowser(_ webBrowser: WebBrowserViewController, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) -> Bool { |
|||
return false |
|||
} |
|||
} |
|||
@ -1,412 +0,0 @@ |
|||
// |
|||
// WebBrowserViewController.swift |
|||
// WebBrowser |
|||
// |
|||
// Created by Xin Hong on 16/4/26. |
|||
// Copyright © 2016年 Teambition. All rights reserved. |
|||
// |
|||
|
|||
import UIKit |
|||
import WebKit |
|||
|
|||
open class WebBrowserViewController: UIViewController { |
|||
open weak var delegate: WebBrowserDelegate? |
|||
open var language: WebBrowserLanguage = .english { |
|||
didSet { |
|||
InternationalControl.sharedControl.language = language |
|||
} |
|||
} |
|||
open var tintColor = UIColor.blue { |
|||
didSet { |
|||
updateTintColor() |
|||
} |
|||
} |
|||
open var barTintColor: UIColor? { |
|||
didSet { |
|||
updateBarTintColor() |
|||
} |
|||
} |
|||
open var isToolbarHidden = false { |
|||
didSet { |
|||
navigationController?.setToolbarHidden(isToolbarHidden, animated: true) |
|||
} |
|||
} |
|||
open var toolbarItemSpace = WebBrowser.defaultToolbarItemSpace { |
|||
didSet { |
|||
itemFixedSeparator.width = toolbarItemSpace |
|||
} |
|||
} |
|||
open var isShowActionBarButton = true { |
|||
didSet { |
|||
updateToolBarState() |
|||
} |
|||
} |
|||
open var customApplicationActivities = [UIActivity]() |
|||
open var isShowURLInNavigationBarWhenLoading = true |
|||
open var isShowPageTitleInNavigationBar = true |
|||
|
|||
fileprivate var webView = WKWebView(frame: CGRect.zero) |
|||
|
|||
public func getWKWebView() -> WKWebView { |
|||
return webView |
|||
} |
|||
|
|||
fileprivate lazy var progressView: UIProgressView = { |
|||
let progressView = UIProgressView(progressViewStyle: .default) |
|||
progressView.trackTintColor = .clear |
|||
progressView.tintColor = self.tintColor |
|||
return progressView |
|||
}() |
|||
fileprivate var previousNavigationControllerNavigationBarAppearance = NavigationBarAppearance() |
|||
fileprivate var previousNavigationControllerToolbarAppearance = ToolbarAppearance() |
|||
|
|||
fileprivate lazy var refreshButton: UIBarButtonItem = { |
|||
let refreshButton = UIBarButtonItem(barButtonSystemItem: .refresh, target: self, action: #selector(WebBrowserViewController.refreshButtonTapped(_:))) |
|||
return refreshButton |
|||
}() |
|||
fileprivate lazy var stopButton: UIBarButtonItem = { |
|||
let stopButton = UIBarButtonItem(barButtonSystemItem: .stop, target: self, action: #selector(WebBrowserViewController.stopButtonTapped(_:))) |
|||
return stopButton |
|||
}() |
|||
fileprivate lazy var backButton: UIBarButtonItem = { |
|||
let backIcon = WebBrowser.image(named: "backIcon") |
|||
let backButton = UIBarButtonItem(image: backIcon, style: .plain, target: self, action: #selector(WebBrowserViewController.backButtonTapped(_:))) |
|||
return backButton |
|||
}() |
|||
fileprivate lazy var forwardButton: UIBarButtonItem = { |
|||
let forwardIcon = WebBrowser.image(named: "forwardIcon") |
|||
let forwardButton = UIBarButtonItem(image: forwardIcon, style: .plain, target: self, action: #selector(WebBrowserViewController.forwardButtonTapped(_:))) |
|||
return forwardButton |
|||
}() |
|||
fileprivate lazy var actionButton: UIBarButtonItem = { |
|||
let actionButton = UIBarButtonItem(barButtonSystemItem: .action, target: self, action: #selector(WebBrowserViewController.actionButtonTapped(_:))) |
|||
return actionButton |
|||
}() |
|||
fileprivate lazy var itemFixedSeparator: UIBarButtonItem = { |
|||
let itemFixedSeparator = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil) |
|||
itemFixedSeparator.width = self.toolbarItemSpace |
|||
return itemFixedSeparator |
|||
}() |
|||
fileprivate lazy var itemFlexibleSeparator: UIBarButtonItem = { |
|||
let itemFlexibleSeparator = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) |
|||
return itemFlexibleSeparator |
|||
}() |
|||
|
|||
public var onOpenExternalAppHandler: ((_ isOpen: Bool) -> Void)? |
|||
|
|||
// MARK: - Life cycle |
|||
open override func viewDidLoad() { |
|||
super.viewDidLoad() |
|||
|
|||
savePreviousNavigationControllerState() |
|||
configureWebView() |
|||
configureProgressView() |
|||
} |
|||
|
|||
open override func viewWillAppear(_ animated: Bool) { |
|||
super.viewWillAppear(animated) |
|||
navigationController?.setNavigationBarHidden(false, animated: true) |
|||
navigationController?.navigationBar.setBackgroundImage(nil, for: .default) |
|||
navigationController?.navigationBar.shadowImage = nil |
|||
navigationController?.navigationBar.isTranslucent = true |
|||
navigationController?.navigationBar.addSubview(progressView) |
|||
navigationController?.setToolbarHidden(isToolbarHidden, animated: true) |
|||
|
|||
progressView.alpha = 0 |
|||
updateTintColor() |
|||
updateBarTintColor() |
|||
updateToolBarState() |
|||
} |
|||
|
|||
open override func viewWillDisappear(_ animated: Bool) { |
|||
super.viewWillDisappear(animated) |
|||
restorePreviousNavigationControllerState(animated: animated) |
|||
progressView.removeFromSuperview() |
|||
} |
|||
|
|||
public convenience init(configuration: WKWebViewConfiguration) { |
|||
self.init() |
|||
webView = WKWebView(frame: CGRect.zero, configuration: configuration) |
|||
} |
|||
|
|||
open class func rootNavigationWebBrowser(webBrowser: WebBrowserViewController) -> UINavigationController { |
|||
webBrowser.navigationItem.rightBarButtonItem = UIBarButtonItem(title: LocalizedString(key: "Done"), style: .done, target: webBrowser, action: #selector(WebBrowserViewController.doneButtonTapped(_:))) |
|||
let navigationController = UINavigationController(rootViewController: webBrowser) |
|||
return navigationController |
|||
} |
|||
|
|||
deinit { |
|||
webView.uiDelegate = nil |
|||
webView.navigationDelegate = nil |
|||
if isViewLoaded { |
|||
webView.removeObserver(self, forKeyPath: WebBrowser.estimatedProgressKeyPath) |
|||
} |
|||
} |
|||
|
|||
// MARK: - Public |
|||
open func loadRequest(_ request: URLRequest) { |
|||
webView.load(request) |
|||
} |
|||
|
|||
open func loadURL(_ url: URL) { |
|||
webView.load(URLRequest(url: url)) |
|||
} |
|||
|
|||
open func loadURLString(_ urlString: String) { |
|||
guard let url = URL(string: urlString) else { |
|||
return |
|||
} |
|||
webView.load(URLRequest(url: url)) |
|||
} |
|||
|
|||
open func loadHTMLString(_ htmlString: String, baseURL: URL?) { |
|||
webView.loadHTMLString(htmlString, baseURL: baseURL) |
|||
} |
|||
} |
|||
|
|||
extension WebBrowserViewController { |
|||
// MARK: - Helper |
|||
fileprivate func configureWebView() { |
|||
webView.frame = view.bounds |
|||
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] |
|||
webView.autoresizesSubviews = true |
|||
webView.navigationDelegate = self |
|||
webView.uiDelegate = self |
|||
webView.isMultipleTouchEnabled = true |
|||
webView.scrollView.alwaysBounceVertical = true |
|||
view.addSubview(webView) |
|||
|
|||
webView.addObserver(self, forKeyPath: WebBrowser.estimatedProgressKeyPath, options: .new, context: &WebBrowser.estimatedProgressContext) |
|||
} |
|||
|
|||
fileprivate func configureProgressView() { |
|||
let yPosition: CGFloat = { |
|||
guard let navigationBar = self.navigationController?.navigationBar else { |
|||
return 0 |
|||
} |
|||
return navigationBar.frame.height - self.progressView.frame.height |
|||
}() |
|||
progressView.frame = CGRect(x: 0, y: yPosition, width: view.frame.width, height: progressView.frame.width) |
|||
progressView.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] |
|||
} |
|||
|
|||
fileprivate func savePreviousNavigationControllerState() { |
|||
guard let navigationController = navigationController else { |
|||
return |
|||
} |
|||
|
|||
var navigationBarAppearance = NavigationBarAppearance(navigationBar: navigationController.navigationBar) |
|||
navigationBarAppearance.isHidden = navigationController.isNavigationBarHidden |
|||
previousNavigationControllerNavigationBarAppearance = navigationBarAppearance |
|||
|
|||
var toolbarAppearance = ToolbarAppearance(toolbar: navigationController.toolbar) |
|||
toolbarAppearance.isHidden = navigationController.isToolbarHidden |
|||
previousNavigationControllerToolbarAppearance = toolbarAppearance |
|||
} |
|||
|
|||
fileprivate func restorePreviousNavigationControllerState(animated: Bool) { |
|||
guard let navigationController = navigationController else { |
|||
return |
|||
} |
|||
|
|||
navigationController.setNavigationBarHidden(previousNavigationControllerNavigationBarAppearance.isHidden, animated: animated) |
|||
navigationController.setToolbarHidden(previousNavigationControllerToolbarAppearance.isHidden, animated: animated) |
|||
|
|||
previousNavigationControllerNavigationBarAppearance.apply(to: navigationController.navigationBar) |
|||
previousNavigationControllerToolbarAppearance.apply(to: navigationController.toolbar) |
|||
} |
|||
|
|||
fileprivate func updateTintColor() { |
|||
progressView.tintColor = tintColor |
|||
navigationController?.navigationBar.tintColor = tintColor |
|||
navigationController?.toolbar.tintColor = tintColor |
|||
} |
|||
|
|||
fileprivate func updateBarTintColor() { |
|||
navigationController?.navigationBar.barTintColor = barTintColor |
|||
navigationController?.toolbar.barTintColor = barTintColor |
|||
} |
|||
} |
|||
|
|||
extension WebBrowserViewController { |
|||
// MARK: - UIBarButtonItem actions |
|||
@objc func refreshButtonTapped(_ sender: UIBarButtonItem) { |
|||
webView.stopLoading() |
|||
webView.reload() |
|||
} |
|||
|
|||
@objc func stopButtonTapped(_ sender: UIBarButtonItem) { |
|||
webView.stopLoading() |
|||
} |
|||
|
|||
@objc func backButtonTapped(_ sender: UIBarButtonItem) { |
|||
webView.goBack() |
|||
updateToolBarState() |
|||
} |
|||
|
|||
@objc func forwardButtonTapped(_ sender: UIBarButtonItem) { |
|||
webView.goForward() |
|||
updateToolBarState() |
|||
} |
|||
|
|||
@objc func actionButtonTapped(_ sender: UIBarButtonItem) { |
|||
DispatchQueue.main.async { |
|||
var activityItems = [Any]() |
|||
if let url = self.webView.url { |
|||
activityItems.append(url) |
|||
} |
|||
var applicationActivities = [UIActivity]() |
|||
applicationActivities.append(SafariActivity()) |
|||
applicationActivities.append(contentsOf: self.customApplicationActivities) |
|||
|
|||
let activityViewController = UIActivityViewController(activityItems: activityItems, applicationActivities: applicationActivities) |
|||
activityViewController.view.tintColor = self.tintColor |
|||
|
|||
if UIDevice.current.userInterfaceIdiom == .pad { |
|||
activityViewController.popoverPresentationController?.barButtonItem = sender |
|||
activityViewController.popoverPresentationController?.permittedArrowDirections = .any |
|||
self.present(activityViewController, animated: true, completion: nil) |
|||
} else { |
|||
self.present(activityViewController, animated: true, completion: nil) |
|||
} |
|||
} |
|||
} |
|||
|
|||
@objc func doneButtonTapped(_ sender: UIBarButtonItem) { |
|||
delegate?.webBrowserWillDismiss(self) |
|||
dismiss(animated: true) { |
|||
self.delegate?.webBrowserDidDismiss(self) |
|||
} |
|||
} |
|||
} |
|||
|
|||
extension WebBrowserViewController { |
|||
// MARK: - Tool bar |
|||
public func updateToolBarState() { |
|||
backButton.isEnabled = webView.canGoBack |
|||
forwardButton.isEnabled = webView.canGoForward |
|||
|
|||
var barButtonItems = [UIBarButtonItem]() |
|||
if webView.isLoading { |
|||
barButtonItems = [backButton, itemFixedSeparator, forwardButton, itemFixedSeparator, stopButton, itemFlexibleSeparator] |
|||
if let urlString = webView.url?.absoluteString, isShowURLInNavigationBarWhenLoading { |
|||
var titleString = urlString.replacingOccurrences(of: "http://", with: "", options: .literal, range: nil) |
|||
titleString = titleString.replacingOccurrences(of: "https://", with: "", options: .literal, range: nil) |
|||
navigationItem.title = titleString |
|||
} |
|||
} else { |
|||
barButtonItems = [backButton, itemFixedSeparator, forwardButton, itemFixedSeparator, refreshButton, itemFlexibleSeparator] |
|||
if isShowPageTitleInNavigationBar { |
|||
navigationItem.title = webView.title |
|||
} |
|||
} |
|||
|
|||
if isShowActionBarButton { |
|||
barButtonItems.append(actionButton) |
|||
} |
|||
|
|||
setToolbarItems(barButtonItems, animated: true) |
|||
} |
|||
} |
|||
|
|||
extension WebBrowserViewController { |
|||
// MARK: - External app support |
|||
fileprivate func externalAppRequiredToOpen(_ url: URL) -> Bool { |
|||
let validSchemes: Set<String> = ["http", "https"] |
|||
if let urlScheme = url.scheme { |
|||
return !validSchemes.contains(urlScheme) |
|||
} else { |
|||
return false |
|||
} |
|||
} |
|||
|
|||
fileprivate func openExternalApp(with url: URL) { |
|||
let externalAppPermissionAlert = UIAlertController(title: LocalizedString(key: "OpenExternalAppAlert.title"), message: LocalizedString(key: "OpenExternalAppAlert.message"), preferredStyle: .alert) |
|||
let cancelAction = UIAlertAction(title: LocalizedString(key: "Cancel"), style: .cancel, handler: { [weak self] (action) in |
|||
self?.onOpenExternalAppHandler?(false) |
|||
}) |
|||
let openAction = UIAlertAction(title: LocalizedString(key: "Open"), style: .default) { [weak self] (action) in |
|||
UIApplication.shared.openURL(url) |
|||
self?.onOpenExternalAppHandler?(true) |
|||
} |
|||
externalAppPermissionAlert.addAction(cancelAction) |
|||
externalAppPermissionAlert.addAction(openAction) |
|||
present(externalAppPermissionAlert, animated: true, completion: nil) |
|||
} |
|||
} |
|||
|
|||
extension WebBrowserViewController { |
|||
// MARK: - Observer |
|||
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { |
|||
if let keyPath = keyPath, (keyPath == WebBrowser.estimatedProgressKeyPath && context == &WebBrowser.estimatedProgressContext) { |
|||
progressView.alpha = 1 |
|||
let animated = webView.estimatedProgress > Double(progressView.progress) |
|||
progressView.setProgress(Float(webView.estimatedProgress), animated: animated) |
|||
|
|||
if webView.estimatedProgress >= 1 { |
|||
UIView.animate(withDuration: 0.3, delay: 0.3, options: .curveEaseOut, animations: { |
|||
self.progressView.alpha = 0 |
|||
}, completion: { (finished) in |
|||
self.progressView.progress = 0 |
|||
}) |
|||
} |
|||
} else { |
|||
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context) |
|||
} |
|||
} |
|||
} |
|||
|
|||
extension WebBrowserViewController: WKNavigationDelegate { |
|||
// MARK: - WKNavigationDelegate |
|||
public func webView(_ webView: WKWebView, didStartProvisionalNavigation navigation: WKNavigation!) { |
|||
updateToolBarState() |
|||
delegate?.webBrowser(self, didStartLoad: webView.url) |
|||
} |
|||
|
|||
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { |
|||
updateToolBarState() |
|||
delegate?.webBrowser(self, didFinishLoad: webView.url) |
|||
} |
|||
|
|||
public func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { |
|||
updateToolBarState() |
|||
delegate?.webBrowser(self, didFailLoad: webView.url, withError: error) |
|||
} |
|||
|
|||
public func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { |
|||
updateToolBarState() |
|||
delegate?.webBrowser(self, didFailLoad: webView.url, withError: error) |
|||
} |
|||
|
|||
|
|||
public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { |
|||
if let oriDelegate = delegate, oriDelegate.webBrowser(self, decidePolicyFor: navigationAction, decisionHandler: decisionHandler) { |
|||
return |
|||
} |
|||
if let url = navigationAction.request.url { |
|||
if !externalAppRequiredToOpen(url) { |
|||
if navigationAction.targetFrame == nil { |
|||
loadURL(url) |
|||
decisionHandler(.cancel) |
|||
return |
|||
} |
|||
} else if UIApplication.shared.canOpenURL(url) { |
|||
openExternalApp(with: url) |
|||
decisionHandler(.cancel) |
|||
return |
|||
} |
|||
} |
|||
|
|||
decisionHandler(.allow) |
|||
} |
|||
} |
|||
|
|||
extension WebBrowserViewController: WKUIDelegate { |
|||
// MARK: - WKUIDelegate |
|||
public func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { |
|||
if let mainFrame = navigationAction.targetFrame?.isMainFrame, mainFrame == false { |
|||
webView.load(navigationAction.request) |
|||
} |
|||
return nil |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
// |
|||
// Created by Marco Schmickler on 20.08.23. |
|||
// Copyright (c) 2023 Marco Schmickler. All rights reserved. |
|||
// |
|||
|
|||
import Foundation |
|||
import SwiftUI |
|||
|
|||
//@main |
|||
struct PlayerApp: App { |
|||
let model: MasterModel |
|||
|
|||
init() { |
|||
let del = NetworkDelegate() |
|||
model = MasterModel(itemModel: LocalManager.sharedInstance.model!, delegate: del, detailDelegate: del); |
|||
} |
|||
|
|||
var body: some Scene { |
|||
WindowGroup { |
|||
|
|||
MasterSplitView(completionHandler: { |
|||
}, model: model); |
|||
} |
|||
} |
|||
} |
|||