Initial commit
|
@ -0,0 +1,28 @@
|
|||
# file
|
||||
|
||||
#########################################################################
|
||||
# #
|
||||
# Title - .gitignore file #
|
||||
# For - iOS - Xcode7 #
|
||||
# Updated on - 01/Dicember/2016 #
|
||||
# #
|
||||
#########################################################################
|
||||
|
||||
############ Various settings
|
||||
xcuserdata/
|
||||
|
||||
############ CRYPTO CLOUD #################
|
||||
Crypto Cloud.xcodeproj
|
||||
iOSClient/CryptoCloud.xcassets
|
||||
iOSClient/Crypto Cloud.entitlements
|
||||
iOSClient/LaunchScreen.xib
|
||||
iOSClient/Networking/DB*.*
|
||||
iOSClient/InfoPlist/InfoCryptoCloud.plist
|
||||
Share Ext/Share Ext.entitlements
|
||||
|
||||
########### OSX ###########
|
||||
# OS X temporary files that should never be committed
|
||||
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
|
@ -0,0 +1 @@
|
|||
Marino Faggiana <m.faggiana@twsweb.it>
|
|
@ -0,0 +1,13 @@
|
|||
The Nextcloud iOS developers are aware that the terms of service that
|
||||
apply to apps distributed via Apple's App Store services may conflict
|
||||
with rights granted under the Nextcloud iOS license, the GNU General
|
||||
Public License, version 3 or (at your option) any later version. The
|
||||
copyright holders of the Nextcloud iOS app do not wish this conflict
|
||||
to prevent the otherwise-compliant distribution of derived apps via
|
||||
the App Store. Therefore, we have committed not to pursue any license
|
||||
violation that results solely from the conflict between the GNU GPLv3
|
||||
or any later version and the Apple App Store terms of service. In
|
||||
other words, as long as you comply with the GPL in all other respects,
|
||||
including its requirements to provide users with source code and the
|
||||
text of the license, we will not object to your distribution of the
|
||||
Nextcloud iOS app through the App Store.
|
|
@ -0,0 +1,15 @@
|
|||
2016–10-31 Marino Faggiana <m.faggiana@twsweb.it>
|
||||
|
||||
* Version 2.14 00015 released.
|
||||
|
||||
* New features:
|
||||
* Control if file exists in 'manual' upload. (Marino Faggiana)
|
||||
* Add main-menu item : Folders on top. (Marino Faggiana)
|
||||
|
||||
* Bug fixes:
|
||||
* Improved (create new class) Synchronization folders. (Marino Faggiana)
|
||||
* Improved 'Upload all camera photos/videos' bugfix if error block all, automatic default overwrite file if exists. (marino Faggiana)
|
||||
* Detect Wi-Fi on automatic upload, problem database context. (Marino Faggiana)
|
||||
* View on Control Center. (Marino Faggiana)
|
||||
|
||||
|
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
{one line to give the program's name and a brief idea of what it does.}
|
||||
Copyright (C) {year} {name of author}
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
{project} Copyright (C) {year} {fullname}
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
|
@ -0,0 +1,37 @@
|
|||
//
|
||||
// AESCrypt.h
|
||||
// Gurpartap Singh
|
||||
//
|
||||
// Created by Gurpartap Singh on 06/05/12.
|
||||
// Copyright (c) 2012 Gurpartap Singh
|
||||
//
|
||||
// MIT License
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface AESCrypt : NSObject
|
||||
|
||||
+ (NSString *)encrypt:(NSString *)message password:(NSString *)password;
|
||||
+ (NSString *)decrypt:(NSString *)base64EncodedString password:(NSString *)password;
|
||||
|
||||
@end
|
|
@ -0,0 +1,50 @@
|
|||
//
|
||||
// AESCrypt.m
|
||||
// Gurpartap Singh
|
||||
//
|
||||
// Created by Gurpartap Singh on 06/05/12.
|
||||
// Copyright (c) 2012 Gurpartap Singh
|
||||
//
|
||||
// MIT License
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import "AESCrypt.h"
|
||||
|
||||
#import "NSData+Base64.h"
|
||||
#import "NSString+Base64.h"
|
||||
#import "NSData+CommonCrypto.h"
|
||||
|
||||
@implementation AESCrypt
|
||||
|
||||
+ (NSString *)encrypt:(NSString *)message password:(NSString *)password {
|
||||
NSData *encryptedData = [[message dataUsingEncoding:NSUTF8StringEncoding] AES256EncryptedDataUsingKey:[[password dataUsingEncoding:NSUTF8StringEncoding] SHA256Hash] error:nil];
|
||||
NSString *base64EncodedString = [NSString base64StringFromData:encryptedData length:[encryptedData length]];
|
||||
return base64EncodedString;
|
||||
}
|
||||
|
||||
+ (NSString *)decrypt:(NSString *)base64EncodedString password:(NSString *)password {
|
||||
NSData *encryptedData = [NSData base64DataFromString:base64EncodedString];
|
||||
NSData *decryptedData = [encryptedData decryptedAES256DataUsingKey:[[password dataUsingEncoding:NSUTF8StringEncoding] SHA256Hash] error:nil];
|
||||
return [[NSString alloc] initWithData:decryptedData encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// NSData+Base64.m
|
||||
// Gurpartap Singh
|
||||
//
|
||||
// Created by Gurpartap Singh on 06/05/12.
|
||||
// Copyright (c) 2012 Gurpartap Singh. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@class NSString;
|
||||
|
||||
@interface NSData (Base64Additions)
|
||||
|
||||
+ (NSData *)base64DataFromString:(NSString *)string;
|
||||
|
||||
@end
|
|
@ -0,0 +1,110 @@
|
|||
//
|
||||
// NSData+Base64.h
|
||||
// Gurpartap Singh
|
||||
//
|
||||
// Created by Gurpartap Singh on 06/05/12.
|
||||
// Copyright (c) 2012 Gurpartap Singh. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSData+Base64.h"
|
||||
|
||||
@implementation NSData (Base64Additions)
|
||||
|
||||
+ (NSData *)base64DataFromString:(NSString *)string {
|
||||
unsigned long ixtext, lentext;
|
||||
unsigned char ch, inbuf[4], outbuf[3];
|
||||
short i, ixinbuf;
|
||||
Boolean flignore, flendtext = false;
|
||||
const unsigned char *tempcstring;
|
||||
NSMutableData *theData;
|
||||
|
||||
if (string == nil) {
|
||||
return [NSData data];
|
||||
}
|
||||
|
||||
ixtext = 0;
|
||||
|
||||
tempcstring = (const unsigned char *)[string UTF8String];
|
||||
|
||||
lentext = [string length];
|
||||
|
||||
theData = [NSMutableData dataWithCapacity: lentext];
|
||||
|
||||
ixinbuf = 0;
|
||||
|
||||
while (true) {
|
||||
if (ixtext >= lentext) {
|
||||
break;
|
||||
}
|
||||
|
||||
ch = tempcstring [ixtext++];
|
||||
|
||||
flignore = false;
|
||||
|
||||
if ((ch >= 'A') && (ch <= 'Z')) {
|
||||
ch = ch - 'A';
|
||||
}
|
||||
else if ((ch >= 'a') && (ch <= 'z')) {
|
||||
ch = ch - 'a' + 26;
|
||||
}
|
||||
else if ((ch >= '0') && (ch <= '9')) {
|
||||
ch = ch - '0' + 52;
|
||||
}
|
||||
else if (ch == '+') {
|
||||
ch = 62;
|
||||
}
|
||||
else if (ch == '=') {
|
||||
flendtext = true;
|
||||
}
|
||||
else if (ch == '/') {
|
||||
ch = 63;
|
||||
}
|
||||
else {
|
||||
flignore = true;
|
||||
}
|
||||
|
||||
if (!flignore) {
|
||||
short ctcharsinbuf = 3;
|
||||
Boolean flbreak = false;
|
||||
|
||||
if (flendtext) {
|
||||
if (ixinbuf == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if ((ixinbuf == 1) || (ixinbuf == 2)) {
|
||||
ctcharsinbuf = 1;
|
||||
}
|
||||
else {
|
||||
ctcharsinbuf = 2;
|
||||
}
|
||||
|
||||
ixinbuf = 3;
|
||||
|
||||
flbreak = true;
|
||||
}
|
||||
|
||||
inbuf [ixinbuf++] = ch;
|
||||
|
||||
if (ixinbuf == 4) {
|
||||
ixinbuf = 0;
|
||||
|
||||
outbuf[0] = (inbuf[0] << 2) | ((inbuf[1] & 0x30) >> 4);
|
||||
outbuf[1] = ((inbuf[1] & 0x0F) << 4) | ((inbuf[2] & 0x3C) >> 2);
|
||||
outbuf[2] = ((inbuf[2] & 0x03) << 6) | (inbuf[3] & 0x3F);
|
||||
|
||||
for (i = 0; i < ctcharsinbuf; i++) {
|
||||
[theData appendBytes: &outbuf[i] length: 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (flbreak) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return theData;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* NSData+CommonCrypto.h
|
||||
* AQToolkit
|
||||
*
|
||||
* Created by Jim Dovey on 31/8/2008.
|
||||
*
|
||||
* Copyright (c) 2008-2009, Jim Dovey
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* Neither the name of this project's author nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
|
||||
#import <Foundation/NSData.h>
|
||||
#import <Foundation/NSError.h>
|
||||
#import <CommonCrypto/CommonCryptor.h>
|
||||
#import <CommonCrypto/CommonHMAC.h>
|
||||
|
||||
extern NSString * const kCommonCryptoErrorDomain;
|
||||
|
||||
@interface NSError (CommonCryptoErrorDomain)
|
||||
+ (NSError *) errorWithCCCryptorStatus: (CCCryptorStatus) status;
|
||||
@end
|
||||
|
||||
@interface NSData (CommonDigest)
|
||||
|
||||
- (NSData *) MD2Sum;
|
||||
- (NSData *) MD4Sum;
|
||||
- (NSData *) MD5Sum;
|
||||
|
||||
- (NSData *) SHA1Hash;
|
||||
- (NSData *) SHA224Hash;
|
||||
- (NSData *) SHA256Hash;
|
||||
- (NSData *) SHA384Hash;
|
||||
- (NSData *) SHA512Hash;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSData (CommonCryptor)
|
||||
|
||||
- (NSData *) AES256EncryptedDataUsingKey: (id) key error: (NSError **) error;
|
||||
- (NSData *) decryptedAES256DataUsingKey: (id) key error: (NSError **) error;
|
||||
|
||||
- (NSData *) DESEncryptedDataUsingKey: (id) key error: (NSError **) error;
|
||||
- (NSData *) decryptedDESDataUsingKey: (id) key error: (NSError **) error;
|
||||
|
||||
- (NSData *) CASTEncryptedDataUsingKey: (id) key error: (NSError **) error;
|
||||
- (NSData *) decryptedCASTDataUsingKey: (id) key error: (NSError **) error;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSData (LowLevelCommonCryptor)
|
||||
|
||||
- (NSData *) dataEncryptedUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key // data or string
|
||||
error: (CCCryptorStatus *) error;
|
||||
- (NSData *) dataEncryptedUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key // data or string
|
||||
options: (CCOptions) options
|
||||
error: (CCCryptorStatus *) error;
|
||||
- (NSData *) dataEncryptedUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key // data or string
|
||||
initializationVector: (id) iv // data or string
|
||||
options: (CCOptions) options
|
||||
error: (CCCryptorStatus *) error;
|
||||
|
||||
- (NSData *) decryptedDataUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key // data or string
|
||||
error: (CCCryptorStatus *) error;
|
||||
- (NSData *) decryptedDataUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key // data or string
|
||||
options: (CCOptions) options
|
||||
error: (CCCryptorStatus *) error;
|
||||
- (NSData *) decryptedDataUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key // data or string
|
||||
initializationVector: (id) iv // data or string
|
||||
options: (CCOptions) options
|
||||
error: (CCCryptorStatus *) error;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSData (CommonHMAC)
|
||||
|
||||
- (NSData *) HMACWithAlgorithm: (CCHmacAlgorithm) algorithm;
|
||||
- (NSData *) HMACWithAlgorithm: (CCHmacAlgorithm) algorithm key: (id) key;
|
||||
|
||||
@end
|
|
@ -0,0 +1,546 @@
|
|||
/*
|
||||
* NSData+CommonCrypto.m
|
||||
* AQToolkit
|
||||
*
|
||||
* Created by Jim Dovey on 31/8/2008.
|
||||
*
|
||||
* Copyright (c) 2008-2009, Jim Dovey
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without
|
||||
* modification, are permitted provided that the following conditions
|
||||
* are met:
|
||||
*
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
* this list of conditions and the following disclaimer.
|
||||
*
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
* notice, this list of conditions and the following disclaimer in the
|
||||
* documentation and/or other materials provided with the distribution.
|
||||
*
|
||||
* Neither the name of this project's author nor the names of its
|
||||
* contributors may be used to endorse or promote products derived from
|
||||
* this software without specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
* HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
|
||||
* TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
||||
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
||||
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
||||
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
*
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "NSData+CommonCrypto.h"
|
||||
#import <CommonCrypto/CommonDigest.h>
|
||||
#import <CommonCrypto/CommonCryptor.h>
|
||||
#import <CommonCrypto/CommonHMAC.h>
|
||||
|
||||
NSString * const kCommonCryptoErrorDomain = @"CommonCryptoErrorDomain";
|
||||
|
||||
@implementation NSError (CommonCryptoErrorDomain)
|
||||
|
||||
+ (NSError *) errorWithCCCryptorStatus: (CCCryptorStatus) status
|
||||
{
|
||||
NSString * description = nil, * reason = nil;
|
||||
|
||||
switch ( status )
|
||||
{
|
||||
case kCCSuccess:
|
||||
description = NSLocalizedString(@"Success", @"Error description");
|
||||
break;
|
||||
|
||||
case kCCParamError:
|
||||
description = NSLocalizedString(@"Parameter Error", @"Error description");
|
||||
reason = NSLocalizedString(@"Illegal parameter supplied to encryption/decryption algorithm", @"Error reason");
|
||||
break;
|
||||
|
||||
case kCCBufferTooSmall:
|
||||
description = NSLocalizedString(@"Buffer Too Small", @"Error description");
|
||||
reason = NSLocalizedString(@"Insufficient buffer provided for specified operation", @"Error reason");
|
||||
break;
|
||||
|
||||
case kCCMemoryFailure:
|
||||
description = NSLocalizedString(@"Memory Failure", @"Error description");
|
||||
reason = NSLocalizedString(@"Failed to allocate memory", @"Error reason");
|
||||
break;
|
||||
|
||||
case kCCAlignmentError:
|
||||
description = NSLocalizedString(@"Alignment Error", @"Error description");
|
||||
reason = NSLocalizedString(@"Input size to encryption algorithm was not aligned correctly", @"Error reason");
|
||||
break;
|
||||
|
||||
case kCCDecodeError:
|
||||
description = NSLocalizedString(@"Decode Error", @"Error description");
|
||||
reason = NSLocalizedString(@"Input data did not decode or decrypt correctly", @"Error reason");
|
||||
break;
|
||||
|
||||
case kCCUnimplemented:
|
||||
description = NSLocalizedString(@"Unimplemented Function", @"Error description");
|
||||
reason = NSLocalizedString(@"Function not implemented for the current algorithm", @"Error reason");
|
||||
break;
|
||||
|
||||
default:
|
||||
description = NSLocalizedString(@"Unknown Error", @"Error description");
|
||||
break;
|
||||
}
|
||||
|
||||
NSMutableDictionary * userInfo = [[NSMutableDictionary alloc] init];
|
||||
[userInfo setObject: description forKey: NSLocalizedDescriptionKey];
|
||||
|
||||
if ( reason != nil )
|
||||
[userInfo setObject: reason forKey: NSLocalizedFailureReasonErrorKey];
|
||||
|
||||
NSError * result = [NSError errorWithDomain: kCommonCryptoErrorDomain code: status userInfo: userInfo];
|
||||
#if !__has_feature(objc_arc)
|
||||
[userInfo release];
|
||||
#endif
|
||||
|
||||
return ( result );
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
|
||||
@implementation NSData (CommonDigest)
|
||||
|
||||
- (NSData *) MD2Sum
|
||||
{
|
||||
unsigned char hash[CC_MD2_DIGEST_LENGTH];
|
||||
(void) CC_MD2( [self bytes], (CC_LONG)[self length], hash );
|
||||
return ( [NSData dataWithBytes: hash length: CC_MD2_DIGEST_LENGTH] );
|
||||
}
|
||||
|
||||
- (NSData *) MD4Sum
|
||||
{
|
||||
unsigned char hash[CC_MD4_DIGEST_LENGTH];
|
||||
(void) CC_MD4( [self bytes], (CC_LONG)[self length], hash );
|
||||
return ( [NSData dataWithBytes: hash length: CC_MD4_DIGEST_LENGTH] );
|
||||
}
|
||||
|
||||
- (NSData *) MD5Sum
|
||||
{
|
||||
unsigned char hash[CC_MD5_DIGEST_LENGTH];
|
||||
(void) CC_MD5( [self bytes], (CC_LONG)[self length], hash );
|
||||
return ( [NSData dataWithBytes: hash length: CC_MD5_DIGEST_LENGTH] );
|
||||
}
|
||||
|
||||
- (NSData *) SHA1Hash
|
||||
{
|
||||
unsigned char hash[CC_SHA1_DIGEST_LENGTH];
|
||||
(void) CC_SHA1( [self bytes], (CC_LONG)[self length], hash );
|
||||
return ( [NSData dataWithBytes: hash length: CC_SHA1_DIGEST_LENGTH] );
|
||||
}
|
||||
|
||||
- (NSData *) SHA224Hash
|
||||
{
|
||||
unsigned char hash[CC_SHA224_DIGEST_LENGTH];
|
||||
(void) CC_SHA224( [self bytes], (CC_LONG)[self length], hash );
|
||||
return ( [NSData dataWithBytes: hash length: CC_SHA224_DIGEST_LENGTH] );
|
||||
}
|
||||
|
||||
- (NSData *) SHA256Hash
|
||||
{
|
||||
unsigned char hash[CC_SHA256_DIGEST_LENGTH];
|
||||
(void) CC_SHA256( [self bytes], (CC_LONG)[self length], hash );
|
||||
return ( [NSData dataWithBytes: hash length: CC_SHA256_DIGEST_LENGTH] );
|
||||
}
|
||||
|
||||
- (NSData *) SHA384Hash
|
||||
{
|
||||
unsigned char hash[CC_SHA384_DIGEST_LENGTH];
|
||||
(void) CC_SHA384( [self bytes], (CC_LONG)[self length], hash );
|
||||
return ( [NSData dataWithBytes: hash length: CC_SHA384_DIGEST_LENGTH] );
|
||||
}
|
||||
|
||||
- (NSData *) SHA512Hash
|
||||
{
|
||||
unsigned char hash[CC_SHA512_DIGEST_LENGTH];
|
||||
(void) CC_SHA512( [self bytes], (CC_LONG)[self length], hash );
|
||||
return ( [NSData dataWithBytes: hash length: CC_SHA512_DIGEST_LENGTH] );
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSData (CommonCryptor)
|
||||
|
||||
- (NSData *) AES256EncryptedDataUsingKey: (id) key error: (NSError **) error
|
||||
{
|
||||
CCCryptorStatus status = kCCSuccess;
|
||||
NSData * result = [self dataEncryptedUsingAlgorithm: kCCAlgorithmAES128
|
||||
key: key
|
||||
options: kCCOptionPKCS7Padding
|
||||
error: &status];
|
||||
|
||||
if ( result != nil )
|
||||
return ( result );
|
||||
|
||||
if ( error != NULL )
|
||||
*error = [NSError errorWithCCCryptorStatus: status];
|
||||
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
- (NSData *) decryptedAES256DataUsingKey: (id) key error: (NSError **) error
|
||||
{
|
||||
CCCryptorStatus status = kCCSuccess;
|
||||
NSData * result = [self decryptedDataUsingAlgorithm: kCCAlgorithmAES128
|
||||
key: key
|
||||
options: kCCOptionPKCS7Padding
|
||||
error: &status];
|
||||
|
||||
if ( result != nil )
|
||||
return ( result );
|
||||
|
||||
if ( error != NULL )
|
||||
*error = [NSError errorWithCCCryptorStatus: status];
|
||||
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
- (NSData *) DESEncryptedDataUsingKey: (id) key error: (NSError **) error
|
||||
{
|
||||
CCCryptorStatus status = kCCSuccess;
|
||||
NSData * result = [self dataEncryptedUsingAlgorithm: kCCAlgorithmDES
|
||||
key: key
|
||||
options: kCCOptionPKCS7Padding
|
||||
error: &status];
|
||||
|
||||
if ( result != nil )
|
||||
return ( result );
|
||||
|
||||
if ( error != NULL )
|
||||
*error = [NSError errorWithCCCryptorStatus: status];
|
||||
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
- (NSData *) decryptedDESDataUsingKey: (id) key error: (NSError **) error
|
||||
{
|
||||
CCCryptorStatus status = kCCSuccess;
|
||||
NSData * result = [self decryptedDataUsingAlgorithm: kCCAlgorithmDES
|
||||
key: key
|
||||
options: kCCOptionPKCS7Padding
|
||||
error: &status];
|
||||
|
||||
if ( result != nil )
|
||||
return ( result );
|
||||
|
||||
if ( error != NULL )
|
||||
*error = [NSError errorWithCCCryptorStatus: status];
|
||||
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
- (NSData *) CASTEncryptedDataUsingKey: (id) key error: (NSError **) error
|
||||
{
|
||||
CCCryptorStatus status = kCCSuccess;
|
||||
NSData * result = [self dataEncryptedUsingAlgorithm: kCCAlgorithmCAST
|
||||
key: key
|
||||
options: kCCOptionPKCS7Padding
|
||||
error: &status];
|
||||
|
||||
if ( result != nil )
|
||||
return ( result );
|
||||
|
||||
if ( error != NULL )
|
||||
*error = [NSError errorWithCCCryptorStatus: status];
|
||||
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
- (NSData *) decryptedCASTDataUsingKey: (id) key error: (NSError **) error
|
||||
{
|
||||
CCCryptorStatus status = kCCSuccess;
|
||||
NSData * result = [self decryptedDataUsingAlgorithm: kCCAlgorithmCAST
|
||||
key: key
|
||||
options: kCCOptionPKCS7Padding
|
||||
error: &status];
|
||||
|
||||
if ( result != nil )
|
||||
return ( result );
|
||||
|
||||
if ( error != NULL )
|
||||
*error = [NSError errorWithCCCryptorStatus: status];
|
||||
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
static void FixKeyLengths( CCAlgorithm algorithm, NSMutableData * keyData, NSMutableData * ivData )
|
||||
{
|
||||
NSUInteger keyLength = [keyData length];
|
||||
switch ( algorithm )
|
||||
{
|
||||
case kCCAlgorithmAES128:
|
||||
{
|
||||
if ( keyLength < 16 )
|
||||
{
|
||||
[keyData setLength: 16];
|
||||
}
|
||||
else if ( keyLength < 24 )
|
||||
{
|
||||
[keyData setLength: 24];
|
||||
}
|
||||
else
|
||||
{
|
||||
[keyData setLength: 32];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case kCCAlgorithmDES:
|
||||
{
|
||||
[keyData setLength: 8];
|
||||
break;
|
||||
}
|
||||
|
||||
case kCCAlgorithm3DES:
|
||||
{
|
||||
[keyData setLength: 24];
|
||||
break;
|
||||
}
|
||||
|
||||
case kCCAlgorithmCAST:
|
||||
{
|
||||
if ( keyLength < 5 )
|
||||
{
|
||||
[keyData setLength: 5];
|
||||
}
|
||||
else if ( keyLength > 16 )
|
||||
{
|
||||
[keyData setLength: 16];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
case kCCAlgorithmRC4:
|
||||
{
|
||||
if ( keyLength > 512 )
|
||||
[keyData setLength: 512];
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
[ivData setLength: [keyData length]];
|
||||
}
|
||||
|
||||
@implementation NSData (LowLevelCommonCryptor)
|
||||
|
||||
- (NSData *) _runCryptor: (CCCryptorRef) cryptor result: (CCCryptorStatus *) status
|
||||
{
|
||||
size_t bufsize = CCCryptorGetOutputLength( cryptor, (size_t)[self length], true );
|
||||
void * buf = malloc( bufsize );
|
||||
size_t bufused = 0;
|
||||
size_t bytesTotal = 0;
|
||||
*status = CCCryptorUpdate( cryptor, [self bytes], (size_t)[self length],
|
||||
buf, bufsize, &bufused );
|
||||
if ( *status != kCCSuccess )
|
||||
{
|
||||
free( buf );
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
bytesTotal += bufused;
|
||||
|
||||
// From Brent Royal-Gordon (Twitter: architechies):
|
||||
// Need to update buf ptr past used bytes when calling CCCryptorFinal()
|
||||
*status = CCCryptorFinal( cryptor, buf + bufused, bufsize - bufused, &bufused );
|
||||
if ( *status != kCCSuccess )
|
||||
{
|
||||
free( buf );
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
bytesTotal += bufused;
|
||||
|
||||
return ( [NSData dataWithBytesNoCopy: buf length: bytesTotal] );
|
||||
}
|
||||
|
||||
- (NSData *) dataEncryptedUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key
|
||||
error: (CCCryptorStatus *) error
|
||||
{
|
||||
return ( [self dataEncryptedUsingAlgorithm: algorithm
|
||||
key: key
|
||||
initializationVector: nil
|
||||
options: 0
|
||||
error: error] );
|
||||
}
|
||||
|
||||
- (NSData *) dataEncryptedUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key
|
||||
options: (CCOptions) options
|
||||
error: (CCCryptorStatus *) error
|
||||
{
|
||||
return ( [self dataEncryptedUsingAlgorithm: algorithm
|
||||
key: key
|
||||
initializationVector: nil
|
||||
options: options
|
||||
error: error] );
|
||||
}
|
||||
|
||||
- (NSData *) dataEncryptedUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key
|
||||
initializationVector: (id) iv
|
||||
options: (CCOptions) options
|
||||
error: (CCCryptorStatus *) error
|
||||
{
|
||||
CCCryptorRef cryptor = NULL;
|
||||
CCCryptorStatus status = kCCSuccess;
|
||||
|
||||
NSParameterAssert([key isKindOfClass: [NSData class]] || [key isKindOfClass: [NSString class]]);
|
||||
NSParameterAssert(iv == nil || [iv isKindOfClass: [NSData class]] || [iv isKindOfClass: [NSString class]]);
|
||||
|
||||
NSMutableData * keyData, * ivData;
|
||||
if ( [key isKindOfClass: [NSData class]] )
|
||||
keyData = (NSMutableData *) [key mutableCopy];
|
||||
else
|
||||
keyData = [[key dataUsingEncoding: NSUTF8StringEncoding] mutableCopy];
|
||||
|
||||
if ( [iv isKindOfClass: [NSString class]] )
|
||||
ivData = [[iv dataUsingEncoding: NSUTF8StringEncoding] mutableCopy];
|
||||
else
|
||||
ivData = (NSMutableData *) [iv mutableCopy]; // data or nil
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
[keyData autorelease];
|
||||
[ivData autorelease];
|
||||
#endif
|
||||
// ensure correct lengths for key and iv data, based on algorithms
|
||||
FixKeyLengths( algorithm, keyData, ivData );
|
||||
|
||||
status = CCCryptorCreate( kCCEncrypt, algorithm, options,
|
||||
[keyData bytes], [keyData length], [ivData bytes],
|
||||
&cryptor );
|
||||
|
||||
if ( status != kCCSuccess )
|
||||
{
|
||||
if ( error != NULL )
|
||||
*error = status;
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
NSData * result = [self _runCryptor: cryptor result: &status];
|
||||
if ( (result == nil) && (error != NULL) )
|
||||
*error = status;
|
||||
|
||||
CCCryptorRelease( cryptor );
|
||||
|
||||
return ( result );
|
||||
}
|
||||
|
||||
- (NSData *) decryptedDataUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key // data or string
|
||||
error: (CCCryptorStatus *) error
|
||||
{
|
||||
return ( [self decryptedDataUsingAlgorithm: algorithm
|
||||
key: key
|
||||
initializationVector: nil
|
||||
options: 0
|
||||
error: error] );
|
||||
}
|
||||
|
||||
- (NSData *) decryptedDataUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key // data or string
|
||||
options: (CCOptions) options
|
||||
error: (CCCryptorStatus *) error
|
||||
{
|
||||
return ( [self decryptedDataUsingAlgorithm: algorithm
|
||||
key: key
|
||||
initializationVector: nil
|
||||
options: options
|
||||
error: error] );
|
||||
}
|
||||
|
||||
- (NSData *) decryptedDataUsingAlgorithm: (CCAlgorithm) algorithm
|
||||
key: (id) key // data or string
|
||||
initializationVector: (id) iv // data or string
|
||||
options: (CCOptions) options
|
||||
error: (CCCryptorStatus *) error
|
||||
{
|
||||
CCCryptorRef cryptor = NULL;
|
||||
CCCryptorStatus status = kCCSuccess;
|
||||
|
||||
NSParameterAssert([key isKindOfClass: [NSData class]] || [key isKindOfClass: [NSString class]]);
|
||||
NSParameterAssert(iv == nil || [iv isKindOfClass: [NSData class]] || [iv isKindOfClass: [NSString class]]);
|
||||
|
||||
NSMutableData * keyData, * ivData;
|
||||
if ( [key isKindOfClass: [NSData class]] )
|
||||
keyData = (NSMutableData *) [key mutableCopy];
|
||||
else
|
||||
keyData = [[key dataUsingEncoding: NSUTF8StringEncoding] mutableCopy];
|
||||
|
||||
if ( [iv isKindOfClass: [NSString class]] )
|
||||
ivData = [[iv dataUsingEncoding: NSUTF8StringEncoding] mutableCopy];
|
||||
else
|
||||
ivData = (NSMutableData *) [iv mutableCopy]; // data or nil
|
||||
|
||||
#if !__has_feature(objc_arc)
|
||||
[keyData autorelease];
|
||||
[ivData autorelease];
|
||||
#endif
|
||||
|
||||
// ensure correct lengths for key and iv data, based on algorithms
|
||||
FixKeyLengths( algorithm, keyData, ivData );
|
||||
|
||||
status = CCCryptorCreate( kCCDecrypt, algorithm, options,
|
||||
[keyData bytes], [keyData length], [ivData bytes],
|
||||
&cryptor );
|
||||
|
||||
if ( status != kCCSuccess )
|
||||
{
|
||||
if ( error != NULL )
|
||||
*error = status;
|
||||
return ( nil );
|
||||
}
|
||||
|
||||
NSData * result = [self _runCryptor: cryptor result: &status];
|
||||
if ( (result == nil) && (error != NULL) )
|
||||
*error = status;
|
||||
|
||||
CCCryptorRelease( cryptor );
|
||||
|
||||
return ( result );
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation NSData (CommonHMAC)
|
||||
|
||||
- (NSData *) HMACWithAlgorithm: (CCHmacAlgorithm) algorithm
|
||||
{
|
||||
return ( [self HMACWithAlgorithm: algorithm key: nil] );
|
||||
}
|
||||
|
||||
- (NSData *) HMACWithAlgorithm: (CCHmacAlgorithm) algorithm key: (id) key
|
||||
{
|
||||
NSParameterAssert(key == nil || [key isKindOfClass: [NSData class]] || [key isKindOfClass: [NSString class]]);
|
||||
|
||||
NSData * keyData = nil;
|
||||
if ( [key isKindOfClass: [NSString class]] )
|
||||
keyData = [key dataUsingEncoding: NSUTF8StringEncoding];
|
||||
else
|
||||
keyData = (NSData *) key;
|
||||
|
||||
// this could be either CC_SHA1_DIGEST_LENGTH or CC_MD5_DIGEST_LENGTH. SHA1 is larger.
|
||||
unsigned char buf[CC_SHA1_DIGEST_LENGTH];
|
||||
CCHmac( algorithm, [keyData bytes], [keyData length], [self bytes], [self length], buf );
|
||||
|
||||
return ( [NSData dataWithBytes: buf length: (algorithm == kCCHmacAlgMD5 ? CC_MD5_DIGEST_LENGTH : CC_SHA1_DIGEST_LENGTH)] );
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// NSString+Base64.h
|
||||
// Gurpartap Singh
|
||||
//
|
||||
// Created by Gurpartap Singh on 06/05/12.
|
||||
// Copyright (c) 2012 Gurpartap Singh. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/NSString.h>
|
||||
|
||||
@interface NSString (Base64Additions)
|
||||
|
||||
+ (NSString *)base64StringFromData:(NSData *)data length:(NSUInteger)length;
|
||||
|
||||
@end
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// NSStringAdditions.m
|
||||
// Gurpartap Singh
|
||||
//
|
||||
// Created by Gurpartap Singh on 06/05/12.
|
||||
// Copyright (c) 2012 Gurpartap Singh. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSString+Base64.h"
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
static char base64EncodingTable[64] = {
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
|
||||
'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
|
||||
'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
|
||||
'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
|
||||
};
|
||||
|
||||
@implementation NSString (Base64Additions)
|
||||
|
||||
+ (NSString *)base64StringFromData: (NSData *)data length: (NSUInteger)length {
|
||||
unsigned long ixtext, lentext;
|
||||
long ctremaining;
|
||||
unsigned char input[3], output[4];
|
||||
short i, charsonline = 0, ctcopy;
|
||||
const unsigned char *raw;
|
||||
NSMutableString *result;
|
||||
|
||||
lentext = [data length];
|
||||
if (lentext < 1) {
|
||||
return @"";
|
||||
}
|
||||
result = [NSMutableString stringWithCapacity: lentext];
|
||||
raw = [data bytes];
|
||||
ixtext = 0;
|
||||
|
||||
while (true) {
|
||||
ctremaining = lentext - ixtext;
|
||||
if (ctremaining <= 0) {
|
||||
break;
|
||||
}
|
||||
for (i = 0; i < 3; i++) {
|
||||
unsigned long ix = ixtext + i;
|
||||
if (ix < lentext) {
|
||||
input[i] = raw[ix];
|
||||
}
|
||||
else {
|
||||
input[i] = 0;
|
||||
}
|
||||
}
|
||||
output[0] = (input[0] & 0xFC) >> 2;
|
||||
output[1] = ((input[0] & 0x03) << 4) | ((input[1] & 0xF0) >> 4);
|
||||
output[2] = ((input[1] & 0x0F) << 2) | ((input[2] & 0xC0) >> 6);
|
||||
output[3] = input[2] & 0x3F;
|
||||
ctcopy = 4;
|
||||
switch (ctremaining) {
|
||||
case 1:
|
||||
ctcopy = 2;
|
||||
break;
|
||||
case 2:
|
||||
ctcopy = 3;
|
||||
break;
|
||||
}
|
||||
|
||||
for (i = 0; i < ctcopy; i++) {
|
||||
[result appendString: [NSString stringWithFormat: @"%c", base64EncodingTable[output[i]]]];
|
||||
}
|
||||
|
||||
for (i = ctcopy; i < 4; i++) {
|
||||
[result appendString: @"="];
|
||||
}
|
||||
|
||||
ixtext += 3;
|
||||
charsonline += 4;
|
||||
|
||||
if ((length > 0) && (charsonline >= length)) {
|
||||
charsonline = 0;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// AFViewShaker
|
||||
// AFViewShaker
|
||||
//
|
||||
// Created by Philip Vasilchenko on 03.12.13.
|
||||
// Copyright (c) 2014 okolodev. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AFViewShaker : NSObject <CAAnimationDelegate>
|
||||
|
||||
- (instancetype)initWithView:(UIView *)view;
|
||||
- (instancetype)initWithViewsArray:(NSArray *)viewsArray;
|
||||
|
||||
- (void)shake;
|
||||
- (void)shakeWithDuration:(NSTimeInterval)duration completion:(void (^)())completion;
|
||||
|
||||
@end
|
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// AFViewShaker
|
||||
// AFViewShaker
|
||||
//
|
||||
// Created by Philip Vasilchenko on 03.12.13.
|
||||
// Copyright (c) 2014 okolodev. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AFViewShaker.h"
|
||||
|
||||
static NSTimeInterval const kAFViewShakerDefaultDuration = 0.5;
|
||||
static NSString * const kAFViewShakerAnimationKey = @"kAFViewShakerAnimationKey";
|
||||
|
||||
|
||||
@interface AFViewShaker ()
|
||||
@property (nonatomic, strong) NSArray * views;
|
||||
@property (nonatomic, assign) NSUInteger completedAnimations;
|
||||
@property (nonatomic, copy) void (^completionBlock)();
|
||||
@end
|
||||
|
||||
|
||||
@implementation AFViewShaker
|
||||
|
||||
- (instancetype)initWithView:(UIView *)view {
|
||||
return [self initWithViewsArray:@[ view ]];
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithViewsArray:(NSArray *)viewsArray {
|
||||
self = [super init];
|
||||
if ( self ) {
|
||||
self.views = viewsArray;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public methods
|
||||
|
||||
- (void)shake {
|
||||
[self shakeWithDuration:kAFViewShakerDefaultDuration completion:nil];
|
||||
}
|
||||
|
||||
|
||||
- (void)shakeWithDuration:(NSTimeInterval)duration completion:(void (^)())completion {
|
||||
self.completionBlock = completion;
|
||||
for (UIView * view in self.views) {
|
||||
[self addShakeAnimationForView:view withDuration:duration];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Shake Animation
|
||||
|
||||
- (void)addShakeAnimationForView:(UIView *)view withDuration:(NSTimeInterval)duration {
|
||||
CAKeyframeAnimation * animation = [CAKeyframeAnimation animationWithKeyPath:@"transform.translation.x"];
|
||||
CGFloat currentTx = view.transform.tx;
|
||||
|
||||
animation.delegate = self;
|
||||
animation.duration = duration;
|
||||
animation.values = @[ @(currentTx), @(currentTx + 10), @(currentTx-8), @(currentTx + 8), @(currentTx -5), @(currentTx + 5), @(currentTx) ];
|
||||
animation.keyTimes = @[ @(0), @(0.225), @(0.425), @(0.6), @(0.75), @(0.875), @(1) ];
|
||||
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
|
||||
[view.layer addAnimation:animation forKey:kAFViewShakerAnimationKey];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - CAAnimation Delegate
|
||||
|
||||
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag {
|
||||
self.completedAnimations += 1;
|
||||
if ( self.completedAnimations >= self.views.count ) {
|
||||
self.completedAnimations = 0;
|
||||
if ( self.completionBlock ) {
|
||||
self.completionBlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,116 @@
|
|||
//
|
||||
// AHKActionSheet.h
|
||||
// AHKActionSheetExample
|
||||
//
|
||||
// Created by Arkadiusz on 08-04-14.
|
||||
// Copyright (c) 2014 Arkadiusz Holko. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_ENUM(NSInteger, AHKActionSheetButtonType) {
|
||||
AHKActionSheetButtonTypeDefault = 0,
|
||||
AHKActionSheetButtonTypeDisabled,
|
||||
AHKActionSheetButtonTypeDestructive,
|
||||
AHKActionSheetButtonTypeCrypto
|
||||
};
|
||||
|
||||
@class AHKActionSheet;
|
||||
typedef void(^AHKActionSheetHandler)(AHKActionSheet *actionSheet);
|
||||
|
||||
|
||||
/// A block-based alternative to the `UIAlertView`.
|
||||
@interface AHKActionSheet : UIView <UIAppearanceContainer>
|
||||
|
||||
// Appearance - all of the following properties should be set before showing the action sheet. See `+initialize` to learn the default values of all properties.
|
||||
|
||||
/**
|
||||
* See UIImage+AHKAdditions.h/.m to learn how these three properties are used.
|
||||
*/
|
||||
@property (nonatomic) CGFloat blurRadius UI_APPEARANCE_SELECTOR;
|
||||
@property (strong, nonatomic) UIColor *blurTintColor UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic) CGFloat blurSaturationDeltaFactor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/// Height of the button (internally it's a `UITableViewCell`).
|
||||
@property (nonatomic) CGFloat buttonHeight UI_APPEARANCE_SELECTOR;
|
||||
/// Height of the cancel button.
|
||||
@property (nonatomic) CGFloat cancelButtonHeight UI_APPEARANCE_SELECTOR;
|
||||
/**
|
||||
* If set, a small shadow (a gradient layer) will be drawn above the cancel button to separate it visually from the other buttons.
|
||||
* It's best to use a color similar (but maybe with a lower alpha value) to blurTintColor.
|
||||
* See "Advanced" example in the example project to see it used.
|
||||
*/
|
||||
@property (strong, nonatomic) UIColor *cancelButtonShadowColor UI_APPEARANCE_SELECTOR;
|
||||
/// Boxed (@YES, @NO) boolean value (enabled by default). Isn't supported on iOS 6.
|
||||
@property (strong, nonatomic) NSNumber *automaticallyTintButtonImages UI_APPEARANCE_SELECTOR;
|
||||
/// Boxed boolean value. Useful when adding buttons without images (in that case text looks better centered). Disabled by default.
|
||||
@property (strong, nonatomic) NSNumber *buttonTextCenteringEnabled UI_APPEARANCE_SELECTOR;
|
||||
/// Color of the separator between buttons.
|
||||
@property (strong, nonatomic) UIColor *separatorColor UI_APPEARANCE_SELECTOR;
|
||||
/// Background color of the button when it's tapped (internally it's a UITableViewCell)
|
||||
@property (strong, nonatomic) UIColor *selectedBackgroundColor UI_APPEARANCE_SELECTOR;
|
||||
/// Text attributes of the title (passed in initWithTitle: or set via `title` property)
|
||||
@property (copy, nonatomic) NSDictionary *titleTextAttributes UI_APPEARANCE_SELECTOR;
|
||||
@property (copy, nonatomic) NSDictionary *buttonTextAttributes UI_APPEARANCE_SELECTOR;
|
||||
@property (copy, nonatomic) NSDictionary *disabledButtonTextAttributes UI_APPEARANCE_SELECTOR;
|
||||
@property (copy, nonatomic) NSDictionary *destructiveButtonTextAttributes UI_APPEARANCE_SELECTOR;
|
||||
@property (copy, nonatomic) NSDictionary *cryptoButtonTextAttributes UI_APPEARANCE_SELECTOR;
|
||||
@property (copy, nonatomic) NSDictionary *cancelButtonTextAttributes UI_APPEARANCE_SELECTOR;
|
||||
/// Duration of the show/dismiss animations. Defaults to 0.5.
|
||||
@property (nonatomic) NSTimeInterval animationDuration UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/// Boxed boolean value. Enables/disables control hiding with pan gesture. Enabled by default.
|
||||
@property (strong, nonatomic) NSNumber *cancelOnPanGestureEnabled UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/// Boxed boolean value. Enables/disables control hiding when tapped on empty area. Disabled by default.
|
||||
@property (strong, nonatomic) NSNumber *cancelOnTapEmptyAreaEnabled UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/// A handler called on every type of dismissal (tapping on "Cancel" or swipe down or flick down).
|
||||
@property (strong, nonatomic) AHKActionSheetHandler cancelHandler;
|
||||
@property (copy, nonatomic) NSString *cancelButtonTitle;
|
||||
|
||||
/// String to display above the buttons.
|
||||
@property (copy, nonatomic) NSString *title;
|
||||
/// View to display above the buttons (only if the title isn't set).
|
||||
@property (strong, nonatomic) UIView *headerView;
|
||||
/// Window visible before the actionSheet was presented.
|
||||
@property (weak, nonatomic, readonly) UIWindow *previousKeyWindow;
|
||||
|
||||
///TWS
|
||||
@property (nonatomic, weak) UIView *view;
|
||||
|
||||
/**
|
||||
* Initializes the action sheet with a specified title. `headerView` can be used if a string is insufficient for the title; set `title` as `nil` in this case.
|
||||
*
|
||||
* It's the designated initializer.
|
||||
*
|
||||
* @param title A string to display in the title area, above the buttons.
|
||||
*
|
||||
* @return A newly initialized action sheet.
|
||||
*/
|
||||
- (instancetype)initWithTitle:(NSString *)title;
|
||||
|
||||
- (instancetype)initWithView:(UIView *)view title:(NSString *)title;
|
||||
|
||||
/**
|
||||
* Adds a button without an image. Has to be called before showing the action sheet.
|
||||
*
|
||||
* @param handler A completion handler block to execute when a dismissal animation (after the user tapped on the button) has finished.
|
||||
*/
|
||||
- (void)addButtonWithTitle:(NSString *)title type:(AHKActionSheetButtonType)type handler:(AHKActionSheetHandler)handler;
|
||||
|
||||
/**
|
||||
* Adds a button with an image. Has to be called before showing the action sheet.
|
||||
*
|
||||
* @param image The image to display on the left of the title.
|
||||
* @param handler A completion handler block to execute when a dismissal animation (after the user tapped on the button) has finished.
|
||||
*/
|
||||
- (void)addButtonWithTitle:(NSString *)title image:(UIImage *)image type:(AHKActionSheetButtonType)type handler:(AHKActionSheetHandler)handler;
|
||||
|
||||
/// Displays the action sheet.
|
||||
- (void)show;
|
||||
|
||||
/// Dismisses the action sheet with an optional animation.
|
||||
- (void)dismissAnimated:(BOOL)animated;
|
||||
|
||||
@end
|
|
@ -0,0 +1,591 @@
|
|||
//
|
||||
// AHKActionSheet.m
|
||||
// AHKActionSheetExample
|
||||
//
|
||||
// Created by Arkadiusz on 08-04-14.
|
||||
// Copyright (c) 2014 Arkadiusz Holko. All rights reserved.
|
||||
//
|
||||
|
||||
#import <QuartzCore/QuartzCore.h>
|
||||
#import "AHKActionSheet.h"
|
||||
#import "AHKActionSheetViewController.h"
|
||||
#import "UIImage+AHKAdditions.h"
|
||||
#import "UIWindow+AHKAdditions.h"
|
||||
|
||||
|
||||
static const NSTimeInterval kDefaultAnimationDuration = 0.5f;
|
||||
// Length of the range at which the blurred background is being hidden when the user scrolls the tableView to the top.
|
||||
static const CGFloat kBlurFadeRangeSize = 200.0f;
|
||||
static NSString * const kCellIdentifier = @"Cell";
|
||||
// How much user has to scroll beyond the top of the tableView for the view to dismiss automatically.
|
||||
static const CGFloat kAutoDismissOffset = 80.0f;
|
||||
// Offset at which there's a check if the user is flicking the tableView down.
|
||||
static const CGFloat kFlickDownHandlingOffset = 20.0f;
|
||||
static const CGFloat kFlickDownMinVelocity = 2000.0f;
|
||||
// How much free space to leave at the top (above the tableView's contents) when there's a lot of elements. It makes this control look similar to the UIActionSheet.
|
||||
static const CGFloat kTopSpaceMarginFraction = 0.0; //TWS 0.333f;
|
||||
// cancelButton's shadow height as the ratio to the cancelButton's height
|
||||
static const CGFloat kCancelButtonShadowHeightRatio = 0.333f;
|
||||
|
||||
|
||||
/// Used for storing button configuration.
|
||||
@interface AHKActionSheetItem : NSObject
|
||||
@property (copy, nonatomic) NSString *title;
|
||||
@property (strong, nonatomic) UIImage *image;
|
||||
@property (nonatomic) AHKActionSheetButtonType type;
|
||||
@property (strong, nonatomic) AHKActionSheetHandler handler;
|
||||
@end
|
||||
|
||||
@implementation AHKActionSheetItem
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@interface AHKActionSheet() <UITableViewDataSource, UITableViewDelegate, UIGestureRecognizerDelegate>
|
||||
@property (strong, nonatomic) NSMutableArray *items;
|
||||
@property (weak, nonatomic, readwrite) UIWindow *previousKeyWindow;
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
@property (weak, nonatomic) UIImageView *blurredBackgroundView;
|
||||
@property (weak, nonatomic) UITableView *tableView;
|
||||
@property (weak, nonatomic) UIButton *cancelButton;
|
||||
@property (weak, nonatomic) UIView *cancelButtonShadowView;
|
||||
@end
|
||||
|
||||
@implementation AHKActionSheet
|
||||
|
||||
#pragma mark - Init
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
if (self != [AHKActionSheet class]) {
|
||||
return;
|
||||
}
|
||||
|
||||
AHKActionSheet *appearance = [self appearance];
|
||||
[appearance setBlurRadius:16.0f];
|
||||
[appearance setBlurTintColor:[UIColor colorWithWhite:1.0f alpha:0.5f]];
|
||||
[appearance setBlurSaturationDeltaFactor:1.8f];
|
||||
[appearance setButtonHeight:60.0f];
|
||||
[appearance setCancelButtonHeight:44.0f];
|
||||
[appearance setAutomaticallyTintButtonImages:@YES];
|
||||
[appearance setSelectedBackgroundColor:[UIColor colorWithWhite:0.1f alpha:0.2f]];
|
||||
[appearance setCancelButtonTextAttributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:17.0f],
|
||||
NSForegroundColorAttributeName : [UIColor darkGrayColor] }];
|
||||
[appearance setButtonTextAttributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:17.0f]}];
|
||||
[appearance setDisabledButtonTextAttributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:14.0f],
|
||||
NSForegroundColorAttributeName : [UIColor colorWithWhite:0.6f alpha:1.0] }];
|
||||
[appearance setDestructiveButtonTextAttributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:17.0f],
|
||||
NSForegroundColorAttributeName : [UIColor redColor] }];
|
||||
[appearance setTitleTextAttributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:14.0f],
|
||||
NSForegroundColorAttributeName : [UIColor grayColor] }];
|
||||
[appearance setCancelOnPanGestureEnabled:@(YES)];
|
||||
[appearance setCancelOnTapEmptyAreaEnabled:@(NO)];
|
||||
[appearance setAnimationDuration:kDefaultAnimationDuration];
|
||||
}
|
||||
|
||||
- (instancetype)initWithTitle:(NSString *)title
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_title = [title copy];
|
||||
_cancelButtonTitle = @"Cancel";
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithView:(UIView *)view title:(NSString *)title
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_title = [title copy];
|
||||
_cancelButtonTitle = @"Cancel";
|
||||
_view = view;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
return [self initWithTitle:nil];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
self.tableView.dataSource = nil;
|
||||
self.tableView.delegate = nil;
|
||||
}
|
||||
|
||||
#pragma mark - UITableViewDataSource
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return (NSInteger)[self.items count];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kCellIdentifier forIndexPath:indexPath];
|
||||
|
||||
//TWS
|
||||
cell.separatorInset = UIEdgeInsetsMake(0.f, 55.f, 0.f, 0.f);
|
||||
|
||||
AHKActionSheetItem *item = self.items[(NSUInteger)indexPath.row];
|
||||
|
||||
NSDictionary *attributes = nil;
|
||||
switch (item.type)
|
||||
{
|
||||
case AHKActionSheetButtonTypeDefault:
|
||||
attributes = self.buttonTextAttributes;
|
||||
break;
|
||||
case AHKActionSheetButtonTypeDisabled:
|
||||
attributes = self.disabledButtonTextAttributes;
|
||||
cell.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
break;
|
||||
case AHKActionSheetButtonTypeDestructive:
|
||||
attributes = self.destructiveButtonTextAttributes;
|
||||
break;
|
||||
case AHKActionSheetButtonTypeCrypto:
|
||||
attributes = self.cryptoButtonTextAttributes;
|
||||
break;
|
||||
}
|
||||
|
||||
NSAttributedString *attrTitle = [[NSAttributedString alloc] initWithString:item.title attributes:attributes];
|
||||
cell.textLabel.attributedText = attrTitle;
|
||||
cell.textLabel.textAlignment = [self.buttonTextCenteringEnabled boolValue] ? NSTextAlignmentCenter : NSTextAlignmentLeft;
|
||||
|
||||
// Use image with template mode with color the same as the text (when enabled).
|
||||
BOOL useTemplateMode = [UIImage instancesRespondToSelector:@selector(imageWithRenderingMode:)] && [self.automaticallyTintButtonImages boolValue];
|
||||
cell.imageView.image = useTemplateMode ? [item.image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] : item.image;
|
||||
|
||||
if ([UIImageView instancesRespondToSelector:@selector(tintColor)]){
|
||||
cell.imageView.tintColor = attributes[NSForegroundColorAttributeName] ? attributes[NSForegroundColorAttributeName] : [UIColor blackColor];
|
||||
}
|
||||
|
||||
//TWS
|
||||
cell.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
if (self.selectedBackgroundColor && ![cell.selectedBackgroundView.backgroundColor isEqual:self.selectedBackgroundColor]) {
|
||||
cell.selectedBackgroundView = [[UIView alloc] init];
|
||||
cell.selectedBackgroundView.backgroundColor = self.selectedBackgroundColor;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
#pragma mark - UITableViewDelegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
AHKActionSheetItem *item = self.items[(NSUInteger)indexPath.row];
|
||||
|
||||
if (item.type != AHKActionSheetButtonTypeDisabled) {
|
||||
[self dismissAnimated:YES duration:self.animationDuration completion:item.handler];
|
||||
}
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return self.buttonHeight;
|
||||
}
|
||||
|
||||
//TWS
|
||||
/*
|
||||
-(void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
// Remove separator inset as described here: http://stackoverflow.com/a/25877725/783960
|
||||
if ([cell respondsToSelector:@selector(setSeparatorInset:)]) {
|
||||
[cell setSeparatorInset:UIEdgeInsetsZero];
|
||||
}
|
||||
|
||||
// Prevent the cell from inheriting the Table View's margin settings
|
||||
if ([cell respondsToSelector:@selector(setPreservesSuperviewLayoutMargins:)]) {
|
||||
[cell setPreservesSuperviewLayoutMargins:NO];
|
||||
}
|
||||
|
||||
// Explictly set your cell's layout margins
|
||||
if ([cell respondsToSelector:@selector(setLayoutMargins:)]) {
|
||||
[cell setLayoutMargins:UIEdgeInsetsZero];
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
#pragma mark - UIScrollViewDelegate
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
if (![self.cancelOnPanGestureEnabled boolValue]) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self fadeBlursOnScrollToTop];
|
||||
}
|
||||
|
||||
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate
|
||||
{
|
||||
if (![self.cancelOnPanGestureEnabled boolValue]) {
|
||||
return;
|
||||
}
|
||||
|
||||
CGPoint scrollVelocity = [scrollView.panGestureRecognizer velocityInView:self];
|
||||
|
||||
BOOL viewWasFlickedDown = scrollVelocity.y > kFlickDownMinVelocity && scrollView.contentOffset.y < -self.tableView.contentInset.top - kFlickDownHandlingOffset;
|
||||
BOOL shouldSlideDown = scrollView.contentOffset.y < -self.tableView.contentInset.top - kAutoDismissOffset;
|
||||
if (viewWasFlickedDown) {
|
||||
// use a shorter duration for a flick down animation
|
||||
static const NSTimeInterval duration = 0.2f;
|
||||
[self dismissAnimated:YES duration:duration completion:self.cancelHandler];
|
||||
} else if (shouldSlideDown) {
|
||||
[self dismissAnimated:YES duration:self.animationDuration completion:self.cancelHandler];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
- (NSMutableArray *)items
|
||||
{
|
||||
if (!_items) {
|
||||
_items = [NSMutableArray array];
|
||||
}
|
||||
|
||||
return _items;
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)cancelButtonTapped:(id)sender
|
||||
{
|
||||
[self dismissAnimated:YES duration:self.animationDuration completion:self.cancelHandler];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)addButtonWithTitle:(NSString *)title type:(AHKActionSheetButtonType)type handler:(AHKActionSheetHandler)handler
|
||||
{
|
||||
[self addButtonWithTitle:title image:nil type:type handler:handler];
|
||||
}
|
||||
|
||||
- (void)addButtonWithTitle:(NSString *)title image:(UIImage *)image type:(AHKActionSheetButtonType)type handler:(AHKActionSheetHandler)handler
|
||||
{
|
||||
AHKActionSheetItem *item = [[AHKActionSheetItem alloc] init];
|
||||
item.title = title;
|
||||
item.image = image;
|
||||
item.type = type;
|
||||
item.handler = handler;
|
||||
[self.items addObject:item];
|
||||
}
|
||||
|
||||
- (void)show
|
||||
{
|
||||
NSAssert([self.items count] > 0, @"Please add some buttons before calling -show.");
|
||||
|
||||
if ([self isVisible]) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.previousKeyWindow = [UIApplication sharedApplication].keyWindow;
|
||||
UIImage *previousKeyWindowSnapshot = [self.previousKeyWindow ahk_snapshot];
|
||||
|
||||
[self setUpNewWindow];
|
||||
[self setUpBlurredBackgroundWithSnapshot:previousKeyWindowSnapshot];
|
||||
[self setUpCancelButton];
|
||||
[self setUpTableView];
|
||||
|
||||
if (self.cancelOnPanGestureEnabled.boolValue) {
|
||||
[self setUpCancelTapGestureForView:self.tableView];
|
||||
}
|
||||
|
||||
CGFloat slideDownMinOffset = (CGFloat)fmin(CGRectGetHeight(self.frame) + self.tableView.contentOffset.y, CGRectGetHeight(self.frame));
|
||||
self.tableView.transform = CGAffineTransformMakeTranslation(0, slideDownMinOffset);
|
||||
|
||||
void(^immediateAnimations)(void) = ^(void) {
|
||||
self.blurredBackgroundView.alpha = 1.0f;
|
||||
};
|
||||
|
||||
void(^delayedAnimations)(void) = ^(void) {
|
||||
self.cancelButton.frame = CGRectMake(0,
|
||||
CGRectGetMaxY(self.bounds) - self.cancelButtonHeight,
|
||||
CGRectGetWidth(_view.bounds),
|
||||
self.cancelButtonHeight);
|
||||
|
||||
//TWS
|
||||
self.cancelButton.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
self.tableView.transform = CGAffineTransformMakeTranslation(0, 0);
|
||||
|
||||
|
||||
// manual calculation of table's contentSize.height
|
||||
CGFloat tableContentHeight = [self.items count] * self.buttonHeight + CGRectGetHeight(self.tableView.tableHeaderView.frame);
|
||||
|
||||
CGFloat topInset;
|
||||
BOOL buttonsFitInWithoutScrolling = tableContentHeight < CGRectGetHeight(self.tableView.frame) * (1.0 - kTopSpaceMarginFraction);
|
||||
if (buttonsFitInWithoutScrolling) {
|
||||
// show all buttons if there isn't many
|
||||
topInset = CGRectGetHeight(self.tableView.frame) - tableContentHeight;
|
||||
} else {
|
||||
// leave an empty space on the top to make the control look similar to UIActionSheet
|
||||
topInset = (CGFloat)round(CGRectGetHeight(self.tableView.frame) * kTopSpaceMarginFraction);
|
||||
}
|
||||
|
||||
self.tableView.contentInset = UIEdgeInsetsMake(topInset, 0, 0, 0);
|
||||
|
||||
self.tableView.bounces = [self.cancelOnPanGestureEnabled boolValue] || !buttonsFitInWithoutScrolling;
|
||||
};
|
||||
|
||||
if ([UIView respondsToSelector:@selector(animateKeyframesWithDuration:delay:options:animations:completion:)]){
|
||||
// Animate sliding in tableView and cancel button with keyframe animation for a nicer effect.
|
||||
[UIView animateKeyframesWithDuration:self.animationDuration delay:0 options:0 animations:^{
|
||||
immediateAnimations();
|
||||
|
||||
[UIView addKeyframeWithRelativeStartTime:0.3f relativeDuration:0.7f animations:^{
|
||||
delayedAnimations();
|
||||
}];
|
||||
} completion:nil];
|
||||
|
||||
} else {
|
||||
|
||||
[UIView animateWithDuration:self.animationDuration animations:^{
|
||||
immediateAnimations();
|
||||
delayedAnimations();
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dismissAnimated:(BOOL)animated
|
||||
{
|
||||
[self dismissAnimated:animated duration:self.animationDuration completion:self.cancelHandler];
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (BOOL)isVisible
|
||||
{
|
||||
// action sheet is visible iff it's associated with a window
|
||||
return !!self.window;
|
||||
}
|
||||
|
||||
- (void)dismissAnimated:(BOOL)animated duration:(NSTimeInterval)duration completion:(AHKActionSheetHandler)completionHandler
|
||||
{
|
||||
if (![self isVisible]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// delegate isn't needed anymore because tableView will be hidden (and we don't want delegate methods to be called now)
|
||||
self.tableView.delegate = nil;
|
||||
self.tableView.userInteractionEnabled = NO;
|
||||
// keep the table from scrolling back up
|
||||
self.tableView.contentInset = UIEdgeInsetsMake(-self.tableView.contentOffset.y, 0, 0, 0);
|
||||
|
||||
void(^tearDownView)(void) = ^(void) {
|
||||
// remove the views because it's easiest to just recreate them if the action sheet is shown again
|
||||
for (UIView *view in @[self.tableView, self.cancelButton, self.blurredBackgroundView, self.window]) {
|
||||
[view removeFromSuperview];
|
||||
}
|
||||
|
||||
self.window = nil;
|
||||
[self.previousKeyWindow makeKeyAndVisible];
|
||||
|
||||
if (completionHandler) {
|
||||
completionHandler(self);
|
||||
}
|
||||
};
|
||||
|
||||
if (animated) {
|
||||
// animate sliding down tableView and cancelButton.
|
||||
[UIView animateWithDuration:duration animations:^{
|
||||
self.blurredBackgroundView.alpha = 0.0f;
|
||||
self.cancelButton.transform = CGAffineTransformTranslate(self.cancelButton.transform, 0, self.cancelButtonHeight);
|
||||
self.cancelButtonShadowView.alpha = 0.0f;
|
||||
|
||||
// Shortest shift of position sufficient to hide all tableView contents below the bottom margin.
|
||||
// contentInset isn't used here (unlike in -show) because it caused weird problems with animations in some cases.
|
||||
CGFloat slideDownMinOffset = (CGFloat)fmin(CGRectGetHeight(self.frame) + self.tableView.contentOffset.y, CGRectGetHeight(self.frame));
|
||||
self.tableView.transform = CGAffineTransformMakeTranslation(0, slideDownMinOffset);
|
||||
} completion:^(BOOL finished) {
|
||||
tearDownView();
|
||||
}];
|
||||
} else {
|
||||
tearDownView();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setUpNewWindow
|
||||
{
|
||||
AHKActionSheetViewController *actionSheetVC = [[AHKActionSheetViewController alloc] initWithNibName:nil bundle:nil];
|
||||
actionSheetVC.actionSheet = self;
|
||||
|
||||
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
self.window.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.window.opaque = NO;
|
||||
self.window.rootViewController = actionSheetVC;
|
||||
[self.window makeKeyAndVisible];
|
||||
}
|
||||
|
||||
- (void)setUpBlurredBackgroundWithSnapshot:(UIImage *)previousKeyWindowSnapshot
|
||||
{
|
||||
UIImage *blurredViewSnapshot = [previousKeyWindowSnapshot
|
||||
ahk_applyBlurWithRadius:self.blurRadius
|
||||
tintColor:self.blurTintColor
|
||||
saturationDeltaFactor:self.blurSaturationDeltaFactor
|
||||
maskImage:nil];
|
||||
UIImageView *backgroundView = [[UIImageView alloc] initWithImage:blurredViewSnapshot];
|
||||
backgroundView.frame = self.bounds;
|
||||
backgroundView.alpha = 0.0f;
|
||||
[self addSubview:backgroundView];
|
||||
self.blurredBackgroundView = backgroundView;
|
||||
}
|
||||
|
||||
- (void)setUpCancelTapGestureForView:(UIView*)view {
|
||||
UITapGestureRecognizer *cancelTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(cancelButtonTapped:)];
|
||||
cancelTap.delegate = self;
|
||||
[view addGestureRecognizer:cancelTap];
|
||||
}
|
||||
|
||||
- (void)setUpCancelButton
|
||||
{
|
||||
UIButton *cancelButton;
|
||||
// It's hard to check if UIButtonTypeSystem enumeration exists, so we're checking existence of another method that was introduced in iOS 7.
|
||||
if ([UIView instancesRespondToSelector:@selector(tintAdjustmentMode)]) {
|
||||
cancelButton= [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
} else {
|
||||
cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
}
|
||||
|
||||
NSAttributedString *attrTitle = [[NSAttributedString alloc] initWithString:self.cancelButtonTitle
|
||||
attributes:self.cancelButtonTextAttributes];
|
||||
[cancelButton setAttributedTitle:attrTitle forState:UIControlStateNormal];
|
||||
[cancelButton addTarget:self action:@selector(cancelButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
|
||||
cancelButton.frame = CGRectMake(0,
|
||||
CGRectGetMaxY(self.bounds) - self.cancelButtonHeight,
|
||||
CGRectGetWidth(self.bounds),
|
||||
self.cancelButtonHeight);
|
||||
// move the button below the screen (ready to be animated -show)
|
||||
cancelButton.transform = CGAffineTransformMakeTranslation(0, self.cancelButtonHeight);
|
||||
cancelButton.clipsToBounds = YES;
|
||||
[self addSubview:cancelButton];
|
||||
|
||||
self.cancelButton = cancelButton;
|
||||
|
||||
// add a small shadow/glow above the button
|
||||
if (self.cancelButtonShadowColor) {
|
||||
self.cancelButton.clipsToBounds = NO;
|
||||
CGFloat gradientHeight = (CGFloat)round(self.cancelButtonHeight * kCancelButtonShadowHeightRatio);
|
||||
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, -gradientHeight, CGRectGetWidth(self.bounds), gradientHeight)];
|
||||
CAGradientLayer *gradient = [CAGradientLayer layer];
|
||||
gradient.frame = view.bounds;
|
||||
gradient.colors = @[ (id)[UIColor colorWithWhite:0.0 alpha:0.0].CGColor, (id)[self.blurTintColor colorWithAlphaComponent:0.1f].CGColor ];
|
||||
[view.layer insertSublayer:gradient atIndex:0];
|
||||
[self.cancelButton addSubview:view];
|
||||
self.cancelButtonShadowView = view;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setUpTableView
|
||||
{
|
||||
CGRect statusBarViewRect = [self convertRect:[UIApplication sharedApplication].statusBarFrame fromView:nil];
|
||||
CGFloat statusBarHeight = CGRectGetHeight(statusBarViewRect);
|
||||
CGRect frame = CGRectMake(0,
|
||||
statusBarHeight,
|
||||
CGRectGetWidth(_view.bounds),
|
||||
CGRectGetHeight(self.bounds) - statusBarHeight - self.cancelButtonHeight);
|
||||
|
||||
UITableView *tableView = [[UITableView alloc] initWithFrame:frame];
|
||||
tableView.backgroundColor = [UIColor clearColor];
|
||||
tableView.showsVerticalScrollIndicator = NO;
|
||||
|
||||
if (self.separatorColor) {
|
||||
tableView.separatorColor = self.separatorColor;
|
||||
}
|
||||
|
||||
tableView.delegate = self;
|
||||
tableView.dataSource = self;
|
||||
[tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:kCellIdentifier];
|
||||
[self insertSubview:tableView aboveSubview:self.blurredBackgroundView];
|
||||
// move the content below the screen, ready to be animated in -show
|
||||
tableView.contentInset = UIEdgeInsetsMake(CGRectGetHeight(self.bounds), 0, 0, 0);
|
||||
// removes separators below the footer (between empty cells)
|
||||
tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
|
||||
|
||||
self.tableView = tableView;
|
||||
|
||||
[self setUpTableViewHeader];
|
||||
}
|
||||
|
||||
- (void)setUpTableViewHeader
|
||||
{
|
||||
if (self.title) {
|
||||
// paddings similar to those in the UITableViewCell
|
||||
static const CGFloat leftRightPadding = 15.0f;
|
||||
static const CGFloat topBottomPadding = 8.0f;
|
||||
CGFloat labelWidth = CGRectGetWidth(self.bounds) - 2*leftRightPadding;
|
||||
|
||||
NSAttributedString *attrText = [[NSAttributedString alloc] initWithString:self.title attributes:self.titleTextAttributes];
|
||||
|
||||
// create a label and calculate its size
|
||||
UILabel *label = [[UILabel alloc] init];
|
||||
label.numberOfLines = 0;
|
||||
[label setAttributedText:attrText];
|
||||
CGSize labelSize = [label sizeThatFits:CGSizeMake(labelWidth, MAXFLOAT)];
|
||||
label.frame = CGRectMake(leftRightPadding, topBottomPadding, labelWidth, labelSize.height);
|
||||
label.backgroundColor = [UIColor clearColor];
|
||||
|
||||
// create and add a header consisting of the label
|
||||
UIView *headerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.bounds), labelSize.height + 2*topBottomPadding)];
|
||||
[headerView addSubview:label];
|
||||
|
||||
self.tableView.tableHeaderView = headerView;
|
||||
|
||||
} else if (self.headerView) {
|
||||
self.tableView.tableHeaderView = self.headerView;
|
||||
}
|
||||
|
||||
// add a separator between the tableHeaderView and a first row (technically at the bottom of the tableHeaderView)
|
||||
if (self.tableView.tableHeaderView && self.tableView.separatorStyle != UITableViewCellSeparatorStyleNone) {
|
||||
CGFloat separatorHeight = 1.0f / [UIScreen mainScreen].scale;
|
||||
CGRect separatorFrame = CGRectMake(0,
|
||||
CGRectGetHeight(self.tableView.tableHeaderView.frame) - separatorHeight,
|
||||
CGRectGetWidth(self.tableView.tableHeaderView.frame),
|
||||
separatorHeight);
|
||||
|
||||
UIView *separator = [[UIView alloc] initWithFrame:separatorFrame];
|
||||
separator.backgroundColor = self.tableView.separatorColor;
|
||||
|
||||
//TWS
|
||||
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(0, 0, CGRectGetWidth(self.tableView.tableHeaderView.frame), 1 / UIScreen.mainScreen.scale)];
|
||||
line.backgroundColor = self.tableView.separatorColor;
|
||||
|
||||
[self.tableView.tableHeaderView addSubview:separator];
|
||||
[self.tableView.tableHeaderView addSubview:line];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fadeBlursOnScrollToTop
|
||||
{
|
||||
if (self.tableView.isDragging || self.tableView.isDecelerating) {
|
||||
CGFloat alphaWithoutBounds = 1.0f - ( -(self.tableView.contentInset.top + self.tableView.contentOffset.y) / kBlurFadeRangeSize);
|
||||
// limit alpha to the interval [0, 1]
|
||||
CGFloat alpha = (CGFloat)fmax(fmin(alphaWithoutBounds, 1.0f), 0.0f);
|
||||
self.blurredBackgroundView.alpha = alpha;
|
||||
self.cancelButtonShadowView.alpha = alpha;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
|
||||
// If the view that is touched is not the view associated with this view's table view, but
|
||||
// is one of the sub-views, we should not recognize the touch.
|
||||
// Original source: http://stackoverflow.com/questions/10755566/how-to-know-uitableview-is-pressed-when-empty
|
||||
if (touch.view != self.tableView && [touch.view isDescendantOfView:self.tableView]) {
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// AHKActionSheetViewController.h
|
||||
// AHKActionSheetExample
|
||||
//
|
||||
// Created by Arkadiusz on 09-04-14.
|
||||
// Copyright (c) 2014 Arkadiusz Holko. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class AHKActionSheet;
|
||||
|
||||
@interface AHKActionSheetViewController : UIViewController
|
||||
@property (strong, nonatomic) AHKActionSheet *actionSheet;
|
||||
@end
|
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// AHKActionSheetViewController.m
|
||||
// AHKActionSheetExample
|
||||
//
|
||||
// Created by Arkadiusz on 09-04-14.
|
||||
// Copyright (c) 2014 Arkadiusz Holko. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AHKActionSheetViewController.h"
|
||||
#import "AHKActionSheet.h"
|
||||
#import "UIWindow+AHKAdditions.h"
|
||||
|
||||
@interface AHKActionSheetViewController ()
|
||||
@property (nonatomic) BOOL viewAlreadyAppear;
|
||||
@end
|
||||
|
||||
@implementation AHKActionSheetViewController
|
||||
|
||||
#pragma mark - UIViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
[self.view addSubview:self.actionSheet];
|
||||
self.actionSheet.frame = self.view.bounds;
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
self.viewAlreadyAppear = YES;
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews
|
||||
{
|
||||
[super viewWillLayoutSubviews];
|
||||
|
||||
self.actionSheet.frame = self.view.bounds;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate
|
||||
{
|
||||
// doesn't allow autorotation after the view did appear (rotation messes up a blurred background)
|
||||
return !self.viewAlreadyAppear;
|
||||
}
|
||||
|
||||
//TWS
|
||||
#if __IPHONE_OS_VERSION_MAX_ALLOWED < 90000
|
||||
- (NSUInteger)supportedInterfaceOrientations
|
||||
#else
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
#endif
|
||||
{
|
||||
return UIInterfaceOrientationMaskAll;
|
||||
}
|
||||
|
||||
- (UIStatusBarStyle)preferredStatusBarStyle
|
||||
{
|
||||
UIWindow *window = self.actionSheet.previousKeyWindow;
|
||||
if (!window) {
|
||||
window = [[UIApplication sharedApplication].windows firstObject];
|
||||
}
|
||||
return [[window ahk_viewControllerForStatusBarStyle] preferredStatusBarStyle];
|
||||
}
|
||||
|
||||
- (BOOL)prefersStatusBarHidden
|
||||
{
|
||||
UIWindow *window = self.actionSheet.previousKeyWindow;
|
||||
if (!window) {
|
||||
window = [[UIApplication sharedApplication].windows firstObject];
|
||||
}
|
||||
return [[window ahk_viewControllerForStatusBarHidden] prefersStatusBarHidden];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
File: UIImage+ImageEffects.h
|
||||
Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code you’ll want to look out to find out how to use vImage to efficiently calculate a blur.
|
||||
Version: 1.0
|
||||
|
||||
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
|
||||
Inc. ("Apple") in consideration of your agreement to the following
|
||||
terms, and your use, installation, modification or redistribution of
|
||||
this Apple software constitutes acceptance of these terms. If you do
|
||||
not agree with these terms, please do not use, install, modify or
|
||||
redistribute this Apple software.
|
||||
|
||||
In consideration of your agreement to abide by the following terms, and
|
||||
subject to these terms, Apple grants you a personal, non-exclusive
|
||||
license, under Apple's copyrights in this original Apple software (the
|
||||
"Apple Software"), to use, reproduce, modify and redistribute the Apple
|
||||
Software, with or without modifications, in source and/or binary forms;
|
||||
provided that if you redistribute the Apple Software in its entirety and
|
||||
without modifications, you must retain this notice and the following
|
||||
text and disclaimers in all such redistributions of the Apple Software.
|
||||
Neither the name, trademarks, service marks or logos of Apple Inc. may
|
||||
be used to endorse or promote products derived from the Apple Software
|
||||
without specific prior written permission from Apple. Except as
|
||||
expressly stated in this notice, no other rights or licenses, express or
|
||||
implied, are granted by Apple herein, including but not limited to any
|
||||
patent rights that may be infringed by your derivative works or by other
|
||||
works in which the Apple Software may be incorporated.
|
||||
|
||||
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
|
||||
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
||||
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
|
||||
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
|
||||
|
||||
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
|
||||
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
|
||||
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
|
||||
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Copyright (C) 2013 Apple Inc. All Rights Reserved.
|
||||
|
||||
|
||||
Copyright © 2013 Apple Inc. All rights reserved.
|
||||
WWDC 2013 License
|
||||
|
||||
NOTE: This Apple Software was supplied by Apple as part of a WWDC 2013
|
||||
Session. Please refer to the applicable WWDC 2013 Session for further
|
||||
information.
|
||||
|
||||
IMPORTANT: This Apple software is supplied to you by Apple Inc.
|
||||
("Apple") in consideration of your agreement to the following terms, and
|
||||
your use, installation, modification or redistribution of this Apple
|
||||
software constitutes acceptance of these terms. If you do not agree with
|
||||
these terms, please do not use, install, modify or redistribute this
|
||||
Apple software.
|
||||
|
||||
In consideration of your agreement to abide by the following terms, and
|
||||
subject to these terms, Apple grants you a non-exclusive license, under
|
||||
Apple's copyrights in this original Apple software (the "Apple
|
||||
Software"), to use, reproduce, modify and redistribute the Apple
|
||||
Software, with or without modifications, in source and/or binary forms;
|
||||
provided that if you redistribute the Apple Software in its entirety and
|
||||
without modifications, you must retain this notice and the following
|
||||
text and disclaimers in all such redistributions of the Apple Software.
|
||||
Neither the name, trademarks, service marks or logos of Apple Inc. may
|
||||
be used to endorse or promote products derived from the Apple Software
|
||||
without specific prior written permission from Apple. Except as
|
||||
expressly stated in this notice, no other rights or licenses, express or
|
||||
implied, are granted by Apple herein, including but not limited to any
|
||||
patent rights that may be infringed by your derivative works or by other
|
||||
works in which the Apple Software may be incorporated.
|
||||
|
||||
The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES
|
||||
NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
|
||||
IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
|
||||
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
|
||||
|
||||
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
|
||||
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
|
||||
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
|
||||
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
EA1002
|
||||
5/3/2013
|
||||
*/
|
||||
|
||||
@import UIKit;
|
||||
|
||||
@interface UIImage (AHKAdditions)
|
||||
|
||||
- (UIImage *)ahk_applyLightEffect;
|
||||
- (UIImage *)ahk_applyExtraLightEffect;
|
||||
- (UIImage *)ahk_applyDarkEffect;
|
||||
- (UIImage *)ahk_applyTintEffectWithColor:(UIColor *)tintColor;
|
||||
|
||||
- (UIImage *)ahk_applyBlurWithRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage;
|
||||
|
||||
@end
|
|
@ -0,0 +1,278 @@
|
|||
/*
|
||||
File: UIImage+ImageEffects.m
|
||||
Abstract: This is a category of UIImage that adds methods to apply blur and tint effects to an image. This is the code you’ll want to look out to find out how to use vImage to efficiently calculate a blur.
|
||||
Version: 1.0
|
||||
|
||||
Disclaimer: IMPORTANT: This Apple software is supplied to you by Apple
|
||||
Inc. ("Apple") in consideration of your agreement to the following
|
||||
terms, and your use, installation, modification or redistribution of
|
||||
this Apple software constitutes acceptance of these terms. If you do
|
||||
not agree with these terms, please do not use, install, modify or
|
||||
redistribute this Apple software.
|
||||
|
||||
In consideration of your agreement to abide by the following terms, and
|
||||
subject to these terms, Apple grants you a personal, non-exclusive
|
||||
license, under Apple's copyrights in this original Apple software (the
|
||||
"Apple Software"), to use, reproduce, modify and redistribute the Apple
|
||||
Software, with or without modifications, in source and/or binary forms;
|
||||
provided that if you redistribute the Apple Software in its entirety and
|
||||
without modifications, you must retain this notice and the following
|
||||
text and disclaimers in all such redistributions of the Apple Software.
|
||||
Neither the name, trademarks, service marks or logos of Apple Inc. may
|
||||
be used to endorse or promote products derived from the Apple Software
|
||||
without specific prior written permission from Apple. Except as
|
||||
expressly stated in this notice, no other rights or licenses, express or
|
||||
implied, are granted by Apple herein, including but not limited to any
|
||||
patent rights that may be infringed by your derivative works or by other
|
||||
works in which the Apple Software may be incorporated.
|
||||
|
||||
The Apple Software is provided by Apple on an "AS IS" basis. APPLE
|
||||
MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
||||
THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
|
||||
FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
|
||||
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
|
||||
|
||||
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
|
||||
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
|
||||
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
|
||||
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
Copyright (C) 2013 Apple Inc. All Rights Reserved.
|
||||
|
||||
|
||||
Copyright © 2013 Apple Inc. All rights reserved.
|
||||
WWDC 2013 License
|
||||
|
||||
NOTE: This Apple Software was supplied by Apple as part of a WWDC 2013
|
||||
Session. Please refer to the applicable WWDC 2013 Session for further
|
||||
information.
|
||||
|
||||
IMPORTANT: This Apple software is supplied to you by Apple Inc.
|
||||
("Apple") in consideration of your agreement to the following terms, and
|
||||
your use, installation, modification or redistribution of this Apple
|
||||
software constitutes acceptance of these terms. If you do not agree with
|
||||
these terms, please do not use, install, modify or redistribute this
|
||||
Apple software.
|
||||
|
||||
In consideration of your agreement to abide by the following terms, and
|
||||
subject to these terms, Apple grants you a non-exclusive license, under
|
||||
Apple's copyrights in this original Apple software (the "Apple
|
||||
Software"), to use, reproduce, modify and redistribute the Apple
|
||||
Software, with or without modifications, in source and/or binary forms;
|
||||
provided that if you redistribute the Apple Software in its entirety and
|
||||
without modifications, you must retain this notice and the following
|
||||
text and disclaimers in all such redistributions of the Apple Software.
|
||||
Neither the name, trademarks, service marks or logos of Apple Inc. may
|
||||
be used to endorse or promote products derived from the Apple Software
|
||||
without specific prior written permission from Apple. Except as
|
||||
expressly stated in this notice, no other rights or licenses, express or
|
||||
implied, are granted by Apple herein, including but not limited to any
|
||||
patent rights that may be infringed by your derivative works or by other
|
||||
works in which the Apple Software may be incorporated.
|
||||
|
||||
The Apple Software is provided by Apple on an "AS IS" basis. APPLE MAKES
|
||||
NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION THE
|
||||
IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
|
||||
OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
|
||||
|
||||
IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
|
||||
OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
||||
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
|
||||
MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
|
||||
AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
|
||||
STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
|
||||
POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
EA1002
|
||||
5/3/2013
|
||||
*/
|
||||
|
||||
#import "UIImage+AHKAdditions.h"
|
||||
|
||||
@import Accelerate;
|
||||
#import <float.h>
|
||||
|
||||
|
||||
@implementation UIImage (AHKAdditions)
|
||||
|
||||
|
||||
- (UIImage *)ahk_applyLightEffect
|
||||
{
|
||||
UIColor *tintColor = [UIColor colorWithWhite:1.0 alpha:0.3f];
|
||||
return [self ahk_applyBlurWithRadius:30 tintColor:tintColor saturationDeltaFactor:1.8f maskImage:nil];
|
||||
}
|
||||
|
||||
|
||||
- (UIImage *)ahk_applyExtraLightEffect
|
||||
{
|
||||
UIColor *tintColor = [UIColor colorWithWhite:0.97f alpha:0.82f];
|
||||
return [self ahk_applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8f maskImage:nil];
|
||||
}
|
||||
|
||||
|
||||
- (UIImage *)ahk_applyDarkEffect
|
||||
{
|
||||
UIColor *tintColor = [UIColor colorWithWhite:0.11f alpha:0.73f];
|
||||
return [self ahk_applyBlurWithRadius:20 tintColor:tintColor saturationDeltaFactor:1.8f maskImage:nil];
|
||||
}
|
||||
|
||||
|
||||
- (UIImage *)ahk_applyTintEffectWithColor:(UIColor *)tintColor
|
||||
{
|
||||
const CGFloat EffectColorAlpha = 0.6f;
|
||||
UIColor *effectColor = tintColor;
|
||||
int componentCount = (int)CGColorGetNumberOfComponents(tintColor.CGColor);
|
||||
if (componentCount == 2) {
|
||||
CGFloat b;
|
||||
if ([tintColor getWhite:&b alpha:NULL]) {
|
||||
effectColor = [UIColor colorWithWhite:b alpha:EffectColorAlpha];
|
||||
}
|
||||
}
|
||||
else {
|
||||
CGFloat r, g, b;
|
||||
if ([tintColor getRed:&r green:&g blue:&b alpha:NULL]) {
|
||||
effectColor = [UIColor colorWithRed:r green:g blue:b alpha:EffectColorAlpha];
|
||||
}
|
||||
}
|
||||
return [self ahk_applyBlurWithRadius:10 tintColor:effectColor saturationDeltaFactor:-1.0 maskImage:nil];
|
||||
}
|
||||
|
||||
|
||||
- (UIImage *)ahk_applyBlurWithRadius:(CGFloat)blurRadius tintColor:(UIColor *)tintColor saturationDeltaFactor:(CGFloat)saturationDeltaFactor maskImage:(UIImage *)maskImage
|
||||
{
|
||||
// Check pre-conditions.
|
||||
if (self.size.width < 1 || self.size.height < 1) {
|
||||
NSLog (@"*** error: invalid size: (%.2f x %.2f). Both dimensions must be >= 1: %@", self.size.width, self.size.height, self);
|
||||
return nil;
|
||||
}
|
||||
if (!self.CGImage) {
|
||||
NSLog (@"*** error: image must be backed by a CGImage: %@", self);
|
||||
return nil;
|
||||
}
|
||||
if (maskImage && !maskImage.CGImage) {
|
||||
NSLog (@"*** error: maskImage must be backed by a CGImage: %@", maskImage);
|
||||
return nil;
|
||||
}
|
||||
|
||||
CGRect imageRect = { CGPointZero, self.size };
|
||||
UIImage *effectImage = self;
|
||||
|
||||
BOOL hasBlur = blurRadius > __FLT_EPSILON__;
|
||||
BOOL hasSaturationChange = fabs(saturationDeltaFactor - 1.) > __FLT_EPSILON__;
|
||||
if (hasBlur || hasSaturationChange) {
|
||||
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
|
||||
CGContextRef effectInContext = UIGraphicsGetCurrentContext();
|
||||
CGContextScaleCTM(effectInContext, 1.0, -1.0);
|
||||
CGContextTranslateCTM(effectInContext, 0, -self.size.height);
|
||||
CGContextDrawImage(effectInContext, imageRect, self.CGImage);
|
||||
|
||||
vImage_Buffer effectInBuffer;
|
||||
effectInBuffer.data = CGBitmapContextGetData(effectInContext);
|
||||
effectInBuffer.width = CGBitmapContextGetWidth(effectInContext);
|
||||
effectInBuffer.height = CGBitmapContextGetHeight(effectInContext);
|
||||
effectInBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectInContext);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
|
||||
CGContextRef effectOutContext = UIGraphicsGetCurrentContext();
|
||||
vImage_Buffer effectOutBuffer;
|
||||
effectOutBuffer.data = CGBitmapContextGetData(effectOutContext);
|
||||
effectOutBuffer.width = CGBitmapContextGetWidth(effectOutContext);
|
||||
effectOutBuffer.height = CGBitmapContextGetHeight(effectOutContext);
|
||||
effectOutBuffer.rowBytes = CGBitmapContextGetBytesPerRow(effectOutContext);
|
||||
|
||||
if (hasBlur) {
|
||||
// A description of how to compute the box kernel width from the Gaussian
|
||||
// radius (aka standard deviation) appears in the SVG spec:
|
||||
// http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
|
||||
//
|
||||
// For larger values of 's' (s >= 2.0), an approximation can be used: Three
|
||||
// successive box-blurs build a piece-wise quadratic convolution kernel, which
|
||||
// approximates the Gaussian kernel to within roughly 3%.
|
||||
//
|
||||
// let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
|
||||
//
|
||||
// ... if d is odd, use three box-blurs of size 'd', centered on the output pixel.
|
||||
//
|
||||
CGFloat inputRadius = blurRadius * [[UIScreen mainScreen] scale];
|
||||
uint32_t radius = (uint32_t)floor(inputRadius * 3. * sqrt(2 * M_PI) / 4 + 0.5);
|
||||
if (radius % 2 != 1) {
|
||||
radius += 1; // force radius to be odd so that the three box-blur methodology works.
|
||||
}
|
||||
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
|
||||
vImageBoxConvolve_ARGB8888(&effectOutBuffer, &effectInBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
|
||||
vImageBoxConvolve_ARGB8888(&effectInBuffer, &effectOutBuffer, NULL, 0, 0, radius, radius, 0, kvImageEdgeExtend);
|
||||
}
|
||||
BOOL effectImageBuffersAreSwapped = NO;
|
||||
if (hasSaturationChange) {
|
||||
CGFloat s = saturationDeltaFactor;
|
||||
CGFloat floatingPointSaturationMatrix[] = {
|
||||
0.0722f + 0.9278f * s, 0.0722f - 0.0722f * s, 0.0722f - 0.0722f * s, 0,
|
||||
0.7152f - 0.7152f * s, 0.7152f + 0.2848f * s, 0.7152f - 0.7152f * s, 0,
|
||||
0.2126f - 0.2126f * s, 0.2126f - 0.2126f * s, 0.2126f + 0.7873f * s, 0,
|
||||
0, 0, 0, 1,
|
||||
};
|
||||
const int32_t divisor = 256;
|
||||
NSUInteger matrixSize = sizeof(floatingPointSaturationMatrix)/sizeof(floatingPointSaturationMatrix[0]);
|
||||
int16_t saturationMatrix[matrixSize];
|
||||
for (NSUInteger i = 0; i < matrixSize; ++i) {
|
||||
saturationMatrix[i] = (int16_t)round(floatingPointSaturationMatrix[i] * divisor);
|
||||
}
|
||||
if (hasBlur) {
|
||||
vImageMatrixMultiply_ARGB8888(&effectOutBuffer, &effectInBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
|
||||
effectImageBuffersAreSwapped = YES;
|
||||
}
|
||||
else {
|
||||
vImageMatrixMultiply_ARGB8888(&effectInBuffer, &effectOutBuffer, saturationMatrix, divisor, NULL, NULL, kvImageNoFlags);
|
||||
}
|
||||
}
|
||||
if (!effectImageBuffersAreSwapped)
|
||||
effectImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
if (effectImageBuffersAreSwapped)
|
||||
effectImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
}
|
||||
|
||||
// Set up output context.
|
||||
UIGraphicsBeginImageContextWithOptions(self.size, NO, [[UIScreen mainScreen] scale]);
|
||||
CGContextRef outputContext = UIGraphicsGetCurrentContext();
|
||||
CGContextScaleCTM(outputContext, 1.0, -1.0);
|
||||
CGContextTranslateCTM(outputContext, 0, -self.size.height);
|
||||
|
||||
// Draw base image.
|
||||
CGContextDrawImage(outputContext, imageRect, self.CGImage);
|
||||
|
||||
// Draw effect image.
|
||||
if (hasBlur) {
|
||||
CGContextSaveGState(outputContext);
|
||||
if (maskImage) {
|
||||
CGContextClipToMask(outputContext, imageRect, maskImage.CGImage);
|
||||
}
|
||||
CGContextDrawImage(outputContext, imageRect, effectImage.CGImage);
|
||||
CGContextRestoreGState(outputContext);
|
||||
}
|
||||
|
||||
// Add in color tint.
|
||||
if (tintColor) {
|
||||
CGContextSaveGState(outputContext);
|
||||
CGContextSetFillColorWithColor(outputContext, tintColor.CGColor);
|
||||
CGContextFillRect(outputContext, imageRect);
|
||||
CGContextRestoreGState(outputContext);
|
||||
}
|
||||
|
||||
// Output image is ready.
|
||||
UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return outputImage;
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// UIWindow+AHKAdditions.h
|
||||
// AHKActionSheetExample
|
||||
//
|
||||
// Created by Arkadiusz on 14-04-14.
|
||||
// Copyright (c) 2014 Arkadiusz Holko. All rights reserved.
|
||||
//
|
||||
// Original source: https://github.com/Sumi-Interactive/SIAlertView/blob/master/SIAlertView/UIWindow%2BSIUtils.h
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface UIWindow (AHKAdditions)
|
||||
|
||||
- (UIViewController *)ahk_viewControllerForStatusBarStyle;
|
||||
- (UIViewController *)ahk_viewControllerForStatusBarHidden;
|
||||
- (UIImage *)ahk_snapshot;
|
||||
|
||||
@end
|
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// UIWindow+AHKAdditions.m
|
||||
// AHKActionSheetExample
|
||||
//
|
||||
// Created by Arkadiusz on 14-04-14.
|
||||
// Copyright (c) 2014 Arkadiusz Holko. All rights reserved.
|
||||
//
|
||||
|
||||
#import "UIWindow+AHKAdditions.h"
|
||||
|
||||
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
|
||||
|
||||
@implementation UIWindow (AHKAdditions)
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (UIViewController *)ahk_viewControllerForStatusBarStyle
|
||||
{
|
||||
UIViewController *currentViewController = [self currentViewController];
|
||||
|
||||
while ([currentViewController childViewControllerForStatusBarStyle]) {
|
||||
currentViewController = [currentViewController childViewControllerForStatusBarStyle];
|
||||
}
|
||||
return currentViewController;
|
||||
}
|
||||
|
||||
- (UIViewController *)ahk_viewControllerForStatusBarHidden
|
||||
{
|
||||
UIViewController *currentViewController = [self currentViewController];
|
||||
|
||||
while ([currentViewController childViewControllerForStatusBarHidden]) {
|
||||
currentViewController = [currentViewController childViewControllerForStatusBarHidden];
|
||||
}
|
||||
return currentViewController;
|
||||
}
|
||||
|
||||
- (UIImage *)ahk_snapshot
|
||||
{
|
||||
// source (under MIT license): https://github.com/shinydevelopment/SDScreenshotCapture/blob/master/SDScreenshotCapture/SDScreenshotCapture.m#L35
|
||||
|
||||
// UIWindow doesn't have to be rotated on iOS 8+.
|
||||
BOOL ignoreOrientation = SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(@"8.0");
|
||||
|
||||
UIInterfaceOrientation orientation = [UIApplication sharedApplication].statusBarOrientation;
|
||||
|
||||
CGSize imageSize = CGSizeZero;
|
||||
if (UIInterfaceOrientationIsPortrait(orientation) || ignoreOrientation) {
|
||||
imageSize = [UIScreen mainScreen].bounds.size;
|
||||
} else {
|
||||
imageSize = CGSizeMake([UIScreen mainScreen].bounds.size.height, [UIScreen mainScreen].bounds.size.width);
|
||||
}
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, NO, 0);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSaveGState(context);
|
||||
CGContextTranslateCTM(context, self.center.x, self.center.y);
|
||||
CGContextConcatCTM(context, self.transform);
|
||||
CGContextTranslateCTM(context, -self.bounds.size.width * self.layer.anchorPoint.x, -self.bounds.size.height * self.layer.anchorPoint.y);
|
||||
|
||||
// correct for the screen orientation
|
||||
if (!ignoreOrientation) {
|
||||
if (orientation == UIInterfaceOrientationLandscapeLeft) {
|
||||
CGContextRotateCTM(context, (CGFloat)M_PI_2);
|
||||
CGContextTranslateCTM(context, 0, -imageSize.width);
|
||||
} else if (orientation == UIInterfaceOrientationLandscapeRight) {
|
||||
CGContextRotateCTM(context, (CGFloat)-M_PI_2);
|
||||
CGContextTranslateCTM(context, -imageSize.height, 0);
|
||||
} else if (orientation == UIInterfaceOrientationPortraitUpsideDown) {
|
||||
CGContextRotateCTM(context, (CGFloat)M_PI);
|
||||
CGContextTranslateCTM(context, -imageSize.width, -imageSize.height);
|
||||
}
|
||||
}
|
||||
|
||||
if([self respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
|
||||
[self drawViewHierarchyInRect:self.bounds afterScreenUpdates:NO];
|
||||
} else {
|
||||
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
|
||||
}
|
||||
|
||||
CGContextRestoreGState(context);
|
||||
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (UIViewController *)currentViewController
|
||||
{
|
||||
UIViewController *viewController = self.rootViewController;
|
||||
while (viewController.presentedViewController) {
|
||||
viewController = viewController.presentedViewController;
|
||||
}
|
||||
return viewController;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// BKPasscodeDummyViewController.h
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 8. 3..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol BKPasscodeDummyViewControllerDelegate;
|
||||
|
||||
|
||||
@interface BKPasscodeDummyViewController : UIViewController
|
||||
|
||||
@property (nonatomic, weak) id<BKPasscodeDummyViewControllerDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol BKPasscodeDummyViewControllerDelegate <NSObject>
|
||||
|
||||
- (void)dummyViewControllerWillAppear:(BKPasscodeDummyViewController *)aViewController;
|
||||
- (void)dummyViewControllerDidAppear:(BKPasscodeDummyViewController *)aViewController;
|
||||
|
||||
@end
|
|
@ -0,0 +1,41 @@
|
|||
//
|
||||
// BKPasscodeDummyViewController.m
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 8. 3..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BKPasscodeDummyViewController.h"
|
||||
|
||||
@interface BKPasscodeDummyViewController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation BKPasscodeDummyViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = [UIColor clearColor];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
[self.delegate dummyViewControllerWillAppear:self];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
if (self.presentedViewController == nil) {
|
||||
// only calls delegate when presented view controller(modal view controller) does not exists.
|
||||
[self.delegate dummyViewControllerDidAppear:self];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,63 @@
|
|||
//
|
||||
// BKPasscodeField.h
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 4. 20..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol BKPasscodeFieldDelegate;
|
||||
@protocol BKPasscodeFieldImageSource;
|
||||
|
||||
@interface BKPasscodeField : UIControl <UIKeyInput>
|
||||
|
||||
// delegate
|
||||
@property (nonatomic, weak) id<BKPasscodeFieldDelegate> delegate;
|
||||
@property (nonatomic, weak) id<BKPasscodeFieldImageSource> imageSource;
|
||||
|
||||
// passcode
|
||||
@property (nonatomic, strong) NSString *passcode;
|
||||
|
||||
// configurations
|
||||
@property (nonatomic) NSUInteger maximumLength;
|
||||
@property (nonatomic) CGSize dotSize;
|
||||
@property (nonatomic) CGFloat lineHeight;
|
||||
@property (nonatomic) CGFloat dotSpacing;
|
||||
@property (nonatomic, strong) UIColor *dotColor;
|
||||
|
||||
@property (nonatomic) UIKeyboardType keyboardType;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol BKPasscodeFieldDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
/**
|
||||
* Ask the delegate that whether passcode field accepts text.
|
||||
* If you want to accept entering text, return YES.
|
||||
*/
|
||||
- (BOOL)passcodeField:(BKPasscodeField *)aPasscodeField shouldInsertText:(NSString *)aText;
|
||||
|
||||
/**
|
||||
* Ask the delegate that whether passcode can be deleted.
|
||||
* If you want to accept deleting passcode, return YES.
|
||||
*/
|
||||
- (BOOL)passcodeFieldShouldDeleteBackward:(BKPasscodeField *)aPasscodeField;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol BKPasscodeFieldImageSource <NSObject>
|
||||
|
||||
@optional
|
||||
|
||||
/**
|
||||
* Ask the image source for a image to display passcode digit at index.
|
||||
* If you don't implement this, default shape (line for blank digit and circule for filled digit) will be displayed.
|
||||
*/
|
||||
- (UIImage *)passcodeField:(BKPasscodeField *)aPasscodeField dotImageAtIndex:(NSInteger)aIndex filled:(BOOL)aFilled;
|
||||
|
||||
@end
|
|
@ -0,0 +1,271 @@
|
|||
//
|
||||
// BKPasscodeField.m
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 4. 20..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BKPasscodeField.h"
|
||||
|
||||
@interface BKPasscodeField ()
|
||||
|
||||
@property (strong, nonatomic) NSMutableString *mutablePasscode;
|
||||
@property (strong, nonatomic) NSRegularExpression *nonDigitRegularExpression;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BKPasscodeField
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self _initialize];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
[self _initialize];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
[self _initialize];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_initialize
|
||||
{
|
||||
_maximumLength = 4;
|
||||
_dotSize = CGSizeMake(18.0f, 19.0f);
|
||||
_dotSpacing = 25.0f;
|
||||
_lineHeight = 3.0f;
|
||||
_dotColor = [UIColor blackColor];
|
||||
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
_mutablePasscode = [[NSMutableString alloc] initWithCapacity:4];
|
||||
|
||||
[self addTarget:self action:@selector(didTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
|
||||
- (NSRegularExpression *)nonDigitRegularExpression
|
||||
{
|
||||
if (nil == _nonDigitRegularExpression) {
|
||||
_nonDigitRegularExpression = [[NSRegularExpression alloc] initWithPattern:@"[^0-9]+" options:0 error:nil];
|
||||
}
|
||||
return _nonDigitRegularExpression;
|
||||
}
|
||||
|
||||
- (NSString *)passcode
|
||||
{
|
||||
return self.mutablePasscode;
|
||||
}
|
||||
|
||||
- (void)setPasscode:(NSString *)passcode
|
||||
{
|
||||
if (passcode) {
|
||||
if (passcode.length > self.maximumLength) {
|
||||
passcode = [passcode substringWithRange:NSMakeRange(0, self.maximumLength)];
|
||||
}
|
||||
self.mutablePasscode = [NSMutableString stringWithString:passcode];
|
||||
} else {
|
||||
self.mutablePasscode = [NSMutableString string];
|
||||
}
|
||||
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
#pragma mark - UIKeyInput
|
||||
|
||||
- (BOOL)hasText
|
||||
{
|
||||
return (self.mutablePasscode.length > 0);
|
||||
}
|
||||
|
||||
- (void)insertText:(NSString *)text
|
||||
{
|
||||
if (self.enabled == NO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.keyboardType == UIKeyboardTypeNumberPad) {
|
||||
text = [self.nonDigitRegularExpression stringByReplacingMatchesInString:text options:0 range:NSMakeRange(0, text.length) withTemplate:@""];
|
||||
}
|
||||
|
||||
if (text.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSInteger newLength = self.mutablePasscode.length + text.length;
|
||||
if (newLength > self.maximumLength) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(passcodeField:shouldInsertText:)]) {
|
||||
if (NO == [self.delegate passcodeField:self shouldInsertText:text]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
[self.mutablePasscode appendString:text];
|
||||
|
||||
[self setNeedsDisplay];
|
||||
|
||||
[self sendActionsForControlEvents:UIControlEventEditingChanged];
|
||||
}
|
||||
|
||||
- (void)deleteBackward
|
||||
{
|
||||
if (self.enabled == NO) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(passcodeFieldShouldDeleteBackward:)]) {
|
||||
if (NO == [self.delegate passcodeFieldShouldDeleteBackward:self]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (self.mutablePasscode.length == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
[self.mutablePasscode deleteCharactersInRange:NSMakeRange(self.mutablePasscode.length - 1, 1)];
|
||||
|
||||
[self setNeedsDisplay];
|
||||
|
||||
[self sendActionsForControlEvents:UIControlEventEditingChanged];
|
||||
}
|
||||
|
||||
- (UITextAutocapitalizationType)autocapitalizationType
|
||||
{
|
||||
return UITextAutocapitalizationTypeNone;
|
||||
}
|
||||
|
||||
- (UITextAutocorrectionType)autocorrectionType
|
||||
{
|
||||
return UITextAutocorrectionTypeNo;
|
||||
}
|
||||
|
||||
- (UITextSpellCheckingType)spellCheckingType
|
||||
{
|
||||
return UITextSpellCheckingTypeNo;
|
||||
}
|
||||
|
||||
- (BOOL)enablesReturnKeyAutomatically
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (UIKeyboardAppearance)keyboardAppearance
|
||||
{
|
||||
return UIKeyboardAppearanceDefault;
|
||||
}
|
||||
|
||||
- (UIReturnKeyType)returnKeyType
|
||||
{
|
||||
return UIReturnKeyDone;
|
||||
}
|
||||
|
||||
- (BOOL)isSecureTextEntry
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - UIView
|
||||
|
||||
- (CGSize)contentSize
|
||||
{
|
||||
return CGSizeMake(self.maximumLength * _dotSize.width + (self.maximumLength - 1) * _dotSpacing,
|
||||
_dotSize.height);
|
||||
}
|
||||
|
||||
- (void)setFrame:(CGRect)frame
|
||||
{
|
||||
[super setFrame:frame];
|
||||
[self setNeedsDisplay];
|
||||
}
|
||||
|
||||
- (void)drawRect:(CGRect)rect
|
||||
{
|
||||
CGSize contentSize = [self contentSize];
|
||||
|
||||
CGPoint origin = CGPointMake(floorf((self.frame.size.width - contentSize.width) * 0.5f),
|
||||
floorf((self.frame.size.height - contentSize.height) * 0.5f));
|
||||
|
||||
if ([self.imageSource respondsToSelector:@selector(passcodeField:dotImageAtIndex:filled:)]) {
|
||||
|
||||
for (NSUInteger i = 0; i < self.maximumLength; i++) {
|
||||
|
||||
UIImage *image = nil;
|
||||
|
||||
if (i < self.mutablePasscode.length) {
|
||||
// draw filled image
|
||||
image = [self.imageSource passcodeField:self dotImageAtIndex:i filled:YES];
|
||||
} else {
|
||||
// draw blank image
|
||||
image = [self.imageSource passcodeField:self dotImageAtIndex:i filled:NO];
|
||||
}
|
||||
|
||||
if (image) {
|
||||
CGRect imageFrame = CGRectMake(origin.x, origin.y, self.dotSize.width, self.dotSize.height);
|
||||
[image drawInRect:imageFrame];
|
||||
}
|
||||
|
||||
origin.x += (self.dotSize.width + self.dotSpacing);
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetFillColorWithColor(context, self.dotColor.CGColor);
|
||||
|
||||
for (NSUInteger i = 0; i < self.maximumLength; i++) {
|
||||
|
||||
if (i < self.mutablePasscode.length) {
|
||||
// draw circle
|
||||
CGRect circleFrame = CGRectMake(origin.x, origin.y, self.dotSize.width, self.dotSize.height);
|
||||
CGContextFillEllipseInRect(context, circleFrame);
|
||||
} else {
|
||||
// draw line
|
||||
CGRect lineFrame = CGRectMake(origin.x, origin.y + floorf((self.dotSize.height - self.lineHeight) * 0.5f),
|
||||
self.dotSize.width, self.lineHeight);
|
||||
CGContextFillRect(context, lineFrame);
|
||||
}
|
||||
|
||||
origin.x += (self.dotSize.width + self.dotSpacing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
return [self contentSize];
|
||||
}
|
||||
|
||||
#pragma mark - UIResponder
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)didTouchUpInside:(id)sender
|
||||
{
|
||||
[self becomeFirstResponder];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// BKPasscodeInputView.h
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 4. 20..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#import "BKPasscodeField.h"
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
BKPasscodeInputViewNumericPasscodeStyle,
|
||||
BKPasscodeInputViewNormalPasscodeStyle,
|
||||
} BKPasscodeInputViewPasscodeStyle;
|
||||
|
||||
|
||||
@protocol BKPasscodeInputViewDelegate;
|
||||
|
||||
|
||||
@interface BKPasscodeInputView : UIView <UITextFieldDelegate, BKPasscodeFieldDelegate, NSCopying>
|
||||
|
||||
@property (nonatomic, weak) id<BKPasscodeInputViewDelegate> delegate;
|
||||
|
||||
@property (nonatomic) BKPasscodeInputViewPasscodeStyle passcodeStyle;
|
||||
@property (nonatomic) UIKeyboardType keyboardType;
|
||||
@property (nonatomic) NSUInteger maximumLength;
|
||||
|
||||
@property (nonatomic, strong) NSString *title;
|
||||
@property (nonatomic, strong) NSString *message;
|
||||
@property (nonatomic, strong) NSString *errorMessage;
|
||||
@property (nonatomic, getter = isEnabled) BOOL enabled;
|
||||
@property (nonatomic, strong) NSString *passcode;
|
||||
|
||||
@property (nonatomic, strong, readonly) UIControl *passcodeField;
|
||||
|
||||
// You can override these methods to customize message label appearance.
|
||||
+ (void)configureTitleLabel:(UILabel *)aLabel;
|
||||
+ (void)configureMessageLabel:(UILabel *)aLabel;
|
||||
+ (void)configureErrorMessageLabel:(UILabel *)aLabel;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol BKPasscodeInputViewDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Tells the delegate that maximum length of passcode is entered or user tapped Done button in the keyboard (in case of BKPasscodeInputViewNormalPasscodeStyle).
|
||||
*/
|
||||
- (void)passcodeInputViewDidFinish:(BKPasscodeInputView *)aInputView;
|
||||
|
||||
@end
|
|
@ -0,0 +1,434 @@
|
|||
//
|
||||
// BKPasscodeInputView.m
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 4. 20..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BKPasscodeInputView.h"
|
||||
|
||||
#define kLabelPasscodeSpacePortrait (30.0f)
|
||||
#define kLabelPasscodeSpaceLandscape (10.0f)
|
||||
|
||||
#define kTextLeftRightSpace (20.0f)
|
||||
|
||||
#define kErrorMessageLeftRightPadding (10.0f)
|
||||
#define kErrorMessageTopBottomPadding (5.0f)
|
||||
|
||||
#define kDefaultNumericPasscodeMaximumLength (4)
|
||||
#define kDefaultNormalPasscodeMaximumLength (20)
|
||||
|
||||
@interface BKPasscodeInputView () {
|
||||
BOOL _isKeyboardTypeSet;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UILabel *messageLabel;
|
||||
@property (nonatomic, strong) UILabel *errorMessageLabel;
|
||||
@property (nonatomic, strong) UIControl *passcodeField;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BKPasscodeInputView
|
||||
|
||||
@synthesize maximumLength = _maximumLength;
|
||||
@synthesize keyboardType = _keyboardType;
|
||||
@synthesize passcodeField = _passcodeField;
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self _initialize];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (instancetype)initWithCoder:(NSCoder *)coder
|
||||
{
|
||||
self = [super initWithCoder:coder];
|
||||
if (self) {
|
||||
[self _initialize];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_initialize
|
||||
{
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
|
||||
_enabled = YES;
|
||||
_passcodeStyle = BKPasscodeInputViewNumericPasscodeStyle;
|
||||
_keyboardType = UIKeyboardTypeNumberPad;
|
||||
_maximumLength = 0;
|
||||
|
||||
_titleLabel = [[UILabel alloc] init];
|
||||
[[self class] configureTitleLabel:_titleLabel];
|
||||
[self addSubview:_titleLabel];
|
||||
|
||||
_messageLabel = [[UILabel alloc] init];
|
||||
[[self class] configureMessageLabel:_messageLabel];
|
||||
[self addSubview:_messageLabel];
|
||||
|
||||
_errorMessageLabel = [[UILabel alloc] init];
|
||||
[[self class] configureErrorMessageLabel:_errorMessageLabel];
|
||||
_errorMessageLabel.hidden = YES;
|
||||
[self addSubview:_errorMessageLabel];
|
||||
}
|
||||
|
||||
+ (void)configureTitleLabel:(UILabel *)aLabel
|
||||
{
|
||||
aLabel.backgroundColor = [UIColor clearColor];
|
||||
aLabel.numberOfLines = 1;
|
||||
aLabel.textAlignment = NSTextAlignmentCenter;
|
||||
aLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
aLabel.font = [UIFont boldSystemFontOfSize:15.0f];
|
||||
}
|
||||
|
||||
+ (void)configureMessageLabel:(UILabel *)aLabel
|
||||
{
|
||||
aLabel.backgroundColor = [UIColor clearColor];
|
||||
aLabel.numberOfLines = 0;
|
||||
aLabel.textAlignment = NSTextAlignmentCenter;
|
||||
aLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
aLabel.font = [UIFont systemFontOfSize:15.0f];
|
||||
}
|
||||
|
||||
+ (void)configureErrorMessageLabel:(UILabel *)aLabel
|
||||
{
|
||||
aLabel.backgroundColor = [UIColor clearColor];
|
||||
aLabel.numberOfLines = 0;
|
||||
aLabel.textAlignment = NSTextAlignmentCenter;
|
||||
aLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
aLabel.backgroundColor = [UIColor colorWithRed:0.63 green:0.2 blue:0.13 alpha:1];
|
||||
aLabel.textColor = [UIColor whiteColor];
|
||||
aLabel.font = [UIFont systemFontOfSize:15.0f];
|
||||
|
||||
aLabel.layer.cornerRadius = 10.0f;
|
||||
aLabel.layer.masksToBounds = YES;
|
||||
}
|
||||
|
||||
- (void)setPasscodeStyle:(BKPasscodeInputViewPasscodeStyle)passcodeStyle
|
||||
{
|
||||
if (_passcodeStyle != passcodeStyle) {
|
||||
_passcodeStyle = passcodeStyle;
|
||||
|
||||
if (_passcodeField) {
|
||||
_passcodeField = nil;
|
||||
[self passcodeField]; // load passcode field immediately if already exists before.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (UIControl *)passcodeField
|
||||
{
|
||||
if (nil == _passcodeField) {
|
||||
|
||||
switch (_passcodeStyle) {
|
||||
case BKPasscodeInputViewNumericPasscodeStyle:
|
||||
{
|
||||
if (_maximumLength == 0) {
|
||||
_maximumLength = kDefaultNumericPasscodeMaximumLength;
|
||||
}
|
||||
|
||||
if (NO == _isKeyboardTypeSet) {
|
||||
_keyboardType = UIKeyboardTypeNumberPad;
|
||||
}
|
||||
|
||||
BKPasscodeField *passcodeField = [[BKPasscodeField alloc] init];
|
||||
passcodeField.delegate = self;
|
||||
passcodeField.keyboardType = _keyboardType;
|
||||
passcodeField.maximumLength = _maximumLength;
|
||||
[passcodeField addTarget:self action:@selector(passcodeControlEditingChanged:) forControlEvents:UIControlEventEditingChanged];
|
||||
|
||||
[self setPasscodeField:passcodeField];
|
||||
break;
|
||||
}
|
||||
|
||||
case BKPasscodeInputViewNormalPasscodeStyle:
|
||||
{
|
||||
if (_maximumLength == 0) {
|
||||
_maximumLength = kDefaultNormalPasscodeMaximumLength;
|
||||
}
|
||||
|
||||
if (NO == _isKeyboardTypeSet) {
|
||||
_keyboardType = UIKeyboardTypeASCIICapable;
|
||||
}
|
||||
|
||||
UITextField *textField = [[UITextField alloc] init];
|
||||
textField.delegate = self;
|
||||
textField.borderStyle = UITextBorderStyleRoundedRect;
|
||||
textField.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
textField.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
textField.spellCheckingType = UITextSpellCheckingTypeNo;
|
||||
textField.enablesReturnKeyAutomatically = YES;
|
||||
textField.keyboardType = _keyboardType;
|
||||
textField.secureTextEntry = YES;
|
||||
textField.font = [UIFont systemFontOfSize:25.0f];
|
||||
textField.clearButtonMode = UITextFieldViewModeWhileEditing;
|
||||
textField.returnKeyType = UIReturnKeyDone;
|
||||
|
||||
[self setPasscodeField:textField];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _passcodeField;
|
||||
}
|
||||
|
||||
- (void)setPasscodeField:(UIControl *)passcodeField
|
||||
{
|
||||
if (_passcodeField != passcodeField) {
|
||||
|
||||
[_passcodeField removeFromSuperview];
|
||||
_passcodeField = passcodeField;
|
||||
if (_passcodeField) {
|
||||
[self addSubview:_passcodeField];
|
||||
}
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setMaximumLength:(NSUInteger)maximumLength
|
||||
{
|
||||
_maximumLength = maximumLength;
|
||||
|
||||
if ([_passcodeField isKindOfClass:[BKPasscodeField class]]) {
|
||||
[(BKPasscodeField *)_passcodeField setMaximumLength:maximumLength];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setKeyboardType:(UIKeyboardType)keyboardType
|
||||
{
|
||||
_isKeyboardTypeSet = YES;
|
||||
_keyboardType = keyboardType;
|
||||
[(id<UITextInputTraits>)_passcodeField setKeyboardType:keyboardType];
|
||||
}
|
||||
|
||||
- (void)setTitle:(NSString *)title
|
||||
{
|
||||
self.titleLabel.text = title;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (NSString *)title
|
||||
{
|
||||
return self.titleLabel.text;
|
||||
}
|
||||
|
||||
- (void)setMessage:(NSString *)message
|
||||
{
|
||||
self.messageLabel.text = message;
|
||||
self.messageLabel.hidden = NO;
|
||||
|
||||
self.errorMessageLabel.text = nil;
|
||||
self.errorMessageLabel.hidden = YES;
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (NSString *)message
|
||||
{
|
||||
return self.messageLabel.text;
|
||||
}
|
||||
|
||||
- (void)setErrorMessage:(NSString *)errorMessage
|
||||
{
|
||||
self.errorMessageLabel.text = errorMessage;
|
||||
self.errorMessageLabel.hidden = NO;
|
||||
|
||||
self.messageLabel.text = nil;
|
||||
self.messageLabel.hidden = YES;
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (NSString *)errorMessage
|
||||
{
|
||||
return self.errorMessageLabel.text;
|
||||
}
|
||||
|
||||
- (NSString *)passcode
|
||||
{
|
||||
switch (self.passcodeStyle) {
|
||||
case BKPasscodeInputViewNumericPasscodeStyle:
|
||||
return [(BKPasscodeField *)self.passcodeField passcode];
|
||||
case BKPasscodeInputViewNormalPasscodeStyle:
|
||||
return [(UITextField *)self.passcodeField text];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setPasscode:(NSString *)passcode
|
||||
{
|
||||
switch (self.passcodeStyle) {
|
||||
case BKPasscodeInputViewNumericPasscodeStyle:
|
||||
[(BKPasscodeField *)self.passcodeField setPasscode:passcode];
|
||||
break;
|
||||
case BKPasscodeInputViewNormalPasscodeStyle:
|
||||
[(UITextField *)self.passcodeField setText:passcode];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIView
|
||||
|
||||
- (CGFloat)labelPasscodeSpace
|
||||
{
|
||||
#ifdef SHARE_IN
|
||||
return (self.frame.size.width < self.frame.size.height) ? kLabelPasscodeSpacePortrait : kLabelPasscodeSpaceLandscape;
|
||||
#else
|
||||
return UIInterfaceOrientationIsPortrait([[UIApplication sharedApplication] statusBarOrientation]) ? kLabelPasscodeSpacePortrait : kLabelPasscodeSpaceLandscape;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
// layout passcode control to center
|
||||
[self.passcodeField sizeToFit];
|
||||
|
||||
if ([self.passcodeField isKindOfClass:[UITextField class]]) {
|
||||
self.passcodeField.frame = CGRectMake(0, 0, self.frame.size.width - kTextLeftRightSpace * 2.0f, CGRectGetHeight(self.passcodeField.frame) + 10.0f);
|
||||
}
|
||||
|
||||
self.passcodeField.center = CGPointMake(CGRectGetWidth(self.frame) * 0.5f, CGRectGetHeight(self.frame) * 0.5f);
|
||||
|
||||
CGFloat maxTextWidth = self.frame.size.width - (kTextLeftRightSpace * 2.0f);
|
||||
CGFloat labelPasscodeSpace = [self labelPasscodeSpace];
|
||||
|
||||
// layout title label
|
||||
_titleLabel.frame = CGRectMake(kTextLeftRightSpace, 0, maxTextWidth, self.frame.size.height);
|
||||
[_titleLabel sizeToFit];
|
||||
|
||||
CGRect rect = _titleLabel.frame;
|
||||
rect.origin.x = floorf((self.frame.size.width - CGRectGetWidth(rect)) * 0.5f);
|
||||
rect.origin.y = CGRectGetMinY(self.passcodeField.frame) - labelPasscodeSpace - CGRectGetHeight(_titleLabel.frame);
|
||||
|
||||
_titleLabel.frame = rect;
|
||||
|
||||
// layout message label
|
||||
if (!_messageLabel.hidden) {
|
||||
_messageLabel.frame = CGRectMake(kTextLeftRightSpace, CGRectGetMaxY(self.passcodeField.frame) + labelPasscodeSpace, maxTextWidth, self.frame.size.height);
|
||||
[_messageLabel sizeToFit];
|
||||
|
||||
rect = _messageLabel.frame;
|
||||
rect.origin.x = floorf((self.frame.size.width - CGRectGetWidth(rect)) * 0.5f);
|
||||
_messageLabel.frame = rect;
|
||||
}
|
||||
|
||||
// layout error message label
|
||||
if (!_errorMessageLabel.hidden) {
|
||||
_errorMessageLabel.frame = CGRectMake(0, CGRectGetMaxY(self.passcodeField.frame) + labelPasscodeSpace,
|
||||
maxTextWidth - kErrorMessageLeftRightPadding * 2.0f,
|
||||
self.frame.size.height);
|
||||
[_errorMessageLabel sizeToFit];
|
||||
|
||||
rect = _errorMessageLabel.frame;
|
||||
rect.size.width += (kErrorMessageLeftRightPadding * 2.0f);
|
||||
rect.size.height += (kErrorMessageTopBottomPadding * 2.0f);
|
||||
rect.origin.x = floorf((self.frame.size.width - rect.size.width) * 0.5f);
|
||||
|
||||
_errorMessageLabel.frame = rect;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIResponder
|
||||
|
||||
- (BOOL)canBecomeFirstResponder
|
||||
{
|
||||
return [self.passcodeField canBecomeFirstResponder];
|
||||
}
|
||||
|
||||
- (BOOL)becomeFirstResponder
|
||||
{
|
||||
return [self.passcodeField becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (BOOL)canResignFirstResponder
|
||||
{
|
||||
return [self.passcodeField canResignFirstResponder];
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
|
||||
{
|
||||
[super touchesBegan:touches withEvent:event];
|
||||
[self.passcodeField becomeFirstResponder];
|
||||
}
|
||||
|
||||
#pragma mark - Actions
|
||||
|
||||
- (void)passcodeControlEditingChanged:(id)sender
|
||||
{
|
||||
if (![self.passcodeField isKindOfClass:[BKPasscodeField class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
BKPasscodeField *passcodeField = (BKPasscodeField *)self.passcodeField;
|
||||
|
||||
if (passcodeField.passcode.length == passcodeField.maximumLength) {
|
||||
if ([self.delegate respondsToSelector:@selector(passcodeInputViewDidFinish:)]) {
|
||||
[self.delegate passcodeInputViewDidFinish:self];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - BKPasscodeFieldDelegate
|
||||
|
||||
- (BOOL)passcodeField:(BKPasscodeField *)aPasscodeField shouldInsertText:(NSString *)aText
|
||||
{
|
||||
return self.isEnabled;
|
||||
}
|
||||
|
||||
- (BOOL)passcodeFieldShouldDeleteBackward:(BKPasscodeField *)aPasscodeField
|
||||
{
|
||||
return self.isEnabled;
|
||||
}
|
||||
|
||||
#pragma mark - UITextFieldDelegate
|
||||
|
||||
- (BOOL)textField:(UITextField *)textField shouldChangeCharactersInRange:(NSRange)range replacementString:(NSString *)string
|
||||
{
|
||||
if (self.isEnabled == NO) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSUInteger length = textField.text.length - range.length + string.length;
|
||||
if (length > self.maximumLength) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField
|
||||
{
|
||||
if (self.isEnabled == NO) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(passcodeInputViewDidFinish:)]) {
|
||||
[self.delegate passcodeInputViewDidFinish:self];
|
||||
return NO;
|
||||
} else {
|
||||
return YES; // default behavior
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - NSCopying
|
||||
|
||||
- (id)copyWithZone:(NSZone *)zone
|
||||
{
|
||||
BKPasscodeInputView *view = [[[self class] alloc] initWithFrame:self.bounds];
|
||||
view.delegate = self.delegate;
|
||||
view.autoresizingMask = self.autoresizingMask;
|
||||
view.passcodeStyle = self.passcodeStyle;
|
||||
view.keyboardType = self.keyboardType;
|
||||
view.maximumLength = self.maximumLength;
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,53 @@
|
|||
//
|
||||
// BKPasscodeLockScreenManager.h
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 8. 2..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "BKPasscodeViewController.h"
|
||||
#import "BKPasscodeDummyViewController.h"
|
||||
|
||||
@protocol BKPasscodeLockScreenManagerDelegate;
|
||||
|
||||
|
||||
@interface BKPasscodeLockScreenManager : NSObject <BKPasscodeDummyViewControllerDelegate>
|
||||
|
||||
@property (weak, nonatomic) id<BKPasscodeLockScreenManagerDelegate> delegate;
|
||||
|
||||
/**
|
||||
* Shared(singleton) instance.
|
||||
*/
|
||||
+ (BKPasscodeLockScreenManager *)sharedManager;
|
||||
|
||||
/**
|
||||
* Shows lock screen. You should call this method at applicationDidEnterBackground: in app delegate.
|
||||
*/
|
||||
- (void)showLockScreen:(BOOL)animated;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol BKPasscodeLockScreenManagerDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Ask the delegate a view controller that should be displayed as lock screen.
|
||||
*/
|
||||
- (UIViewController *)lockScreenManagerPasscodeViewController:(BKPasscodeLockScreenManager *)aManager;
|
||||
|
||||
@optional
|
||||
/**
|
||||
* Ask the delegate that lock screen should be displayed or not.
|
||||
* If you prevent displaying lock screen, return NO.
|
||||
* If delegate does not implement this method, the lock screen will be shown everytime when application did enter background.
|
||||
*/
|
||||
- (BOOL)lockScreenManagerShouldShowLockScreen:(BKPasscodeLockScreenManager *)aManager;
|
||||
|
||||
/**
|
||||
* Ask the delegate for the view that will be used as snapshot.
|
||||
*/
|
||||
- (UIView *)lockScreenManagerBlindView:(BKPasscodeLockScreenManager *)aManager;
|
||||
|
||||
@end
|
|
@ -0,0 +1,120 @@
|
|||
//
|
||||
// BKPasscodeLockScreenManager.m
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 8. 2..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BKPasscodeLockScreenManager.h"
|
||||
#import "BKPasscodeViewController.h"
|
||||
|
||||
static BKPasscodeLockScreenManager *_sharedManager;
|
||||
|
||||
@interface BKPasscodeLockScreenManager ()
|
||||
|
||||
@property (strong, nonatomic) UIWindow *mainWindow;
|
||||
@property (strong, nonatomic) UIWindow *lockScreenWindow;
|
||||
@property (strong, nonatomic) UIView *blindView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BKPasscodeLockScreenManager
|
||||
|
||||
+ (BKPasscodeLockScreenManager *)sharedManager
|
||||
{
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
_sharedManager = [[BKPasscodeLockScreenManager alloc] init];
|
||||
});
|
||||
return _sharedManager;
|
||||
}
|
||||
|
||||
- (void)showLockScreen:(BOOL)animated
|
||||
{
|
||||
NSAssert(self.delegate, @"delegate is not assigned.");
|
||||
|
||||
if (self.lockScreenWindow && self.lockScreenWindow.rootViewController) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(lockScreenManagerShouldShowLockScreen:)]) {
|
||||
if (NO == [self.delegate lockScreenManagerShouldShowLockScreen:self]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// get the main window
|
||||
#ifndef SHARE_IN
|
||||
self.mainWindow = [[UIApplication sharedApplication] keyWindow];
|
||||
|
||||
// dismiss keyboard before showing lock screen
|
||||
[self.mainWindow.rootViewController.view endEditing:YES];
|
||||
#endif
|
||||
|
||||
// add blind view
|
||||
UIView *blindView;
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(lockScreenManagerBlindView:)]) {
|
||||
blindView = [self.delegate lockScreenManagerBlindView:self];
|
||||
}
|
||||
|
||||
if (nil == blindView) {
|
||||
blindView = [[UIView alloc] init];
|
||||
blindView.backgroundColor = [UIColor whiteColor];
|
||||
}
|
||||
|
||||
blindView.frame = self.mainWindow.bounds;
|
||||
blindView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
[self.mainWindow addSubview:blindView];
|
||||
|
||||
self.blindView = blindView;
|
||||
|
||||
// set dummy view controller as root view controller
|
||||
BKPasscodeDummyViewController *dummyViewController = [[BKPasscodeDummyViewController alloc] init];
|
||||
|
||||
UIWindow *lockScreenWindow = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
lockScreenWindow.windowLevel = self.mainWindow.windowLevel + 1;
|
||||
lockScreenWindow.rootViewController = dummyViewController;
|
||||
lockScreenWindow.backgroundColor = [UIColor clearColor];
|
||||
[lockScreenWindow makeKeyAndVisible];
|
||||
|
||||
// present lock screen
|
||||
UIViewController *lockScreenViewController = [self.delegate lockScreenManagerPasscodeViewController:self];
|
||||
|
||||
if (animated) {
|
||||
blindView.hidden = YES;
|
||||
}
|
||||
|
||||
[lockScreenWindow.rootViewController presentViewController:lockScreenViewController animated:animated completion:^{
|
||||
blindView.hidden = NO;
|
||||
}];
|
||||
|
||||
self.lockScreenWindow = lockScreenWindow;
|
||||
|
||||
[lockScreenViewController.view.superview bringSubviewToFront:lockScreenViewController.view];
|
||||
|
||||
dummyViewController.delegate = self;
|
||||
}
|
||||
|
||||
- (void)dummyViewControllerWillAppear:(BKPasscodeDummyViewController *)aViewController
|
||||
{
|
||||
// remove blind view
|
||||
[self.blindView removeFromSuperview];
|
||||
self.blindView = nil;
|
||||
}
|
||||
|
||||
- (void)dummyViewControllerDidAppear:(BKPasscodeDummyViewController *)aViewController
|
||||
{
|
||||
if ([UIView instancesRespondToSelector:@selector(tintColor)]) {
|
||||
self.lockScreenWindow = nil;
|
||||
} else {
|
||||
[self performSelector:@selector(setLockScreenWindow:) withObject:nil afterDelay:0.1f]; // workaround for wired dealloc on iOS 6
|
||||
}
|
||||
|
||||
[self.mainWindow makeKeyAndVisible];
|
||||
self.mainWindow = nil;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// BKPasscodeUtils.h
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 10. 4..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
/*
|
||||
* System Versioning Preprocessor Macros
|
||||
*/
|
||||
|
||||
#define SYSTEM_VERSION_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedSame)
|
||||
#define SYSTEM_VERSION_GREATER_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedDescending)
|
||||
#define SYSTEM_VERSION_GREATER_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedAscending)
|
||||
#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
|
||||
#define SYSTEM_VERSION_LESS_THAN_OR_EQUAL_TO(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] != NSOrderedDescending)
|
|
@ -0,0 +1,98 @@
|
|||
//
|
||||
// BKPasscodeViewController.h
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 4. 20..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "BKPasscodeInputView.h"
|
||||
#import "BKTouchIDSwitchView.h"
|
||||
#import "BKTouchIDManager.h"
|
||||
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
BKPasscodeViewControllerNewPasscodeType,
|
||||
BKPasscodeViewControllerChangePasscodeType,
|
||||
BKPasscodeViewControllerCheckPasscodeType
|
||||
} BKPasscodeViewControllerType;
|
||||
|
||||
@protocol BKPasscodeViewControllerDelegate;
|
||||
|
||||
@interface BKPasscodeViewController : UIViewController <BKPasscodeInputViewDelegate, BKTouchIDSwitchViewDelegate>
|
||||
|
||||
@property (nonatomic, weak) id<BKPasscodeViewControllerDelegate> delegate;
|
||||
|
||||
@property (nonatomic) BKPasscodeViewControllerType type;
|
||||
@property (nonatomic) BKPasscodeInputViewPasscodeStyle passcodeStyle;
|
||||
@property (nonatomic) UIKeyboardType keyboardType;
|
||||
@property (nonatomic, strong, readonly) BKPasscodeInputView *passcodeInputView;
|
||||
@property (nonatomic, strong) BKTouchIDManager *touchIDManager;
|
||||
|
||||
@property BOOL inputViewTitlePassword;
|
||||
|
||||
/**
|
||||
* Customize passcode input view
|
||||
* You may override to customize passcode input view appearance.
|
||||
*/
|
||||
- (void)customizePasscodeInputView:(BKPasscodeInputView *)aPasscodeInputView;
|
||||
|
||||
/**
|
||||
* Instantiate passcode input view.
|
||||
* You may override to use custom passcode input view.
|
||||
*/
|
||||
- (BKPasscodeInputView *)instantiatePasscodeInputView;
|
||||
|
||||
/**
|
||||
* Prompts Touch ID view to scan fingerprint.
|
||||
*/
|
||||
- (void)startTouchIDAuthenticationIfPossible;
|
||||
|
||||
/**
|
||||
* Prompts Touch ID view to scan fingerprint.
|
||||
* If Touch ID is disabled or unavailable, value of 'prompted' will be NO.
|
||||
*/
|
||||
- (void)startTouchIDAuthenticationIfPossible:(void(^)(BOOL prompted))aCompletionBlock;
|
||||
|
||||
@end
|
||||
|
||||
@protocol BKPasscodeViewControllerDelegate <NSObject>
|
||||
|
||||
/**
|
||||
* Tells the delegate that passcode is created or authenticated successfully.
|
||||
*/
|
||||
- (void)passcodeViewController:(BKPasscodeViewController *)aViewController didFinishWithPasscode:(NSString *)aPasscode;
|
||||
|
||||
@optional
|
||||
|
||||
/**
|
||||
* Tells the delegate that Touch ID error occured.
|
||||
*/
|
||||
- (void)passcodeViewControllerDidFailTouchIDKeychainOperation:(BKPasscodeViewController *)aViewController;
|
||||
|
||||
/**
|
||||
* Ask the delegate to verify that a passcode is correct. You must call the resultHandler with result.
|
||||
* You can check passcode asynchronously and show progress view (e.g. UIActivityIndicator) in the view controller if authentication takes too long.
|
||||
* You must call result handler in main thread.
|
||||
*/
|
||||
- (void)passcodeViewController:(BKPasscodeViewController *)aViewController authenticatePasscode:(NSString *)aPasscode resultHandler:(void(^)(BOOL succeed))aResultHandler;
|
||||
|
||||
/**
|
||||
* Tells the delegate that user entered incorrect passcode.
|
||||
* You should manage failed attempts yourself and it should be returned by -[BKPasscodeViewControllerDelegate passcodeViewControllerNumberOfFailedAttempts:] method.
|
||||
*/
|
||||
- (void)passcodeViewControllerDidFailAttempt:(BKPasscodeViewController *)aViewController;
|
||||
|
||||
/**
|
||||
* Ask the delegate that how many times incorrect passcode entered to display failed attempt count.
|
||||
*/
|
||||
- (NSUInteger)passcodeViewControllerNumberOfFailedAttempts:(BKPasscodeViewController *)aViewController;
|
||||
|
||||
/**
|
||||
* Ask the delegate that whether passcode view should lock or unlock.
|
||||
* If you return nil, passcode view will unlock otherwise it will lock until the date.
|
||||
*/
|
||||
- (NSDate *)passcodeViewControllerLockUntilDate:(BKPasscodeViewController *)aViewController;
|
||||
|
||||
@end
|
|
@ -0,0 +1,583 @@
|
|||
//
|
||||
// BKPasscodeViewController.m
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 4. 20..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BKPasscodeViewController.h"
|
||||
#import "BKShiftingView.h"
|
||||
#import "AFViewShaker.h"
|
||||
#import "BKPasscodeUtils.h"
|
||||
|
||||
typedef enum : NSUInteger {
|
||||
BKPasscodeViewControllerStateUnknown,
|
||||
BKPasscodeViewControllerStateCheckPassword,
|
||||
BKPasscodeViewControllerStateInputPassword,
|
||||
BKPasscodeViewControllerStateReinputPassword
|
||||
} BKPasscodeViewControllerState;
|
||||
|
||||
#define kBKPasscodeOneMinuteInSeconds (60)
|
||||
#define kBKPasscodeDefaultKeyboardHeight (216)
|
||||
|
||||
@interface BKPasscodeViewController ()
|
||||
|
||||
@property (nonatomic, strong) BKShiftingView *shiftingView;
|
||||
|
||||
@property (nonatomic) BKPasscodeViewControllerState currentState;
|
||||
@property (nonatomic, strong) NSString *oldPasscode;
|
||||
@property (nonatomic, strong) NSString *theNewPasscode;
|
||||
@property (nonatomic, strong) NSTimer *lockStateUpdateTimer;
|
||||
@property (nonatomic) CGFloat keyboardHeight;
|
||||
@property (nonatomic, strong) AFViewShaker *viewShaker;
|
||||
|
||||
@property (nonatomic) BOOL promptingTouchID;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BKPasscodeViewController
|
||||
|
||||
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
|
||||
{
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (self) {
|
||||
// init state
|
||||
_type = BKPasscodeViewControllerNewPasscodeType;
|
||||
_currentState = BKPasscodeViewControllerStateInputPassword;
|
||||
|
||||
// create shifting view
|
||||
self.shiftingView = [[BKShiftingView alloc] init];
|
||||
self.shiftingView.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
self.shiftingView.currentView = [self instantiatePasscodeInputView];
|
||||
|
||||
// keyboard notifications
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveKeyboardWillShowHideNotification:) name:UIKeyboardWillShowNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveKeyboardWillShowHideNotification:) name:UIKeyboardWillHideNotification object:nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didReceiveApplicationWillEnterForegroundNotification:)
|
||||
name:UIApplicationWillEnterForegroundNotification
|
||||
object:nil];
|
||||
|
||||
self.keyboardHeight = kBKPasscodeDefaultKeyboardHeight; // sometimes keyboard notification is not posted at all. so setting default value.
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self.lockStateUpdateTimer invalidate];
|
||||
self.lockStateUpdateTimer = nil;
|
||||
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)setType:(BKPasscodeViewControllerType)type
|
||||
{
|
||||
if (_type == type) {
|
||||
return;
|
||||
}
|
||||
|
||||
_type = type;
|
||||
|
||||
switch (type) {
|
||||
case BKPasscodeViewControllerNewPasscodeType:
|
||||
self.currentState = BKPasscodeViewControllerStateInputPassword;
|
||||
break;
|
||||
default:
|
||||
self.currentState = BKPasscodeViewControllerStateCheckPassword;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (BKPasscodeInputView *)passcodeInputView
|
||||
{
|
||||
if (NO == [self.shiftingView.currentView isKindOfClass:[BKPasscodeInputView class]]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return (BKPasscodeInputView *)self.shiftingView.currentView;
|
||||
}
|
||||
|
||||
- (BKPasscodeInputView *)instantiatePasscodeInputView
|
||||
{
|
||||
BKPasscodeInputView *view = [[BKPasscodeInputView alloc] init];
|
||||
view.delegate = self;
|
||||
view.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
- (void)customizePasscodeInputView:(BKPasscodeInputView *)aPasscodeInputView
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
[self.view setBackgroundColor:[UIColor colorWithRed:0.94 green:0.94 blue:0.96 alpha:1]];
|
||||
|
||||
[self updatePasscodeInputViewTitle:self.passcodeInputView];
|
||||
|
||||
[self customizePasscodeInputView:self.passcodeInputView];
|
||||
|
||||
[self.view addSubview:self.shiftingView];
|
||||
|
||||
[self lockIfNeeded];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (self.passcodeInputView.isEnabled) {
|
||||
|
||||
//TWS
|
||||
[self performSelector:@selector(startTouchIDAuthenticationIfPossible) withObject:nil afterDelay:0.2];
|
||||
}
|
||||
|
||||
[self.passcodeInputView becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
[self.view endEditing:YES];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews
|
||||
{
|
||||
[super viewDidLayoutSubviews];
|
||||
|
||||
CGRect frame = self.view.bounds;
|
||||
|
||||
CGFloat topBarOffset = 0;
|
||||
if ([self respondsToSelector:@selector(topLayoutGuide)]) {
|
||||
topBarOffset = [self.topLayoutGuide length];
|
||||
}
|
||||
|
||||
frame.origin.y += topBarOffset;
|
||||
frame.size.height -= (topBarOffset + self.keyboardHeight);
|
||||
|
||||
self.shiftingView.frame = frame;
|
||||
}
|
||||
|
||||
#pragma mark - Public methods
|
||||
|
||||
- (void)setPasscodeStyle:(BKPasscodeInputViewPasscodeStyle)passcodeStyle
|
||||
{
|
||||
self.passcodeInputView.passcodeStyle = passcodeStyle;
|
||||
}
|
||||
|
||||
- (BKPasscodeInputViewPasscodeStyle)passcodeStyle
|
||||
{
|
||||
return self.passcodeInputView.passcodeStyle;
|
||||
}
|
||||
|
||||
- (void)setKeyboardType:(UIKeyboardType)keyboardType
|
||||
{
|
||||
self.passcodeInputView.keyboardType = keyboardType;
|
||||
}
|
||||
|
||||
- (UIKeyboardType)keyboardType
|
||||
{
|
||||
return self.passcodeInputView.keyboardType;
|
||||
}
|
||||
|
||||
- (void)showLockMessageWithLockUntilDate:(NSDate *)lockUntil
|
||||
{
|
||||
NSTimeInterval timeInterval = [lockUntil timeIntervalSinceNow];
|
||||
NSUInteger minutes = ceilf(timeInterval / 60.0f);
|
||||
|
||||
BKPasscodeInputView *inputView = self.passcodeInputView;
|
||||
inputView.enabled = NO;
|
||||
|
||||
if (minutes == 1) {
|
||||
inputView.title = NSLocalizedStringFromTable(@"Try again in 1 minute", @"BKPasscodeView", @"1분 후에 다시 시도");
|
||||
} else {
|
||||
inputView.title = [NSString stringWithFormat:NSLocalizedStringFromTable(@"Try again in %d minutes", @"BKPasscodeView", @"%d분 후에 다시 시도"), minutes];
|
||||
}
|
||||
|
||||
NSUInteger numberOfFailedAttempts = [self.delegate passcodeViewControllerNumberOfFailedAttempts:self];
|
||||
|
||||
[self showFailedAttemptsCount:numberOfFailedAttempts inputView:inputView];
|
||||
|
||||
if (self.lockStateUpdateTimer == nil) {
|
||||
|
||||
NSTimeInterval delay = timeInterval + kBKPasscodeOneMinuteInSeconds - (kBKPasscodeOneMinuteInSeconds * (NSTimeInterval)minutes);
|
||||
|
||||
self.lockStateUpdateTimer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:delay]
|
||||
interval:60.f
|
||||
target:self
|
||||
selector:@selector(lockStateUpdateTimerFired:)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
|
||||
[[NSRunLoop currentRunLoop] addTimer:self.lockStateUpdateTimer forMode:NSDefaultRunLoopMode];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)lockIfNeeded
|
||||
{
|
||||
if (self.currentState != BKPasscodeViewControllerStateCheckPassword) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (NO == [self.delegate respondsToSelector:@selector(passcodeViewControllerLockUntilDate:)]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSDate *lockUntil = [self.delegate passcodeViewControllerLockUntilDate:self];
|
||||
if (lockUntil == nil || [lockUntil timeIntervalSinceNow] < 0) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
[self showLockMessageWithLockUntilDate:lockUntil];
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)updateLockMessageOrUnlockIfNeeded
|
||||
{
|
||||
if (self.currentState != BKPasscodeViewControllerStateCheckPassword) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (NO == [self.delegate respondsToSelector:@selector(passcodeViewControllerLockUntilDate:)]) {
|
||||
return;
|
||||
}
|
||||
|
||||
BKPasscodeInputView *inputView = self.passcodeInputView;
|
||||
|
||||
NSDate *lockUntil = [self.delegate passcodeViewControllerLockUntilDate:self];
|
||||
|
||||
if (lockUntil == nil || [lockUntil timeIntervalSinceNow] < 0) {
|
||||
|
||||
// invalidate timer
|
||||
[self.lockStateUpdateTimer invalidate];
|
||||
self.lockStateUpdateTimer = nil;
|
||||
|
||||
[self updatePasscodeInputViewTitle:inputView];
|
||||
|
||||
inputView.enabled = YES;
|
||||
|
||||
} else {
|
||||
[self showLockMessageWithLockUntilDate:lockUntil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)lockStateUpdateTimerFired:(NSTimer *)timer
|
||||
{
|
||||
[self updateLockMessageOrUnlockIfNeeded];
|
||||
}
|
||||
|
||||
- (void)startTouchIDAuthenticationIfPossible
|
||||
{
|
||||
[self startTouchIDAuthenticationIfPossible:nil];
|
||||
}
|
||||
|
||||
- (void)startTouchIDAuthenticationIfPossible:(void (^)(BOOL))aCompletionBlock
|
||||
{
|
||||
if (NO == [self canAuthenticateWithTouchID]) {
|
||||
if (aCompletionBlock) {
|
||||
aCompletionBlock(NO);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
self.promptingTouchID = YES;
|
||||
|
||||
[self.touchIDManager loadPasscodeWithCompletionBlock:^(NSString *passcode) {
|
||||
|
||||
self.promptingTouchID = NO;
|
||||
|
||||
if (passcode) {
|
||||
|
||||
self.passcodeInputView.passcode = passcode;
|
||||
|
||||
[self passcodeInputViewDidFinish:self.passcodeInputView];
|
||||
}
|
||||
|
||||
if (aCompletionBlock) {
|
||||
aCompletionBlock(YES);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark - Private methods
|
||||
|
||||
- (void)updatePasscodeInputViewTitle:(BKPasscodeInputView *)passcodeInputView
|
||||
{
|
||||
switch (self.currentState) {
|
||||
case BKPasscodeViewControllerStateCheckPassword:
|
||||
if (self.type == BKPasscodeViewControllerChangePasscodeType) {
|
||||
if (self.inputViewTitlePassword) passcodeInputView.title = NSLocalizedStringFromTable(@"Enter your old password", @"BKPasscodeView", @"Enter your old password");
|
||||
else passcodeInputView.title = NSLocalizedStringFromTable(@"Enter your old passcode", @"BKPasscodeView", @"기존 암호 입력");
|
||||
} else {
|
||||
if (self.inputViewTitlePassword) passcodeInputView.title = NSLocalizedStringFromTable(@"Enter your password", @"BKPasscodeView", @"Enter your password");
|
||||
else passcodeInputView.title = NSLocalizedStringFromTable(@"Enter your passcode", @"BKPasscodeView", @"암호 입력");
|
||||
}
|
||||
break;
|
||||
|
||||
case BKPasscodeViewControllerStateInputPassword:
|
||||
if (self.type == BKPasscodeViewControllerChangePasscodeType) {
|
||||
if (self.inputViewTitlePassword) passcodeInputView.title = NSLocalizedStringFromTable(@"Enter your new password", @"BKPasscodeView", @"Enter your new password");
|
||||
else passcodeInputView.title = NSLocalizedStringFromTable(@"Enter your new passcode", @"BKPasscodeView", @"새로운 암호 입력");
|
||||
} else {
|
||||
if (self.inputViewTitlePassword) passcodeInputView.title = NSLocalizedStringFromTable(@"Enter a password", @"BKPasscodeView", @"Enter a password");
|
||||
else passcodeInputView.title = NSLocalizedStringFromTable(@"Enter a passcode", @"BKPasscodeView", @"암호 입력");
|
||||
}
|
||||
break;
|
||||
|
||||
case BKPasscodeViewControllerStateReinputPassword:
|
||||
if (self.inputViewTitlePassword) passcodeInputView.title = NSLocalizedStringFromTable(@"Re-enter your password", @"BKPasscodeView", @"Re-enter your password");
|
||||
else passcodeInputView.title = NSLocalizedStringFromTable(@"Re-enter your passcode", @"BKPasscodeView", @"암호 재입력");
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showFailedAttemptsCount:(NSUInteger)failCount inputView:(BKPasscodeInputView *)aInputView
|
||||
{
|
||||
if (failCount == 0) {
|
||||
if (self.inputViewTitlePassword) aInputView.errorMessage = NSLocalizedStringFromTable(@"Invalid Password", @"BKPasscodeView", @"Invalid Password");
|
||||
else aInputView.errorMessage = NSLocalizedStringFromTable(@"Invalid Passcode", @"BKPasscodeView", @"잘못된 암호");
|
||||
} else if (failCount == 1) {
|
||||
if (self.inputViewTitlePassword) aInputView.errorMessage = NSLocalizedStringFromTable(@"1 Failed Password Attempt", @"BKPasscodeView", @"1 Failed Password Attempt");
|
||||
else aInputView.errorMessage = NSLocalizedStringFromTable(@"1 Failed Passcode Attempt", @"BKPasscodeView", @"1번의 암호 입력 시도 실패");
|
||||
} else {
|
||||
if (self.inputViewTitlePassword) aInputView.errorMessage = [NSString stringWithFormat:NSLocalizedStringFromTable(@"%d Failed Password Attempts", @"BKPasscodeView", @"%d Failed Password Attempts"), failCount];
|
||||
else aInputView.errorMessage = [NSString stringWithFormat:NSLocalizedStringFromTable(@"%d Failed Passcode Attempts", @"BKPasscodeView", @"%d번의 암호 입력 시도 실패"), failCount];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showTouchIDSwitchView
|
||||
{
|
||||
BKTouchIDSwitchView *view = [[BKTouchIDSwitchView alloc] init];
|
||||
view.delegate = self;
|
||||
view.touchIDSwitch.on = self.touchIDManager.isTouchIDEnabled;
|
||||
|
||||
[self.shiftingView showView:view withDirection:BKShiftingDirectionForward];
|
||||
}
|
||||
|
||||
- (BOOL)canAuthenticateWithTouchID
|
||||
{
|
||||
if (NO == [BKTouchIDManager canUseTouchID]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (self.type != BKPasscodeViewControllerCheckPasscodeType) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (nil == self.touchIDManager || NO == self.touchIDManager.isTouchIDEnabled) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (self.promptingTouchID) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
#ifndef SHARE_IN
|
||||
if ([UIApplication sharedApplication].applicationState == UIApplicationStateInactive) {
|
||||
return NO;
|
||||
}
|
||||
#endif
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
#pragma mark - BKPasscodeInputViewDelegate
|
||||
|
||||
- (void)passcodeInputViewDidFinish:(BKPasscodeInputView *)aInputView
|
||||
{
|
||||
NSString *passcode = aInputView.passcode;
|
||||
|
||||
switch (self.currentState) {
|
||||
case BKPasscodeViewControllerStateCheckPassword:
|
||||
{
|
||||
NSAssert([self.delegate respondsToSelector:@selector(passcodeViewController:authenticatePasscode:resultHandler:)],
|
||||
@"delegate must implement passcodeViewController:authenticatePasscode:resultHandler:");
|
||||
|
||||
[self.delegate passcodeViewController:self authenticatePasscode:passcode resultHandler:^(BOOL succeed) {
|
||||
|
||||
NSAssert([NSThread isMainThread], @"you must invoke result handler in main thread.");
|
||||
|
||||
if (succeed) {
|
||||
|
||||
if (self.type == BKPasscodeViewControllerChangePasscodeType) {
|
||||
|
||||
self.oldPasscode = passcode;
|
||||
self.currentState = BKPasscodeViewControllerStateInputPassword;
|
||||
|
||||
BKPasscodeInputView *newPasscodeInputView = [self.passcodeInputView copy];
|
||||
|
||||
[self customizePasscodeInputView:newPasscodeInputView];
|
||||
|
||||
[self updatePasscodeInputViewTitle:newPasscodeInputView];
|
||||
[self.shiftingView showView:newPasscodeInputView withDirection:BKShiftingDirectionForward];
|
||||
|
||||
[self.passcodeInputView becomeFirstResponder];
|
||||
|
||||
} else {
|
||||
|
||||
[self.delegate passcodeViewController:self didFinishWithPasscode:passcode];
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(passcodeViewControllerDidFailAttempt:)]) {
|
||||
[self.delegate passcodeViewControllerDidFailAttempt:self];
|
||||
}
|
||||
|
||||
NSUInteger failCount = 0;
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(passcodeViewControllerNumberOfFailedAttempts:)]) {
|
||||
failCount = [self.delegate passcodeViewControllerNumberOfFailedAttempts:self];
|
||||
}
|
||||
|
||||
[self showFailedAttemptsCount:failCount inputView:aInputView];
|
||||
|
||||
// reset entered passcode
|
||||
aInputView.passcode = nil;
|
||||
|
||||
// shake
|
||||
self.viewShaker = [[AFViewShaker alloc] initWithView:aInputView.passcodeField];
|
||||
[self.viewShaker shakeWithDuration:0.5f completion:nil];
|
||||
|
||||
// lock if needed
|
||||
if ([self.delegate respondsToSelector:@selector(passcodeViewControllerLockUntilDate:)]) {
|
||||
NSDate *lockUntilDate = [self.delegate passcodeViewControllerLockUntilDate:self];
|
||||
if (lockUntilDate != nil) {
|
||||
[self showLockMessageWithLockUntilDate:lockUntilDate];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}];
|
||||
|
||||
break;
|
||||
}
|
||||
case BKPasscodeViewControllerStateInputPassword:
|
||||
{
|
||||
if (self.type == BKPasscodeViewControllerChangePasscodeType && [self.oldPasscode isEqualToString:passcode]) {
|
||||
|
||||
aInputView.passcode = nil;
|
||||
|
||||
if (self.inputViewTitlePassword) aInputView.message = NSLocalizedStringFromTable(@"Enter a different password. Cannot re-use the same password.", @"BKPasscodeView", @"Enter a different password. Cannot re-use the same password.");
|
||||
else aInputView.message = NSLocalizedStringFromTable(@"Enter a different passcode. Cannot re-use the same passcode.", @"BKPasscodeView", @"다른 암호를 입력하십시오. 동일한 암호를 다시 사용할 수 없습니다.");
|
||||
|
||||
} else {
|
||||
|
||||
self.theNewPasscode = passcode;
|
||||
self.currentState = BKPasscodeViewControllerStateReinputPassword;
|
||||
|
||||
BKPasscodeInputView *newPasscodeInputView = [self.passcodeInputView copy];
|
||||
|
||||
[self customizePasscodeInputView:newPasscodeInputView];
|
||||
|
||||
[self updatePasscodeInputViewTitle:newPasscodeInputView];
|
||||
[self.shiftingView showView:newPasscodeInputView withDirection:BKShiftingDirectionForward];
|
||||
|
||||
[self.passcodeInputView becomeFirstResponder];
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case BKPasscodeViewControllerStateReinputPassword:
|
||||
{
|
||||
if ([passcode isEqualToString:self.theNewPasscode]) {
|
||||
|
||||
if (self.touchIDManager && [BKTouchIDManager canUseTouchID]) {
|
||||
[self showTouchIDSwitchView];
|
||||
} else {
|
||||
[self.delegate passcodeViewController:self didFinishWithPasscode:passcode];
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
self.currentState = BKPasscodeViewControllerStateInputPassword;
|
||||
|
||||
BKPasscodeInputView *newPasscodeInputView = [self.passcodeInputView copy];
|
||||
|
||||
[self customizePasscodeInputView:newPasscodeInputView];
|
||||
|
||||
[self updatePasscodeInputViewTitle:newPasscodeInputView];
|
||||
|
||||
if (self.inputViewTitlePassword) newPasscodeInputView.message = NSLocalizedStringFromTable(@"Password did not match.\nTry again.", @"BKPasscodeView", @"Password did not match.\nTry again.");
|
||||
else newPasscodeInputView.message = NSLocalizedStringFromTable(@"Passcodes did not match.\nTry again.", @"BKPasscodeView", @"암호가 일치하지 않습니다.\n다시 시도하십시오.");
|
||||
|
||||
[self.shiftingView showView:newPasscodeInputView withDirection:BKShiftingDirectionBackward];
|
||||
|
||||
[self.passcodeInputView becomeFirstResponder];
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - BKTouchIDSwitchViewDelegate
|
||||
|
||||
- (void)touchIDSwitchViewDidPressDoneButton:(BKTouchIDSwitchView *)view
|
||||
{
|
||||
BOOL enabled = view.touchIDSwitch.isOn;
|
||||
|
||||
if (enabled) {
|
||||
|
||||
[self.touchIDManager savePasscode:self.theNewPasscode completionBlock:^(BOOL success) {
|
||||
if (success) {
|
||||
[self.delegate passcodeViewController:self didFinishWithPasscode:self.theNewPasscode];
|
||||
} else {
|
||||
if ([self.delegate respondsToSelector:@selector(passcodeViewControllerDidFailTouchIDKeychainOperation:)]) {
|
||||
[self.delegate passcodeViewControllerDidFailTouchIDKeychainOperation:self];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
} else {
|
||||
|
||||
[self.touchIDManager deletePasscodeWithCompletionBlock:^(BOOL success) {
|
||||
if (success) {
|
||||
[self.delegate passcodeViewController:self didFinishWithPasscode:self.theNewPasscode];
|
||||
} else {
|
||||
if ([self.delegate respondsToSelector:@selector(passcodeViewControllerDidFailTouchIDKeychainOperation:)]) {
|
||||
[self.delegate passcodeViewControllerDidFailTouchIDKeychainOperation:self];
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)didReceiveKeyboardWillShowHideNotification:(NSNotification *)notification
|
||||
{
|
||||
CGRect keyboardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
|
||||
/*
|
||||
#ifdef SHARE_IN
|
||||
self.keyboardHeight = CGRectGetHeight(keyboardRect);
|
||||
#else
|
||||
UIInterfaceOrientation statusBarOrientation = [[UIApplication sharedApplication] statusBarOrientation];
|
||||
self.keyboardHeight = UIInterfaceOrientationIsPortrait(statusBarOrientation) ? CGRectGetWidth(keyboardRect) : CGRectGetHeight(keyboardRect);
|
||||
#endif
|
||||
*/
|
||||
self.keyboardHeight = CGRectGetHeight(keyboardRect);
|
||||
|
||||
[self.view setNeedsLayout];
|
||||
}
|
||||
|
||||
- (void)didReceiveApplicationWillEnterForegroundNotification:(NSNotification *)notification
|
||||
{
|
||||
[self startTouchIDAuthenticationIfPossible];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// BKShiftingView.h
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 10. 11..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, BKShiftingDirection) {
|
||||
BKShiftingDirectionForward,
|
||||
BKShiftingDirectionBackward,
|
||||
};
|
||||
|
||||
@interface BKShiftingView : UIView
|
||||
|
||||
@property (nonatomic, strong) UIView *currentView;
|
||||
|
||||
- (void)showView:(UIView *)view withDirection:(BKShiftingDirection)direction;
|
||||
|
||||
@end
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// BKShiftingView.m
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 10. 11..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BKShiftingView.h"
|
||||
|
||||
@implementation BKShiftingView
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
self.currentView.frame = self.bounds;
|
||||
}
|
||||
|
||||
- (void)setCurrentView:(UIView *)currentView
|
||||
{
|
||||
if (_currentView == currentView) {
|
||||
return;
|
||||
}
|
||||
|
||||
[_currentView removeFromSuperview];
|
||||
|
||||
_currentView = currentView;
|
||||
|
||||
if (currentView) {
|
||||
[self addSubview:currentView];
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (void)showView:(UIView *)view withDirection:(BKShiftingDirection)direction
|
||||
{
|
||||
UIView *oldView = self.currentView;
|
||||
oldView.userInteractionEnabled = NO;
|
||||
|
||||
CGRect nextFrame = self.bounds;
|
||||
|
||||
switch (direction) {
|
||||
case BKShiftingDirectionForward:
|
||||
nextFrame.origin.x = CGRectGetWidth(self.bounds);
|
||||
break;
|
||||
case BKShiftingDirectionBackward:
|
||||
nextFrame.origin.x = -CGRectGetWidth(self.bounds);
|
||||
break;
|
||||
}
|
||||
|
||||
view.frame = nextFrame;
|
||||
|
||||
[self addSubview:view];
|
||||
|
||||
// start animation
|
||||
[UIView animateWithDuration:0.3f delay:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
|
||||
|
||||
switch (direction) {
|
||||
case BKShiftingDirectionForward:
|
||||
oldView.frame = CGRectOffset(oldView.frame, -CGRectGetWidth(self.bounds), 0);
|
||||
view.frame = CGRectOffset(view.frame, -CGRectGetWidth(self.bounds), 0);
|
||||
break;
|
||||
case BKShiftingDirectionBackward:
|
||||
oldView.frame = CGRectOffset(oldView.frame, CGRectGetWidth(self.bounds), 0);
|
||||
view.frame = CGRectOffset(view.frame, CGRectGetWidth(self.bounds), 0);
|
||||
break;
|
||||
}
|
||||
|
||||
} completion:^(BOOL finished) {
|
||||
|
||||
[oldView removeFromSuperview];
|
||||
|
||||
}];
|
||||
|
||||
_currentView = view;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// BKTouchIDManager.h
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 10. 12..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface BKTouchIDManager : NSObject
|
||||
|
||||
@property (nonatomic, strong, readonly) NSString *keychainServiceName;
|
||||
@property (nonatomic, strong) NSString *promptText;
|
||||
@property (nonatomic, readonly, getter=isTouchIDEnabled) BOOL touchIDEnabled;
|
||||
|
||||
+ (BOOL)canUseTouchID;
|
||||
|
||||
- (instancetype)initWithKeychainServiceName:(NSString *)serviceName;
|
||||
|
||||
- (void)savePasscode:(NSString *)passcode completionBlock:(void(^)(BOOL success))completionBlock;
|
||||
|
||||
- (void)loadPasscodeWithCompletionBlock:(void(^)(NSString *passcode))completionBlock;
|
||||
|
||||
- (void)deletePasscodeWithCompletionBlock:(void(^)(BOOL success))completionBlock;
|
||||
|
||||
@end
|
|
@ -0,0 +1,236 @@
|
|||
//
|
||||
// BKTouchIDManager.m
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 10. 12..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BKTouchIDManager.h"
|
||||
#import <LocalAuthentication/LocalAuthentication.h>
|
||||
|
||||
static NSString *const BKTouchIDManagerPasscodeAccountName = @"passcode";
|
||||
static NSString *const BKTouchIDManagerTouchIDEnabledAccountName = @"enabled";
|
||||
|
||||
@interface BKTouchIDManager () {
|
||||
dispatch_queue_t _queue;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) NSString *keychainServiceName;
|
||||
|
||||
@end
|
||||
|
||||
@implementation BKTouchIDManager
|
||||
|
||||
- (instancetype)initWithKeychainServiceName:(NSString *)serviceName
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
|
||||
_queue = dispatch_queue_create("BKTouchIDManagerQueue", DISPATCH_QUEUE_SERIAL);
|
||||
|
||||
NSParameterAssert(serviceName);
|
||||
|
||||
self.keychainServiceName = serviceName;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (BOOL)canUseTouchID
|
||||
{
|
||||
if (![LAContext class]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
LAContext *context = [[LAContext alloc] init];
|
||||
|
||||
NSError *error = nil;
|
||||
BOOL result = [context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error];
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)savePasscode:(NSString *)passcode completionBlock:(void(^)(BOOL success))completionBlock
|
||||
{
|
||||
NSParameterAssert(passcode);
|
||||
|
||||
if (NO == [[self class] canUseTouchID]) {
|
||||
if (completionBlock) {
|
||||
completionBlock(NO);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSString *serviceName = self.keychainServiceName;
|
||||
NSData *passcodeData = [passcode dataUsingEncoding:NSUTF8StringEncoding];
|
||||
|
||||
dispatch_async(_queue, ^{
|
||||
|
||||
BOOL success = [[self class] saveKeychainItemWithServiceName:serviceName
|
||||
accountName:BKTouchIDManagerPasscodeAccountName
|
||||
data:passcodeData
|
||||
sacFlags:kSecAccessControlUserPresence];
|
||||
|
||||
if (success) {
|
||||
|
||||
BOOL enabled = YES;
|
||||
|
||||
success = [[self class] saveKeychainItemWithServiceName:serviceName
|
||||
accountName:BKTouchIDManagerTouchIDEnabledAccountName
|
||||
data:[NSData dataWithBytes:&enabled length:sizeof(BOOL)]
|
||||
sacFlags:0];
|
||||
}
|
||||
|
||||
if (completionBlock) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
completionBlock(success);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)loadPasscodeWithCompletionBlock:(void (^)(NSString *))completionBlock
|
||||
{
|
||||
if (NO == [[self class] canUseTouchID]) {
|
||||
if (completionBlock) {
|
||||
completionBlock(nil);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableDictionary *query = [NSMutableDictionary dictionaryWithDictionary:@{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: self.keychainServiceName,
|
||||
(__bridge id)kSecAttrAccount: BKTouchIDManagerPasscodeAccountName,
|
||||
(__bridge id)kSecReturnData: @YES }];
|
||||
|
||||
if (self.promptText) {
|
||||
query[(__bridge id)kSecUseOperationPrompt] = self.promptText;
|
||||
}
|
||||
|
||||
dispatch_async(_queue, ^{
|
||||
|
||||
CFTypeRef dataTypeRef = NULL;
|
||||
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(query), &dataTypeRef);
|
||||
|
||||
NSString *result = nil;
|
||||
|
||||
if (status == errSecSuccess) {
|
||||
|
||||
NSData *resultData = ( __bridge_transfer NSData *)dataTypeRef;
|
||||
result = [[NSString alloc] initWithData:resultData encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
if (completionBlock) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
completionBlock(result);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)deletePasscodeWithCompletionBlock:(void (^)(BOOL))completionBlock
|
||||
{
|
||||
dispatch_async(_queue, ^{
|
||||
|
||||
BOOL success = ([[self class] deleteKeychainItemWithServiceName:self.keychainServiceName accountName:BKTouchIDManagerPasscodeAccountName] &&
|
||||
[[self class] deleteKeychainItemWithServiceName:self.keychainServiceName accountName:BKTouchIDManagerTouchIDEnabledAccountName]);
|
||||
|
||||
if (completionBlock) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
completionBlock(success);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)isTouchIDEnabled
|
||||
{
|
||||
NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: self.keychainServiceName,
|
||||
(__bridge id)kSecAttrAccount: BKTouchIDManagerTouchIDEnabledAccountName,
|
||||
(__bridge id)kSecReturnData: @YES };
|
||||
|
||||
CFTypeRef dataTypeRef = NULL;
|
||||
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)(query), &dataTypeRef);
|
||||
|
||||
if (status == errSecSuccess) {
|
||||
|
||||
NSData *resultData = ( __bridge_transfer NSData *)dataTypeRef;
|
||||
BOOL result;
|
||||
[resultData getBytes:&result length:sizeof(BOOL)];
|
||||
|
||||
return result;
|
||||
|
||||
} else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Static Methods
|
||||
|
||||
+ (BOOL)saveKeychainItemWithServiceName:(NSString *)serviceName accountName:(NSString *)accountName data:(NSData *)data sacFlags:(SecAccessControlCreateFlags)sacFlags
|
||||
{
|
||||
// try to update first
|
||||
BOOL success = [self updateKeychainItemWithServiceName:serviceName accountName:accountName data:data];
|
||||
|
||||
if (success) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// try deleting when update failed (workaround for iOS 8 bug)
|
||||
[self deleteKeychainItemWithServiceName:serviceName accountName:accountName];
|
||||
|
||||
// try add
|
||||
return [self addKeychainItemWithServiceName:serviceName accountName:accountName data:data sacFlags:sacFlags];
|
||||
}
|
||||
|
||||
+ (BOOL)addKeychainItemWithServiceName:(NSString *)serviceName accountName:(NSString *)accountName data:(NSData *)data sacFlags:(SecAccessControlCreateFlags)sacFlags
|
||||
{
|
||||
CFErrorRef error = NULL;
|
||||
SecAccessControlRef sacObject = SecAccessControlCreateWithFlags(kCFAllocatorDefault,
|
||||
kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly,
|
||||
sacFlags, &error);
|
||||
|
||||
if (sacObject == NULL || error != NULL) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSDictionary *attributes = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: serviceName,
|
||||
(__bridge id)kSecAttrAccount: accountName,
|
||||
(__bridge id)kSecValueData: data,
|
||||
(__bridge id)kSecUseNoAuthenticationUI: @YES,
|
||||
(__bridge id)kSecAttrAccessControl: (__bridge_transfer id)sacObject };
|
||||
|
||||
OSStatus status = SecItemAdd((__bridge CFDictionaryRef)attributes, nil);
|
||||
|
||||
return (status == errSecSuccess);
|
||||
}
|
||||
|
||||
+ (BOOL)updateKeychainItemWithServiceName:(NSString *)serviceName accountName:(NSString *)accountName data:(NSData *)data
|
||||
{
|
||||
NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: serviceName,
|
||||
(__bridge id)kSecAttrAccount: accountName };
|
||||
|
||||
NSDictionary *changes = @{ (__bridge id)kSecValueData: data };
|
||||
|
||||
OSStatus status = SecItemUpdate((__bridge CFDictionaryRef)query, (__bridge CFDictionaryRef)changes);
|
||||
|
||||
return (status == errSecSuccess);
|
||||
}
|
||||
|
||||
+ (BOOL)deleteKeychainItemWithServiceName:(NSString *)serviceName accountName:(NSString *)accountName
|
||||
{
|
||||
NSDictionary *query = @{ (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
|
||||
(__bridge id)kSecAttrService: serviceName,
|
||||
(__bridge id)kSecAttrAccount: accountName };
|
||||
|
||||
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)(query));
|
||||
|
||||
return (status == errSecSuccess || status == errSecItemNotFound);
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// BKTouchIDSwitchView.h
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 10. 11..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol BKTouchIDSwitchViewDelegate;
|
||||
|
||||
|
||||
@interface BKTouchIDSwitchView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<BKTouchIDSwitchViewDelegate> delegate;
|
||||
|
||||
@property (nonatomic, strong) UIView *switchBackgroundView;
|
||||
@property (nonatomic, strong) UILabel *messageLabel;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UISwitch *touchIDSwitch;
|
||||
@property (nonatomic, strong) UIButton *doneButton;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol BKTouchIDSwitchViewDelegate <NSObject>
|
||||
|
||||
- (void)touchIDSwitchViewDidPressDoneButton:(BKTouchIDSwitchView *)view;
|
||||
|
||||
@end
|
|
@ -0,0 +1,125 @@
|
|||
//
|
||||
// BKTouchIDSwitchView.m
|
||||
// BKPasscodeViewDemo
|
||||
//
|
||||
// Created by Byungkook Jang on 2014. 10. 11..
|
||||
// Copyright (c) 2014년 Byungkook Jang. All rights reserved.
|
||||
//
|
||||
|
||||
#import "BKTouchIDSwitchView.h"
|
||||
|
||||
@implementation BKTouchIDSwitchView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self _initialize];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithCoder:(NSCoder *)aDecoder
|
||||
{
|
||||
self = [super initWithCoder:aDecoder];
|
||||
if (self) {
|
||||
[self _initialize];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)_initialize
|
||||
{
|
||||
self.switchBackgroundView = [[UIView alloc] init];
|
||||
self.switchBackgroundView.backgroundColor = [UIColor whiteColor];
|
||||
self.switchBackgroundView.layer.borderColor = [UIColor lightGrayColor].CGColor;
|
||||
self.switchBackgroundView.layer.borderWidth = .5f;
|
||||
[self addSubview:self.switchBackgroundView];
|
||||
|
||||
self.messageLabel = [[UILabel alloc] init];
|
||||
self.messageLabel.numberOfLines = 0;
|
||||
self.messageLabel.lineBreakMode = NSLineBreakByWordWrapping;
|
||||
self.messageLabel.textAlignment = NSTextAlignmentCenter;
|
||||
self.messageLabel.text = NSLocalizedStringFromTable(@"Do you want to use Touch ID for authentication?", @"BKPasscodeView", @"Touch ID를 사용하시겠습니까?");
|
||||
self.messageLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
|
||||
[self addSubview:self.messageLabel];
|
||||
|
||||
self.titleLabel = [[UILabel alloc] init];
|
||||
self.titleLabel.text = NSLocalizedStringFromTable(@"Enable Touch ID", @"BKPasscodeView", @"Touch ID 사용");
|
||||
self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
|
||||
[self addSubview:self.titleLabel];
|
||||
|
||||
self.touchIDSwitch = [[UISwitch alloc] init];
|
||||
[self addSubview:self.touchIDSwitch];
|
||||
|
||||
self.doneButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[self.doneButton.titleLabel setFont:[UIFont systemFontOfSize:20.f]];
|
||||
[self.doneButton setTitle:NSLocalizedStringFromTable(@"Done", @"BKPasscodeView", @"확인") forState:UIControlStateNormal];
|
||||
[self.doneButton addTarget:self action:@selector(doneButtonPressed:) forControlEvents:UIControlEventTouchUpInside];
|
||||
[self addSubview:self.doneButton];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
UIEdgeInsets contentInset = UIEdgeInsetsMake(20, 20, 20, 20);
|
||||
static CGFloat verticalSpaces[] = { 40, 30 };
|
||||
|
||||
CGRect contentBounds = UIEdgeInsetsInsetRect(self.bounds, contentInset);
|
||||
|
||||
self.messageLabel.frame = CGRectMake(0, 0, CGRectGetWidth(contentBounds), 0);
|
||||
[self.messageLabel sizeToFit];
|
||||
|
||||
[self.titleLabel sizeToFit];
|
||||
|
||||
[self.doneButton sizeToFit];
|
||||
|
||||
CGFloat contentHeight = (CGRectGetHeight(self.messageLabel.frame) + verticalSpaces[0] +
|
||||
CGRectGetHeight(self.touchIDSwitch.frame) + verticalSpaces[1] +
|
||||
CGRectGetHeight(self.doneButton.frame));
|
||||
|
||||
CGFloat offsetY = floorf((CGRectGetHeight(self.frame) - contentHeight) * 0.5f);
|
||||
|
||||
CGRect rect;
|
||||
|
||||
rect = self.messageLabel.frame;
|
||||
rect.origin = CGPointMake(contentInset.left, offsetY);
|
||||
rect.size.width = CGRectGetWidth(contentBounds);
|
||||
self.messageLabel.frame = rect;
|
||||
|
||||
offsetY += CGRectGetHeight(rect) + verticalSpaces[0];
|
||||
|
||||
rect = self.touchIDSwitch.frame;
|
||||
rect.origin = CGPointMake(CGRectGetMaxX(contentBounds) - CGRectGetWidth(self.touchIDSwitch.frame), offsetY);
|
||||
self.touchIDSwitch.frame = rect;
|
||||
|
||||
rect = self.titleLabel.frame;
|
||||
rect.origin = CGPointMake(contentInset.left, offsetY);
|
||||
rect.size.height = CGRectGetHeight(self.touchIDSwitch.frame);
|
||||
self.titleLabel.frame = rect;
|
||||
|
||||
offsetY += CGRectGetHeight(rect) + verticalSpaces[1];
|
||||
|
||||
rect = self.doneButton.frame;
|
||||
rect.size.width += 10;
|
||||
rect.size.height += 10;
|
||||
rect.origin.x = floorf((CGRectGetWidth(self.frame) - CGRectGetWidth(rect)) * 0.5f);
|
||||
rect.origin.y = offsetY;
|
||||
self.doneButton.frame = rect;
|
||||
|
||||
self.switchBackgroundView.frame = CGRectMake(-1,
|
||||
CGRectGetMinY(self.touchIDSwitch.frame) - 12,
|
||||
CGRectGetWidth(self.frame) + 2,
|
||||
CGRectGetHeight(self.touchIDSwitch.frame) + 24);
|
||||
|
||||
}
|
||||
|
||||
- (void)doneButtonPressed:(id)sender
|
||||
{
|
||||
if ([self.delegate respondsToSelector:@selector(touchIDSwitchViewDidPressDoneButton:)]) {
|
||||
[self.delegate touchIDSwitchViewDidPressDoneButton:self];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
|
||||
/**
|
||||
* The check mark to show selected asset.
|
||||
*/
|
||||
@interface CTAssetCheckmark : UIView
|
||||
|
||||
#pragma mark Customizing Appearance
|
||||
|
||||
/**
|
||||
* @name Customizing Appearance
|
||||
*/
|
||||
|
||||
/**
|
||||
* To set margin of the check mark from specific edges.
|
||||
*
|
||||
* @param margin The margin from the edges.
|
||||
* @param edgeX The layout attribute respresents vertical edge that the check mark pins to. Either `NSLayoutAttributeLeft` or `NSLayoutAttributeRight`.
|
||||
* @param edgeY The layout attribute respresents horizontal edge that the check mark pins to. Either `NSLayoutAttributeTop` or `NSLayoutAttributeBottom`.
|
||||
*/
|
||||
- (void)setMargin:(CGFloat)margin forVerticalEdge:(NSLayoutAttribute)edgeX horizontalEdge:(NSLayoutAttribute)edgeY UI_APPEARANCE_SELECTOR;
|
||||
|
||||
@end
|
|
@ -0,0 +1,165 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetCheckmark.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
#import "UIImage+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
/**
|
||||
* The check mark to show selected asset.
|
||||
*/
|
||||
@interface CTAssetCheckmark ()
|
||||
|
||||
|
||||
#pragma mark Managing Subviews
|
||||
|
||||
/**
|
||||
* The image view of the check mark shadow.
|
||||
*/
|
||||
@property (nonatomic, strong) UIImageView *shadowImageView;
|
||||
|
||||
/**
|
||||
* The image view of the check mark.
|
||||
*/
|
||||
@property (nonatomic, strong) UIImageView *checkmarkImageView;
|
||||
|
||||
|
||||
#pragma mark Managing Auto Layout
|
||||
|
||||
/**
|
||||
* The constraint for pinning the check mark to vertical edge.
|
||||
*/
|
||||
@property (nonatomic, strong) NSLayoutConstraint *verticalConstraint;
|
||||
|
||||
/**
|
||||
* The constraint for pinning the check mark to horizontal edge.
|
||||
*/
|
||||
@property (nonatomic, strong) NSLayoutConstraint *horizontalConstraint;
|
||||
|
||||
/**
|
||||
* Determines whether or not the constraints have been set up.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation CTAssetCheckmark
|
||||
|
||||
#pragma mark Initializing a Check Mark Object
|
||||
|
||||
/**
|
||||
* Designated Initializer
|
||||
*
|
||||
* @return an initialized check mark object
|
||||
*/
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
|
||||
if (self)
|
||||
{
|
||||
self.userInteractionEnabled = NO;
|
||||
self.isAccessibilityElement = NO;
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Setting up Subviews
|
||||
|
||||
/**
|
||||
* To setup subviews.
|
||||
*/
|
||||
- (void)setupViews
|
||||
{
|
||||
UIImage *shadowImage = [UIImage ctassetsPickerImageNamed:@"CheckmarkShadow"];
|
||||
UIImageView *shadowImageView = [[UIImageView alloc] initWithImage:shadowImage];
|
||||
shadowImageView.userInteractionEnabled = NO;
|
||||
self.shadowImageView = shadowImageView;
|
||||
[self addSubview:self.shadowImageView];
|
||||
|
||||
UIImage *checkmarkImage = [UIImage ctassetsPickerImageNamed:@"Checkmark"];
|
||||
checkmarkImage = [checkmarkImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
UIImageView *checkmarkImageView = [[UIImageView alloc] initWithImage:checkmarkImage];
|
||||
checkmarkImageView.userInteractionEnabled = NO;
|
||||
self.checkmarkImageView = checkmarkImageView;
|
||||
[self addSubview:self.checkmarkImageView];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Customizing Appearance
|
||||
|
||||
/**
|
||||
* To set margin of the check mark from specific edges.
|
||||
*
|
||||
* @param margin The margin from the edges.
|
||||
* @param edgeX The layout attribute respresents vertical edge that the check mark pins to. Either `NSLayoutAttributeLeft` or `NSLayoutAttributeRight`.
|
||||
* @param edgeY The layout attribute respresents horizontal edge that the check mark pins to. Either `NSLayoutAttributeTop` or `NSLayoutAttributeBottom`.
|
||||
*/
|
||||
- (void)setMargin:(CGFloat)margin forVerticalEdge:(NSLayoutAttribute)edgeX horizontalEdge:(NSLayoutAttribute)edgeY
|
||||
{
|
||||
NSAssert(edgeX == NSLayoutAttributeLeft || edgeX == NSLayoutAttributeRight, @"Vertical edge must be NSLayoutAttributeLeft or NSLayoutAttributeRight");
|
||||
NSAssert(edgeY == NSLayoutAttributeTop || edgeY == NSLayoutAttributeBottom, @"Horizontal edge must be NSLayoutAttributeTop or NSLayoutAttributeBottom");
|
||||
|
||||
[self.superview removeConstraints:@[self.verticalConstraint, self.horizontalConstraint]];
|
||||
self.verticalConstraint = [self autoPinEdgeToSuperviewEdge:(ALEdge)edgeX withInset:margin];
|
||||
self.horizontalConstraint = [self autoPinEdgeToSuperviewEdge:(ALEdge)edgeY withInset:margin];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Triggering Auto Layout
|
||||
|
||||
/**
|
||||
* Updates constraints of the check mark.
|
||||
*/
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
CGSize size = [UIImage ctassetsPickerImageNamed:@"CheckmarkShadow"].size;
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
|
||||
[self autoSetDimensionsToSize:size];
|
||||
}];
|
||||
|
||||
[self.shadowImageView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self.checkmarkImageView autoCenterInSuperview];
|
||||
|
||||
self.verticalConstraint = [self autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0];
|
||||
self.horizontalConstraint = [self autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:0];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
#import "CTAssetThumbnailStacks.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetCollectionViewCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong, readonly, nonnull) CTAssetThumbnailStacks *thumbnailStacks;
|
||||
|
||||
@property (nonatomic, weak, nullable) UIFont *titleFont UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic, strong, nullable) UIColor *titleTextColor UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic, strong, nullable) UIColor *selectedTitleTextColor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
@property (nonatomic, weak, nullable) UIFont *countFont UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic, strong, nullable) UIColor *countTextColor UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic, strong, nullable) UIColor *selectedCountTextColor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
@property (nonatomic, strong, nullable) UIColor *accessoryColor UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic, strong, nullable) UIColor *selectedAccessoryColor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
@property (nonatomic, weak, nullable) UIColor *selectedBackgroundColor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
|
||||
- (instancetype)initWithThumbnailSize:(CGSize)size reuseIdentifier:(nullable NSString *)reuseIdentifier;
|
||||
- (void)bind:(nonnull PHAssetCollection *)collection count:(NSUInteger)count;
|
||||
|
||||
@end
|
|
@ -0,0 +1,319 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetCollectionViewCell.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
#import "UIImage+CTAssetsPickerController.h"
|
||||
#import "NSNumberFormatter+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetCollectionViewCell ()
|
||||
|
||||
@property (nonatomic, assign) CGSize thumbnailSize;
|
||||
|
||||
@property (nonatomic, strong) CTAssetThumbnailStacks *thumbnailStacks;
|
||||
@property (nonatomic, strong) UIView *labelsView;
|
||||
@property (nonatomic, strong) UILabel *titleLabel;
|
||||
@property (nonatomic, strong) UILabel *countLabel;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@property (nonatomic, strong) PHAssetCollection *collection;
|
||||
@property (nonatomic, assign) NSUInteger count;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetCollectionViewCell
|
||||
|
||||
- (instancetype)initWithThumbnailSize:(CGSize)size reuseIdentifier:(NSString *)reuseIdentifier;
|
||||
{
|
||||
if (self = [super initWithStyle:UITableViewCellStyleDefault reuseIdentifier:reuseIdentifier])
|
||||
{
|
||||
_thumbnailSize = size;
|
||||
|
||||
_titleTextColor = CTAssetCollectionViewCellTitleTextColor;
|
||||
_selectedTitleTextColor = CTAssetCollectionViewCellTitleTextColor;
|
||||
_countTextColor = CTAssetCollectionViewCellCountTextColor;
|
||||
_selectedCountTextColor = CTAssetCollectionViewCellCountTextColor;
|
||||
|
||||
_accessoryColor = CTAssetCollectionViewCellAccessoryColor;
|
||||
_selectedAccessoryColor = CTAssetCollectionViewCellAccessoryColor;
|
||||
|
||||
self.opaque = YES;
|
||||
self.isAccessibilityElement = YES;
|
||||
self.textLabel.backgroundColor = self.backgroundColor;
|
||||
self.detailTextLabel.backgroundColor = self.backgroundColor;
|
||||
self.accessoryType = UITableViewCellAccessoryNone;
|
||||
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
CTAssetThumbnailStacks *thumbnailStacks = [CTAssetThumbnailStacks newAutoLayoutView];
|
||||
thumbnailStacks.thumbnailSize = self.thumbnailSize;
|
||||
self.thumbnailStacks = thumbnailStacks;
|
||||
|
||||
UILabel *titleLabel = [UILabel newAutoLayoutView];
|
||||
titleLabel.font = CTAssetCollectionViewCellTitleFont;
|
||||
titleLabel.textColor = self.titleTextColor;
|
||||
self.titleLabel = titleLabel;
|
||||
|
||||
UILabel *countLabel = [UILabel newAutoLayoutView];
|
||||
countLabel.font = CTAssetCollectionViewCellCountFont;
|
||||
countLabel.textColor = self.countTextColor;
|
||||
self.countLabel = countLabel;
|
||||
|
||||
UIView *labelsView = [UIView newAutoLayoutView];
|
||||
[labelsView addSubview:self.titleLabel];
|
||||
[labelsView addSubview:self.countLabel];
|
||||
self.labelsView = labelsView;
|
||||
|
||||
[self.contentView addSubview:self.thumbnailStacks];
|
||||
[self.contentView addSubview:self.labelsView];
|
||||
|
||||
UIImage *accessory = [UIImage ctassetsPickerImageNamed:@"DisclosureArrow"];
|
||||
accessory = [accessory imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
UIImageView *accessoryView = [[UIImageView alloc] initWithImage:accessory];
|
||||
accessoryView.tintColor = self.accessoryColor;
|
||||
self.accessoryView = accessoryView;
|
||||
}
|
||||
|
||||
- (void)setupPlaceholderImage
|
||||
{
|
||||
NSString *imageName = [self placeHolderImageNameOfCollectionSubtype:self.collection.assetCollectionSubtype];
|
||||
UIImage *image = [UIImage ctassetsPickerImageNamed:imageName];
|
||||
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
|
||||
for (CTAssetThumbnailView *thumbnailView in self.thumbnailStacks.thumbnailViews)
|
||||
{
|
||||
[thumbnailView bind:nil assetCollection:nil];
|
||||
thumbnailView.backgroundImage = image;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)placeHolderImageNameOfCollectionSubtype:(PHAssetCollectionSubtype)subtype
|
||||
{
|
||||
if (subtype == PHAssetCollectionSubtypeSmartAlbumUserLibrary)
|
||||
return @"GridEmptyCameraRoll";
|
||||
|
||||
else if (subtype == PHAssetCollectionSubtypeSmartAlbumAllHidden)
|
||||
return @"GridHiddenAlbum";
|
||||
|
||||
else if (subtype == PHAssetCollectionSubtypeAlbumCloudShared)
|
||||
return @"GridEmptyAlbumShared";
|
||||
|
||||
else
|
||||
return @"GridEmptyAlbum";
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Apperance
|
||||
|
||||
- (UIFont *)titleFont
|
||||
{
|
||||
return self.titleLabel.font;
|
||||
}
|
||||
|
||||
- (void)setTitleFont:(UIFont *)titleFont
|
||||
{
|
||||
UIFont *font = (titleFont) ? titleFont : CTAssetCollectionViewCellTitleFont;
|
||||
self.titleLabel.font = font;
|
||||
}
|
||||
|
||||
- (void)setTitleTextColor:(UIColor *)titleTextColor
|
||||
{
|
||||
UIColor *color = (titleTextColor) ? titleTextColor : CTAssetCollectionViewCellTitleTextColor;
|
||||
_titleTextColor = color;
|
||||
}
|
||||
|
||||
- (void)setSelectedTitleTextColor:(UIColor *)titleTextColor
|
||||
{
|
||||
UIColor *color = (titleTextColor) ? titleTextColor : CTAssetCollectionViewCellTitleTextColor;
|
||||
_selectedTitleTextColor = color;
|
||||
}
|
||||
|
||||
- (UIFont *)countFont
|
||||
{
|
||||
return self.countLabel.font;
|
||||
}
|
||||
|
||||
- (void)setCountFont:(UIFont *)countFont
|
||||
{
|
||||
UIFont *font = (countFont) ? countFont : CTAssetCollectionViewCellCountFont;
|
||||
self.countLabel.font = font;
|
||||
}
|
||||
|
||||
- (void)setCountTextColor:(UIColor *)countTextColor
|
||||
{
|
||||
UIColor *color = (countTextColor) ? countTextColor : CTAssetCollectionViewCellCountTextColor;
|
||||
_countTextColor = color;
|
||||
}
|
||||
|
||||
- (void)setSelectedCountTextColor:(UIColor *)countTextColor
|
||||
{
|
||||
UIColor *color = (countTextColor) ? countTextColor : CTAssetCollectionViewCellCountTextColor;
|
||||
_selectedCountTextColor = color;
|
||||
}
|
||||
|
||||
- (void)setAccessoryColor:(UIColor *)accessoryColor
|
||||
{
|
||||
UIColor *color = (accessoryColor) ? accessoryColor : CTAssetCollectionViewCellAccessoryColor;
|
||||
_accessoryColor = color;
|
||||
}
|
||||
|
||||
- (void)setSelectedAccessoryColor:(UIColor *)accessoryColor
|
||||
{
|
||||
UIColor *color = (accessoryColor) ? accessoryColor : CTAssetCollectionViewCellAccessoryColor;
|
||||
_selectedAccessoryColor = color;
|
||||
}
|
||||
|
||||
- (UIColor *)selectedBackgroundColor
|
||||
{
|
||||
return self.selectedBackgroundView.backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setSelectedBackgroundColor:(UIColor *)selectedBackgroundColor
|
||||
{
|
||||
if (!selectedBackgroundColor)
|
||||
self.selectedBackgroundView = nil;
|
||||
else
|
||||
{
|
||||
UIView *view = [UIView new];
|
||||
view.backgroundColor = selectedBackgroundColor;
|
||||
self.selectedBackgroundView = view;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Override highlighted / selected
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
|
||||
{
|
||||
[super setHighlighted:highlighted animated:animated];
|
||||
[self.thumbnailStacks setHighlighted:highlighted];
|
||||
|
||||
self.titleLabel.textColor = (highlighted) ? self.selectedTitleTextColor : self.titleTextColor;
|
||||
self.countLabel.textColor = (highlighted) ? self.selectedCountTextColor : self.countTextColor;
|
||||
self.accessoryView.tintColor = (highlighted) ? self.selectedAccessoryColor : self.accessoryColor;
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
|
||||
{
|
||||
[super setSelected:selected animated:animated];
|
||||
[self.thumbnailStacks setHighlighted:selected];
|
||||
|
||||
self.titleLabel.textColor = (selected) ? self.selectedTitleTextColor : self.titleTextColor;
|
||||
self.countLabel.textColor = (selected) ? self.selectedCountTextColor : self.countTextColor;
|
||||
self.accessoryView.tintColor = (selected) ? self.selectedAccessoryColor : self.accessoryColor;
|
||||
}
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
CGSize size = self.thumbnailSize;
|
||||
CGFloat top = self.thumbnailStacks.edgeInsets.top;
|
||||
size.height += top;
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
|
||||
[self.thumbnailStacks autoSetDimensionsToSize:size];
|
||||
}];
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{
|
||||
[self.thumbnailStacks autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeTrailing];
|
||||
}];
|
||||
|
||||
[self.labelsView autoAlignAxisToSuperviewAxis:ALAxisHorizontal];
|
||||
[self.labelsView autoPinEdge:ALEdgeLeading
|
||||
toEdge:ALEdgeTrailing
|
||||
ofView:self.thumbnailStacks
|
||||
withOffset:self.labelsView.layoutMargins.left
|
||||
relation:NSLayoutRelationGreaterThanOrEqual];
|
||||
|
||||
[self.titleLabel autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeBottom];
|
||||
[self.countLabel autoPinEdgesToSuperviewMarginsExcludingEdge:ALEdgeTop];
|
||||
[self.countLabel autoPinEdge:ALEdgeTop
|
||||
toEdge:ALEdgeBottom
|
||||
ofView:self.titleLabel
|
||||
withOffset:self.countLabel.layoutMargins.top
|
||||
relation:NSLayoutRelationGreaterThanOrEqual];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Bind asset collection
|
||||
|
||||
- (void)bind:(PHAssetCollection *)collection count:(NSUInteger)count
|
||||
{
|
||||
self.collection = collection;
|
||||
self.count = count;
|
||||
|
||||
[self setupPlaceholderImage];
|
||||
|
||||
self.titleLabel.text = collection.localizedTitle;
|
||||
|
||||
if (count != NSNotFound)
|
||||
{
|
||||
NSNumberFormatter *nf = [NSNumberFormatter new];
|
||||
self.countLabel.text = [nf ctassetsPickerStringFromAssetsCount:count];
|
||||
}
|
||||
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessibility label
|
||||
|
||||
- (NSString *)accessibilityLabel
|
||||
{
|
||||
NSString *title = self.titleLabel.text;
|
||||
NSString *count = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ Photos", nil), self.countLabel.text];
|
||||
|
||||
NSArray *labels = @[title, count];
|
||||
return [labels componentsJoinedByString:@","];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CTAssetCollectionViewController : UITableViewController
|
||||
|
||||
- (void)reloadUserInterface;
|
||||
|
||||
@end
|
|
@ -0,0 +1,564 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetsPickerController.h"
|
||||
#import "CTAssetsPickerController+Internal.h"
|
||||
#import "CTAssetCollectionViewController.h"
|
||||
#import "CTAssetCollectionViewCell.h"
|
||||
#import "CTAssetsGridViewController.h"
|
||||
#import "PHAssetCollection+CTAssetsPickerController.h"
|
||||
#import "PHAsset+CTAssetsPickerController.h"
|
||||
#import "PHImageManager+CTAssetsPickerController.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@interface CTAssetCollectionViewController()
|
||||
<PHPhotoLibraryChangeObserver, CTAssetsGridViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, weak) CTAssetsPickerController *picker;
|
||||
|
||||
@property (nonatomic, strong) UIBarButtonItem *cancelButton;
|
||||
@property (nonatomic, strong) UIBarButtonItem *doneButton;
|
||||
|
||||
@property (nonatomic, copy) NSArray *fetchResults;
|
||||
@property (nonatomic, copy) NSArray *assetCollections;
|
||||
@property (nonatomic, strong) PHCachingImageManager *imageManager;
|
||||
|
||||
@property (nonatomic, strong) PHAssetCollection *defaultAssetCollection;
|
||||
@property (nonatomic, assign) BOOL didShowDefaultAssetCollection;
|
||||
@property (nonatomic, assign) BOOL didSelectDefaultAssetCollection;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetCollectionViewController
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if (self = [super initWithStyle:UITableViewStylePlain])
|
||||
{
|
||||
_imageManager = [PHCachingImageManager new];
|
||||
[self addNotificationObserver];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
[self setupViews];
|
||||
[self localize];
|
||||
[self setupDefaultAssetCollection];
|
||||
[self setupFetchResults];
|
||||
[self registerChangeObserver];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
[self setupButtons];
|
||||
[self updateTitle:self.picker.selectedAssets];
|
||||
[self updateButton:self.picker.selectedAssets];
|
||||
[self selectDefaultAssetCollection];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self unregisterChangeObserver];
|
||||
[self removeNotificationObserver];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Reload user interface
|
||||
|
||||
- (void)reloadUserInterface
|
||||
{
|
||||
[self setupViews];
|
||||
[self setupButtons];
|
||||
[self localize];
|
||||
[self setupDefaultAssetCollection];
|
||||
[self setupFetchResults];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
- (CTAssetsPickerController *)picker
|
||||
{
|
||||
return (CTAssetsPickerController *)self.splitViewController.parentViewController;
|
||||
}
|
||||
|
||||
- (NSIndexPath *)indexPathForAssetCollection:(PHAssetCollection *)assetCollection
|
||||
{
|
||||
NSInteger row = [self.assetCollections indexOfObject:assetCollection];
|
||||
|
||||
if (row != NSNotFound)
|
||||
return [NSIndexPath indexPathForRow:row inSection:0];
|
||||
else
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
self.tableView.rowHeight = UITableViewAutomaticDimension;
|
||||
|
||||
self.tableView.estimatedRowHeight =
|
||||
self.picker.assetCollectionThumbnailSize.height +
|
||||
self.tableView.layoutMargins.top +
|
||||
self.tableView.layoutMargins.bottom;
|
||||
|
||||
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
}
|
||||
|
||||
- (void)setupButtons
|
||||
{
|
||||
if (self.doneButton == nil)
|
||||
{
|
||||
NSString *title = (self.picker.doneButtonTitle) ?
|
||||
self.picker.doneButtonTitle : CTAssetsPickerLocalizedString(@"Done", nil);
|
||||
|
||||
self.doneButton =
|
||||
[[UIBarButtonItem alloc] initWithTitle:title
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self.picker
|
||||
action:@selector(finishPickingAssets:)];
|
||||
}
|
||||
|
||||
if (self.cancelButton == nil)
|
||||
{
|
||||
self.cancelButton =
|
||||
[[UIBarButtonItem alloc] initWithTitle:CTAssetsPickerLocalizedString(@"Cancel", nil)
|
||||
style:UIBarButtonItemStylePlain
|
||||
target:self.picker
|
||||
action:@selector(dismiss:)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)localize
|
||||
{
|
||||
[self resetTitle];
|
||||
}
|
||||
|
||||
- (void)setupFetchResults
|
||||
{
|
||||
NSMutableArray *fetchResults = [NSMutableArray new];
|
||||
|
||||
for (NSNumber *subtypeNumber in self.picker.assetCollectionSubtypes)
|
||||
{
|
||||
PHAssetCollectionType type = [PHAssetCollection ctassetPickerAssetCollectionTypeOfSubtype:subtypeNumber.integerValue];
|
||||
PHAssetCollectionSubtype subtype = subtypeNumber.integerValue;
|
||||
|
||||
PHFetchResult *fetchResult =
|
||||
[PHAssetCollection fetchAssetCollectionsWithType:type
|
||||
subtype:subtype
|
||||
options:self.picker.assetCollectionFetchOptions];
|
||||
|
||||
[fetchResults addObject:fetchResult];
|
||||
}
|
||||
|
||||
self.fetchResults = [NSMutableArray arrayWithArray:fetchResults];
|
||||
|
||||
[self updateAssetCollections];
|
||||
[self reloadData];
|
||||
[self showDefaultAssetCollection];
|
||||
}
|
||||
|
||||
- (void)updateAssetCollections
|
||||
{
|
||||
NSMutableArray *assetCollections = [NSMutableArray new];
|
||||
|
||||
for (PHFetchResult *fetchResult in self.fetchResults)
|
||||
{
|
||||
for (PHAssetCollection *assetCollection in fetchResult)
|
||||
{
|
||||
BOOL showsAssetCollection = YES;
|
||||
|
||||
if (!self.picker.showsEmptyAlbums)
|
||||
{
|
||||
PHFetchOptions *options = [PHFetchOptions new];
|
||||
options.predicate = self.picker.assetsFetchOptions.predicate;
|
||||
|
||||
if ([options respondsToSelector:@selector(setFetchLimit:)])
|
||||
options.fetchLimit = 1;
|
||||
|
||||
NSInteger count = [assetCollection ctassetPikcerCountOfAssetsFetchedWithOptions:options];
|
||||
|
||||
showsAssetCollection = (count > 0);
|
||||
}
|
||||
|
||||
if (showsAssetCollection)
|
||||
[assetCollections addObject:assetCollection];
|
||||
}
|
||||
}
|
||||
|
||||
self.assetCollections = [NSMutableArray arrayWithArray:assetCollections];
|
||||
}
|
||||
|
||||
- (void)setupDefaultAssetCollection
|
||||
{
|
||||
if (!self.picker || self.picker.defaultAssetCollection == PHAssetCollectionSubtypeAny) {
|
||||
self.defaultAssetCollection = nil;
|
||||
return;
|
||||
}
|
||||
|
||||
PHAssetCollectionType type = [PHAssetCollection ctassetPickerAssetCollectionTypeOfSubtype:self.picker.defaultAssetCollection];
|
||||
PHFetchResult *fetchResult = [PHAssetCollection fetchAssetCollectionsWithType:type subtype:self.picker.defaultAssetCollection options:self.picker.assetCollectionFetchOptions];
|
||||
|
||||
self.defaultAssetCollection = fetchResult.firstObject;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Rotation
|
||||
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
|
||||
{
|
||||
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||
|
||||
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
||||
[self updateTitle:self.picker.selectedAssets];
|
||||
[self updateButton:self.picker.selectedAssets];
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)addNotificationObserver
|
||||
{
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(selectedAssetsChanged:)
|
||||
name:CTAssetsPickerSelectedAssetsDidChangeNotification
|
||||
object:nil];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(contentSizeCategoryChanged:)
|
||||
name:UIContentSizeCategoryDidChangeNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)removeNotificationObserver
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:CTAssetsPickerSelectedAssetsDidChangeNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIContentSizeCategoryDidChangeNotification object:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Photo library change observer
|
||||
|
||||
- (void)registerChangeObserver
|
||||
{
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
|
||||
}
|
||||
|
||||
- (void)unregisterChangeObserver
|
||||
{
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Photo library changed
|
||||
|
||||
- (void)photoLibraryDidChange:(PHChange *)changeInstance
|
||||
{
|
||||
// Call might come on any background queue. Re-dispatch to the main queue to handle it.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
NSMutableArray *updatedFetchResults = nil;
|
||||
|
||||
for (PHFetchResult *fetchResult in self.fetchResults)
|
||||
{
|
||||
PHFetchResultChangeDetails *changeDetails = [changeInstance changeDetailsForFetchResult:fetchResult];
|
||||
|
||||
if (changeDetails)
|
||||
{
|
||||
if (!updatedFetchResults)
|
||||
updatedFetchResults = [self.fetchResults mutableCopy];
|
||||
|
||||
updatedFetchResults[[self.fetchResults indexOfObject:fetchResult]] = changeDetails.fetchResultAfterChanges;
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedFetchResults)
|
||||
{
|
||||
self.fetchResults = updatedFetchResults;
|
||||
[self updateAssetCollections];
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Selected assets changed
|
||||
|
||||
- (void)selectedAssetsChanged:(NSNotification *)notification
|
||||
{
|
||||
NSArray *selectedAssets = (NSArray *)notification.object;
|
||||
[self updateTitle:selectedAssets];
|
||||
[self updateButton:selectedAssets];
|
||||
}
|
||||
|
||||
- (void)updateTitle:(NSArray *)selectedAssets
|
||||
{
|
||||
if ([self isTopViewController] && selectedAssets.count > 0)
|
||||
self.title = self.picker.selectedAssetsString;
|
||||
else
|
||||
[self resetTitle];
|
||||
}
|
||||
|
||||
- (void)updateButton:(NSArray *)selectedAssets
|
||||
{
|
||||
self.navigationItem.leftBarButtonItem = (self.picker.showsCancelButton) ? self.cancelButton : nil;
|
||||
self.navigationItem.rightBarButtonItem = [self isTopViewController] ? self.doneButton : nil;
|
||||
|
||||
if (self.picker.alwaysEnableDoneButton)
|
||||
self.navigationItem.rightBarButtonItem.enabled = YES;
|
||||
else
|
||||
self.navigationItem.rightBarButtonItem.enabled = (self.picker.selectedAssets.count > 0);
|
||||
}
|
||||
|
||||
- (BOOL)isTopViewController
|
||||
{
|
||||
UIViewController *vc = self.splitViewController.viewControllers.lastObject;
|
||||
|
||||
if ([vc isMemberOfClass:[UINavigationController class]])
|
||||
return (self == ((UINavigationController *)vc).topViewController);
|
||||
else
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)resetTitle
|
||||
{
|
||||
if (!self.picker.title)
|
||||
self.title = CTAssetsPickerLocalizedString(@"Photos", nil);
|
||||
else
|
||||
self.title = self.picker.title;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Content size category changed
|
||||
|
||||
- (void)contentSizeCategoryChanged:(NSNotification *)notification
|
||||
{
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Reload data
|
||||
|
||||
- (void)reloadData
|
||||
{
|
||||
if (self.assetCollections.count > 0)
|
||||
[self.tableView reloadData];
|
||||
else
|
||||
[self.picker showNoAssets];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.assetCollections.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
PHAssetCollection *collection = self.assetCollections[indexPath.row];
|
||||
NSUInteger count;
|
||||
|
||||
if (self.picker.showsNumberOfAssets)
|
||||
count = [collection ctassetPikcerCountOfAssetsFetchedWithOptions:self.picker.assetsFetchOptions];
|
||||
else
|
||||
count = NSNotFound;
|
||||
|
||||
static NSString *cellIdentifier = @"CellIdentifier";
|
||||
|
||||
CTAssetCollectionViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
|
||||
|
||||
if (cell == nil)
|
||||
cell = [[CTAssetCollectionViewCell alloc] initWithThumbnailSize:self.picker.assetCollectionThumbnailSize
|
||||
reuseIdentifier:cellIdentifier];
|
||||
|
||||
[cell bind:collection count:count];
|
||||
[self requestThumbnailsForCell:cell assetCollection:collection];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)requestThumbnailsForCell:(CTAssetCollectionViewCell *)cell assetCollection:(PHAssetCollection *)collection
|
||||
{
|
||||
NSUInteger count = cell.thumbnailStacks.thumbnailViews.count;
|
||||
NSArray *assets = [self posterAssetsFromAssetCollection:collection count:count];
|
||||
CGSize targetSize = [self.picker imageSizeForContainerSize:self.picker.assetCollectionThumbnailSize];
|
||||
|
||||
for (NSUInteger index = 0; index < count; index++)
|
||||
{
|
||||
CTAssetThumbnailView *thumbnailView = [cell.thumbnailStacks thumbnailAtIndex:index];
|
||||
thumbnailView.hidden = (assets.count > 0) ? YES : NO;
|
||||
|
||||
if (index < assets.count)
|
||||
{
|
||||
PHAsset *asset = assets[index];
|
||||
[self.imageManager ctassetsPickerRequestImageForAsset:asset
|
||||
targetSize:targetSize
|
||||
contentMode:PHImageContentModeAspectFill
|
||||
options:self.picker.thumbnailRequestOptions
|
||||
resultHandler:^(UIImage *image, NSDictionary *info){
|
||||
[thumbnailView setHidden:NO];
|
||||
[thumbnailView bind:image assetCollection:collection];
|
||||
}];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)posterAssetsFromAssetCollection:(PHAssetCollection *)collection count:(NSUInteger)count;
|
||||
{
|
||||
PHFetchOptions *options = [PHFetchOptions new];
|
||||
options.predicate = self.picker.assetsFetchOptions.predicate; // aligned specified predicate
|
||||
options.sortDescriptors = @[[NSSortDescriptor sortDescriptorWithKey:@"creationDate" ascending:YES]];
|
||||
|
||||
PHFetchResult *result = [PHAsset fetchKeyAssetsInAssetCollection:collection options:options];
|
||||
|
||||
NSUInteger location = 0;
|
||||
NSUInteger length = (result.count < count) ? result.count : count;
|
||||
NSArray *assets = [self itemsFromFetchResult:result range:NSMakeRange(location, length)];
|
||||
|
||||
return assets;
|
||||
}
|
||||
|
||||
- (NSArray *)itemsFromFetchResult:(PHFetchResult *)result range:(NSRange)range
|
||||
{
|
||||
if (result.count == 0)
|
||||
return nil;
|
||||
|
||||
NSIndexSet *indexSet = [NSIndexSet indexSetWithIndexesInRange:range];
|
||||
NSArray *array = [result objectsAtIndexes:indexSet];
|
||||
|
||||
return array;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table view delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
PHAssetCollection *collection = self.assetCollections[indexPath.row];
|
||||
|
||||
CTAssetsGridViewController *vc = [CTAssetsGridViewController new];
|
||||
vc.title = self.picker.selectedAssetsString ? : collection.localizedTitle;
|
||||
vc.assetCollection = collection;
|
||||
vc.delegate = self;
|
||||
|
||||
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
|
||||
nav.delegate = (id<UINavigationControllerDelegate>)self.picker;
|
||||
|
||||
[self.picker setShouldCollapseDetailViewController:NO];
|
||||
[self.splitViewController showDetailViewController:nav sender:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Show / select default asset collection
|
||||
|
||||
- (void)showDefaultAssetCollection
|
||||
{
|
||||
if (self.defaultAssetCollection && !self.didShowDefaultAssetCollection)
|
||||
{
|
||||
CTAssetsGridViewController *vc = [CTAssetsGridViewController new];
|
||||
vc.title = self.picker.selectedAssetsString ? : self.defaultAssetCollection.localizedTitle;
|
||||
vc.assetCollection = self.defaultAssetCollection;
|
||||
vc.delegate = self;
|
||||
|
||||
UINavigationController *nav = [[UINavigationController alloc] initWithRootViewController:vc];
|
||||
nav.delegate = (id<UINavigationControllerDelegate>)self.picker;
|
||||
|
||||
[self.picker setShouldCollapseDetailViewController:(self.picker.modalPresentationStyle == UIModalPresentationFormSheet)];
|
||||
[self.splitViewController showDetailViewController:nav sender:nil];
|
||||
|
||||
NSIndexPath *indexPath = [self indexPathForAssetCollection:self.defaultAssetCollection];
|
||||
[self.tableView selectRowAtIndexPath:indexPath animated:YES scrollPosition:UITableViewScrollPositionTop];
|
||||
|
||||
self.didShowDefaultAssetCollection = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)selectDefaultAssetCollection
|
||||
{
|
||||
if (self.defaultAssetCollection && !self.didSelectDefaultAssetCollection)
|
||||
{
|
||||
NSIndexPath *indexPath = [self indexPathForAssetCollection:self.defaultAssetCollection];
|
||||
|
||||
if (indexPath)
|
||||
{
|
||||
[UIView animateWithDuration:0.0f
|
||||
animations:^{
|
||||
[self.tableView selectRowAtIndexPath:indexPath
|
||||
animated:(!self.splitViewController.collapsed)
|
||||
scrollPosition:UITableViewScrollPositionTop];
|
||||
}
|
||||
completion:^(BOOL finished){
|
||||
// mimic clearsSelectionOnViewWillAppear
|
||||
if (finished && self.splitViewController.collapsed)
|
||||
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
}];
|
||||
}
|
||||
|
||||
self.didSelectDefaultAssetCollection = YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Grid view controller delegate
|
||||
|
||||
- (void)assetsGridViewController:(CTAssetsGridViewController *)picker photoLibraryDidChangeForAssetCollection:(PHAssetCollection *)assetCollection
|
||||
{
|
||||
NSIndexPath *indexPath = [self indexPathForAssetCollection:assetCollection];
|
||||
|
||||
if (indexPath)
|
||||
{
|
||||
[self.tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
|
||||
[self.tableView selectRowAtIndexPath:indexPath animated:NO scrollPosition:UITableViewScrollPositionNone];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface CTAssetItemViewController : UIViewController
|
||||
|
||||
@property (nonatomic, assign) BOOL allowsSelection;
|
||||
@property (nonatomic, strong, readonly) PHAsset *asset;
|
||||
@property (nonatomic, strong, readonly, nullable) UIImage *image;
|
||||
|
||||
+ (CTAssetItemViewController *)assetItemViewControllerForAsset:(PHAsset *)asset;
|
||||
|
||||
- (void)playAsset:(nullable id)sender;
|
||||
- (void)pauseAsset:(nullable id)sender;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,422 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerController.h"
|
||||
#import "CTAssetItemViewController.h"
|
||||
#import "CTAssetScrollView.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
#import "PHAsset+CTAssetsPickerController.h"
|
||||
#import "PHImageManager+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
|
||||
@interface CTAssetItemViewController ()
|
||||
|
||||
@property (nonatomic, weak) CTAssetsPickerController *picker;
|
||||
|
||||
@property (nonatomic, strong) PHAsset *asset;
|
||||
@property (nonatomic, strong) UIImage *image;
|
||||
|
||||
@property (nonatomic, strong) PHImageManager *imageManager;
|
||||
@property (nonatomic, assign) PHImageRequestID imageRequestID;
|
||||
@property (nonatomic, assign) PHImageRequestID playerItemRequestID;
|
||||
@property (nonatomic, strong) CTAssetScrollView *scrollView;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetItemViewController
|
||||
|
||||
+ (CTAssetItemViewController *)assetItemViewControllerForAsset:(PHAsset *)asset
|
||||
{
|
||||
return [[self alloc] initWithAsset:asset];
|
||||
}
|
||||
|
||||
- (instancetype)initWithAsset:(PHAsset *)asset
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
_imageManager = [PHImageManager defaultManager];
|
||||
self.asset = asset;
|
||||
self.allowsSelection = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
[self setupScrollViewButtons];
|
||||
[self requestAssetImage];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewWillDisappear:animated];
|
||||
[self pauseAsset:self.view];
|
||||
[self cancelRequestAsset];
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews
|
||||
{
|
||||
[super viewWillLayoutSubviews];
|
||||
|
||||
[self.scrollView setNeedsUpdateConstraints];
|
||||
[self.scrollView updateConstraintsIfNeeded];
|
||||
}
|
||||
|
||||
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
|
||||
{
|
||||
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
|
||||
|
||||
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
|
||||
[self.scrollView updateZoomScalesAndZoom:YES];
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
- (CTAssetsPickerController *)picker
|
||||
{
|
||||
return (CTAssetsPickerController *)self.splitViewController.parentViewController;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
CTAssetScrollView *scrollView = [CTAssetScrollView newAutoLayoutView];
|
||||
scrollView.allowsSelection = self.allowsSelection;
|
||||
|
||||
self.scrollView = scrollView;
|
||||
[self.view addSubview:self.scrollView];
|
||||
[self.view layoutIfNeeded];
|
||||
}
|
||||
|
||||
- (void)setupScrollViewButtons
|
||||
{
|
||||
CTAssetPlayButton *playButton = self.scrollView.playButton;
|
||||
[playButton addTarget:self action:@selector(playAsset:) forControlEvents:UIControlEventTouchUpInside];
|
||||
|
||||
CTAssetSelectionButton *selectionButton = self.scrollView.selectionButton;
|
||||
|
||||
selectionButton.enabled = [self assetScrollView:self.scrollView shouldEnableAsset:self.asset];
|
||||
selectionButton.selected = [self.picker.selectedAssets containsObject:self.asset];
|
||||
|
||||
[selectionButton addTarget:self action:@selector(selectionButtonTouchDown:) forControlEvents:UIControlEventTouchDown];
|
||||
[selectionButton addTarget:self action:@selector(selectionButtonTouchUpInside:) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Cancel request
|
||||
|
||||
- (void)cancelRequestAsset
|
||||
{
|
||||
[self cancelRequestImage];
|
||||
[self cancelRequestPlayerItem];
|
||||
}
|
||||
|
||||
- (void)cancelRequestImage
|
||||
{
|
||||
if (self.imageRequestID)
|
||||
{
|
||||
[self.scrollView setProgress:1];
|
||||
[self.imageManager cancelImageRequest:self.imageRequestID];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cancelRequestPlayerItem
|
||||
{
|
||||
if (self.playerItemRequestID)
|
||||
{
|
||||
[self.scrollView stopActivityAnimating];
|
||||
[self.imageManager cancelImageRequest:self.playerItemRequestID];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Request image
|
||||
|
||||
- (void)requestAssetImage
|
||||
{
|
||||
[self.scrollView setProgress:0];
|
||||
|
||||
CGSize targetSize = [self targetImageSize];
|
||||
PHImageRequestOptions *options = [self imageRequestOptions];
|
||||
|
||||
self.imageRequestID =
|
||||
[self.imageManager ctassetsPickerRequestImageForAsset:self.asset
|
||||
targetSize:targetSize
|
||||
contentMode:PHImageContentModeAspectFit
|
||||
options:options
|
||||
resultHandler:^(UIImage *image, NSDictionary *info) {
|
||||
|
||||
// this image is set for transition animation
|
||||
self.image = image;
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
NSError *error = info[PHImageErrorKey];
|
||||
|
||||
if (error)
|
||||
[self showRequestImageError:error title:nil];
|
||||
else
|
||||
[self.scrollView bind:self.asset image:image requestInfo:info];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (CGSize)targetImageSize
|
||||
{
|
||||
UIScreen *screen = UIScreen.mainScreen;
|
||||
CGFloat scale = screen.scale;
|
||||
return CGSizeMake(CGRectGetWidth(screen.bounds) * scale, CGRectGetHeight(screen.bounds) * scale);
|
||||
}
|
||||
|
||||
- (PHImageRequestOptions *)imageRequestOptions
|
||||
{
|
||||
PHImageRequestOptions *options = [PHImageRequestOptions new];
|
||||
options.networkAccessAllowed = YES;
|
||||
options.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.scrollView setProgress:progress];
|
||||
});
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Request player item
|
||||
|
||||
- (void)requestAssetPlayerItem:(id)sender
|
||||
{
|
||||
[self.scrollView startActivityAnimating];
|
||||
|
||||
PHVideoRequestOptions *options = [self videoRequestOptions];
|
||||
|
||||
self.playerItemRequestID =
|
||||
[self.imageManager requestPlayerItemForVideo:self.asset
|
||||
options:options
|
||||
resultHandler:^(AVPlayerItem *playerItem, NSDictionary *info) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
NSError *error = info[PHImageErrorKey];
|
||||
NSString * title = CTAssetsPickerLocalizedString(@"Cannot Play Stream Video", nil);
|
||||
|
||||
if (error)
|
||||
[self showRequestVideoError:error title:title];
|
||||
else
|
||||
[self.scrollView bind:playerItem requestInfo:info];
|
||||
});
|
||||
}];
|
||||
}
|
||||
|
||||
- (PHVideoRequestOptions *)videoRequestOptions
|
||||
{
|
||||
PHVideoRequestOptions *options = [PHVideoRequestOptions new];
|
||||
options.networkAccessAllowed = YES;
|
||||
options.progressHandler = ^(double progress, NSError *error, BOOL *stop, NSDictionary *info) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
//do nothing
|
||||
});
|
||||
};
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Request error
|
||||
|
||||
- (void)showRequestImageError:(NSError *)error title:(NSString *)title
|
||||
{
|
||||
[self.scrollView setProgress:1];
|
||||
[self showRequestError:error title:title];
|
||||
}
|
||||
|
||||
- (void)showRequestVideoError:(NSError *)error title:(NSString *)title
|
||||
{
|
||||
[self.scrollView stopActivityAnimating];
|
||||
[self showRequestError:error title:title];
|
||||
}
|
||||
|
||||
- (void)showRequestError:(NSError *)error title:(NSString *)title
|
||||
{
|
||||
UIAlertController *alert =
|
||||
[UIAlertController alertControllerWithTitle:title
|
||||
message:error.localizedDescription
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
UIAlertAction *action =
|
||||
[UIAlertAction actionWithTitle:CTAssetsPickerLocalizedString(@"OK", nil)
|
||||
style:UIAlertActionStyleDefault
|
||||
handler:nil];
|
||||
|
||||
[alert addAction:action];
|
||||
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Playback
|
||||
|
||||
- (void)playAsset:(id)sender
|
||||
{
|
||||
if (!self.scrollView.player)
|
||||
[self requestAssetPlayerItem:sender];
|
||||
else
|
||||
[self.scrollView playVideo];
|
||||
}
|
||||
|
||||
- (void)pauseAsset:(id)sender
|
||||
{
|
||||
if (!self.scrollView.player)
|
||||
[self cancelRequestPlayerItem];
|
||||
else
|
||||
[self.scrollView pauseVideo];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Selection
|
||||
|
||||
- (void)selectionButtonTouchDown:(id)sender
|
||||
{
|
||||
PHAsset *asset = self.asset;
|
||||
CTAssetScrollView *scrollView = self.scrollView;
|
||||
|
||||
if ([self assetScrollView:scrollView shouldHighlightAsset:asset])
|
||||
[self assetScrollView:scrollView didHighlightAsset:asset];
|
||||
}
|
||||
|
||||
- (void)selectionButtonTouchUpInside:(id)sender
|
||||
{
|
||||
PHAsset *asset = self.asset;
|
||||
CTAssetScrollView *scrollView = self.scrollView;
|
||||
CTAssetSelectionButton *selectionButton = scrollView.selectionButton;
|
||||
|
||||
|
||||
if (!selectionButton.selected)
|
||||
{
|
||||
if ([self assetScrollView:scrollView shouldSelectAsset:asset])
|
||||
{
|
||||
[self.picker selectAsset:asset];
|
||||
[selectionButton setSelected:YES];
|
||||
[self assetScrollView:scrollView didSelectAsset:asset];
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
if ([self assetScrollView:scrollView shouldDeselectAsset:asset])
|
||||
{
|
||||
[self.picker deselectAsset:asset];
|
||||
[selectionButton setSelected:NO];
|
||||
[self assetScrollView:scrollView didDeselectAsset:asset];
|
||||
}
|
||||
}
|
||||
|
||||
[self assetScrollView:self.scrollView didUnhighlightAsset:self.asset];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Asset scrollView delegate
|
||||
|
||||
- (BOOL)assetScrollView:(CTAssetScrollView *)scrollView shouldEnableAsset:(PHAsset *)asset
|
||||
{
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldEnableAsset:)])
|
||||
return [self.picker.delegate assetsPickerController:self.picker shouldEnableAsset:asset];
|
||||
else
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)assetScrollView:(CTAssetScrollView *)scrollView shouldSelectAsset:(PHAsset *)asset
|
||||
{
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldSelectAsset:)])
|
||||
return [self.picker.delegate assetsPickerController:self.picker shouldSelectAsset:asset];
|
||||
else
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)assetScrollView:(CTAssetScrollView *)scrollView didSelectAsset:(PHAsset *)asset
|
||||
{
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didSelectAsset:)])
|
||||
[self.picker.delegate assetsPickerController:self.picker didSelectAsset:asset];
|
||||
}
|
||||
|
||||
- (BOOL)assetScrollView:(CTAssetScrollView *)scrollView shouldDeselectAsset:(PHAsset *)asset
|
||||
{
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldDeselectAsset:)])
|
||||
return [self.picker.delegate assetsPickerController:self.picker shouldDeselectAsset:asset];
|
||||
else
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)assetScrollView:(CTAssetScrollView *)scrollView didDeselectAsset:(PHAsset *)asset
|
||||
{
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didDeselectAsset:)])
|
||||
[self.picker.delegate assetsPickerController:self.picker didDeselectAsset:asset];
|
||||
}
|
||||
|
||||
- (BOOL)assetScrollView:(CTAssetScrollView *)scrollView shouldHighlightAsset:(PHAsset *)asset
|
||||
{
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldHighlightAsset:)])
|
||||
return [self.picker.delegate assetsPickerController:self.picker shouldHighlightAsset:asset];
|
||||
else
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)assetScrollView:(CTAssetScrollView *)scrollView didHighlightAsset:(PHAsset *)asset
|
||||
{
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didHighlightAsset:)])
|
||||
[self.picker.delegate assetsPickerController:self.picker didHighlightAsset:asset];
|
||||
}
|
||||
|
||||
- (void)assetScrollView:(CTAssetScrollView *)scrollView didUnhighlightAsset:(PHAsset *)asset
|
||||
{
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didUnhighlightAsset:)])
|
||||
[self.picker.delegate assetsPickerController:self.picker didUnhighlightAsset:asset];
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CTAssetPlayButton : UIControl
|
||||
|
||||
@end
|
|
@ -0,0 +1,180 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetPlayButton.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
#import "UIImage+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetPlayButton ()
|
||||
|
||||
@property (nonatomic, strong) UIVisualEffectView *blurView;
|
||||
@property (nonatomic, strong) UIVisualEffectView *vibrancyView;
|
||||
@property (nonatomic, strong) UIView *vibrancyFill;
|
||||
|
||||
@property (nonatomic, strong) UIView *highlightedView;
|
||||
@property (nonatomic, strong) UIView *colorView;
|
||||
|
||||
@property (nonatomic, strong) UIImageView *glyphMask;
|
||||
@property (nonatomic, strong) UIImageView *buttonMask;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetPlayButton
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
|
||||
if (self)
|
||||
{
|
||||
self.isAccessibilityElement = YES;
|
||||
self.accessibilityTraits = UIAccessibilityTraitButton;
|
||||
|
||||
[self setupViews];
|
||||
[self localize];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
[self setupEffectViews];
|
||||
[self setupHightlightedView];
|
||||
[self setupColorView];
|
||||
[self setupMaskViews];
|
||||
}
|
||||
|
||||
- (void)setupEffectViews
|
||||
{
|
||||
// Blur effect
|
||||
UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
|
||||
UIVisualEffectView *blurView = [[UIVisualEffectView alloc] initWithEffect:blurEffect];
|
||||
blurView.userInteractionEnabled = NO;
|
||||
self.blurView = blurView;
|
||||
|
||||
// Vibrancy effect
|
||||
UIVibrancyEffect *vibrancyEffect = [UIVibrancyEffect effectForBlurEffect:blurEffect];
|
||||
UIVisualEffectView *vibrancyView = [[UIVisualEffectView alloc] initWithEffect:vibrancyEffect];
|
||||
vibrancyView.userInteractionEnabled = NO;
|
||||
self.vibrancyView = vibrancyView;
|
||||
|
||||
UIView *vibrancyFill = [UIView newAutoLayoutView];
|
||||
vibrancyFill.backgroundColor = [UIColor whiteColor];
|
||||
vibrancyFill.userInteractionEnabled = NO;
|
||||
self.vibrancyFill = vibrancyFill;
|
||||
|
||||
// Add fill to the vibrancy view
|
||||
[vibrancyView.contentView addSubview:self.vibrancyFill];
|
||||
[blurView.contentView addSubview:self.vibrancyView];
|
||||
|
||||
[self addSubview:blurView];
|
||||
}
|
||||
|
||||
- (void)setupHightlightedView
|
||||
{
|
||||
UIView *highlightedView = [UIView newAutoLayoutView];
|
||||
highlightedView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.5];
|
||||
highlightedView.userInteractionEnabled = NO;
|
||||
highlightedView.hidden = YES;
|
||||
self.highlightedView = highlightedView;
|
||||
|
||||
[self addSubview:self.highlightedView];
|
||||
}
|
||||
|
||||
- (void)setupColorView
|
||||
{
|
||||
UIView *colorView = [UIView newAutoLayoutView];
|
||||
colorView.backgroundColor = [UIColor colorWithWhite:1 alpha:0.8];
|
||||
colorView.userInteractionEnabled = NO;
|
||||
self.colorView = colorView;
|
||||
|
||||
[self addSubview:self.colorView];
|
||||
}
|
||||
|
||||
- (void)setupMaskViews
|
||||
{
|
||||
UIImage *glyphMaskImage = [UIImage ctassetsPickerImageNamed:@"VideoPlayGlyphMask"];
|
||||
UIImageView *glyphMask = [[UIImageView alloc] initWithImage:glyphMaskImage];
|
||||
glyphMask.userInteractionEnabled = NO;
|
||||
self.colorView.maskView = glyphMask;
|
||||
|
||||
UIImage *buttonMaskImage = [UIImage ctassetsPickerImageNamed:@"VideoPlayButtonMask"];
|
||||
UIImageView *buttonMask = [[UIImageView alloc] initWithImage:buttonMaskImage];
|
||||
buttonMask.userInteractionEnabled = NO;
|
||||
self.maskView = buttonMask;
|
||||
}
|
||||
|
||||
- (void)localize
|
||||
{
|
||||
self.accessibilityLabel = CTAssetsPickerLocalizedString(@"Play", nil);
|
||||
}
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
CGSize size = [UIImage ctassetsPickerImageNamed:@"VideoPlayButtonMask"].size;
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
|
||||
[self autoSetDimensionsToSize:size];
|
||||
}];
|
||||
|
||||
[self.blurView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self.vibrancyView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self.vibrancyFill autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self.highlightedView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self.colorView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - States
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
super.highlighted = highlighted;
|
||||
self.highlightedView.hidden = !highlighted;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "CTAssetItemViewController.h"
|
||||
#import "CTAssetPlayButton.h"
|
||||
#import "CTAssetSelectionButton.h"
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString * const CTAssetScrollViewDidTapNotification;
|
||||
extern NSString * const CTAssetScrollViewPlayerWillPlayNotification;
|
||||
extern NSString * const CTAssetScrollViewPlayerWillPauseNotification;
|
||||
|
||||
|
||||
@interface CTAssetScrollView : UIScrollView
|
||||
|
||||
@property (nonatomic, assign) BOOL allowsSelection;
|
||||
|
||||
@property (nonatomic, strong, readonly, nullable) UIImage *image;
|
||||
@property (nonatomic, strong, readonly, nullable) AVPlayer *player;
|
||||
|
||||
@property (nonatomic, strong, readonly) UIImageView *imageView;
|
||||
@property (nonatomic, strong, readonly) CTAssetPlayButton *playButton;
|
||||
@property (nonatomic, strong, readonly) CTAssetSelectionButton *selectionButton;
|
||||
|
||||
|
||||
- (void)startActivityAnimating;
|
||||
- (void)stopActivityAnimating;
|
||||
|
||||
- (void)setProgress:(CGFloat)progress;
|
||||
|
||||
- (void)bind:(PHAsset *)asset image:(nullable UIImage *)image requestInfo:(nullable NSDictionary<NSString*, id> *)info;
|
||||
- (void)bind:(AVPlayerItem *)playerItem requestInfo:(nullable NSDictionary *)info;
|
||||
|
||||
- (void)updateZoomScalesAndZoom:(BOOL)zoom;
|
||||
|
||||
- (void)playVideo;
|
||||
- (void)pauseVideo;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,713 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetScrollView.h"
|
||||
#import "CTAssetPlayButton.h"
|
||||
#import "PHAsset+CTAssetsPickerController.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
#import "UIImage+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
|
||||
NSString * const CTAssetScrollViewDidTapNotification = @"CTAssetScrollViewDidTapNotification";
|
||||
NSString * const CTAssetScrollViewPlayerWillPlayNotification = @"CTAssetScrollViewPlayerWillPlayNotification";
|
||||
NSString * const CTAssetScrollViewPlayerWillPauseNotification = @"CTAssetScrollViewPlayerWillPauseNotification";
|
||||
|
||||
|
||||
|
||||
|
||||
@interface CTAssetScrollView ()
|
||||
<UIScrollViewDelegate, UIGestureRecognizerDelegate>
|
||||
|
||||
@property (nonatomic, strong) PHAsset *asset;
|
||||
@property (nonatomic, strong) UIImage *image;
|
||||
@property (nonatomic, strong) AVPlayer *player;
|
||||
|
||||
@property (nonatomic, assign) BOOL didLoadPlayerItem;
|
||||
|
||||
@property (nonatomic, assign) CGFloat perspectiveZoomScale;
|
||||
|
||||
@property (nonatomic, strong) UIImageView *imageView;
|
||||
|
||||
@property (nonatomic, strong) UIProgressView *progressView;
|
||||
@property (nonatomic, strong) UIActivityIndicatorView *activityView;
|
||||
@property (nonatomic, strong) CTAssetPlayButton *playButton;
|
||||
@property (nonatomic, strong) CTAssetSelectionButton *selectionButton;
|
||||
|
||||
@property (nonatomic, assign) BOOL shouldUpdateConstraints;
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetScrollView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
|
||||
if (self)
|
||||
{
|
||||
_shouldUpdateConstraints = YES;
|
||||
self.allowsSelection = NO;
|
||||
self.showsVerticalScrollIndicator = NO;
|
||||
self.showsHorizontalScrollIndicator = NO;
|
||||
self.bouncesZoom = YES;
|
||||
self.decelerationRate = UIScrollViewDecelerationRateFast;
|
||||
self.delegate = self;
|
||||
|
||||
[self setupViews];
|
||||
[self addGestureRecognizers];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self removePlayerNotificationObserver];
|
||||
[self removePlayerLoadedTimeRangesObserver];
|
||||
[self removePlayerRateObserver];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
UIImageView *imageView = [UIImageView new];
|
||||
imageView.isAccessibilityElement = YES;
|
||||
imageView.accessibilityTraits = UIAccessibilityTraitImage;
|
||||
self.imageView = imageView;
|
||||
[self addSubview:self.imageView];
|
||||
|
||||
UIProgressView *progressView =
|
||||
[[UIProgressView alloc] initWithProgressViewStyle:UIProgressViewStyleDefault];
|
||||
self.progressView = progressView;
|
||||
[self addSubview:self.progressView];
|
||||
|
||||
UIActivityIndicatorView *activityView =
|
||||
[[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
||||
self.activityView = activityView;
|
||||
[self addSubview:self.activityView];
|
||||
|
||||
CTAssetPlayButton *playButton = [CTAssetPlayButton newAutoLayoutView];
|
||||
self.playButton = playButton;
|
||||
[self addSubview:self.playButton];
|
||||
|
||||
CTAssetSelectionButton *selectionButton = [CTAssetSelectionButton newAutoLayoutView];
|
||||
self.selectionButton = selectionButton;
|
||||
[self addSubview:self.selectionButton];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
[self updateSelectionButtonIfNeeded];
|
||||
[self autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self updateProgressConstraints];
|
||||
[self updateActivityConstraints];
|
||||
[self updateButtonsConstraints];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[self updateContentFrame];
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
- (void)updateSelectionButtonIfNeeded
|
||||
{
|
||||
if (!self.allowsSelection)
|
||||
{
|
||||
[self.selectionButton removeFromSuperview];
|
||||
self.selectionButton = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateProgressConstraints
|
||||
{
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow forConstraints:^{
|
||||
[self.progressView autoConstrainAttribute:ALAttributeLeading toAttribute:ALAttributeLeading ofView:self.superview withMultiplier:1 relation:NSLayoutRelationEqual];
|
||||
[self.progressView autoConstrainAttribute:ALAttributeTrailing toAttribute:ALAttributeTrailing ofView:self.superview withMultiplier:1 relation:NSLayoutRelationEqual];
|
||||
[self.progressView autoConstrainAttribute:ALAttributeBottom toAttribute:ALAttributeBottom ofView:self.superview withMultiplier:1 relation:NSLayoutRelationEqual];
|
||||
}];
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{
|
||||
[self.progressView autoConstrainAttribute:ALAttributeLeading toAttribute:ALAttributeLeading ofView:self.imageView withMultiplier:1 relation:NSLayoutRelationGreaterThanOrEqual];
|
||||
[self.progressView autoConstrainAttribute:ALAttributeTrailing toAttribute:ALAttributeTrailing ofView:self.imageView withMultiplier:1 relation:NSLayoutRelationLessThanOrEqual];
|
||||
[self.progressView autoConstrainAttribute:ALAttributeBottom toAttribute:ALAttributeBottom ofView:self.imageView withMultiplier:1 relation:NSLayoutRelationLessThanOrEqual];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateActivityConstraints
|
||||
{
|
||||
[self.activityView autoAlignAxis:ALAxisVertical toSameAxisOfView:self.superview];
|
||||
[self.activityView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.superview];
|
||||
}
|
||||
|
||||
- (void)updateButtonsConstraints
|
||||
{
|
||||
[self.playButton autoAlignAxis:ALAxisVertical toSameAxisOfView:self.superview];
|
||||
[self.playButton autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.superview];
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultLow forConstraints:^{
|
||||
[self.selectionButton autoConstrainAttribute:ALAttributeTrailing toAttribute:ALAttributeTrailing ofView:self.superview withOffset:-self.layoutMargins.right relation:NSLayoutRelationEqual];
|
||||
[self.selectionButton autoConstrainAttribute:ALAttributeBottom toAttribute:ALAttributeBottom ofView:self.superview withOffset:-self.layoutMargins.bottom relation:NSLayoutRelationEqual];
|
||||
}];
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{
|
||||
[self.selectionButton autoConstrainAttribute:ALAttributeTrailing toAttribute:ALAttributeTrailing ofView:self.imageView withOffset:-self.layoutMargins.right relation:NSLayoutRelationLessThanOrEqual];
|
||||
[self.selectionButton autoConstrainAttribute:ALAttributeBottom toAttribute:ALAttributeBottom ofView:self.imageView withOffset:-self.layoutMargins.bottom relation:NSLayoutRelationLessThanOrEqual];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateContentFrame
|
||||
{
|
||||
CGSize boundsSize = self.bounds.size;
|
||||
|
||||
CGFloat w = self.zoomScale * self.asset.pixelWidth;
|
||||
CGFloat h = self.zoomScale * self.asset.pixelHeight;
|
||||
|
||||
CGFloat dx = (boundsSize.width - w) / 2.0;
|
||||
CGFloat dy = (boundsSize.height - h) / 2.0;
|
||||
|
||||
self.contentOffset = CGPointZero;
|
||||
self.imageView.frame = CGRectMake(dx, dy, w, h);
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Start/stop loading animation
|
||||
|
||||
- (void)startActivityAnimating
|
||||
{
|
||||
[self.playButton setHidden:YES];
|
||||
[self.selectionButton setHidden:YES];
|
||||
[self.activityView startAnimating];
|
||||
[self postPlayerWillPlayNotification];
|
||||
}
|
||||
|
||||
- (void)stopActivityAnimating
|
||||
{
|
||||
[self.playButton setHidden:NO];
|
||||
[self.selectionButton setHidden:NO];
|
||||
[self.activityView stopAnimating];
|
||||
[self postPlayerWillPauseNotification];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Set progress
|
||||
|
||||
- (void)setProgress:(CGFloat)progress
|
||||
{
|
||||
#if !defined(CT_APP_EXTENSIONS)
|
||||
[UIApplication sharedApplication].networkActivityIndicatorVisible = progress < 1;
|
||||
#endif
|
||||
[self.progressView setProgress:progress animated:(progress < 1)];
|
||||
self.progressView.hidden = progress == 1;
|
||||
}
|
||||
|
||||
// To mimic image downloading progress
|
||||
// as PHImageRequestOptions does not work as expected
|
||||
- (void)mimicProgress
|
||||
{
|
||||
CGFloat progress = self.progressView.progress;
|
||||
|
||||
if (progress < 0.95)
|
||||
{
|
||||
int lowerbound = progress * 100 + 1;
|
||||
int upperbound = 95;
|
||||
|
||||
int random = lowerbound + arc4random() % (upperbound - lowerbound);
|
||||
CGFloat randomProgress = random / 100.0f;
|
||||
|
||||
[self setProgress:randomProgress];
|
||||
|
||||
NSInteger randomDelay = 1 + arc4random() % (3 - 1);
|
||||
[self performSelector:@selector(mimicProgress) withObject:nil afterDelay:randomDelay];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - asset size
|
||||
|
||||
- (CGSize)assetSize
|
||||
{
|
||||
return CGSizeMake(self.asset.pixelWidth, self.asset.pixelHeight);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Bind asset image
|
||||
|
||||
- (void)bind:(PHAsset *)asset image:(UIImage *)image requestInfo:(NSDictionary *)info
|
||||
{
|
||||
self.asset = asset;
|
||||
self.imageView.accessibilityLabel = asset.accessibilityLabel;
|
||||
self.playButton.hidden = [asset ctassetsPickerIsPhoto];
|
||||
|
||||
BOOL isDegraded = [info[PHImageResultIsDegradedKey] boolValue];
|
||||
|
||||
if (self.image == nil || !isDegraded)
|
||||
{
|
||||
BOOL zoom = (!self.image);
|
||||
self.image = image;
|
||||
self.imageView.image = image;
|
||||
|
||||
if (isDegraded)
|
||||
[self mimicProgress];
|
||||
else
|
||||
[self setProgress:1];
|
||||
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
[self updateZoomScalesAndZoom:zoom];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Bind player item
|
||||
|
||||
- (void)bind:(AVPlayerItem *)playerItem requestInfo:(NSDictionary *)info
|
||||
{
|
||||
[self unbindPlayerItem];
|
||||
|
||||
AVPlayer *player = [AVPlayer playerWithPlayerItem:playerItem];
|
||||
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
|
||||
playerLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
||||
|
||||
CALayer *layer = self.imageView.layer;
|
||||
[layer addSublayer:playerLayer];
|
||||
playerLayer.frame = layer.bounds;
|
||||
|
||||
self.player = player;
|
||||
|
||||
[self addPlayerNotificationObserver];
|
||||
[self addPlayerLoadedTimeRangesObserver];
|
||||
}
|
||||
|
||||
- (void)unbindPlayerItem
|
||||
{
|
||||
[self removePlayerNotificationObserver];
|
||||
[self removePlayerLoadedTimeRangesObserver];
|
||||
|
||||
for (CALayer *layer in self.imageView.layer.sublayers)
|
||||
[layer removeFromSuperlayer];
|
||||
|
||||
self.player = nil;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Upate zoom scales
|
||||
|
||||
- (void)updateZoomScalesAndZoom:(BOOL)zoom
|
||||
{
|
||||
if (!self.asset)
|
||||
return;
|
||||
|
||||
CGSize assetSize = [self assetSize];
|
||||
CGSize boundsSize = self.bounds.size;
|
||||
|
||||
CGFloat xScale = boundsSize.width / assetSize.width; //scale needed to perfectly fit the image width-wise
|
||||
CGFloat yScale = boundsSize.height / assetSize.height; //scale needed to perfectly fit the image height-wise
|
||||
|
||||
CGFloat minScale = MIN(xScale, yScale);
|
||||
CGFloat maxScale = 3.0 * minScale;
|
||||
|
||||
if ([self.asset ctassetsPickerIsVideo])
|
||||
{
|
||||
self.minimumZoomScale = minScale;
|
||||
self.maximumZoomScale = minScale;
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
self.minimumZoomScale = minScale;
|
||||
self.maximumZoomScale = maxScale;
|
||||
}
|
||||
|
||||
// update perspective zoom scale
|
||||
self.perspectiveZoomScale = (boundsSize.width > boundsSize.height) ? xScale : yScale;
|
||||
|
||||
if (zoom)
|
||||
[self zoomToInitialScale];
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Zoom
|
||||
|
||||
- (void)zoomToInitialScale
|
||||
{
|
||||
if ([self canPerspectiveZoom])
|
||||
[self zoomToPerspectiveZoomScaleAnimated:NO];
|
||||
else
|
||||
[self zoomToMinimumZoomScaleAnimated:NO];
|
||||
}
|
||||
|
||||
- (void)zoomToMinimumZoomScaleAnimated:(BOOL)animated
|
||||
{
|
||||
[self setZoomScale:self.minimumZoomScale animated:animated];
|
||||
}
|
||||
|
||||
- (void)zoomToMaximumZoomScaleWithGestureRecognizer:(UITapGestureRecognizer *)recognizer
|
||||
{
|
||||
CGRect zoomRect = [self zoomRectWithScale:self.maximumZoomScale withCenter:[recognizer locationInView:recognizer.view]];
|
||||
|
||||
self.shouldUpdateConstraints = NO;
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{
|
||||
[self zoomToRect:zoomRect animated:NO];
|
||||
|
||||
CGRect frame = self.imageView.frame;
|
||||
frame.origin.x = 0;
|
||||
frame.origin.y = 0;
|
||||
|
||||
self.imageView.frame = frame;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Perspective zoom
|
||||
|
||||
- (BOOL)canPerspectiveZoom
|
||||
{
|
||||
CGSize assetSize = [self assetSize];
|
||||
CGSize boundsSize = self.bounds.size;
|
||||
|
||||
CGFloat assetRatio = assetSize.width / assetSize.height;
|
||||
CGFloat boundsRatio = boundsSize.width / boundsSize.height;
|
||||
|
||||
// can perform perspective zoom when the difference of aspect ratios is smaller than 20%
|
||||
return (fabs( (assetRatio - boundsRatio) / boundsRatio ) < 0.2f);
|
||||
}
|
||||
|
||||
- (void)zoomToPerspectiveZoomScaleAnimated:(BOOL)animated;
|
||||
{
|
||||
CGRect zoomRect = [self zoomRectWithScale:self.perspectiveZoomScale];
|
||||
[self zoomToRect:zoomRect animated:animated];
|
||||
}
|
||||
|
||||
- (CGRect)zoomRectWithScale:(CGFloat)scale
|
||||
{
|
||||
CGSize targetSize;
|
||||
targetSize.width = self.bounds.size.width / scale;
|
||||
targetSize.height = self.bounds.size.height / scale;
|
||||
|
||||
CGPoint targetOrigin;
|
||||
targetOrigin.x = (self.asset.pixelWidth - targetSize.width) / 2.0;
|
||||
targetOrigin.y = (self.asset.pixelHeight - targetSize.height) / 2.0;
|
||||
|
||||
CGRect zoomRect;
|
||||
zoomRect.origin = targetOrigin;
|
||||
zoomRect.size = targetSize;
|
||||
|
||||
return zoomRect;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Zoom with gesture recognizer
|
||||
|
||||
- (void)zoomWithGestureRecognizer:(UITapGestureRecognizer *)recognizer
|
||||
{
|
||||
if (self.minimumZoomScale == self.maximumZoomScale)
|
||||
return;
|
||||
|
||||
if ([self canPerspectiveZoom])
|
||||
{
|
||||
if ((self.zoomScale >= self.minimumZoomScale && self.zoomScale < self.perspectiveZoomScale) ||
|
||||
(self.zoomScale <= self.maximumZoomScale && self.zoomScale > self.perspectiveZoomScale))
|
||||
[self zoomToPerspectiveZoomScaleAnimated:YES];
|
||||
else
|
||||
[self zoomToMaximumZoomScaleWithGestureRecognizer:recognizer];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.zoomScale < self.maximumZoomScale)
|
||||
[self zoomToMaximumZoomScaleWithGestureRecognizer:recognizer];
|
||||
else
|
||||
[self zoomToMinimumZoomScaleAnimated:YES];
|
||||
}
|
||||
|
||||
- (CGRect)zoomRectWithScale:(CGFloat)scale withCenter:(CGPoint)center
|
||||
{
|
||||
center = [self.imageView convertPoint:center fromView:self];
|
||||
|
||||
CGRect zoomRect;
|
||||
|
||||
zoomRect.size.height = self.imageView.frame.size.height / scale;
|
||||
zoomRect.size.width = self.imageView.frame.size.width / scale;
|
||||
|
||||
zoomRect.origin.x = center.x - ((zoomRect.size.width / 2.0));
|
||||
zoomRect.origin.y = center.y - ((zoomRect.size.height / 2.0));
|
||||
|
||||
return zoomRect;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Gesture recognizers
|
||||
|
||||
- (void)addGestureRecognizers
|
||||
{
|
||||
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapping:)];
|
||||
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleTapping:)];
|
||||
|
||||
doubleTap.numberOfTapsRequired = 2.0;
|
||||
[singleTap requireGestureRecognizerToFail:doubleTap];
|
||||
|
||||
singleTap.delegate = self;
|
||||
doubleTap.delegate = self;
|
||||
|
||||
[self addGestureRecognizer:singleTap];
|
||||
[self addGestureRecognizer:doubleTap];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Handle tappings
|
||||
|
||||
- (void)handleTapping:(UITapGestureRecognizer *)recognizer
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:CTAssetScrollViewDidTapNotification object:recognizer];
|
||||
|
||||
if (recognizer.numberOfTapsRequired == 2)
|
||||
[self zoomWithGestureRecognizer:recognizer];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Scroll view delegate
|
||||
|
||||
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView
|
||||
{
|
||||
return self.imageView;
|
||||
}
|
||||
|
||||
- (void)scrollViewWillBeginZooming:(UIScrollView *)scrollView withView:(UIView *)view
|
||||
{
|
||||
self.shouldUpdateConstraints = YES;
|
||||
}
|
||||
|
||||
- (void)scrollViewDidZoom:(UIScrollView *)scrollView
|
||||
{
|
||||
self.scrollEnabled = self.zoomScale != self.perspectiveZoomScale;
|
||||
|
||||
if (self.shouldUpdateConstraints)
|
||||
{
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Gesture recognizer delegate
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
|
||||
{
|
||||
return !([touch.view isDescendantOfView:self.playButton] || [touch.view isDescendantOfView:self.selectionButton]);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Notification observer
|
||||
|
||||
- (void)addPlayerNotificationObserver
|
||||
{
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(applicationWillResignActive:)
|
||||
name:UIApplicationWillResignActiveNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)removePlayerNotificationObserver
|
||||
{
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
[center removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Video player item key-value observer
|
||||
|
||||
- (void)addPlayerLoadedTimeRangesObserver
|
||||
{
|
||||
[self.player addObserver:self
|
||||
forKeyPath:@"currentItem.loadedTimeRanges"
|
||||
options:NSKeyValueObservingOptionNew
|
||||
context:nil];
|
||||
}
|
||||
|
||||
- (void)removePlayerLoadedTimeRangesObserver
|
||||
{
|
||||
@try {
|
||||
[self.player removeObserver:self forKeyPath:@"currentItem.loadedTimeRanges"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addPlayerRateObserver
|
||||
{
|
||||
[self.player addObserver:self
|
||||
forKeyPath:@"rate"
|
||||
options:NSKeyValueObservingOptionNew
|
||||
context:nil];
|
||||
}
|
||||
|
||||
- (void)removePlayerRateObserver
|
||||
{
|
||||
@try {
|
||||
[self.player removeObserver:self forKeyPath:@"rate"];
|
||||
}
|
||||
@catch (NSException *exception) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Video playback Key-Value changed
|
||||
|
||||
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
|
||||
{
|
||||
if (object == self.player && [keyPath isEqual:@"currentItem.loadedTimeRanges"])
|
||||
{
|
||||
NSArray *timeRanges = change[NSKeyValueChangeNewKey];
|
||||
|
||||
if (timeRanges && timeRanges.count)
|
||||
{
|
||||
CMTimeRange timeRange = [timeRanges.firstObject CMTimeRangeValue];
|
||||
|
||||
if (CMTIME_COMPARE_INLINE(timeRange.duration, ==, self.player.currentItem.duration))
|
||||
[self performSelector:@selector(playerDidLoadItem:) withObject:object];
|
||||
}
|
||||
}
|
||||
|
||||
if (object == self.player && [keyPath isEqual:@"rate"])
|
||||
{
|
||||
CGFloat rate = [[change valueForKey:NSKeyValueChangeNewKey] floatValue];
|
||||
|
||||
if (rate > 0)
|
||||
[self performSelector:@selector(playerDidPlay:) withObject:object];
|
||||
|
||||
if (rate == 0)
|
||||
[self performSelector:@selector(playerDidPause:) withObject:object];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)postPlayerWillPlayNotification
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:CTAssetScrollViewPlayerWillPlayNotification object:nil];
|
||||
}
|
||||
|
||||
- (void)postPlayerWillPauseNotification
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:CTAssetScrollViewPlayerWillPauseNotification object:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Playback events
|
||||
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification
|
||||
{
|
||||
[self pauseVideo];
|
||||
}
|
||||
|
||||
|
||||
- (void)playerDidPlay:(id)sender
|
||||
{
|
||||
[self setProgress:1];
|
||||
[self.playButton setHidden:YES];
|
||||
[self.selectionButton setHidden:YES];
|
||||
[self.activityView stopAnimating];
|
||||
}
|
||||
|
||||
|
||||
- (void)playerDidPause:(id)sender
|
||||
{
|
||||
[self.playButton setHidden:NO];
|
||||
[self.selectionButton setHidden:NO];
|
||||
}
|
||||
|
||||
- (void)playerDidLoadItem:(id)sender
|
||||
{
|
||||
if (!self.didLoadPlayerItem)
|
||||
{
|
||||
[self setDidLoadPlayerItem:YES];
|
||||
[self addPlayerRateObserver];
|
||||
|
||||
[self.activityView stopAnimating];
|
||||
[self playVideo];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Playback
|
||||
|
||||
- (void)playVideo
|
||||
{
|
||||
if (self.didLoadPlayerItem)
|
||||
{
|
||||
if (CMTIME_COMPARE_INLINE(self.player.currentTime, == , self.player.currentItem.duration))
|
||||
[self.player seekToTime:kCMTimeZero];
|
||||
|
||||
[self postPlayerWillPlayNotification];
|
||||
[self.player play];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pauseVideo
|
||||
{
|
||||
if (self.didLoadPlayerItem)
|
||||
{
|
||||
[self postPlayerWillPauseNotification];
|
||||
[self.player pause];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self stopActivityAnimating];
|
||||
[self unbindPlayerItem];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CTAssetSelectionButton : UIControl
|
||||
|
||||
@end
|
|
@ -0,0 +1,122 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetSelectionButton.h"
|
||||
#import "CTAssetCheckmark.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
#import "UIImage+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetSelectionButton ()
|
||||
|
||||
@property (nonatomic, strong) CTAssetCheckmark *checkmark;
|
||||
@property (nonatomic, strong) UIImageView *backgroundView;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetSelectionButton
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
|
||||
if (self)
|
||||
{
|
||||
self.isAccessibilityElement = YES;
|
||||
self.accessibilityTraits = UIAccessibilityTraitButton;
|
||||
|
||||
[self setupViews];
|
||||
[self localize];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
// Background
|
||||
UIImage *backgroundImage = [UIImage ctassetsPickerImageNamed:@"CheckmarkUnselected"];
|
||||
UIImageView *backgroundView = [[UIImageView alloc] initWithImage:backgroundImage];
|
||||
backgroundView.userInteractionEnabled = NO;
|
||||
self.backgroundView = backgroundView;
|
||||
|
||||
[self addSubview:self.backgroundView];
|
||||
|
||||
// Checkmark
|
||||
CTAssetCheckmark *checkmark = [CTAssetCheckmark newAutoLayoutView];
|
||||
checkmark.userInteractionEnabled = NO;
|
||||
checkmark.hidden = YES;
|
||||
self.checkmark = checkmark;
|
||||
|
||||
[self addSubview:self.checkmark];
|
||||
}
|
||||
|
||||
- (void)localize
|
||||
{
|
||||
self.accessibilityLabel = CTAssetsPickerLocalizedString(@"Select", nil);
|
||||
}
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
CGSize size = [UIImage ctassetsPickerImageNamed:@"CheckmarkUnselected"].size;
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
|
||||
[self autoSetDimensionsToSize:size];
|
||||
}];
|
||||
|
||||
[self.backgroundView autoCenterInSuperview];
|
||||
[self.checkmark autoCenterInSuperview];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
#pragma mark - States
|
||||
|
||||
- (void)setSelected:(BOOL)selected
|
||||
{
|
||||
super.selected = selected;
|
||||
self.checkmark.hidden = !selected;
|
||||
|
||||
self.accessibilityLabel = (selected) ? CTAssetsPickerLocalizedString(@"Deselect", nil) : CTAssetsPickerLocalizedString(@"Select", nil);
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,99 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/**
|
||||
* The label to show selection index.
|
||||
*/
|
||||
@interface CTAssetSelectionLabel : UILabel
|
||||
|
||||
|
||||
#pragma mark Customizing Appearance
|
||||
|
||||
/**
|
||||
* @name Customizing Appearance
|
||||
*/
|
||||
|
||||
/**
|
||||
* Determines whether the label is circular or not. *Deprecated*.
|
||||
*/
|
||||
@property (nonatomic, assign, getter=isCircular) BOOL circular UI_APPEARANCE_SELECTOR DEPRECATED_MSG_ATTRIBUTE("Use setCornerRadius: instead.");
|
||||
|
||||
/**
|
||||
* The width of the label's border.
|
||||
*/
|
||||
@property (nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/**
|
||||
* The color of the label's border.
|
||||
*/
|
||||
@property (nonatomic, weak) UIColor *borderColor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/**
|
||||
* To set the size of label.
|
||||
*
|
||||
* @param size The size of the label.
|
||||
*/
|
||||
- (void)setSize:(CGSize)size UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/**
|
||||
* To set the corner radius of label.
|
||||
*
|
||||
* @param cornerRadius The radius to use when drawing rounded corners for the label’s background.
|
||||
*/
|
||||
- (void)setCornerRadius:(CGFloat)cornerRadius UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/**
|
||||
* To set margin of the label from the edges.
|
||||
*
|
||||
* @param margin The margin from the edges.
|
||||
*
|
||||
* @see setMargin:forVerticalEdge:horizontalEdge:
|
||||
*/
|
||||
- (void)setMargin:(CGFloat)margin UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/**
|
||||
* To set margin of the label from specific edges.
|
||||
*
|
||||
* @param margin The margin from the edges.
|
||||
* @param edgeX The layout attribute respresents vertical edge that the label pins to. Either `NSLayoutAttributeLeft` or `NSLayoutAttributeRight`.
|
||||
* @param edgeY The layout attribute respresents horizontal edge that the label pins to. Either `NSLayoutAttributeTop` or `NSLayoutAttributeBottom`.
|
||||
*
|
||||
* @see setMargin:
|
||||
*/
|
||||
- (void)setMargin:(CGFloat)margin forVerticalEdge:(NSLayoutAttribute)edgeX horizontalEdge:(NSLayoutAttribute)edgeY UI_APPEARANCE_SELECTOR;
|
||||
|
||||
/**
|
||||
* To set the text attributes to display the label.
|
||||
*
|
||||
* Currently only supports attributes `NSFontAttributeName`, `NSForegroundColorAttributeName` and `NSBackgroundColorAttributeName`.
|
||||
*
|
||||
* @param textAttributes The text attributes used to display the label.
|
||||
*/
|
||||
- (void)setTextAttributes:(NSDictionary<NSString*, id> *)textAttributes UI_APPEARANCE_SELECTOR;
|
||||
|
||||
@end
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetSelectionLabel.h"
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
|
||||
|
||||
/**
|
||||
* The label to show selection index.
|
||||
*/
|
||||
@interface CTAssetSelectionLabel ()
|
||||
|
||||
#pragma mark Managing Auto Layout
|
||||
|
||||
/**
|
||||
* The constraints of the size of the label.
|
||||
*/
|
||||
@property (nonatomic, strong) NSArray *sizeConstraints;
|
||||
|
||||
/**
|
||||
* The constraint for pinning the label to vertical edge.
|
||||
*/
|
||||
@property (nonatomic, strong) NSLayoutConstraint *verticalConstraint;
|
||||
|
||||
/**
|
||||
* The constraint for pinning the label to horizontal edge.
|
||||
*/
|
||||
@property (nonatomic, strong) NSLayoutConstraint *horizontalConstraint;
|
||||
|
||||
/**
|
||||
* Determines whether or not the constraints have been set up.
|
||||
*/
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation CTAssetSelectionLabel
|
||||
|
||||
#pragma mark Initializing a Label Object
|
||||
|
||||
/**
|
||||
* Designated Initializer
|
||||
*
|
||||
* @return an initialized label object
|
||||
*/
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
|
||||
if (self)
|
||||
{
|
||||
self.textAlignment = NSTextAlignmentCenter;
|
||||
self.font = CTAssetLabelFont;
|
||||
self.textColor = CTAssetLabelTextColor;
|
||||
self.backgroundColor = CTAssetLabelBackgroundColor;
|
||||
self.layer.borderColor = CTAssetLabelBorderColor.CGColor;
|
||||
self.layer.masksToBounds = YES;
|
||||
self.isAccessibilityElement = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Customizing Appearance
|
||||
|
||||
/**
|
||||
* The width of the label's border
|
||||
*/
|
||||
- (CGFloat)borderWidth
|
||||
{
|
||||
return self.layer.borderWidth;
|
||||
}
|
||||
|
||||
- (void)setBorderWidth:(CGFloat)borderWidth
|
||||
{
|
||||
self.layer.borderWidth = borderWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* The color of the label's border
|
||||
*/
|
||||
|
||||
- (UIColor *)borderColor
|
||||
{
|
||||
return [UIColor colorWithCGColor:self.layer.borderColor];
|
||||
}
|
||||
|
||||
- (void)setBorderColor:(UIColor *)borderColor
|
||||
{
|
||||
UIColor *color = (borderColor) ? borderColor : CTAssetLabelBorderColor;
|
||||
self.layer.borderColor = color.CGColor;
|
||||
}
|
||||
|
||||
/**
|
||||
* To set the size of label.
|
||||
*
|
||||
* @param size The size of the label.
|
||||
*/
|
||||
- (void)setSize:(CGSize)size
|
||||
{
|
||||
if (CGSizeEqualToSize(size, CGSizeZero)) {
|
||||
size = CTAssetLabelSize;
|
||||
}
|
||||
|
||||
[self removeConstraints:self.sizeConstraints];
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
|
||||
self.sizeConstraints = [self autoSetDimensionsToSize:size];
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
* To set the size of label.
|
||||
*
|
||||
* @param cornerRadius The radius to use when drawing rounded corners for the label’s background.
|
||||
*/
|
||||
- (void)setCornerRadius:(CGFloat)cornerRadius
|
||||
{
|
||||
self.layer.cornerRadius = cornerRadius;
|
||||
}
|
||||
|
||||
/**
|
||||
* To set margin of the label from the edges.
|
||||
*
|
||||
* @param margin The margin from the edges.
|
||||
*
|
||||
* @see setMargin:forVerticalEdge:horizontalEdge:
|
||||
*/
|
||||
- (void)setMargin:(CGFloat)margin
|
||||
{
|
||||
[self setMargin:margin forVerticalEdge:NSLayoutAttributeRight horizontalEdge:NSLayoutAttributeBottom];
|
||||
}
|
||||
|
||||
/**
|
||||
* To set margin of the label from specific edges.
|
||||
*
|
||||
* @param margin The margin from the edges.
|
||||
* @param edgeX The layout attribute respresents vertical edge that the label pins to. Either `NSLayoutAttributeLeft` or `NSLayoutAttributeRight`.
|
||||
* @param edgeY The layout attribute respresents horizontal edge that the label pins to. Either `NSLayoutAttributeTop` or `NSLayoutAttributeBottom`.
|
||||
*
|
||||
* @see setMargin:
|
||||
*/
|
||||
- (void)setMargin:(CGFloat)margin forVerticalEdge:(NSLayoutAttribute)edgeX horizontalEdge:(NSLayoutAttribute)edgeY
|
||||
{
|
||||
NSAssert(edgeX == NSLayoutAttributeLeft || edgeX == NSLayoutAttributeRight,
|
||||
@"Vertical edge must be NSLayoutAttributeLeft or NSLayoutAttributeRight");
|
||||
|
||||
NSAssert(edgeY == NSLayoutAttributeTop || edgeY == NSLayoutAttributeBottom,
|
||||
@"Horizontal edge must be NSLayoutAttributeTop or NSLayoutAttributeBottom");
|
||||
|
||||
[self.superview removeConstraints:@[self.verticalConstraint, self.horizontalConstraint]];
|
||||
self.verticalConstraint = [self autoPinEdgeToSuperviewEdge:(ALEdge)edgeX withInset:margin];
|
||||
self.horizontalConstraint = [self autoPinEdgeToSuperviewEdge:(ALEdge)edgeY withInset:margin];
|
||||
}
|
||||
|
||||
/**
|
||||
* To set the text attributes to display the label.
|
||||
*
|
||||
* Currently only supports attributes `NSFontAttributeName`, `NSForegroundColorAttributeName` and `NSBackgroundColorAttributeName`.
|
||||
*
|
||||
* @param textAttributes The text attributes used to display the label.
|
||||
*/
|
||||
- (void)setTextAttributes:(NSDictionary *)textAttributes
|
||||
{
|
||||
self.font = (textAttributes) ? textAttributes[NSFontAttributeName] : CTAssetLabelFont;
|
||||
self.textColor = (textAttributes) ? textAttributes[NSForegroundColorAttributeName] : CTAssetLabelTextColor;
|
||||
self.backgroundColor = (textAttributes) ? textAttributes[NSBackgroundColorAttributeName] : CTAssetLabelBackgroundColor;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Triggering Auto Layout
|
||||
|
||||
/**
|
||||
* Updates constraints of the label.
|
||||
*/
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
|
||||
self.sizeConstraints = [self autoSetDimensionsToSize:CTAssetLabelSize];
|
||||
}];
|
||||
|
||||
self.verticalConstraint = [self autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:0];
|
||||
self.horizontalConstraint = [self autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:0];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
|
||||
@interface CTAssetThumbnailOverlay : UIView
|
||||
|
||||
- (void)bind:(nullable PHAsset *)asset duration:(nullable NSString *)duration;
|
||||
- (void)bind:(nullable PHAssetCollection *)assetCollection;
|
||||
|
||||
@end
|
|
@ -0,0 +1,156 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetThumbnailOverlay.h"
|
||||
#import "UIImage+CTAssetsPickerController.h"
|
||||
#import "PHAsset+CTAssetsPickerController.h"
|
||||
#import "PHAssetCollection+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetThumbnailOverlay ()
|
||||
|
||||
@property (nonatomic, strong) UIImageView *gradient;
|
||||
@property (nonatomic, strong) UIImageView *badge;
|
||||
@property (nonatomic, strong) UILabel *duration;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetThumbnailOverlay
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
self.opaque = NO;
|
||||
self.clipsToBounds = YES;
|
||||
self.isAccessibilityElement = NO;
|
||||
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma markt - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
UIImageView *gradient = [UIImageView newAutoLayoutView];
|
||||
gradient.image = [UIImage ctassetsPickerImageNamed:@"GridGradient"];
|
||||
self.gradient = gradient;
|
||||
|
||||
[self addSubview:self.gradient];
|
||||
|
||||
UIImageView *badge = [UIImageView newAutoLayoutView];
|
||||
badge.tintColor = [UIColor whiteColor];
|
||||
self.badge = badge;
|
||||
|
||||
[self addSubview:self.badge];
|
||||
|
||||
UILabel *duration = [UILabel newAutoLayoutView];
|
||||
duration.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCaption2];
|
||||
duration.textColor = [UIColor whiteColor];
|
||||
duration.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
duration.layoutMargins = UIEdgeInsetsMake(2.5, 2.5, 2.5, 2.5);
|
||||
self.duration = duration;
|
||||
|
||||
[self addSubview:self.duration];
|
||||
}
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
[self.gradient autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero excludingEdge:ALEdgeTop];
|
||||
[self.gradient autoSetDimension:ALDimensionHeight toSize:self.gradient.image.size.height];
|
||||
[self.badge autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:self.badge.layoutMargins.left];
|
||||
[self.badge autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.badge.layoutMargins.bottom];
|
||||
[self.duration autoPinEdgeToSuperviewEdge:ALEdgeRight withInset:self.duration.layoutMargins.right];
|
||||
[self.duration autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:self.duration.layoutMargins.bottom];
|
||||
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
#pragma - Bind asset and duration
|
||||
|
||||
- (void)bind:(PHAsset *)asset duration:(NSString *)duration;
|
||||
{
|
||||
self.badge.image = [asset badgeImage];
|
||||
self.badge.layoutMargins = [self layoutMarginsForAsset:asset];
|
||||
self.duration.text = duration;
|
||||
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
}
|
||||
|
||||
- (UIEdgeInsets)layoutMarginsForAsset:(PHAsset *)asset
|
||||
{
|
||||
if (asset.ctassetsPickerIsHighFrameRateVideo)
|
||||
return UIEdgeInsetsMake(2.5, 2.5, 2.5, 2.5);
|
||||
|
||||
else if (asset.ctassetsPickerIsTimelapseVideo)
|
||||
return UIEdgeInsetsMake(2.5, 2.5, 2.5, 2.5);
|
||||
|
||||
else if (asset.ctassetsPickerIsVideo)
|
||||
return UIEdgeInsetsMake(4.5, 4.5, 4.5, 4.5);
|
||||
|
||||
else
|
||||
return UIEdgeInsetsZero;
|
||||
}
|
||||
|
||||
|
||||
#pragma - Bind asset collection
|
||||
|
||||
- (void)bind:(PHAssetCollection *)assetCollection;
|
||||
{
|
||||
self.badge.image = [assetCollection badgeImage];
|
||||
self.badge.layoutMargins = [self layoutMarginsForAssetCollection:assetCollection];
|
||||
self.duration.text = nil;
|
||||
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
}
|
||||
|
||||
- (UIEdgeInsets)layoutMarginsForAssetCollection:(PHAssetCollection *)assetCollection
|
||||
{
|
||||
return UIEdgeInsetsMake(4.0, 4.0, 4.0, 4.0);
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "CTAssetThumbnailView.h"
|
||||
|
||||
@interface CTAssetThumbnailStacks : UIView
|
||||
|
||||
@property (nonatomic, assign) CGSize thumbnailSize;
|
||||
@property (nonatomic, copy, readonly) NSArray<CTAssetThumbnailView*> *thumbnailViews;
|
||||
@property (nonatomic, assign, readonly) UIEdgeInsets edgeInsets;
|
||||
|
||||
- (nonnull CTAssetThumbnailView *)thumbnailAtIndex:(NSUInteger)index;
|
||||
- (void)setHighlighted:(BOOL)highlighted;
|
||||
|
||||
@end
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetThumbnailStacks.h"
|
||||
#import "CTAssetThumbnailView.h"
|
||||
|
||||
|
||||
@interface CTAssetThumbnailStacks ()
|
||||
|
||||
@property (nonatomic, copy) NSArray *thumbnailViews;
|
||||
@property (nonatomic, assign) UIEdgeInsets edgeInsets;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetThumbnailStacks
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
_edgeInsets = UIEdgeInsetsMake(4.0, 0, 0, 0);
|
||||
|
||||
self.opaque = YES;
|
||||
self.clipsToBounds = YES;
|
||||
self.isAccessibilityElement = NO;
|
||||
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
NSMutableArray *thumbnailViews = [NSMutableArray new];
|
||||
|
||||
for (NSUInteger index = 0; index < 3; index++)
|
||||
{
|
||||
CTAssetThumbnailView *thumbnailView = [CTAssetThumbnailView newAutoLayoutView];
|
||||
thumbnailView.showsDuration = NO;
|
||||
thumbnailView.layer.borderColor = [UIColor whiteColor].CGColor;
|
||||
thumbnailView.layer.borderWidth = 0.5f;
|
||||
|
||||
[thumbnailViews addObject:thumbnailView];
|
||||
[self insertSubview:thumbnailView atIndex:0];
|
||||
}
|
||||
|
||||
self.thumbnailViews = [NSArray arrayWithArray:thumbnailViews];
|
||||
}
|
||||
|
||||
|
||||
#pragma markt - Setters
|
||||
|
||||
- (void)setThumbnailSize:(CGSize)thumbnailSize
|
||||
{
|
||||
_thumbnailSize = thumbnailSize;
|
||||
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
for (NSUInteger index = 0; index < self.thumbnailViews.count; index++)
|
||||
{
|
||||
CTAssetThumbnailView *thumbnailView = [self thumbnailAtIndex:index];
|
||||
|
||||
CGFloat delta = self.edgeInsets.top / 2;
|
||||
|
||||
CGSize size = self.thumbnailSize;
|
||||
size.width -= index * delta * 2;
|
||||
size.height -= index * delta * 2;
|
||||
|
||||
CGFloat inset = (index * delta * 3);
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
|
||||
[thumbnailView autoSetDimensionsToSize:size];
|
||||
}];
|
||||
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityDefaultHigh forConstraints:^{
|
||||
[thumbnailView autoAlignAxisToSuperviewAxis:ALAxisVertical];
|
||||
[thumbnailView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:inset];
|
||||
}];
|
||||
}
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
- (CTAssetThumbnailView *)thumbnailAtIndex:(NSUInteger)index
|
||||
{
|
||||
return self.thumbnailViews[index];
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
for (CTAssetThumbnailView *thumbnailView in self.thumbnailViews)
|
||||
thumbnailView.backgroundColor = CTAssetsPikcerThumbnailBackgroundColor;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
@interface CTAssetThumbnailView : UIView
|
||||
|
||||
@property (nonatomic, assign) BOOL showsDuration;
|
||||
@property (nonatomic, strong, nullable) UIImage *backgroundImage;
|
||||
|
||||
- (void)bind:(nullable UIImage *)image asset:(nullable PHAsset *)asset;
|
||||
- (void)bind:(nullable UIImage *)image assetCollection:(nullable PHAssetCollection *)assetCollection;
|
||||
|
||||
@end
|
|
@ -0,0 +1,196 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetThumbnailView.h"
|
||||
#import "CTAssetThumbnailOverlay.h"
|
||||
#import "PHAsset+CTAssetsPickerController.h"
|
||||
#import "NSDateFormatter+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetThumbnailView ()
|
||||
|
||||
@property (nonatomic, strong) CTAssetThumbnailOverlay *overlay;
|
||||
@property (nonatomic, strong) UIImageView *imageView;
|
||||
@property (nonatomic, strong) UIImageView *backgroundView;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetThumbnailView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
_showsDuration = YES;
|
||||
self.opaque = YES;
|
||||
self.clipsToBounds = YES;
|
||||
self.isAccessibilityElement = NO;
|
||||
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma markt - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
self.backgroundColor = CTAssetsPikcerThumbnailBackgroundColor;
|
||||
|
||||
UIImageView *backgroundView = [UIImageView new];
|
||||
backgroundView.contentMode = UIViewContentModeCenter;
|
||||
backgroundView.tintColor = CTAssetsPikcerThumbnailTintColor;
|
||||
self.backgroundView = backgroundView;
|
||||
|
||||
UIImageView *imageView = [UIImageView new];
|
||||
imageView.contentMode = UIViewContentModeScaleAspectFill;
|
||||
self.imageView = imageView;
|
||||
|
||||
[self addSubview:self.backgroundView];
|
||||
[self addSubview:self.imageView];
|
||||
}
|
||||
|
||||
|
||||
#pragma markt - Setters
|
||||
|
||||
- (void)setBackgroundImage:(UIImage *)backgroundImage
|
||||
{
|
||||
_backgroundImage = backgroundImage;
|
||||
self.backgroundView.image = backgroundImage;
|
||||
}
|
||||
|
||||
|
||||
#pragma markt - Override set bounds
|
||||
|
||||
-(void)setBounds:(CGRect)bounds
|
||||
{
|
||||
super.bounds = bounds;
|
||||
|
||||
self.overlay.frame = bounds;
|
||||
[self.overlay setNeedsDisplay];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
[self.backgroundView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self.imageView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
#pragma - Bind asset and image
|
||||
|
||||
- (void)bind:(UIImage *)image asset:(PHAsset *)asset;
|
||||
{
|
||||
[self setupOverlayForAsset:asset];
|
||||
|
||||
self.imageView.image = image;
|
||||
self.backgroundView.hidden = (image != nil);
|
||||
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
}
|
||||
|
||||
- (void)setupOverlayForAsset:(PHAsset *)asset
|
||||
{
|
||||
if (asset.ctassetsPickerIsVideo)
|
||||
{
|
||||
if (!self.overlay) {
|
||||
self.overlay = [[CTAssetThumbnailOverlay alloc] initWithFrame:self.bounds];
|
||||
[self addSubview:self.overlay];
|
||||
}
|
||||
|
||||
NSString *duration = nil;
|
||||
|
||||
if (self.showsDuration)
|
||||
{
|
||||
NSDateFormatter *df = [NSDateFormatter new];
|
||||
duration = [df ctassetsPickerStringFromTimeInterval:asset.duration];
|
||||
}
|
||||
|
||||
[self.overlay bind:asset duration:duration];
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
[self.overlay removeFromSuperview];
|
||||
self.overlay = nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma - Bind asset collection and image
|
||||
|
||||
- (void)bind:(UIImage *)image assetCollection:(PHAssetCollection *)assetCollection;
|
||||
{
|
||||
[self setupOverlayForAssetCollection:assetCollection];
|
||||
|
||||
self.imageView.image = image;
|
||||
self.backgroundView.hidden = (image != nil);
|
||||
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
}
|
||||
|
||||
- (void)setupOverlayForAssetCollection:(PHAssetCollection *)assetCollection
|
||||
{
|
||||
if (assetCollection.assetCollectionType == PHAssetCollectionTypeSmartAlbum &&
|
||||
assetCollection.assetCollectionSubtype != PHAssetCollectionSubtypeSmartAlbumAllHidden)
|
||||
{
|
||||
if (!self.overlay) {
|
||||
self.overlay = [[CTAssetThumbnailOverlay alloc] initWithFrame:self.bounds];
|
||||
[self addSubview:self.overlay];
|
||||
}
|
||||
|
||||
[self.overlay bind:assetCollection];
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
[self.overlay removeFromSuperview];
|
||||
self.overlay = nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CTAssetsGridSelectedView : UIView
|
||||
|
||||
@property (nonatomic, assign) BOOL showsSelectionIndex;
|
||||
@property (nonatomic, assign) NSUInteger selectionIndex;
|
||||
|
||||
@property (nonatomic, weak, nullable) UIColor *selectedBackgroundColor UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic, assign) CGFloat borderWidth UI_APPEARANCE_SELECTOR;
|
||||
|
||||
@property (nonatomic, weak, nullable) UIFont *font UI_APPEARANCE_SELECTOR DEPRECATED_MSG_ATTRIBUTE("Use setTextAttributes: of CTAssetSelectionLabel instead.");
|
||||
@property (nonatomic, weak, nullable) UIColor *textColor UI_APPEARANCE_SELECTOR DEPRECATED_MSG_ATTRIBUTE("Use setTextAttributes: of CTAssetSelectionLabel instead.");
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2013 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetsGridSelectedView.h"
|
||||
#import "CTAssetCheckmark.h"
|
||||
#import "CTAssetSelectionLabel.h"
|
||||
|
||||
|
||||
|
||||
|
||||
@interface CTAssetsGridSelectedView ()
|
||||
|
||||
@property (nonatomic, strong) CTAssetCheckmark *checkmark;
|
||||
@property (nonatomic, strong) CTAssetSelectionLabel *selectionIndexLabel;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetsGridSelectedView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
[self setupViews];
|
||||
self.showsSelectionIndex = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
self.backgroundColor = CTAssetsGridSelectedViewBackgroundColor;
|
||||
self.layer.borderColor = CTAssetsGridSelectedViewTintColor.CGColor;
|
||||
|
||||
CTAssetCheckmark *checkmark = [CTAssetCheckmark newAutoLayoutView];
|
||||
self.checkmark = checkmark;
|
||||
[self addSubview:checkmark];
|
||||
|
||||
CTAssetSelectionLabel *selectionIndexLabel = [CTAssetSelectionLabel newAutoLayoutView];
|
||||
self.selectionIndexLabel = selectionIndexLabel;
|
||||
|
||||
[self addSubview:self.selectionIndexLabel];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
- (void)setShowsSelectionIndex:(BOOL)showsSelectionIndex
|
||||
{
|
||||
_showsSelectionIndex = showsSelectionIndex;
|
||||
|
||||
if (showsSelectionIndex)
|
||||
{
|
||||
self.checkmark.hidden = YES;
|
||||
self.selectionIndexLabel.hidden = NO;
|
||||
}
|
||||
else
|
||||
{
|
||||
self.checkmark.hidden = NO;
|
||||
self.selectionIndexLabel.hidden = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSelectionIndex:(NSUInteger)selectionIndex
|
||||
{
|
||||
_selectionIndex = selectionIndex;
|
||||
self.selectionIndexLabel.text = [NSString stringWithFormat:@"%lu", (unsigned long)(selectionIndex + 1)];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Apperance
|
||||
|
||||
- (UIColor *)selectedBackgroundColor
|
||||
{
|
||||
return self.backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setSelectedBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
UIColor *color = (backgroundColor) ? backgroundColor : CTAssetsGridSelectedViewBackgroundColor;
|
||||
self.backgroundColor = color;
|
||||
}
|
||||
|
||||
- (CGFloat)borderWidth
|
||||
{
|
||||
return self.layer.borderWidth;
|
||||
}
|
||||
|
||||
- (void)setBorderWidth:(CGFloat)borderWidth
|
||||
{
|
||||
self.layer.borderWidth = borderWidth;
|
||||
}
|
||||
|
||||
- (void)setTintColor:(UIColor *)tintColor
|
||||
{
|
||||
UIColor *color = (tintColor) ? tintColor : CTAssetsGridSelectedViewTintColor;
|
||||
self.layer.borderColor = color.CGColor;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessibility Label
|
||||
|
||||
- (NSString *)accessibilityLabel
|
||||
{
|
||||
return self.selectionIndexLabel.text;
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CTAssetsGridView : UIView
|
||||
|
||||
@property (nonatomic, weak, nullable) UIColor *gridBackgroundColor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
@end
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetsGridView.h"
|
||||
|
||||
@interface CTAssetsGridView ()
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation CTAssetsGridView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
self.backgroundColor = CTAssetsGridViewBackgroundColor;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Apperance
|
||||
|
||||
- (UIColor *)gridBackgroundColor
|
||||
{
|
||||
return self.backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setGridBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
UIColor *color = (backgroundColor) ? (backgroundColor) : CTAssetsGridViewBackgroundColor;
|
||||
self.backgroundColor = color;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
[self autoPinEdgesToSuperviewEdges];
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
#import "CTAssetThumbnailView.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetsGridViewCell : UICollectionViewCell
|
||||
|
||||
@property (nonatomic, assign, getter = isEnabled) BOOL enabled;
|
||||
@property (nonatomic, assign) BOOL showsSelectionIndex;
|
||||
@property (nonatomic, assign) NSUInteger selectionIndex;
|
||||
|
||||
@property (nonatomic, weak, nullable) UIColor *disabledColor UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic, weak, nullable) UIColor *highlightedColor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
- (void)bind:(nonnull PHAsset *)asset;
|
||||
|
||||
@end
|
|
@ -0,0 +1,204 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetsGridViewCell.h"
|
||||
#import "CTAssetsGridSelectedView.h"
|
||||
#import "PHAsset+CTAssetsPickerController.h"
|
||||
#import "NSDateFormatter+CTAssetsPickerController.h"
|
||||
#import "UIImage+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetsGridViewCell ()
|
||||
|
||||
@property (nonatomic, strong) PHAsset *asset;
|
||||
|
||||
@property (nonatomic, strong) UIImageView *disabledImageView;
|
||||
@property (nonatomic, strong) UIView *disabledView;
|
||||
@property (nonatomic, strong) UIView *highlightedView;
|
||||
@property (nonatomic, strong) CTAssetsGridSelectedView *selectedView;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetsGridViewCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
self.opaque = YES;
|
||||
self.isAccessibilityElement = YES;
|
||||
self.accessibilityTraits = UIAccessibilityTraitImage;
|
||||
self.enabled = YES;
|
||||
self.showsSelectionIndex = NO;
|
||||
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
CTAssetThumbnailView *thumbnailView = [CTAssetThumbnailView newAutoLayoutView];
|
||||
self.backgroundView = thumbnailView;
|
||||
|
||||
UIImage *disabledImage = [UIImage ctassetsPickerImageNamed:@"GridDisabledAsset"];
|
||||
disabledImage = [disabledImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
UIImageView *disabledImageView = [[UIImageView alloc] initWithImage:disabledImage];
|
||||
disabledImageView.tintColor = CTAssetsPikcerThumbnailTintColor;
|
||||
self.disabledImageView = disabledImageView;
|
||||
|
||||
UIView *disabledView = [UIView newAutoLayoutView];
|
||||
disabledView.backgroundColor = CTAssetsGridViewCellDisabledColor;
|
||||
disabledView.hidden = YES;
|
||||
[disabledView addSubview:self.disabledImageView];
|
||||
self.disabledView = disabledView;
|
||||
[self addSubview:self.disabledView];
|
||||
|
||||
UIView *highlightedView = [UIView newAutoLayoutView];
|
||||
highlightedView.backgroundColor = CTAssetsGridViewCellHighlightedColor;
|
||||
highlightedView.hidden = YES;
|
||||
self.highlightedView = highlightedView;
|
||||
[self addSubview:self.highlightedView];
|
||||
|
||||
CTAssetsGridSelectedView *selectedView = [CTAssetsGridSelectedView newAutoLayoutView];
|
||||
selectedView.hidden = YES;
|
||||
self.selectedView = selectedView;
|
||||
[self addSubview:self.selectedView];
|
||||
}
|
||||
|
||||
#pragma mark - Apperance
|
||||
|
||||
- (UIColor *)disabledColor
|
||||
{
|
||||
return self.disabledView.backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setDisabledColor:(UIColor *)disabledColor
|
||||
{
|
||||
UIColor *color = (disabledColor) ? disabledColor : CTAssetsGridViewCellDisabledColor;
|
||||
self.disabledView.backgroundColor = color;
|
||||
}
|
||||
|
||||
- (UIColor *)highlightedColor
|
||||
{
|
||||
return self.highlightedView.backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setHighlightedColor:(UIColor *)highlightedColor
|
||||
{
|
||||
UIColor *color = (highlightedColor) ? highlightedColor : CTAssetsGridViewCellHighlightedColor;
|
||||
self.highlightedView.backgroundColor = color;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
- (void)setEnabled:(BOOL)enabled
|
||||
{
|
||||
_enabled = enabled;
|
||||
self.disabledView.hidden = enabled;
|
||||
}
|
||||
|
||||
- (void)setHighlighted:(BOOL)highlighted
|
||||
{
|
||||
super.highlighted = highlighted;
|
||||
self.highlightedView.hidden = !highlighted;
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected
|
||||
{
|
||||
super.selected = selected;
|
||||
self.selectedView.hidden = !selected;
|
||||
}
|
||||
|
||||
- (void)setShowsSelectionIndex:(BOOL)showsSelectionIndex
|
||||
{
|
||||
_showsSelectionIndex = showsSelectionIndex;
|
||||
self.selectedView.showsSelectionIndex = showsSelectionIndex;
|
||||
}
|
||||
|
||||
- (void)setSelectionIndex:(NSUInteger)selectionIndex
|
||||
{
|
||||
_selectionIndex = selectionIndex;
|
||||
self.selectedView.selectionIndex = selectionIndex;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
[NSLayoutConstraint autoSetPriority:UILayoutPriorityRequired forConstraints:^{
|
||||
[self.backgroundView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self.disabledView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self.highlightedView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
[self.selectedView autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero];
|
||||
}];
|
||||
|
||||
[self.disabledImageView autoCenterInSuperview];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
- (void)bind:(PHAsset *)asset
|
||||
{
|
||||
self.asset = asset;
|
||||
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessibility Label
|
||||
|
||||
- (NSString *)accessibilityLabel
|
||||
{
|
||||
if (self.selectedView.accessibilityLabel)
|
||||
return [NSString stringWithFormat:@"%@, %@", self.selectedView.accessibilityLabel, self.asset.accessibilityLabel];
|
||||
else
|
||||
return self.asset.accessibilityLabel;
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
|
||||
@class CTAssetsGridViewController;
|
||||
|
||||
|
||||
|
||||
@protocol CTAssetsGridViewControllerDelegate <NSObject>
|
||||
|
||||
- (void)assetsGridViewController:(nonnull CTAssetsGridViewController *)picker photoLibraryDidChangeForAssetCollection:(nonnull PHAssetCollection *)assetCollection;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@interface CTAssetsGridViewController : UICollectionViewController
|
||||
|
||||
@property (nonatomic, weak) id<CTAssetsGridViewControllerDelegate> delegate;
|
||||
@property (nonatomic, strong, nonnull) PHAssetCollection *assetCollection;
|
||||
|
||||
@end
|
||||
|
||||
|
|
@ -0,0 +1,802 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetsPickerController.h"
|
||||
#import "CTAssetsPickerController+Internal.h"
|
||||
#import "CTAssetsGridViewController.h"
|
||||
#import "CTAssetsGridView.h"
|
||||
#import "CTAssetsGridViewLayout.h"
|
||||
#import "CTAssetsGridViewCell.h"
|
||||
#import "CTAssetsGridViewFooter.h"
|
||||
#import "CTAssetsPickerNoAssetsView.h"
|
||||
#import "CTAssetsPageViewController.h"
|
||||
#import "CTAssetsPageViewController+Internal.h"
|
||||
#import "CTAssetsViewControllerTransition.h"
|
||||
#import "UICollectionView+CTAssetsPickerController.h"
|
||||
#import "NSIndexSet+CTAssetsPickerController.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
#import "PHImageManager+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
NSString * const CTAssetsGridViewCellIdentifier = @"CTAssetsGridViewCellIdentifier";
|
||||
NSString * const CTAssetsGridViewFooterIdentifier = @"CTAssetsGridViewFooterIdentifier";
|
||||
|
||||
|
||||
@interface CTAssetsGridViewController ()
|
||||
<PHPhotoLibraryChangeObserver>
|
||||
|
||||
@property (nonatomic, weak) CTAssetsPickerController *picker;
|
||||
@property (nonatomic, strong) PHFetchResult *fetchResult;
|
||||
@property (nonatomic, strong) PHCachingImageManager *imageManager;
|
||||
|
||||
@property (nonatomic, assign) CGRect previousPreheatRect;
|
||||
@property (nonatomic, assign) CGRect previousBounds;
|
||||
|
||||
@property (nonatomic, strong) CTAssetsGridViewFooter *footer;
|
||||
@property (nonatomic, strong) CTAssetsPickerNoAssetsView *noAssetsView;
|
||||
|
||||
@property (nonatomic, assign) BOOL didLayoutSubviews;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetsGridViewController
|
||||
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
CTAssetsGridViewLayout *layout = [CTAssetsGridViewLayout new];
|
||||
|
||||
if (self = [super initWithCollectionViewLayout:layout])
|
||||
{
|
||||
_imageManager = [PHCachingImageManager new];
|
||||
|
||||
self.extendedLayoutIncludesOpaqueBars = YES;
|
||||
|
||||
self.collectionView.allowsMultipleSelection = YES;
|
||||
|
||||
[self.collectionView registerClass:CTAssetsGridViewCell.class
|
||||
forCellWithReuseIdentifier:CTAssetsGridViewCellIdentifier];
|
||||
|
||||
[self.collectionView registerClass:CTAssetsGridViewFooter.class
|
||||
forSupplementaryViewOfKind:UICollectionElementKindSectionFooter
|
||||
withReuseIdentifier:CTAssetsGridViewFooterIdentifier];
|
||||
|
||||
[self addNotificationObserver];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
[self setupViews];
|
||||
[self registerChangeObserver];
|
||||
[self addGestureRecognizer];
|
||||
[self addNotificationObserver];
|
||||
[self resetCachedAssetImages];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
[self setupButtons];
|
||||
[self setupAssets];
|
||||
[self updateTitle:self.picker.selectedAssets];
|
||||
[self updateButton:self.picker.selectedAssets];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:animated];
|
||||
[self updateCachedAssetImages];
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews
|
||||
{
|
||||
[super viewWillLayoutSubviews];
|
||||
|
||||
if (!CGRectEqualToRect(self.view.bounds, self.previousBounds))
|
||||
{
|
||||
[self updateCollectionViewLayout];
|
||||
self.previousBounds = self.view.bounds;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews
|
||||
{
|
||||
[super viewDidLayoutSubviews];
|
||||
|
||||
if (!self.didLayoutSubviews && self.fetchResult.count > 0)
|
||||
{
|
||||
[self scrollToBottomIfNeeded];
|
||||
self.didLayoutSubviews = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self unregisterChangeObserver];
|
||||
[self removeNotificationObserver];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
- (CTAssetsPickerController *)picker
|
||||
{
|
||||
return (CTAssetsPickerController *)self.splitViewController.parentViewController;
|
||||
}
|
||||
|
||||
- (PHAsset *)assetAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return (self.fetchResult.count > 0) ? self.fetchResult[indexPath.item] : nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
self.collectionView.backgroundColor = [UIColor colorWithWhite:0 alpha:0];
|
||||
CTAssetsGridView *gridView = [CTAssetsGridView new];
|
||||
[self.view insertSubview:gridView atIndex:0];
|
||||
[self.view setNeedsUpdateConstraints];
|
||||
}
|
||||
|
||||
- (void)setupButtons
|
||||
{
|
||||
if (self.navigationItem.rightBarButtonItem == nil)
|
||||
{
|
||||
NSString *title = (self.picker.doneButtonTitle) ?
|
||||
self.picker.doneButtonTitle : CTAssetsPickerLocalizedString(@"Done", nil);
|
||||
|
||||
self.navigationItem.rightBarButtonItem =
|
||||
[[UIBarButtonItem alloc] initWithTitle:title
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self.picker
|
||||
action:@selector(finishPickingAssets:)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupAssets
|
||||
{
|
||||
PHFetchResult *fetchResult =
|
||||
[PHAsset fetchAssetsInAssetCollection:self.assetCollection
|
||||
options:self.picker.assetsFetchOptions];
|
||||
|
||||
self.fetchResult = fetchResult;
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
#pragma mark - Collection view layout
|
||||
|
||||
- (void)updateCollectionViewLayout
|
||||
{
|
||||
UITraitCollection *trait = self.traitCollection;
|
||||
CGSize contentSize = self.view.bounds.size;
|
||||
UICollectionViewLayout *layout;
|
||||
|
||||
NSArray *attributes = [self.collectionView.collectionViewLayout layoutAttributesForElementsInRect:self.collectionView.bounds];
|
||||
UICollectionViewLayoutAttributes *attr = (UICollectionViewLayoutAttributes*)attributes.firstObject;
|
||||
// new content size should be at least of first item size, else ignoring
|
||||
if (contentSize.width < attr.size.width || contentSize.height < attr.size.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:collectionViewLayoutForContentSize:traitCollection:)]) {
|
||||
layout = [self.picker.delegate assetsPickerController:self.picker collectionViewLayoutForContentSize:contentSize traitCollection:trait];
|
||||
} else {
|
||||
layout = [[CTAssetsGridViewLayout alloc] initWithContentSize:contentSize traitCollection:trait];
|
||||
}
|
||||
|
||||
__weak CTAssetsGridViewController *weakSelf = self;
|
||||
|
||||
[self.collectionView setCollectionViewLayout:layout animated:NO completion:^(BOOL finished){
|
||||
[weakSelf.collectionView reloadItemsAtIndexPaths:[weakSelf.collectionView indexPathsForVisibleItems]];
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Scroll to bottom
|
||||
|
||||
- (void)scrollToBottomIfNeeded
|
||||
{
|
||||
BOOL shouldScrollToBottom;
|
||||
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldScrollToBottomForAssetCollection:)])
|
||||
shouldScrollToBottom = [self.picker.delegate assetsPickerController:self.picker shouldScrollToBottomForAssetCollection:self.assetCollection];
|
||||
else
|
||||
shouldScrollToBottom = YES;
|
||||
|
||||
if (shouldScrollToBottom)
|
||||
{
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:self.fetchResult.count-1 inSection:0];
|
||||
[self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:UICollectionViewScrollPositionTop animated:NO];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Notifications
|
||||
|
||||
- (void)addNotificationObserver
|
||||
{
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(assetsPickerSelectedAssetsDidChange:)
|
||||
name:CTAssetsPickerSelectedAssetsDidChangeNotification
|
||||
object:nil];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(assetsPickerDidSelectAsset:)
|
||||
name:CTAssetsPickerDidSelectAssetNotification
|
||||
object:nil];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(assetsPickerDidDeselectAsset:)
|
||||
name:CTAssetsPickerDidDeselectAssetNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)removeNotificationObserver
|
||||
{
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
[center removeObserver:self name:CTAssetsPickerSelectedAssetsDidChangeNotification object:nil];
|
||||
[center removeObserver:self name:CTAssetsPickerDidSelectAssetNotification object:nil];
|
||||
[center removeObserver:self name:CTAssetsPickerDidDeselectAssetNotification object:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Photo library change observer
|
||||
|
||||
- (void)registerChangeObserver
|
||||
{
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] registerChangeObserver:self];
|
||||
}
|
||||
|
||||
- (void)unregisterChangeObserver
|
||||
{
|
||||
[[PHPhotoLibrary sharedPhotoLibrary] unregisterChangeObserver:self];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Photo library changed
|
||||
|
||||
- (void)photoLibraryDidChange:(PHChange *)changeInstance
|
||||
{
|
||||
// Call might come on any background queue. Re-dispatch to the main queue to handle it.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
|
||||
PHFetchResultChangeDetails *changeDetails = [changeInstance changeDetailsForFetchResult:self.fetchResult];
|
||||
|
||||
if (changeDetails)
|
||||
{
|
||||
self.fetchResult = changeDetails.fetchResultAfterChanges;
|
||||
|
||||
UICollectionView *collectionView = self.collectionView;
|
||||
|
||||
if (!changeDetails.hasIncrementalChanges || changeDetails.hasMoves)
|
||||
{
|
||||
[collectionView reloadData];
|
||||
[self resetCachedAssetImages];
|
||||
}
|
||||
else
|
||||
{
|
||||
NSArray *removedPaths;
|
||||
NSArray *insertedPaths;
|
||||
NSArray *changedPaths;
|
||||
|
||||
NSIndexSet *removedIndexes = changeDetails.removedIndexes;
|
||||
removedPaths = [removedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0];
|
||||
|
||||
NSIndexSet *insertedIndexes = changeDetails.insertedIndexes;
|
||||
insertedPaths = [insertedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0];
|
||||
|
||||
NSIndexSet *changedIndexes = changeDetails.changedIndexes;
|
||||
changedPaths = [changedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0];
|
||||
|
||||
BOOL shouldReload = NO;
|
||||
|
||||
if (changedPaths != nil && removedPaths != nil)
|
||||
{
|
||||
for (NSIndexPath *changedPath in changedPaths)
|
||||
{
|
||||
if ([removedPaths containsObject:changedPath])
|
||||
{
|
||||
shouldReload = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (removedPaths.lastObject && ((NSIndexPath *)removedPaths.lastObject).item >= self.fetchResult.count)
|
||||
{
|
||||
shouldReload = YES;
|
||||
}
|
||||
|
||||
if (shouldReload)
|
||||
{
|
||||
[collectionView reloadData];
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// if we have incremental diffs, tell the collection view to animate insertions and deletions
|
||||
[collectionView performBatchUpdates:^{
|
||||
if (removedPaths.count)
|
||||
{
|
||||
[collectionView deleteItemsAtIndexPaths:[removedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0]];
|
||||
}
|
||||
|
||||
if (insertedPaths.count)
|
||||
{
|
||||
[collectionView insertItemsAtIndexPaths:[insertedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0]];
|
||||
}
|
||||
|
||||
if (changedPaths.count)
|
||||
{
|
||||
[collectionView reloadItemsAtIndexPaths:[changedIndexes ctassetsPickerIndexPathsFromIndexesWithSection:0] ];
|
||||
}
|
||||
} completion:^(BOOL finished){
|
||||
if (finished)
|
||||
[self resetCachedAssetImages];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
[self.footer bind:self.fetchResult];
|
||||
|
||||
if (self.fetchResult.count == 0)
|
||||
[self showNoAssets];
|
||||
else
|
||||
[self hideNoAssets];
|
||||
}
|
||||
|
||||
if ([self.delegate respondsToSelector:@selector(assetsGridViewController:photoLibraryDidChangeForAssetCollection:)])
|
||||
[self.delegate assetsGridViewController:self photoLibraryDidChangeForAssetCollection:self.assetCollection];
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Selected assets changed
|
||||
|
||||
- (void)assetsPickerSelectedAssetsDidChange:(NSNotification *)notification
|
||||
{
|
||||
NSArray *selectedAssets = (NSArray *)notification.object;
|
||||
[self updateTitle:selectedAssets];
|
||||
[self updateButton:selectedAssets];
|
||||
}
|
||||
|
||||
- (void)updateTitle:(NSArray *)selectedAssets
|
||||
{
|
||||
if (selectedAssets.count > 0)
|
||||
self.title = self.picker.selectedAssetsString;
|
||||
else
|
||||
self.title = self.assetCollection.localizedTitle;
|
||||
}
|
||||
|
||||
- (void)updateButton:(NSArray *)selectedAssets
|
||||
{
|
||||
if (self.picker.alwaysEnableDoneButton)
|
||||
self.navigationItem.rightBarButtonItem.enabled = YES;
|
||||
else
|
||||
self.navigationItem.rightBarButtonItem.enabled = (self.picker.selectedAssets.count > 0);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Did de/select asset notifications
|
||||
|
||||
- (void)assetsPickerDidSelectAsset:(NSNotification *)notification
|
||||
{
|
||||
PHAsset *asset = (PHAsset *)notification.object;
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:[self.fetchResult indexOfObject:asset] inSection:0];
|
||||
[self.collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
|
||||
|
||||
[self updateSelectionOrderLabels];
|
||||
}
|
||||
|
||||
- (void)assetsPickerDidDeselectAsset:(NSNotification *)notification
|
||||
{
|
||||
PHAsset *asset = (PHAsset *)notification.object;
|
||||
NSIndexPath *indexPath = [NSIndexPath indexPathForItem:[self.fetchResult indexOfObject:asset] inSection:0];
|
||||
[self.collectionView deselectItemAtIndexPath:indexPath animated:NO];
|
||||
|
||||
[self updateSelectionOrderLabels];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update Selection Order Labels
|
||||
|
||||
- (void)updateSelectionOrderLabels
|
||||
{
|
||||
for (NSIndexPath *indexPath in [self.collectionView indexPathsForSelectedItems])
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
CTAssetsGridViewCell *cell = (CTAssetsGridViewCell *)[self.collectionView cellForItemAtIndexPath:indexPath];
|
||||
cell.selectionIndex = [self.picker.selectedAssets indexOfObject:asset];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Gesture recognizer
|
||||
|
||||
- (void)addGestureRecognizer
|
||||
{
|
||||
UILongPressGestureRecognizer *longPress =
|
||||
[[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(pushPageViewController:)];
|
||||
|
||||
[self.collectionView addGestureRecognizer:longPress];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Push assets page view controller
|
||||
|
||||
- (void)pushPageViewController:(UILongPressGestureRecognizer *)longPress
|
||||
{
|
||||
if (longPress.state == UIGestureRecognizerStateBegan)
|
||||
{
|
||||
CGPoint point = [longPress locationInView:self.collectionView];
|
||||
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
|
||||
|
||||
CTAssetsPageViewController *vc = [[CTAssetsPageViewController alloc] initWithFetchResult:self.fetchResult];
|
||||
vc.allowsSelection = YES;
|
||||
vc.pageIndex = indexPath.item;
|
||||
|
||||
[self.navigationController pushViewController:vc animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Reload data
|
||||
|
||||
- (void)reloadData
|
||||
{
|
||||
if (self.fetchResult.count > 0)
|
||||
{
|
||||
[self hideNoAssets];
|
||||
[self.collectionView reloadData];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self showNoAssets];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Asset images caching
|
||||
|
||||
- (void)resetCachedAssetImages
|
||||
{
|
||||
[self.imageManager stopCachingImagesForAllAssets];
|
||||
self.previousPreheatRect = CGRectZero;
|
||||
}
|
||||
|
||||
- (void)updateCachedAssetImages
|
||||
{
|
||||
BOOL isViewVisible = [self isViewLoaded] && self.view.window != nil;
|
||||
|
||||
if (!isViewVisible)
|
||||
return;
|
||||
|
||||
// The preheat window is twice the height of the visible rect
|
||||
CGRect preheatRect = self.collectionView.bounds;
|
||||
preheatRect = CGRectInset(preheatRect, 0.0f, -0.5f * CGRectGetHeight(preheatRect));
|
||||
|
||||
// If scrolled by a "reasonable" amount...
|
||||
CGFloat delta = ABS(CGRectGetMidY(preheatRect) - CGRectGetMidY(self.previousPreheatRect));
|
||||
|
||||
if (delta > CGRectGetHeight(self.collectionView.bounds) / 3.0f)
|
||||
{
|
||||
// Compute the assets to start caching and to stop caching.
|
||||
NSMutableArray *addedIndexPaths = [NSMutableArray array];
|
||||
NSMutableArray *removedIndexPaths = [NSMutableArray array];
|
||||
|
||||
[self computeDifferenceBetweenRect:self.previousPreheatRect
|
||||
andRect:preheatRect
|
||||
removedHandler:^(CGRect removedRect) {
|
||||
NSArray *indexPaths = [self.collectionView ctassetsPickerIndexPathsForElementsInRect:removedRect];
|
||||
[removedIndexPaths addObjectsFromArray:indexPaths];
|
||||
} addedHandler:^(CGRect addedRect) {
|
||||
NSArray *indexPaths = [self.collectionView ctassetsPickerIndexPathsForElementsInRect:addedRect];
|
||||
[addedIndexPaths addObjectsFromArray:indexPaths];
|
||||
}];
|
||||
|
||||
[self startCachingThumbnailsForIndexPaths:addedIndexPaths];
|
||||
[self stopCachingThumbnailsForIndexPaths:removedIndexPaths];
|
||||
|
||||
self.previousPreheatRect = preheatRect;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)startCachingThumbnailsForIndexPaths:(NSArray *)indexPaths
|
||||
{
|
||||
for (NSIndexPath *indexPath in indexPaths)
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
if (!asset) break;
|
||||
|
||||
UICollectionViewLayoutAttributes *attributes =
|
||||
[self.collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
|
||||
|
||||
CGSize targetSize = [self.picker imageSizeForContainerSize:attributes.size];
|
||||
|
||||
[self.imageManager startCachingImagesForAssets:@[asset]
|
||||
targetSize:targetSize
|
||||
contentMode:PHImageContentModeAspectFill
|
||||
options:self.picker.thumbnailRequestOptions];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)stopCachingThumbnailsForIndexPaths:(NSArray *)indexPaths
|
||||
{
|
||||
for (NSIndexPath *indexPath in indexPaths)
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
if (!asset) break;
|
||||
|
||||
UICollectionViewLayoutAttributes *attributes =
|
||||
[self.collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
|
||||
|
||||
CGSize targetSize = [self.picker imageSizeForContainerSize:attributes.size];
|
||||
|
||||
[self.imageManager stopCachingImagesForAssets:@[asset]
|
||||
targetSize:targetSize
|
||||
contentMode:PHImageContentModeAspectFill
|
||||
options:self.picker.thumbnailRequestOptions];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)computeDifferenceBetweenRect:(CGRect)oldRect andRect:(CGRect)newRect removedHandler:(void (^)(CGRect removedRect))removedHandler addedHandler:(void (^)(CGRect addedRect))addedHandler
|
||||
{
|
||||
if (CGRectIntersectsRect(newRect, oldRect)) {
|
||||
CGFloat oldMaxY = CGRectGetMaxY(oldRect);
|
||||
CGFloat oldMinY = CGRectGetMinY(oldRect);
|
||||
CGFloat newMaxY = CGRectGetMaxY(newRect);
|
||||
CGFloat newMinY = CGRectGetMinY(newRect);
|
||||
if (newMaxY > oldMaxY) {
|
||||
CGRect rectToAdd = CGRectMake(newRect.origin.x, oldMaxY, newRect.size.width, (newMaxY - oldMaxY));
|
||||
addedHandler(rectToAdd);
|
||||
}
|
||||
if (oldMinY > newMinY) {
|
||||
CGRect rectToAdd = CGRectMake(newRect.origin.x, newMinY, newRect.size.width, (oldMinY - newMinY));
|
||||
addedHandler(rectToAdd);
|
||||
}
|
||||
if (newMaxY < oldMaxY) {
|
||||
CGRect rectToRemove = CGRectMake(newRect.origin.x, newMaxY, newRect.size.width, (oldMaxY - newMaxY));
|
||||
removedHandler(rectToRemove);
|
||||
}
|
||||
if (oldMinY < newMinY) {
|
||||
CGRect rectToRemove = CGRectMake(newRect.origin.x, oldMinY, newRect.size.width, (newMinY - oldMinY));
|
||||
removedHandler(rectToRemove);
|
||||
}
|
||||
} else {
|
||||
addedHandler(newRect);
|
||||
removedHandler(oldRect);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Scroll view delegate
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
[self updateCachedAssetImages];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - No assets
|
||||
|
||||
- (void)showNoAssets
|
||||
{
|
||||
CTAssetsPickerNoAssetsView *view = [CTAssetsPickerNoAssetsView new];
|
||||
[self.view addSubview:view];
|
||||
[view setNeedsUpdateConstraints];
|
||||
[view updateConstraintsIfNeeded];
|
||||
|
||||
self.noAssetsView = view;
|
||||
}
|
||||
|
||||
- (void)hideNoAssets
|
||||
{
|
||||
if (self.noAssetsView)
|
||||
{
|
||||
[self.noAssetsView removeFromSuperview];
|
||||
self.noAssetsView = nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Collection view data source
|
||||
|
||||
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
|
||||
{
|
||||
return self.fetchResult.count;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
CTAssetsGridViewCell *cell =
|
||||
[collectionView dequeueReusableCellWithReuseIdentifier:CTAssetsGridViewCellIdentifier
|
||||
forIndexPath:indexPath];
|
||||
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldEnableAsset:)])
|
||||
cell.enabled = [self.picker.delegate assetsPickerController:self.picker shouldEnableAsset:asset];
|
||||
else
|
||||
cell.enabled = YES;
|
||||
|
||||
cell.showsSelectionIndex = self.picker.showsSelectionIndex;
|
||||
|
||||
// XXX
|
||||
// Setting `selected` property blocks further deselection.
|
||||
// Have to call selectItemAtIndexPath too. ( ref: http://stackoverflow.com/a/17812116/1648333 )
|
||||
if ([self.picker.selectedAssets containsObject:asset])
|
||||
{
|
||||
cell.selected = YES;
|
||||
cell.selectionIndex = [self.picker.selectedAssets indexOfObject:asset];
|
||||
[collectionView selectItemAtIndexPath:indexPath animated:NO scrollPosition:UICollectionViewScrollPositionNone];
|
||||
}
|
||||
|
||||
[cell bind:asset];
|
||||
|
||||
UICollectionViewLayoutAttributes *attributes =
|
||||
[collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath];
|
||||
|
||||
CGSize targetSize = [self.picker imageSizeForContainerSize:attributes.size];
|
||||
|
||||
[self requestThumbnailForCell:cell targetSize:targetSize asset:asset];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)requestThumbnailForCell:(CTAssetsGridViewCell *)cell targetSize:(CGSize)targetSize asset:(PHAsset *)asset
|
||||
{
|
||||
NSInteger tag = cell.tag + 1;
|
||||
cell.tag = tag;
|
||||
|
||||
[self.imageManager ctassetsPickerRequestImageForAsset:asset
|
||||
targetSize:targetSize
|
||||
contentMode:PHImageContentModeAspectFill
|
||||
options:self.picker.thumbnailRequestOptions
|
||||
resultHandler:^(UIImage *image, NSDictionary *info){
|
||||
// Only update the image if the cell tag hasn't changed. Otherwise, the cell has been re-used.
|
||||
if (cell.tag == tag)
|
||||
[(CTAssetThumbnailView *)cell.backgroundView bind:image asset:asset];
|
||||
}];
|
||||
}
|
||||
|
||||
- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
CTAssetsGridViewFooter *footer =
|
||||
[collectionView dequeueReusableSupplementaryViewOfKind:UICollectionElementKindSectionFooter
|
||||
withReuseIdentifier:CTAssetsGridViewFooterIdentifier
|
||||
forIndexPath:indexPath];
|
||||
|
||||
[footer bind:self.fetchResult];
|
||||
|
||||
self.footer = footer;
|
||||
|
||||
return footer;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Collection view delegate
|
||||
|
||||
- (BOOL)collectionView:(UICollectionView *)collectionView shouldSelectItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
CTAssetsGridViewCell *cell = (CTAssetsGridViewCell *)[collectionView cellForItemAtIndexPath:indexPath];
|
||||
|
||||
if (!cell.isEnabled)
|
||||
return NO;
|
||||
else if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldSelectAsset:)])
|
||||
return [self.picker.delegate assetsPickerController:self.picker shouldSelectAsset:asset];
|
||||
else
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
[self.picker selectAsset:asset];
|
||||
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didSelectAsset:)])
|
||||
[self.picker.delegate assetsPickerController:self.picker didSelectAsset:asset];
|
||||
}
|
||||
|
||||
- (BOOL)collectionView:(UICollectionView *)collectionView shouldDeselectItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldDeselectAsset:)])
|
||||
return [self.picker.delegate assetsPickerController:self.picker shouldDeselectAsset:asset];
|
||||
else
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
[self.picker deselectAsset:asset];
|
||||
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didDeselectAsset:)])
|
||||
[self.picker.delegate assetsPickerController:self.picker didDeselectAsset:asset];
|
||||
}
|
||||
|
||||
- (BOOL)collectionView:(UICollectionView *)collectionView shouldHighlightItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:shouldHighlightAsset:)])
|
||||
return [self.picker.delegate assetsPickerController:self.picker shouldHighlightAsset:asset];
|
||||
else
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didHighlightAsset:)])
|
||||
[self.picker.delegate assetsPickerController:self.picker didHighlightAsset:asset];
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
PHAsset *asset = [self assetAtIndexPath:indexPath];
|
||||
|
||||
if ([self.picker.delegate respondsToSelector:@selector(assetsPickerController:didUnhighlightAsset:)])
|
||||
[self.picker.delegate assetsPickerController:self.picker didUnhighlightAsset:asset];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
|
||||
@interface CTAssetsGridViewFooter : UICollectionReusableView
|
||||
|
||||
@property (nonatomic, weak, nullable) UIFont *font UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic, weak, nullable) UIColor *textColor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
- (void)bind:(nonnull PHFetchResult *)result;
|
||||
|
||||
@end
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetsGridViewFooter.h"
|
||||
#import "NSNumberFormatter+CTAssetsPickerController.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
|
||||
@interface CTAssetsGridViewFooter ()
|
||||
|
||||
@property (nonatomic, strong) UILabel *label;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetsGridViewFooter
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
UILabel *label = [UILabel newAutoLayoutView];
|
||||
label.textAlignment = NSTextAlignmentCenter;
|
||||
label.font = CTAssetsGridViewFooterFont;
|
||||
label.textColor = CTAssetsGridViewFooterTextColor;
|
||||
|
||||
self.label = label;
|
||||
[self addSubview:self.label];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Appearance
|
||||
|
||||
- (UIFont *)font
|
||||
{
|
||||
return self.label.font;
|
||||
}
|
||||
|
||||
- (void)setFont:(UIFont *)font
|
||||
{
|
||||
UIFont *labelFont = (font) ? font : CTAssetsGridViewFooterFont;
|
||||
self.label.font = labelFont;
|
||||
}
|
||||
|
||||
- (UIColor *)textColor
|
||||
{
|
||||
return self.label.textColor;
|
||||
}
|
||||
|
||||
- (void)setTextColor:(UIColor *)textColor
|
||||
{
|
||||
UIColor *color = (textColor) ? textColor : CTAssetsGridViewFooterTextColor;
|
||||
self.label.textColor = color;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
[self.label autoPinEdgesToSuperviewMargins];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
- (void)bind:(PHFetchResult *)result
|
||||
{
|
||||
NSNumberFormatter *nf = [NSNumberFormatter new];
|
||||
|
||||
NSString *numberOfVideos = @"";
|
||||
NSString *numberOfPhotos = @"";
|
||||
|
||||
NSUInteger videoCount = [result countOfAssetsWithMediaType:PHAssetMediaTypeVideo];
|
||||
NSUInteger photoCount = [result countOfAssetsWithMediaType:PHAssetMediaTypeImage];
|
||||
|
||||
if (videoCount > 0)
|
||||
numberOfVideos = [nf ctassetsPickerStringFromAssetsCount:videoCount];
|
||||
|
||||
if (photoCount > 0)
|
||||
numberOfPhotos = [nf ctassetsPickerStringFromAssetsCount:photoCount];
|
||||
|
||||
if (photoCount > 0 && videoCount > 0)
|
||||
self.label.text = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ Photos, %@ Videos", nil), numberOfPhotos, numberOfVideos];
|
||||
else if (photoCount > 0 && videoCount <= 0)
|
||||
self.label.text = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ Photos", nil), numberOfPhotos];
|
||||
else if (photoCount <= 0 && videoCount > 0)
|
||||
self.label.text = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ Videos", nil), numberOfVideos];
|
||||
else
|
||||
self.label.text = @"";
|
||||
|
||||
self.hidden = (result.count == 0);
|
||||
|
||||
[self setNeedsUpdateConstraints];
|
||||
[self updateConstraintsIfNeeded];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CTAssetsGridViewLayout : UICollectionViewFlowLayout
|
||||
|
||||
- (instancetype)initWithContentSize:(CGSize)contentSize traitCollection:(UITraitCollection *)traits;
|
||||
|
||||
@end
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import "CTAssetsGridViewLayout.h"
|
||||
|
||||
@implementation CTAssetsGridViewLayout
|
||||
|
||||
- (instancetype)initWithContentSize:(CGSize)contentSize traitCollection:(UITraitCollection *)traits
|
||||
{
|
||||
if (self = [super init])
|
||||
{
|
||||
CGFloat scale = traits.displayScale;
|
||||
NSInteger numberOfColumns = [self numberOfColumnsForTraitCollection:traits];
|
||||
CGFloat onePixel = (scale == 3.0) ? (2.0 / scale) : (1.0 / scale);
|
||||
|
||||
// spacing is as small as possible
|
||||
self.minimumInteritemSpacing = onePixel;
|
||||
self.minimumLineSpacing = onePixel;
|
||||
|
||||
// total spaces between items (in pixel)
|
||||
CGFloat spaces = self.minimumInteritemSpacing * (numberOfColumns - 1);
|
||||
|
||||
// item length (in pixel)
|
||||
CGFloat length = (scale * (contentSize.width - spaces)) / numberOfColumns;
|
||||
|
||||
// remaining spaces (in pixel) after rounding the length to integer
|
||||
CGFloat insets = (length - floor(length)) * numberOfColumns;
|
||||
|
||||
// round the length to integer (in pixel)
|
||||
length = floor(length);
|
||||
|
||||
// divide insets to two
|
||||
CGFloat left = insets / 2;
|
||||
CGFloat right = insets / 2;
|
||||
|
||||
// adjust if insets is odd
|
||||
if (fmodf(insets, 2.0) == 1.0f)
|
||||
{
|
||||
left -= 0.5;
|
||||
right += 0.5;
|
||||
}
|
||||
|
||||
// left / right insets (in point, 2 decimal)
|
||||
left = floorf(left / scale * 100) / 100;
|
||||
right = floorf(right / scale * 100) / 100;
|
||||
|
||||
// item length (in point, 2 decimal)
|
||||
length = floorf(length / scale * 100) / 100;
|
||||
|
||||
self.sectionInset = UIEdgeInsetsMake(0, left, 0, right);
|
||||
self.itemSize = CGSizeMake(length, length);
|
||||
|
||||
self.footerReferenceSize = CGSizeMake(contentSize.width, floor(length * 2/3));
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfColumnsForTraitCollection:(UITraitCollection *)traits
|
||||
{
|
||||
switch (traits.userInterfaceIdiom) {
|
||||
case UIUserInterfaceIdiomPad:
|
||||
{
|
||||
return 6;
|
||||
break;
|
||||
}
|
||||
case UIUserInterfaceIdiomPhone:
|
||||
{
|
||||
// iPhone 6+ landscape
|
||||
if (traits.horizontalSizeClass == UIUserInterfaceSizeClassRegular)
|
||||
return 4;
|
||||
// iPhone landscape
|
||||
else if (traits.verticalSizeClass == UIUserInterfaceSizeClassCompact)
|
||||
return 6;
|
||||
// iPhone portrait
|
||||
else
|
||||
return 4;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 bawn
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CTAssetsNavigationController : UINavigationController
|
||||
|
||||
@end
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 bawn
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import "CTAssetsNavigationController.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetsNavigationController ()<UIGestureRecognizerDelegate, UINavigationControllerDelegate>
|
||||
|
||||
@end
|
||||
|
||||
@implementation CTAssetsNavigationController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
__weak CTAssetsNavigationController<UIGestureRecognizerDelegate> *weakSelf = self;
|
||||
|
||||
if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)])
|
||||
{
|
||||
self.interactivePopGestureRecognizer.delegate = weakSelf;
|
||||
|
||||
self.delegate = weakSelf;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated
|
||||
{
|
||||
|
||||
if ( [self respondsToSelector:@selector(interactivePopGestureRecognizer)] && animated == YES )
|
||||
{
|
||||
self.interactivePopGestureRecognizer.enabled = NO;
|
||||
}
|
||||
|
||||
[super pushViewController:viewController animated:animated];
|
||||
|
||||
}
|
||||
|
||||
- (NSArray *)popToRootViewControllerAnimated:(BOOL)animated
|
||||
{
|
||||
if ( [self respondsToSelector:@selector(interactivePopGestureRecognizer)] && animated == YES )
|
||||
{
|
||||
self.interactivePopGestureRecognizer.enabled = NO;
|
||||
}
|
||||
|
||||
return [super popToRootViewControllerAnimated:animated];
|
||||
|
||||
}
|
||||
|
||||
- (NSArray *)popToViewController:(UIViewController *)viewController animated:(BOOL)animated
|
||||
{
|
||||
if( [self respondsToSelector:@selector(interactivePopGestureRecognizer)] )
|
||||
{
|
||||
self.interactivePopGestureRecognizer.enabled = NO;
|
||||
}
|
||||
|
||||
return [super popToViewController:viewController animated:animated];
|
||||
|
||||
}
|
||||
|
||||
#pragma mark UINavigationControllerDelegate
|
||||
|
||||
- (void)navigationController:(UINavigationController *)navigationController
|
||||
didShowViewController:(UIViewController *)viewController
|
||||
animated:(BOOL)animate
|
||||
{
|
||||
if ([self respondsToSelector:@selector(interactivePopGestureRecognizer)])
|
||||
{
|
||||
self.interactivePopGestureRecognizer.enabled = YES;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer
|
||||
{
|
||||
|
||||
if ( gestureRecognizer == self.interactivePopGestureRecognizer )
|
||||
{
|
||||
if ( self.viewControllers.count < 2 || self.visibleViewController == self.viewControllers[0] )
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CTAssetsPageView : UIView
|
||||
|
||||
@property (nonatomic, strong, nullable) UIColor *pageBackgroundColor UI_APPEARANCE_SELECTOR;
|
||||
@property (nonatomic, strong, nullable) UIColor *fullscreenBackgroundColor UI_APPEARANCE_SELECTOR;
|
||||
|
||||
- (void)enterFullscreen;
|
||||
- (void)exitFullscreen;
|
||||
|
||||
@end
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetsPageView.h"
|
||||
|
||||
@interface CTAssetsPageView ()
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetsPageView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
_pageBackgroundColor = CTAssetsPageViewPageBackgroundColor;
|
||||
_fullscreenBackgroundColor = CTAssetsPageViewFullscreenBackgroundColor;
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
self.backgroundColor = self.pageBackgroundColor;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Apperance
|
||||
|
||||
- (void)setPageBackgroundColor:(UIColor *)backgroundColor
|
||||
{
|
||||
UIColor *color = (backgroundColor) ? backgroundColor : CTAssetsPageViewPageBackgroundColor;
|
||||
_pageBackgroundColor = color;
|
||||
self.backgroundColor = color;
|
||||
}
|
||||
|
||||
- (void)setFullscreenBackgroundColor:(UIColor *)fullscreenBackgroundColor
|
||||
{
|
||||
UIColor *color = (fullscreenBackgroundColor) ? fullscreenBackgroundColor : CTAssetsPageViewFullscreenBackgroundColor;
|
||||
_fullscreenBackgroundColor = color;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
[self autoPinEdgesToSuperviewEdges];
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Fading views
|
||||
|
||||
- (void)enterFullscreen
|
||||
{
|
||||
[UIView animateWithDuration:0.2
|
||||
animations:^{
|
||||
self.backgroundColor = self.fullscreenBackgroundColor;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)exitFullscreen
|
||||
{
|
||||
[UIView animateWithDuration:0.2
|
||||
animations:^{
|
||||
self.backgroundColor = self.pageBackgroundColor;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Photos/Photos.h>
|
||||
|
||||
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/**
|
||||
* A view controller that shows selected photos and vidoes from user's photo library that let the user navigate the item page by page.
|
||||
*/
|
||||
@interface CTAssetsPageViewController : UIPageViewController
|
||||
|
||||
/**
|
||||
* The index of the photo or video with the currently showing item.
|
||||
*/
|
||||
@property (nonatomic, assign) NSInteger pageIndex;
|
||||
|
||||
|
||||
/**
|
||||
* @name Creating a Assets Page View Controller
|
||||
*/
|
||||
|
||||
/**
|
||||
* Initializes a newly created view controller with a fetech result.
|
||||
*
|
||||
* @param fetchResult A fetch result of `PHAsset` objects.
|
||||
*
|
||||
* @return An instance of `CTAssetPageViewController` initialized to show the asset items in `fetchResult`.
|
||||
*/
|
||||
- (instancetype)initWithFetchResult:(PHFetchResult *)fetchResult;
|
||||
|
||||
/**
|
||||
* Initializes a newly created view controller with an array of assets.
|
||||
*
|
||||
* @param assets An array of `PHAsset` objects.
|
||||
*
|
||||
* @return An instance of `CTAssetPageViewController` initialized to show the asset items in `assets`.
|
||||
*/
|
||||
- (instancetype)initWithAssets:(NSArray<PHAsset*> *)assets;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,422 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import "CTAssetsPageViewController.h"
|
||||
#import "CTAssetsPageView.h"
|
||||
#import "CTAssetItemViewController.h"
|
||||
#import "CTAssetScrollView.h"
|
||||
#import "NSNumberFormatter+CTAssetsPickerController.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
#import "UIImage+CTAssetsPickerController.h"
|
||||
#import "PHAsset+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@interface CTAssetsPageViewController ()
|
||||
<UIPageViewControllerDataSource, UIPageViewControllerDelegate>
|
||||
|
||||
|
||||
@property (nonatomic, assign) BOOL allowsSelection;
|
||||
|
||||
@property (nonatomic, assign, getter = isStatusBarHidden) BOOL statusBarHidden;
|
||||
|
||||
@property (nonatomic, copy) NSArray *assets;
|
||||
@property (nonatomic, strong, readonly) PHAsset *asset;
|
||||
|
||||
@property (nonatomic, strong) CTAssetsPageView *pageView;
|
||||
|
||||
@property (nonatomic, strong) UIBarButtonItem *playButton;
|
||||
@property (nonatomic, strong) UIBarButtonItem *pauseButton;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@implementation CTAssetsPageViewController
|
||||
|
||||
- (instancetype)initWithFetchResult:(PHFetchResult *)fetchResult
|
||||
{
|
||||
NSMutableArray *assets = [NSMutableArray new];
|
||||
|
||||
for (PHAsset *asset in fetchResult)
|
||||
[assets addObject:asset];
|
||||
|
||||
return [self initWithAssets:[NSArray arrayWithArray:assets]];
|
||||
}
|
||||
|
||||
- (instancetype)initWithAssets:(NSArray *)assets
|
||||
{
|
||||
self = [super initWithTransitionStyle:UIPageViewControllerTransitionStyleScroll
|
||||
navigationOrientation:UIPageViewControllerNavigationOrientationHorizontal
|
||||
options:@{UIPageViewControllerOptionInterPageSpacingKey:@30.f}];
|
||||
|
||||
if (self)
|
||||
{
|
||||
self.assets = assets;
|
||||
self.dataSource = self;
|
||||
self.delegate = self;
|
||||
self.allowsSelection = NO;
|
||||
self.automaticallyAdjustsScrollViewInsets = NO;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
[self setupViews];
|
||||
[self addNotificationObserver];
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[self removeNotificationObserver];
|
||||
}
|
||||
|
||||
- (BOOL)prefersStatusBarHidden
|
||||
{
|
||||
if (self.traitCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact)
|
||||
return YES;
|
||||
else
|
||||
return self.isStatusBarHidden;
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
self.pageView = [CTAssetsPageView new];
|
||||
[self.view insertSubview:self.pageView atIndex:0];
|
||||
[self.view setNeedsUpdateConstraints];
|
||||
}
|
||||
|
||||
- (void)setupButtons
|
||||
{
|
||||
if (!self.playButton)
|
||||
{
|
||||
UIImage *playImage = [UIImage ctassetsPickerImageNamed:@"PlayButton"];
|
||||
playImage = [playImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
|
||||
UIBarButtonItem *playButton =
|
||||
[[UIBarButtonItem alloc] initWithImage:playImage style:UIBarButtonItemStyleDone target:self action:@selector(playAsset:)];
|
||||
|
||||
self.playButton = playButton;
|
||||
}
|
||||
|
||||
if (!self.pauseButton)
|
||||
{
|
||||
UIImage *pasueImage = [UIImage ctassetsPickerImageNamed:@"PauseButton"];
|
||||
pasueImage = [pasueImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
|
||||
UIBarButtonItem *pauseButton =
|
||||
[[UIBarButtonItem alloc] initWithImage:pasueImage style:UIBarButtonItemStylePlain target:self action:@selector(pauseAsset:)];
|
||||
|
||||
self.pauseButton = pauseButton;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update title
|
||||
|
||||
- (void)updateTitle:(NSInteger)index
|
||||
{
|
||||
NSNumberFormatter *nf = [NSNumberFormatter new];
|
||||
|
||||
NSInteger count = self.assets.count;
|
||||
self.title = [NSString stringWithFormat:CTAssetsPickerLocalizedString(@"%@ of %@", nil),
|
||||
[nf ctassetsPickerStringFromAssetsCount:index],
|
||||
[nf ctassetsPickerStringFromAssetsCount:count]];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Update toolbar
|
||||
|
||||
- (void)updateToolbar
|
||||
{
|
||||
[self setupButtons];
|
||||
|
||||
if ([self.asset ctassetsPickerIsVideo])
|
||||
self.toolbarItems = @[[self toolbarSpace], self.playButton, [self toolbarSpace]];
|
||||
else
|
||||
self.toolbarItems = nil;
|
||||
}
|
||||
|
||||
- (void)replaceToolbarButton:(UIBarButtonItem *)button
|
||||
{
|
||||
if (button)
|
||||
{
|
||||
UIBarButtonItem *space = [self toolbarSpace];
|
||||
self.toolbarItems = @[space, button, space];
|
||||
}
|
||||
}
|
||||
|
||||
- (UIBarButtonItem *)toolbarSpace
|
||||
{
|
||||
return [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
|
||||
}
|
||||
|
||||
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
- (NSInteger)pageIndex
|
||||
{
|
||||
return [self.assets indexOfObject:self.asset];
|
||||
}
|
||||
|
||||
- (void)setPageIndex:(NSInteger)pageIndex
|
||||
{
|
||||
NSInteger count = self.assets.count;
|
||||
|
||||
if (pageIndex >= 0 && pageIndex < count)
|
||||
{
|
||||
PHAsset *asset = self.assets[pageIndex];
|
||||
|
||||
CTAssetItemViewController *page = [CTAssetItemViewController assetItemViewControllerForAsset:asset];
|
||||
page.allowsSelection = self.allowsSelection;
|
||||
|
||||
[self setViewControllers:@[page]
|
||||
direction:UIPageViewControllerNavigationDirectionForward
|
||||
animated:NO
|
||||
completion:NULL];
|
||||
|
||||
[self updateTitle:pageIndex + 1];
|
||||
[self updateToolbar];
|
||||
}
|
||||
}
|
||||
|
||||
- (PHAsset *)asset
|
||||
{
|
||||
return ((CTAssetItemViewController *)self.viewControllers[0]).asset;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Page view controller data source
|
||||
|
||||
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerBeforeViewController:(UIViewController *)viewController
|
||||
{
|
||||
PHAsset *asset = ((CTAssetItemViewController *)viewController).asset;
|
||||
NSInteger index = [self.assets indexOfObject:asset];
|
||||
|
||||
if (index > 0)
|
||||
{
|
||||
PHAsset *beforeAsset = self.assets[(index - 1)];
|
||||
CTAssetItemViewController *page = [CTAssetItemViewController assetItemViewControllerForAsset:beforeAsset];
|
||||
page.allowsSelection = self.allowsSelection;
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UIViewController *)pageViewController:(UIPageViewController *)pageViewController viewControllerAfterViewController:(UIViewController *)viewController
|
||||
{
|
||||
PHAsset *asset = ((CTAssetItemViewController *)viewController).asset;
|
||||
NSInteger index = [self.assets indexOfObject:asset];
|
||||
NSInteger count = self.assets.count;
|
||||
|
||||
if (index < count - 1)
|
||||
{
|
||||
PHAsset *afterAsset = self.assets[(index + 1)];
|
||||
CTAssetItemViewController *page = [CTAssetItemViewController assetItemViewControllerForAsset:afterAsset];
|
||||
page.allowsSelection = self.allowsSelection;
|
||||
|
||||
return page;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Page view controller delegate
|
||||
|
||||
- (void)pageViewController:(UIPageViewController *)pageViewController didFinishAnimating:(BOOL)finished previousViewControllers:(NSArray *)previousViewControllers transitionCompleted:(BOOL)completed
|
||||
{
|
||||
if (completed)
|
||||
{
|
||||
CTAssetItemViewController *vc = (CTAssetItemViewController *)pageViewController.viewControllers[0];
|
||||
NSInteger index = [self.assets indexOfObject:vc.asset] + 1;
|
||||
|
||||
[self updateTitle:index];
|
||||
[self updateToolbar];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pageViewController:(UIPageViewController *)pageViewController willTransitionToViewControllers:(NSArray *)pendingViewControllers
|
||||
{
|
||||
[self.navigationController setToolbarHidden:YES animated:YES];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Notification observer
|
||||
|
||||
- (void)addNotificationObserver
|
||||
{
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(assetScrollViewDidTap:)
|
||||
name:CTAssetScrollViewDidTapNotification
|
||||
object:nil];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(assetScrollViewPlayerDidPlayToEnd:)
|
||||
name:AVPlayerItemDidPlayToEndTimeNotification
|
||||
object:nil];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(assetScrollViewPlayerWillPlay:)
|
||||
name:CTAssetScrollViewPlayerWillPlayNotification
|
||||
object:nil];
|
||||
|
||||
[center addObserver:self
|
||||
selector:@selector(assetScrollViewPlayerWillPause:)
|
||||
name:CTAssetScrollViewPlayerWillPauseNotification
|
||||
object:nil];
|
||||
}
|
||||
|
||||
- (void)removeNotificationObserver
|
||||
{
|
||||
NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
|
||||
|
||||
[center removeObserver:self name:CTAssetScrollViewDidTapNotification object:nil];
|
||||
[center removeObserver:self name:AVPlayerItemDidPlayToEndTimeNotification object:nil];
|
||||
[center removeObserver:self name:CTAssetScrollViewPlayerWillPlayNotification object:nil];
|
||||
[center removeObserver:self name:CTAssetScrollViewPlayerWillPauseNotification object:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Notification events
|
||||
|
||||
- (void)assetScrollViewDidTap:(NSNotification *)notification
|
||||
{
|
||||
UITapGestureRecognizer *gesture = (UITapGestureRecognizer *)notification.object;
|
||||
|
||||
if (gesture.numberOfTapsRequired == 1)
|
||||
[self toggleFullscreen:gesture];
|
||||
}
|
||||
|
||||
- (void)assetScrollViewPlayerDidPlayToEnd:(NSNotification *)notification
|
||||
{
|
||||
[self replaceToolbarButton:self.playButton];
|
||||
[self setFullscreen:NO];
|
||||
}
|
||||
|
||||
- (void)assetScrollViewPlayerWillPlay:(NSNotification *)notification
|
||||
{
|
||||
[self replaceToolbarButton:self.pauseButton];
|
||||
[self setFullscreen:YES];
|
||||
}
|
||||
|
||||
- (void)assetScrollViewPlayerWillPause:(NSNotification *)notification
|
||||
{
|
||||
[self replaceToolbarButton:self.playButton];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toggle fullscreen
|
||||
|
||||
- (void)toggleFullscreen:(id)sender
|
||||
{
|
||||
[self setFullscreen:!self.isStatusBarHidden];
|
||||
}
|
||||
|
||||
- (void)setFullscreen:(BOOL)fullscreen
|
||||
{
|
||||
if (fullscreen)
|
||||
{
|
||||
[self.pageView enterFullscreen];
|
||||
[self fadeAwayControls:self.navigationController];
|
||||
}
|
||||
else
|
||||
{
|
||||
[self.pageView exitFullscreen];
|
||||
[self fadeInControls:self.navigationController];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)fadeInControls:(UINavigationController *)nav
|
||||
{
|
||||
self.statusBarHidden = NO;
|
||||
|
||||
[nav setNavigationBarHidden:NO];
|
||||
nav.navigationBar.alpha = 0.0f;
|
||||
|
||||
if ([self.asset ctassetsPickerIsVideo])
|
||||
{
|
||||
[nav setToolbarHidden:NO];
|
||||
nav.toolbar.alpha = 0.0f;
|
||||
}
|
||||
|
||||
[UIView animateWithDuration:0.2
|
||||
animations:^{
|
||||
[self setNeedsStatusBarAppearanceUpdate];
|
||||
nav.navigationBar.alpha = 1.0f;
|
||||
|
||||
if ([self.asset ctassetsPickerIsVideo])
|
||||
nav.toolbar.alpha = 1.0f;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)fadeAwayControls:(UINavigationController *)nav
|
||||
{
|
||||
self.statusBarHidden = YES;
|
||||
|
||||
[UIView animateWithDuration:0.2
|
||||
animations:^{
|
||||
[self setNeedsStatusBarAppearanceUpdate];
|
||||
[nav setNavigationBarHidden:YES animated:NO];
|
||||
[nav setToolbarHidden:YES animated:NO];
|
||||
nav.navigationBar.alpha = 0.0f;
|
||||
nav.toolbar.alpha = 0.0f;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Playback
|
||||
|
||||
- (void)playAsset:(id)sender
|
||||
{
|
||||
[((CTAssetItemViewController *)self.viewControllers[0]) playAsset:sender];
|
||||
}
|
||||
|
||||
- (void)pauseAsset:(id)sender
|
||||
{
|
||||
[((CTAssetItemViewController *)self.viewControllers[0]) pauseAsset:sender];
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface CTAssetsPickerAccessDeniedView : UIView
|
||||
|
||||
@end
|
|
@ -0,0 +1,126 @@
|
|||
/*
|
||||
|
||||
MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Clement CN Tsang
|
||||
|
||||
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.
|
||||
|
||||
*/
|
||||
|
||||
#import <PureLayout/PureLayout.h>
|
||||
#import "CTAssetsPickerDefines.h"
|
||||
#import "CTAssetsPickerAccessDeniedView.h"
|
||||
#import "NSBundle+CTAssetsPickerController.h"
|
||||
#import "UIImage+CTAssetsPickerController.h"
|
||||
|
||||
|
||||
|
||||
@interface CTAssetsPickerAccessDeniedView ()
|
||||
|
||||
@property (nonatomic, strong) UIImageView *padlock;
|
||||
@property (nonatomic, strong) UILabel *title;
|
||||
@property (nonatomic, strong) UILabel *message;
|
||||
|
||||
@property (nonatomic, assign) BOOL didSetupConstraints;
|
||||
|
||||
@end;
|
||||
|
||||
|
||||
@implementation CTAssetsPickerAccessDeniedView
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
if (self = [super initWithFrame:frame])
|
||||
{
|
||||
[self setupViews];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Setup
|
||||
|
||||
- (void)setupViews
|
||||
{
|
||||
UIImageView *padlock = [self padlockImageView];
|
||||
self.padlock = padlock;
|
||||
|
||||
UILabel *title = [UILabel new];
|
||||
title.textColor = CTAssetsPikcerAccessDeniedViewTextColor;
|
||||
title.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
|
||||
title.textAlignment = NSTextAlignmentCenter;
|
||||
title.numberOfLines = 5;
|
||||
title.text = CTAssetsPickerLocalizedString(@"This app does not have access to your photos or videos.", nil);
|
||||
self.title = title;
|
||||
|
||||
UILabel *message = [UILabel new];
|
||||
message.textColor = CTAssetsPikcerAccessDeniedViewTextColor;
|
||||
message.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
|
||||
message.textAlignment = NSTextAlignmentCenter;
|
||||
message.numberOfLines = 5;
|
||||
message.text = CTAssetsPickerLocalizedString(@"You can enable access in Privacy Settings.", nil);
|
||||
self.message = message;
|
||||
|
||||
[self addSubview:self.padlock];
|
||||
[self addSubview:self.title];
|
||||
[self addSubview:self.message];
|
||||
}
|
||||
|
||||
- (UIImageView *)padlockImageView
|
||||
{
|
||||
UIImage *image = [UIImage ctassetsPickerImageNamed:@"AccessDeniedViewLock"];
|
||||
image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate];
|
||||
|
||||
UIImageView *padlock = [[UIImageView alloc] initWithImage:image];
|
||||
padlock.tintColor = CTAssetsPikcerAccessDeniedViewTextColor;
|
||||
|
||||
return padlock;
|
||||
}
|
||||
|
||||
#pragma mark - Update auto layout constraints
|
||||
|
||||
- (void)updateConstraints
|
||||
{
|
||||
if (!self.didSetupConstraints)
|
||||
{
|
||||
[self autoCenterInSuperview];
|
||||
|
||||
// suggested solution for issue #176
|
||||
[self autoPinEdgeToSuperviewEdge:ALEdgeLeading withInset:self.layoutMargins.top];
|
||||
[self autoPinEdgeToSuperviewEdge:ALEdgeTrailing withInset:self.layoutMargins.bottom];
|
||||
|
||||
[self.padlock autoAlignAxisToSuperviewAxis:ALAxisVertical];
|
||||
[self.padlock autoPinEdgeToSuperviewEdge:ALEdgeTop];
|
||||
[self.title autoAlignAxis:ALAxisVertical toSameAxisOfView:self.padlock];
|
||||
[self.title autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.padlock withOffset:20];
|
||||
[self.title autoPinEdgeToSuperviewEdge:ALEdgeLeading];
|
||||
[self.title autoPinEdgeToSuperviewEdge:ALEdgeTrailing];
|
||||
[self.message autoAlignAxis:ALAxisVertical toSameAxisOfView:self.padlock];
|
||||
[self.message autoPinEdge:ALEdgeTop toEdge:ALEdgeBottom ofView:self.title withOffset:10];
|
||||
[self.message autoPinEdgesToSuperviewEdgesWithInsets:UIEdgeInsetsZero excludingEdge:ALEdgeTop];
|
||||
|
||||
self.didSetupConstraints = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
|
||||
@end
|
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/AccessDeniedViewLock.png
Executable file
После Ширина: | Высота: | Размер: 1.6 KiB |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/AccessDeniedViewLock@2x.png
Executable file
После Ширина: | Высота: | Размер: 9.2 KiB |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/AccessDeniedViewLock@3x.png
Executable file
После Ширина: | Высота: | Размер: 7.4 KiB |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeAllPhotos@2x.png
Executable file
После Ширина: | Высота: | Размер: 264 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeAllPhotos@3x.png
Executable file
После Ширина: | Высота: | Размер: 389 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeBurst@2x.png
Executable file
После Ширина: | Высота: | Размер: 240 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeBurst@3x.png
Executable file
После Ширина: | Высота: | Размер: 332 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeFavorites@2x.png
Executable file
После Ширина: | Высота: | Размер: 594 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeFavorites@3x.png
Executable file
После Ширина: | Высота: | Размер: 859 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeLastImport@2x.png
Executable file
После Ширина: | Высота: | Размер: 647 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeLastImport@3x.png
Executable file
После Ширина: | Высота: | Размер: 865 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgePanorama@2x.png
Executable file
После Ширина: | Высота: | Размер: 442 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgePanorama@3x.png
Executable file
После Ширина: | Высота: | Размер: 596 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeRecentlyDeleted@2x.png
Executable file
После Ширина: | Высота: | Размер: 853 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeRecentlyDeleted@3x.png
Executable file
После Ширина: | Высота: | Размер: 1007 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeScreenshots@2x.png
Executable file
После Ширина: | Высота: | Размер: 346 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeScreenshots@3x.png
Executable file
После Ширина: | Высота: | Размер: 483 B |
Двоичные данные
Libraries external/CTAssetsPickerController/CTAssetsPickerController.bundle/BadgeSelfPortraits@2x.png
Executable file
После Ширина: | Высота: | Размер: 551 B |