├── LICENSE
├── README.md
├── UnsplashPhotosApp.xcodeproj
├── project.pbxproj
├── project.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
└── xcuserdata
│ └── ahmed.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
└── UnsplashPhotosApp
├── Assets.xcassets
├── AccentColor.colorset
│ └── Contents.json
├── AppIcon.appiconset
│ └── Contents.json
├── Background.colorset
│ └── Contents.json
├── Contents.json
└── Search.imageset
│ ├── Contents.json
│ └── search.png
├── Preview Content
└── Preview Assets.xcassets
│ └── Contents.json
├── Source
├── Models
│ ├── DownloadPhoto.swift
│ ├── Photo.swift
│ ├── QueryPhoto.swift
│ ├── Topic.swift
│ └── Urls.swift
├── Services
│ └── APIService.swift
├── Utility
│ ├── ApiKeys.swift
│ ├── Extensions.swift
│ └── ImageSaver.swift
├── ViewModels
│ ├── DownloadPhotoViewModel.swift
│ ├── PhotoViewModel.swift
│ ├── SearchPhotoViewModel.swift
│ └── TopicViewModel.swift
└── Views
│ ├── HelperViews
│ ├── ErrorView.swift
│ ├── LoadingView.swift
│ ├── PhotoTile.swift
│ └── StaggeredPhotosView.swift
│ ├── HomeView.swift
│ ├── PhotoView
│ └── PhotoView.swift
│ ├── RecentPhotosView
│ ├── RecentPhotosView.swift
│ └── TopicRow.swift
│ └── SearchPhotosView
│ └── SearchPhotosView.swift
└── UnsplashPhotosAppApp.swift
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # UnsplashPhotosApp
2 |
3 |
4 | ## Features
5 |
6 | ✅ Used MVVM\
7 | ✅ Nested JSON serialization\
8 | ✅ Combine framework\
9 | ✅ Staggered grid with Pagination\
10 | ✅ handled idle, loading, loaded, error state\
11 | ✅ Search option, managed most usecases.\
12 | ✅ Download image to user device
13 |
14 | ### ⚠️ Before you run this project you need to add your accessKey.
15 |
16 | ## Don't know how to add accessKey?
17 |
18 | 1. Create Unsplash account
19 | 2. At the top right corner open the menu
20 | 3. Select "Developers/API" under "Product"
21 | 4. Select "Your Apps"
22 | 5. Add a new app and copy the accessKey
23 | 6. Open this project
24 | 7. Open `/UnsplashPhotosApp/Utility/ApiKeys.swift` this file
25 | 8. Paste your accessKey there
26 |
27 |
28 | ## Screen recordings
29 |
30 |
31 |
32 |
33 |
34 |
35 | |
36 |
37 |
38 | |
39 |
40 |
41 |
42 |
43 | |
44 |
45 |
46 | |
47 |
48 |
49 |
50 |
51 | ## Known issue
52 |
53 | AsyncImage has an issue loading image
54 | https://developer.apple.com/forums/thread/682498
55 | if you don't want to see the error then
56 |
57 | Replace this 👇
58 |
59 | https://github.com/watery-desert/UnsplashPhotosApp/blob/c48a5149cf941d557b7600532d5057a1b58dda0d/UnsplashPhotosApp/Source/Views/HelperViews/PhotoTile.swift#L26
60 |
61 | ```swift
62 | Image(systemName: "exclamationmark.triangle")
63 | .padding()
64 | .font(.largeTitle)
65 | ```
66 | with this 👇
67 |
68 | ```swift
69 | EmptyView()
70 | ```
71 |
72 |
73 |
74 |
75 | ## Illustration Credit
76 |
77 | People vector created by iwat1929 - www.freepik.com
78 |
79 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp.xcodeproj/project.pbxproj:
--------------------------------------------------------------------------------
1 | // !$*UTF8*$!
2 | {
3 | archiveVersion = 1;
4 | classes = {
5 | };
6 | objectVersion = 55;
7 | objects = {
8 |
9 | /* Begin PBXBuildFile section */
10 | E23BCD2527A994360072C847 /* UnsplashPhotosAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD2427A994360072C847 /* UnsplashPhotosAppApp.swift */; };
11 | E23BCD2927A994370072C847 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E23BCD2827A994370072C847 /* Assets.xcassets */; };
12 | E23BCD2C27A994370072C847 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = E23BCD2B27A994370072C847 /* Preview Assets.xcassets */; };
13 | E23BCD4F27A994790072C847 /* TopicViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3427A994790072C847 /* TopicViewModel.swift */; };
14 | E23BCD5027A994790072C847 /* DownloadPhotoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3527A994790072C847 /* DownloadPhotoViewModel.swift */; };
15 | E23BCD5127A994790072C847 /* PhotoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3627A994790072C847 /* PhotoViewModel.swift */; };
16 | E23BCD5227A994790072C847 /* SearchPhotoViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3727A994790072C847 /* SearchPhotoViewModel.swift */; };
17 | E23BCD5327A994790072C847 /* ApiKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3827A994790072C847 /* ApiKeys.swift */; };
18 | E23BCD5427A994790072C847 /* Photo.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3A27A994790072C847 /* Photo.swift */; };
19 | E23BCD5527A994790072C847 /* QueryPhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3B27A994790072C847 /* QueryPhoto.swift */; };
20 | E23BCD5627A994790072C847 /* DownloadPhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3C27A994790072C847 /* DownloadPhoto.swift */; };
21 | E23BCD5727A994790072C847 /* Urls.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3D27A994790072C847 /* Urls.swift */; };
22 | E23BCD5827A994790072C847 /* Topic.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3E27A994790072C847 /* Topic.swift */; };
23 | E23BCD5927A994790072C847 /* Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD3F27A994790072C847 /* Extensions.swift */; };
24 | E23BCD5A27A994790072C847 /* ImageSaver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4027A994790072C847 /* ImageSaver.swift */; };
25 | E23BCD5B27A994790072C847 /* SearchPhotosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4327A994790072C847 /* SearchPhotosView.swift */; };
26 | E23BCD5C27A994790072C847 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4427A994790072C847 /* HomeView.swift */; };
27 | E23BCD5D27A994790072C847 /* PhotoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4627A994790072C847 /* PhotoView.swift */; };
28 | E23BCD5E27A994790072C847 /* PhotoTile.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4827A994790072C847 /* PhotoTile.swift */; };
29 | E23BCD5F27A994790072C847 /* StaggeredPhotosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4927A994790072C847 /* StaggeredPhotosView.swift */; };
30 | E23BCD6027A994790072C847 /* LoadingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4A27A994790072C847 /* LoadingView.swift */; };
31 | E23BCD6127A994790072C847 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4B27A994790072C847 /* ErrorView.swift */; };
32 | E23BCD6227A994790072C847 /* RecentPhotosView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4D27A994790072C847 /* RecentPhotosView.swift */; };
33 | E23BCD6327A994790072C847 /* TopicRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = E23BCD4E27A994790072C847 /* TopicRow.swift */; };
34 | E2B46C2B27AA0EB0006A747B /* APIService.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2B46C2A27AA0EB0006A747B /* APIService.swift */; };
35 | /* End PBXBuildFile section */
36 |
37 | /* Begin PBXFileReference section */
38 | E23BCD2127A994360072C847 /* UnsplashPhotosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = UnsplashPhotosApp.app; sourceTree = BUILT_PRODUCTS_DIR; };
39 | E23BCD2427A994360072C847 /* UnsplashPhotosAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsplashPhotosAppApp.swift; sourceTree = "
"; };
40 | E23BCD2827A994370072C847 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
41 | E23BCD2B27A994370072C847 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
42 | E23BCD3427A994790072C847 /* TopicViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopicViewModel.swift; sourceTree = ""; };
43 | E23BCD3527A994790072C847 /* DownloadPhotoViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPhotoViewModel.swift; sourceTree = ""; };
44 | E23BCD3627A994790072C847 /* PhotoViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoViewModel.swift; sourceTree = ""; };
45 | E23BCD3727A994790072C847 /* SearchPhotoViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPhotoViewModel.swift; sourceTree = ""; };
46 | E23BCD3827A994790072C847 /* ApiKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApiKeys.swift; sourceTree = ""; };
47 | E23BCD3A27A994790072C847 /* Photo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Photo.swift; sourceTree = ""; };
48 | E23BCD3B27A994790072C847 /* QueryPhoto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = QueryPhoto.swift; sourceTree = ""; };
49 | E23BCD3C27A994790072C847 /* DownloadPhoto.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DownloadPhoto.swift; sourceTree = ""; };
50 | E23BCD3D27A994790072C847 /* Urls.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Urls.swift; sourceTree = ""; };
51 | E23BCD3E27A994790072C847 /* Topic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Topic.swift; sourceTree = ""; };
52 | E23BCD3F27A994790072C847 /* Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = ""; };
53 | E23BCD4027A994790072C847 /* ImageSaver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageSaver.swift; sourceTree = ""; };
54 | E23BCD4327A994790072C847 /* SearchPhotosView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchPhotosView.swift; sourceTree = ""; };
55 | E23BCD4427A994790072C847 /* HomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = ""; };
56 | E23BCD4627A994790072C847 /* PhotoView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoView.swift; sourceTree = ""; };
57 | E23BCD4827A994790072C847 /* PhotoTile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTile.swift; sourceTree = ""; };
58 | E23BCD4927A994790072C847 /* StaggeredPhotosView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StaggeredPhotosView.swift; sourceTree = ""; };
59 | E23BCD4A27A994790072C847 /* LoadingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoadingView.swift; sourceTree = ""; };
60 | E23BCD4B27A994790072C847 /* ErrorView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; };
61 | E23BCD4D27A994790072C847 /* RecentPhotosView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RecentPhotosView.swift; sourceTree = ""; };
62 | E23BCD4E27A994790072C847 /* TopicRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopicRow.swift; sourceTree = ""; };
63 | E23BCD6527A996310072C847 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; };
64 | E23BCD6627A9999B0072C847 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; };
65 | E2B46C2A27AA0EB0006A747B /* APIService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIService.swift; sourceTree = ""; };
66 | /* End PBXFileReference section */
67 |
68 | /* Begin PBXFrameworksBuildPhase section */
69 | E23BCD1E27A994360072C847 /* Frameworks */ = {
70 | isa = PBXFrameworksBuildPhase;
71 | buildActionMask = 2147483647;
72 | files = (
73 | );
74 | runOnlyForDeploymentPostprocessing = 0;
75 | };
76 | /* End PBXFrameworksBuildPhase section */
77 |
78 | /* Begin PBXGroup section */
79 | E23BCD1827A994360072C847 = {
80 | isa = PBXGroup;
81 | children = (
82 | E23BCD6627A9999B0072C847 /* LICENSE */,
83 | E23BCD6527A996310072C847 /* README.md */,
84 | E23BCD2327A994360072C847 /* UnsplashPhotosApp */,
85 | E23BCD2227A994360072C847 /* Products */,
86 | );
87 | sourceTree = "";
88 | };
89 | E23BCD2227A994360072C847 /* Products */ = {
90 | isa = PBXGroup;
91 | children = (
92 | E23BCD2127A994360072C847 /* UnsplashPhotosApp.app */,
93 | );
94 | name = Products;
95 | sourceTree = "";
96 | };
97 | E23BCD2327A994360072C847 /* UnsplashPhotosApp */ = {
98 | isa = PBXGroup;
99 | children = (
100 | E23BCD3227A994790072C847 /* Source */,
101 | E23BCD2427A994360072C847 /* UnsplashPhotosAppApp.swift */,
102 | E23BCD2827A994370072C847 /* Assets.xcassets */,
103 | E23BCD2A27A994370072C847 /* Preview Content */,
104 | );
105 | path = UnsplashPhotosApp;
106 | sourceTree = "";
107 | };
108 | E23BCD2A27A994370072C847 /* Preview Content */ = {
109 | isa = PBXGroup;
110 | children = (
111 | E23BCD2B27A994370072C847 /* Preview Assets.xcassets */,
112 | );
113 | path = "Preview Content";
114 | sourceTree = "";
115 | };
116 | E23BCD3227A994790072C847 /* Source */ = {
117 | isa = PBXGroup;
118 | children = (
119 | E23BCD6727A9A7ED0072C847 /* Services */,
120 | E23BCD6427A995600072C847 /* Utility */,
121 | E23BCD3327A994790072C847 /* ViewModels */,
122 | E23BCD3927A994790072C847 /* Models */,
123 | E23BCD4127A994790072C847 /* Views */,
124 | );
125 | path = Source;
126 | sourceTree = "";
127 | };
128 | E23BCD3327A994790072C847 /* ViewModels */ = {
129 | isa = PBXGroup;
130 | children = (
131 | E23BCD3427A994790072C847 /* TopicViewModel.swift */,
132 | E23BCD3527A994790072C847 /* DownloadPhotoViewModel.swift */,
133 | E23BCD3627A994790072C847 /* PhotoViewModel.swift */,
134 | E23BCD3727A994790072C847 /* SearchPhotoViewModel.swift */,
135 | );
136 | path = ViewModels;
137 | sourceTree = "";
138 | };
139 | E23BCD3927A994790072C847 /* Models */ = {
140 | isa = PBXGroup;
141 | children = (
142 | E23BCD3A27A994790072C847 /* Photo.swift */,
143 | E23BCD3B27A994790072C847 /* QueryPhoto.swift */,
144 | E23BCD3C27A994790072C847 /* DownloadPhoto.swift */,
145 | E23BCD3D27A994790072C847 /* Urls.swift */,
146 | E23BCD3E27A994790072C847 /* Topic.swift */,
147 | );
148 | path = Models;
149 | sourceTree = "";
150 | };
151 | E23BCD4127A994790072C847 /* Views */ = {
152 | isa = PBXGroup;
153 | children = (
154 | E23BCD4227A994790072C847 /* SearchPhotosView */,
155 | E23BCD4427A994790072C847 /* HomeView.swift */,
156 | E23BCD4527A994790072C847 /* PhotoView */,
157 | E23BCD4727A994790072C847 /* HelperViews */,
158 | E23BCD4C27A994790072C847 /* RecentPhotosView */,
159 | );
160 | path = Views;
161 | sourceTree = "";
162 | };
163 | E23BCD4227A994790072C847 /* SearchPhotosView */ = {
164 | isa = PBXGroup;
165 | children = (
166 | E23BCD4327A994790072C847 /* SearchPhotosView.swift */,
167 | );
168 | path = SearchPhotosView;
169 | sourceTree = "";
170 | };
171 | E23BCD4527A994790072C847 /* PhotoView */ = {
172 | isa = PBXGroup;
173 | children = (
174 | E23BCD4627A994790072C847 /* PhotoView.swift */,
175 | );
176 | path = PhotoView;
177 | sourceTree = "";
178 | };
179 | E23BCD4727A994790072C847 /* HelperViews */ = {
180 | isa = PBXGroup;
181 | children = (
182 | E23BCD4827A994790072C847 /* PhotoTile.swift */,
183 | E23BCD4927A994790072C847 /* StaggeredPhotosView.swift */,
184 | E23BCD4A27A994790072C847 /* LoadingView.swift */,
185 | E23BCD4B27A994790072C847 /* ErrorView.swift */,
186 | );
187 | path = HelperViews;
188 | sourceTree = "";
189 | };
190 | E23BCD4C27A994790072C847 /* RecentPhotosView */ = {
191 | isa = PBXGroup;
192 | children = (
193 | E23BCD4D27A994790072C847 /* RecentPhotosView.swift */,
194 | E23BCD4E27A994790072C847 /* TopicRow.swift */,
195 | );
196 | path = RecentPhotosView;
197 | sourceTree = "";
198 | };
199 | E23BCD6427A995600072C847 /* Utility */ = {
200 | isa = PBXGroup;
201 | children = (
202 | E23BCD3F27A994790072C847 /* Extensions.swift */,
203 | E23BCD3827A994790072C847 /* ApiKeys.swift */,
204 | E23BCD4027A994790072C847 /* ImageSaver.swift */,
205 | );
206 | path = Utility;
207 | sourceTree = "";
208 | };
209 | E23BCD6727A9A7ED0072C847 /* Services */ = {
210 | isa = PBXGroup;
211 | children = (
212 | E2B46C2A27AA0EB0006A747B /* APIService.swift */,
213 | );
214 | path = Services;
215 | sourceTree = "";
216 | };
217 | /* End PBXGroup section */
218 |
219 | /* Begin PBXNativeTarget section */
220 | E23BCD2027A994360072C847 /* UnsplashPhotosApp */ = {
221 | isa = PBXNativeTarget;
222 | buildConfigurationList = E23BCD2F27A994370072C847 /* Build configuration list for PBXNativeTarget "UnsplashPhotosApp" */;
223 | buildPhases = (
224 | E23BCD1D27A994360072C847 /* Sources */,
225 | E23BCD1E27A994360072C847 /* Frameworks */,
226 | E23BCD1F27A994360072C847 /* Resources */,
227 | );
228 | buildRules = (
229 | );
230 | dependencies = (
231 | );
232 | name = UnsplashPhotosApp;
233 | productName = UnsplashPhotosApp;
234 | productReference = E23BCD2127A994360072C847 /* UnsplashPhotosApp.app */;
235 | productType = "com.apple.product-type.application";
236 | };
237 | /* End PBXNativeTarget section */
238 |
239 | /* Begin PBXProject section */
240 | E23BCD1927A994360072C847 /* Project object */ = {
241 | isa = PBXProject;
242 | attributes = {
243 | BuildIndependentTargetsInParallel = 1;
244 | LastSwiftUpdateCheck = 1320;
245 | LastUpgradeCheck = 1320;
246 | TargetAttributes = {
247 | E23BCD2027A994360072C847 = {
248 | CreatedOnToolsVersion = 13.2.1;
249 | };
250 | };
251 | };
252 | buildConfigurationList = E23BCD1C27A994360072C847 /* Build configuration list for PBXProject "UnsplashPhotosApp" */;
253 | compatibilityVersion = "Xcode 13.0";
254 | developmentRegion = en;
255 | hasScannedForEncodings = 0;
256 | knownRegions = (
257 | en,
258 | Base,
259 | );
260 | mainGroup = E23BCD1827A994360072C847;
261 | productRefGroup = E23BCD2227A994360072C847 /* Products */;
262 | projectDirPath = "";
263 | projectRoot = "";
264 | targets = (
265 | E23BCD2027A994360072C847 /* UnsplashPhotosApp */,
266 | );
267 | };
268 | /* End PBXProject section */
269 |
270 | /* Begin PBXResourcesBuildPhase section */
271 | E23BCD1F27A994360072C847 /* Resources */ = {
272 | isa = PBXResourcesBuildPhase;
273 | buildActionMask = 2147483647;
274 | files = (
275 | E23BCD2C27A994370072C847 /* Preview Assets.xcassets in Resources */,
276 | E23BCD2927A994370072C847 /* Assets.xcassets in Resources */,
277 | );
278 | runOnlyForDeploymentPostprocessing = 0;
279 | };
280 | /* End PBXResourcesBuildPhase section */
281 |
282 | /* Begin PBXSourcesBuildPhase section */
283 | E23BCD1D27A994360072C847 /* Sources */ = {
284 | isa = PBXSourcesBuildPhase;
285 | buildActionMask = 2147483647;
286 | files = (
287 | E23BCD5427A994790072C847 /* Photo.swift in Sources */,
288 | E23BCD5827A994790072C847 /* Topic.swift in Sources */,
289 | E23BCD5327A994790072C847 /* ApiKeys.swift in Sources */,
290 | E23BCD5D27A994790072C847 /* PhotoView.swift in Sources */,
291 | E23BCD6127A994790072C847 /* ErrorView.swift in Sources */,
292 | E23BCD5A27A994790072C847 /* ImageSaver.swift in Sources */,
293 | E23BCD5227A994790072C847 /* SearchPhotoViewModel.swift in Sources */,
294 | E23BCD5F27A994790072C847 /* StaggeredPhotosView.swift in Sources */,
295 | E23BCD5727A994790072C847 /* Urls.swift in Sources */,
296 | E23BCD5E27A994790072C847 /* PhotoTile.swift in Sources */,
297 | E23BCD5027A994790072C847 /* DownloadPhotoViewModel.swift in Sources */,
298 | E23BCD6327A994790072C847 /* TopicRow.swift in Sources */,
299 | E23BCD4F27A994790072C847 /* TopicViewModel.swift in Sources */,
300 | E2B46C2B27AA0EB0006A747B /* APIService.swift in Sources */,
301 | E23BCD5927A994790072C847 /* Extensions.swift in Sources */,
302 | E23BCD6027A994790072C847 /* LoadingView.swift in Sources */,
303 | E23BCD2527A994360072C847 /* UnsplashPhotosAppApp.swift in Sources */,
304 | E23BCD5127A994790072C847 /* PhotoViewModel.swift in Sources */,
305 | E23BCD5627A994790072C847 /* DownloadPhoto.swift in Sources */,
306 | E23BCD5C27A994790072C847 /* HomeView.swift in Sources */,
307 | E23BCD5B27A994790072C847 /* SearchPhotosView.swift in Sources */,
308 | E23BCD6227A994790072C847 /* RecentPhotosView.swift in Sources */,
309 | E23BCD5527A994790072C847 /* QueryPhoto.swift in Sources */,
310 | );
311 | runOnlyForDeploymentPostprocessing = 0;
312 | };
313 | /* End PBXSourcesBuildPhase section */
314 |
315 | /* Begin XCBuildConfiguration section */
316 | E23BCD2D27A994370072C847 /* Debug */ = {
317 | isa = XCBuildConfiguration;
318 | buildSettings = {
319 | ALWAYS_SEARCH_USER_PATHS = NO;
320 | CLANG_ANALYZER_NONNULL = YES;
321 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
322 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
323 | CLANG_CXX_LIBRARY = "libc++";
324 | CLANG_ENABLE_MODULES = YES;
325 | CLANG_ENABLE_OBJC_ARC = YES;
326 | CLANG_ENABLE_OBJC_WEAK = YES;
327 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
328 | CLANG_WARN_BOOL_CONVERSION = YES;
329 | CLANG_WARN_COMMA = YES;
330 | CLANG_WARN_CONSTANT_CONVERSION = YES;
331 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
332 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
333 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
334 | CLANG_WARN_EMPTY_BODY = YES;
335 | CLANG_WARN_ENUM_CONVERSION = YES;
336 | CLANG_WARN_INFINITE_RECURSION = YES;
337 | CLANG_WARN_INT_CONVERSION = YES;
338 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
339 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
340 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
341 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
342 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
343 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
344 | CLANG_WARN_STRICT_PROTOTYPES = YES;
345 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
346 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
347 | CLANG_WARN_UNREACHABLE_CODE = YES;
348 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
349 | COPY_PHASE_STRIP = NO;
350 | DEBUG_INFORMATION_FORMAT = dwarf;
351 | ENABLE_STRICT_OBJC_MSGSEND = YES;
352 | ENABLE_TESTABILITY = YES;
353 | GCC_C_LANGUAGE_STANDARD = gnu11;
354 | GCC_DYNAMIC_NO_PIC = NO;
355 | GCC_NO_COMMON_BLOCKS = YES;
356 | GCC_OPTIMIZATION_LEVEL = 0;
357 | GCC_PREPROCESSOR_DEFINITIONS = (
358 | "DEBUG=1",
359 | "$(inherited)",
360 | );
361 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
362 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
363 | GCC_WARN_UNDECLARED_SELECTOR = YES;
364 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
365 | GCC_WARN_UNUSED_FUNCTION = YES;
366 | GCC_WARN_UNUSED_VARIABLE = YES;
367 | IPHONEOS_DEPLOYMENT_TARGET = 15.2;
368 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
369 | MTL_FAST_MATH = YES;
370 | ONLY_ACTIVE_ARCH = YES;
371 | SDKROOT = iphoneos;
372 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
373 | SWIFT_OPTIMIZATION_LEVEL = "-Onone";
374 | };
375 | name = Debug;
376 | };
377 | E23BCD2E27A994370072C847 /* Release */ = {
378 | isa = XCBuildConfiguration;
379 | buildSettings = {
380 | ALWAYS_SEARCH_USER_PATHS = NO;
381 | CLANG_ANALYZER_NONNULL = YES;
382 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
383 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
384 | CLANG_CXX_LIBRARY = "libc++";
385 | CLANG_ENABLE_MODULES = YES;
386 | CLANG_ENABLE_OBJC_ARC = YES;
387 | CLANG_ENABLE_OBJC_WEAK = YES;
388 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
389 | CLANG_WARN_BOOL_CONVERSION = YES;
390 | CLANG_WARN_COMMA = YES;
391 | CLANG_WARN_CONSTANT_CONVERSION = YES;
392 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
393 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
394 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
395 | CLANG_WARN_EMPTY_BODY = YES;
396 | CLANG_WARN_ENUM_CONVERSION = YES;
397 | CLANG_WARN_INFINITE_RECURSION = YES;
398 | CLANG_WARN_INT_CONVERSION = YES;
399 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
400 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
401 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
402 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
403 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
404 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
405 | CLANG_WARN_STRICT_PROTOTYPES = YES;
406 | CLANG_WARN_SUSPICIOUS_MOVE = YES;
407 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
408 | CLANG_WARN_UNREACHABLE_CODE = YES;
409 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
410 | COPY_PHASE_STRIP = NO;
411 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
412 | ENABLE_NS_ASSERTIONS = NO;
413 | ENABLE_STRICT_OBJC_MSGSEND = YES;
414 | GCC_C_LANGUAGE_STANDARD = gnu11;
415 | GCC_NO_COMMON_BLOCKS = YES;
416 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
417 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
418 | GCC_WARN_UNDECLARED_SELECTOR = YES;
419 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
420 | GCC_WARN_UNUSED_FUNCTION = YES;
421 | GCC_WARN_UNUSED_VARIABLE = YES;
422 | IPHONEOS_DEPLOYMENT_TARGET = 15.2;
423 | MTL_ENABLE_DEBUG_INFO = NO;
424 | MTL_FAST_MATH = YES;
425 | SDKROOT = iphoneos;
426 | SWIFT_COMPILATION_MODE = wholemodule;
427 | SWIFT_OPTIMIZATION_LEVEL = "-O";
428 | VALIDATE_PRODUCT = YES;
429 | };
430 | name = Release;
431 | };
432 | E23BCD3027A994370072C847 /* Debug */ = {
433 | isa = XCBuildConfiguration;
434 | buildSettings = {
435 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
436 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
437 | CODE_SIGN_STYLE = Automatic;
438 | CURRENT_PROJECT_VERSION = 1;
439 | DEVELOPMENT_ASSET_PATHS = "\"UnsplashPhotosApp/Preview Content\"";
440 | ENABLE_PREVIEWS = YES;
441 | GENERATE_INFOPLIST_FILE = YES;
442 | INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Download photos";
443 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
444 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
445 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
446 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
447 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
448 | LD_RUNPATH_SEARCH_PATHS = (
449 | "$(inherited)",
450 | "@executable_path/Frameworks",
451 | );
452 | MARKETING_VERSION = 1.0;
453 | PRODUCT_BUNDLE_IDENTIFIER = com.waterydesert.UnsplashPhotosApp;
454 | PRODUCT_NAME = "$(TARGET_NAME)";
455 | SWIFT_EMIT_LOC_STRINGS = YES;
456 | SWIFT_VERSION = 5.0;
457 | TARGETED_DEVICE_FAMILY = "1,2";
458 | };
459 | name = Debug;
460 | };
461 | E23BCD3127A994370072C847 /* Release */ = {
462 | isa = XCBuildConfiguration;
463 | buildSettings = {
464 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
465 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
466 | CODE_SIGN_STYLE = Automatic;
467 | CURRENT_PROJECT_VERSION = 1;
468 | DEVELOPMENT_ASSET_PATHS = "\"UnsplashPhotosApp/Preview Content\"";
469 | ENABLE_PREVIEWS = YES;
470 | GENERATE_INFOPLIST_FILE = YES;
471 | INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Download photos";
472 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
473 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
474 | INFOPLIST_KEY_UILaunchScreen_Generation = YES;
475 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
476 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
477 | LD_RUNPATH_SEARCH_PATHS = (
478 | "$(inherited)",
479 | "@executable_path/Frameworks",
480 | );
481 | MARKETING_VERSION = 1.0;
482 | PRODUCT_BUNDLE_IDENTIFIER = com.waterydesert.UnsplashPhotosApp;
483 | PRODUCT_NAME = "$(TARGET_NAME)";
484 | SWIFT_EMIT_LOC_STRINGS = YES;
485 | SWIFT_VERSION = 5.0;
486 | TARGETED_DEVICE_FAMILY = "1,2";
487 | };
488 | name = Release;
489 | };
490 | /* End XCBuildConfiguration section */
491 |
492 | /* Begin XCConfigurationList section */
493 | E23BCD1C27A994360072C847 /* Build configuration list for PBXProject "UnsplashPhotosApp" */ = {
494 | isa = XCConfigurationList;
495 | buildConfigurations = (
496 | E23BCD2D27A994370072C847 /* Debug */,
497 | E23BCD2E27A994370072C847 /* Release */,
498 | );
499 | defaultConfigurationIsVisible = 0;
500 | defaultConfigurationName = Release;
501 | };
502 | E23BCD2F27A994370072C847 /* Build configuration list for PBXNativeTarget "UnsplashPhotosApp" */ = {
503 | isa = XCConfigurationList;
504 | buildConfigurations = (
505 | E23BCD3027A994370072C847 /* Debug */,
506 | E23BCD3127A994370072C847 /* Release */,
507 | );
508 | defaultConfigurationIsVisible = 0;
509 | defaultConfigurationName = Release;
510 | };
511 | /* End XCConfigurationList section */
512 | };
513 | rootObject = E23BCD1927A994360072C847 /* Project object */;
514 | }
515 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp.xcodeproj/xcuserdata/ahmed.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | UnsplashPhotosApp.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "iphone",
5 | "scale" : "2x",
6 | "size" : "20x20"
7 | },
8 | {
9 | "idiom" : "iphone",
10 | "scale" : "3x",
11 | "size" : "20x20"
12 | },
13 | {
14 | "idiom" : "iphone",
15 | "scale" : "2x",
16 | "size" : "29x29"
17 | },
18 | {
19 | "idiom" : "iphone",
20 | "scale" : "3x",
21 | "size" : "29x29"
22 | },
23 | {
24 | "idiom" : "iphone",
25 | "scale" : "2x",
26 | "size" : "40x40"
27 | },
28 | {
29 | "idiom" : "iphone",
30 | "scale" : "3x",
31 | "size" : "40x40"
32 | },
33 | {
34 | "idiom" : "iphone",
35 | "scale" : "2x",
36 | "size" : "60x60"
37 | },
38 | {
39 | "idiom" : "iphone",
40 | "scale" : "3x",
41 | "size" : "60x60"
42 | },
43 | {
44 | "idiom" : "ipad",
45 | "scale" : "1x",
46 | "size" : "20x20"
47 | },
48 | {
49 | "idiom" : "ipad",
50 | "scale" : "2x",
51 | "size" : "20x20"
52 | },
53 | {
54 | "idiom" : "ipad",
55 | "scale" : "1x",
56 | "size" : "29x29"
57 | },
58 | {
59 | "idiom" : "ipad",
60 | "scale" : "2x",
61 | "size" : "29x29"
62 | },
63 | {
64 | "idiom" : "ipad",
65 | "scale" : "1x",
66 | "size" : "40x40"
67 | },
68 | {
69 | "idiom" : "ipad",
70 | "scale" : "2x",
71 | "size" : "40x40"
72 | },
73 | {
74 | "idiom" : "ipad",
75 | "scale" : "1x",
76 | "size" : "76x76"
77 | },
78 | {
79 | "idiom" : "ipad",
80 | "scale" : "2x",
81 | "size" : "76x76"
82 | },
83 | {
84 | "idiom" : "ipad",
85 | "scale" : "2x",
86 | "size" : "83.5x83.5"
87 | },
88 | {
89 | "idiom" : "ios-marketing",
90 | "scale" : "1x",
91 | "size" : "1024x1024"
92 | }
93 | ],
94 | "info" : {
95 | "author" : "xcode",
96 | "version" : 1
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Assets.xcassets/Background.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "color" : {
5 | "color-space" : "srgb",
6 | "components" : {
7 | "alpha" : "1.000",
8 | "blue" : "0.957",
9 | "green" : "1.000",
10 | "red" : "1.000"
11 | }
12 | },
13 | "idiom" : "universal"
14 | }
15 | ],
16 | "info" : {
17 | "author" : "xcode",
18 | "version" : 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Assets.xcassets/Search.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "search.png",
5 | "idiom" : "universal",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "scale" : "2x"
11 | },
12 | {
13 | "idiom" : "universal",
14 | "scale" : "3x"
15 | }
16 | ],
17 | "info" : {
18 | "author" : "xcode",
19 | "version" : 1
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Assets.xcassets/Search.imageset/search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/watery-desert/UnsplashPhotosApp/ad19f11bfc85de7f086567c8438363fc4f256cc8/UnsplashPhotosApp/Assets.xcassets/Search.imageset/search.png
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Models/DownloadPhoto.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownloadPhoto.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 01/02/22.
6 | //
7 |
8 | import Foundation
9 |
10 | struct DownloadPhoto: Codable {
11 | let url: String
12 | }
13 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Models/Photo.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Photo.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 25/01/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 |
12 | struct Photo: Identifiable, Codable {
13 | let id: String
14 | let description: String?
15 | let urls: Urls
16 | let links: Links
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case id
20 | case description
21 | case urls
22 | case links
23 | }
24 | }
25 |
26 |
27 | struct Links: Codable {
28 | let downloadLocation: String
29 |
30 | enum CodingKeys: String, CodingKey {
31 | case downloadLocation = "download_location"
32 | }
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Models/QueryPhoto.swift:
--------------------------------------------------------------------------------
1 | //
2 | // QueryWallpaper.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 30/01/22.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | class QueryPhoto: Codable {
12 | let total: Int
13 | let totalPages: Int
14 | let photos: [Photo]
15 |
16 | enum CodingKeys: String, CodingKey {
17 | case total
18 | case totalPages = "total_pages"
19 | case photos = "results"
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Models/Topic.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Topic.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 29/01/22.
6 | //
7 |
8 |
9 |
10 |
11 | struct Topic: Identifiable, Codable {
12 |
13 | let id: String
14 | let title: String
15 | let totalPhotos: Int
16 | let coverPhoto: CoverPhoto
17 |
18 | enum CodingKeys: String, CodingKey {
19 | case id
20 | case title
21 | case totalPhotos = "total_photos"
22 | case coverPhoto = "cover_photo"
23 | }
24 |
25 | }
26 |
27 | // MARK: - CoverPhoto
28 | struct CoverPhoto: Codable {
29 | let urls: Urls
30 | }
31 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Models/Urls.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Urls.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 29/01/22.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 | struct Urls: Codable {
12 | let raw, full, regular, small, thumb: String
13 | }
14 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Services/APIService.swift:
--------------------------------------------------------------------------------
1 | //
2 | // APIService.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 02/02/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 | class APIService {
12 | let url: URL
13 |
14 | init(_ url :URL) {
15 | self.url = url
16 | }
17 |
18 | func getData() -> AnyPublisher {
19 | URLSession.shared.dataTaskPublisher(for: url)
20 | .receive(on: DispatchQueue.main)
21 | .map{$0.data}
22 | .decode(type: T.self, decoder: JSONDecoder())
23 | .eraseToAnyPublisher()
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Utility/ApiKeys.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ApiKeys.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 29/01/22.
6 | //
7 |
8 | import Foundation
9 |
10 | let accessKey: String = "" // ADD you own accessKey
11 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Utility/Extensions.swift:
--------------------------------------------------------------------------------
1 | //
2 | // Extensions.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 30/01/22.
6 | //
7 |
8 | import Foundation
9 |
10 |
11 |
12 | import SwiftUI
13 |
14 |
15 |
16 | extension UINavigationController {
17 | override open func viewDidLoad() {
18 | super.viewDidLoad()
19 | interactivePopGestureRecognizer?.delegate = nil
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Utility/ImageSaver.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ImageSaver.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 31/01/22.
6 | //
7 |
8 | import UIKit
9 |
10 | class ImageSaver: NSObject {
11 | func writeToPhotoAlbum(image: UIImage) {
12 | UIImageWriteToSavedPhotosAlbum(image, self, #selector(saveCompleted), nil)
13 | }
14 |
15 | @objc func saveCompleted(_ image: UIImage, didFinishSavingWithError error: Error?, contextInfo: UnsafeRawPointer) {
16 | print("Save finished!")
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/ViewModels/DownloadPhotoViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // DownloadPhotoViewModel.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 01/02/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 | import UIKit
11 |
12 | class DownloadPhotoViewModel: ObservableObject {
13 |
14 |
15 | private var cancellable = Set()
16 | @Published private(set) var state = State.idle
17 |
18 |
19 | enum State {
20 | case idle
21 | case loading
22 | case loaded
23 | case failed
24 | }
25 |
26 |
27 | func downloadPhoto(_ url: String) {
28 | state = .loading
29 | guard let url = URL(string: "\(url)?client_id=\(accessKey)")
30 | else {
31 | state = .failed
32 | return
33 | }
34 |
35 | let photoService = APIService(url)
36 |
37 | photoService.getData()
38 | .sink { [weak self] completion in
39 | switch completion {
40 | case .failure(let error):
41 | guard let self = self else {return}
42 | self.state = .failed
43 | print("Error is \(error)")
44 | case .finished: break
45 | }
46 | } receiveValue: { [weak self] downloaded in
47 |
48 | guard let self = self else { return }
49 | self.getData(downloaded.url) { data in
50 |
51 | if let uiImage = UIImage(data: data) {
52 | let imageSaver = ImageSaver()
53 | imageSaver.writeToPhotoAlbum(image: uiImage)
54 | self.state = .loaded
55 | } else {
56 | self.state = .failed
57 | }
58 | }
59 | }
60 | .store(in: &cancellable)
61 | }
62 |
63 |
64 | private func getData(_ url: String, receiveCallback: @escaping (Data) -> Void) {
65 | guard let url = URL(string: url) else {return}
66 | URLSession.shared.dataTaskPublisher(for: url)
67 | .receive(on: DispatchQueue.main)
68 | .map{$0.data}
69 | .sink { _ in
70 | } receiveValue: { data in
71 | receiveCallback(data)
72 | }
73 | .store(in: &cancellable)
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/ViewModels/PhotoViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoViewModel.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 26/01/22.
6 | //
7 | import Combine
8 | import Foundation
9 |
10 | class PhotoViewModel : ObservableObject {
11 |
12 | enum State {
13 | case loading
14 | case loaded([Photo])
15 | case failed(Error)
16 | }
17 |
18 |
19 | @Published private(set) var state = State.loading
20 | private var currentIndex = 1
21 | private var cancellable = Set()
22 | private var allPhotos: [Photo] = []
23 |
24 | init() {
25 | loadPhotos()
26 | }
27 |
28 | func loadPhotos() {
29 |
30 | guard let url = URL(string: "https://api.unsplash.com/photos/?client_id=\(accessKey)&page=\(currentIndex)&content_filter=high")
31 | else {
32 | state = .failed(URLError(URLError.badURL))
33 | return
34 | }
35 |
36 | let photosService = APIService<[Photo]>(url)
37 |
38 | photosService.getData()
39 | .delay(for: 0.8, scheduler: RunLoop.main)
40 | .sink { [weak self] completion in
41 | switch completion {
42 | case .failure(let error):
43 | guard let self = self else {return}
44 | self.state = .failed(error)
45 | case .finished: break
46 | }
47 | }
48 | receiveValue: { [weak self] receivedPhotos in
49 | guard let self = self else {return}
50 | self.allPhotos.append(contentsOf: receivedPhotos)
51 | self.state = .loaded(self.allPhotos)
52 | self.currentIndex = self.currentIndex + 1
53 | }
54 | .store(in: &cancellable)
55 | }
56 | }
57 |
58 |
59 |
60 |
61 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/ViewModels/SearchPhotoViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchPhotoViewModel.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 28/01/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 |
12 |
13 | class SearchPhotoViewModel : ObservableObject {
14 |
15 | enum State {
16 | case initial
17 | case loading
18 | case loaded([Photo])
19 | case failed(Error)
20 | }
21 |
22 |
23 | @Published private(set) var state = State.initial
24 | @Published private(set) var hasReachedEnd: Bool = false
25 | private var queryText: String = ""
26 | private var allPhotos: [Photo] = []
27 | private var currentIndex: Int = 1
28 | private var cancellable = Set()
29 |
30 | func searchPicture(_ query: String) {
31 |
32 | if query != queryText {
33 | state = .loading
34 | queryText = query
35 | allPhotos = []
36 | hasReachedEnd = false
37 | currentIndex = 1
38 | } else if hasReachedEnd {
39 | return
40 | }
41 |
42 | let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
43 |
44 | guard
45 | let encodedQuery = encodedQuery,
46 | let url = URL(string: "https://api.unsplash.com/search/photos/?client_id=\(accessKey)&page=\(currentIndex)&query=\(encodedQuery)")
47 | else {
48 | state = .failed(URLError(URLError.badURL))
49 | return
50 | }
51 | let photoService = APIService(url)
52 |
53 | photoService.getData()
54 | .sink { [weak self] completion in
55 | switch completion {
56 | case .failure(let error):
57 | guard let self = self else {return}
58 | self.state = .failed(error)
59 | print("Error is \(error)")
60 | case .finished: break
61 | }
62 | } receiveValue: { [weak self] queryPhotos in
63 | guard let self = self else {return}
64 | if queryPhotos.totalPages == 0 {
65 | self.allPhotos = []
66 | self.state = .loaded(self.allPhotos)
67 | } else if self.currentIndex <= queryPhotos.totalPages {
68 | self.allPhotos.append(contentsOf: queryPhotos.photos)
69 | self.state = .loaded(self.allPhotos)
70 | self.currentIndex = self.currentIndex + 1
71 | } else {
72 | self.hasReachedEnd = true
73 | }
74 | }
75 | .store(in: &cancellable)
76 | }
77 | }
78 |
79 |
80 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/ViewModels/TopicViewModel.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicViewModel.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 29/01/22.
6 | //
7 |
8 | import Foundation
9 | import Combine
10 |
11 |
12 | class TopicViewModel: ObservableObject {
13 |
14 | enum State {
15 | case loading
16 | case loaded([Topic])
17 | case failed(Error)
18 | }
19 |
20 | @Published private(set) var state = State.loading
21 | private var cancellable = Set()
22 |
23 | init() {
24 | loadTopics()
25 | }
26 |
27 |
28 | func loadTopics() {
29 | guard let url = URL(string: "https://api.unsplash.com/topics?client_id=\(accessKey)")
30 | else {
31 | state = .failed(URLError(URLError.badURL))
32 | return
33 | }
34 |
35 | let photosService = APIService<[Topic]>(url)
36 |
37 | photosService.getData()
38 | .sink { [weak self] completion in
39 | switch completion {
40 | case .failure(let error):
41 | guard let self = self else {return}
42 | self.state = .failed(error)
43 | print("Error is \(error)")
44 | case .finished: break
45 | }
46 | } receiveValue: { [weak self] data in
47 | guard let self = self else {return}
48 | self.state = .loaded(data)
49 | }
50 | .store(in: &cancellable)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Views/HelperViews/ErrorView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // ErrorView.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 27/01/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct ErrorView: View {
11 | let error: String
12 |
13 | var body: some View {
14 | VStack {
15 | Spacer()
16 | Text(error)
17 | Spacer()
18 | }
19 | }
20 | }
21 |
22 | struct ErrorView_Previews: PreviewProvider {
23 | static var previews: some View {
24 | ErrorView(error: "Error")
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Views/HelperViews/LoadingView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // LoadingView.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 27/01/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct LoadingView: View {
11 | var body: some View {
12 | VStack {
13 | Spacer()
14 | ProgressView()
15 | Spacer()
16 | }
17 | }
18 | }
19 |
20 | struct LoadingView_Previews: PreviewProvider {
21 | static var previews: some View {
22 | LoadingView()
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Views/HelperViews/PhotoTile.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PictureTile.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 29/01/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PhotoTile: View {
11 |
12 | let photo: Photo
13 | private let deviceWidth = UIScreen.main.bounds.width
14 | @State private var isActive = false
15 |
16 | var body: some View {
17 |
18 | AsyncImage(url: URL(string: photo.urls.small)) { image in
19 | switch image {
20 | case .empty:
21 | ProgressView()
22 | .frame(width: deviceWidth/2.0, height: 250)
23 | case .failure:
24 |
25 | // issue with AsyncImage --> https://developer.apple.com/forums/thread/682498
26 | Image(systemName: "exclamationmark.triangle")
27 | .padding()
28 | .font(.largeTitle)
29 | case .success(let image):
30 | NavigationLink {
31 | PhotoView(photo: photo)
32 | } label: {
33 | image
34 | .resizable()
35 | .scaledToFit()
36 | .clipShape(RoundedRectangle(cornerRadius: 16))
37 | }
38 | .buttonStyle(BouncyStyle())
39 | @unknown default:
40 | fatalError()
41 | }
42 | }
43 | }
44 | }
45 |
46 | struct PhotoTile_Previews: PreviewProvider {
47 | static var previews: some View {
48 | PhotoTile(photo:
49 |
50 | Photo(
51 | id: "1",
52 | // color: "#262626",
53 | // blurHash: "LzJPxUR+NIj@~AjZWWfQEls.s.az",
54 | description: "Flower in the sun",
55 | urls: Urls(
56 | raw: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1",
57 | full: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?crop=entropy&cs=srgb&fm=jpg&ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1&q=85",
58 | regular: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1&q=80&w=1080",
59 | small: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1&q=80&w=400",
60 | thumb: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1&q=80&w=200"
61 |
62 | ),
63 |
64 | links: Links(
65 |
66 | // selfLink: "https://api.unsplash.com/photos/rN1y-_EV8kE",
67 | // html: "https://unsplash.com/photos/rN1y-_EV8kE",
68 | // download: "https://unsplash.com/photos/rN1y-_EV8kE/download?ixid=MnwyOTQ1MjZ8MXwxfGFsbHwxfHx8fHx8Mnx8MTY0MzY5MjYxNg",
69 | downloadLocation: "https://api.unsplash.com/photos/rN1y-_EV8kE/download?ixid=MnwyOTQ1MjZ8MXwxfGFsbHwxfHx8fHx8Mnx8MTY0MzY5MjYxNg"
70 |
71 | )
72 | )
73 | )
74 | }
75 | }
76 |
77 |
78 |
79 | private struct BouncyStyle: ButtonStyle {
80 |
81 | func makeBody(configuration: Configuration) -> some View {
82 | configuration.label
83 | .frame(maxWidth: .infinity)
84 | .scaleEffect(configuration.isPressed ? 0.9 : 1)
85 | .animation(.easeOut(duration: 0.2), value: configuration.isPressed)
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Views/HelperViews/StaggeredPhotosView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // StaggeredPhotosView.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 01/02/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 |
11 | struct StaggeredPhotosView: View {
12 |
13 | let photos: [Photo]
14 | let onTileCallback: ((Photo) -> Void)?
15 |
16 |
17 | init(_ pictures: [Photo], onTileAppear: ((Photo) -> Void)? = nil) {
18 | self.photos = pictures
19 | self.onTileCallback = onTileAppear
20 | }
21 |
22 |
23 |
24 |
25 | private var splitArray: [[Photo]] {
26 | var result: [[Photo]] = []
27 |
28 | var list1: [Photo] = []
29 | var list2: [Photo] = []
30 |
31 | photos.forEach { photo in
32 | let index = photos.firstIndex {$0.id == photo.id }
33 |
34 | if let index = index {
35 | if index % 2 == 0 {
36 | list1.append(photo)
37 | } else {
38 | list2.append(photo)
39 | }
40 | }
41 | }
42 | result.append(list1)
43 | result.append(list2)
44 | return result
45 |
46 | }
47 |
48 | var body: some View {
49 |
50 | HStack(alignment: .top) {
51 |
52 |
53 | LazyVStack(spacing: 8) {
54 | ForEach(splitArray[0]) { photo in
55 | PhotoTile(photo: photo)
56 | .onAppear(perform: {onAppearClosure(photo)})
57 |
58 | }
59 | }
60 |
61 | LazyVStack(spacing: 8) {
62 | ForEach(splitArray[1]) { photo in
63 | PhotoTile(photo: photo)
64 | .onAppear(perform: {onAppearClosure(photo)})
65 |
66 | }
67 | }
68 | }
69 | }
70 |
71 | func onAppearClosure(_ photo: Photo) {
72 | guard let onTileCallback = onTileCallback else {
73 | return
74 | }
75 | onTileCallback(photo)
76 | }
77 |
78 | }
79 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Views/HomeView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // HomeView.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 25/01/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct HomeView: View {
11 |
12 |
13 |
14 | var body: some View {
15 | NavigationView {
16 | RecentPhotosView()
17 | .navigationTitle("Home")
18 | .toolbar {
19 |
20 | ToolbarItem(placement: ToolbarItemPlacement.navigationBarTrailing) {
21 | NavigationLink {
22 | SearchPhotosView()
23 | } label: {
24 | Image(systemName: "magnifyingglass")
25 | }
26 |
27 | }
28 | }
29 | .font(.system(size: 18))
30 | }
31 |
32 | }
33 | }
34 |
35 | struct HomeView_Previews: PreviewProvider {
36 | static var previews: some View {
37 | HomeView()
38 | }
39 | }
40 |
41 |
42 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Views/PhotoView/PhotoView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // PhotoView.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 30/01/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct PhotoView: View {
11 |
12 | @Environment(\.presentationMode) private var presentationMode: Binding
13 |
14 | @StateObject private var downloadPhotoViewModel: DownloadPhotoViewModel = DownloadPhotoViewModel()
15 |
16 | let photo: Photo
17 | var body: some View {
18 | AsyncImage(url: URL(string: photo.urls.full)) { imagePhase in
19 | switch imagePhase {
20 | case .empty:
21 | ZStack {
22 | Color.white
23 | .ignoresSafeArea()
24 | LoadingView()
25 | }
26 |
27 | case .success(let image):
28 | ZStack {
29 | Color.black
30 | .ignoresSafeArea()
31 | image
32 | .resizable()
33 | .aspectRatio( contentMode: .fit)
34 |
35 | VStack {
36 |
37 | HStack {
38 | Button {
39 | presentationMode.wrappedValue.dismiss()
40 | } label: {
41 | Image(systemName: "arrow.backward")
42 | .padding()
43 | }
44 |
45 | Text(photo.description ?? "NO DESCRIPTION")
46 | Spacer()
47 | }
48 | .font(.system(size: 18))
49 |
50 | Spacer()
51 | Button {
52 | print("\(photo.links.downloadLocation)")
53 | downloadPhotoViewModel.downloadPhoto(photo.links.downloadLocation)
54 | } label: {
55 | getIcon(state: downloadPhotoViewModel.state)
56 | }
57 | .font(.headline)
58 | .frame(maxWidth: 56, maxHeight: 56)
59 | .background(getColor(state: downloadPhotoViewModel.state))
60 | .cornerRadius(16)
61 | .padding()
62 | }
63 |
64 | .foregroundColor(.white)
65 |
66 | }
67 |
68 |
69 | case .failure(let error):
70 | Text("\(error.localizedDescription)")
71 | @unknown default:
72 | fatalError()
73 | }
74 | }
75 | .navigationBarHidden(true)
76 | }
77 |
78 | @ViewBuilder
79 | private func getIcon(state: DownloadPhotoViewModel.State) -> some View {
80 | switch state {
81 | case .idle:
82 | Image(systemName: "arrow.down")
83 | case .loading:
84 | ProgressView()
85 | case .failed:
86 | Image(systemName: "exclamationmark.circle")
87 | case .loaded:
88 | Image(systemName: "checkmark")
89 | }
90 | }
91 |
92 | private func getColor(state: DownloadPhotoViewModel.State) -> Color {
93 | switch state {
94 | case .idle:
95 | return Color.gray
96 | case .loading:
97 | return Color.blue
98 | case .failed:
99 | return Color.red
100 |
101 | case .loaded:
102 | return Color.green
103 | }
104 | }
105 | }
106 |
107 | struct PhotoView_Previews: PreviewProvider {
108 | static var previews: some View {
109 | PhotoView(photo: Photo(
110 | id: "1",
111 | // color: "#262626",
112 | // blurHash: "LzJPxUR+NIj@~AjZWWfQEls.s.az",
113 | description: "Flower in the sun",
114 | urls: Urls(
115 | raw: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1",
116 | full: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?crop=entropy&cs=srgb&fm=jpg&ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1&q=85",
117 | regular: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1&q=80&w=1080",
118 | small: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1&q=80&w=400",
119 | thumb: "https://images.unsplash.com/photo-1643472009925-b86e009e21a2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwyOTQ1MjZ8MHwxfGFsbHw0fHx8fHx8Mnx8MTY0MzU1ODQ4OA&ixlib=rb-1.2.1&q=80&w=200"
120 |
121 | ),
122 | links: Links(
123 |
124 | // selfLink: "https://api.unsplash.com/photos/rN1y-_EV8kE",
125 | // html: "https://unsplash.com/photos/rN1y-_EV8kE",
126 | // download: "https://unsplash.com/photos/rN1y-_EV8kE/download?ixid=MnwyOTQ1MjZ8MXwxfGFsbHwxfHx8fHx8Mnx8MTY0MzY5MjYxNg",
127 | downloadLocation: "https://api.unsplash.com/photos/rN1y-_EV8kE/download?ixid=MnwyOTQ1MjZ8MXwxfGFsbHwxfHx8fHx8Mnx8MTY0MzY5MjYxNg"
128 |
129 | )
130 | )
131 | )
132 | }
133 | }
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Views/RecentPhotosView/RecentPhotosView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // RecentPhotosView.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 25/01/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct RecentPhotosView: View {
11 |
12 | @StateObject private var photoViewModel: PhotoViewModel = PhotoViewModel()
13 |
14 |
15 | var body: some View {
16 |
17 | ZStack {
18 | Color("Background")
19 | .ignoresSafeArea()
20 |
21 | switch photoViewModel.state {
22 | case .loading:
23 | LoadingView()
24 | case .loaded(let photos):
25 |
26 | ScrollView {
27 | heading("Topics")
28 |
29 | TopicRow()
30 | heading("Recent Pictures")
31 | StaggeredPhotosView(photos) { photo in
32 |
33 | guard let lastPhoto = photos.last else {return }
34 | if lastPhoto.id == photo.id {
35 | photoViewModel.loadPhotos()
36 | }
37 | }
38 | .padding([.horizontal], 16)
39 | }
40 |
41 | case .failed(let error):
42 | ErrorView(error: "\(error.localizedDescription)")
43 | }
44 | }
45 |
46 | }
47 |
48 | private func heading(_ text: String) -> some View {
49 | Text(text)
50 | .frame(maxWidth: .infinity, alignment: .leading)
51 | .padding(16)
52 | .font(.system(size: 18, weight: .bold))
53 | }
54 |
55 | }
56 |
57 |
58 | struct RecentPhotosView_Previews: PreviewProvider {
59 | static var previews: some View {
60 | RecentPhotosView()
61 | }
62 | }
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Views/RecentPhotosView/TopicRow.swift:
--------------------------------------------------------------------------------
1 | //
2 | // TopicRow.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 29/01/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct TopicRow: View {
11 | @StateObject private var topicViewModel: TopicViewModel = TopicViewModel()
12 |
13 |
14 | var body: some View {
15 |
16 | switch topicViewModel.state {
17 |
18 | case .loading:
19 | Text("Loading")
20 | case .loaded(let topics):
21 | ScrollView(.horizontal, showsIndicators: false ) {
22 |
23 | HStack(spacing: 16) {
24 | ForEach(topics) { topic in
25 | ZStack {
26 | buildImageView("\(topic.coverPhoto.urls.thumb)")
27 | Text("\(topic.title)")
28 | .foregroundColor(.white)
29 | .font(.system(size: 20,weight: .bold))
30 | }
31 | .clipShape(RoundedRectangle(cornerRadius: 16))
32 | }
33 |
34 | }
35 | .padding(.horizontal, 16)
36 | }
37 | case .failed:
38 |
39 | Image(systemName: "exclamationmark.triangle")
40 |
41 | }
42 |
43 | }
44 |
45 | private func buildImageView(_ url: String) -> some View {
46 | AsyncImage(url: URL(string: url)) { imagePhase in
47 | switch imagePhase {
48 | case .empty:
49 | Color.gray
50 | .frame(height: 80)
51 | case .failure:
52 | Image(systemName: "exclamationmark.triangle")
53 | case .success(let image):
54 | image
55 | @unknown default:
56 | fatalError()
57 | }
58 | }
59 | .overlay(.black.opacity(0.2))
60 | .frame( maxHeight: 80)
61 | }
62 |
63 | }
64 |
65 | struct TopicRow_Previews: PreviewProvider {
66 | static var previews: some View {
67 | TopicRow()
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/Source/Views/SearchPhotosView/SearchPhotosView.swift:
--------------------------------------------------------------------------------
1 | //
2 | // SearchPhotosView.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 28/01/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | struct SearchPhotosView: View {
11 |
12 |
13 | @StateObject private var searchPhotoViewModel: SearchPhotoViewModel = SearchPhotoViewModel()
14 | @State private var textFieldText = ""
15 | @FocusState private var textFieldFocus: Bool
16 |
17 | var body: some View {
18 | ZStack {
19 | Color("Background")
20 | .ignoresSafeArea()
21 |
22 | VStack(spacing: 0.0) {
23 | TextField("Type anything", text: $textFieldText)
24 | .focused($textFieldFocus)
25 | .padding()
26 | .background(.white)
27 | .clipShape(RoundedRectangle(cornerRadius: 8))
28 | .shadow(color: .black.opacity(0.15), radius: 2.0, x: 1.0, y: 1.0)
29 | .padding([.horizontal , .top], 16)
30 |
31 |
32 |
33 | switch searchPhotoViewModel.state {
34 | case .initial:
35 | VStack {
36 | Spacer()
37 | Image("Search")
38 | .resizable()
39 | .scaledToFit()
40 | Spacer()
41 | }
42 |
43 |
44 | case .loading:
45 | LoadingView()
46 |
47 | case .loaded(let photos):
48 |
49 | if photos.isEmpty {
50 | VStack{
51 | Spacer()
52 | Text("😞 Sad nothing found")
53 | Spacer()
54 | }
55 | } else {
56 | ScrollView {
57 | StaggeredPhotosView(photos) { photo in
58 |
59 | guard let lastPhoto = photos.last else {return }
60 | if lastPhoto.id == photo.id {
61 | searchPhotoViewModel.searchPicture(textFieldText)
62 | }
63 | }
64 | .padding([.horizontal, .top], 16)
65 | }
66 | }
67 |
68 |
69 |
70 | case .failed(let error):
71 | ErrorView(error: "\(error.localizedDescription)")
72 | }
73 | }
74 | .ignoresSafeArea(.keyboard)
75 | }
76 | .navigationTitle("Search")
77 | .navigationBarTitleDisplayMode(.inline)
78 | .toolbar {
79 | ToolbarItem(placement: ToolbarItemPlacement.keyboard) {
80 | Button("Done") {
81 | textFieldFocus = false
82 | searchPhotoViewModel.searchPicture(textFieldText)
83 | }
84 |
85 | }
86 | }
87 |
88 | }
89 | }
90 |
91 | struct SearchPhotosView_Previews: PreviewProvider {
92 | static var previews: some View {
93 | SearchPhotosView()
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/UnsplashPhotosApp/UnsplashPhotosAppApp.swift:
--------------------------------------------------------------------------------
1 | //
2 | // UnsplashPhotosAppApp.swift
3 | // UnsplashPhotosApp
4 | //
5 | // Created by Ahmed on 01/02/22.
6 | //
7 |
8 | import SwiftUI
9 |
10 | @main
11 | struct UnsplashPhotosAppApp: App {
12 | var body: some Scene {
13 | WindowGroup {
14 | HomeView()
15 | }
16 | }
17 | }
18 |
--------------------------------------------------------------------------------