Rewrite Swift Example (#1002)

* Rewrite Swift Example

* Add license header to OrderedDictionary
This commit is contained in:
Michael Schneider 2018-07-08 08:55:28 -07:00 committed by GitHub
parent 6c487dd26c
commit c8b5a1b323
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 1397 additions and 573 deletions

View File

@ -278,8 +278,6 @@
05E2127D19D4DB510098F589 /* Sources */, 05E2127D19D4DB510098F589 /* Sources */,
05E2127E19D4DB510098F589 /* Frameworks */, 05E2127E19D4DB510098F589 /* Frameworks */,
05E2127F19D4DB510098F589 /* Resources */, 05E2127F19D4DB510098F589 /* Resources */,
F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */,
06770D39D4186D6446B1BDD5 /* [CP] Embed Pods Frameworks */,
); );
buildRules = ( buildRules = (
); );
@ -338,21 +336,6 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
06770D39D4186D6446B1BDD5 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = { E080B80F89C34A25B3488E26 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -368,22 +351,7 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "diff \"${PODS_ROOT}/../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"; 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";
showEnvVarsInLog = 0;
};
F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-Sample/Pods-Sample-resources.sh\"\n";
showEnvVarsInLog = 0; showEnvVarsInLog = 0;
}; };
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
3A2362FB1E2D33A0007E08F1 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2362FA1E2D33A0007E08F1 /* Date.swift */; }; 3A2362FB1E2D33A0007E08F1 /* Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A2362FA1E2D33A0007E08F1 /* Date.swift */; };
3A7A28D91E2F7410003E2B8D /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3A7A28D81E2F7410003E2B8D /* UIImage.swift */; };
3AB33F5E1E1F94530039F711 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F5D1E1F94530039F711 /* AppDelegate.swift */; }; 3AB33F5E1E1F94530039F711 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F5D1E1F94530039F711 /* AppDelegate.swift */; };
3AB33F651E1F94530039F711 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AB33F641E1F94530039F711 /* Assets.xcassets */; }; 3AB33F651E1F94530039F711 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AB33F641E1F94530039F711 /* Assets.xcassets */; };
3AB33F681E1F94530039F711 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3AB33F661E1F94530039F711 /* LaunchScreen.storyboard */; }; 3AB33F681E1F94530039F711 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 3AB33F661E1F94530039F711 /* LaunchScreen.storyboard */; };
@ -21,17 +20,20 @@
3AB33F881E20ED460039F711 /* PhotoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F871E20ED460039F711 /* PhotoModel.swift */; }; 3AB33F881E20ED460039F711 /* PhotoModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F871E20ED460039F711 /* PhotoModel.swift */; };
3AB33F8C1E2106F30039F711 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F8B1E2106F30039F711 /* URL.swift */; }; 3AB33F8C1E2106F30039F711 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F8B1E2106F30039F711 /* URL.swift */; };
3AB33F961E2269D40039F711 /* PopularPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F951E2269D40039F711 /* PopularPageModel.swift */; }; 3AB33F961E2269D40039F711 /* PopularPageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F951E2269D40039F711 /* PopularPageModel.swift */; };
3AB33F981E22A0080039F711 /* PX500Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F971E22A0080039F711 /* PX500Convenience.swift */; }; 3AB33F981E22A0080039F711 /* ParseResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F971E22A0080039F711 /* ParseResponse.swift */; };
3AB33F9E1E22D9DB0039F711 /* PhotoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */; }; 3AB33F9E1E22D9DB0039F711 /* PhotoTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */; };
3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33FA11E230A160039F711 /* NetworkImageView.swift */; }; 3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33FA11E230A160039F711 /* NetworkImageView.swift */; };
3AB33FA41E2337850039F711 /* PhotoTableNodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */; }; 3AB33FA41E2337850039F711 /* PhotoTableNodeCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */; };
692CD06E20E1A40D00D9B963 /* NumberFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 692CD06D20E1A40D00D9B963 /* NumberFormatter.swift */; };
7E438240D2C4026931D60594 /* Pods_ASDKgram_Swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */; }; 7E438240D2C4026931D60594 /* Pods_ASDKgram_Swift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */; };
9D4DFC5E20E1DF660067C960 /* OrderedDictionary+Codable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */; };
9D4DFC5F20E1DF660067C960 /* OrderedDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */; };
9D4DFC6020E1DF660067C960 /* OrderedDictionary+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
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 = "<group>"; }; 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 = "<group>"; };
3A2362FA1E2D33A0007E08F1 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; }; 3A2362FA1E2D33A0007E08F1 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
3A7A28D81E2F7410003E2B8D /* UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
3AB33F5A1E1F94520039F711 /* ASDKgram-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ASDKgram-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 3AB33F5A1E1F94520039F711 /* ASDKgram-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ASDKgram-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; };
3AB33F5D1E1F94530039F711 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 3AB33F5D1E1F94530039F711 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
3AB33F641E1F94530039F711 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 3AB33F641E1F94530039F711 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -46,11 +48,15 @@
3AB33F871E20ED460039F711 /* PhotoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoModel.swift; sourceTree = "<group>"; }; 3AB33F871E20ED460039F711 /* PhotoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoModel.swift; sourceTree = "<group>"; };
3AB33F8B1E2106F30039F711 /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; }; 3AB33F8B1E2106F30039F711 /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
3AB33F951E2269D40039F711 /* PopularPageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularPageModel.swift; sourceTree = "<group>"; }; 3AB33F951E2269D40039F711 /* PopularPageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularPageModel.swift; sourceTree = "<group>"; };
3AB33F971E22A0080039F711 /* PX500Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PX500Convenience.swift; sourceTree = "<group>"; }; 3AB33F971E22A0080039F711 /* ParseResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseResponse.swift; sourceTree = "<group>"; };
3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableViewCell.swift; sourceTree = "<group>"; }; 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableViewCell.swift; sourceTree = "<group>"; };
3AB33FA11E230A160039F711 /* NetworkImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageView.swift; sourceTree = "<group>"; }; 3AB33FA11E230A160039F711 /* NetworkImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageView.swift; sourceTree = "<group>"; };
3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableNodeCell.swift; sourceTree = "<group>"; }; 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableNodeCell.swift; sourceTree = "<group>"; };
4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ASDKgram_Swift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ASDKgram_Swift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
692CD06D20E1A40D00D9B963 /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = "<group>"; };
9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Codable.swift"; sourceTree = "<group>"; };
9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = "<group>"; };
9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Description.swift"; sourceTree = "<group>"; };
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 = "<group>"; }; 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 = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -87,6 +93,7 @@
3AB33F5C1E1F94530039F711 /* ASDKgram-Swift */ = { 3AB33F5C1E1F94530039F711 /* ASDKgram-Swift */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
9D4DFC5A20E1DF660067C960 /* OrderedDictionary */,
3AB33F991E22CF160039F711 /* Views */, 3AB33F991E22CF160039F711 /* Views */,
3AB33F841E20E98C0039F711 /* Model */, 3AB33F841E20E98C0039F711 /* Model */,
3AB33F7D1E1FDA890039F711 /* Client */, 3AB33F7D1E1FDA890039F711 /* Client */,
@ -129,10 +136,10 @@
3AB33F791E1F9E4E0039F711 /* Extensions */ = { 3AB33F791E1F9E4E0039F711 /* Extensions */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3A2362FA1E2D33A0007E08F1 /* Date.swift */,
692CD06D20E1A40D00D9B963 /* NumberFormatter.swift */,
3AB33F7A1E1F9E630039F711 /* UIColor.swift */, 3AB33F7A1E1F9E630039F711 /* UIColor.swift */,
3AB33F8B1E2106F30039F711 /* URL.swift */, 3AB33F8B1E2106F30039F711 /* URL.swift */,
3A2362FA1E2D33A0007E08F1 /* Date.swift */,
3A7A28D81E2F7410003E2B8D /* UIImage.swift */,
); );
name = Extensions; name = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -141,7 +148,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
3AB33F801E1FDE100039F711 /* Webservice.swift */, 3AB33F801E1FDE100039F711 /* Webservice.swift */,
3AB33F971E22A0080039F711 /* PX500Convenience.swift */, 3AB33F971E22A0080039F711 /* ParseResponse.swift */,
); );
name = Client; name = Client;
sourceTree = "<group>"; sourceTree = "<group>";
@ -191,6 +198,16 @@
name = Pods; name = Pods;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
9D4DFC5A20E1DF660067C960 /* OrderedDictionary */ = {
isa = PBXGroup;
children = (
9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */,
9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */,
9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */,
);
path = OrderedDictionary;
sourceTree = "<group>";
};
A7DD645D70CF34C7CA3B1A8B /* Frameworks */ = { A7DD645D70CF34C7CA3B1A8B /* Frameworks */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -211,7 +228,6 @@
3AB33F571E1F94520039F711 /* Frameworks */, 3AB33F571E1F94520039F711 /* Frameworks */,
3AB33F581E1F94520039F711 /* Resources */, 3AB33F581E1F94520039F711 /* Resources */,
154783123A953C3AFB9805CF /* [CP] Embed Pods Frameworks */, 154783123A953C3AFB9805CF /* [CP] Embed Pods Frameworks */,
07D25AC7E9C9518F14F0C929 /* [CP] Copy Pods Resources */,
3A7BEDD71E254278005769D4 /* ShellScript */, 3A7BEDD71E254278005769D4 /* ShellScript */,
); );
buildRules = ( buildRules = (
@ -272,21 +288,6 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
07D25AC7E9C9518F14F0C929 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
);
name = "[CP] Copy Pods Resources";
outputPaths = (
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${SRCROOT}/Pods/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift-resources.sh\"\n";
showEnvVarsInLog = 0;
};
154783123A953C3AFB9805CF /* [CP] Embed Pods Frameworks */ = { 154783123A953C3AFB9805CF /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@ -352,14 +353,17 @@
3AB33F781E1F9C400039F711 /* PhotoFeedTableNodeController.swift in Sources */, 3AB33F781E1F9C400039F711 /* PhotoFeedTableNodeController.swift in Sources */,
3A2362FB1E2D33A0007E08F1 /* Date.swift in Sources */, 3A2362FB1E2D33A0007E08F1 /* Date.swift in Sources */,
3AB33F7B1E1F9E630039F711 /* UIColor.swift in Sources */, 3AB33F7B1E1F9E630039F711 /* UIColor.swift in Sources */,
3AB33F981E22A0080039F711 /* PX500Convenience.swift in Sources */, 3AB33F981E22A0080039F711 /* ParseResponse.swift in Sources */,
692CD06E20E1A40D00D9B963 /* NumberFormatter.swift in Sources */,
3AB33FA41E2337850039F711 /* PhotoTableNodeCell.swift in Sources */, 3AB33FA41E2337850039F711 /* PhotoTableNodeCell.swift in Sources */,
3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */, 3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */,
9D4DFC6020E1DF660067C960 /* OrderedDictionary+Description.swift in Sources */,
3AB33F8C1E2106F30039F711 /* URL.swift in Sources */, 3AB33F8C1E2106F30039F711 /* URL.swift in Sources */,
3AB33F831E20E81E0039F711 /* Constants.swift in Sources */, 3AB33F831E20E81E0039F711 /* Constants.swift in Sources */,
9D4DFC5F20E1DF660067C960 /* OrderedDictionary.swift in Sources */,
3AB33F961E2269D40039F711 /* PopularPageModel.swift in Sources */, 3AB33F961E2269D40039F711 /* PopularPageModel.swift in Sources */,
3A7A28D91E2F7410003E2B8D /* UIImage.swift in Sources */,
3AB33F5E1E1F94530039F711 /* AppDelegate.swift in Sources */, 3AB33F5E1E1F94530039F711 /* AppDelegate.swift in Sources */,
9D4DFC5E20E1DF660067C960 /* OrderedDictionary+Codable.swift in Sources */,
3AB33F811E1FDE100039F711 /* Webservice.swift in Sources */, 3AB33F811E1FDE100039F711 /* Webservice.swift in Sources */,
3AB33F9E1E22D9DB0039F711 /* PhotoTableViewCell.swift in Sources */, 3AB33F9E1E22D9DB0039F711 /* PhotoTableViewCell.swift in Sources */,
3AB33F861E20E9B10039F711 /* PhotoFeedModel.swift in Sources */, 3AB33F861E20E9B10039F711 /* PhotoFeedModel.swift in Sources */,

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -2,19 +2,17 @@
// AppDelegate.swift // AppDelegate.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 06/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import UIKit import UIKit
@ -42,11 +40,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
let tabBarController = UITabBarController() let tabBarController = UITabBarController()
tabBarController.viewControllers = [UIKitNavController, ASDKNavController] tabBarController.viewControllers = [UIKitNavController, ASDKNavController]
tabBarController.selectedIndex = 1 tabBarController.selectedIndex = 1
tabBarController.tabBar.tintColor = UIColor.mainBarTintColor() tabBarController.tabBar.tintColor = UIColor.mainBarTintColor
// Nav Bar appearance // Nav Bar appearance
UINavigationBar.appearance().barTintColor = UIColor.mainBarTintColor() UINavigationBar.appearance().barTintColor = UIColor.mainBarTintColor
// UIWindow // UIWindow

View File

@ -2,35 +2,33 @@
// Constants // Constants
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 07/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
// swiftlint:disable nesting // swiftlint:disable nesting
import UIKit import UIKit
struct Constants { struct Constants {
struct Unsplash {
struct PX500 { struct URLS {
struct URLS { static let Host = "https://api.unsplash.com/"
static let Host = "https://api.500px.com/v1/" static let PopularEndpoint = "photos?order_by=popular"
static let PopularEndpoint = "photos?feature=popular&exclude=Nude,People,Fashion&sort=rating&image_size=3&include_store=store_download&include_states=voted" static let SearchEndpoint = "photos/search?geo=" //latitude,longitude,radius<units>
static let SearchEndpoint = "photos/search?geo=" //latitude,longitude,radius<units> static let UserEndpoint = "photos?user_id="
static let UserEndpoint = "photos?user_id=" static let ConsumerKey = "&client_id=3b99a69cee09770a4a0bbb870b437dbda53efb22f6f6de63714b71c4df7c9642"
static let ConsumerKey = "&consumer_key=Fi13GVb8g53sGvHICzlram7QkKOlSDmAmp9s9aqC" static let ImagesPerPage = 30
} }
} }
struct CellLayout { struct CellLayout {
static let FontSize: CGFloat = 14 static let FontSize: CGFloat = 14

View File

@ -2,25 +2,22 @@
// Date.swift // Date.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 16/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import Foundation import Foundation
extension Date { extension Date {
static let iso8601Formatter: DateFormatter = { static let iso8601Formatter: DateFormatter = {
let formatter = DateFormatter() let formatter = DateFormatter()
formatter.calendar = Calendar(identifier: .iso8601) formatter.calendar = Calendar(identifier: .iso8601)
@ -29,4 +26,22 @@ extension Date {
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
return formatter return formatter
}() }()
static func timeStringSince(fromConverted date: Date) -> String {
let diffDates = NSCalendar.current.dateComponents([.day, .hour, .second], from: date, to: Date())
if let week = diffDates.day, week > 7 {
return "\(week / 7)w"
} else if let day = diffDates.day, day > 0 {
return "\(day)d"
} else if let hour = diffDates.hour, hour > 0 {
return "\(hour)h"
} else if let second = diffDates.second, second > 0 {
return "\(second)s"
} else if let zero = diffDates.second, zero == 0 {
return "1s"
} else {
return "ERROR"
}
}
} }

View File

@ -2,19 +2,17 @@
// NetworkImageView.swift // NetworkImageView.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 09/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import UIKit import UIKit

View File

@ -0,0 +1,26 @@
//
// NumberFormatter.swift
// ASDKgram-Swift
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// grant of patent rights can be found in the PATENTS file in the same directory.
//
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
import Foundation
extension NumberFormatter {
static let decimalNumberFormatter: NumberFormatter = {
let formatter = NumberFormatter()
formatter.numberStyle = .decimal
return formatter
}()
}

View File

@ -0,0 +1,144 @@
/**
Copyright (c) 2015-2017 Lukas Kubanek
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
#if swift(>=4.1)
extension OrderedDictionary: Encodable where Key: Encodable, Value: Encodable {
/// __inheritdoc__
public func encode(to encoder: Encoder) throws {
// Encode the ordered dictionary as an array of alternating key-value pairs.
var container = encoder.unkeyedContainer()
for (key, value) in self {
try container.encode(key)
try container.encode(value)
}
}
}
extension OrderedDictionary: Decodable where Key: Decodable, Value: Decodable {
/// __inheritdoc__
public init(from decoder: Decoder) throws {
// Decode the ordered dictionary from an array of alternating key-value pairs.
self.init()
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
let key = try container.decode(Key.self)
guard !container.isAtEnd else { throw DecodingError.unkeyedContainerReachedEndBeforeValue(decoder.codingPath) }
let value = try container.decode(Value.self)
self[key] = value
}
}
}
#else
extension OrderedDictionary: Encodable {
/// __inheritdoc__
public func encode(to encoder: Encoder) throws {
// Since Swift 4.0 lacks the protocol conditional conformance support, we have to make the
// whole OrderedDictionary type conform to Encodable and assert that the key and value
// types conform to Encodable. Furthermore, we leverage a trick of super encoders to be
// able to encode objects without knowing their exact types. This trick was used in the
// standard library for encoding/decoding Dictionary before Swift 4.1.
_assertTypeIsEncodable(Key.self, in: type(of: self))
_assertTypeIsEncodable(Value.self, in: type(of: self))
var container = encoder.unkeyedContainer()
for (key, value) in self {
let keyEncoder = container.superEncoder()
try (key as! Encodable).encode(to: keyEncoder)
let valueEncoder = container.superEncoder()
try (value as! Encodable).encode(to: valueEncoder)
}
}
private func _assertTypeIsEncodable<T>(_ type: T.Type, in wrappingType: Any.Type) {
guard T.self is Encodable.Type else {
if T.self == Encodable.self || T.self == Codable.self {
preconditionFailure("\(wrappingType) does not conform to Encodable because Encodable does not conform to itself. You must use a concrete type to encode or decode.")
} else {
preconditionFailure("\(wrappingType) does not conform to Encodable because \(T.self) does not conform to Encodable.")
}
}
}
}
extension OrderedDictionary: Decodable {
/// __inheritdoc__
public init(from decoder: Decoder) throws {
// Since Swift 4.0 lacks the protocol conditional conformance support, we have to make the
// whole OrderedDictionary type conform to Decodable and assert that the key and value
// types conform to Decodable. Furthermore, we leverage a trick of super decoders to be
// able to decode objects without knowing their exact types. This trick was used in the
// standard library for encoding/decoding Dictionary before Swift 4.1.
self.init()
_assertTypeIsDecodable(Key.self, in: type(of: self))
_assertTypeIsDecodable(Value.self, in: type(of: self))
var container = try decoder.unkeyedContainer()
let keyMetaType = (Key.self as! Decodable.Type)
let valueMetaType = (Value.self as! Decodable.Type)
while !container.isAtEnd {
let keyDecoder = try container.superDecoder()
let key = try keyMetaType.init(from: keyDecoder) as! Key
guard !container.isAtEnd else { throw DecodingError.unkeyedContainerReachedEndBeforeValue(decoder.codingPath) }
let valueDecoder = try container.superDecoder()
let value = try valueMetaType.init(from: valueDecoder) as! Value
self[key] = value
}
}
private func _assertTypeIsDecodable<T>(_ type: T.Type, in wrappingType: Any.Type) {
guard T.self is Decodable.Type else {
if T.self == Decodable.self || T.self == Codable.self {
preconditionFailure("\(wrappingType) does not conform to Decodable because Decodable does not conform to itself. You must use a concrete type to encode or decode.")
} else {
preconditionFailure("\(wrappingType) does not conform to Decodable because \(T.self) does not conform to Decodable.")
}
}
}
}
#endif
fileprivate extension DecodingError {
fileprivate static func unkeyedContainerReachedEndBeforeValue(_ codingPath: [CodingKey]) -> DecodingError {
return DecodingError.dataCorrupted(
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Unkeyed container reached end before value in key-value pair."
)
)
}
}

View File

@ -0,0 +1,60 @@
/**
Copyright (c) 2015-2017 Lukas Kubanek
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
extension OrderedDictionary: CustomStringConvertible {
/// A textual representation of the ordered dictionary.
public var description: String {
return makeDescription(debug: false)
}
}
extension OrderedDictionary: CustomDebugStringConvertible {
/// A textual representation of the ordered dictionary, suitable for debugging.
public var debugDescription: String {
return makeDescription(debug: true)
}
}
extension OrderedDictionary {
fileprivate func makeDescription(debug: Bool) -> String {
// The implementation of the description is inspired by zwaldowski's implementation of the
// ordered dictionary. See http://bit.ly/2iqGhrb
if isEmpty { return "[:]" }
let printFunction: (Any, inout String) -> () = {
if debug {
return { debugPrint($0, separator: "", terminator: "", to: &$1) }
} else {
return { print($0, separator: "", terminator: "", to: &$1) }
}
}()
let descriptionForItem: (Any) -> String = { item in
var description = ""
printFunction(item, &description)
return description
}
let bodyComponents = map { element in
return descriptionForItem(element.key) + ": " + descriptionForItem(element.value)
}
let body = bodyComponents.joined(separator: ", ")
return "[\(body)]"
}
}

View File

@ -0,0 +1,620 @@
/**
Copyright (c) 2015-2017 Lukas Kubanek
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
/// A generic collection for storing key-value pairs in an ordered manner.
///
/// Same as in a dictionary all keys in the collection are unique and have an associated value.
/// Same as in an array, all key-value pairs (elements) are kept sorted and accessible by
/// a zero-based integer index.
public struct OrderedDictionary<Key: Hashable, Value>: BidirectionalCollection {
// ======================================================= //
// MARK: - Type Aliases
// ======================================================= //
/// The type of the key-value pair stored in the ordered dictionary.
public typealias Element = (key: Key, value: Value)
/// The type of the index.
public typealias Index = Int
/// The type of the indices collection.
public typealias Indices = CountableRange<Int>
/// The type of the contiguous subrange of the ordered dictionary's elements.
///
/// - SeeAlso: OrderedDictionarySlice
public typealias SubSequence = OrderedDictionarySlice<Key, Value>
// ======================================================= //
// MARK: - Initialization
// ======================================================= //
/// Creates an empty ordered dictionary.
public init() {}
/// Creates an ordered dictionary from a sequence of values keyed by a key which gets extracted
/// from the value in the provided closure.
///
/// - Parameter values: The sequence of values.
/// - Parameter getKey: The closure which provides a key for the given value from the values
/// sequence.
public init<Values: Sequence>(values: Values, keyedBy getKey: (Value) -> Key) where Values.Element == Value {
self.init(values.map { (getKey($0), $0) })
}
/// Creates an ordered dictionary from a sequence of values keyed by a key loaded from the value
/// at the given key path.
///
/// - Parameter values: The sequence of values.
/// - Parameter keyPath: The key path for the value to locate its key at.
public init(values: [Value], keyedBy keyPath: KeyPath<Value, Key>) {
self.init(values.map { ($0[keyPath: keyPath], $0) })
}
/// Creates an ordered dictionary from a regular unsorted dictionary by sorting it using the
/// the given sort function.
///
/// - Parameter unsorted: The unsorted dictionary.
/// - Parameter areInIncreasingOrder: The sort function which compares the key-value pairs.
public init(unsorted: Dictionary<Key, Value>, areInIncreasingOrder: (Element, Element) -> Bool) {
let elements = unsorted
.map { (key: $0.key, value: $0.value) }
.sorted(by: areInIncreasingOrder)
self.init(elements)
}
/// Creates an ordered dictionary from a sequence of key-value pairs.
///
/// - Parameter elements: The key-value pairs that will make up the new ordered dictionary.
/// Each key in `elements` must be unique.
public init<S: Sequence>(_ elements: S) where S.Element == Element {
for (key, value) in elements {
precondition(!containsKey(key), "Elements sequence contains duplicate keys")
self[key] = value
}
}
// ======================================================= //
// MARK: - Ordered Keys & Values
// ======================================================= //
/// A collection containing just the keys of the ordered dictionary in the correct order.
public var orderedKeys: OrderedDictionaryKeys<Key, Value> {
return self.lazy.map { $0.key }
}
/// A collection containing just the values of the ordered dictionary in the correct order.
public var orderedValues: OrderedDictionaryValues<Key, Value> {
return self.lazy.map { $0.value }
}
// ======================================================= //
// MARK: - Dictionary
// ======================================================= //
/// Converts itself to a common unsorted dictionary.
public var unorderedDictionary: Dictionary<Key, Value> {
return _keysToValues
}
// ======================================================= //
// MARK: - Key-based Access
// ======================================================= //
/// Accesses the value associated with the given key for reading and writing.
///
/// This key-based subscript returns the value for the given key if the key is found in the
/// ordered dictionary, or `nil` if the key is not found.
///
/// When you assign a value for a key and that key already exists, the ordered dictionary
/// overwrites the existing value and preservers the index of the key-value pair. If the ordered
/// dictionary does not contain the key, a new key-value pair is appended to the end of the
/// ordered dictionary.
///
/// If you assign `nil` as the value for the given key, the ordered dictionary removes that key
/// and its associated value if it exists.
///
/// - Parameter key: The key to find in the ordered dictionary.
/// - Returns: The value associated with `key` if `key` is in the ordered dictionary; otherwise,
/// `nil`.
public subscript(key: Key) -> Value? {
get {
return value(forKey: key)
}
set(newValue) {
if let newValue = newValue {
updateValue(newValue, forKey: key)
} else {
removeValue(forKey: key)
}
}
}
/// Returns a Boolean value indicating whether the ordered dictionary contains the given key.
///
/// - Parameter key: The key to be looked up.
/// - Returns: `true` if the ordered dictionary contains the given key; otherwise, `false`.
public func containsKey(_ key: Key) -> Bool {
return _keysToValues[key] != nil
}
/// Returns the value associated with the given key if the key is found in the ordered
/// dictionary, or `nil` if the key is not found.
///
/// - Parameter key: The key to find in the ordered dictionary.
/// - Returns: The value associated with `key` if `key` is in the ordered dictionary; otherwise,
/// `nil`.
public func value(forKey key: Key) -> Value? {
return _keysToValues[key]
}
/// Updates the value stored in the ordered dictionary for the given key, or appends a new
/// key-value pair if the key does not exist.
///
/// - Parameter value: The new value to add to the ordered dictionary.
/// - Parameter key: The key to associate with `value`. If `key` already exists in the ordered
/// dictionary, `value` replaces the existing associated value. If `key` is not already a key
/// of the ordered dictionary, the `(key, value)` pair is appended at the end of the ordered
/// dictionary.
@discardableResult
public mutating func updateValue(_ value: Value, forKey key: Key) -> Value? {
if containsKey(key) {
let currentValue = _unsafeValue(forKey: key)
_keysToValues[key] = value
return currentValue
} else {
_orderedKeys.append(key)
_keysToValues[key] = value
return nil
}
}
/// Removes the given key and its associated value from the ordered dictionary.
///
/// If the key is found in the ordered dictionary, this method returns the key's associated
/// value. On removal, the indices of the ordered dictionary are invalidated. If the key is
/// not found in the ordered dictionary, this method returns `nil`.
///
/// - Parameter key: The key to remove along with its associated value.
/// - Returns: The value that was removed, or `nil` if the key was not present in the
/// ordered dictionary.
///
/// - SeeAlso: remove(at:)
@discardableResult
public mutating func removeValue(forKey key: Key) -> Value? {
guard let index = index(forKey: key) else { return nil }
let currentValue = self[index].value
_orderedKeys.remove(at: index)
_keysToValues[key] = nil
return currentValue
}
/// Removes all key-value pairs from the ordered dictionary and invalidates all indices.
///
/// - Parameter keepCapacity: Whether the ordered dictionary should keep its underlying storage.
/// If you pass `true`, the operation preserves the storage capacity that the collection has,
/// otherwise the underlying storage is released. The default is `false`.
public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) {
_orderedKeys.removeAll(keepingCapacity: keepCapacity)
_keysToValues.removeAll(keepingCapacity: keepCapacity)
}
private func _unsafeValue(forKey key: Key) -> Value {
let value = _keysToValues[key]
precondition(value != nil, "Inconsistency error occurred in OrderedDictionary")
return value!
}
// ======================================================= //
// MARK: - Index-based Access
// ======================================================= //
/// Accesses the key-value pair at the specified position.
///
/// The specified position has to be a valid index of the ordered dictionary. The index-base
/// subscript returns the key-value pair corresponding to the index.
///
/// - Parameter position: The position of the key-value pair to access. `position` must be
/// a valid index of the ordered dictionary and not equal to `endIndex`.
/// - Returns: A tuple containing the key-value pair corresponding to `position`.
///
/// - SeeAlso: update(:at:)
public subscript(position: Index) -> Element {
precondition(indices.contains(position), "OrderedDictionary index is out of range")
let key = _orderedKeys[position]
let value = _unsafeValue(forKey: key)
return (key, value)
}
/// Returns the index for the given key.
///
/// - Parameter key: The key to find in the ordered dictionary.
/// - Returns: The index for `key` and its associated value if `key` is in the ordered
/// dictionary; otherwise, `nil`.
public func index(forKey key: Key) -> Index? {
return _orderedKeys.index(of: key)
}
/// Returns the key-value pair at the specified index, or `nil` if there is no key-value pair
/// at that index.
///
/// - Parameter index: The index of the key-value pair to be looked up. `index` does not have to
/// be a valid index.
/// - Returns: A tuple containing the key-value pair corresponding to `index` if the index is
/// valid; otherwise, `nil`.
public func elementAt(_ index: Index) -> Element? {
return indices.contains(index) ? self[index] : nil
}
/// Checks whether the given key-value pair can be inserted into to ordered dictionary by
/// validating the presence of the key.
///
/// - Parameter newElement: The key-value pair to be inserted into the ordered dictionary.
/// - Returns: `true` if the key-value pair can be safely inserted; otherwise, `false`.
///
/// - SeeAlso: canInsert(key:)
/// - SeeAlso: canInsert(at:)
@available(*, deprecated, message: "Use canInsert(key:) with the element's key instead")
public func canInsert(_ newElement: Element) -> Bool {
return canInsert(key: newElement.key)
}
/// Checks whether a key-value pair with the given key can be inserted into the ordered
/// dictionary by validating its presence.
///
/// - Parameter key: The key to be inserted into the ordered dictionary.
/// - Returns: `true` if the key can safely be inserted; ortherwise, `false`.
///
/// - SeeAlso: canInsert(at:)
public func canInsert(key: Key) -> Bool {
return !containsKey(key)
}
/// Checks whether a new key-value pair can be inserted into the ordered dictionary at the
/// given index.
///
/// - Parameter index: The index the new key-value pair should be inserted at.
/// - Returns: `true` if a new key-value pair can be inserted at the specified index; otherwise,
/// `false`.
///
/// - SeeAlso: canInsert(key:)
public func canInsert(at index: Index) -> Bool {
return index >= startIndex && index <= endIndex
}
/// Inserts a new key-value pair at the specified position.
///
/// If the key of the inserted pair already exists in the ordered dictionary, a runtime error
/// is triggered. Use `canInsert(_:)` for performing a check first, so that this method can
/// be executed safely.
///
/// - Parameter newElement: The new key-value pair to insert into the ordered dictionary. The
/// key contained in the pair must not be already present in the ordered dictionary.
/// - Parameter index: The position at which to insert the new key-value pair. `index` must be
/// a valid index of the ordered dictionary or equal to `endIndex` property.
///
/// - SeeAlso: canInsert(key:)
/// - SeeAlso: canInsert(at:)
/// - SeeAlso: update(:at:)
public mutating func insert(_ newElement: Element, at index: Index) {
precondition(canInsert(key: newElement.key), "Cannot insert duplicate key in OrderedDictionary")
precondition(canInsert(at: index), "Cannot insert at invalid index in OrderedDictionary")
let (key, value) = newElement
_orderedKeys.insert(key, at: index)
_keysToValues[key] = value
}
/// Checks whether the key-value pair at the given index can be updated with the given key-value
/// pair. This is not the case if the key of the updated element is already present in the
/// ordered dictionary and located at another index than the updated one.
///
/// Although this is a checking method, a valid index has to be provided.
///
/// - Parameter newElement: The key-value pair to be set at the specified position.
/// - Parameter index: The position at which to set the key-value pair. `index` must be a valid
/// index of the ordered dictionary.
public func canUpdate(_ newElement: Element, at index: Index) -> Bool {
var keyPresentAtIndex = false
return _canUpdate(newElement, at: index, keyPresentAtIndex: &keyPresentAtIndex)
}
/// Updates the key-value pair located at the specified position.
///
/// If the key of the updated pair already exists in the ordered dictionary *and* is located at
/// a different position than the specified one, a runtime error is triggered. Use
/// `canUpdate(_:at:)` for performing a check first, so that this method can be executed safely.
///
/// - Parameter newElement: The key-value pair to be set at the specified position.
/// - Parameter index: The position at which to set the key-value pair. `index` must be a valid
/// index of the ordered dictionary.
///
/// - SeeAlso: canUpdate(_:at:)
/// - SeeAlso: insert(:at:)
@discardableResult
public mutating func update(_ newElement: Element, at index: Index) -> Element? {
// Store the flag indicating whether the key of the inserted element
// is present at the updated index
var keyPresentAtIndex = false
precondition(
_canUpdate(newElement, at: index, keyPresentAtIndex: &keyPresentAtIndex),
"OrderedDictionary update duplicates key"
)
// Decompose the element
let (key, value) = newElement
// Load the current element at the index
let replacedElement = self[index]
// If its key differs, remove its associated value
if (!keyPresentAtIndex) {
_keysToValues.removeValue(forKey: replacedElement.key)
}
// Store the new position of the key and the new value associated with the key
_orderedKeys[index] = key
_keysToValues[key] = value
return replacedElement
}
/// Removes and returns the key-value pair at the specified position if there is any key-value
/// pair, or `nil` if there is none.
///
/// - Parameter index: The position of the key-value pair to remove.
/// - Returns: The element at the specified index, or `nil` if the position is not taken.
///
/// - SeeAlso: removeValue(forKey:)
@discardableResult
public mutating func remove(at index: Index) -> Element? {
guard let element = elementAt(index) else { return nil }
_orderedKeys.remove(at: index)
_keysToValues.removeValue(forKey: element.key)
return element
}
private func _canUpdate(_ newElement: Element, at index: Index, keyPresentAtIndex: inout Bool) -> Bool {
precondition(indices.contains(index), "OrderedDictionary index is out of range")
let currentIndexOfKey = self.index(forKey: newElement.key)
let keyNotPresent = currentIndexOfKey == nil
keyPresentAtIndex = currentIndexOfKey == index
return keyNotPresent || keyPresentAtIndex
}
// ======================================================= //
// MARK: - Moving Elements
// ======================================================= //
/// Moves an existing key-value pair specified by the given key to the new index by removing it
/// from its original index first and inserting it at the new index. If the movement is
/// actually performed, the previous index of the key-value pair is returned. Otherwise, `nil`
/// is returned.
///
/// - Parameter key: The key specifying the key-value pair to move.
/// - Parameter newIndex: The new index the key-value pair should be moved to.
/// - Returns: The previous index of the key-value pair if it was sucessfully moved.
@discardableResult
public mutating func moveElement(forKey key: Key, to newIndex: Index) -> Index? {
// Load the previous index and return nil if the index is not found.
guard let previousIndex = index(forKey: key) else { return nil }
// If the previous and new indices match, threat it as if the movement was already
// performed.
guard previousIndex != newIndex else { return previousIndex }
// Remove the value for the key at its original index.
let value = removeValue(forKey: key)!
// Validate the new index.
precondition(canInsert(at: newIndex), "Cannot move to invalid index in OrderedDictionary")
// Insert the element at the new index.
insert((key: key, value: value), at: newIndex)
return previousIndex
}
// ======================================================= //
// MARK: - Sorting Elements
// ======================================================= //
/// Sorts the ordered dictionary in place, using the given predicate as the comparison between
/// elements.
///
/// The predicate must be a *strict weak ordering* over the elements.
///
/// - Parameter areInIncreasingOrder: A predicate that returns `true` if its first argument
/// should be ordered before its second argument; otherwise, `false`.
///
/// - SeeAlso: MutableCollection.sort(by:), sorted(by:)
public mutating func sort(by areInIncreasingOrder: (Element, Element) -> Bool) {
_orderedKeys = _sortedElements(by: areInIncreasingOrder).map { $0.key }
}
/// Returns a new ordered dictionary, sorted using the given predicate as the comparison between
/// elements.
///
/// The predicate must be a *strict weak ordering* over the elements.
///
/// - Parameter areInIncreasingOrder: A predicate that returns `true` if its first argument
/// should be ordered before its second argument; otherwise, `false`.
/// - Returns: A new ordered dictionary sorted according to the predicate.
///
/// - SeeAlso: MutableCollection.sorted(by:), sort(by:)
/// - MutatingVariant: sort
public func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> OrderedDictionary<Key, Value> {
return OrderedDictionary(_sortedElements(by: areInIncreasingOrder))
}
private func _sortedElements(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] {
return sorted(by: areInIncreasingOrder)
}
// ======================================================= //
// MARK: - Slices
// ======================================================= //
/// Accesses a contiguous subrange of the ordered dictionary.
///
/// - Parameter bounds: A range of the ordered dictionary's indices. The bounds of the range
/// must be valid indices of the ordered dictionary.
/// - Returns: The slice view at the ordered dictionary in the specified subrange.
public subscript(bounds: Range<Index>) -> SubSequence {
return OrderedDictionarySlice(base: self, bounds: bounds)
}
// ======================================================= //
// MARK: - Indices
// ======================================================= //
/// The indices that are valid for subscripting the ordered dictionary.
public var indices: Indices {
return _orderedKeys.indices
}
/// The position of the first key-value pair in a non-empty ordered dictionary.
public var startIndex: Index {
return _orderedKeys.startIndex
}
/// The position which is one greater than the position of the last valid key-value pair in the
/// ordered dictionary.
public var endIndex: Index {
return _orderedKeys.endIndex
}
/// Returns the position immediately after the given index.
public func index(after i: Index) -> Index {
return _orderedKeys.index(after: i)
}
/// Returns the position immediately before the given index.
public func index(before i: Index) -> Index {
return _orderedKeys.index(before: i)
}
// ======================================================= //
// MARK: - Internal Storage
// ======================================================= //
/// The backing storage for the ordered keys.
fileprivate var _orderedKeys = [Key]()
/// The backing storage for the mapping of keys to values.
fileprivate var _keysToValues = [Key: Value]()
}
// ======================================================= //
// MARK: - Subtypes
// ======================================================= //
#if swift(>=4.1)
/// A view into an ordered dictionary whose indices are a subrange of the indices of the ordered
/// dictionary.
public typealias OrderedDictionarySlice<Key: Hashable, Value> = Slice<OrderedDictionary<Key, Value>>
/// A collection containing the keys of the ordered dictionary.
///
/// Under the hood this is a lazily evaluated bidirectional collection deriving the keys from
/// the base ordered dictionary on-the-fly.
public typealias OrderedDictionaryKeys<Key: Hashable, Value> = LazyMapCollection<OrderedDictionary<Key, Value>, Key>
/// A collection containing the values of the ordered dictionary.
///
/// Under the hood this is a lazily evaluated bidirectional collection deriving the values from
/// the base ordered dictionary on-the-fly.
public typealias OrderedDictionaryValues<Key: Hashable, Value> = LazyMapCollection<OrderedDictionary<Key, Value>, Value>
#else
/// A view into an ordered dictionary whose indices are a subrange of the indices of the ordered
/// dictionary.
public typealias OrderedDictionarySlice<Key: Hashable, Value> = Slice<OrderedDictionary<Key, Value>>
/// A collection containing the keys of the ordered dictionary.
///
/// Under the hood this is a lazily evaluated bidirectional collection deriving the keys from
/// the base ordered dictionary on-the-fly.
public typealias OrderedDictionaryKeys<Key: Hashable, Value> = LazyMapCollection<OrderedDictionary<Key, Value>, Key>
/// A collection containing the values of the ordered dictionary.
///
/// Under the hood this is a lazily evaluated bidirectional collection deriving the values from
/// the base ordered dictionary on-the-fly.
public typealias OrderedDictionaryValues<Key: Hashable, Value> = LazyMapCollection<OrderedDictionary<Key, Value>, Value>
#endif
// ======================================================= //
// MARK: - Literals
// ======================================================= //
extension OrderedDictionary: ExpressibleByArrayLiteral {
/// Creates an ordered dictionary initialized from an array literal containing a list of
/// key-value pairs.
public init(arrayLiteral elements: Element...) {
self.init(elements)
}
}
extension OrderedDictionary: ExpressibleByDictionaryLiteral {
/// Creates an ordered dictionary initialized from a dictionary literal.
public init(dictionaryLiteral elements: (Key, Value)...) {
self.init(elements.map { element in
let (key, value) = element
return (key: key, value: value)
})
}
}
// ======================================================= //
// MARK: - Equatable Conformance
// ======================================================= //
#if swift(>=4.1)
extension OrderedDictionary: Equatable where Value: Equatable {}
#endif
extension OrderedDictionary where Value: Equatable {
/// Returns a Boolean value that indicates whether the two given ordered dictionaries with
/// equatable values are equal.
public static func == (lhs: OrderedDictionary, rhs: OrderedDictionary) -> Bool {
return lhs._orderedKeys == rhs._orderedKeys
&& lhs._keysToValues == rhs._keysToValues
}
}

View File

@ -0,0 +1,31 @@
//
// ParseResponse.swift
// ASDKgram-Swift
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// grant of patent rights can be found in the PATENTS file in the same directory.
//
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
import Foundation
func parsePopularPage(withURL: URL, page: Int) -> Resource<PopularPageModel> {
let parse = Resource<PopularPageModel>(url: withURL, page: page) { metaData, jsonData in
do {
let photos = try JSONDecoder().decode([PhotoModel].self, from: jsonData)
return .success(PopularPageModel(metaData: metaData, photos: photos))
} catch {
return .failure(.errorParsingJSON)
}
}
return parse
}

View File

@ -2,116 +2,114 @@
// PhotoFeedModel.swift // PhotoFeedModel.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 07/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import UIKit import UIKit
final class PhotoFeedModel { final class PhotoFeedModel {
// MARK: Properties
public private(set) var photoFeedModelType: PhotoFeedModelType public private(set) var photoFeedModelType: PhotoFeedModelType
public private(set) var photos: [PhotoModel] = []
public private(set) var imageSize: CGSize private var orderedPhotos: OrderedDictionary<String, PhotoModel> = [:]
private var url: URL
private var ids: [Int] = []
private var currentPage: Int = 0 private var currentPage: Int = 0
private var totalPages: Int = 0 private var totalPages: Int = 0
public private(set) var totalItems: Int = 0 private var totalItems: Int = 0
private var fetchPageInProgress: Bool = false private var fetchPageInProgress: Bool = false
private var refreshFeedInProgress: Bool = false
init(initWithPhotoFeedModelType: PhotoFeedModelType, requiredImageSize: CGSize) { // MARK: Lifecycle
self.photoFeedModelType = initWithPhotoFeedModelType
self.imageSize = requiredImageSize
self.url = URL.URLForFeedModelType(feedModelType: initWithPhotoFeedModelType)
}
var numberOfItemsInFeed: Int { init(photoFeedModelType: PhotoFeedModelType) {
return photos.count self.photoFeedModelType = photoFeedModelType
} }
// MARK: API
lazy var url: URL = {
return URL.URLForFeedModelType(feedModelType: self.photoFeedModelType)
}()
var numberOfItems: Int {
return orderedPhotos.count
}
func itemAtIndexPath(_ indexPath: IndexPath) -> PhotoModel {
return orderedPhotos[indexPath.row].value
}
// return in completion handler the number of additions and the status of internet connection // return in completion handler the number of additions and the status of internet connection
func updateNewBatchOfPopularPhotos(additionsAndConnectionStatusCompletion: @escaping (Int, InternetStatus) -> ()) { func updateNewBatchOfPopularPhotos(additionsAndConnectionStatusCompletion: @escaping (Int, InternetStatus) -> ()) {
// For this example let's use the main thread as locking queue
DispatchQueue.main.async {
guard !self.fetchPageInProgress else {
return
}
guard !fetchPageInProgress else { return } self.fetchPageInProgress = true
self.fetchNextPageOfPopularPhotos(replaceData: false) { [unowned self] additions, error in
self.fetchPageInProgress = false
fetchPageInProgress = true if let error = error {
fetchNextPageOfPopularPhotos(replaceData: false) { [unowned self] additions, errors in switch error {
self.fetchPageInProgress = false case .noInternetConnection:
additionsAndConnectionStatusCompletion(0, .noConnection)
if let error = errors { default:
switch error { additionsAndConnectionStatusCompletion(0, .connected)
case .noInternetConnection: }
additionsAndConnectionStatusCompletion(0, .noConnection) } else {
default: additionsAndConnectionStatusCompletion(0, .connected) additionsAndConnectionStatusCompletion(additions, .connected)
} }
} else { }
additionsAndConnectionStatusCompletion(additions, .connected) }
}
}
} }
private func fetchNextPageOfPopularPhotos(replaceData: Bool, numberOfAdditionsCompletion: @escaping (Int, NetworkingErrors?) -> ()) { private func fetchNextPageOfPopularPhotos(replaceData: Bool, numberOfAdditionsCompletion: @escaping (Int, NetworkingError?) -> ()) {
if currentPage == totalPages, currentPage != 0 { if currentPage == totalPages, currentPage != 0 {
DispatchQueue.main.async { numberOfAdditionsCompletion(0, .customError("No pages left to parse"))
numberOfAdditionsCompletion(0, .customError("No pages left to parse"))
}
return return
} }
var newPhotos: [PhotoModel] = []
var newIDs: [Int] = []
let pageToFetch = currentPage + 1 let pageToFetch = currentPage + 1
WebService().load(resource: parsePopularPage(withURL: url, page: pageToFetch)) { [unowned self] result in
let url = self.url.addImageParameterForClosestImageSizeAndpage(size: imageSize, page: pageToFetch) // Callback will happen on main for now
WebService().load(resource: parsePopularPage(withURL: url)) { [unowned self] result in
switch result { switch result {
case .success(let popularPage): case .success(let itemsPage):
self.totalItems = popularPage.totalNumberOfItems // Update current state
self.totalPages = popularPage.totalPages self.totalItems = itemsPage.totalNumberOfItems
self.currentPage = popularPage.page self.totalPages = itemsPage.totalPages
self.currentPage = itemsPage.page
for photo in popularPage.photos { // Update photos
if !replaceData || !self.ids.contains(photo.photoID) { if replaceData {
newPhotos.append(photo) self.orderedPhotos = []
newIDs.append(photo.photoID) }
} var insertedItems = 0
} for photo in itemsPage.photos {
if !self.orderedPhotos.containsKey(photo.photoID) {
DispatchQueue.main.async { // Append a new key-value pair by setting a value for an non-existent key
if replaceData { self.orderedPhotos[photo.photoID] = photo
self.photos = newPhotos insertedItems += 1
self.ids = newIDs }
} else { }
self.photos += newPhotos
self.ids += newIDs
}
numberOfAdditionsCompletion(newPhotos.count, nil)
}
numberOfAdditionsCompletion(insertedItems, nil)
case .failure(let fail): case .failure(let fail):
print(fail) print(fail)
DispatchQueue.main.async {
numberOfAdditionsCompletion(0, fail) numberOfAdditionsCompletion(0, fail)
}
} }
} }
} }

View File

@ -2,103 +2,102 @@
// PhotoFeedTableNodeController.swift // PhotoFeedTableNodeController.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 06/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import AsyncDisplayKit import AsyncDisplayKit
class PhotoFeedTableNodeController: ASViewController<ASTableNode> { class PhotoFeedTableNodeController: ASViewController<ASTableNode> {
// MARK: Lifecycle
var activityIndicator: UIActivityIndicatorView! private lazy var activityIndicatorView: UIActivityIndicatorView = {
var photoFeed: PhotoFeedModel return UIActivityIndicatorView(activityIndicatorStyle: .gray)
}()
var photoFeedModel = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular)
init() { init() {
photoFeed = PhotoFeedModel(initWithPhotoFeedModelType: .photoFeedModelTypePopular, requiredImageSize: screenSizeForWidth) super.init(node: ASTableNode())
super.init(node: ASTableNode())
self.navigationItem.title = "ASDK"
navigationItem.title = "ASDK"
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented") fatalError("init(coder:) has not been implemented")
} }
// MAKR: UIViewController
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupActivityIndicator()
node.allowsSelection = false node.allowsSelection = false
node.view.separatorStyle = .none
node.dataSource = self node.dataSource = self
node.delegate = self node.delegate = self
node.leadingScreensForBatching = 2.5 node.leadingScreensForBatching = 2.5
navigationController?.hidesBarsOnSwipe = true node.view.separatorStyle = .none
navigationController?.hidesBarsOnSwipe = true
node.view.addSubview(activityIndicatorView)
} }
// helper functions override func viewDidLayoutSubviews() {
func setupActivityIndicator() { super.viewDidLayoutSubviews()
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
self.activityIndicator = activityIndicator // Center the activity indicator view
let bounds = self.node.frame let bounds = node.bounds
var refreshRect = activityIndicator.frame activityIndicatorView.frame.origin = CGPoint(
refreshRect.origin = CGPoint(x: (bounds.size.width - activityIndicator.frame.size.width) / 2.0, y: (bounds.size.height - activityIndicator.frame.size.height) / 2.0) x: (bounds.width - activityIndicatorView.frame.width) / 2.0,
activityIndicator.frame = refreshRect y: (bounds.height - activityIndicatorView.frame.height) / 2.0
self.node.view.addSubview(activityIndicator) )
} }
var screenSizeForWidth: CGSize = {
let screenRect = UIScreen.main.bounds
let screenScale = UIScreen.main.scale
return CGSize(width: screenRect.size.width * screenScale, height: screenRect.size.width * screenScale)
}()
func fetchNewBatchWithContext(_ context: ASBatchContext?) { func fetchNewBatchWithContext(_ context: ASBatchContext?) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.activityIndicator.startAnimating() self.activityIndicatorView.startAnimating()
} }
photoFeed.updateNewBatchOfPopularPhotos() { additions, connectionStatus in photoFeedModel.updateNewBatchOfPopularPhotos() { additions, connectionStatus in
switch connectionStatus { switch connectionStatus {
case .connected: case .connected:
self.activityIndicator.stopAnimating() self.activityIndicatorView.stopAnimating()
self.addRowsIntoTableNode(newPhotoCount: additions) self.addRowsIntoTableNode(newPhotoCount: additions)
context?.completeBatchFetching(true) context?.completeBatchFetching(true)
case .noConnection: case .noConnection:
self.activityIndicator.stopAnimating() self.activityIndicatorView.stopAnimating()
if context != nil { context?.completeBatchFetching(true)
context!.completeBatchFetching(true)
}
break
} }
} }
} }
func addRowsIntoTableNode(newPhotoCount newPhotos: Int) { func addRowsIntoTableNode(newPhotoCount newPhotos: Int) {
let indexRange = (photoFeed.photos.count - newPhotos..<photoFeed.photos.count) let indexRange = (photoFeedModel.numberOfItems - newPhotos..<photoFeedModel.numberOfItems)
let indexPaths = indexRange.map { IndexPath(row: $0, section: 0) } let indexPaths = indexRange.map { IndexPath(row: $0, section: 0) }
node.insertRows(at: indexPaths, with: .none) node.insertRows(at: indexPaths, with: .none)
} }
} }
// MARK: ASTableDataSource / ASTableDelegate
extension PhotoFeedTableNodeController: ASTableDataSource, ASTableDelegate { extension PhotoFeedTableNodeController: ASTableDataSource, ASTableDelegate {
func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int { func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> Int {
return photoFeed.numberOfItemsInFeed return photoFeedModel.numberOfItems
} }
func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock {
let photo = photoFeed.photos[indexPath.row] let photo = photoFeedModel.itemAtIndexPath(indexPath)
let nodeBlock: ASCellNodeBlock = { _ in let nodeBlock: ASCellNodeBlock = { _ in
return PhotoTableNodeCell(photoModel: photo) return PhotoTableNodeCell(photoModel: photo)
} }

View File

@ -2,19 +2,17 @@
// PhotoFeedTableViewController.swift // PhotoFeedTableViewController.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 06/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import UIKit import UIKit
@ -22,13 +20,12 @@ import UIKit
class PhotoFeedTableViewController: UITableViewController { class PhotoFeedTableViewController: UITableViewController {
var activityIndicator: UIActivityIndicatorView! var activityIndicator: UIActivityIndicatorView!
var photoFeed: PhotoFeedModel var photoFeed = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular)
init() { init() {
photoFeed = PhotoFeedModel(initWithPhotoFeedModelType: .photoFeedModelTypePopular, requiredImageSize: screenSizeForWidth) super.init(nibName: nil, bundle: nil)
super.init(nibName: nil, bundle: nil)
self.navigationItem.title = "UIKit" navigationItem.title = "UIKit"
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
@ -37,12 +34,13 @@ class PhotoFeedTableViewController: UITableViewController {
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
navigationController?.hidesBarsOnSwipe = true
setupActivityIndicator() setupActivityIndicator()
configureTableView() configureTableView()
fetchNewBatch() fetchNewBatch()
navigationController?.hidesBarsOnSwipe = true
} }
func fetchNewBatch() { func fetchNewBatch() {
activityIndicator.startAnimating() activityIndicator.startAnimating()
photoFeed.updateNewBatchOfPopularPhotos() { additions, connectionStatus in photoFeed.updateNewBatchOfPopularPhotos() { additions, connectionStatus in
@ -57,22 +55,17 @@ class PhotoFeedTableViewController: UITableViewController {
} }
} }
var screenSizeForWidth: CGSize = { // Helper functions
let screenRect = UIScreen.main.bounds
let screenScale = UIScreen.main.scale
return CGSize(width: screenRect.size.width * screenScale, height: screenRect.size.width * screenScale)
}()
// helper functions
func setupActivityIndicator() { func setupActivityIndicator() {
let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
self.activityIndicator = activityIndicator self.activityIndicator = activityIndicator
self.tableView.addSubview(activityIndicator) self.tableView.addSubview(activityIndicator)
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
activityIndicator.centerXAnchor.constraint(equalTo: self.tableView.centerXAnchor), activityIndicator.centerXAnchor.constraint(equalTo: self.tableView.centerXAnchor),
activityIndicator.centerYAnchor.constraint(equalTo: self.tableView.centerYAnchor) activityIndicator.centerYAnchor.constraint(equalTo: self.tableView.centerYAnchor)
]) ])
} }
func configureTableView() { func configureTableView() {
@ -87,7 +80,7 @@ extension PhotoFeedTableViewController {
func addRowsIntoTableView(newPhotoCount newPhotos: Int) { func addRowsIntoTableView(newPhotoCount newPhotos: Int) {
let indexRange = (photoFeed.photos.count - newPhotos..<photoFeed.photos.count) let indexRange = (photoFeed.numberOfItems - newPhotos..<photoFeed.numberOfItems)
let indexPaths = indexRange.map { IndexPath(row: $0, section: 0) } let indexPaths = indexRange.map { IndexPath(row: $0, section: 0) }
tableView.insertRows(at: indexPaths, with: .none) tableView.insertRows(at: indexPaths, with: .none)
} }
@ -95,24 +88,26 @@ extension PhotoFeedTableViewController {
// TableView Data Source // TableView Data Source
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return photoFeed.photos.count return photoFeed.numberOfItems
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "photoCell", for: indexPath) as? PhotoTableViewCell else { fatalError("Wrong cell type") } guard let cell = tableView.dequeueReusableCell(withIdentifier: "photoCell", for: indexPath) as? PhotoTableViewCell else { fatalError("Wrong cell type") }
cell.photoModel = photoFeed.photos[indexPath.row] cell.photoModel = photoFeed.itemAtIndexPath(indexPath)
return cell return cell
} }
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return PhotoTableViewCell.height(for: photoFeed.photos[indexPath.row], withWidth: self.view.frame.size.width) return PhotoTableViewCell.height(
for: photoFeed.itemAtIndexPath(indexPath),
withWidth: self.view.frame.size.width
)
} }
override func scrollViewDidScroll(_ scrollView: UIScrollView) { override func scrollViewDidScroll(_ scrollView: UIScrollView) {
let currentOffSetY = scrollView.contentOffset.y let currentOffSetY = scrollView.contentOffset.y
let contentHeight = scrollView.contentSize.height let contentHeight = scrollView.contentSize.height
let screenHeight = UIScreen.main.bounds.size.height let screenHeight = UIScreen.main.bounds.height
let screenfullsBeforeBottom = (contentHeight - currentOffSetY) / screenHeight let screenfullsBeforeBottom = (contentHeight - currentOffSetY) / screenHeight
if screenfullsBeforeBottom < 2.5 { if screenfullsBeforeBottom < 2.5 {
self.fetchNewBatch() self.fetchNewBatch()

View File

@ -2,91 +2,123 @@
// PhotoModel.swift // PhotoModel.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 07/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import UIKit import UIKit
typealias JSONDictionary = [String : Any] // MARK: ProfileImage
struct PhotoModel { struct ProfileImage: Codable {
let large: String
let url: String let medium: String
let photoID: Int let small: String
let dateString: String }
let descriptionText: String
// MARK: UserModel
struct UserModel: Codable {
let userName: String
let profileImages: ProfileImage
enum CodingKeys: String, CodingKey {
case userName = "username"
case profileImages = "profile_image"
}
}
extension UserModel {
var profileImage: String {
return profileImages.medium
}
}
// MARK: PhotoURL
struct PhotoURL: Codable {
let full: String
let raw: String
let regular: String
let small: String
let thumb: String
}
// MARK: PhotoModel
struct PhotoModel: Codable {
let urls: PhotoURL
let photoID: String
let uploadedDateString: String
let descriptionText: String?
let likesCount: Int let likesCount: Int
let ownerUserName: String let width: Int
let ownerPicURL: String let height: Int
let user: UserModel
init?(dictionary: JSONDictionary) {
enum CodingKeys: String, CodingKey {
guard let images = dictionary["images"] as? [[String: Any]], case photoID = "id"
let url = images[0]["url"] as? String, case urls = "urls"
let date = dictionary["created_at"] as? String, case uploadedDateString = "created_at"
let photoID = dictionary["id"] as? Int, case descriptionText = "description"
let descriptionText = dictionary["name"] as? String, case likesCount = "likes"
let likesCount = dictionary["positive_votes_count"] as? Int else case width = "width"
{ print("error parsing JSON within PhotoModel Init"); return nil } case height = "height"
case user = "user"
guard let user = dictionary["user"] as? JSONDictionary, }
let username = user["username"] as? String, }
let ownerPicURL = user["userpic_url"] as? String else
{ print("error parsing JSON within PhotoModel Init"); return nil } extension PhotoModel {
var url: String {
self.url = url return urls.regular
self.photoID = photoID }
self.descriptionText = descriptionText
self.likesCount = likesCount
self.dateString = date
self.ownerUserName = username
self.ownerPicURL = ownerPicURL
}
} }
extension PhotoModel { extension PhotoModel {
// MARK: - Attributed Strings // MARK: - Attributed Strings
func attrStringForUserName(withSize size: CGFloat) -> NSAttributedString { func attributedStringForUserName(withSize size: CGFloat) -> NSAttributedString {
let attr = [ let attributes = [
NSForegroundColorAttributeName : UIColor.darkGray, NSForegroundColorAttributeName : UIColor.darkGray,
NSFontAttributeName: UIFont.boldSystemFont(ofSize: size) NSFontAttributeName: UIFont.boldSystemFont(ofSize: size)
] ]
return NSAttributedString(string: self.ownerUserName, attributes: attr) return NSAttributedString(string: user.userName, attributes: attributes)
} }
func attrStringForDescription(withSize size: CGFloat) -> NSAttributedString { func attributedStringForDescription(withSize size: CGFloat) -> NSAttributedString {
let attr = [ let attributes = [
NSForegroundColorAttributeName : UIColor.darkGray, NSForegroundColorAttributeName : UIColor.darkGray,
NSFontAttributeName: UIFont.systemFont(ofSize: size) NSFontAttributeName: UIFont.systemFont(ofSize: size)
] ]
return NSAttributedString(string: self.descriptionText, attributes: attr) return NSAttributedString(string: descriptionText ?? "", attributes: attributes)
} }
func attrStringLikes(withSize size: CGFloat) -> NSAttributedString { func attributedStringLikes(withSize size: CGFloat) -> NSAttributedString {
guard let formattedLikesNumber = NumberFormatter.decimalNumberFormatter.string(from: NSNumber(value: likesCount)) else {
return NSAttributedString()
}
let formatter = NumberFormatter() let likesAttributes = [
formatter.numberStyle = .decimal NSForegroundColorAttributeName : UIColor.mainBarTintColor,
let formattedLikesNumber: String? = formatter.string(from: NSNumber(value: self.likesCount)) NSFontAttributeName: UIFont.systemFont(ofSize: size)
let likesString: String = "\(formattedLikesNumber!) Likes" ]
let textAttr = [NSForegroundColorAttributeName : UIColor.mainBarTintColor(), NSFontAttributeName: UIFont.systemFont(ofSize: size)] let likesAttrString = NSAttributedString(string: "\(formattedLikesNumber) Likes", attributes: likesAttributes)
let likesAttrString = NSAttributedString(string: likesString, attributes: textAttr)
let heartAttr = [NSForegroundColorAttributeName : UIColor.red, NSFontAttributeName: UIFont.systemFont(ofSize: size)] let heartAttributes = [
let heartAttrString = NSAttributedString(string: "♥︎ ", attributes: heartAttr) NSForegroundColorAttributeName : UIColor.red,
NSFontAttributeName: UIFont.systemFont(ofSize: size)
]
let heartAttrString = NSAttributedString(string: "♥︎ ", attributes: heartAttributes)
let combine = NSMutableAttributedString() let combine = NSMutableAttributedString()
combine.append(heartAttrString) combine.append(heartAttrString)
@ -94,32 +126,16 @@ extension PhotoModel {
return combine return combine
} }
func attrStringForTimeSinceString(withSize size: CGFloat) -> NSAttributedString { func attributedStringForTimeSinceString(withSize size: CGFloat) -> NSAttributedString {
guard let date = Date.iso8601Formatter.date(from: self.uploadedDateString) else {
let attr = [ return NSAttributedString();
NSForegroundColorAttributeName : UIColor.mainBarTintColor(), }
let attributes = [
NSForegroundColorAttributeName : UIColor.mainBarTintColor,
NSFontAttributeName: UIFont.systemFont(ofSize: size) NSFontAttributeName: UIFont.systemFont(ofSize: size)
] ]
let date = Date.iso8601Formatter.date(from: self.dateString)! return NSAttributedString(string: Date.timeStringSince(fromConverted: date), attributes: attributes)
return NSAttributedString(string: timeStringSince(fromConverted: date), attributes: attr)
}
private func timeStringSince(fromConverted date: Date) -> String {
let diffDates = NSCalendar.current.dateComponents([.day, .hour, .second], from: date, to: Date())
if let week = diffDates.day, week > 7 {
return "\(week / 7)w"
} else if let day = diffDates.day, day > 0 {
return "\(day)d"
} else if let hour = diffDates.hour, hour > 0 {
return "\(hour)h"
} else if let second = diffDates.second, second > 0 {
return "\(second)s"
} else if let zero = diffDates.second, zero == 0 {
return "1s"
} else {
return "ERROR"
}
} }
} }

View File

@ -2,53 +2,60 @@
// PhotoTableNodeCell.swift // PhotoTableNodeCell.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 09/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
//
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.//
import Foundation import Foundation
import AsyncDisplayKit import AsyncDisplayKit
class PhotoTableNodeCell: ASCellNode { class PhotoTableNodeCell: ASCellNode {
// MARK: Properties
let usernameLabel = ASTextNode() let usernameLabel = ASTextNode()
let timeIntervalLabel = ASTextNode() let timeIntervalLabel = ASTextNode()
let photoLikesLabel = ASTextNode() let photoLikesLabel = ASTextNode()
let photoDescriptionLabel = ASTextNode() let photoDescriptionLabel = ASTextNode()
let avatarImageNode: ASNetworkImageNode = { let avatarImageNode: ASNetworkImageNode = {
let imageNode = ASNetworkImageNode() let node = ASNetworkImageNode()
imageNode.contentMode = .scaleAspectFill node.contentMode = .scaleAspectFill
imageNode.imageModificationBlock = ASImageNodeRoundBorderModificationBlock(0, nil) // Set the imageModificationBlock for a rounded avatar
return imageNode node.imageModificationBlock = ASImageNodeRoundBorderModificationBlock(0, nil)
return node
}() }()
let photoImageNode: ASNetworkImageNode = { let photoImageNode: ASNetworkImageNode = {
let imageNode = ASNetworkImageNode() let node = ASNetworkImageNode()
imageNode.contentMode = .scaleAspectFill node.contentMode = .scaleAspectFill
return imageNode return node
}() }()
// MARK: Lifecycle
init(photoModel: PhotoModel) { init(photoModel: PhotoModel) {
super.init() super.init()
self.photoImageNode.url = URL(string: photoModel.url)
self.avatarImageNode.url = URL(string: photoModel.ownerPicURL) automaticallyManagesSubnodes = true
self.usernameLabel.attributedText = photoModel.attrStringForUserName(withSize: Constants.CellLayout.FontSize) photoImageNode.url = URL(string: photoModel.url)
self.timeIntervalLabel.attributedText = photoModel.attrStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) avatarImageNode.url = URL(string: photoModel.user.profileImage)
self.photoLikesLabel.attributedText = photoModel.attrStringLikes(withSize: Constants.CellLayout.FontSize) usernameLabel.attributedText = photoModel.attributedStringForUserName(withSize: Constants.CellLayout.FontSize)
self.photoDescriptionLabel.attributedText = photoModel.attrStringForDescription(withSize: Constants.CellLayout.FontSize) timeIntervalLabel.attributedText = photoModel.attributedStringForTimeSinceString(withSize: Constants.CellLayout.FontSize)
self.automaticallyManagesSubnodes = true photoLikesLabel.attributedText = photoModel.attributedStringLikes(withSize: Constants.CellLayout.FontSize)
photoDescriptionLabel.attributedText = photoModel.attributedStringForDescription(withSize: Constants.CellLayout.FontSize)
} }
// MARK: ASDisplayNode
override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec {
@ -58,9 +65,13 @@ class PhotoTableNodeCell: ASCellNode {
let headerStack = ASStackLayoutSpec.horizontal() let headerStack = ASStackLayoutSpec.horizontal()
headerStack.alignItems = .center headerStack.alignItems = .center
avatarImageNode.style.preferredSize = CGSize(width: Constants.CellLayout.UserImageHeight, height: Constants.CellLayout.UserImageHeight) avatarImageNode.style.preferredSize = CGSize(
width: Constants.CellLayout.UserImageHeight,
height: Constants.CellLayout.UserImageHeight
)
headerChildren.append(ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForAvatar, child: avatarImageNode)) headerChildren.append(ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForAvatar, child: avatarImageNode))
usernameLabel.style.flexShrink = 1.0
usernameLabel.style.flexShrink = 1.0
headerChildren.append(usernameLabel) headerChildren.append(usernameLabel)
let spacer = ASLayoutSpec() let spacer = ASLayoutSpec()
@ -76,9 +87,11 @@ class PhotoTableNodeCell: ASCellNode {
headerStack.children = headerChildren headerStack.children = headerChildren
let verticalStack = ASStackLayoutSpec.vertical() let verticalStack = ASStackLayoutSpec.vertical()
verticalStack.children = [
verticalStack.children = [ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForHeader, child: headerStack), ASRatioLayoutSpec(ratio: 1.0, child: photoImageNode), ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForFooter, child: footerStack)] ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForHeader, child: headerStack),
ASRatioLayoutSpec(ratio: 1.0, child: photoImageNode),
ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForFooter, child: footerStack)
]
return verticalStack return verticalStack
} }
} }

View File

@ -2,19 +2,17 @@
// PhotoTableViewCell.swift // PhotoTableViewCell.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 08/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import UIKit import UIKit
@ -25,15 +23,15 @@ class PhotoTableViewCell: UITableViewCell {
didSet { didSet {
if let model = photoModel { if let model = photoModel {
photoImageView.loadImageUsingUrlString(urlString: model.url) photoImageView.loadImageUsingUrlString(urlString: model.url)
avatarImageView.loadImageUsingUrlString(urlString: model.ownerPicURL) avatarImageView.loadImageUsingUrlString(urlString: model.user.profileImage)
photoLikesLabel.attributedText = model.attrStringLikes(withSize: Constants.CellLayout.FontSize) photoLikesLabel.attributedText = model.attributedStringLikes(withSize: Constants.CellLayout.FontSize)
usernameLabel.attributedText = model.attrStringForUserName(withSize: Constants.CellLayout.FontSize) usernameLabel.attributedText = model.attributedStringForUserName(withSize: Constants.CellLayout.FontSize)
timeIntervalLabel.attributedText = model.attrStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) timeIntervalLabel.attributedText = model.attributedStringForTimeSinceString(withSize: Constants.CellLayout.FontSize)
photoDescriptionLabel.attributedText = model.attrStringForDescription(withSize: Constants.CellLayout.FontSize) photoDescriptionLabel.attributedText = model.attributedStringForDescription(withSize: Constants.CellLayout.FontSize)
photoDescriptionLabel.sizeToFit() photoDescriptionLabel.sizeToFit()
var rect = photoDescriptionLabel.frame var rect = photoDescriptionLabel.frame
let availableWidth = self.bounds.size.width - Constants.CellLayout.HorizontalBuffer * 2 let availableWidth = self.bounds.size.width - Constants.CellLayout.HorizontalBuffer * 2
rect.size = model.attrStringForDescription(withSize: Constants.CellLayout.FontSize).boundingRect(with: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size rect.size = model.attributedStringForDescription(withSize: Constants.CellLayout.FontSize).boundingRect(with: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size
photoDescriptionLabel.frame = rect photoDescriptionLabel.frame = rect
} }
} }
@ -133,7 +131,7 @@ class PhotoTableViewCell: UITableViewCell {
let photoHeight = width let photoHeight = width
let font = UIFont.systemFont(ofSize: Constants.CellLayout.FontSize) let font = UIFont.systemFont(ofSize: Constants.CellLayout.FontSize)
let likesHeight = round(font.lineHeight) let likesHeight = round(font.lineHeight)
let descriptionAttrString = photo.attrStringForDescription(withSize: Constants.CellLayout.FontSize) let descriptionAttrString = photo.attributedStringForDescription(withSize: Constants.CellLayout.FontSize)
let availableWidth = width - Constants.CellLayout.HorizontalBuffer * 2 let availableWidth = width - Constants.CellLayout.HorizontalBuffer * 2
let descriptionHeight = descriptionAttrString.boundingRect(with: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size.height let descriptionHeight = descriptionAttrString.boundingRect(with: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size.height

View File

@ -2,36 +2,31 @@
// PopularPageModel.swift // PopularPageModel.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 08/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import Foundation import Foundation
class PopularPageModel: NSObject { struct PopularPageModel {
let page: Int
let page: Int let totalPages: Int
let totalPages: Int let totalNumberOfItems: Int
let totalNumberOfItems: Int let photos: [PhotoModel]
let photos: [PhotoModel]
init(metaData: ResponseMetadata, photos:[PhotoModel]) {
init?(dictionary: JSONDictionary, photosArray: [PhotoModel]) { self.page = metaData.currentPage
guard let page = dictionary["current_page"] as? Int, let totalPages = dictionary["total_pages"] as? Int, let totalItems = dictionary["total_items"] as? Int else { print("error parsing JSON within PhotoModel Init"); return nil } self.totalPages = metaData.pagesTotal
self.totalNumberOfItems = metaData.itemsTotal
self.page = page self.photos = photos
self.totalPages = totalPages }
self.totalNumberOfItems = totalItems
self.photos = photosArray
}
} }

View File

@ -2,25 +2,23 @@
// UIColor.swift // UIColor.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 06/01/2017.
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import UIKit import UIKit
extension UIColor { extension UIColor {
static var mainBarTintColor: UIColor {
class func mainBarTintColor() -> UIColor {
return UIColor(red: 69/255, green: 142/255, blue: 255/255, alpha: 1) return UIColor(red: 69/255, green: 142/255, blue: 255/255, alpha: 1)
} }
} }

View File

@ -1,60 +0,0 @@
//
// UIImage.swift
// ASDKgram-Swift
//
// Created by Calum Harris on 18/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant
// of patent rights can be found in the PATENTS file in the same directory.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
//
import UIKit
// 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.
extension UIImage {
func makeCircularImage(size: CGSize, borderWidth width: CGFloat) -> UIImage {
// make a CGRect with the image's size
let circleRect = CGRect(origin: .zero, size: size)
// begin the image context since we're not in a drawRect:
UIGraphicsBeginImageContextWithOptions(circleRect.size, false, 0)
// create a UIBezierPath circle
let circle = UIBezierPath(roundedRect: circleRect, cornerRadius: circleRect.size.width * 0.5)
// clip to the circle
circle.addClip()
UIColor.white.set()
circle.fill()
// draw the image in the circleRect *AFTER* the context is clipped
self.draw(in: circleRect)
// create a border (for white background pictures)
if width > 0 {
circle.lineWidth = width
UIColor.white.set()
circle.stroke()
}
// get an image from the image context
let roundedImage = UIGraphicsGetImageFromCurrentImageContext()
// end the image context since we're not in a drawRect:
UIGraphicsEndImageContext()
return roundedImage ?? self
}
}

View File

@ -2,66 +2,36 @@
// URL.swift // URL.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 07/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
import UIKit import UIKit
extension URL { extension URL {
static func URLForFeedModelType(feedModelType: PhotoFeedModelType) -> URL { static func URLForFeedModelType(feedModelType: PhotoFeedModelType) -> URL {
switch feedModelType { switch feedModelType {
case .photoFeedModelTypePopular: case .photoFeedModelTypePopular:
return URL(string: assemble500PXURLString(endpoint: Constants.PX500.URLS.PopularEndpoint))! return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.PopularEndpoint))!
case .photoFeedModelTypeLocation: case .photoFeedModelTypeLocation:
return URL(string: assemble500PXURLString(endpoint: Constants.PX500.URLS.SearchEndpoint))! return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.SearchEndpoint))!
case .photoFeedModelTypeUserPhotos: case .photoFeedModelTypeUserPhotos:
return URL(string: assemble500PXURLString(endpoint: Constants.PX500.URLS.UserEndpoint))! return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.UserEndpoint))!
} }
} }
private static func assemble500PXURLString(endpoint: String) -> String { private static func assembleUnsplashURLString(endpoint: String) -> String {
return Constants.PX500.URLS.Host + endpoint + Constants.PX500.URLS.ConsumerKey return Constants.Unsplash.URLS.Host + endpoint + Constants.Unsplash.URLS.ConsumerKey
} }
mutating func addImageParameterForClosestImageSizeAndpage(size: CGSize, page: Int) -> URL {
let imageParameterID: Int
if size.height <= 70 {
imageParameterID = 1
} else if size.height <= 100 {
imageParameterID = 100
} else if size.height <= 140 {
imageParameterID = 2
} else if size.height <= 200 {
imageParameterID = 200
} else if size.height <= 280 {
imageParameterID = 3
} else if size.height <= 400 {
imageParameterID = 400
} else {
imageParameterID = 600
}
var urlString = self.absoluteString
urlString.append("&image_size=\(imageParameterID)&page=\(page)")
return URL(string: urlString)!
}
} }

View File

@ -2,33 +2,33 @@
// Webservice.swift // Webservice.swift
// ASDKgram-Swift // ASDKgram-Swift
// //
// Created by Calum Harris on 06/01/2017.
//
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. // Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
// This source code is licensed under the BSD-style license found in the // This source code is licensed under the BSD-style license found in the
// LICENSE file in the root directory of this source tree. An additional grant // LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
// of patent rights can be found in the PATENTS file in the same directory. // grant of patent rights can be found in the PATENTS file in the same directory.
// //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL // you may not use this file except in compliance with the License.
// FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // You may obtain a copy of the License at
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN //
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // http://www.apache.org/licenses/LICENSE-2.0
// //
// swiftlint:disable force_cast // swiftlint:disable force_cast
import UIKit import UIKit
final class WebService { final class WebService {
/// Load a new resource. Callback is called on main
func load<A>(resource: Resource<A>, completion: @escaping (Result<A>) -> ()) { func load<A>(resource: Resource<A>, completion: @escaping (Result<A>) -> ()) {
URLSession.shared.dataTask(with: resource.url) { data, response, error in URLSession.shared.dataTask(with: resource.url) { data, response, error in
// Check for errors in responses. // Check for errors in responses.
let result = self.checkForNetworkErrors(data, response, error) let result = self.checkForNetworkErrors(data, response, error)
DispatchQueue.main.async { DispatchQueue.main.async {
// Parsing should happen off main
switch result { switch result {
case .success(let data): case .success(let data):
completion(resource.parse(data)) completion(resource.parse(data, response))
case .failure(let error): case .failure(let error):
completion(.failure(error)) completion(.failure(error))
} }
@ -38,54 +38,77 @@ final class WebService {
} }
extension WebService { extension WebService {
/// // Check for errors in responses.
fileprivate func checkForNetworkErrors(_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Result<Data> { fileprivate func checkForNetworkErrors(_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Result<Data> {
// Check for errors in responses.
if let error = error { if let error = error {
let nsError = error as NSError switch error {
if nsError.domain == NSURLErrorDomain && (nsError.code == NSURLErrorNotConnectedToInternet || nsError.code == NSURLErrorTimedOut) { case URLError.notConnectedToInternet, URLError.timedOut:
return .failure(.noInternetConnection) return .failure(.noInternetConnection)
} else { default:
return .failure(.returnedError(error)) return .failure(.returnedError(error))
} }
} }
if let response = response as? HTTPURLResponse, response.statusCode <= 200 && response.statusCode >= 299 { if let response = response as? HTTPURLResponse, response.statusCode <= 200 && response.statusCode >= 299 {
return .failure((.invalidStatusCode("Request returned status code other than 2xx \(response)"))) return .failure((.invalidStatusCode("Request returned status code other than 2xx \(response)")))
} }
guard let data = data else { return .failure(.dataReturnedNil) } guard let data = data else {
return .failure(.dataReturnedNil)
}
return .success(data) return .success(data)
} }
} }
struct ResponseMetadata {
let currentPage: Int
let itemsTotal: Int
let itemsPerPage: Int
}
extension ResponseMetadata {
var pagesTotal: Int {
return itemsTotal / itemsPerPage
}
}
struct Resource<A> { struct Resource<A> {
let url: URL let url: URL
let parse: (Data) -> Result<A> let parse: (Data, URLResponse?) -> Result<A>
} }
extension Resource { extension Resource {
init(url: URL, page: Int, parseResponse: @escaping (ResponseMetadata, Data) -> Result<A>) {
init(url: URL, parseJSON: @escaping (Any) -> Result<A>) { // Append extra data to url for paging
self.url = url guard let url = URL(string: url.absoluteString.appending("&page=\(page)")) else {
self.parse = { data in fatalError("Malformed URL given");
do { }
let jsonData = try JSONSerialization.jsonObject(with: data, options: []) self.url = url
return parseJSON(jsonData) self.parse = { data, response in
} catch { // Parse out metadata from header
fatalError("Error parsing data") guard let httpUrlResponse = response as? HTTPURLResponse,
} let xTotalString = httpUrlResponse.allHeaderFields["x-total"] as? String,
let xTotal = Int(xTotalString),
let xPerPageString = httpUrlResponse.allHeaderFields["x-per-page"] as? String,
let xPerPage = Int(xPerPageString)
else {
return .failure(.errorParsingResponse)
}
let metadata = ResponseMetadata(currentPage: page, itemsTotal: xTotal, itemsPerPage: xPerPage)
return parseResponse(metadata, data)
} }
} }
} }
enum Result<T> { enum Result<T> {
case success(T) case success(T)
case failure(NetworkingErrors) case failure(NetworkingError)
} }
enum NetworkingErrors: Error { enum NetworkingError: Error {
case errorParsingResponse
case errorParsingJSON case errorParsingJSON
case noInternetConnection case noInternetConnection
case dataReturnedNil case dataReturnedNil

View File

@ -1,8 +1,9 @@
source 'https://github.com/CocoaPods/Specs.git'
platform :ios, '9.0'
target 'ASDKgram-Swift' do target 'ASDKgram-Swift' do
use_frameworks! use_frameworks!
inhibit_all_warnings!
pod 'Texture', '>= 2.0' pod 'Texture/PINRemoteImage', :path => '../..'
end end