-
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); |
||||
|
} |
||||
|
} |
||||
|
} |
||||