├── .gitignore ├── ASDKgram-Swift.xcodeproj └── project.pbxproj ├── ASDKgram-Swift ├── ASCollectionSectionController.swift ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Constants.swift ├── Date.swift ├── Diffable.swift ├── Info.plist ├── NetworkImageView.swift ├── NumberFormatter.swift ├── OrderedDictionary │ ├── Dictionary+OrderedDictionary.swift │ ├── OrderedDictionary+Codable.swift │ ├── OrderedDictionary+Deprecated.swift │ ├── OrderedDictionary+Description.swift │ ├── OrderedDictionary.swift │ └── OrderedDictionarySlice.swift ├── ParsePesponse.swift ├── PhotoFeedListKitViewController.swift ├── PhotoFeedModel.swift ├── PhotoFeedSectionController.swift ├── PhotoFeedTableNodeController.swift ├── PhotoFeedTableViewController.swift ├── PhotoModel.swift ├── PhotoTableNodeCell.swift ├── PhotoTableViewCell.swift ├── PopularPageModel.swift ├── UIColor.swift ├── UIImage.swift ├── URL.swift └── Webservice.swift ├── Podfile ├── Podfile.lock ├── README.md └── snap.gif /.gitignore: -------------------------------------------------------------------------------- 1 | # other 2 | nginx_access.log 3 | nohup.out 4 | # Xcode 5 | .DS_Store 6 | build/ 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | DerivedData 19 | *.hmap 20 | *.ipa 21 | 22 | cache/* 23 | log/* 24 | crond/lock/* 25 | # CocoaPods 26 | # 27 | # We recommend against adding the Pods directory to your .gitignore. However 28 | # you should judge for yourself, the pros and cons are mentioned at: 29 | # http://guides.cocoapods.org/using/using-cocoapods.html#should-i-ignore-the-pods-directory-in-source-control? 30 | # 31 | Pods/ 32 | Podfile.lock 33 | 34 | ### AppCode ### 35 | ## Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm 36 | # 37 | ### Directory-based project format 38 | .idea/ 39 | ## if you remove the above rule, at least ignore user-specific stuff: 40 | # .idea/workspace.xml 41 | ## .idea/tasks.xml 42 | ## and these sensitive or high-churn files: 43 | ## .idea/dataSources.ids 44 | ## .idea/dataSources.xml 45 | ## .idea/sqlDataSources.xml 46 | ## .idea/dynamic.xml 47 | # 48 | ### File-based project format 49 | *.ipr 50 | *.iml 51 | *.iws 52 | # 53 | ### Additional for IntelliJ 54 | out/ 55 | # 56 | ## generated by mpeltonen/sbt-idea plugin 57 | .idea_modules/ 58 | # 59 | ## generated by JIRA plugin 60 | atlassian-ide-plugin.xml 61 | # 62 | ## generated by Crashlytics plugin (for Android Studio and Intellij) 63 | com_crashlytics_export_strings.xml 64 | # 65 | # 66 | -------------------------------------------------------------------------------- /ASDKgram-Swift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 3A2362FB1E2D33A0007E08F1 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2362FA1E2D33A0007E08F1 /* Date.swift */; }; 11 | 3A7A28D91E2F7410003E2B8D /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7A28D81E2F7410003E2B8D /* UIImage.swift */; }; 12 | 3AB33F5E1E1F94530039F711 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F5D1E1F94530039F711 /* AppDelegate.swift */; }; 13 | 3AB33F651E1F94530039F711 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AB33F641E1F94530039F711 /* Assets.xcassets */; }; 14 | 3AB33F681E1F94530039F711 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3AB33F661E1F94530039F711 /* LaunchScreen.storyboard */; }; 15 | 3AB33F761E1F9C330039F711 /* PhotoFeedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F751E1F9C330039F711 /* PhotoFeedTableViewController.swift */; }; 16 | 3AB33F781E1F9C400039F711 /* PhotoFeedTableNodeController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F771E1F9C400039F711 /* PhotoFeedTableNodeController.swift */; }; 17 | 3AB33F7B1E1F9E630039F711 /* UIColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F7A1E1F9E630039F711 /* UIColor.swift */; }; 18 | 3AB33F811E1FDE100039F711 /* Webservice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F801E1FDE100039F711 /* Webservice.swift */; }; 19 | 3AB33F831E20E81E0039F711 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F821E20E81E0039F711 /* Constants.swift */; }; 20 | 3AB33F861E20E9B10039F711 /* PhotoFeedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F851E20E9B10039F711 /* PhotoFeedModel.swift */; }; 21 | 3AB33F881E20ED460039F711 /* PhotoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F871E20ED460039F711 /* PhotoModel.swift */; }; 22 | 3AB33F8C1E2106F30039F711 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F8B1E2106F30039F711 /* URL.swift */; }; 23 | 3AB33F961E2269D40039F711 /* PopularPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F951E2269D40039F711 /* PopularPageModel.swift */; }; 24 | 3AB33F981E22A0080039F711 /* ParsePesponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F971E22A0080039F711 /* ParsePesponse.swift */; }; 25 | 3AB33F9E1E22D9DB0039F711 /* PhotoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */; }; 26 | 3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33FA11E230A160039F711 /* NetworkImageView.swift */; }; 27 | 3AB33FA41E2337850039F711 /* PhotoTableNodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */; }; 28 | 65537E7321B79B6000F13882 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65537E7221B79B6000F13882 /* NumberFormatter.swift */; }; 29 | 6554D8A42D04419D00F50231 /* OrderedDictionarySlice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6554D8A22D04419D00F50231 /* OrderedDictionarySlice.swift */; }; 30 | 6554D8A52D04419D00F50231 /* OrderedDictionary+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6554D8A12D04419D00F50231 /* OrderedDictionary+Description.swift */; }; 31 | 6554D8A62D04419D00F50231 /* OrderedDictionary+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6554D89F2D04419D00F50231 /* OrderedDictionary+Codable.swift */; }; 32 | 6554D8A72D04419D00F50231 /* OrderedDictionary+Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6554D8A02D04419D00F50231 /* OrderedDictionary+Deprecated.swift */; }; 33 | 6554D8A82D04419D00F50231 /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6554D89E2D04419D00F50231 /* OrderedDictionary.swift */; }; 34 | 6554D8A92D04419D00F50231 /* Dictionary+OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6554D89D2D04419D00F50231 /* Dictionary+OrderedDictionary.swift */; }; 35 | 6570DDF11F6F954600369D3E /* ASCollectionSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6570DDF01F6F954600369D3E /* ASCollectionSectionController.swift */; }; 36 | 6570DDF71F6FC00500369D3E /* PhotoFeedSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6570DDF61F6FC00500369D3E /* PhotoFeedSectionController.swift */; }; 37 | 6570DDFB1F70BB5800369D3E /* Diffable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6570DDFA1F70BB5800369D3E /* Diffable.swift */; }; 38 | 657909BF1F59640900936FD3 /* PhotoFeedListKitViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 657909BE1F59640900936FD3 /* PhotoFeedListKitViewController.swift */; }; 39 | 7E438240D2C4026931D60594 /* Pods_ASDKgram_Swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */; }; 40 | /* End PBXBuildFile section */ 41 | 42 | /* Begin PBXFileReference section */ 43 | 019E984FADA258377FC6B2D8 /* Pods-ASDKgram-Swift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ASDKgram-Swift.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift.debug.xcconfig"; sourceTree = ""; }; 44 | 3A2362FA1E2D33A0007E08F1 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; 45 | 3A7A28D81E2F7410003E2B8D /* UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 46 | 3AB33F5A1E1F94520039F711 /* ASDKgram-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ASDKgram-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 47 | 3AB33F5D1E1F94530039F711 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 48 | 3AB33F641E1F94530039F711 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49 | 3AB33F671E1F94530039F711 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 50 | 3AB33F691E1F94530039F711 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 3AB33F751E1F9C330039F711 /* PhotoFeedTableViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFeedTableViewController.swift; sourceTree = ""; }; 52 | 3AB33F771E1F9C400039F711 /* PhotoFeedTableNodeController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFeedTableNodeController.swift; sourceTree = ""; }; 53 | 3AB33F7A1E1F9E630039F711 /* UIColor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIColor.swift; sourceTree = ""; }; 54 | 3AB33F801E1FDE100039F711 /* Webservice.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Webservice.swift; sourceTree = ""; }; 55 | 3AB33F821E20E81E0039F711 /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 56 | 3AB33F851E20E9B10039F711 /* PhotoFeedModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFeedModel.swift; sourceTree = ""; }; 57 | 3AB33F871E20ED460039F711 /* PhotoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoModel.swift; sourceTree = ""; }; 58 | 3AB33F8B1E2106F30039F711 /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 59 | 3AB33F951E2269D40039F711 /* PopularPageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularPageModel.swift; sourceTree = ""; }; 60 | 3AB33F971E22A0080039F711 /* ParsePesponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParsePesponse.swift; sourceTree = ""; }; 61 | 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableViewCell.swift; sourceTree = ""; }; 62 | 3AB33FA11E230A160039F711 /* NetworkImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageView.swift; sourceTree = ""; }; 63 | 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableNodeCell.swift; sourceTree = ""; }; 64 | 4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ASDKgram_Swift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | 65537E7221B79B6000F13882 /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = ""; }; 66 | 6554D89D2D04419D00F50231 /* Dictionary+OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Dictionary+OrderedDictionary.swift"; sourceTree = ""; }; 67 | 6554D89E2D04419D00F50231 /* OrderedDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; 68 | 6554D89F2D04419D00F50231 /* OrderedDictionary+Codable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Codable.swift"; sourceTree = ""; }; 69 | 6554D8A02D04419D00F50231 /* OrderedDictionary+Deprecated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Deprecated.swift"; sourceTree = ""; }; 70 | 6554D8A12D04419D00F50231 /* OrderedDictionary+Description.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Description.swift"; sourceTree = ""; }; 71 | 6554D8A22D04419D00F50231 /* OrderedDictionarySlice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OrderedDictionarySlice.swift; sourceTree = ""; }; 72 | 6570DDF01F6F954600369D3E /* ASCollectionSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ASCollectionSectionController.swift; sourceTree = ""; }; 73 | 6570DDF61F6FC00500369D3E /* PhotoFeedSectionController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFeedSectionController.swift; sourceTree = ""; }; 74 | 6570DDFA1F70BB5800369D3E /* Diffable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Diffable.swift; sourceTree = ""; }; 75 | 657909BE1F59640900936FD3 /* PhotoFeedListKitViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoFeedListKitViewController.swift; sourceTree = ""; }; 76 | A3A86E74A8C3F06D7688AACB /* Pods-ASDKgram-Swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ASDKgram-Swift.release.xcconfig"; path = "Pods/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift.release.xcconfig"; sourceTree = ""; }; 77 | /* End PBXFileReference section */ 78 | 79 | /* Begin PBXFrameworksBuildPhase section */ 80 | 3AB33F571E1F94520039F711 /* Frameworks */ = { 81 | isa = PBXFrameworksBuildPhase; 82 | buildActionMask = 2147483647; 83 | files = ( 84 | 7E438240D2C4026931D60594 /* Pods_ASDKgram_Swift.framework in Frameworks */, 85 | ); 86 | runOnlyForDeploymentPostprocessing = 0; 87 | }; 88 | /* End PBXFrameworksBuildPhase section */ 89 | 90 | /* Begin PBXGroup section */ 91 | 3AB33F511E1F94520039F711 = { 92 | isa = PBXGroup; 93 | children = ( 94 | 3AB33F5C1E1F94530039F711 /* ASDKgram-Swift */, 95 | 3AB33F5B1E1F94520039F711 /* Products */, 96 | 78A64CA59A49BE1637214DF1 /* Pods */, 97 | A7DD645D70CF34C7CA3B1A8B /* Frameworks */, 98 | ); 99 | sourceTree = ""; 100 | }; 101 | 3AB33F5B1E1F94520039F711 /* Products */ = { 102 | isa = PBXGroup; 103 | children = ( 104 | 3AB33F5A1E1F94520039F711 /* ASDKgram-Swift.app */, 105 | ); 106 | name = Products; 107 | sourceTree = ""; 108 | }; 109 | 3AB33F5C1E1F94530039F711 /* ASDKgram-Swift */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 6554D8A32D04419D00F50231 /* OrderedDictionary */, 113 | 3AB33F991E22CF160039F711 /* Views */, 114 | 3AB33F841E20E98C0039F711 /* Model */, 115 | 3AB33F7D1E1FDA890039F711 /* Client */, 116 | 3AB33F791E1F9E4E0039F711 /* Extensions */, 117 | 3AB33F721E1F9B650039F711 /* Controllers */, 118 | 3AB33F5D1E1F94530039F711 /* AppDelegate.swift */, 119 | 3AB33F821E20E81E0039F711 /* Constants.swift */, 120 | 3AB33F641E1F94530039F711 /* Assets.xcassets */, 121 | 3AB33F661E1F94530039F711 /* LaunchScreen.storyboard */, 122 | 3AB33F691E1F94530039F711 /* Info.plist */, 123 | ); 124 | path = "ASDKgram-Swift"; 125 | sourceTree = ""; 126 | }; 127 | 3AB33F721E1F9B650039F711 /* Controllers */ = { 128 | isa = PBXGroup; 129 | children = ( 130 | 657909BB1F5963A200936FD3 /* ListKit */, 131 | 3AB33F741E1F9B9F0039F711 /* ASDK */, 132 | 3AB33F731E1F9B950039F711 /* UIKit */, 133 | ); 134 | name = Controllers; 135 | sourceTree = ""; 136 | }; 137 | 3AB33F731E1F9B950039F711 /* UIKit */ = { 138 | isa = PBXGroup; 139 | children = ( 140 | 3AB33F751E1F9C330039F711 /* PhotoFeedTableViewController.swift */, 141 | ); 142 | name = UIKit; 143 | sourceTree = ""; 144 | }; 145 | 3AB33F741E1F9B9F0039F711 /* ASDK */ = { 146 | isa = PBXGroup; 147 | children = ( 148 | 3AB33F771E1F9C400039F711 /* PhotoFeedTableNodeController.swift */, 149 | ); 150 | name = ASDK; 151 | sourceTree = ""; 152 | }; 153 | 3AB33F791E1F9E4E0039F711 /* Extensions */ = { 154 | isa = PBXGroup; 155 | children = ( 156 | 3AB33F7A1E1F9E630039F711 /* UIColor.swift */, 157 | 3AB33F8B1E2106F30039F711 /* URL.swift */, 158 | 3A2362FA1E2D33A0007E08F1 /* Date.swift */, 159 | 3A7A28D81E2F7410003E2B8D /* UIImage.swift */, 160 | 65537E7221B79B6000F13882 /* NumberFormatter.swift */, 161 | ); 162 | name = Extensions; 163 | sourceTree = ""; 164 | }; 165 | 3AB33F7D1E1FDA890039F711 /* Client */ = { 166 | isa = PBXGroup; 167 | children = ( 168 | 3AB33F801E1FDE100039F711 /* Webservice.swift */, 169 | 3AB33F971E22A0080039F711 /* ParsePesponse.swift */, 170 | ); 171 | name = Client; 172 | sourceTree = ""; 173 | }; 174 | 3AB33F841E20E98C0039F711 /* Model */ = { 175 | isa = PBXGroup; 176 | children = ( 177 | 3AB33F851E20E9B10039F711 /* PhotoFeedModel.swift */, 178 | 3AB33F871E20ED460039F711 /* PhotoModel.swift */, 179 | 3AB33F951E2269D40039F711 /* PopularPageModel.swift */, 180 | ); 181 | name = Model; 182 | sourceTree = ""; 183 | }; 184 | 3AB33F991E22CF160039F711 /* Views */ = { 185 | isa = PBXGroup; 186 | children = ( 187 | 3AB33F9B1E22CF3C0039F711 /* UIKit */, 188 | 3AB33F9C1E22CF5C0039F711 /* ASDK */, 189 | ); 190 | name = Views; 191 | sourceTree = ""; 192 | }; 193 | 3AB33F9B1E22CF3C0039F711 /* UIKit */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */, 197 | 3AB33FA11E230A160039F711 /* NetworkImageView.swift */, 198 | ); 199 | name = UIKit; 200 | sourceTree = ""; 201 | }; 202 | 3AB33F9C1E22CF5C0039F711 /* ASDK */ = { 203 | isa = PBXGroup; 204 | children = ( 205 | 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */, 206 | ); 207 | name = ASDK; 208 | sourceTree = ""; 209 | }; 210 | 6554D8A32D04419D00F50231 /* OrderedDictionary */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 6554D89D2D04419D00F50231 /* Dictionary+OrderedDictionary.swift */, 214 | 6554D89E2D04419D00F50231 /* OrderedDictionary.swift */, 215 | 6554D89F2D04419D00F50231 /* OrderedDictionary+Codable.swift */, 216 | 6554D8A02D04419D00F50231 /* OrderedDictionary+Deprecated.swift */, 217 | 6554D8A12D04419D00F50231 /* OrderedDictionary+Description.swift */, 218 | 6554D8A22D04419D00F50231 /* OrderedDictionarySlice.swift */, 219 | ); 220 | path = OrderedDictionary; 221 | sourceTree = ""; 222 | }; 223 | 657909BB1F5963A200936FD3 /* ListKit */ = { 224 | isa = PBXGroup; 225 | children = ( 226 | 657909BE1F59640900936FD3 /* PhotoFeedListKitViewController.swift */, 227 | 6570DDF01F6F954600369D3E /* ASCollectionSectionController.swift */, 228 | 6570DDF61F6FC00500369D3E /* PhotoFeedSectionController.swift */, 229 | 6570DDFA1F70BB5800369D3E /* Diffable.swift */, 230 | ); 231 | name = ListKit; 232 | sourceTree = ""; 233 | }; 234 | 78A64CA59A49BE1637214DF1 /* Pods */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | 019E984FADA258377FC6B2D8 /* Pods-ASDKgram-Swift.debug.xcconfig */, 238 | A3A86E74A8C3F06D7688AACB /* Pods-ASDKgram-Swift.release.xcconfig */, 239 | ); 240 | name = Pods; 241 | sourceTree = ""; 242 | }; 243 | A7DD645D70CF34C7CA3B1A8B /* Frameworks */ = { 244 | isa = PBXGroup; 245 | children = ( 246 | 4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */, 247 | ); 248 | name = Frameworks; 249 | sourceTree = ""; 250 | }; 251 | /* End PBXGroup section */ 252 | 253 | /* Begin PBXNativeTarget section */ 254 | 3AB33F591E1F94520039F711 /* ASDKgram-Swift */ = { 255 | isa = PBXNativeTarget; 256 | buildConfigurationList = 3AB33F6C1E1F94530039F711 /* Build configuration list for PBXNativeTarget "ASDKgram-Swift" */; 257 | buildPhases = ( 258 | A5A729883237749EE5D2DB1C /* [CP] Check Pods Manifest.lock */, 259 | 3AB33F561E1F94520039F711 /* Sources */, 260 | 3AB33F571E1F94520039F711 /* Frameworks */, 261 | 3AB33F581E1F94520039F711 /* Resources */, 262 | 154783123A953C3AFB9805CF /* [CP] Embed Pods Frameworks */, 263 | 3A7BEDD71E254278005769D4 /* ShellScript */, 264 | ); 265 | buildRules = ( 266 | ); 267 | dependencies = ( 268 | ); 269 | name = "ASDKgram-Swift"; 270 | productName = "ASDKgram-Swift"; 271 | productReference = 3AB33F5A1E1F94520039F711 /* ASDKgram-Swift.app */; 272 | productType = "com.apple.product-type.application"; 273 | }; 274 | /* End PBXNativeTarget section */ 275 | 276 | /* Begin PBXProject section */ 277 | 3AB33F521E1F94520039F711 /* Project object */ = { 278 | isa = PBXProject; 279 | attributes = { 280 | BuildIndependentTargetsInParallel = YES; 281 | LastSwiftUpdateCheck = 0820; 282 | LastUpgradeCheck = 1610; 283 | ORGANIZATIONNAME = Daniel; 284 | TargetAttributes = { 285 | 3AB33F591E1F94520039F711 = { 286 | CreatedOnToolsVersion = 8.2; 287 | DevelopmentTeam = B3H446T9U7; 288 | LastSwiftMigration = 0900; 289 | ProvisioningStyle = Automatic; 290 | }; 291 | }; 292 | }; 293 | buildConfigurationList = 3AB33F551E1F94520039F711 /* Build configuration list for PBXProject "ASDKgram-Swift" */; 294 | compatibilityVersion = "Xcode 3.2"; 295 | developmentRegion = English; 296 | hasScannedForEncodings = 0; 297 | knownRegions = ( 298 | English, 299 | en, 300 | Base, 301 | ); 302 | mainGroup = 3AB33F511E1F94520039F711; 303 | productRefGroup = 3AB33F5B1E1F94520039F711 /* Products */; 304 | projectDirPath = ""; 305 | projectRoot = ""; 306 | targets = ( 307 | 3AB33F591E1F94520039F711 /* ASDKgram-Swift */, 308 | ); 309 | }; 310 | /* End PBXProject section */ 311 | 312 | /* Begin PBXResourcesBuildPhase section */ 313 | 3AB33F581E1F94520039F711 /* Resources */ = { 314 | isa = PBXResourcesBuildPhase; 315 | buildActionMask = 2147483647; 316 | files = ( 317 | 3AB33F681E1F94530039F711 /* LaunchScreen.storyboard in Resources */, 318 | 3AB33F651E1F94530039F711 /* Assets.xcassets in Resources */, 319 | ); 320 | runOnlyForDeploymentPostprocessing = 0; 321 | }; 322 | /* End PBXResourcesBuildPhase section */ 323 | 324 | /* Begin PBXShellScriptBuildPhase section */ 325 | 154783123A953C3AFB9805CF /* [CP] Embed Pods Frameworks */ = { 326 | isa = PBXShellScriptBuildPhase; 327 | buildActionMask = 2147483647; 328 | files = ( 329 | ); 330 | inputPaths = ( 331 | "${PODS_ROOT}/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift-frameworks.sh", 332 | "${BUILT_PRODUCTS_DIR}/IGListDiffKit/IGListDiffKit.framework", 333 | "${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework", 334 | "${BUILT_PRODUCTS_DIR}/Texture/AsyncDisplayKit.framework", 335 | ); 336 | name = "[CP] Embed Pods Frameworks"; 337 | outputPaths = ( 338 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListDiffKit.framework", 339 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListKit.framework", 340 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/AsyncDisplayKit.framework", 341 | ); 342 | runOnlyForDeploymentPostprocessing = 0; 343 | shellPath = /bin/sh; 344 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift-frameworks.sh\"\n"; 345 | showEnvVarsInLog = 0; 346 | }; 347 | 3A7BEDD71E254278005769D4 /* ShellScript */ = { 348 | isa = PBXShellScriptBuildPhase; 349 | buildActionMask = 2147483647; 350 | files = ( 351 | ); 352 | inputPaths = ( 353 | ); 354 | outputPaths = ( 355 | ); 356 | runOnlyForDeploymentPostprocessing = 0; 357 | shellPath = /bin/sh; 358 | shellScript = ""; 359 | }; 360 | A5A729883237749EE5D2DB1C /* [CP] Check Pods Manifest.lock */ = { 361 | isa = PBXShellScriptBuildPhase; 362 | buildActionMask = 2147483647; 363 | files = ( 364 | ); 365 | inputPaths = ( 366 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 367 | "${PODS_ROOT}/Manifest.lock", 368 | ); 369 | name = "[CP] Check Pods Manifest.lock"; 370 | outputPaths = ( 371 | "$(DERIVED_FILE_DIR)/Pods-ASDKgram-Swift-checkManifestLockResult.txt", 372 | ); 373 | runOnlyForDeploymentPostprocessing = 0; 374 | shellPath = /bin/sh; 375 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 376 | showEnvVarsInLog = 0; 377 | }; 378 | /* End PBXShellScriptBuildPhase section */ 379 | 380 | /* Begin PBXSourcesBuildPhase section */ 381 | 3AB33F561E1F94520039F711 /* Sources */ = { 382 | isa = PBXSourcesBuildPhase; 383 | buildActionMask = 2147483647; 384 | files = ( 385 | 6554D8A42D04419D00F50231 /* OrderedDictionarySlice.swift in Sources */, 386 | 6554D8A52D04419D00F50231 /* OrderedDictionary+Description.swift in Sources */, 387 | 6554D8A62D04419D00F50231 /* OrderedDictionary+Codable.swift in Sources */, 388 | 6554D8A72D04419D00F50231 /* OrderedDictionary+Deprecated.swift in Sources */, 389 | 6554D8A82D04419D00F50231 /* OrderedDictionary.swift in Sources */, 390 | 6554D8A92D04419D00F50231 /* Dictionary+OrderedDictionary.swift in Sources */, 391 | 3AB33F781E1F9C400039F711 /* PhotoFeedTableNodeController.swift in Sources */, 392 | 6570DDF11F6F954600369D3E /* ASCollectionSectionController.swift in Sources */, 393 | 3A2362FB1E2D33A0007E08F1 /* Date.swift in Sources */, 394 | 3AB33F7B1E1F9E630039F711 /* UIColor.swift in Sources */, 395 | 3AB33F981E22A0080039F711 /* ParsePesponse.swift in Sources */, 396 | 3AB33FA41E2337850039F711 /* PhotoTableNodeCell.swift in Sources */, 397 | 657909BF1F59640900936FD3 /* PhotoFeedListKitViewController.swift in Sources */, 398 | 3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */, 399 | 3AB33F8C1E2106F30039F711 /* URL.swift in Sources */, 400 | 3AB33F831E20E81E0039F711 /* Constants.swift in Sources */, 401 | 6570DDFB1F70BB5800369D3E /* Diffable.swift in Sources */, 402 | 65537E7321B79B6000F13882 /* NumberFormatter.swift in Sources */, 403 | 3AB33F961E2269D40039F711 /* PopularPageModel.swift in Sources */, 404 | 3A7A28D91E2F7410003E2B8D /* UIImage.swift in Sources */, 405 | 3AB33F5E1E1F94530039F711 /* AppDelegate.swift in Sources */, 406 | 3AB33F811E1FDE100039F711 /* Webservice.swift in Sources */, 407 | 6570DDF71F6FC00500369D3E /* PhotoFeedSectionController.swift in Sources */, 408 | 3AB33F9E1E22D9DB0039F711 /* PhotoTableViewCell.swift in Sources */, 409 | 3AB33F861E20E9B10039F711 /* PhotoFeedModel.swift in Sources */, 410 | 3AB33F881E20ED460039F711 /* PhotoModel.swift in Sources */, 411 | 3AB33F761E1F9C330039F711 /* PhotoFeedTableViewController.swift in Sources */, 412 | ); 413 | runOnlyForDeploymentPostprocessing = 0; 414 | }; 415 | /* End PBXSourcesBuildPhase section */ 416 | 417 | /* Begin PBXVariantGroup section */ 418 | 3AB33F661E1F94530039F711 /* LaunchScreen.storyboard */ = { 419 | isa = PBXVariantGroup; 420 | children = ( 421 | 3AB33F671E1F94530039F711 /* Base */, 422 | ); 423 | name = LaunchScreen.storyboard; 424 | sourceTree = ""; 425 | }; 426 | /* End PBXVariantGroup section */ 427 | 428 | /* Begin XCBuildConfiguration section */ 429 | 3AB33F6A1E1F94530039F711 /* Debug */ = { 430 | isa = XCBuildConfiguration; 431 | buildSettings = { 432 | ALWAYS_SEARCH_USER_PATHS = NO; 433 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 434 | CLANG_ANALYZER_NONNULL = YES; 435 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 436 | CLANG_CXX_LIBRARY = "libc++"; 437 | CLANG_ENABLE_MODULES = YES; 438 | CLANG_ENABLE_OBJC_ARC = YES; 439 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 440 | CLANG_WARN_BOOL_CONVERSION = YES; 441 | CLANG_WARN_COMMA = YES; 442 | CLANG_WARN_CONSTANT_CONVERSION = YES; 443 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 444 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 445 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 446 | CLANG_WARN_EMPTY_BODY = YES; 447 | CLANG_WARN_ENUM_CONVERSION = YES; 448 | CLANG_WARN_INFINITE_RECURSION = YES; 449 | CLANG_WARN_INT_CONVERSION = YES; 450 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 451 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 452 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 453 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 454 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 455 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 456 | CLANG_WARN_STRICT_PROTOTYPES = YES; 457 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 458 | CLANG_WARN_UNREACHABLE_CODE = YES; 459 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 460 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 461 | COPY_PHASE_STRIP = NO; 462 | DEBUG_INFORMATION_FORMAT = dwarf; 463 | ENABLE_STRICT_OBJC_MSGSEND = YES; 464 | ENABLE_TESTABILITY = YES; 465 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 466 | GCC_C_LANGUAGE_STANDARD = gnu99; 467 | GCC_DYNAMIC_NO_PIC = NO; 468 | GCC_NO_COMMON_BLOCKS = YES; 469 | GCC_OPTIMIZATION_LEVEL = 0; 470 | GCC_PREPROCESSOR_DEFINITIONS = ( 471 | "DEBUG=1", 472 | "$(inherited)", 473 | ); 474 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 475 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 476 | GCC_WARN_UNDECLARED_SELECTOR = YES; 477 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 478 | GCC_WARN_UNUSED_FUNCTION = YES; 479 | GCC_WARN_UNUSED_VARIABLE = YES; 480 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 481 | MTL_ENABLE_DEBUG_INFO = YES; 482 | ONLY_ACTIVE_ARCH = YES; 483 | SDKROOT = iphoneos; 484 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 485 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 486 | TARGETED_DEVICE_FAMILY = "1,2"; 487 | }; 488 | name = Debug; 489 | }; 490 | 3AB33F6B1E1F94530039F711 /* Release */ = { 491 | isa = XCBuildConfiguration; 492 | buildSettings = { 493 | ALWAYS_SEARCH_USER_PATHS = NO; 494 | CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; 495 | CLANG_ANALYZER_NONNULL = YES; 496 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 497 | CLANG_CXX_LIBRARY = "libc++"; 498 | CLANG_ENABLE_MODULES = YES; 499 | CLANG_ENABLE_OBJC_ARC = YES; 500 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 501 | CLANG_WARN_BOOL_CONVERSION = YES; 502 | CLANG_WARN_COMMA = YES; 503 | CLANG_WARN_CONSTANT_CONVERSION = YES; 504 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 505 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 506 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 507 | CLANG_WARN_EMPTY_BODY = YES; 508 | CLANG_WARN_ENUM_CONVERSION = YES; 509 | CLANG_WARN_INFINITE_RECURSION = YES; 510 | CLANG_WARN_INT_CONVERSION = YES; 511 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 512 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 513 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 514 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 515 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 516 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 517 | CLANG_WARN_STRICT_PROTOTYPES = YES; 518 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 519 | CLANG_WARN_UNREACHABLE_CODE = YES; 520 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 521 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 522 | COPY_PHASE_STRIP = NO; 523 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 524 | ENABLE_NS_ASSERTIONS = NO; 525 | ENABLE_STRICT_OBJC_MSGSEND = YES; 526 | ENABLE_USER_SCRIPT_SANDBOXING = YES; 527 | GCC_C_LANGUAGE_STANDARD = gnu99; 528 | GCC_NO_COMMON_BLOCKS = YES; 529 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 530 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 531 | GCC_WARN_UNDECLARED_SELECTOR = YES; 532 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 533 | GCC_WARN_UNUSED_FUNCTION = YES; 534 | GCC_WARN_UNUSED_VARIABLE = YES; 535 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 536 | MTL_ENABLE_DEBUG_INFO = NO; 537 | SDKROOT = iphoneos; 538 | SWIFT_COMPILATION_MODE = wholemodule; 539 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 540 | TARGETED_DEVICE_FAMILY = "1,2"; 541 | VALIDATE_PRODUCT = YES; 542 | }; 543 | name = Release; 544 | }; 545 | 3AB33F6D1E1F94530039F711 /* Debug */ = { 546 | isa = XCBuildConfiguration; 547 | baseConfigurationReference = 019E984FADA258377FC6B2D8 /* Pods-ASDKgram-Swift.debug.xcconfig */; 548 | buildSettings = { 549 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 550 | CLANG_ENABLE_MODULES = YES; 551 | DEVELOPMENT_TEAM = B3H446T9U7; 552 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 553 | INFOPLIST_FILE = "ASDKgram-Swift/Info.plist"; 554 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 555 | LD_RUNPATH_SEARCH_PATHS = ( 556 | "$(inherited)", 557 | "@executable_path/Frameworks", 558 | ); 559 | PRODUCT_BUNDLE_IDENTIFIER = "com.RenaldoMoon.ASDKgram-Swift"; 560 | PRODUCT_NAME = "$(TARGET_NAME)"; 561 | SWIFT_INSTALL_OBJC_HEADER = NO; 562 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 563 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 564 | SWIFT_VERSION = 5.0; 565 | }; 566 | name = Debug; 567 | }; 568 | 3AB33F6E1E1F94530039F711 /* Release */ = { 569 | isa = XCBuildConfiguration; 570 | baseConfigurationReference = A3A86E74A8C3F06D7688AACB /* Pods-ASDKgram-Swift.release.xcconfig */; 571 | buildSettings = { 572 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 573 | CLANG_ENABLE_MODULES = YES; 574 | DEVELOPMENT_TEAM = B3H446T9U7; 575 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 576 | INFOPLIST_FILE = "ASDKgram-Swift/Info.plist"; 577 | IPHONEOS_DEPLOYMENT_TARGET = 12.0; 578 | LD_RUNPATH_SEARCH_PATHS = ( 579 | "$(inherited)", 580 | "@executable_path/Frameworks", 581 | ); 582 | PRODUCT_BUNDLE_IDENTIFIER = "com.RenaldoMoon.ASDKgram-Swift"; 583 | PRODUCT_NAME = "$(TARGET_NAME)"; 584 | SWIFT_INSTALL_OBJC_HEADER = NO; 585 | SWIFT_SWIFT3_OBJC_INFERENCE = Default; 586 | SWIFT_VERSION = 5.0; 587 | }; 588 | name = Release; 589 | }; 590 | /* End XCBuildConfiguration section */ 591 | 592 | /* Begin XCConfigurationList section */ 593 | 3AB33F551E1F94520039F711 /* Build configuration list for PBXProject "ASDKgram-Swift" */ = { 594 | isa = XCConfigurationList; 595 | buildConfigurations = ( 596 | 3AB33F6A1E1F94530039F711 /* Debug */, 597 | 3AB33F6B1E1F94530039F711 /* Release */, 598 | ); 599 | defaultConfigurationIsVisible = 0; 600 | defaultConfigurationName = Release; 601 | }; 602 | 3AB33F6C1E1F94530039F711 /* Build configuration list for PBXNativeTarget "ASDKgram-Swift" */ = { 603 | isa = XCConfigurationList; 604 | buildConfigurations = ( 605 | 3AB33F6D1E1F94530039F711 /* Debug */, 606 | 3AB33F6E1E1F94530039F711 /* Release */, 607 | ); 608 | defaultConfigurationIsVisible = 0; 609 | defaultConfigurationName = Release; 610 | }; 611 | /* End XCConfigurationList section */ 612 | }; 613 | rootObject = 3AB33F521E1F94520039F711 /* Project object */; 614 | } 615 | -------------------------------------------------------------------------------- /ASDKgram-Swift/ASCollectionSectionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ASCollectionSectionController.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Daniel on 2017/9/18. 6 | // Copyright © 2017年 Calum Harris. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import IGListKit 11 | 12 | class ASCollectionSectionController: ListSectionController { 13 | fileprivate var diffingQueue: DispatchQueue { 14 | let queue = DispatchQueue(label: "ASCollectionSectionController.diffingQueue") 15 | return queue 16 | } 17 | 18 | var initialItemsRead: Bool = false 19 | 20 | var items: [PhotoModel] = [PhotoModel]() 21 | 22 | fileprivate var pendingItmes: [PhotoModel] = [PhotoModel]() 23 | 24 | override func numberOfItems() -> Int { 25 | if !initialItemsRead { 26 | pendingItmes = items 27 | initialItemsRead = true 28 | } 29 | return items.count; 30 | } 31 | 32 | func setItmes(newItems: [PhotoModel], animated: Bool, completion: (() -> ())? = nil) { 33 | if !initialItemsRead { 34 | items = newItems 35 | guard let completion = completion else { return } 36 | completion() 37 | } 38 | 39 | let wasEmpty = self.items.count == 0 40 | 41 | self.diffingQueue.async { 42 | let result = DiffUtility.diff(originalItems: self.pendingItmes, newItems: newItems) 43 | self.pendingItmes = newItems 44 | DispatchQueue.main.async { 45 | if let ctx = self.collectionContext { 46 | ctx.performBatch(animated: animated, updates: { bactchContext in 47 | bactchContext.insert(in: self, at: result.inserts) 48 | bactchContext.delete(in: self, at: result.deletes) 49 | self.items = newItems; 50 | }, completion: { (finished) in 51 | if let completion = completion { 52 | completion() 53 | } 54 | 55 | if wasEmpty { 56 | let adapter = ctx as! ListAdapter 57 | adapter.performUpdates(animated: false, completion: nil) 58 | } 59 | 60 | }) 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /ASDKgram-Swift/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 06/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | 20 | import UIKit 21 | import AsyncDisplayKit 22 | 23 | @UIApplicationMain 24 | class AppDelegate: UIResponder, UIApplicationDelegate { 25 | 26 | var window: UIWindow? 27 | 28 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 29 | 30 | // UIKit Home Feed viewController & navController 31 | 32 | let UIKitNavController = UINavigationController(rootViewController: PhotoFeedTableViewController()) 33 | UIKitNavController.tabBarItem.title = "UIKit" 34 | 35 | // ASDK Home Feed viewController & navController 36 | 37 | let ASDKNavController = UINavigationController(rootViewController: PhotoFeedTableNodeController()) 38 | ASDKNavController.tabBarItem.title = "ASDK" 39 | 40 | let ListKitNavController = UINavigationController(rootViewController: PhotoFeedListKitViewController()) 41 | ListKitNavController.tabBarItem.title = "ASDK-IGListKit" 42 | // UITabBarController 43 | 44 | let tabBarController = UITabBarController() 45 | tabBarController.viewControllers = [UIKitNavController, ASDKNavController, ListKitNavController] 46 | tabBarController.selectedIndex = 0 47 | tabBarController.tabBar.tintColor = UIColor.mainBarTintColor 48 | 49 | // Nav Bar appearance 50 | 51 | UINavigationBar.appearance().barTintColor = UIColor.mainBarTintColor 52 | 53 | // UIWindow 54 | 55 | window = UIWindow() 56 | window?.backgroundColor = .white 57 | window?.rootViewController = tabBarController 58 | window?.makeKeyAndVisible() 59 | 60 | return true 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /ASDKgram-Swift/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "20x20", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "20x20", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "29x29", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "29x29", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "40x40", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "40x40", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "size" : "60x60", 36 | "scale" : "2x" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "size" : "60x60", 41 | "scale" : "3x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "20x20", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "20x20", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "29x29", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "29x29", 61 | "scale" : "2x" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "size" : "40x40", 66 | "scale" : "1x" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "size" : "40x40", 71 | "scale" : "2x" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "size" : "76x76", 76 | "scale" : "1x" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "size" : "76x76", 81 | "scale" : "2x" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "size" : "83.5x83.5", 86 | "scale" : "2x" 87 | } 88 | ], 89 | "info" : { 90 | "version" : 1, 91 | "author" : "xcode" 92 | } 93 | } -------------------------------------------------------------------------------- /ASDKgram-Swift/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 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ASDKgram-Swift/Base.lproj/Main.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 | 27 | -------------------------------------------------------------------------------- /ASDKgram-Swift/Constants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Constants 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 07/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | // swiftlint:disable nesting 20 | 21 | import UIKit 22 | 23 | struct Constants { 24 | 25 | struct Unsplash { 26 | struct URLS { 27 | static let Host = "https://api.unsplash.com/" 28 | static let PopularEndpoint = "photos?order_by=popular" 29 | static let SearchEndpoint = "photos/search?geo=" //latitude,longitude,radius 30 | static let UserEndpoint = "photos?user_id=" 31 | static let ConsumerKey = "&client_id=3b99a69cee09770a4a0bbb870b437dbda53efb22f6f6de63714b71c4df7c9642" 32 | static let ImagesPerPage = 30 33 | } 34 | } 35 | 36 | struct CellLayout { 37 | static let FontSize: CGFloat = 14 38 | static let HeaderHeight: CGFloat = 50 39 | static let UserImageHeight: CGFloat = 30 40 | static let HorizontalBuffer: CGFloat = 10 41 | static let VerticalBuffer: CGFloat = 5 42 | static let InsetForAvatar = UIEdgeInsets(top: HorizontalBuffer, left: 0, bottom: HorizontalBuffer, right: HorizontalBuffer) 43 | static let InsetForHeader = UIEdgeInsets(top: 0, left: HorizontalBuffer, bottom: 0, right: HorizontalBuffer) 44 | static let InsetForFooter = UIEdgeInsets(top: VerticalBuffer, left: HorizontalBuffer, bottom: VerticalBuffer, right: HorizontalBuffer) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /ASDKgram-Swift/Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 16/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | 20 | import Foundation 21 | 22 | extension Date { 23 | 24 | static let iso8601Formatter: DateFormatter = { 25 | let formatter = DateFormatter() 26 | formatter.calendar = Calendar(identifier: .iso8601) 27 | formatter.locale = Locale(identifier: "en_US_POSIX") 28 | formatter.timeZone = TimeZone(secondsFromGMT: 0) 29 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 30 | return formatter 31 | }() 32 | 33 | static func timeStringSince(fromConverted date: Date) -> String { 34 | let diffDates = NSCalendar.current.dateComponents([.day, .hour, .second], from: date, to:Date()) 35 | if let week = diffDates.day, week > 7 { 36 | return "\(week / 7)w" 37 | } else if let day = diffDates.day, day > 0 { 38 | return "\(day)d" 39 | } else if let hour = diffDates.hour, hour > 0 { 40 | return "\(hour)h" 41 | } else if let second = diffDates.second, second > 0 { 42 | return "\(second)s" 43 | } else if let zero = diffDates.second, zero == 0 { 44 | return "1s" 45 | } else { 46 | return "ERROR" 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /ASDKgram-Swift/Diffable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Diffable.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Daniel on 2017/9/19. 6 | // Copyright © 2017年 Calum Harris. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import IGListKit 11 | 12 | public protocol Diffable: Equatable { 13 | 14 | var diffIdentifier: String { get } 15 | } 16 | 17 | public struct DiffUtility { 18 | public struct DiffResult { 19 | public typealias Move = (from: Int, to: Int) 20 | public var inserts: IndexSet 21 | public var deletes: IndexSet 22 | public var updates: IndexSet 23 | public let moves: [Move] 24 | 25 | public let oldIndexForID: (_ id: String) -> Int 26 | public let newIndexForID: (_ id: String) -> Int 27 | } 28 | 29 | public static func diff(originalItems: [T], newItems: [T]) -> DiffResult { 30 | let old = originalItems.map({ DiffableBox(value: $0, identifier: $0.diffIdentifier as NSObjectProtocol, equal: ==) }) 31 | let new = newItems.map({ DiffableBox(value: $0, identifier: $0.diffIdentifier as NSObjectProtocol, equal: ==) }) 32 | let result = ListDiff(oldArray: old, newArray: new, option: .equality) 33 | 34 | let inserts = result.inserts 35 | let deletes = result.deletes 36 | let updates = result.updates 37 | 38 | let moves: [DiffResult.Move] = result.moves.map({ (from: $0.from, to: $0.to) }) 39 | 40 | let oldIndexForID: (_ id: String) -> Int = { id in 41 | return result.oldIndex(forIdentifier: NSString(string: id)) 42 | } 43 | let newIndexForID: (_ id: String) -> Int = { id in 44 | return result.newIndex(forIdentifier: NSString(string: id)) 45 | } 46 | return DiffResult(inserts: inserts, deletes: deletes, updates: updates, moves: moves, oldIndexForID: oldIndexForID, newIndexForID: newIndexForID) 47 | } 48 | 49 | public final class DiffableBox: ListDiffable { 50 | 51 | let value: T 52 | let identifier: NSObjectProtocol 53 | let equal: (T, T) -> Bool 54 | 55 | init(value: T, identifier: NSObjectProtocol, equal: @escaping(T, T) -> Bool) { 56 | self.value = value 57 | self.identifier = identifier 58 | self.equal = equal 59 | } 60 | 61 | // IGListDiffable 62 | 63 | public func diffIdentifier() -> NSObjectProtocol { 64 | return identifier 65 | } 66 | 67 | public func isEqual(toDiffableObject object: ListDiffable?) -> Bool { 68 | if let other = object as? DiffableBox { 69 | return equal(value, other.value) 70 | } 71 | return false 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /ASDKgram-Swift/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /ASDKgram-Swift/NetworkImageView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NetworkImageView.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 09/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | 20 | import UIKit 21 | 22 | let imageCache = NSCache() 23 | 24 | class NetworkImageView: UIImageView { 25 | 26 | var imageUrlString: String? 27 | 28 | func loadImageUsingUrlString(urlString: String) { 29 | 30 | imageUrlString = urlString 31 | 32 | let url = URL(string: urlString) 33 | 34 | image = nil 35 | 36 | if let imageFromCache = imageCache.object(forKey: urlString as NSString) { 37 | self.image = imageFromCache 38 | return 39 | } 40 | 41 | URLSession.shared.dataTask(with: url!, completionHandler: { (data, respones, error) in 42 | 43 | if error != nil { 44 | print(error!) 45 | return 46 | } 47 | 48 | DispatchQueue.main.async { 49 | let imageToCache = UIImage(data: data!) 50 | if self.imageUrlString == urlString { 51 | self.image = imageToCache 52 | } 53 | imageCache.setObject(imageToCache!, forKey: urlString as NSString) 54 | } 55 | }).resume() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ASDKgram-Swift/NumberFormatter.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NumberFormatter.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Daniel on 2018/12/5. 6 | // Copyright © 2018 Daniel. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension NumberFormatter { 12 | static let decimalNumberFormatter: NumberFormatter = { 13 | let formatter = NumberFormatter() 14 | formatter.numberStyle = .decimal 15 | return formatter 16 | }() 17 | } 18 | -------------------------------------------------------------------------------- /ASDKgram-Swift/OrderedDictionary/Dictionary+OrderedDictionary.swift: -------------------------------------------------------------------------------- 1 | extension Dictionary { 2 | 3 | /// Returns an ordered dictionary containing the key-value pairs from the dictionary, sorted 4 | /// using the given sort function. 5 | /// 6 | /// - Parameters: 7 | /// - areInIncreasingOrder: The sort function which compares the key-value pairs. 8 | /// - Returns: The ordered dictionary. 9 | /// 10 | /// - SeeAlso: `OrderedDictionary.init(unsorted:areInIncreasingOrder:)` 11 | public func sorted( 12 | by areInIncreasingOrder: (Element, Element) throws -> Bool 13 | ) rethrows -> OrderedDictionary { 14 | return try OrderedDictionary( 15 | unsorted: self, 16 | areInIncreasingOrder: areInIncreasingOrder 17 | ) 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Codable.swift: -------------------------------------------------------------------------------- 1 | extension OrderedDictionary: Encodable where Key: Encodable, Value: Encodable { 2 | 3 | /// Encodes the contents of this ordered dictionary into the given encoder. 4 | public func encode(to encoder: Encoder) throws { 5 | // Encode the ordered dictionary as an array of alternating key-value pairs. 6 | var container = encoder.unkeyedContainer() 7 | 8 | for (key, value) in self { 9 | try container.encode(key) 10 | try container.encode(value) 11 | } 12 | } 13 | 14 | } 15 | 16 | extension OrderedDictionary: Decodable where Key: Decodable, Value: Decodable { 17 | 18 | /// Creates a new ordered dictionary by decoding from the given decoder. 19 | public init(from decoder: Decoder) throws { 20 | // Decode the ordered dictionary from an array of alternating key-value pairs. 21 | self.init() 22 | 23 | var container = try decoder.unkeyedContainer() 24 | 25 | while !container.isAtEnd { 26 | let key = try container.decode(Key.self) 27 | guard !container.isAtEnd else { throw DecodingError.unkeyedContainerReachedEndBeforeValue(decoder.codingPath) } 28 | let value = try container.decode(Value.self) 29 | 30 | self[key] = value 31 | } 32 | } 33 | 34 | } 35 | 36 | extension DecodingError { 37 | 38 | fileprivate static func unkeyedContainerReachedEndBeforeValue( 39 | _ codingPath: [CodingKey] 40 | ) -> DecodingError { 41 | return DecodingError.dataCorrupted( 42 | DecodingError.Context( 43 | codingPath: codingPath, 44 | debugDescription: "Unkeyed container reached end before value in key-value pair." 45 | ) 46 | ) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Deprecated.swift: -------------------------------------------------------------------------------- 1 | extension OrderedDictionary { 2 | 3 | // ============================================================================ // 4 | // MARK: - Initialization 5 | // ============================================================================ // 6 | 7 | @available(*, deprecated, message: "Please use init(values:uniquelyKeyedBy:).", renamed: "init(values:uniquelyKeyedBy:)") 8 | public init( 9 | values: S, 10 | keyedBy extractKey: (Value) -> Key 11 | ) where S.Element == Value { 12 | self.init(values: values, uniquelyKeyedBy: extractKey) 13 | } 14 | 15 | @available(*, deprecated, message: "Please use init(values:uniquelyKeyedBy:).", renamed: "init(values:uniquelyKeyedBy:)") 16 | public init( 17 | values: [Value], 18 | keyedBy keyPath: KeyPath 19 | ) { 20 | self.init(values: values, uniquelyKeyedBy: keyPath) 21 | } 22 | 23 | @available(*, deprecated, message: "Please use init(uniqueKeysWithValues:).", renamed: "init(uniqueKeysWithValues:)") 24 | public init(_ elements: S) where S.Element == Element { 25 | self.init(uniqueKeysWithValues: elements) 26 | } 27 | 28 | // ============================================================================ // 29 | // MARK: - Insertion Checks 30 | // ============================================================================ // 31 | 32 | /// Checks whether the given key-value pair can be inserted into to ordered dictionary 33 | /// by validating the presence of the key. 34 | /// 35 | /// - Parameters: 36 | /// - newElement: The key-value pair to be inserted into the ordered dictionary. 37 | /// - Returns: `true` if the key-value pair can be safely inserted; otherwise, `false`. 38 | /// 39 | /// - SeeAlso: `canInsert(key:)` 40 | /// - SeeAlso: `canInsert(at:)` 41 | @available(*, deprecated, message: "Use canInsert(key:) with the element's key instead.") 42 | public func canInsert(_ newElement: Element) -> Bool { 43 | return canInsert(key: newElement.key) 44 | } 45 | 46 | // ============================================================================ // 47 | // MARK: - Moving Elements 48 | // ============================================================================ // 49 | 50 | /// Moves an existing key-value pair specified by the given key to the new index by removing 51 | /// it from its original index first and inserting it at the new index. If the movement is 52 | /// actually performed, the previous index of the key-value pair is returned. Otherwise, `nil` 53 | /// is returned. 54 | /// 55 | /// - Parameters: 56 | /// - key: The key specifying the key-value pair to move. 57 | /// - newIndex: The new index the key-value pair should be moved to. 58 | /// - Returns: The previous index of the key-value pair if it was sucessfully moved. 59 | @available(*, deprecated, message: "Since the concrete behavior of the element movement highly depends on concrete use cases, its official support will be dropped in the future. Please use the public API for modeling a move operation instead.") 60 | @discardableResult 61 | public mutating func moveElement(forKey key: Key, to newIndex: Index) -> Index? { 62 | // Load the previous index and return nil if the index is not found. 63 | guard let previousIndex = index(forKey: key) else { return nil } 64 | 65 | // If the previous and new indices match, treat it as if the movement was already 66 | // performed. 67 | guard previousIndex != newIndex else { return previousIndex } 68 | 69 | // Remove the value for the key at its original index. 70 | let value = removeValue(forKey: key)! 71 | 72 | // Validate the new index. 73 | precondition(canInsert(at: newIndex), "Cannot move to invalid index in OrderedDictionary") 74 | 75 | // Insert the element at the new index. 76 | insert((key: key, value: value), at: newIndex) 77 | 78 | return previousIndex 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Description.swift: -------------------------------------------------------------------------------- 1 | extension OrderedDictionary: CustomStringConvertible { 2 | 3 | /// A textual representation of the ordered dictionary. 4 | public var description: String { 5 | return makeDescription(debug: false) 6 | } 7 | 8 | } 9 | 10 | extension OrderedDictionary: CustomDebugStringConvertible { 11 | 12 | /// A textual representation of the ordered dictionary, suitable for debugging. 13 | public var debugDescription: String { 14 | return makeDescription(debug: true) 15 | } 16 | 17 | } 18 | 19 | extension OrderedDictionary { 20 | 21 | fileprivate func makeDescription(debug: Bool) -> String { 22 | // The implementation of the description is inspired by zwaldowski's implementation of the 23 | // ordered dictionary. See https://bit.ly/2RiWfJu 24 | 25 | if isEmpty { return "[:]" } 26 | 27 | let printFunction: (Any, inout String) -> () = { 28 | if debug { 29 | return { debugPrint($0, separator: "", terminator: "", to: &$1) } 30 | } else { 31 | return { print($0, separator: "", terminator: "", to: &$1) } 32 | } 33 | }() 34 | 35 | let descriptionForItem: (Any) -> String = { item in 36 | var description = "" 37 | printFunction(item, &description) 38 | return description 39 | } 40 | 41 | let bodyComponents = map { element in 42 | return descriptionForItem(element.key) + ": " + descriptionForItem(element.value) 43 | } 44 | 45 | let body = bodyComponents.joined(separator: ", ") 46 | 47 | return "[\(body)]" 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /ASDKgram-Swift/OrderedDictionary/OrderedDictionary.swift: -------------------------------------------------------------------------------- 1 | /// A generic collection for storing key-value pairs in an ordered manner. 2 | /// 3 | /// See the following example for a brief showcase including the initialization from a dictionary 4 | /// literal as well as iteration over its sorted key-value pairs: 5 | /// 6 | /// let orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 7 | /// 8 | /// print(orderedDictionary) 9 | /// // => ["a": 1, "b": 2, "c": 3] 10 | /// 11 | /// for (key, value) in orderedDictionary { 12 | /// print("key=\(key), value=\(value)") 13 | /// } 14 | /// // => key="a", value=1 15 | /// // => key="b", value=2 16 | /// // => key="c", value=3 17 | /// 18 | /// for (index, element) in orderedDictionary.enumerated() { 19 | /// print("index=\(index), element=\(element)") 20 | /// } 21 | /// // => index=0, element=(key: "a", value: 1) 22 | /// // => index=1, element=(key: "b", value: 2) 23 | /// // => index=2, element=(key: "c", value: 3) 24 | public struct OrderedDictionary: RandomAccessCollection, MutableCollection { 25 | 26 | // ============================================================================ // 27 | // MARK: - Type Aliases 28 | // ============================================================================ // 29 | 30 | /// The type of the key-value pair stored in the ordered dictionary. 31 | public typealias Element = (key: Key, value: Value) 32 | 33 | /// The type of the index. 34 | public typealias Index = Int 35 | 36 | /// The type of the contiguous subrange of the ordered dictionary's elements. 37 | public typealias SubSequence = OrderedDictionarySlice 38 | 39 | /// The type of the lazily evaluated collection of the ordered dictionary's values. 40 | public typealias LazyValues = LazyMapCollection 41 | 42 | // ============================================================================ // 43 | // MARK: - Initialization 44 | // ============================================================================ // 45 | 46 | /// Initializes an empty ordered dictionary. 47 | public init() { 48 | self.init( 49 | uniqueKeysWithValues: EmptyCollection(), 50 | minimumCapacity: nil 51 | ) 52 | } 53 | 54 | /// Initializes an empty ordered dictionary with preallocated space for at least 55 | /// the specified number of elements. 56 | public init(minimumCapacity: Int) { 57 | self.init( 58 | uniqueKeysWithValues: EmptyCollection(), 59 | minimumCapacity: minimumCapacity 60 | ) 61 | } 62 | 63 | /// Initializes an ordered dictionary from a regular unsorted dictionary by sorting it 64 | /// using the given sort function. 65 | /// 66 | /// - Parameters: 67 | /// - unsorted: The unsorted dictionary. 68 | /// - areInIncreasingOrder: The sort function which compares the key-value pairs. 69 | public init( 70 | unsorted: Dictionary, 71 | areInIncreasingOrder: (Element, Element) throws -> Bool 72 | ) rethrows { 73 | let keysAndValues = try Array(unsorted).sorted(by: areInIncreasingOrder) 74 | 75 | self.init( 76 | uniqueKeysWithValues: keysAndValues, 77 | minimumCapacity: unsorted.count 78 | ) 79 | } 80 | 81 | /// Initializes an ordered dictionary from a sequence of values keyed by a unique key 82 | /// extracted from the value using the given closure. 83 | /// 84 | /// - Parameters: 85 | /// - values: The sequence of values. 86 | /// - extractKey: The closure which extracts a key from the value. The returned keys must 87 | /// be unique for all values from the sequence. 88 | public init( 89 | values: S, 90 | uniquelyKeyedBy extractKey: (Value) throws -> Key 91 | ) rethrows where S.Element == Value { 92 | self.init(uniqueKeysWithValues: try values.map { value in 93 | return (try extractKey(value), value) 94 | }) 95 | } 96 | 97 | /// Initializes an ordered dictionary from a sequence of values keyed by a unique key 98 | /// extracted from the value using the given key path. 99 | /// 100 | /// - Parameters: 101 | /// - values: The sequence of values. 102 | /// - keyPath: The key path to use for extracting a key from the value. The extracted keys 103 | /// must be unique for all values from the sequence. 104 | public init( 105 | values: S, 106 | uniquelyKeyedBy keyPath: KeyPath 107 | ) where S.Element == Value { 108 | self.init(uniqueKeysWithValues: values.map { value in 109 | return (value[keyPath: keyPath], value) 110 | }) 111 | } 112 | 113 | /// Initializes an ordered dictionary from a sequence of key-value pairs. 114 | /// 115 | /// - Parameters: 116 | /// - keysAndValues: A sequence of key-value pairs to use for the new ordered dictionary. 117 | /// Every key in `keysAndValues` must be unique. 118 | public init( 119 | uniqueKeysWithValues keysAndValues: S 120 | ) where S.Element == Element { 121 | self.init( 122 | uniqueKeysWithValues: keysAndValues, 123 | minimumCapacity: keysAndValues.underestimatedCount 124 | ) 125 | } 126 | 127 | private init( 128 | uniqueKeysWithValues keysAndValues: S, 129 | minimumCapacity: Int? 130 | ) where S.Element == Element { 131 | defer { _assertInvariant() } 132 | 133 | var orderedKeys = [Key](minimumCapacity: minimumCapacity ?? 0) 134 | var keysToValues = [Key: Value](minimumCapacity: minimumCapacity ?? 0) 135 | 136 | for (key, value) in keysAndValues { 137 | precondition( 138 | keysToValues[key] == nil, 139 | "[OrderedDictionary] Sequence of key-value pairs contains duplicate keys (\(key))" 140 | ) 141 | 142 | orderedKeys.append(key) 143 | keysToValues[key] = value 144 | } 145 | 146 | self._orderedKeys = orderedKeys 147 | self._keysToValues = keysToValues 148 | } 149 | 150 | // ============================================================================ // 151 | // MARK: - Ordered Keys & Values 152 | // ============================================================================ // 153 | 154 | /// An array containing just the keys of the ordered dictionary in the correct order. 155 | /// 156 | /// The following example shows how the ordered keys can be iterated over and accessed. 157 | /// 158 | /// let orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 159 | /// 160 | /// for key in orderedDictionary.orderedKeys { 161 | /// print(key) 162 | /// } 163 | /// // => "a" 164 | /// // => "b" 165 | /// // => "c" 166 | /// 167 | /// print(orderedDictionary.orderedKeys) 168 | /// // => ["a", "b", "c"] 169 | public var orderedKeys: [Key] { 170 | return _orderedKeys 171 | } 172 | 173 | /// A lazily evaluated collection containing just the values of the ordered dictionary 174 | /// in the correct order. 175 | /// 176 | /// The following example shows how the ordered values can be iterated over and accessed. 177 | /// Note that the collection is of type `LazyValues` which wraps the `OrderedDictionary` 178 | /// as its base collection. Depending on the use case it might be desirable to convert 179 | /// the collection to an `Array` which creates a copy of the values. 180 | /// 181 | /// let orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 182 | /// 183 | /// for value in orderedDictionary.orderedValues { 184 | /// print(value) 185 | /// } 186 | /// // => 1 187 | /// // => 2 188 | /// // => 3 189 | /// 190 | /// print(Array(orderedDictionary.orderedValues)) 191 | /// // => [1, 2, 3] 192 | public var orderedValues: LazyValues { 193 | return self.lazy.map { $0.value } 194 | } 195 | 196 | // ============================================================================ // 197 | // MARK: - Unordered Dictionary 198 | // ============================================================================ // 199 | 200 | /// Converts itself to a common unsorted dictionary. 201 | public var unorderedDictionary: Dictionary { 202 | return _keysToValues 203 | } 204 | 205 | // ============================================================================ // 206 | // MARK: - Indices 207 | // ============================================================================ // 208 | 209 | /// The indices that are valid for subscripting the ordered dictionary. 210 | public var indices: CountableRange { 211 | return _orderedKeys.indices 212 | } 213 | 214 | /// The position of the first key-value pair in a non-empty ordered dictionary. 215 | public var startIndex: Index { 216 | return _orderedKeys.startIndex 217 | } 218 | 219 | /// The position which is one greater than the position of the last valid key-value pair 220 | /// in the ordered dictionary. 221 | public var endIndex: Index { 222 | return _orderedKeys.endIndex 223 | } 224 | 225 | /// Returns the position immediately after the given index. 226 | public func index(after i: Index) -> Index { 227 | return _orderedKeys.index(after: i) 228 | } 229 | 230 | /// Returns the position immediately before the given index. 231 | public func index(before i: Index) -> Index { 232 | return _orderedKeys.index(before: i) 233 | } 234 | 235 | /// Returns the index for the given key. 236 | /// 237 | /// The following example shows how to get indices for given keys: 238 | /// 239 | /// var orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 240 | /// 241 | /// print(orderedDictionary.index(forKey: "a")) 242 | /// // => Optional(0) 243 | /// 244 | /// print(orderedDictionary.index(forKey: "x")) 245 | /// // => nil 246 | /// 247 | /// - Parameters: 248 | /// - key: The key to find in the ordered dictionary. 249 | /// - Returns: The index for `key` and its associated value if `key` is in the ordered 250 | /// dictionary; otherwise, `nil`. 251 | /// 252 | /// - Complexity: O(*n*), where *n* is the length of the ordered dictionary. 253 | public func index(forKey key: Key) -> Index? { 254 | return _orderedKeys.firstIndex(of: key) 255 | } 256 | 257 | // ============================================================================ // 258 | // MARK: - Key-based Access 259 | // ============================================================================ // 260 | 261 | /// Accesses the value associated with the given key for reading and writing. 262 | /// 263 | /// This key-based subscript returns the value for the given key if the key is found in the 264 | /// ordered dictionary, or `nil` if the key is not found. 265 | /// 266 | /// When you assign a value for a key and that key already exists, the ordered dictionary 267 | /// overwrites the existing value and preservers the index of the key-value pair. If the 268 | /// ordered dictionary does not contain the key, a new key-value pair is appended to the end 269 | /// of the ordered dictionary. 270 | /// 271 | /// When you assign `nil` as the value for the given key, the ordered dictionary removes 272 | /// that key and its associated value if it exists. 273 | /// 274 | /// See the following example that shows how to access and set values for keys: 275 | /// 276 | /// var orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 277 | /// 278 | /// print(orderedDictionary["a"]) 279 | /// // => Optional(1) 280 | /// 281 | /// print(orderedDictionary["x"]) 282 | /// // => nil 283 | /// 284 | /// orderedDictionary["b"] = 42 285 | /// print(orderedDictionary["b"]) 286 | /// // => Optional(42) 287 | /// 288 | /// print(orderedDictionary) 289 | /// // => ["a": 1, "b": 42, "c": 3] 290 | /// 291 | /// orderedDictionary["d"] = 4 292 | /// print(orderedDictionary["d"]) 293 | /// // => Optional(4) 294 | /// 295 | /// print(orderedDictionary) 296 | /// // => ["a": 1, "b": 42, "c": 3, "d": 4] 297 | /// 298 | /// orderedDictionary["c"] = nil 299 | /// print(orderedDictionary["c"]) 300 | /// // => nil 301 | /// 302 | /// print(orderedDictionary) 303 | /// // => ["a": 1, "b": 42, "d": 4] 304 | /// 305 | /// - Parameters: 306 | /// - key: The key to find in the ordered dictionary. 307 | /// - Returns: The value associated with `key` if `key` is in the ordered dictionary; 308 | /// otherwise, `nil`. 309 | public subscript(key: Key) -> Value? { 310 | get { 311 | return value(forKey: key) 312 | } 313 | set(newValue) { 314 | if let newValue = newValue { 315 | updateValue(newValue, forKey: key) 316 | } else { 317 | removeValue(forKey: key) 318 | } 319 | } 320 | } 321 | 322 | /// Returns whether whether the ordered dictionary contains the given key. 323 | /// 324 | /// - Parameter key: The key to be looked up. 325 | /// - Returns: `true` if the ordered dictionary contains the given key; otherwise, `false`. 326 | /// 327 | /// - SeeAlso: `subscript(key:)` 328 | public func containsKey(_ key: Key) -> Bool { 329 | return _keysToValues[key] != nil 330 | } 331 | 332 | /// Returns the value associated with the given key if the key is found in the ordered 333 | /// dictionary, or `nil` if the key is not found. 334 | /// 335 | /// The following example shows how to access values for keys: 336 | /// 337 | /// var orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 338 | /// 339 | /// print(orderedDictionary.value(forKey: "a")) 340 | /// // => Optional(1) 341 | /// 342 | /// print(orderedDictionary.value(forKey: "x")) 343 | /// // => nil 344 | /// 345 | /// - Parameters: 346 | /// - key: The key to find in the ordered dictionary. 347 | /// - Returns: The value associated with `key` if `key` is in the ordered dictionary; 348 | /// otherwise, `nil`. 349 | /// 350 | /// - SeeAlso: `subscript(key:)` 351 | public func value(forKey key: Key) -> Value? { 352 | return _keysToValues[key] 353 | } 354 | 355 | /// Updates the value stored in the ordered dictionary for the given key, or appends a new 356 | /// key-value pair if the key does not exist. 357 | /// 358 | /// The following example shows how to update the value for an existing key: 359 | /// 360 | /// var orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 361 | /// 362 | /// let previousValue = orderedDictionary.updateValue(42, forKey: "b") 363 | /// 364 | /// print(previousValue) 365 | /// // => Optional(2) 366 | /// 367 | /// print(orderedDictionary["b"]) 368 | /// // => Optional(42) 369 | /// 370 | /// print(orderedDictionary) 371 | /// // => ["a": 1, "b": 42, "c": 3] 372 | /// 373 | /// See the second example for the case where the updated key is not yet present in 374 | /// the ordered dictionary: 375 | /// 376 | /// var orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 377 | /// 378 | /// let previousValue = orderedDictionary.updateValue(4, forKey: "d") 379 | /// 380 | /// print(previousValue) 381 | /// // => nil 382 | /// 383 | /// print(orderedDictionary["d"]) 384 | /// // => Optional(4) 385 | /// 386 | /// print(orderedDictionary) 387 | /// // => ["a": 1, "b": 2, "c": 3, "d": 4] 388 | /// 389 | /// - Parameters: 390 | /// - value: The new value to add to the ordered dictionary. 391 | /// - key: The key to associate with `value`. If `key` already exists in the ordered 392 | /// dictionary, `value` replaces the existing associated value. If `key` is not yet 393 | /// a key of the ordered dictionary, the `(key, value)` pair is appended at the end 394 | /// of the ordered dictionary. 395 | /// 396 | /// - SeeAlso: `subscript(key:)` 397 | @discardableResult 398 | public mutating func updateValue( 399 | _ value: Value, 400 | forKey key: Key 401 | ) -> Value? { 402 | defer { _assertInvariant() } 403 | 404 | if containsKey(key) { 405 | guard let currentValue = _keysToValues[key] else { 406 | fatalError("[OrderedDictionary] Inconsistency error") 407 | } 408 | 409 | _keysToValues[key] = value 410 | 411 | return currentValue 412 | } else { 413 | _orderedKeys.append(key) 414 | _keysToValues[key] = value 415 | 416 | return nil 417 | } 418 | } 419 | 420 | /// Removes the given key and its associated value from the ordered dictionary. 421 | /// 422 | /// If the key is found in the ordered dictionary, this method returns the key's associated 423 | /// value. On removal, the indices of the ordered dictionary are invalidated. If the key is 424 | /// not found in the ordered dictionary, this method returns `nil`. 425 | /// 426 | /// The following example shows how to remove a value for a key: 427 | /// 428 | /// var orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 429 | /// 430 | /// let removedValue = orderedDictionary.removeValue(forKey: "b") 431 | /// 432 | /// print(removedValue) 433 | /// // => Optional(2) 434 | /// 435 | /// print(orderedDictionary["b"]) 436 | /// // => nil 437 | /// 438 | /// print(orderedDictionary) 439 | /// // => ["a": 1, "c": 3] 440 | /// 441 | /// - Parameters: 442 | /// - key: The key to remove along with its associated value. 443 | /// - Returns: The value that was removed, or `nil` if the key was not present in the 444 | /// ordered dictionary. 445 | /// 446 | /// - SeeAlso: `subscript(key:)` 447 | /// - SeeAlso: `remove(at:)` 448 | @discardableResult 449 | public mutating func removeValue(forKey key: Key) -> Value? { 450 | guard let currentValue = _keysToValues[key] else { return nil } 451 | guard let index = index(forKey: key) else { return nil } 452 | 453 | defer { _assertInvariant() } 454 | 455 | _orderedKeys.remove(at: index) 456 | _keysToValues[key] = nil 457 | 458 | return currentValue 459 | } 460 | 461 | /// Removes all key-value pairs from the ordered dictionary and invalidates all indices. 462 | /// 463 | /// - Parameters: 464 | /// - keepCapacity: Whether the ordered dictionary should keep its underlying storage. 465 | /// If you pass `true`, the operation preserves the storage capacity that the collection 466 | /// has, otherwise the underlying storage is released. The default is `false`. 467 | public mutating func removeAll( 468 | keepingCapacity keepCapacity: Bool = false 469 | ) { 470 | defer { _assertInvariant() } 471 | 472 | _orderedKeys.removeAll(keepingCapacity: keepCapacity) 473 | _keysToValues.removeAll(keepingCapacity: keepCapacity) 474 | } 475 | 476 | // ============================================================================ // 477 | // MARK: - Index-based Access 478 | // ============================================================================ // 479 | 480 | /// Accesses the key-value pair at the specified position for reading and writing. 481 | /// 482 | /// When accessing a key-value pair the given position must be a valid index of the ordered 483 | /// dictionary. 484 | /// 485 | /// When assigning a key-value pair for a particular position, the position must be either 486 | /// a valid index of the ordered dictionary or equal to `endIndex`. Furthermore, the given 487 | /// key must not be already present at a different position of the ordered dictionary. 488 | /// However, it is safe to set a key equal to the key that is currently present at that 489 | /// position. 490 | /// 491 | /// The following example shows how to access and set key-value pairs at specific indices: 492 | /// 493 | /// var orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 494 | /// 495 | /// print(orderedDictionary[0]) 496 | /// // => (key: "a", value: 1) 497 | /// 498 | /// orderedDictionary[1] = (key: "d", value: 42) 499 | /// print(orderedDictionary[1]) 500 | /// // => (key: "d", value: 42) 501 | /// 502 | /// print(orderedDictionary) 503 | /// // => ["a": 1, "d": 42, "c": 3] 504 | /// 505 | /// orderedDictionary[0] = (key: "a", value: 5) 506 | /// print(orderedDictionary[0]) 507 | /// // => (key: "a", value: 5) 508 | /// 509 | /// print(orderedDictionary) 510 | /// // => ["a": 5, "d": 42, "c": 3] 511 | /// 512 | /// - Parameters: 513 | /// - position: The position of the key-value pair to access. `position` must be a valid 514 | /// index of the ordered dictionary and not equal to `endIndex`. 515 | /// - Returns: A tuple containing the key-value pair corresponding to `position`. 516 | /// 517 | /// - SeeAlso: `update(_:at:)` 518 | /// - SeeAlso: `subscript(bounds:)` 519 | public subscript(position: Index) -> Element { 520 | get { 521 | precondition( 522 | indices.contains(position), 523 | "[OrderedDictionary] Index is out of bounds" 524 | ) 525 | 526 | let key = _orderedKeys[position] 527 | 528 | guard let value = _keysToValues[key] else { 529 | fatalError("[OrderedDictionary] Inconsistency error") 530 | } 531 | 532 | return (key, value) 533 | } 534 | set(newElement) { 535 | update(newElement, at: position) 536 | } 537 | } 538 | 539 | /// Accesses a contiguous subrange of the ordered dictionary's elements. 540 | /// 541 | /// - Parameters: 542 | /// - bounds: A range of indices. The bounds of the range must be valid indices of 543 | /// the ordered dictionary. 544 | /// - Returns: An instance of `OrderedDictionarySlice`. 545 | /// 546 | /// - Precondition: When replacing a certain range with a slice, it has to have an equal size. 547 | /// Furthermore, the key-value pairs must not contain keys that are present in the ordered 548 | /// dictionary, but lie outside of the replaced range. It is safe to provide the keys 549 | /// from the replaced range in the different order or keys that are not present in the 550 | /// ordered dictionary. 551 | /// - SeeAlso: `subscript(position:)` 552 | public subscript(bounds: Range) -> SubSequence { 553 | get { 554 | precondition( 555 | bounds.clamped(to: indices) == bounds, 556 | "[OrderedDictionary] Range is out of bounds" 557 | ) 558 | 559 | return SubSequence(base: self, bounds: bounds) 560 | } 561 | set(newElements) { 562 | precondition( 563 | bounds.clamped(to: indices) == bounds, 564 | "[OrderedDictionary] Range is out of bounds" 565 | ) 566 | 567 | defer { _assertInvariant() } 568 | 569 | let innerKeys = orderedKeys[bounds] 570 | let outerKeys = Set(orderedKeys).subtracting(innerKeys) 571 | 572 | let newKeys = Set(newElements.map { $0.key }) 573 | 574 | precondition( 575 | newKeys.isDisjoint(with: outerKeys), 576 | "[OrderedDictionary] Range-based update produced duplicate keys" 577 | ) 578 | 579 | // WriteBackMutableSlice.swift 580 | // _writeBackMutableSlice(_:bounds:slice:) 581 | // https://bit.ly/3lhaIA8 582 | var baseIndex = bounds.lowerBound 583 | let baseEndIndex = bounds.upperBound 584 | 585 | var sliceIndex = newElements.startIndex 586 | let sliceEndIndex = newElements.endIndex 587 | 588 | while baseIndex != baseEndIndex && sliceIndex != sliceEndIndex { 589 | let (newKey, newValue) = newElements[sliceIndex] 590 | 591 | let previousElement = self[baseIndex] 592 | let previousKey = previousElement.key 593 | 594 | // If the key of the previously stored element at the updated index is not 595 | // replaced as part of the range-based update operation, its value is removed. 596 | if !newKeys.contains(previousKey) { 597 | _keysToValues.removeValue(forKey: previousKey) 598 | } 599 | 600 | _orderedKeys[baseIndex] = newKey 601 | _keysToValues[newKey] = newValue 602 | 603 | self.formIndex(after: &baseIndex) 604 | newElements.formIndex(after: &sliceIndex) 605 | } 606 | 607 | precondition( 608 | baseIndex == baseEndIndex, 609 | "[OrderedDictionary] Cannot replace a range with a slice of a smaller size" 610 | ) 611 | 612 | precondition( 613 | sliceIndex == sliceEndIndex, 614 | "[OrderedDictionary] Cannot replace a range with a slice of a larger size" 615 | ) 616 | } 617 | } 618 | 619 | /// Returns the key-value pair at the specified index, or `nil` if there is no key-value 620 | /// pair at that index. 621 | /// 622 | /// - Parameters: 623 | /// - index: The index of the key-value pair to be looked up. `index` does not have to 624 | /// be a valid index. 625 | /// - Returns: A tuple containing the key-value pair corresponding to `index` if the index 626 | /// is valid; otherwise, `nil`. 627 | /// 628 | /// - SeeAlso: `subscript(position:)` 629 | public func elementAt(_ index: Index) -> Element? { 630 | return indices.contains(index) ? self[index] : nil 631 | } 632 | 633 | /// Checks whether a key-value pair with the given key can be inserted into the ordered 634 | /// dictionary by validating its presence. 635 | /// 636 | /// - Parameters: 637 | /// - key: The key to be inserted into the ordered dictionary. 638 | /// - Returns: `true` if the key can safely be inserted; otherwise, `false`. 639 | /// 640 | /// - SeeAlso: `canInsert(at:)` 641 | public func canInsert(key: Key) -> Bool { 642 | return !containsKey(key) 643 | } 644 | 645 | /// Checks whether a new key-value pair can be inserted into the ordered dictionary at the 646 | /// given index. 647 | /// 648 | /// - Parameters: 649 | /// - index: The index the new key-value pair should be inserted at. 650 | /// - Returns: `true` if a new key-value pair can be inserted at the specified index; 651 | /// otherwise, `false`. 652 | /// 653 | /// - SeeAlso: `canInsert(key:)` 654 | public func canInsert(at index: Index) -> Bool { 655 | return index >= startIndex && index <= endIndex 656 | } 657 | 658 | /// Inserts a new key-value pair at the specified position. 659 | /// 660 | /// If the key of the inserted pair already exists in the ordered dictionary, a runtime error 661 | /// is triggered. Use `canInsert(_:)` for performing a check first, so that this method can 662 | /// be executed safely. 663 | /// 664 | /// - Parameters: 665 | /// - newElement: The new key-value pair to insert into the ordered dictionary. The key 666 | /// contained in the pair must not be already present in the ordered dictionary. 667 | /// - index: The position at which to insert the new key-value pair. `index` must be 668 | /// a valid index of the ordered dictionary or equal to `endIndex` property. 669 | /// 670 | /// - SeeAlso: `canInsert(key:)` 671 | /// - SeeAlso: `canInsert(at:)` 672 | /// - SeeAlso: `update(_:at:)` 673 | public mutating func insert( 674 | _ newElement: Element, 675 | at index: Index 676 | ) { 677 | precondition( 678 | canInsert(key: newElement.key), 679 | "[OrderedDictionary] Cannot insert duplicate key" 680 | ) 681 | 682 | precondition( 683 | canInsert(at: index), 684 | "[OrderedDictionary] Cannot insert key-value pair at invalid index" 685 | ) 686 | 687 | defer { _assertInvariant() } 688 | 689 | let (key, value) = newElement 690 | 691 | _orderedKeys.insert(key, at: index) 692 | _keysToValues[key] = value 693 | } 694 | 695 | /// Checks whether the key-value pair at the given index can be updated with the given 696 | /// key-value pair. This is not the case if the key of the updated element is already present 697 | /// in the ordered dictionary and located at another index than the updated one. 698 | /// 699 | /// Although this is a checking method, a valid index has to be provided. 700 | /// 701 | /// - Parameters: 702 | /// - newElement: The key-value pair to be set at the specified position. 703 | /// - index: The position at which to set the key-value pair. `index` must be a valid index 704 | /// of the ordered dictionary. 705 | public func canUpdate( 706 | _ newElement: Element, 707 | at index: Index 708 | ) -> Bool { 709 | precondition( 710 | indices.contains(index), 711 | "[OrderedDictionary] Index is out of bounds" 712 | ) 713 | 714 | let newKey = newElement.key 715 | 716 | let previousElement = self[index] 717 | let previousKey = previousElement.key 718 | 719 | let isSameKey = previousKey == newKey 720 | let isExistingKey = containsKey(newKey) 721 | 722 | return isSameKey || !isExistingKey 723 | } 724 | 725 | /// Updates the key-value pair located at the specified position. 726 | /// 727 | /// If the key of the updated pair already exists in the ordered dictionary *and* is located 728 | /// at a different position than the specified one, a runtime error is triggered. 729 | /// 730 | /// Use `canUpdate(_:at:)` to perform a check first, so that this method can be executed safely. 731 | /// 732 | /// - Parameters: 733 | /// - newElement: The key-value pair to be set at the specified position. 734 | /// - index: The position at which to set the key-value pair. `index` must be a valid index 735 | /// of the ordered dictionary. 736 | /// - Returns: A tuple containing the key-value pair previously associated with the `index`. 737 | /// 738 | /// - SeeAlso: `canUpdate(_:at:)` 739 | /// - SeeAlso: `subscript(position:)` 740 | /// - SeeAlso: `insert(_:at:)` 741 | @discardableResult 742 | public mutating func update( 743 | _ newElement: Element, 744 | at index: Index 745 | ) -> Element { 746 | precondition( 747 | indices.contains(index), 748 | "[OrderedDictionary] Index is out of bounds" 749 | ) 750 | 751 | defer { _assertInvariant() } 752 | 753 | let (newKey, newValue) = newElement 754 | 755 | let previousElement = self[index] 756 | let previousKey = previousElement.key 757 | 758 | let isSameKey = previousKey == newKey 759 | let isExistingKey = containsKey(newKey) 760 | 761 | precondition( 762 | isSameKey || !isExistingKey, 763 | "[OrderedDictionary] Index-based update produced duplicate keys" 764 | ) 765 | 766 | if (!isSameKey) { 767 | _keysToValues.removeValue(forKey: previousKey) 768 | } 769 | 770 | _orderedKeys[index] = newKey 771 | _keysToValues[newKey] = newValue 772 | 773 | return previousElement 774 | } 775 | 776 | /// Removes and returns the key-value pair at the specified position if there is any key-value 777 | /// pair, or `nil` if there is none. 778 | /// 779 | /// The following example shows how to remove a key-value pair at a specific index: 780 | /// 781 | /// var orderedDictionary: OrderedDictionary = ["a": 1, "b": 2, "c": 3] 782 | /// 783 | /// let removedElement = orderedDictionary.remove(at: 1) 784 | /// 785 | /// print(removedElement) 786 | /// // => Optional((key: "b", value: 2)) 787 | /// 788 | /// print(orderedDictionary) 789 | /// // => ["a": 1, "c": 3] 790 | /// 791 | /// - Parameters: 792 | /// - index: The position of the key-value pair to remove. 793 | /// - Returns: The element at the specified index, or `nil` if the position is not taken. 794 | /// 795 | /// - SeeAlso: `subscript(position:)` 796 | /// - SeeAlso: `removeValue(forKey:)` 797 | @discardableResult 798 | public mutating func remove(at index: Index) -> Element? { 799 | guard let element = elementAt(index) else { return nil } 800 | 801 | defer { _assertInvariant() } 802 | 803 | _orderedKeys.remove(at: index) 804 | _keysToValues.removeValue(forKey: element.key) 805 | 806 | return element 807 | } 808 | 809 | // ============================================================================ // 810 | // MARK: - Removing First & Last Elements 811 | // ============================================================================ // 812 | 813 | /// Removes and returns the first key-value pair of the ordered dictionary if it is not empty. 814 | public mutating func popFirst() -> Element? { 815 | guard !isEmpty else { return nil } 816 | return remove(at: startIndex) 817 | } 818 | 819 | /// Removes and returns the last key-value pair of the ordered dictionary if it is not empty. 820 | public mutating func popLast() -> Element? { 821 | guard !isEmpty else { return nil } 822 | return remove(at: index(before: endIndex)) 823 | } 824 | 825 | /// Removes and returns the first key-value pair of the ordered dictionary. 826 | public mutating func removeFirst() -> Element { 827 | precondition( 828 | !isEmpty, 829 | "[OrderedDictionary] Cannot remove key-value pairs when empty" 830 | ) 831 | 832 | return remove(at: startIndex)! 833 | } 834 | 835 | /// Removes and returns the last key-value pair of the ordered dictionary. 836 | public mutating func removeLast() -> Element { 837 | precondition( 838 | !isEmpty, 839 | "[OrderedDictionary] Cannot remove key-value pairs when empty" 840 | ) 841 | 842 | return remove(at: index(before: endIndex))! 843 | } 844 | 845 | // ============================================================================ // 846 | // MARK: - Sorting Elements 847 | // ============================================================================ // 848 | 849 | /// Sorts the ordered dictionary in place, using the given predicate as the comparison between 850 | /// elements. 851 | /// 852 | /// The predicate must be a *strict weak ordering* over the elements. 853 | /// 854 | /// - Parameters: 855 | /// - areInIncreasingOrder: A predicate that returns `true` if its first argument should 856 | /// be ordered before its second argument; otherwise, `false`. 857 | /// 858 | /// - SeeAlso: `sorted(by:)` 859 | public mutating func sort( 860 | by areInIncreasingOrder: (Element, Element) throws -> Bool 861 | ) rethrows { 862 | try _sort( 863 | in: indices, 864 | by: areInIncreasingOrder 865 | ) 866 | } 867 | 868 | /// Returns a new ordered dictionary, sorted using the given predicate as the comparison 869 | /// between elements. 870 | /// 871 | /// The predicate must be a *strict weak ordering* over the elements. 872 | /// 873 | /// The following example shows how to sort an ordered dictionary according the keys or values: 874 | /// 875 | /// let orderedDictionary: OrderedDictionary = ["c": 3, "d": 2, "b": 1, "a": 4] 876 | /// 877 | /// print(orderedDictionary.sorted { $0.key < $1.key }) 878 | /// // => ["a": 1, "b": 2, "c": 3, "d": 4] 879 | /// 880 | /// print(orderedDictionary.sorted { $0.value < $1.value }) 881 | /// // => ["b": 1, "d": 2, "c": 3, "a": 4] 882 | /// 883 | /// - Parameters: 884 | /// - areInIncreasingOrder: A predicate that returns `true` if its first argument should 885 | /// be ordered before its second argument; otherwise, `false`. 886 | /// - Returns: A new ordered dictionary sorted according to the predicate. 887 | /// 888 | /// - SeeAlso: `sort(by:)` 889 | public func sorted( 890 | by areInIncreasingOrder: (Element, Element) throws -> Bool 891 | ) rethrows -> Self { 892 | var new = self 893 | try new.sort(by: areInIncreasingOrder) 894 | return new 895 | } 896 | 897 | internal mutating func _sort( 898 | in range: Range, 899 | by areInIncreasingOrder: (Element, Element) throws -> Bool 900 | ) rethrows { 901 | defer { _assertInvariant() } 902 | 903 | try _orderedKeys[range].sort { key1, key2 in 904 | let element1 = (key: key1, value: _keysToValues[key1]!) 905 | let element2 = (key: key2, value: _keysToValues[key2]!) 906 | return try areInIncreasingOrder(element1, element2) 907 | } 908 | } 909 | 910 | // ============================================================================ // 911 | // MARK: - Reordering Elements 912 | // ============================================================================ // 913 | 914 | // ------------------------------------------------------- // 915 | // reverse() 916 | // ------------------------------------------------------- // 917 | 918 | /// Reverses the key-value pairs of the ordered dictionary in place. 919 | public mutating func reverse() { 920 | _reverse(in: indices) 921 | } 922 | 923 | internal mutating func _reverse(in range: Range) { 924 | defer { _assertInvariant() } 925 | 926 | _orderedKeys[range].reverse() 927 | } 928 | 929 | // ------------------------------------------------------- // 930 | // shuffle(using:) 931 | // ------------------------------------------------------- // 932 | 933 | /// Shuffles the ordered dictionary in place, using the given generator as a source 934 | /// for randomness. 935 | public mutating func shuffle( 936 | using generator: inout T 937 | ) where T: RandomNumberGenerator { 938 | _shuffle(in: indices, using: &generator) 939 | } 940 | 941 | public mutating func _shuffle( 942 | in range: Range, 943 | using generator: inout T 944 | ) where T: RandomNumberGenerator { 945 | defer { _assertInvariant() } 946 | 947 | _orderedKeys[range].shuffle(using: &generator) 948 | } 949 | 950 | // ------------------------------------------------------- // 951 | // partition(by:) 952 | // ------------------------------------------------------- // 953 | 954 | /// Reorders the key-value pairs of the ordered dictionary such that all the key-value pairs 955 | /// that match the given predicate are after all the key-value pairs that do not match. 956 | public mutating func partition( 957 | by belongsInSecondPartition: (Element) throws -> Bool 958 | ) rethrows -> Index { 959 | return try _partition( 960 | in: indices, 961 | by: belongsInSecondPartition 962 | ) 963 | } 964 | 965 | internal mutating func _partition( 966 | in range: Range, 967 | by belongsInSecondPartition: (Element) throws -> Bool 968 | ) rethrows -> Index { 969 | defer { _assertInvariant() } 970 | 971 | return try _orderedKeys[range].partition { key in 972 | let element = (key: key, value: _keysToValues[key]!) 973 | return try belongsInSecondPartition(element) 974 | } 975 | } 976 | 977 | // ------------------------------------------------------- // 978 | // swapAt(_:_:) 979 | // ------------------------------------------------------- // 980 | 981 | /// Exchanges the elements at the specified indices. 982 | /// 983 | /// - Parameters: 984 | /// - i: The index of the first value to swap. 985 | /// - j: The index of the second value to swap. 986 | /// 987 | /// - Precondition: Both indices must be valid existing indices of the ordered dictionary. 988 | /// - Complexity: O(1) 989 | public mutating func swapAt(_ i: Index, _ j: Index) { 990 | _orderedKeys.swapAt(i, j) 991 | } 992 | 993 | // ============================================================================ // 994 | // MARK: - Transformations 995 | // ============================================================================ // 996 | 997 | /// Returns a new ordered dictionary containing the keys of this ordered dictionary with 998 | /// the values transformed by the given closure while preserving the original order. 999 | public func mapValues( 1000 | _ transform: (Value) throws -> T 1001 | ) rethrows -> OrderedDictionary { 1002 | var result = OrderedDictionary() 1003 | 1004 | for (key, value) in self { 1005 | result[key] = try transform(value) 1006 | } 1007 | 1008 | return result 1009 | } 1010 | 1011 | /// Returns a new ordered dictionary containing only the key-value pairs that have non-nil 1012 | /// values as the result of transformation by the given closure while preserving the original 1013 | /// order. 1014 | public func compactMapValues( 1015 | _ transform: (Value) throws -> T? 1016 | ) rethrows -> OrderedDictionary { 1017 | var result = OrderedDictionary() 1018 | 1019 | for (key, value) in self { 1020 | if let transformedValue = try transform(value) { 1021 | result[key] = transformedValue 1022 | } 1023 | } 1024 | 1025 | return result 1026 | } 1027 | 1028 | /// Returns a new ordered dictionary container the key-value pairs that satisfy the given 1029 | /// predicate while preserving the original order. 1030 | public func filter( 1031 | _ isIncluded: (Element) throws -> Bool 1032 | ) rethrows -> Self { 1033 | return Self(uniqueKeysWithValues: try self.lazy.filter(isIncluded)) 1034 | } 1035 | 1036 | // ============================================================================ // 1037 | // MARK: - Capacity 1038 | // ============================================================================ // 1039 | 1040 | /// The total number of elements that the ordered dictionary can contain without allocating 1041 | /// new storage. 1042 | public var capacity: Int { 1043 | return Swift.min(_orderedKeys.capacity, _keysToValues.capacity) 1044 | } 1045 | 1046 | /// Reserves enough space to store the specified number of elements, when appropriate 1047 | /// for the underlying types. 1048 | /// 1049 | /// If you are adding a known number of elements to an ordered dictionary, use this method 1050 | /// to avoid multiple reallocations. This method ensures that the underlying types of the 1051 | /// ordered dictionary have space allocated for at least the requested number of elements. 1052 | /// 1053 | /// - Parameters: 1054 | /// - minimumCapacity: The requested number of elements to store. 1055 | public mutating func reserveCapacity(_ minimumCapacity: Int) { 1056 | defer { _assertInvariant() } 1057 | 1058 | _orderedKeys.reserveCapacity(minimumCapacity) 1059 | _keysToValues.reserveCapacity(minimumCapacity) 1060 | } 1061 | 1062 | // ============================================================================ // 1063 | // MARK: - Invariant 1064 | // ============================================================================ // 1065 | 1066 | /// Asserts whether the internal invariant is met and traps in the debug mode otherwise. 1067 | /// Inspired by the implementation in PenguinStructures: https://bit.ly/3dDLidO 1068 | /// 1069 | /// - Complexity: O(`count`) 1070 | private func _assertInvariant() { 1071 | assert( 1072 | _computeInvariant(), 1073 | """ 1074 | [OrderedDictionary] Broken internal invariant: 1075 | orderedKeys(count: \(_orderedKeys.count)) = \(_orderedKeys) 1076 | keysToValues(count: \(_keysToValues.count)) = \(_keysToValues) 1077 | """ 1078 | ) 1079 | } 1080 | 1081 | /// Computes the internal invariant for the count and key presence in the underlying storage, 1082 | /// and returns `true` if the invariant is met. 1083 | private func _computeInvariant() -> Bool { 1084 | if _orderedKeys.count != _keysToValues.count { return false } 1085 | 1086 | for index in _orderedKeys.indices { 1087 | let key = _orderedKeys[index] 1088 | if _keysToValues[key] == nil { return false } 1089 | } 1090 | 1091 | return true 1092 | } 1093 | 1094 | // ============================================================================ // 1095 | // MARK: - Internal Storage 1096 | // ============================================================================ // 1097 | 1098 | /// The backing storage for the ordered keys. 1099 | private var _orderedKeys: [Key] 1100 | 1101 | /// The backing storage for the mapping of keys to values. 1102 | private var _keysToValues: [Key: Value] 1103 | 1104 | } 1105 | 1106 | extension OrderedDictionary: Hashable where Value: Hashable {} 1107 | extension OrderedDictionary: Equatable where Value: Equatable {} 1108 | 1109 | extension OrderedDictionary: ExpressibleByArrayLiteral { 1110 | 1111 | /// Initializes an ordered dictionary initialized from an array literal containing a list 1112 | /// of key-value pairs. Every key in `elements` must be unique. 1113 | public init(arrayLiteral elements: Element...) { 1114 | self.init(uniqueKeysWithValues: elements) 1115 | } 1116 | 1117 | } 1118 | 1119 | extension OrderedDictionary: ExpressibleByDictionaryLiteral { 1120 | 1121 | /// Initializes an ordered dictionary initialized from a dictionary literal. Every key 1122 | /// in `elements` must be unique. 1123 | public init(dictionaryLiteral elements: (Key, Value)...) { 1124 | self.init(uniqueKeysWithValues: elements.map { element in 1125 | let (key, value) = element 1126 | return (key: key, value: value) 1127 | }) 1128 | } 1129 | 1130 | } 1131 | 1132 | extension Array { 1133 | 1134 | /// Initializes an empty array with preallocated space for at least the specified number 1135 | /// of elements. 1136 | fileprivate init(minimumCapacity: Int) { 1137 | self.init() 1138 | self.reserveCapacity(minimumCapacity) 1139 | } 1140 | 1141 | } 1142 | -------------------------------------------------------------------------------- /ASDKgram-Swift/OrderedDictionary/OrderedDictionarySlice.swift: -------------------------------------------------------------------------------- 1 | public struct OrderedDictionarySlice: RandomAccessCollection, MutableCollection { 2 | 3 | // ============================================================================ // 4 | // MARK: - Type Aliases 5 | // ============================================================================ // 6 | 7 | /// The type of the underlying ordered dictionary. 8 | public typealias Base = OrderedDictionary 9 | 10 | /// The type of the contiguous subrange of the ordered dictionary's elements. 11 | public typealias SubSequence = Self 12 | 13 | // ============================================================================ // 14 | // MARK: - Initialization 15 | // ============================================================================ // 16 | 17 | public init(base: Base, bounds: Base.Indices) { 18 | self.base = base 19 | self.startIndex = bounds.lowerBound 20 | self.endIndex = bounds.upperBound 21 | } 22 | 23 | // ============================================================================ // 24 | // MARK: - Base 25 | // ============================================================================ // 26 | 27 | /// The underlying ordered dictionary. 28 | public private(set) var base: Base 29 | 30 | // ============================================================================ // 31 | // MARK: - Indices 32 | // ============================================================================ // 33 | 34 | /// The start index. 35 | public let startIndex: Base.Index 36 | 37 | /// The end index. 38 | public let endIndex: Base.Index 39 | 40 | // ============================================================================ // 41 | // MARK: - Subscripts 42 | // ============================================================================ // 43 | 44 | public subscript( 45 | position: Base.Index 46 | ) -> Base.Element { 47 | get { 48 | base[position] 49 | } 50 | set(newElement) { 51 | base[position] = newElement 52 | } 53 | } 54 | 55 | public subscript( 56 | bounds: Range 57 | ) -> OrderedDictionarySlice { 58 | get { 59 | base[bounds] 60 | } 61 | set(newElements) { 62 | base[bounds] = newElements 63 | } 64 | } 65 | 66 | // ============================================================================ // 67 | // MARK: - Reordering Methods Overloads 68 | // ============================================================================ // 69 | 70 | public mutating func sort( 71 | by areInIncreasingOrder: (Base.Element, Base.Element) throws -> Bool 72 | ) rethrows { 73 | try base._sort( 74 | in: indices, 75 | by: areInIncreasingOrder 76 | ) 77 | } 78 | 79 | public mutating func reverse() { 80 | base._reverse(in: indices) 81 | } 82 | 83 | public mutating func shuffle( 84 | using generator: inout T 85 | ) where T: RandomNumberGenerator { 86 | base._shuffle( 87 | in: indices, 88 | using: &generator 89 | ) 90 | } 91 | 92 | public mutating func partition( 93 | by belongsInSecondPartition: (Base.Element) throws -> Bool 94 | ) rethrows -> Index { 95 | return try base._partition( 96 | in: indices, 97 | by: belongsInSecondPartition 98 | ) 99 | } 100 | 101 | public mutating func swapAt(_ i: Base.Index, _ j: Base.Index) { 102 | base.swapAt(i, j) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /ASDKgram-Swift/ParsePesponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PX500Convenience.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 08/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | 20 | import Foundation 21 | 22 | func parsePopularPage(withURL: URL, page: Int) -> Resource { 23 | 24 | let parse = Resource(url: withURL, page: page) { metaData, jsonData in 25 | do { 26 | let photos = try JSONDecoder().decode([PhotoModel].self, from: jsonData) 27 | return .success(PopularPageModel(metaData: metaData, photos: photos)) 28 | } catch { 29 | return .failure(.errorParsingJSON) 30 | } 31 | } 32 | 33 | return parse 34 | } 35 | -------------------------------------------------------------------------------- /ASDKgram-Swift/PhotoFeedListKitViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoFeedListKitViewController.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Daniel on 2017/9/1. 6 | // Copyright © 2017年 Calum Harris. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import IGListKit 11 | import AsyncDisplayKit 12 | 13 | class PhotoFeedListKitViewController: ASDKViewController { 14 | 15 | lazy var adapter: ListAdapter = { 16 | let listAdapter = ListAdapter(updater: ListAdapterUpdater(), viewController: self, workingRangeSize: 0) 17 | return listAdapter 18 | }() 19 | var photoFeed = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular) 20 | 21 | // Lifecycle 22 | 23 | override init() { 24 | let flowLayout = UICollectionViewFlowLayout() 25 | 26 | super.init(node: ASCollectionNode(collectionViewLayout: flowLayout)) 27 | adapter.setASDKCollectionNode(node) 28 | adapter.dataSource = self 29 | } 30 | 31 | required init?(coder aDecoder: NSCoder) { 32 | fatalError("init(coder:) has not been implemented") 33 | } 34 | 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | } 39 | 40 | override func didReceiveMemoryWarning() { 41 | super.didReceiveMemoryWarning() 42 | // Dispose of any resources that can be recreated. 43 | } 44 | 45 | } 46 | 47 | extension PhotoFeedListKitViewController: ListAdapterDataSource { 48 | func objects(for listAdapter: ListAdapter) -> [ListDiffable] { 49 | return [DiffUtility.DiffableBox(value: self.photoFeed, identifier: self.photoFeed.diffIdentifier as NSObjectProtocol, equal: ==) as ListDiffable] 50 | 51 | } 52 | 53 | func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { 54 | if object is DiffUtility.DiffableBox { 55 | return PhotoFeedSectionController() 56 | } 57 | 58 | return ListSectionController() 59 | } 60 | 61 | func emptyView(for listAdapter: ListAdapter) -> UIView? { 62 | return nil 63 | } 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /ASDKgram-Swift/PhotoFeedModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoFeedModel.swift 3 | // Texture 4 | // 5 | // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. 6 | // Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. 7 | // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | 10 | import UIKit 11 | 12 | final class PhotoFeedModel { 13 | 14 | // MARK: Properties 15 | 16 | public private(set) var photoFeedModelType: PhotoFeedModelType 17 | 18 | private var orderedPhotos: OrderedDictionary = [:] 19 | private var currentPage: Int = 0 20 | private var totalPages: Int = 0 21 | private var totalItems: Int = 0 22 | private var fetchPageInProgress: Bool = false 23 | 24 | // MARK: Lifecycle 25 | 26 | init(photoFeedModelType: PhotoFeedModelType) { 27 | self.photoFeedModelType = photoFeedModelType 28 | } 29 | 30 | // MARK: API 31 | 32 | lazy var url: URL = { 33 | return URL.URLForFeedModelType(feedModelType: self.photoFeedModelType) 34 | }() 35 | 36 | var numberOfItems: Int { 37 | return orderedPhotos.count 38 | } 39 | 40 | var photos: [PhotoModel] { 41 | return orderedPhotos.orderedValues.reversed().reversed(); 42 | } 43 | 44 | func itemAtIndexPath(_ indexPath: IndexPath) -> PhotoModel { 45 | return orderedPhotos[indexPath.row].value 46 | } 47 | 48 | // return in completion handler the number of additions and the status of internet connection 49 | 50 | func updateNewBatchOfPopularPhotos(additionsAndConnectionStatusCompletion: @escaping (Int, InternetStatus) -> ()) { 51 | 52 | // For this example let's use the main thread as locking queue 53 | DispatchQueue.main.async { 54 | guard !self.fetchPageInProgress else { 55 | return 56 | } 57 | 58 | self.fetchPageInProgress = true 59 | self.fetchNextPageOfPopularPhotos(replaceData: false) { [unowned self] additions, error in 60 | self.fetchPageInProgress = false 61 | 62 | if let error = error { 63 | switch error { 64 | case .noInternetConnection: 65 | additionsAndConnectionStatusCompletion(0, .noConnection) 66 | default: 67 | additionsAndConnectionStatusCompletion(0, .connected) 68 | } 69 | } else { 70 | additionsAndConnectionStatusCompletion(additions, .connected) 71 | } 72 | } 73 | } 74 | } 75 | 76 | private func fetchNextPageOfPopularPhotos(replaceData: Bool, numberOfAdditionsCompletion: @escaping (Int, NetworkingErrors?) -> ()) { 77 | if currentPage == totalPages, currentPage != 0 { 78 | numberOfAdditionsCompletion(0, .customError("No pages left to parse")) 79 | return 80 | } 81 | 82 | let pageToFetch = currentPage + 1 83 | WebService().load(resource: parsePopularPage(withURL: url, page: pageToFetch)) { [unowned self] result in 84 | // Callback will happen on main for now 85 | switch result { 86 | case .success(let itemsPage): 87 | // Update current state 88 | self.totalItems = itemsPage.totalNumberOfItems 89 | self.totalPages = itemsPage.totalPages 90 | self.currentPage = itemsPage.page 91 | 92 | // Update photos 93 | if replaceData { 94 | self.orderedPhotos = [] 95 | } 96 | var insertedItems = 0 97 | for photo in itemsPage.photos { 98 | if !self.orderedPhotos.containsKey(photo.photoID) { 99 | // Append a new key-value pair by setting a value for an non-existent key 100 | self.orderedPhotos[photo.photoID] = photo 101 | insertedItems += 1 102 | } 103 | } 104 | 105 | numberOfAdditionsCompletion(insertedItems, nil) 106 | case .failure(let fail): 107 | print(fail) 108 | numberOfAdditionsCompletion(0, fail) 109 | } 110 | } 111 | } 112 | } 113 | 114 | enum PhotoFeedModelType { 115 | case photoFeedModelTypePopular 116 | case photoFeedModelTypeLocation 117 | case photoFeedModelTypeUserPhotos 118 | } 119 | 120 | enum InternetStatus { 121 | case connected 122 | case noConnection 123 | } 124 | 125 | extension PhotoFeedModel: Diffable { 126 | var diffIdentifier: String { 127 | return url.absoluteString; 128 | } 129 | 130 | static func ==(lhs: PhotoFeedModel, rhs: PhotoFeedModel) -> Bool { 131 | return lhs.url == rhs.url 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /ASDKgram-Swift/PhotoFeedSectionController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoFeedSectionController.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Daniel on 2017/9/18. 6 | // Copyright © 2017年 Calum Harris. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import AsyncDisplayKit 11 | 12 | 13 | class PhotoFeedSectionController: ASCollectionSectionController, ASSectionController { 14 | 15 | var photoFeed = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular) 16 | 17 | 18 | override func didUpdate(to object: Any) { 19 | if let other = object as? DiffUtility.DiffableBox { 20 | photoFeed = other.value 21 | if photoFeed.photos.count > 0 { 22 | self.setItmes(newItems: photoFeed.photos, animated: false) 23 | } 24 | } 25 | } 26 | 27 | override func cellForItem(at index: Int) -> UICollectionViewCell { 28 | let cell = ASIGListSectionControllerMethods.cellForItem(at: index, sectionController: self) 29 | return cell 30 | } 31 | 32 | override func sizeForItem(at index: Int) -> CGSize { 33 | return ASIGListSectionControllerMethods .sizeForItem(at: index) 34 | } 35 | 36 | override func didSelectItem(at index: Int) { 37 | 38 | } 39 | 40 | // MARK: - ASSectionController 41 | 42 | func nodeBlockForItem(at index: Int) -> ASCellNodeBlock { 43 | let photo = photoFeed.itemAtIndexPath(IndexPath(row: index, section: 0)) 44 | let nodeBlock: ASCellNodeBlock = { 45 | let node = PhotoTableNodeCell(photoModel: photo) 46 | return node 47 | } 48 | return nodeBlock 49 | 50 | } 51 | 52 | func nodeForItem(at index: Int) -> ASCellNode { 53 | let photo = photoFeed.itemAtIndexPath(IndexPath(row: index, section: 0)) 54 | let node = PhotoTableNodeCell(photoModel: photo) 55 | return node 56 | } 57 | 58 | func beginBatchFetch(with context: ASBatchContext) { 59 | DispatchQueue.main.async { 60 | self.fetchNewBatchWithContext(context) 61 | } 62 | } 63 | 64 | // MARK: - Data 65 | func fetchNewBatchWithContext(_ context: ASBatchContext?) { 66 | photoFeed.updateNewBatchOfPopularPhotos(additionsAndConnectionStatusCompletion: { [weak self] (additions, connectionStatus) in 67 | guard let `self` = self else { return } 68 | switch connectionStatus { 69 | case .connected: 70 | self.setItmes(newItems: self.photoFeed.photos, animated: false, completion: { 71 | context?.completeBatchFetching(true) 72 | }) 73 | case .noConnection: 74 | if context != nil { 75 | context!.completeBatchFetching(true) 76 | } 77 | break 78 | } 79 | 80 | }) 81 | } 82 | 83 | } 84 | 85 | extension PhotoFeedSectionController: ListSupplementaryViewSource { 86 | func supportedElementKinds() -> [String] { 87 | return [UICollectionView.elementKindSectionHeader] 88 | } 89 | 90 | func viewForSupplementaryElement(ofKind elementKind: String, at index: Int) -> UICollectionReusableView { 91 | return ASIGListSupplementaryViewSourceMethods.viewForSupplementaryElement(ofKind: elementKind, at: index, sectionController: self) 92 | } 93 | 94 | func sizeForSupplementaryView(ofKind elementKind: String, at index: Int) -> CGSize { 95 | return ASIGListSupplementaryViewSourceMethods.sizeForSupplementaryView(ofKind:elementKind, at:index) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /ASDKgram-Swift/PhotoFeedTableNodeController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoFeedTableNodeController.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 06/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | 20 | import AsyncDisplayKit 21 | 22 | class PhotoFeedTableNodeController: ASDKViewController { 23 | 24 | private lazy var activityIndicator: UIActivityIndicatorView = { 25 | return UIActivityIndicatorView(style: .gray) 26 | }() 27 | var photoFeed = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular) 28 | 29 | // MARK: LifeCycle 30 | 31 | override init() { 32 | super.init(node: ASTableNode()) 33 | navigationItem.title = "ASDK" 34 | } 35 | 36 | required init?(coder aDecoder: NSCoder) { 37 | fatalError("init(coder:) has not been implemented") 38 | } 39 | 40 | override func viewDidLoad() { 41 | super.viewDidLoad() 42 | node.allowsSelection = false 43 | node.view.separatorStyle = .none 44 | node.dataSource = self 45 | node.delegate = self 46 | node.leadingScreensForBatching = 2.5 47 | navigationController?.hidesBarsOnSwipe = true 48 | node.view.addSubview(activityIndicator) 49 | } 50 | 51 | override func viewDidLayoutSubviews() { 52 | super.viewDidLayoutSubviews() 53 | 54 | let bounds = node.bounds 55 | activityIndicator.frame.origin = CGPoint(x: (bounds.width - activityIndicator.frame.width) / 2.0, y: (bounds.height - activityIndicator.frame.height) / 2.0) 56 | 57 | } 58 | 59 | func fetchNewBatchWithContext(_ context: ASBatchContext?) { 60 | DispatchQueue.main.async { 61 | self.activityIndicator.startAnimating() 62 | } 63 | 64 | photoFeed.updateNewBatchOfPopularPhotos() { additions, connectionStatus in 65 | switch connectionStatus { 66 | case .connected: 67 | self.activityIndicator.stopAnimating() 68 | self.addRowsIntoTableNode(newPhotoCount: additions) 69 | context?.completeBatchFetching(true) 70 | case .noConnection: 71 | self.activityIndicator.stopAnimating() 72 | context?.completeBatchFetching(true) 73 | } 74 | } 75 | } 76 | 77 | func addRowsIntoTableNode(newPhotoCount newPhotos: Int) { 78 | let indexRange = (photoFeed.numberOfItems - newPhotos.. Int { 87 | return photoFeed.numberOfItems 88 | } 89 | 90 | func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { 91 | let photo = photoFeed.itemAtIndexPath(indexPath) 92 | let nodeBlock: ASCellNodeBlock = { 93 | return PhotoTableNodeCell(photoModel: photo) 94 | } 95 | return nodeBlock 96 | } 97 | 98 | func shouldBatchFetchForCollectionNode(collectionNode: ASCollectionNode) -> Bool { 99 | return true 100 | } 101 | 102 | func tableNode(_ tableNode: ASTableNode, willBeginBatchFetchWith context: ASBatchContext) { 103 | fetchNewBatchWithContext(context) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /ASDKgram-Swift/PhotoFeedTableViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoFeedTableViewController.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 06/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | 20 | import UIKit 21 | 22 | class PhotoFeedTableViewController: UITableViewController { 23 | 24 | var activityIndicator: UIActivityIndicatorView! 25 | var photoFeed = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular) 26 | 27 | init() { 28 | super.init(nibName: nil, bundle: nil) 29 | navigationItem.title = "UIKit" 30 | } 31 | 32 | required init?(coder aDecoder: NSCoder) { 33 | fatalError("init(coder:) has not been implemented") 34 | } 35 | 36 | override func viewDidLoad() { 37 | super.viewDidLoad() 38 | setupActivityIndicator() 39 | configureTableView() 40 | fetchNewBatch() 41 | navigationController?.hidesBarsOnSwipe = true 42 | } 43 | 44 | func fetchNewBatch() { 45 | activityIndicator.startAnimating() 46 | photoFeed.updateNewBatchOfPopularPhotos() { additions, connectionStatus in 47 | switch connectionStatus { 48 | case .connected: 49 | self.activityIndicator.stopAnimating() 50 | self.addRowsIntoTableView(newPhotoCount: additions) 51 | case .noConnection: 52 | self.activityIndicator.stopAnimating() 53 | break 54 | } 55 | } 56 | } 57 | 58 | // helper functions 59 | func setupActivityIndicator() { 60 | let activityIndicator = UIActivityIndicatorView(style: .gray) 61 | self.activityIndicator = activityIndicator 62 | self.tableView.addSubview(activityIndicator) 63 | activityIndicator.translatesAutoresizingMaskIntoConstraints = false 64 | NSLayoutConstraint.activate([ 65 | activityIndicator.centerXAnchor.constraint(equalTo: self.tableView.centerXAnchor), 66 | activityIndicator.centerYAnchor.constraint(equalTo: self.tableView.centerYAnchor) 67 | ]) 68 | } 69 | 70 | func configureTableView() { 71 | tableView.register(PhotoTableViewCell.self, forCellReuseIdentifier: "photoCell") 72 | tableView.allowsSelection = false 73 | tableView.rowHeight = UITableView.automaticDimension 74 | tableView.separatorStyle = .none 75 | } 76 | } 77 | 78 | extension PhotoFeedTableViewController { 79 | 80 | func addRowsIntoTableView(newPhotoCount newPhotos: Int) { 81 | 82 | let indexRange = (photoFeed.numberOfItems - newPhotos.. Int { 90 | return photoFeed.numberOfItems 91 | } 92 | 93 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 94 | guard let cell = tableView.dequeueReusableCell(withIdentifier: "photoCell", for: indexPath) as? PhotoTableViewCell else { fatalError("Wrong cell type") } 95 | cell.photoModel = photoFeed.itemAtIndexPath(indexPath) 96 | return cell 97 | } 98 | 99 | override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 100 | return PhotoTableViewCell.height(for: photoFeed.itemAtIndexPath(indexPath), withWidth: self.view.frame.size.width) 101 | } 102 | 103 | override func scrollViewDidScroll(_ scrollView: UIScrollView) { 104 | 105 | let currentOffSetY = scrollView.contentOffset.y 106 | let contentHeight = scrollView.contentSize.height 107 | let screenHeight = UIScreen.main.bounds.size.height 108 | let screenfullsBeforeBottom = (contentHeight - currentOffSetY) / screenHeight 109 | if screenfullsBeforeBottom < 2.5 { 110 | self.fetchNewBatch() 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /ASDKgram-Swift/PhotoModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoModel.swift 3 | // Texture 4 | // 5 | // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. 6 | // Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. 7 | // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | 10 | import UIKit 11 | 12 | // MARK: ProfileImage 13 | 14 | struct ProfileImage: Codable { 15 | let large: String 16 | let medium: String 17 | let small: String 18 | } 19 | 20 | // MARK: UserModel 21 | 22 | struct UserModel: Codable { 23 | let userName: String 24 | let profileImages: ProfileImage 25 | 26 | enum CodingKeys: String, CodingKey { 27 | case userName = "username" 28 | case profileImages = "profile_image" 29 | } 30 | } 31 | 32 | extension UserModel { 33 | var profileImage: String { 34 | return profileImages.medium 35 | } 36 | } 37 | 38 | // MARK: PhotoURL 39 | 40 | struct PhotoURL: Codable { 41 | let full: String 42 | let raw: String 43 | let regular: String 44 | let small: String 45 | let thumb: String 46 | } 47 | 48 | // MARK: PhotoModel 49 | 50 | struct PhotoModel: Codable, Diffable { 51 | var diffIdentifier: String { 52 | return String(photoID) 53 | } 54 | 55 | static func == (lhs: PhotoModel, rhs: PhotoModel) -> Bool { 56 | return lhs.url == rhs.url 57 | } 58 | 59 | let urls: PhotoURL 60 | let photoID: String 61 | let uploadedDateString: String 62 | let descriptionText: String? 63 | let likesCount: Int 64 | let width: Int 65 | let height: Int 66 | let user: UserModel 67 | 68 | enum CodingKeys: String, CodingKey { 69 | case photoID = "id" 70 | case urls = "urls" 71 | case uploadedDateString = "created_at" 72 | case descriptionText = "description" 73 | case likesCount = "likes" 74 | case width = "width" 75 | case height = "height" 76 | case user = "user" 77 | } 78 | } 79 | 80 | extension PhotoModel { 81 | var url: String { 82 | return urls.regular 83 | } 84 | } 85 | 86 | extension PhotoModel { 87 | 88 | // MARK: - Attributed Strings 89 | 90 | func attributedStringForUserName(withSize size: CGFloat) -> NSAttributedString { 91 | let attributes = [ 92 | NSAttributedString.Key.foregroundColor : UIColor.darkGray, 93 | NSAttributedString.Key.font: UIFont.boldSystemFont(ofSize: size) 94 | ] 95 | return NSAttributedString(string: user.userName, attributes: attributes) 96 | } 97 | 98 | func attributedStringForDescription(withSize size: CGFloat) -> NSAttributedString { 99 | let attributes = [ 100 | NSAttributedString.Key.foregroundColor : UIColor.darkGray, 101 | NSAttributedString.Key.font: UIFont.systemFont(ofSize: size) 102 | ] 103 | return NSAttributedString(string: descriptionText ?? "", attributes: attributes) 104 | } 105 | 106 | func attributedStringLikes(withSize size: CGFloat) -> NSAttributedString { 107 | guard let formattedLikesNumber = NumberFormatter.decimalNumberFormatter.string(from: NSNumber(value: likesCount)) else { 108 | return NSAttributedString() 109 | } 110 | 111 | let likesAttributes = [ 112 | NSAttributedString.Key.foregroundColor : UIColor.red, 113 | NSAttributedString.Key.font: UIFont.systemFont(ofSize: size) 114 | ] 115 | 116 | let likesAttrString = NSAttributedString(string: "\(formattedLikesNumber) Likes", attributes: likesAttributes) 117 | 118 | let heartAttributes = [ 119 | NSAttributedString.Key.foregroundColor : UIColor.mainBarTintColor, 120 | NSAttributedString.Key.font: UIFont.systemFont(ofSize: size) 121 | ] 122 | let heartAttrString = NSAttributedString(string: "♥︎ ", attributes: heartAttributes) 123 | 124 | let combine = NSMutableAttributedString() 125 | combine.append(heartAttrString) 126 | combine.append(likesAttrString) 127 | return combine 128 | } 129 | 130 | func attributedStringForTimeSinceString(withSize size: CGFloat) -> NSAttributedString { 131 | guard let date = Date.iso8601Formatter.date(from: self.uploadedDateString) else { 132 | return NSAttributedString(); 133 | } 134 | 135 | let attributes = [ 136 | NSAttributedString.Key.foregroundColor : UIColor.mainBarTintColor, 137 | NSAttributedString.Key.font: UIFont.systemFont(ofSize: size) 138 | ] 139 | 140 | return NSAttributedString(string: Date.timeStringSince(fromConverted: date), attributes: attributes) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /ASDKgram-Swift/PhotoTableNodeCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoTableNodeCell.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 09/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.// 18 | 19 | import Foundation 20 | import AsyncDisplayKit 21 | 22 | class PhotoTableNodeCell: ASCellNode { 23 | 24 | let usernameLabel = ASTextNode() 25 | let timeIntervalLabel = ASTextNode() 26 | let photoLikesLabel = ASTextNode() 27 | let photoDescriptionLabel = ASTextNode() 28 | 29 | let avatarImageNode: ASNetworkImageNode = { 30 | let imageNode = ASNetworkImageNode() 31 | imageNode.contentMode = .scaleAspectFill 32 | imageNode.imageModificationBlock = ASImageNodeRoundBorderModificationBlock(0, nil) 33 | return imageNode 34 | }() 35 | 36 | let photoImageNode: ASNetworkImageNode = { 37 | let imageNode = ASNetworkImageNode() 38 | imageNode.contentMode = .scaleAspectFill 39 | return imageNode 40 | }() 41 | 42 | // MARK: Lifecycle 43 | 44 | init(photoModel: PhotoModel) { 45 | super.init() 46 | automaticallyManagesSubnodes = true 47 | photoImageNode.url = URL(string: photoModel.url) 48 | avatarImageNode.url = URL(string: photoModel.user.profileImage) 49 | usernameLabel.attributedText = photoModel.attributedStringForUserName(withSize: Constants.CellLayout.FontSize) 50 | timeIntervalLabel.attributedText = photoModel.attributedStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) 51 | photoLikesLabel.attributedText = photoModel.attributedStringLikes(withSize: Constants.CellLayout.FontSize) 52 | photoDescriptionLabel.attributedText = photoModel.attributedStringForDescription(withSize: Constants.CellLayout.FontSize) 53 | } 54 | 55 | // MARK: ASDisplayNode 56 | 57 | override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { 58 | 59 | // Header Stack 60 | 61 | var headerChildren: [ASLayoutElement] = [] 62 | 63 | let headerStack = ASStackLayoutSpec.horizontal() 64 | headerStack.alignItems = .center 65 | avatarImageNode.style.preferredSize = CGSize(width: Constants.CellLayout.UserImageHeight, height: Constants.CellLayout.UserImageHeight) 66 | headerChildren.append(ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForAvatar, child: avatarImageNode)) 67 | usernameLabel.style.flexShrink = 1.0 68 | headerChildren.append(usernameLabel) 69 | 70 | let spacer = ASLayoutSpec() 71 | spacer.style.flexGrow = 1.0 72 | headerChildren.append(spacer) 73 | 74 | timeIntervalLabel.style.spacingBefore = Constants.CellLayout.HorizontalBuffer 75 | headerChildren.append(timeIntervalLabel) 76 | 77 | let footerStack = ASStackLayoutSpec.vertical() 78 | footerStack.spacing = Constants.CellLayout.VerticalBuffer 79 | footerStack.children = [photoLikesLabel, photoDescriptionLabel] 80 | headerStack.children = headerChildren 81 | 82 | let verticalStack = ASStackLayoutSpec.vertical() 83 | 84 | verticalStack.children = [ 85 | ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForHeader, child: headerStack), 86 | ASRatioLayoutSpec(ratio: 1.0, child: photoImageNode), 87 | ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForFooter, child: footerStack) 88 | ] 89 | 90 | return verticalStack 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /ASDKgram-Swift/PhotoTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PhotoTableViewCell.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 08/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | 20 | import UIKit 21 | 22 | class PhotoTableViewCell: UITableViewCell { 23 | 24 | var photoModel: PhotoModel? { 25 | didSet { 26 | if let model = photoModel { 27 | photoImageView.loadImageUsingUrlString(urlString: model.url) 28 | avatarImageView.loadImageUsingUrlString(urlString: model.user.profileImage) 29 | photoLikesLabel.attributedText = model.attributedStringLikes(withSize: Constants.CellLayout.FontSize) 30 | usernameLabel.attributedText = model.attributedStringForUserName(withSize: Constants.CellLayout.FontSize) 31 | timeIntervalLabel.attributedText = model.attributedStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) 32 | photoDescriptionLabel.attributedText = model.attributedStringForDescription(withSize: Constants.CellLayout.FontSize) 33 | photoDescriptionLabel.sizeToFit() 34 | var rect = photoDescriptionLabel.frame 35 | let availableWidth = self.bounds.size.width - Constants.CellLayout.HorizontalBuffer * 2 36 | rect.size = model.attributedStringForDescription(withSize: Constants.CellLayout.FontSize).boundingRect(with: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size 37 | photoDescriptionLabel.frame = rect 38 | } 39 | } 40 | } 41 | 42 | let photoImageView: NetworkImageView = { 43 | let imageView = NetworkImageView() 44 | imageView.contentMode = .scaleAspectFill 45 | imageView.translatesAutoresizingMaskIntoConstraints = false 46 | return imageView 47 | }() 48 | 49 | let avatarImageView: NetworkImageView = { 50 | let imageView = NetworkImageView() 51 | imageView.contentMode = .scaleAspectFill 52 | imageView.translatesAutoresizingMaskIntoConstraints = false 53 | imageView.layer.cornerRadius = Constants.CellLayout.UserImageHeight / 2 54 | imageView.clipsToBounds = true 55 | return imageView 56 | }() 57 | 58 | let usernameLabel: UILabel = { 59 | let label = UILabel() 60 | label.translatesAutoresizingMaskIntoConstraints = false 61 | return label 62 | }() 63 | 64 | let timeIntervalLabel: UILabel = { 65 | let label = UILabel() 66 | label.translatesAutoresizingMaskIntoConstraints = false 67 | return label 68 | }() 69 | 70 | let photoLikesLabel: UILabel = { 71 | let label = UILabel() 72 | label.translatesAutoresizingMaskIntoConstraints = false 73 | return label 74 | }() 75 | 76 | let photoDescriptionLabel: UILabel = { 77 | let label = UILabel() 78 | label.translatesAutoresizingMaskIntoConstraints = false 79 | label.numberOfLines = 3 80 | return label 81 | }() 82 | 83 | override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { 84 | super.init(style: style, reuseIdentifier: reuseIdentifier) 85 | setupViews() 86 | } 87 | 88 | required init?(coder aDecoder: NSCoder) { 89 | fatalError("init(coder:) has not been implemented") 90 | } 91 | 92 | func setupViews() { 93 | addSubview(photoImageView) 94 | addSubview(avatarImageView) 95 | addSubview(usernameLabel) 96 | addSubview(timeIntervalLabel) 97 | addSubview(photoLikesLabel) 98 | addSubview(photoDescriptionLabel) 99 | setupConstraints() 100 | } 101 | 102 | func setupConstraints() { 103 | 104 | NSLayoutConstraint.activate ([ 105 | //photoImageView 106 | photoImageView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.CellLayout.HeaderHeight), 107 | photoImageView.widthAnchor.constraint(equalTo: widthAnchor), 108 | photoImageView.heightAnchor.constraint(equalTo: photoImageView.widthAnchor), 109 | // avatarImageView 110 | avatarImageView.leftAnchor.constraint(equalTo: leftAnchor, constant: Constants.CellLayout.HorizontalBuffer), 111 | avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: Constants.CellLayout.HorizontalBuffer), 112 | avatarImageView.heightAnchor.constraint(equalToConstant: Constants.CellLayout.UserImageHeight), 113 | avatarImageView.widthAnchor.constraint(equalTo: avatarImageView.heightAnchor), 114 | // usernameLabel 115 | usernameLabel.leftAnchor.constraint(equalTo: avatarImageView.rightAnchor, constant: Constants.CellLayout.HorizontalBuffer), 116 | usernameLabel.rightAnchor.constraint(equalTo: timeIntervalLabel.leftAnchor, constant: -Constants.CellLayout.HorizontalBuffer), 117 | usernameLabel.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), 118 | // timeIntervalLabel 119 | timeIntervalLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -Constants.CellLayout.HorizontalBuffer), 120 | timeIntervalLabel.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor), 121 | // photoLikesLabel 122 | photoLikesLabel.topAnchor.constraint(equalTo: photoImageView.bottomAnchor, constant: Constants.CellLayout.VerticalBuffer), 123 | photoLikesLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: Constants.CellLayout.HorizontalBuffer), 124 | // photoDescriptionLabel 125 | photoDescriptionLabel.topAnchor.constraint(equalTo: photoLikesLabel.bottomAnchor, constant: Constants.CellLayout.VerticalBuffer), 126 | photoDescriptionLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: Constants.CellLayout.HorizontalBuffer), 127 | photoDescriptionLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -Constants.CellLayout.HorizontalBuffer), 128 | photoDescriptionLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -Constants.CellLayout.VerticalBuffer) 129 | ]) 130 | } 131 | 132 | class func height(for photo: PhotoModel, withWidth width: CGFloat) -> CGFloat { 133 | let photoHeight = width 134 | let font = UIFont.systemFont(ofSize: Constants.CellLayout.FontSize) 135 | let likesHeight = round(font.lineHeight) 136 | let descriptionAttrString = photo.attributedStringForDescription(withSize: Constants.CellLayout.FontSize) 137 | let availableWidth = width - Constants.CellLayout.HorizontalBuffer * 2 138 | let descriptionHeight = descriptionAttrString.boundingRect(with: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size.height 139 | 140 | return likesHeight + descriptionHeight + photoHeight + Constants.CellLayout.HeaderHeight + Constants.CellLayout.VerticalBuffer * 3 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /ASDKgram-Swift/PopularPageModel.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PopularPageModel.swift 3 | // Texture 4 | // 5 | // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. 6 | // Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. 7 | // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | 10 | import Foundation 11 | 12 | struct PopularPageModel { 13 | let page: Int 14 | let totalPages: Int 15 | let totalNumberOfItems: Int 16 | let photos: [PhotoModel] 17 | 18 | init(metaData: ResponseMetadata, photos:[PhotoModel]) { 19 | self.page = metaData.currentPage 20 | self.totalPages = metaData.pagesTotal 21 | self.totalNumberOfItems = metaData.itemsTotal 22 | self.photos = photos 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ASDKgram-Swift/UIColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIColor.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 06/01/2017. 6 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 7 | // This source code is licensed under the BSD-style license found in the 8 | // LICENSE file in the root directory of this source tree. An additional grant 9 | // of patent rights can be found in the PATENTS file in the same directory. 10 | // 11 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 14 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 15 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 16 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 17 | // 18 | 19 | import UIKit 20 | 21 | extension UIColor { 22 | 23 | static var mainBarTintColor: UIColor { 24 | return UIColor(red: 69/255, green: 142/255, blue: 255/255, alpha: 1) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /ASDKgram-Swift/UIImage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // UIImage.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 18/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | 20 | import UIKit 21 | 22 | // This extension was copied directly from LayoutSpecExamples-Swift. It is an example of how to create Precomoposed Alpha Corners. I have used the helper ASImageNodeRoundBorderModificationBlock:boarderWidth:boarderColor function in practice which does the same. 23 | 24 | extension UIImage { 25 | 26 | func makeCircularImage(size: CGSize, borderWidth width: CGFloat) -> UIImage { 27 | // make a CGRect with the image's size 28 | let circleRect = CGRect(origin: .zero, size: size) 29 | 30 | // begin the image context since we're not in a drawRect: 31 | UIGraphicsBeginImageContextWithOptions(circleRect.size, false, 0) 32 | 33 | // create a UIBezierPath circle 34 | let circle = UIBezierPath(roundedRect: circleRect, cornerRadius: circleRect.size.width * 0.5) 35 | 36 | // clip to the circle 37 | circle.addClip() 38 | 39 | UIColor.white.set() 40 | circle.fill() 41 | 42 | // draw the image in the circleRect *AFTER* the context is clipped 43 | self.draw(in: circleRect) 44 | 45 | // create a border (for white background pictures) 46 | if width > 0 { 47 | circle.lineWidth = width 48 | UIColor.white.set() 49 | circle.stroke() 50 | } 51 | 52 | // get an image from the image context 53 | let roundedImage = UIGraphicsGetImageFromCurrentImageContext() 54 | 55 | // end the image context since we're not in a drawRect: 56 | UIGraphicsEndImageContext() 57 | 58 | return roundedImage ?? self 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ASDKgram-Swift/URL.swift: -------------------------------------------------------------------------------- 1 | // 2 | // URL.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 07/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | 20 | import UIKit 21 | 22 | extension URL { 23 | 24 | static func URLForFeedModelType(feedModelType: PhotoFeedModelType) -> URL { 25 | switch feedModelType { 26 | case .photoFeedModelTypePopular: 27 | return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.PopularEndpoint))! 28 | 29 | case .photoFeedModelTypeLocation: 30 | return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.SearchEndpoint))! 31 | 32 | case .photoFeedModelTypeUserPhotos: 33 | return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.UserEndpoint))! 34 | } 35 | } 36 | 37 | private static func assembleUnsplashURLString(endpoint: String) -> String { 38 | return Constants.Unsplash.URLS.Host + endpoint + Constants.Unsplash.URLS.ConsumerKey 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /ASDKgram-Swift/Webservice.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Webservice.swift 3 | // ASDKgram-Swift 4 | // 5 | // Created by Calum Harris on 06/01/2017. 6 | // 7 | // Copyright (c) 2014-present, Facebook, Inc. All rights reserved. 8 | // This source code is licensed under the BSD-style license found in the 9 | // LICENSE file in the root directory of this source tree. An additional grant 10 | // of patent rights can be found in the PATENTS file in the same directory. 11 | // 12 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 13 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 14 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 15 | // FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN 16 | // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 17 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 18 | // 19 | // swiftlint:disable force_cast 20 | 21 | import UIKit 22 | 23 | final class WebService { 24 | func load(resource: Resource, completion: @escaping (Result) -> ()) { 25 | URLSession.shared.dataTask(with: resource.url) { data, response, error in 26 | // Check for errors in responses. 27 | let result = self.checkForNetworkErrors(data, response, error) 28 | DispatchQueue.main.async { 29 | switch result { 30 | case .success(let data): 31 | completion(resource.parse(data, response)) 32 | case .failure(let error): 33 | completion(.failure(error)) 34 | } 35 | } 36 | }.resume() 37 | } 38 | } 39 | 40 | extension WebService { 41 | 42 | fileprivate func checkForNetworkErrors(_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Result { 43 | // Check for errors in responses. 44 | guard error == nil else { 45 | switch error! { 46 | case URLError.notConnectedToInternet, URLError.timedOut: 47 | return .failure(.noInternetConnection) 48 | default: 49 | return .failure(.returnedError(error!)) 50 | } 51 | } 52 | 53 | guard let statusCode = (response as? HTTPURLResponse)?.statusCode, statusCode >= 200 && statusCode <= 299 else { 54 | return .failure((.invalidStatusCode("Request returned status code other than 2xx \(String(describing: response))"))) 55 | } 56 | 57 | guard let data = data else { return .failure(.dataReturnedNil) } 58 | 59 | return .success(data) 60 | } 61 | } 62 | 63 | struct ResponseMetadata { 64 | let currentPage: Int 65 | let itemsTotal: Int 66 | let itemsPerPage: Int 67 | } 68 | 69 | extension ResponseMetadata { 70 | var pagesTotal: Int { 71 | return itemsTotal / itemsPerPage 72 | } 73 | } 74 | 75 | struct Resource { 76 | let url: URL 77 | let parse: (Data, URLResponse?) -> Result 78 | } 79 | 80 | extension Resource { 81 | 82 | init(url: URL, page:Int, parseResponse: @escaping (ResponseMetadata, Data) -> Result) { 83 | 84 | guard let url = URL(string: url.absoluteString.appending("&page=\(page)")) else { fatalError("Malformed URL given") } 85 | self.url = url 86 | self.parse = { data, response in 87 | guard let httpUrlResponse = response as? HTTPURLResponse, 88 | let xTotalString = httpUrlResponse.allHeaderFields["x-total"] as? String, 89 | let xTotal = Int(xTotalString), 90 | let xPerPageString = httpUrlResponse.allHeaderFields["x-per-page"] as? String, 91 | let xPerPage = Int(xPerPageString) 92 | else { return .failure(.errorParsingResponse) } 93 | let metadata = ResponseMetadata(currentPage: page, itemsTotal: xTotal, itemsPerPage: xPerPage) 94 | return parseResponse(metadata, data) 95 | } 96 | } 97 | } 98 | 99 | enum Result { 100 | case success(T) 101 | case failure(NetworkingErrors) 102 | } 103 | 104 | enum NetworkingErrors: Error { 105 | case errorParsingResponse 106 | case errorParsingJSON 107 | case noInternetConnection 108 | case dataReturnedNil 109 | case returnedError(Error) 110 | case invalidStatusCode(String) 111 | case customError(String) 112 | } 113 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | source 'https://github.com/CocoaPods/Specs.git' 2 | platform :ios, '12.0' 3 | target 'ASDKgram-Swift' do 4 | use_frameworks! 5 | inhibit_all_warnings! 6 | pod 'Texture/IGListKit' 7 | 8 | end 9 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - IGListKit (3.4.0): 3 | - IGListKit/Default (= 3.4.0) 4 | - IGListKit/Default (3.4.0): 5 | - IGListKit/Diffing 6 | - IGListKit/Diffing (3.4.0) 7 | - Texture/Core (2.8.1) 8 | - Texture/IGListKit (2.8.1): 9 | - IGListKit (~> 3.0) 10 | - Texture/Core 11 | 12 | DEPENDENCIES: 13 | - Texture/IGListKit 14 | 15 | SPEC REPOS: 16 | https://github.com/cocoapods/specs.git: 17 | - IGListKit 18 | - Texture 19 | 20 | SPEC CHECKSUMS: 21 | IGListKit: 7a5d788e9fb746bcd402baa8e8b24bc3bd2a5a07 22 | Texture: 8ecf6984065a1e54f06bf97b349ecca28582acd7 23 | 24 | PODFILE CHECKSUM: fbdc42ff35e465b5f2d0031d9a59493fadc842da 25 | 26 | COCOAPODS: 1.6.1 27 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IGListKit-AsyncDisplayKit-Example 2 | An Swift programming example using AsyncDisplayKit and IGListKit based on Offical example. 3 | 4 | ![Result](https://github.com/QiuDaniel/IGListKit-AsyncDisplayKit-Example/blob/master/snap.gif) -------------------------------------------------------------------------------- /snap.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QiuDaniel/IGListKit-AsyncDisplayKit-Example/47e73296d135294a3a1817de790d7cbba1803142/snap.gif --------------------------------------------------------------------------------