├── .gitignore ├── PinterestCompositionalLayout.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm │ │ └── Package.resolved └── xcuserdata │ └── v.chistiakov.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── PinterestCompositionalLayout ├── App │ ├── AppDelegate.swift │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj │ │ └── LaunchScreen.storyboard │ ├── Info.plist │ └── SceneDelegate.swift ├── BlurHashDecode │ └── BlurHashDecode.swift └── Pinterest │ ├── CustomCompositionalLayout.swift │ ├── Models │ ├── Config.swift │ ├── PictureModel.swift │ └── Ratioable.swift │ ├── NetworkLayer │ ├── ImagesEndpoint.swift │ └── ImagesNetworkService.swift │ ├── PinterestLayoutSection.swift │ ├── PinterestViewController.swift │ ├── PinterestViewModel.swift │ └── Views │ ├── HeaderView.swift │ └── PictureCell.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## User settings 6 | xcuserdata/ 7 | 8 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 9 | *.xcscmblueprint 10 | *.xccheckout 11 | 12 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 13 | build/ 14 | DerivedData/ 15 | *.moved-aside 16 | *.pbxuser 17 | !default.pbxuser 18 | *.mode1v3 19 | !default.mode1v3 20 | *.mode2v3 21 | !default.mode2v3 22 | *.perspectivev3 23 | !default.perspectivev3 24 | 25 | ## Obj-C/Swift specific 26 | *.hmap 27 | 28 | ## App packaging 29 | *.ipa 30 | *.dSYM.zip 31 | *.dSYM 32 | 33 | ## Playgrounds 34 | timeline.xctimeline 35 | playground.xcworkspace 36 | 37 | # Swift Package Manager 38 | # 39 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 40 | # Packages/ 41 | # Package.pins 42 | # Package.resolved 43 | # *.xcodeproj 44 | # 45 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 46 | # hence it is not needed unless you have added a package configuration file to your project 47 | # .swiftpm 48 | 49 | .build/ 50 | 51 | # CocoaPods 52 | # 53 | # We recommend against adding the Pods directory to your .gitignore. However 54 | # you should judge for yourself, the pros and cons are mentioned at: 55 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 56 | # 57 | # Pods/ 58 | # 59 | # Add this line if you want to avoid checking in source code from the Xcode workspace 60 | # *.xcworkspace 61 | 62 | # Carthage 63 | # 64 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 65 | # Carthage/Checkouts 66 | 67 | Carthage/Build/ 68 | 69 | # Accio dependency management 70 | Dependencies/ 71 | .accio/ 72 | 73 | # fastlane 74 | # 75 | # It is recommended to not store the screenshots in the git repo. 76 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 77 | # For more information about the recommended setup visit: 78 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 79 | 80 | fastlane/report.xml 81 | fastlane/Preview.html 82 | fastlane/screenshots/**/*.png 83 | fastlane/test_output 84 | 85 | # Code Injection 86 | # 87 | # After new code Injection tools there's a generated folder /iOSInjectionProject 88 | # https://github.com/johnno1962/injectionforxcode 89 | 90 | iOSInjectionProject/ -------------------------------------------------------------------------------- /PinterestCompositionalLayout.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | AE15BA89298FDEA4000D2C92 /* EasyNetwork in Frameworks */ = {isa = PBXBuildFile; productRef = AE15BA88298FDEA4000D2C92 /* EasyNetwork */; }; 11 | AE15BA8C29903782000D2C92 /* BlurHashDecode.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE15BA8B29903782000D2C92 /* BlurHashDecode.swift */; }; 12 | AE1C7DDC298AEAFD00362448 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7DDB298AEAFD00362448 /* AppDelegate.swift */; }; 13 | AE1C7DDE298AEAFD00362448 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7DDD298AEAFD00362448 /* SceneDelegate.swift */; }; 14 | AE1C7DE0298AEAFD00362448 /* PinterestViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7DDF298AEAFD00362448 /* PinterestViewController.swift */; }; 15 | AE1C7DE5298AEAFD00362448 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AE1C7DE4298AEAFD00362448 /* Assets.xcassets */; }; 16 | AE1C7DE8298AEAFD00362448 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = AE1C7DE6298AEAFD00362448 /* LaunchScreen.storyboard */; }; 17 | AE1C7DF0298AF15000362448 /* PinterestLayoutSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7DEF298AF15000362448 /* PinterestLayoutSection.swift */; }; 18 | AE1C7DF2298AF8FC00362448 /* CustomCompositionalLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7DF1298AF8FC00362448 /* CustomCompositionalLayout.swift */; }; 19 | AE1C7DF5298AFCB200362448 /* PictureModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7DF4298AFCB200362448 /* PictureModel.swift */; }; 20 | AE1C7DF8298BCFC200362448 /* PictureCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7DF7298BCFC200362448 /* PictureCell.swift */; }; 21 | AE1C7DFB298C4F3B00362448 /* SnapKit in Frameworks */ = {isa = PBXBuildFile; productRef = AE1C7DFA298C4F3B00362448 /* SnapKit */; }; 22 | AE1C7E03298FC0AA00362448 /* Ratioable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7E02298FC0AA00362448 /* Ratioable.swift */; }; 23 | AE1C7E05298FC25700362448 /* Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7E04298FC25700362448 /* Config.swift */; }; 24 | AE1C7E08298FC40300362448 /* ImagesNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7E07298FC40300362448 /* ImagesNetworkService.swift */; }; 25 | AE1C7E0D298FC58700362448 /* ImagesEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7E0C298FC58700362448 /* ImagesEndpoint.swift */; }; 26 | AE1C7E10298FC8C000362448 /* PinterestViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE1C7E0F298FC8BF00362448 /* PinterestViewModel.swift */; }; 27 | AE888C2329984CE300A02A36 /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE888C2229984CE300A02A36 /* HeaderView.swift */; }; 28 | /* End PBXBuildFile section */ 29 | 30 | /* Begin PBXFileReference section */ 31 | AE15BA8B29903782000D2C92 /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = ""; }; 32 | AE1C7DD8298AEAFD00362448 /* PinterestCompositionalLayout.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = PinterestCompositionalLayout.app; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | AE1C7DDB298AEAFD00362448 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 34 | AE1C7DDD298AEAFD00362448 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 35 | AE1C7DDF298AEAFD00362448 /* PinterestViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinterestViewController.swift; sourceTree = ""; }; 36 | AE1C7DE4298AEAFD00362448 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 37 | AE1C7DE7298AEAFD00362448 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 38 | AE1C7DE9298AEAFD00362448 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 39 | AE1C7DEF298AF15000362448 /* PinterestLayoutSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinterestLayoutSection.swift; sourceTree = ""; }; 40 | AE1C7DF1298AF8FC00362448 /* CustomCompositionalLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomCompositionalLayout.swift; sourceTree = ""; }; 41 | AE1C7DF4298AFCB200362448 /* PictureModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureModel.swift; sourceTree = ""; }; 42 | AE1C7DF7298BCFC200362448 /* PictureCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PictureCell.swift; sourceTree = ""; }; 43 | AE1C7E02298FC0AA00362448 /* Ratioable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ratioable.swift; sourceTree = ""; }; 44 | AE1C7E04298FC25700362448 /* Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Config.swift; sourceTree = ""; }; 45 | AE1C7E07298FC40300362448 /* ImagesNetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesNetworkService.swift; sourceTree = ""; }; 46 | AE1C7E0C298FC58700362448 /* ImagesEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesEndpoint.swift; sourceTree = ""; }; 47 | AE1C7E0F298FC8BF00362448 /* PinterestViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinterestViewModel.swift; sourceTree = ""; }; 48 | AE1C7E14298FD19E00362448 /* EasyNetwork */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = EasyNetwork; path = ../EasyNetwork; sourceTree = ""; }; 49 | AE888C2229984CE300A02A36 /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = ""; }; 50 | /* End PBXFileReference section */ 51 | 52 | /* Begin PBXFrameworksBuildPhase section */ 53 | AE1C7DD5298AEAFD00362448 /* Frameworks */ = { 54 | isa = PBXFrameworksBuildPhase; 55 | buildActionMask = 2147483647; 56 | files = ( 57 | AE1C7DFB298C4F3B00362448 /* SnapKit in Frameworks */, 58 | AE15BA89298FDEA4000D2C92 /* EasyNetwork in Frameworks */, 59 | ); 60 | runOnlyForDeploymentPostprocessing = 0; 61 | }; 62 | /* End PBXFrameworksBuildPhase section */ 63 | 64 | /* Begin PBXGroup section */ 65 | AE15BA8A29903771000D2C92 /* BlurHashDecode */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | AE15BA8B29903782000D2C92 /* BlurHashDecode.swift */, 69 | ); 70 | path = BlurHashDecode; 71 | sourceTree = ""; 72 | }; 73 | AE15BA8D29903A86000D2C92 /* App */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | AE1C7DDB298AEAFD00362448 /* AppDelegate.swift */, 77 | AE1C7DDD298AEAFD00362448 /* SceneDelegate.swift */, 78 | AE1C7DE4298AEAFD00362448 /* Assets.xcassets */, 79 | AE1C7DE6298AEAFD00362448 /* LaunchScreen.storyboard */, 80 | AE1C7DE9298AEAFD00362448 /* Info.plist */, 81 | ); 82 | path = App; 83 | sourceTree = ""; 84 | }; 85 | AE1C7DCF298AEAFD00362448 = { 86 | isa = PBXGroup; 87 | children = ( 88 | AE1C7E13298FD19E00362448 /* Packages */, 89 | AE1C7DDA298AEAFD00362448 /* PinterestCompositionalLayout */, 90 | AE1C7DD9298AEAFD00362448 /* Products */, 91 | ); 92 | sourceTree = ""; 93 | }; 94 | AE1C7DD9298AEAFD00362448 /* Products */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | AE1C7DD8298AEAFD00362448 /* PinterestCompositionalLayout.app */, 98 | ); 99 | name = Products; 100 | sourceTree = ""; 101 | }; 102 | AE1C7DDA298AEAFD00362448 /* PinterestCompositionalLayout */ = { 103 | isa = PBXGroup; 104 | children = ( 105 | AE15BA8D29903A86000D2C92 /* App */, 106 | AE15BA8A29903771000D2C92 /* BlurHashDecode */, 107 | AE1C7E0E298FC8A500362448 /* Pinterest */, 108 | ); 109 | path = PinterestCompositionalLayout; 110 | sourceTree = ""; 111 | }; 112 | AE1C7DF3298AFCA100362448 /* Models */ = { 113 | isa = PBXGroup; 114 | children = ( 115 | AE1C7E04298FC25700362448 /* Config.swift */, 116 | AE1C7E02298FC0AA00362448 /* Ratioable.swift */, 117 | AE1C7DF4298AFCB200362448 /* PictureModel.swift */, 118 | ); 119 | path = Models; 120 | sourceTree = ""; 121 | }; 122 | AE1C7DF6298BCF9D00362448 /* Views */ = { 123 | isa = PBXGroup; 124 | children = ( 125 | AE1C7DF7298BCFC200362448 /* PictureCell.swift */, 126 | AE888C2229984CE300A02A36 /* HeaderView.swift */, 127 | ); 128 | path = Views; 129 | sourceTree = ""; 130 | }; 131 | AE1C7E06298FC3EE00362448 /* NetworkLayer */ = { 132 | isa = PBXGroup; 133 | children = ( 134 | AE1C7E07298FC40300362448 /* ImagesNetworkService.swift */, 135 | AE1C7E0C298FC58700362448 /* ImagesEndpoint.swift */, 136 | ); 137 | path = NetworkLayer; 138 | sourceTree = ""; 139 | }; 140 | AE1C7E0E298FC8A500362448 /* Pinterest */ = { 141 | isa = PBXGroup; 142 | children = ( 143 | AE1C7E06298FC3EE00362448 /* NetworkLayer */, 144 | AE1C7DF6298BCF9D00362448 /* Views */, 145 | AE1C7DF3298AFCA100362448 /* Models */, 146 | AE1C7DDF298AEAFD00362448 /* PinterestViewController.swift */, 147 | AE1C7DEF298AF15000362448 /* PinterestLayoutSection.swift */, 148 | AE1C7DF1298AF8FC00362448 /* CustomCompositionalLayout.swift */, 149 | AE1C7E0F298FC8BF00362448 /* PinterestViewModel.swift */, 150 | ); 151 | path = Pinterest; 152 | sourceTree = ""; 153 | }; 154 | AE1C7E13298FD19E00362448 /* Packages */ = { 155 | isa = PBXGroup; 156 | children = ( 157 | AE1C7E14298FD19E00362448 /* EasyNetwork */, 158 | ); 159 | name = Packages; 160 | sourceTree = ""; 161 | }; 162 | /* End PBXGroup section */ 163 | 164 | /* Begin PBXNativeTarget section */ 165 | AE1C7DD7298AEAFD00362448 /* PinterestCompositionalLayout */ = { 166 | isa = PBXNativeTarget; 167 | buildConfigurationList = AE1C7DEC298AEAFD00362448 /* Build configuration list for PBXNativeTarget "PinterestCompositionalLayout" */; 168 | buildPhases = ( 169 | AE1C7DD4298AEAFD00362448 /* Sources */, 170 | AE1C7DD5298AEAFD00362448 /* Frameworks */, 171 | AE1C7DD6298AEAFD00362448 /* Resources */, 172 | ); 173 | buildRules = ( 174 | ); 175 | dependencies = ( 176 | ); 177 | name = PinterestCompositionalLayout; 178 | packageProductDependencies = ( 179 | AE1C7DFA298C4F3B00362448 /* SnapKit */, 180 | AE15BA88298FDEA4000D2C92 /* EasyNetwork */, 181 | ); 182 | productName = PinterestCompositionalLayout; 183 | productReference = AE1C7DD8298AEAFD00362448 /* PinterestCompositionalLayout.app */; 184 | productType = "com.apple.product-type.application"; 185 | }; 186 | /* End PBXNativeTarget section */ 187 | 188 | /* Begin PBXProject section */ 189 | AE1C7DD0298AEAFD00362448 /* Project object */ = { 190 | isa = PBXProject; 191 | attributes = { 192 | BuildIndependentTargetsInParallel = 1; 193 | LastSwiftUpdateCheck = 1400; 194 | LastUpgradeCheck = 1400; 195 | TargetAttributes = { 196 | AE1C7DD7298AEAFD00362448 = { 197 | CreatedOnToolsVersion = 14.0.1; 198 | }; 199 | }; 200 | }; 201 | buildConfigurationList = AE1C7DD3298AEAFD00362448 /* Build configuration list for PBXProject "PinterestCompositionalLayout" */; 202 | compatibilityVersion = "Xcode 14.0"; 203 | developmentRegion = en; 204 | hasScannedForEncodings = 0; 205 | knownRegions = ( 206 | en, 207 | Base, 208 | ); 209 | mainGroup = AE1C7DCF298AEAFD00362448; 210 | packageReferences = ( 211 | AE1C7DF9298C4F3B00362448 /* XCRemoteSwiftPackageReference "SnapKit" */, 212 | AE15BA87298FDEA4000D2C92 /* XCRemoteSwiftPackageReference "EasyNetwork" */, 213 | ); 214 | productRefGroup = AE1C7DD9298AEAFD00362448 /* Products */; 215 | projectDirPath = ""; 216 | projectRoot = ""; 217 | targets = ( 218 | AE1C7DD7298AEAFD00362448 /* PinterestCompositionalLayout */, 219 | ); 220 | }; 221 | /* End PBXProject section */ 222 | 223 | /* Begin PBXResourcesBuildPhase section */ 224 | AE1C7DD6298AEAFD00362448 /* Resources */ = { 225 | isa = PBXResourcesBuildPhase; 226 | buildActionMask = 2147483647; 227 | files = ( 228 | AE1C7DE8298AEAFD00362448 /* LaunchScreen.storyboard in Resources */, 229 | AE1C7DE5298AEAFD00362448 /* Assets.xcassets in Resources */, 230 | ); 231 | runOnlyForDeploymentPostprocessing = 0; 232 | }; 233 | /* End PBXResourcesBuildPhase section */ 234 | 235 | /* Begin PBXSourcesBuildPhase section */ 236 | AE1C7DD4298AEAFD00362448 /* Sources */ = { 237 | isa = PBXSourcesBuildPhase; 238 | buildActionMask = 2147483647; 239 | files = ( 240 | AE1C7DF5298AFCB200362448 /* PictureModel.swift in Sources */, 241 | AE15BA8C29903782000D2C92 /* BlurHashDecode.swift in Sources */, 242 | AE1C7DF0298AF15000362448 /* PinterestLayoutSection.swift in Sources */, 243 | AE1C7E05298FC25700362448 /* Config.swift in Sources */, 244 | AE1C7DE0298AEAFD00362448 /* PinterestViewController.swift in Sources */, 245 | AE1C7DDC298AEAFD00362448 /* AppDelegate.swift in Sources */, 246 | AE1C7E10298FC8C000362448 /* PinterestViewModel.swift in Sources */, 247 | AE1C7DDE298AEAFD00362448 /* SceneDelegate.swift in Sources */, 248 | AE1C7E08298FC40300362448 /* ImagesNetworkService.swift in Sources */, 249 | AE1C7E03298FC0AA00362448 /* Ratioable.swift in Sources */, 250 | AE1C7E0D298FC58700362448 /* ImagesEndpoint.swift in Sources */, 251 | AE1C7DF8298BCFC200362448 /* PictureCell.swift in Sources */, 252 | AE1C7DF2298AF8FC00362448 /* CustomCompositionalLayout.swift in Sources */, 253 | AE888C2329984CE300A02A36 /* HeaderView.swift in Sources */, 254 | ); 255 | runOnlyForDeploymentPostprocessing = 0; 256 | }; 257 | /* End PBXSourcesBuildPhase section */ 258 | 259 | /* Begin PBXVariantGroup section */ 260 | AE1C7DE6298AEAFD00362448 /* LaunchScreen.storyboard */ = { 261 | isa = PBXVariantGroup; 262 | children = ( 263 | AE1C7DE7298AEAFD00362448 /* Base */, 264 | ); 265 | name = LaunchScreen.storyboard; 266 | sourceTree = ""; 267 | }; 268 | /* End PBXVariantGroup section */ 269 | 270 | /* Begin XCBuildConfiguration section */ 271 | AE1C7DEA298AEAFD00362448 /* Debug */ = { 272 | isa = XCBuildConfiguration; 273 | buildSettings = { 274 | ALWAYS_SEARCH_USER_PATHS = NO; 275 | CLANG_ANALYZER_NONNULL = YES; 276 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 277 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 278 | CLANG_ENABLE_MODULES = YES; 279 | CLANG_ENABLE_OBJC_ARC = YES; 280 | CLANG_ENABLE_OBJC_WEAK = YES; 281 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 282 | CLANG_WARN_BOOL_CONVERSION = YES; 283 | CLANG_WARN_COMMA = YES; 284 | CLANG_WARN_CONSTANT_CONVERSION = YES; 285 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 286 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 287 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 288 | CLANG_WARN_EMPTY_BODY = YES; 289 | CLANG_WARN_ENUM_CONVERSION = YES; 290 | CLANG_WARN_INFINITE_RECURSION = YES; 291 | CLANG_WARN_INT_CONVERSION = YES; 292 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 293 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 294 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 295 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 296 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 297 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 298 | CLANG_WARN_STRICT_PROTOTYPES = YES; 299 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 300 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 301 | CLANG_WARN_UNREACHABLE_CODE = YES; 302 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 303 | COPY_PHASE_STRIP = NO; 304 | DEBUG_INFORMATION_FORMAT = dwarf; 305 | ENABLE_STRICT_OBJC_MSGSEND = YES; 306 | ENABLE_TESTABILITY = YES; 307 | GCC_C_LANGUAGE_STANDARD = gnu11; 308 | GCC_DYNAMIC_NO_PIC = NO; 309 | GCC_NO_COMMON_BLOCKS = YES; 310 | GCC_OPTIMIZATION_LEVEL = 0; 311 | GCC_PREPROCESSOR_DEFINITIONS = ( 312 | "DEBUG=1", 313 | "$(inherited)", 314 | ); 315 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 316 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 317 | GCC_WARN_UNDECLARED_SELECTOR = YES; 318 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 319 | GCC_WARN_UNUSED_FUNCTION = YES; 320 | GCC_WARN_UNUSED_VARIABLE = YES; 321 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 322 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 323 | MTL_FAST_MATH = YES; 324 | ONLY_ACTIVE_ARCH = YES; 325 | SDKROOT = iphoneos; 326 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 327 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 328 | }; 329 | name = Debug; 330 | }; 331 | AE1C7DEB298AEAFD00362448 /* Release */ = { 332 | isa = XCBuildConfiguration; 333 | buildSettings = { 334 | ALWAYS_SEARCH_USER_PATHS = NO; 335 | CLANG_ANALYZER_NONNULL = YES; 336 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 337 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 338 | CLANG_ENABLE_MODULES = YES; 339 | CLANG_ENABLE_OBJC_ARC = YES; 340 | CLANG_ENABLE_OBJC_WEAK = YES; 341 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 342 | CLANG_WARN_BOOL_CONVERSION = YES; 343 | CLANG_WARN_COMMA = YES; 344 | CLANG_WARN_CONSTANT_CONVERSION = YES; 345 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 346 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 347 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 348 | CLANG_WARN_EMPTY_BODY = YES; 349 | CLANG_WARN_ENUM_CONVERSION = YES; 350 | CLANG_WARN_INFINITE_RECURSION = YES; 351 | CLANG_WARN_INT_CONVERSION = YES; 352 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 353 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 354 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 355 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 356 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 357 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 358 | CLANG_WARN_STRICT_PROTOTYPES = YES; 359 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 360 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 361 | CLANG_WARN_UNREACHABLE_CODE = YES; 362 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 363 | COPY_PHASE_STRIP = NO; 364 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 365 | ENABLE_NS_ASSERTIONS = NO; 366 | ENABLE_STRICT_OBJC_MSGSEND = YES; 367 | GCC_C_LANGUAGE_STANDARD = gnu11; 368 | GCC_NO_COMMON_BLOCKS = YES; 369 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 370 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 371 | GCC_WARN_UNDECLARED_SELECTOR = YES; 372 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 373 | GCC_WARN_UNUSED_FUNCTION = YES; 374 | GCC_WARN_UNUSED_VARIABLE = YES; 375 | IPHONEOS_DEPLOYMENT_TARGET = 16.0; 376 | MTL_ENABLE_DEBUG_INFO = NO; 377 | MTL_FAST_MATH = YES; 378 | SDKROOT = iphoneos; 379 | SWIFT_COMPILATION_MODE = wholemodule; 380 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 381 | VALIDATE_PRODUCT = YES; 382 | }; 383 | name = Release; 384 | }; 385 | AE1C7DED298AEAFD00362448 /* Debug */ = { 386 | isa = XCBuildConfiguration; 387 | buildSettings = { 388 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 389 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 390 | CODE_SIGN_STYLE = Automatic; 391 | CURRENT_PROJECT_VERSION = 1; 392 | DEVELOPMENT_TEAM = X2N73UHTAA; 393 | GENERATE_INFOPLIST_FILE = YES; 394 | INFOPLIST_FILE = PinterestCompositionalLayout/App/Info.plist; 395 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 396 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 397 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 398 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 399 | LD_RUNPATH_SEARCH_PATHS = ( 400 | "$(inherited)", 401 | "@executable_path/Frameworks", 402 | ); 403 | MARKETING_VERSION = 1.0; 404 | PRODUCT_BUNDLE_IDENTIFIER = com.swift.app.PinterestCompositionalLayout; 405 | PRODUCT_NAME = "$(TARGET_NAME)"; 406 | SWIFT_EMIT_LOC_STRINGS = YES; 407 | SWIFT_VERSION = 5.0; 408 | TARGETED_DEVICE_FAMILY = "1,2"; 409 | }; 410 | name = Debug; 411 | }; 412 | AE1C7DEE298AEAFD00362448 /* Release */ = { 413 | isa = XCBuildConfiguration; 414 | buildSettings = { 415 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 416 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 417 | CODE_SIGN_STYLE = Automatic; 418 | CURRENT_PROJECT_VERSION = 1; 419 | DEVELOPMENT_TEAM = X2N73UHTAA; 420 | GENERATE_INFOPLIST_FILE = YES; 421 | INFOPLIST_FILE = PinterestCompositionalLayout/App/Info.plist; 422 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 423 | INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; 424 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 425 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 426 | LD_RUNPATH_SEARCH_PATHS = ( 427 | "$(inherited)", 428 | "@executable_path/Frameworks", 429 | ); 430 | MARKETING_VERSION = 1.0; 431 | PRODUCT_BUNDLE_IDENTIFIER = com.swift.app.PinterestCompositionalLayout; 432 | PRODUCT_NAME = "$(TARGET_NAME)"; 433 | SWIFT_EMIT_LOC_STRINGS = YES; 434 | SWIFT_VERSION = 5.0; 435 | TARGETED_DEVICE_FAMILY = "1,2"; 436 | }; 437 | name = Release; 438 | }; 439 | /* End XCBuildConfiguration section */ 440 | 441 | /* Begin XCConfigurationList section */ 442 | AE1C7DD3298AEAFD00362448 /* Build configuration list for PBXProject "PinterestCompositionalLayout" */ = { 443 | isa = XCConfigurationList; 444 | buildConfigurations = ( 445 | AE1C7DEA298AEAFD00362448 /* Debug */, 446 | AE1C7DEB298AEAFD00362448 /* Release */, 447 | ); 448 | defaultConfigurationIsVisible = 0; 449 | defaultConfigurationName = Release; 450 | }; 451 | AE1C7DEC298AEAFD00362448 /* Build configuration list for PBXNativeTarget "PinterestCompositionalLayout" */ = { 452 | isa = XCConfigurationList; 453 | buildConfigurations = ( 454 | AE1C7DED298AEAFD00362448 /* Debug */, 455 | AE1C7DEE298AEAFD00362448 /* Release */, 456 | ); 457 | defaultConfigurationIsVisible = 0; 458 | defaultConfigurationName = Release; 459 | }; 460 | /* End XCConfigurationList section */ 461 | 462 | /* Begin XCRemoteSwiftPackageReference section */ 463 | AE15BA87298FDEA4000D2C92 /* XCRemoteSwiftPackageReference "EasyNetwork" */ = { 464 | isa = XCRemoteSwiftPackageReference; 465 | repositoryURL = "https://github.com/vadim-chistiakov/EasyNetwork"; 466 | requirement = { 467 | branch = master; 468 | kind = branch; 469 | }; 470 | }; 471 | AE1C7DF9298C4F3B00362448 /* XCRemoteSwiftPackageReference "SnapKit" */ = { 472 | isa = XCRemoteSwiftPackageReference; 473 | repositoryURL = "https://github.com/SnapKit/SnapKit.git"; 474 | requirement = { 475 | branch = develop; 476 | kind = branch; 477 | }; 478 | }; 479 | /* End XCRemoteSwiftPackageReference section */ 480 | 481 | /* Begin XCSwiftPackageProductDependency section */ 482 | AE15BA88298FDEA4000D2C92 /* EasyNetwork */ = { 483 | isa = XCSwiftPackageProductDependency; 484 | package = AE15BA87298FDEA4000D2C92 /* XCRemoteSwiftPackageReference "EasyNetwork" */; 485 | productName = EasyNetwork; 486 | }; 487 | AE1C7DFA298C4F3B00362448 /* SnapKit */ = { 488 | isa = XCSwiftPackageProductDependency; 489 | package = AE1C7DF9298C4F3B00362448 /* XCRemoteSwiftPackageReference "SnapKit" */; 490 | productName = SnapKit; 491 | }; 492 | /* End XCSwiftPackageProductDependency section */ 493 | }; 494 | rootObject = AE1C7DD0298AEAFD00362448 /* Project object */; 495 | } 496 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "pins" : [ 3 | { 4 | "identity" : "snapkit", 5 | "kind" : "remoteSourceControl", 6 | "location" : "https://github.com/SnapKit/SnapKit.git", 7 | "state" : { 8 | "branch" : "develop", 9 | "revision" : "58320fe80522414bf3a7e24c88123581dc586752" 10 | } 11 | } 12 | ], 13 | "version" : 2 14 | } 15 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout.xcodeproj/xcuserdata/v.chistiakov.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | PinterestCompositionalLayout.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | SnapKitPlayground (Playground) 1.xcscheme 13 | 14 | isShown 15 | 16 | orderHint 17 | 3 18 | 19 | SnapKitPlayground (Playground) 2.xcscheme 20 | 21 | isShown 22 | 23 | orderHint 24 | 4 25 | 26 | SnapKitPlayground (Playground) 3.xcscheme 27 | 28 | isShown 29 | 30 | orderHint 31 | 2 32 | 33 | SnapKitPlayground (Playground) 4.xcscheme 34 | 35 | isShown 36 | 37 | orderHint 38 | 5 39 | 40 | SnapKitPlayground (Playground) 5.xcscheme 41 | 42 | isShown 43 | 44 | orderHint 45 | 6 46 | 47 | SnapKitPlayground (Playground).xcscheme 48 | 49 | isShown 50 | 51 | orderHint 52 | 0 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/App/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 01.02.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | @main 11 | class AppDelegate: UIResponder, UIApplicationDelegate { 12 | 13 | 14 | 15 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 16 | // Override point for customization after application launch. 17 | return true 18 | } 19 | 20 | // MARK: UISceneSession Lifecycle 21 | 22 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 23 | // Called when a new scene session is being created. 24 | // Use this method to select a configuration to create the new scene with. 25 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 26 | } 27 | 28 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 29 | // Called when the user discards a scene session. 30 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 31 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 32 | } 33 | 34 | 35 | } 36 | 37 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/App/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 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/App/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/App/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/App/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/App/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | UIApplicationSceneManifest 6 | 7 | UIApplicationSupportsMultipleScenes 8 | 9 | UISceneConfigurations 10 | 11 | UIWindowSceneSessionRoleApplication 12 | 13 | 14 | UISceneConfigurationName 15 | Default Configuration 16 | UISceneDelegateClassName 17 | $(PRODUCT_MODULE_NAME).SceneDelegate 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/App/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 01.02.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 11 | 12 | var window: UIWindow? 13 | 14 | 15 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 16 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 17 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 18 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 19 | guard let scene = scene as? UIWindowScene else { return } 20 | self.window = UIWindow(windowScene: scene) 21 | 22 | let viewModel = PinterestViewModelImpl(imagesNetworkService: ImagesNetworkServiceImpl()) 23 | let vc = PinterestViewController(viewModel: viewModel) 24 | let navigationController = UINavigationController(rootViewController: vc) 25 | window?.rootViewController = navigationController 26 | window?.makeKeyAndVisible() 27 | } 28 | 29 | func sceneDidDisconnect(_ scene: UIScene) { 30 | // Called as the scene is being released by the system. 31 | // This occurs shortly after the scene enters the background, or when its session is discarded. 32 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 33 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 34 | } 35 | 36 | func sceneDidBecomeActive(_ scene: UIScene) { 37 | // Called when the scene has moved from an inactive state to an active state. 38 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 39 | } 40 | 41 | func sceneWillResignActive(_ scene: UIScene) { 42 | // Called when the scene will move from an active state to an inactive state. 43 | // This may occur due to temporary interruptions (ex. an incoming phone call). 44 | } 45 | 46 | func sceneWillEnterForeground(_ scene: UIScene) { 47 | // Called as the scene transitions from the background to the foreground. 48 | // Use this method to undo the changes made on entering the background. 49 | } 50 | 51 | func sceneDidEnterBackground(_ scene: UIScene) { 52 | // Called as the scene transitions from the foreground to the background. 53 | // Use this method to save data, release shared resources, and store enough scene-specific state information 54 | // to restore the scene back to its current state. 55 | } 56 | 57 | 58 | } 59 | 60 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/BlurHashDecode/BlurHashDecode.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BlurHashDecode.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 05.02.2023. 6 | // 7 | /* 8 | # BlurHash for iOS, in Swift 9 | 10 | ## Standalone decoder and encoder 11 | 12 | [BlurHashDecode.swift](BlurHashDecode.swift) and [BlurHashEncode.swift](BlurHashEncode.swift) contain a decoder 13 | and encoder for BlurHash to and from `UIImage`. Both files are completeiy standalone, and can simply be copied into your 14 | project directly. 15 | 16 | ### Decoding 17 | 18 | [BlurHashDecode.swift](BlurHashDecode.swift) implements the following extension on `UIImage`: 19 | 20 | public convenience init?(blurHash: String, size: CGSize, punch: Float = 1) 21 | 22 | This creates a UIImage containing the placeholder image decoded from the BlurHash string, or returns nil if decoding failed. 23 | The parameters are: 24 | 25 | * `blurHash` - A string containing the BlurHash. 26 | * `size` - The requested output size. You should keep this small, and let UIKit scale it up for you. 32 pixels wide is plenty. 27 | * `punch` - Adjusts the contrast of the output image. Tweak it if you want a different look for your placeholders. 28 | */ 29 | 30 | import UIKit 31 | 32 | //TODO: - Refactor like class-service 33 | 34 | extension UIImage { 35 | convenience init?(blurHash: String, size: CGSize, punch: Float = 1) { 36 | guard blurHash.count >= 6 else { return nil } 37 | 38 | let sizeFlag = String(blurHash[0]).decode83() 39 | let numY = (sizeFlag / 9) + 1 40 | let numX = (sizeFlag % 9) + 1 41 | 42 | let quantisedMaximumValue = String(blurHash[1]).decode83() 43 | let maximumValue = Float(quantisedMaximumValue + 1) / 166 44 | 45 | guard blurHash.count == 4 + 2 * numX * numY else { return nil } 46 | 47 | let colours: [(Float, Float, Float)] = (0 ..< numX * numY).map { i in 48 | if i == 0 { 49 | let value = String(blurHash[2 ..< 6]).decode83() 50 | return decodeDC(value) 51 | } else { 52 | let value = String(blurHash[4 + i * 2 ..< 4 + i * 2 + 2]).decode83() 53 | return decodeAC(value, maximumValue: maximumValue * punch) 54 | } 55 | } 56 | 57 | let width = Int(size.width) 58 | let height = Int(size.height) 59 | let bytesPerRow = width * 3 60 | guard let data = CFDataCreateMutable(kCFAllocatorDefault, bytesPerRow * height) 61 | else { return nil } 62 | 63 | CFDataSetLength(data, bytesPerRow * height) 64 | guard let pixels = CFDataGetMutableBytePtr(data) else { return nil } 65 | 66 | for y in 0 ..< height { 67 | for x in 0 ..< width { 68 | var r: Float = 0 69 | var g: Float = 0 70 | var b: Float = 0 71 | 72 | for j in 0 ..< numY { 73 | for i in 0 ..< numX { 74 | let basis = cos(Float.pi * Float(x) * Float(i) / Float(width)) * cos(Float.pi * Float(y) * Float(j) / Float(height)) 75 | let colour = colours[i + j * numX] 76 | r += colour.0 * basis 77 | g += colour.1 * basis 78 | b += colour.2 * basis 79 | } 80 | } 81 | 82 | let intR = UInt8(linearTosRGB(r)) 83 | let intG = UInt8(linearTosRGB(g)) 84 | let intB = UInt8(linearTosRGB(b)) 85 | 86 | pixels[3 * x + 0 + y * bytesPerRow] = intR 87 | pixels[3 * x + 1 + y * bytesPerRow] = intG 88 | pixels[3 * x + 2 + y * bytesPerRow] = intB 89 | } 90 | } 91 | 92 | let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.none.rawValue) 93 | 94 | guard let provider = CGDataProvider(data: data) else { return nil } 95 | guard let cgImage = CGImage( 96 | width: width, 97 | height: height, 98 | bitsPerComponent: 8, 99 | bitsPerPixel: 24, 100 | bytesPerRow: bytesPerRow, 101 | space: CGColorSpaceCreateDeviceRGB(), 102 | bitmapInfo: bitmapInfo, 103 | provider: provider, 104 | decode: nil, 105 | shouldInterpolate: true, 106 | intent: .defaultIntent 107 | ) else { return nil } 108 | 109 | self.init(cgImage: cgImage) 110 | } 111 | } 112 | 113 | private func decodeDC(_ value: Int) -> (Float, Float, Float) { 114 | let intR = value >> 16 115 | let intG = (value >> 8) & 255 116 | let intB = value & 255 117 | return (sRGBToLinear(intR), sRGBToLinear(intG), sRGBToLinear(intB)) 118 | } 119 | 120 | private func decodeAC(_ value: Int, maximumValue: Float) -> (Float, Float, Float) { 121 | let quantR = value / (19 * 19) 122 | let quantG = (value / 19) % 19 123 | let quantB = value % 19 124 | 125 | let rgb = ( 126 | signPow((Float(quantR) - 9) / 9, 2) * maximumValue, 127 | signPow((Float(quantG) - 9) / 9, 2) * maximumValue, 128 | signPow((Float(quantB) - 9) / 9, 2) * maximumValue 129 | ) 130 | 131 | return rgb 132 | } 133 | 134 | private func signPow(_ value: Float, _ exp: Float) -> Float { 135 | copysign(pow(abs(value), exp), value) 136 | } 137 | 138 | private func linearTosRGB(_ value: Float) -> Int { 139 | let v = max(0, min(1, value)) 140 | if v <= 0.0031308 { return Int(v * 12.92 * 255 + 0.5) } 141 | else { return Int((1.055 * pow(v, 1 / 2.4) - 0.055) * 255 + 0.5) } 142 | } 143 | 144 | private func sRGBToLinear(_ value: Type) -> Float { 145 | let v = Float(Int64(value)) / 255 146 | if v <= 0.04045 { return v / 12.92 } 147 | else { return pow((v + 0.055) / 1.055, 2.4) } 148 | } 149 | 150 | private let encodeCharacters: [String] = { 151 | "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" 152 | .map { String($0) } 153 | }() 154 | 155 | private let decodeCharacters: [String: Int] = { 156 | var dict: [String: Int] = [:] 157 | for (index, character) in encodeCharacters.enumerated() { 158 | dict[character] = index 159 | } 160 | return dict 161 | }() 162 | 163 | extension String { 164 | func decode83() -> Int { 165 | var value: Int = 0 166 | for character in self { 167 | if let digit = decodeCharacters[String(character)] { 168 | value = value * 83 + digit 169 | } 170 | } 171 | return value 172 | } 173 | } 174 | 175 | private extension String { 176 | subscript (offset: Int) -> Character { 177 | return self[index(startIndex, offsetBy: offset)] 178 | } 179 | 180 | subscript (bounds: CountableClosedRange) -> Substring { 181 | let start = index(startIndex, offsetBy: bounds.lowerBound) 182 | let end = index(startIndex, offsetBy: bounds.upperBound) 183 | return self[start...end] 184 | } 185 | 186 | subscript (bounds: CountableRange) -> Substring { 187 | let start = index(startIndex, offsetBy: bounds.lowerBound) 188 | let end = index(startIndex, offsetBy: bounds.upperBound) 189 | return self[start.. UICollectionViewCompositionalLayout { 14 | .init { sectionIndex, enviroment in 15 | guard let section = Section(rawValue: sectionIndex) 16 | else { return nil } 17 | switch section { 18 | case .carousel : 19 | return carouselBannerSection() 20 | case .widget : 21 | return widgetBannerSection() 22 | case .pinterest: 23 | return pinterestSection(ratios: ratios, contentWidth: contentWidth) 24 | } 25 | } 26 | } 27 | 28 | private static func carouselBannerSection() -> NSCollectionLayoutSection { 29 | let itemSize = NSCollectionLayoutSize( 30 | widthDimension: .fractionalWidth(1), 31 | heightDimension: .fractionalHeight(1) 32 | ) 33 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 34 | 35 | let groupSize = NSCollectionLayoutSize( 36 | widthDimension: .fractionalWidth(1), 37 | heightDimension: .fractionalWidth(1) 38 | ) 39 | let group = NSCollectionLayoutGroup.horizontal( 40 | layoutSize: groupSize, 41 | subitems: [item] 42 | ) 43 | let section = NSCollectionLayoutSection(group: group) 44 | section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary 45 | section.visibleItemsInvalidationHandler = { (items, offset, environment) in 46 | items.forEach { item in 47 | let distanceFromCenter = abs((item.frame.midX - offset.x) - environment.container.contentSize.width / 2.0) 48 | let minScale: CGFloat = 0.8 49 | let maxScale: CGFloat = 1.0 - distanceFromCenter / environment.container.contentSize.width 50 | let scale = max(maxScale, minScale) 51 | item.transform = CGAffineTransform(scaleX: scale, y: scale) 52 | } 53 | } 54 | return section 55 | } 56 | 57 | private static func widgetBannerSection() -> NSCollectionLayoutSection { 58 | let itemSize = NSCollectionLayoutSize( 59 | widthDimension: .fractionalWidth(1), 60 | heightDimension: .fractionalHeight(1) 61 | ) 62 | let item = NSCollectionLayoutItem(layoutSize: itemSize) 63 | item.contentInsets = .init(top: 0, leading: 5, bottom: 0, trailing: 5) 64 | 65 | let groupSize = NSCollectionLayoutSize( 66 | widthDimension: .fractionalWidth(0.2), 67 | heightDimension: .fractionalWidth(0.3) 68 | ) 69 | let group = NSCollectionLayoutGroup.horizontal( 70 | layoutSize: groupSize, 71 | subitems: [item] 72 | ) 73 | let section = NSCollectionLayoutSection(group: group) 74 | let supplementaryItem = NSCollectionLayoutBoundarySupplementaryItem( 75 | layoutSize: .init( 76 | widthDimension: .fractionalWidth(1), 77 | heightDimension: .absolute(30) 78 | ), 79 | elementKind: UICollectionView.elementKindSectionHeader, alignment: .top 80 | ) 81 | supplementaryItem.contentInsets = .init( 82 | top: 0, 83 | leading: 5, 84 | bottom: 0, 85 | trailing: 5 86 | ) 87 | section.boundarySupplementaryItems = [supplementaryItem] 88 | section.contentInsets = .init(top: 10, leading: 5, bottom: 10, trailing: 5) 89 | section.orthogonalScrollingBehavior = .continuous 90 | return section 91 | } 92 | 93 | private static func pinterestSection( 94 | ratios: [Ratioable], 95 | contentWidth: CGFloat 96 | ) -> NSCollectionLayoutSection { 97 | let spacing: CGFloat = 5 98 | let pinterestSection = PinterestLayoutSection( 99 | columnsCount: 2, 100 | itemRatios: ratios, 101 | spacing: spacing * 2, 102 | contentWidth: contentWidth 103 | ).section 104 | let supplementaryItem = NSCollectionLayoutBoundarySupplementaryItem( 105 | layoutSize: .init( 106 | widthDimension: .fractionalWidth(1), 107 | heightDimension: .absolute(30) 108 | ), 109 | elementKind: UICollectionView.elementKindSectionHeader, alignment: .top 110 | ) 111 | supplementaryItem.contentInsets = .init( 112 | top: 0, 113 | leading: spacing, 114 | bottom: 0, 115 | trailing: spacing 116 | ) 117 | pinterestSection.boundarySupplementaryItems = [supplementaryItem] 118 | return pinterestSection 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/Models/Config.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Config.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 05.02.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | let accessKey = "tswvohI9oyY9BT3dxrFrdKavGSrh7HN-QjlZ-avMctQ" 11 | let secretKey = "Obkqp6GqiGBKGYIA_YexFIcItrJmTyJzWI63H5WNnB4" 12 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/Models/PictureModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PictureModel.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 01.02.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | struct PictureModel: Hashable, Decodable { 11 | 12 | // MARK: - Urls 13 | struct Urls: Hashable, Decodable { 14 | let raw, full, regular, small, thumb: String 15 | } 16 | 17 | let description: String? 18 | let urls: Urls 19 | let width: CGFloat 20 | let height: CGFloat 21 | let blurHash: String 22 | 23 | var blurHashSize: CGSize { 24 | .init(width: width/100, height: height/100) 25 | } 26 | } 27 | 28 | extension PictureModel: Ratioable { 29 | var ratio: CGFloat { 30 | width / height 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/Models/Ratioable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Ratioable.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 05.02.2023. 6 | // 7 | 8 | import Foundation 9 | 10 | protocol Ratioable { 11 | var ratio: CGFloat { get } 12 | } 13 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/NetworkLayer/ImagesEndpoint.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagesEndpoint.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 05.02.2023. 6 | // 7 | 8 | import Foundation 9 | import EasyNetwork 10 | 11 | enum ImagesEndpoint { 12 | case images(page: Int) 13 | } 14 | 15 | extension ImagesEndpoint: GetEndpoint { 16 | 17 | var header: Header? { 18 | ["Authorization": "Client-ID \(accessKey)"] 19 | } 20 | 21 | var host: String { 22 | "api.unsplash.com" 23 | } 24 | 25 | var path: String { 26 | switch self { 27 | case .images: 28 | return "/photos" 29 | } 30 | } 31 | 32 | var params: [URLQueryItem]? { 33 | switch self { 34 | case .images(let page): 35 | return [ 36 | .init(name: "page", value: "\(page)"), 37 | .init(name: "per_page", value: "30") 38 | ] 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/NetworkLayer/ImagesNetworkService.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ImagesNetworkService.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 05.02.2023. 6 | // 7 | 8 | import Foundation 9 | import EasyNetwork 10 | import Combine 11 | 12 | protocol ImagesNetworkService { 13 | func getImages(page: Int) -> AnyPublisher<[PictureModel], RequestError> 14 | func getImage(urlString: String) -> AnyPublisher 15 | } 16 | 17 | final class ImagesNetworkServiceImpl: EasyNetworkClient, ImagesNetworkService { 18 | func getImages(page: Int) -> AnyPublisher<[PictureModel], RequestError> { 19 | sendRequest( 20 | endpoint: ImagesEndpoint.images(page: page), 21 | responseModelType: [PictureModel].self 22 | ) 23 | } 24 | 25 | func getImage(urlString: String) -> AnyPublisher { 26 | guard let url = URL(string: urlString) else { 27 | return Fail(error: RequestError.urlMalformed) 28 | .eraseToAnyPublisher() 29 | } 30 | let request = URLRequest(url: url) 31 | return URLSession.shared.dataTaskPublisher(for: request) 32 | .map { $0.data } 33 | .mapError { _ in .unknown("Image can't load")} 34 | .eraseToAnyPublisher() 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/PinterestLayoutSection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PinterestLayoutSection.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 01.02.2023. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | final class PinterestLayoutSection { 12 | 13 | var section: NSCollectionLayoutSection { 14 | let section = NSCollectionLayoutSection(group: customLayoutGroup) 15 | section.contentInsets = .init(top: 0, leading: padding, bottom: 0, trailing: padding) 16 | return section 17 | } 18 | 19 | //MARK: - Private methods 20 | 21 | private let numberOfColumns: Int 22 | private let itemRatios: [Ratioable] 23 | private let spacing: CGFloat 24 | private let contentWidth: CGFloat 25 | 26 | private var padding: CGFloat { 27 | spacing / 2 28 | } 29 | 30 | // Padding around cells equal to the distance between cells 31 | private var insets: NSDirectionalEdgeInsets { 32 | return .init(top: padding, leading: padding, bottom: padding, trailing: padding) 33 | } 34 | 35 | private lazy var frames: [CGRect] = { 36 | calculateFrames() 37 | }() 38 | 39 | // Max height for section 40 | private lazy var sectionHeight: CGFloat = { 41 | (frames 42 | .map(\.maxY) 43 | .max() ?? 0 44 | ) + insets.bottom 45 | }() 46 | 47 | private lazy var customLayoutGroup: NSCollectionLayoutGroup = { 48 | let layoutSize = NSCollectionLayoutSize( 49 | widthDimension: .fractionalWidth(1.0), 50 | heightDimension: .absolute(sectionHeight) 51 | ) 52 | return NSCollectionLayoutGroup.custom(layoutSize: layoutSize) { _ in 53 | self.frames.map { .init(frame: $0) } 54 | } 55 | }() 56 | 57 | init( 58 | columnsCount: Int, 59 | itemRatios: [Ratioable], 60 | spacing: CGFloat, 61 | contentWidth: CGFloat 62 | ) { 63 | self.numberOfColumns = columnsCount 64 | self.itemRatios = itemRatios 65 | self.spacing = spacing 66 | self.contentWidth = contentWidth 67 | } 68 | 69 | private func calculateFrames() -> [CGRect] { 70 | var contentHeight: CGFloat = 0 71 | 72 | // Subtract the margin from the total width and divide by the number of columns 73 | let columnWidth = (contentWidth - insets.leading - insets.trailing) / CGFloat(numberOfColumns) 74 | 75 | // Stores x-coordinate offset for each column. Not changing 76 | let xOffset = (0.. 0 else { return nil } 121 | var min = first 122 | var index = 0 123 | 124 | indices.forEach { i in 125 | let currentItem = self[i] 126 | if let minumum = min, currentItem < minumum { 127 | min = currentItem 128 | index = i 129 | } 130 | } 131 | 132 | return index 133 | } 134 | } 135 | 136 | private extension CGRect { 137 | func setHeight(ratio: CGFloat) -> CGRect { 138 | .init(x: minX, y: minY, width: width, height: width / ratio) 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/PinterestViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 01.02.2023. 6 | // 7 | 8 | import UIKit 9 | import SnapKit 10 | import Combine 11 | 12 | typealias DataSource = UICollectionViewDiffableDataSource 13 | typealias DataSourceSnapshot = NSDiffableDataSourceSnapshot 14 | 15 | enum Section: Int, CaseIterable { 16 | case carousel 17 | case widget 18 | case pinterest 19 | } 20 | 21 | final class PinterestViewController: UIViewController, UICollectionViewDelegate { 22 | 23 | private enum Const { 24 | static let cellId = "cellId" 25 | static let headerId = "headerId" 26 | } 27 | 28 | private let viewModel: PinterestViewModel 29 | private var cancellables = Set() 30 | 31 | init(viewModel: PinterestViewModel) { 32 | self.viewModel = viewModel 33 | super.init(nibName: nil, bundle: nil) 34 | } 35 | 36 | required init?(coder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | private lazy var collectionView: UICollectionView = { 41 | let collectionView = UICollectionView( 42 | frame: .zero, 43 | collectionViewLayout: UICollectionViewLayout() 44 | ) 45 | collectionView.delegate = self 46 | collectionView.showsHorizontalScrollIndicator = false 47 | collectionView.register( 48 | PictureCell.self, 49 | forCellWithReuseIdentifier: Const.cellId 50 | ) 51 | collectionView.register( 52 | HeaderView.self, 53 | forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, 54 | withReuseIdentifier: Const.headerId 55 | ) 56 | return collectionView 57 | }() 58 | 59 | override func viewDidLoad() { 60 | super.viewDidLoad() 61 | configureUI() 62 | configureRefresh() 63 | configureDataSource() 64 | viewModel 65 | .loadImages(animatingDifferences: false) 66 | .sink { [weak self] ratios in 67 | self?.configureLayout(ratios: ratios) 68 | } 69 | .store(in: &cancellables) 70 | } 71 | 72 | private func configureUI() { 73 | title = "Pinterest Compositional Layout" 74 | view.backgroundColor = .black 75 | view.addSubview(collectionView) 76 | collectionView.snp.makeConstraints { make in 77 | make.edges.equalToSuperview() 78 | } 79 | } 80 | 81 | private func configureRefresh() { 82 | let refreshControl = UIRefreshControl() 83 | refreshControl.addTarget(self, action: #selector(handleRefresh), for: .valueChanged) 84 | refreshControl.tintColor = .gray 85 | collectionView.refreshControl = refreshControl 86 | } 87 | 88 | @objc 89 | private func handleRefresh() { 90 | guard !viewModel.isRefreshing else { return } 91 | viewModel 92 | .refresh() 93 | .sink { [weak self] ratios in 94 | self?.configureLayout(ratios: ratios) 95 | self?.collectionView.refreshControl?.endRefreshing() 96 | } 97 | .store(in: &cancellables) 98 | } 99 | 100 | private func configureLayout(ratios: [Ratioable]) { 101 | let layout = CustomCompositionalLayout.layout( 102 | ratios: ratios, 103 | contentWidth: view.frame.width 104 | ) 105 | collectionView.setCollectionViewLayout(layout, animated: true) 106 | } 107 | 108 | } 109 | 110 | extension PinterestViewController { 111 | 112 | private func configureDataSource() { 113 | viewModel.dataSource = DataSource( 114 | collectionView: collectionView, 115 | cellProvider: { [weak self] (collectionView, indexPath, model) -> PictureCell? in 116 | guard let self, 117 | let section = Section(rawValue: indexPath.section) 118 | else { return .init() } 119 | let cell = collectionView.dequeueReusableCell( 120 | withReuseIdentifier: Const.cellId, for: indexPath) as! PictureCell 121 | cell.imageView.image = UIImage(blurHash: model.blurHash, size: model.blurHashSize) 122 | self.viewModel.loadImage(for: indexPath.item, inSection: section) 123 | .delay(for: 2, scheduler: DispatchQueue.main) 124 | .sink { _ in } 125 | receiveValue: { data in 126 | 127 | cell.imageView.image = UIImage(data: data) 128 | } 129 | .store(in: &self.cancellables) 130 | return cell 131 | } 132 | ) 133 | viewModel.dataSource.supplementaryViewProvider = { (collectionView, kind, indexPath) -> UICollectionReusableView? in 134 | guard let section = Section(rawValue: indexPath.section), 135 | let header: HeaderView = collectionView.dequeueReusableSupplementaryView( 136 | ofKind: UICollectionView.elementKindSectionHeader, 137 | withReuseIdentifier: Const.headerId, 138 | for: indexPath 139 | ) as? HeaderView 140 | else { return .init() } 141 | switch section { 142 | case .carousel: 143 | break 144 | case .widget: 145 | header.titleLabel.text = "Widget" 146 | case .pinterest: 147 | header.titleLabel.text = "Pinterest" 148 | } 149 | return header 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/PinterestViewModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PinterestViewModel.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 05.02.2023. 6 | // 7 | 8 | import Foundation 9 | import Combine 10 | import EasyNetwork 11 | 12 | protocol PinterestViewModel: AnyObject { 13 | var dataSource: DataSource! { get set } 14 | var isRefreshing: Bool { get } 15 | 16 | func refresh() -> AnyPublisher<[Ratioable], Never> 17 | func loadImages(animatingDifferences: Bool) -> AnyPublisher<[Ratioable], Never> 18 | func loadImage(for index: Int, inSection section: Section) -> AnyPublisher 19 | } 20 | 21 | final class PinterestViewModelImpl: PinterestViewModel { 22 | 23 | var dataSource: DataSource! 24 | 25 | private var snapshot = DataSourceSnapshot() 26 | private(set) var isRefreshing: Bool = false 27 | 28 | private let imagesNetworkService: ImagesNetworkService 29 | private var cancellables = Set() 30 | 31 | init(imagesNetworkService: ImagesNetworkService) { 32 | self.imagesNetworkService = imagesNetworkService 33 | } 34 | 35 | func loadImages(animatingDifferences: Bool = false) -> AnyPublisher<[Ratioable], Never> { 36 | imagesNetworkService.getImages(page: (1...10).randomElement() ?? 1) 37 | .receive(on: DispatchQueue.main) 38 | .handleEvents(receiveOutput: { [weak self] pictures in 39 | self?.configureDataSource(pictures: pictures, animatingDifferences: false) 40 | }, receiveCompletion: { completion in 41 | switch completion { 42 | case .finished: 43 | break 44 | case .failure(let error): 45 | print("error \(error.debugDescription)") 46 | } 47 | }) 48 | // .map { $0[($0.count/2)+1...$0.count-1] } 49 | .map { $0.map { $0 as Ratioable }} 50 | .replaceError(with: []) 51 | .eraseToAnyPublisher() 52 | } 53 | 54 | func loadImage(for index: Int, inSection section: Section) -> AnyPublisher { 55 | imagesNetworkService.getImage(urlString: snapshot.itemIdentifiers(inSection: section)[index].urls.small) 56 | .receive(on: DispatchQueue.main) 57 | .eraseToAnyPublisher() 58 | } 59 | 60 | func refresh() -> AnyPublisher<[Ratioable], Never> { 61 | isRefreshing = true 62 | return loadImages(animatingDifferences: true) 63 | } 64 | 65 | //MARK: - Private methods 66 | 67 | private func configureDataSource(pictures: [PictureModel], animatingDifferences: Bool) { 68 | snapshot.deleteAllItems() 69 | snapshot.appendSections(Section.allCases) 70 | 71 | snapshot.appendItems(pictures[20...29].map { $0 }, toSection: .carousel) 72 | snapshot.appendItems(pictures[10...19].map { $0 }, toSection: .widget) 73 | snapshot.appendItems(pictures[0...9].map { $0 }, toSection: .pinterest) 74 | 75 | dataSource.apply(snapshot, animatingDifferences: animatingDifferences) 76 | isRefreshing = false 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/Views/HeaderView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // HeaderView.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 11.02.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | final class HeaderView: UICollectionReusableView { 11 | 12 | let titleLabel: UILabel = { 13 | let label = UILabel() 14 | label.font = UIFont.boldSystemFont(ofSize: 16) 15 | label.textColor = .black 16 | return label 17 | }() 18 | 19 | override init(frame: CGRect) { 20 | super.init(frame: frame) 21 | addSubview(titleLabel) 22 | titleLabel.snp.makeConstraints { make in 23 | make.edges.equalToSuperview() 24 | } 25 | } 26 | 27 | required init?(coder: NSCoder) { 28 | fatalError("init(coder:) has not been implemented") 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /PinterestCompositionalLayout/Pinterest/Views/PictureCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PictureCell.swift 3 | // PinterestCompositionalLayout 4 | // 5 | // Created by Vadim Chistiakov on 02.02.2023. 6 | // 7 | 8 | import UIKit 9 | 10 | final class PictureCell: UICollectionViewCell { 11 | 12 | lazy var imageView: UIImageView = { 13 | let imageView = UIImageView() 14 | imageView.contentMode = .scaleAspectFill 15 | return imageView 16 | }() 17 | 18 | lazy var titleLabel: UILabel = { 19 | let titleLabel = UILabel() 20 | titleLabel.textAlignment = .center 21 | return titleLabel 22 | }() 23 | 24 | override init(frame: CGRect) { 25 | super.init(frame: frame) 26 | contentView.backgroundColor = .lightGray 27 | contentView.layer.cornerRadius = 10 28 | contentView.layer.masksToBounds = true 29 | contentView.addSubview(imageView) 30 | contentView.addSubview(titleLabel) 31 | 32 | imageView.snp.makeConstraints { make in 33 | make.edges.equalToSuperview() 34 | } 35 | 36 | titleLabel.snp.makeConstraints { make in 37 | make.bottom.left.right.equalToSuperview().inset(5) 38 | } 39 | } 40 | 41 | required init?(coder: NSCoder) { 42 | fatalError("init(coder:) has not been implemented") 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CoolDiscover 2 | 3 | The most exciting first screen in apps 4 | 5 | 6 | ![ezgif-4-dbd8ab7133](https://user-images.githubusercontent.com/22453570/218311471-b10d9b9f-475d-427d-8bbf-d3ee89a8bef4.gif) 7 | 8 | Detailed explanation you can find via link below: 9 | 10 | Implementing custom UICollectionViewCompositionalLayout with Pinterest Section 11 | 12 | https://hackernoon.com/implementing-uicollectionview-compositional-layout-with-pinterest-section 13 | --------------------------------------------------------------------------------