From 725e8218b7399edd55bc6e49ef335eab3be648fa Mon Sep 17 00:00:00 2001 From: Michael Schneider Date: Tue, 26 Jun 2018 10:29:09 -0700 Subject: [PATCH] Rewrite Swift Example --- .../ASDKgram/Sample.xcodeproj/project.pbxproj | 34 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../ASDKgram-Swift.xcodeproj/project.pbxproj | 54 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../ASDKgram-Swift/AppDelegate.swift | 22 +- .../ASDKgram-Swift/Constants.swift | 38 +- .../ASDKgram-Swift/ASDKgram-Swift/Date.swift | 37 +- .../ASDKgram-Swift/NetworkImageView.swift | 18 +- .../ASDKgram-Swift/NumberFormatter.swift | 26 + .../OrderedDictionary+Codable.swift | 134 ++++ .../OrderedDictionary+Description.swift | 50 ++ .../OrderedDictionary/OrderedDictionary.swift | 610 ++++++++++++++++++ .../ASDKgram-Swift/ParseResponse.swift | 31 + .../ASDKgram-Swift/PhotoFeedModel.swift | 158 +++-- .../PhotoFeedTableNodeController.swift | 93 ++- .../PhotoFeedTableViewController.swift | 61 +- .../ASDKgram-Swift/PhotoModel.swift | 180 +++--- .../ASDKgram-Swift/PhotoTableNodeCell.swift | 73 ++- .../ASDKgram-Swift/PhotoTableViewCell.swift | 32 +- .../ASDKgram-Swift/PopularPageModel.swift | 45 +- .../ASDKgram-Swift/UIColor.swift | 20 +- .../ASDKgram-Swift/UIImage.swift | 60 -- .../ASDKgram-Swift/ASDKgram-Swift/URL.swift | 60 +- .../ASDKgram-Swift/Webservice.swift | 89 ++- examples_extra/ASDKgram-Swift/Podfile | 7 +- 25 files changed, 1371 insertions(+), 577 deletions(-) create mode 100644 examples/ASDKgram/Sample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 examples_extra/ASDKgram-Swift/ASDKgram-Swift/NumberFormatter.swift create mode 100644 examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Codable.swift create mode 100644 examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Description.swift create mode 100644 examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary.swift create mode 100644 examples_extra/ASDKgram-Swift/ASDKgram-Swift/ParseResponse.swift delete mode 100644 examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIImage.swift diff --git a/examples/ASDKgram/Sample.xcodeproj/project.pbxproj b/examples/ASDKgram/Sample.xcodeproj/project.pbxproj index abafabf31..08713d5b1 100644 --- a/examples/ASDKgram/Sample.xcodeproj/project.pbxproj +++ b/examples/ASDKgram/Sample.xcodeproj/project.pbxproj @@ -278,8 +278,6 @@ 05E2127D19D4DB510098F589 /* Sources */, 05E2127E19D4DB510098F589 /* Frameworks */, 05E2127F19D4DB510098F589 /* Resources */, - F012A6F39E0149F18F564F50 /* [CP] Copy Pods Resources */, - 06770D39D4186D6446B1BDD5 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -338,21 +336,6 @@ /* End PBXResourcesBuildPhase 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 */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -368,22 +351,7 @@ ); runOnlyForDeploymentPostprocessing = 0; 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"; - 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"; + 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; }; /* End PBXShellScriptBuildPhase section */ diff --git a/examples/ASDKgram/Sample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples/ASDKgram/Sample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples/ASDKgram/Sample.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj index 23ef18b40..e073f01f5 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcodeproj/project.pbxproj @@ -8,7 +8,6 @@ /* Begin PBXBuildFile section */ 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 */; }; 3AB33F651E1F94530039F711 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3AB33F641E1F94530039F711 /* Assets.xcassets */; }; 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 */; }; 3AB33F8C1E2106F30039F711 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33F8B1E2106F30039F711 /* URL.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 */; }; 3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AB33FA11E230A160039F711 /* NetworkImageView.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 */; }; + 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 */ /* 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 = ""; }; 3A2362FA1E2D33A0007E08F1 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = ""; }; - 3A7A28D81E2F7410003E2B8D /* UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; 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 = ""; }; 3AB33F641E1F94530039F711 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -46,11 +48,15 @@ 3AB33F871E20ED460039F711 /* PhotoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoModel.swift; sourceTree = ""; }; 3AB33F8B1E2106F30039F711 /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 3AB33F951E2269D40039F711 /* PopularPageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularPageModel.swift; sourceTree = ""; }; - 3AB33F971E22A0080039F711 /* PX500Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PX500Convenience.swift; sourceTree = ""; }; + 3AB33F971E22A0080039F711 /* ParseResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseResponse.swift; sourceTree = ""; }; 3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableViewCell.swift; sourceTree = ""; }; 3AB33FA11E230A160039F711 /* NetworkImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageView.swift; sourceTree = ""; }; 3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableNodeCell.swift; sourceTree = ""; }; 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 = ""; }; + 9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Codable.swift"; sourceTree = ""; }; + 9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = ""; }; + 9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Description.swift"; sourceTree = ""; }; 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 = ""; }; /* End PBXFileReference section */ @@ -87,6 +93,7 @@ 3AB33F5C1E1F94530039F711 /* ASDKgram-Swift */ = { isa = PBXGroup; children = ( + 9D4DFC5A20E1DF660067C960 /* OrderedDictionary */, 3AB33F991E22CF160039F711 /* Views */, 3AB33F841E20E98C0039F711 /* Model */, 3AB33F7D1E1FDA890039F711 /* Client */, @@ -129,10 +136,10 @@ 3AB33F791E1F9E4E0039F711 /* Extensions */ = { isa = PBXGroup; children = ( + 3A2362FA1E2D33A0007E08F1 /* Date.swift */, + 692CD06D20E1A40D00D9B963 /* NumberFormatter.swift */, 3AB33F7A1E1F9E630039F711 /* UIColor.swift */, 3AB33F8B1E2106F30039F711 /* URL.swift */, - 3A2362FA1E2D33A0007E08F1 /* Date.swift */, - 3A7A28D81E2F7410003E2B8D /* UIImage.swift */, ); name = Extensions; sourceTree = ""; @@ -141,7 +148,7 @@ isa = PBXGroup; children = ( 3AB33F801E1FDE100039F711 /* Webservice.swift */, - 3AB33F971E22A0080039F711 /* PX500Convenience.swift */, + 3AB33F971E22A0080039F711 /* ParseResponse.swift */, ); name = Client; sourceTree = ""; @@ -191,6 +198,16 @@ name = Pods; sourceTree = ""; }; + 9D4DFC5A20E1DF660067C960 /* OrderedDictionary */ = { + isa = PBXGroup; + children = ( + 9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */, + 9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */, + 9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */, + ); + path = OrderedDictionary; + sourceTree = ""; + }; A7DD645D70CF34C7CA3B1A8B /* Frameworks */ = { isa = PBXGroup; children = ( @@ -211,7 +228,6 @@ 3AB33F571E1F94520039F711 /* Frameworks */, 3AB33F581E1F94520039F711 /* Resources */, 154783123A953C3AFB9805CF /* [CP] Embed Pods Frameworks */, - 07D25AC7E9C9518F14F0C929 /* [CP] Copy Pods Resources */, 3A7BEDD71E254278005769D4 /* ShellScript */, ); buildRules = ( @@ -272,21 +288,6 @@ /* End PBXResourcesBuildPhase 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 */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; @@ -352,14 +353,17 @@ 3AB33F781E1F9C400039F711 /* PhotoFeedTableNodeController.swift in Sources */, 3A2362FB1E2D33A0007E08F1 /* Date.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 */, 3AB33FA21E230A160039F711 /* NetworkImageView.swift in Sources */, + 9D4DFC6020E1DF660067C960 /* OrderedDictionary+Description.swift in Sources */, 3AB33F8C1E2106F30039F711 /* URL.swift in Sources */, 3AB33F831E20E81E0039F711 /* Constants.swift in Sources */, + 9D4DFC5F20E1DF660067C960 /* OrderedDictionary.swift in Sources */, 3AB33F961E2269D40039F711 /* PopularPageModel.swift in Sources */, - 3A7A28D91E2F7410003E2B8D /* UIImage.swift in Sources */, 3AB33F5E1E1F94530039F711 /* AppDelegate.swift in Sources */, + 9D4DFC5E20E1DF660067C960 /* OrderedDictionary+Codable.swift in Sources */, 3AB33F811E1FDE100039F711 /* Webservice.swift in Sources */, 3AB33F9E1E22D9DB0039F711 /* PhotoTableViewCell.swift in Sources */, 3AB33F861E20E9B10039F711 /* PhotoFeedModel.swift in Sources */, diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 000000000..18d981003 --- /dev/null +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/AppDelegate.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/AppDelegate.swift index 886d28ca7..85b7de107 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/AppDelegate.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/AppDelegate.swift @@ -2,19 +2,17 @@ // AppDelegate.swift // ASDKgram-Swift // -// Created by Calum Harris on 06/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import UIKit @@ -42,11 +40,11 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let tabBarController = UITabBarController() tabBarController.viewControllers = [UIKitNavController, ASDKNavController] tabBarController.selectedIndex = 1 - tabBarController.tabBar.tintColor = UIColor.mainBarTintColor() + tabBarController.tabBar.tintColor = UIColor.mainBarTintColor // Nav Bar appearance - UINavigationBar.appearance().barTintColor = UIColor.mainBarTintColor() + UINavigationBar.appearance().barTintColor = UIColor.mainBarTintColor // UIWindow diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Constants.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Constants.swift index f85bb817f..c56cc8f68 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Constants.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Constants.swift @@ -2,35 +2,33 @@ // Constants // ASDKgram-Swift // -// Created by Calum Harris on 07/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // // swiftlint:disable nesting import UIKit struct Constants { - - struct PX500 { - struct URLS { - static let Host = "https://api.500px.com/v1/" - 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 - static let UserEndpoint = "photos?user_id=" - static let ConsumerKey = "&consumer_key=Fi13GVb8g53sGvHICzlram7QkKOlSDmAmp9s9aqC" - } - } + struct Unsplash { + struct URLS { + static let Host = "https://api.unsplash.com/" + static let PopularEndpoint = "photos?order_by=popular" + static let SearchEndpoint = "photos/search?geo=" //latitude,longitude,radius + static let UserEndpoint = "photos?user_id=" + static let ConsumerKey = "&client_id=3b99a69cee09770a4a0bbb870b437dbda53efb22f6f6de63714b71c4df7c9642" + static let ImagesPerPage = 30 + } + } struct CellLayout { static let FontSize: CGFloat = 14 diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Date.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Date.swift index 0d7cde2d3..df4d189a2 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Date.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Date.swift @@ -2,25 +2,22 @@ // Date.swift // ASDKgram-Swift // -// Created by Calum Harris on 16/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import Foundation extension Date { - static let iso8601Formatter: DateFormatter = { let formatter = DateFormatter() formatter.calendar = Calendar(identifier: .iso8601) @@ -29,4 +26,22 @@ extension Date { formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 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" + } + } } diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NetworkImageView.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NetworkImageView.swift index aeb860d8a..47d02a2e1 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NetworkImageView.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NetworkImageView.swift @@ -2,19 +2,17 @@ // NetworkImageView.swift // ASDKgram-Swift // -// Created by Calum Harris on 09/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import UIKit diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NumberFormatter.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NumberFormatter.swift new file mode 100644 index 000000000..699db0355 --- /dev/null +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/NumberFormatter.swift @@ -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 + }() +} diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Codable.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Codable.swift new file mode 100644 index 000000000..b09643325 --- /dev/null +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Codable.swift @@ -0,0 +1,134 @@ +#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(_ 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(_ 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." + ) + ) + } + +} diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Description.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Description.swift new file mode 100644 index 000000000..efc991633 --- /dev/null +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary+Description.swift @@ -0,0 +1,50 @@ +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)]" + } + +} diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary.swift new file mode 100644 index 000000000..64065332e --- /dev/null +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/OrderedDictionary/OrderedDictionary.swift @@ -0,0 +1,610 @@ +/// 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: 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 + + /// The type of the contiguous subrange of the ordered dictionary's elements. + /// + /// - SeeAlso: OrderedDictionarySlice + public typealias SubSequence = OrderedDictionarySlice + + // ======================================================= // + // 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: 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) { + 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, 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(_ 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 { + return self.lazy.map { $0.key } + } + + /// A collection containing just the values of the ordered dictionary in the correct order. + public var orderedValues: OrderedDictionaryValues { + return self.lazy.map { $0.value } + } + + // ======================================================= // + // MARK: - Dictionary + // ======================================================= // + + /// Converts itself to a common unsorted dictionary. + public var unorderedDictionary: Dictionary { + 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 { + 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) -> 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 = Slice> + +/// 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 = LazyMapCollection, 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 = LazyMapCollection, Value> + +#else + +/// A view into an ordered dictionary whose indices are a subrange of the indices of the ordered +/// dictionary. +public typealias OrderedDictionarySlice = Slice> + +/// 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 = LazyMapCollection, 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 = LazyMapCollection, 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 + } + +} diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/ParseResponse.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/ParseResponse.swift new file mode 100644 index 000000000..5a82c1014 --- /dev/null +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/ParseResponse.swift @@ -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 { + let parse = Resource(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 +} diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift index f344df34a..9e7cca4ca 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedModel.swift @@ -2,116 +2,114 @@ // PhotoFeedModel.swift // ASDKgram-Swift // -// Created by Calum Harris on 07/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import UIKit final class PhotoFeedModel { + + // MARK: Properties public private(set) var photoFeedModelType: PhotoFeedModelType - public private(set) var photos: [PhotoModel] = [] - public private(set) var imageSize: CGSize - private var url: URL - private var ids: [Int] = [] + + private var orderedPhotos: OrderedDictionary = [:] private var currentPage: 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 refreshFeedInProgress: Bool = false - init(initWithPhotoFeedModelType: PhotoFeedModelType, requiredImageSize: CGSize) { - self.photoFeedModelType = initWithPhotoFeedModelType - self.imageSize = requiredImageSize - self.url = URL.URLForFeedModelType(feedModelType: initWithPhotoFeedModelType) - } + // MARK: Lifecycle - var numberOfItemsInFeed: Int { - return photos.count + init(photoFeedModelType: PhotoFeedModelType) { + 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 func updateNewBatchOfPopularPhotos(additionsAndConnectionStatusCompletion: @escaping (Int, InternetStatus) -> ()) { - - guard !fetchPageInProgress else { return } - - fetchPageInProgress = true - fetchNextPageOfPopularPhotos(replaceData: false) { [unowned self] additions, errors in - self.fetchPageInProgress = false - - if let error = errors { - switch error { - case .noInternetConnection: - additionsAndConnectionStatusCompletion(0, .noConnection) - default: additionsAndConnectionStatusCompletion(0, .connected) - } - } else { - additionsAndConnectionStatusCompletion(additions, .connected) - } - } + + // For this example let's use the main thread as locking queue + DispatchQueue.main.async { + guard !self.fetchPageInProgress else { + return + } + + self.fetchPageInProgress = true + self.fetchNextPageOfPopularPhotos(replaceData: false) { [unowned self] additions, error in + self.fetchPageInProgress = false + + if let error = error { + switch error { + case .noInternetConnection: + additionsAndConnectionStatusCompletion(0, .noConnection) + default: + additionsAndConnectionStatusCompletion(0, .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 { - DispatchQueue.main.async { - numberOfAdditionsCompletion(0, .customError("No pages left to parse")) - } + numberOfAdditionsCompletion(0, .customError("No pages left to parse")) return } - var newPhotos: [PhotoModel] = [] - var newIDs: [Int] = [] - let pageToFetch = currentPage + 1 - - let url = self.url.addImageParameterForClosestImageSizeAndpage(size: imageSize, page: pageToFetch) - - WebService().load(resource: parsePopularPage(withURL: url)) { [unowned self] result in - + WebService().load(resource: parsePopularPage(withURL: url, page: pageToFetch)) { [unowned self] result in + // Callback will happen on main for now switch result { - case .success(let popularPage): - self.totalItems = popularPage.totalNumberOfItems - self.totalPages = popularPage.totalPages - self.currentPage = popularPage.page - - for photo in popularPage.photos { - if !replaceData || !self.ids.contains(photo.photoID) { - newPhotos.append(photo) - newIDs.append(photo.photoID) - } - } - - DispatchQueue.main.async { - if replaceData { - self.photos = newPhotos - self.ids = newIDs - } else { - self.photos += newPhotos - self.ids += newIDs - } - - numberOfAdditionsCompletion(newPhotos.count, nil) - } - + case .success(let itemsPage): + // Update current state + self.totalItems = itemsPage.totalNumberOfItems + self.totalPages = itemsPage.totalPages + self.currentPage = itemsPage.page + + // Update photos + if replaceData { + self.orderedPhotos = [] + } + var insertedItems = 0 + for photo in itemsPage.photos { + if !self.orderedPhotos.containsKey(photo.photoID) { + // Append a new key-value pair by setting a value for an non-existent key + self.orderedPhotos[photo.photoID] = photo + insertedItems += 1 + } + } + + numberOfAdditionsCompletion(insertedItems, nil) case .failure(let fail): - print(fail) - DispatchQueue.main.async { + print(fail) numberOfAdditionsCompletion(0, fail) - } } } } diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift index 08c1bc327..51d509ed8 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableNodeController.swift @@ -2,103 +2,102 @@ // PhotoFeedTableNodeController.swift // ASDKgram-Swift // -// Created by Calum Harris on 06/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import AsyncDisplayKit class PhotoFeedTableNodeController: ASViewController { + + // MARK: Lifecycle - var activityIndicator: UIActivityIndicatorView! - var photoFeed: PhotoFeedModel + private lazy var activityIndicatorView: UIActivityIndicatorView = { + return UIActivityIndicatorView(activityIndicatorStyle: .gray) + }() + + var photoFeedModel = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular) init() { - photoFeed = PhotoFeedModel(initWithPhotoFeedModelType: .photoFeedModelTypePopular, requiredImageSize: screenSizeForWidth) - super.init(node: ASTableNode()) - self.navigationItem.title = "ASDK" + super.init(node: ASTableNode()) + navigationItem.title = "ASDK" } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } + + // MAKR: UIViewController override func viewDidLoad() { super.viewDidLoad() - setupActivityIndicator() + node.allowsSelection = false - node.view.separatorStyle = .none node.dataSource = self node.delegate = self node.leadingScreensForBatching = 2.5 - navigationController?.hidesBarsOnSwipe = true - } - - // helper functions - func setupActivityIndicator() { - let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) - self.activityIndicator = activityIndicator - let bounds = self.node.frame - var refreshRect = activityIndicator.frame - refreshRect.origin = CGPoint(x: (bounds.size.width - activityIndicator.frame.size.width) / 2.0, y: (bounds.size.height - activityIndicator.frame.size.height) / 2.0) - activityIndicator.frame = refreshRect - self.node.view.addSubview(activityIndicator) + node.view.separatorStyle = .none + + navigationController?.hidesBarsOnSwipe = true + + node.view.addSubview(activityIndicatorView) } - - 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) - }() + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // Center the activity indicator view + let bounds = node.bounds + activityIndicatorView.frame.origin = CGPoint( + x: (bounds.width - activityIndicatorView.frame.width) / 2.0, + y: (bounds.height - activityIndicatorView.frame.height) / 2.0 + ) + } func fetchNewBatchWithContext(_ context: ASBatchContext?) { DispatchQueue.main.async { - self.activityIndicator.startAnimating() + self.activityIndicatorView.startAnimating() } - photoFeed.updateNewBatchOfPopularPhotos() { additions, connectionStatus in + photoFeedModel.updateNewBatchOfPopularPhotos() { additions, connectionStatus in switch connectionStatus { case .connected: - self.activityIndicator.stopAnimating() + self.activityIndicatorView.stopAnimating() self.addRowsIntoTableNode(newPhotoCount: additions) context?.completeBatchFetching(true) case .noConnection: - self.activityIndicator.stopAnimating() - if context != nil { - context!.completeBatchFetching(true) - } - break + self.activityIndicatorView.stopAnimating() + context?.completeBatchFetching(true) } } } func addRowsIntoTableNode(newPhotoCount newPhotos: Int) { - let indexRange = (photoFeed.photos.count - newPhotos.. Int { - return photoFeed.numberOfItemsInFeed + return photoFeedModel.numberOfItems } func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { - let photo = photoFeed.photos[indexPath.row] + let photo = photoFeedModel.itemAtIndexPath(indexPath) let nodeBlock: ASCellNodeBlock = { _ in return PhotoTableNodeCell(photoModel: photo) } diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableViewController.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableViewController.swift index 0e7cbef97..e09b89607 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableViewController.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoFeedTableViewController.swift @@ -2,19 +2,17 @@ // PhotoFeedTableViewController.swift // ASDKgram-Swift // -// Created by Calum Harris on 06/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import UIKit @@ -22,13 +20,12 @@ import UIKit class PhotoFeedTableViewController: UITableViewController { var activityIndicator: UIActivityIndicatorView! - var photoFeed: PhotoFeedModel + var photoFeed = PhotoFeedModel(photoFeedModelType: .photoFeedModelTypePopular) init() { - photoFeed = PhotoFeedModel(initWithPhotoFeedModelType: .photoFeedModelTypePopular, requiredImageSize: screenSizeForWidth) - super.init(nibName: nil, bundle: nil) - self.navigationItem.title = "UIKit" - + super.init(nibName: nil, bundle: nil) + + navigationItem.title = "UIKit" } required init?(coder aDecoder: NSCoder) { @@ -37,12 +34,13 @@ class PhotoFeedTableViewController: UITableViewController { override func viewDidLoad() { super.viewDidLoad() + + navigationController?.hidesBarsOnSwipe = true setupActivityIndicator() configureTableView() fetchNewBatch() - navigationController?.hidesBarsOnSwipe = true } - + func fetchNewBatch() { activityIndicator.startAnimating() photoFeed.updateNewBatchOfPopularPhotos() { additions, connectionStatus in @@ -57,22 +55,17 @@ class PhotoFeedTableViewController: UITableViewController { } } - 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) - }() - - // helper functions + // Helper functions func setupActivityIndicator() { let activityIndicator = UIActivityIndicatorView(activityIndicatorStyle: .gray) + activityIndicator.translatesAutoresizingMaskIntoConstraints = false self.activityIndicator = activityIndicator - self.tableView.addSubview(activityIndicator) - activityIndicator.translatesAutoresizingMaskIntoConstraints = false + self.tableView.addSubview(activityIndicator) + NSLayoutConstraint.activate([ activityIndicator.centerXAnchor.constraint(equalTo: self.tableView.centerXAnchor), activityIndicator.centerYAnchor.constraint(equalTo: self.tableView.centerYAnchor) - ]) + ]) } func configureTableView() { @@ -87,7 +80,7 @@ extension PhotoFeedTableViewController { func addRowsIntoTableView(newPhotoCount newPhotos: Int) { - let indexRange = (photoFeed.photos.count - newPhotos.. Int { - return photoFeed.photos.count + return photoFeed.numberOfItems } 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") } - cell.photoModel = photoFeed.photos[indexPath.row] + cell.photoModel = photoFeed.itemAtIndexPath(indexPath) return cell } 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) { - let currentOffSetY = scrollView.contentOffset.y let contentHeight = scrollView.contentSize.height - let screenHeight = UIScreen.main.bounds.size.height + let screenHeight = UIScreen.main.bounds.height let screenfullsBeforeBottom = (contentHeight - currentOffSetY) / screenHeight if screenfullsBeforeBottom < 2.5 { self.fetchNewBatch() diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoModel.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoModel.swift index 13508631b..e9a049f43 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoModel.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoModel.swift @@ -2,91 +2,123 @@ // PhotoModel.swift // ASDKgram-Swift // -// Created by Calum Harris on 07/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import UIKit -typealias JSONDictionary = [String : Any] +// MARK: ProfileImage -struct PhotoModel { - - let url: String - let photoID: Int - let dateString: String - let descriptionText: String +struct ProfileImage: Codable { + let large: String + let medium: String + let small: 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 ownerUserName: String - let ownerPicURL: String - - init?(dictionary: JSONDictionary) { - - guard let images = dictionary["images"] as? [[String: Any]], - let url = images[0]["url"] as? String, - let date = dictionary["created_at"] as? String, - let photoID = dictionary["id"] as? Int, - let descriptionText = dictionary["name"] as? String, - let likesCount = dictionary["positive_votes_count"] as? Int else - { print("error parsing JSON within PhotoModel Init"); return nil } - - 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 } - - self.url = url - self.photoID = photoID - self.descriptionText = descriptionText - self.likesCount = likesCount - self.dateString = date - self.ownerUserName = username - self.ownerPicURL = ownerPicURL - } + let width: Int + let height: Int + let user: UserModel + + enum CodingKeys: String, CodingKey { + case photoID = "id" + case urls = "urls" + case uploadedDateString = "created_at" + case descriptionText = "description" + case likesCount = "likes" + case width = "width" + case height = "height" + case user = "user" + } +} + +extension PhotoModel { + var url: String { + return urls.regular + } } extension PhotoModel { // MARK: - Attributed Strings - func attrStringForUserName(withSize size: CGFloat) -> NSAttributedString { - let attr = [ + func attributedStringForUserName(withSize size: CGFloat) -> NSAttributedString { + let attributes = [ NSForegroundColorAttributeName : UIColor.darkGray, 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 { - let attr = [ + func attributedStringForDescription(withSize size: CGFloat) -> NSAttributedString { + let attributes = [ NSForegroundColorAttributeName : UIColor.darkGray, 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() - formatter.numberStyle = .decimal - let formattedLikesNumber: String? = formatter.string(from: NSNumber(value: self.likesCount)) - let likesString: String = "\(formattedLikesNumber!) Likes" - let textAttr = [NSForegroundColorAttributeName : UIColor.mainBarTintColor(), NSFontAttributeName: UIFont.systemFont(ofSize: size)] - let likesAttrString = NSAttributedString(string: likesString, attributes: textAttr) + let likesAttributes = [ + NSForegroundColorAttributeName : UIColor.mainBarTintColor, + NSFontAttributeName: UIFont.systemFont(ofSize: size) + ] + let likesAttrString = NSAttributedString(string: "\(formattedLikesNumber) Likes", attributes: likesAttributes) - let heartAttr = [NSForegroundColorAttributeName : UIColor.red, NSFontAttributeName: UIFont.systemFont(ofSize: size)] - let heartAttrString = NSAttributedString(string: "♥︎ ", attributes: heartAttr) + let heartAttributes = [ + NSForegroundColorAttributeName : UIColor.red, + NSFontAttributeName: UIFont.systemFont(ofSize: size) + ] + let heartAttrString = NSAttributedString(string: "♥︎ ", attributes: heartAttributes) let combine = NSMutableAttributedString() combine.append(heartAttrString) @@ -94,32 +126,16 @@ extension PhotoModel { return combine } - func attrStringForTimeSinceString(withSize size: CGFloat) -> NSAttributedString { - - let attr = [ - NSForegroundColorAttributeName : UIColor.mainBarTintColor(), + func attributedStringForTimeSinceString(withSize size: CGFloat) -> NSAttributedString { + guard let date = Date.iso8601Formatter.date(from: self.uploadedDateString) else { + return NSAttributedString(); + } + + let attributes = [ + NSForegroundColorAttributeName : UIColor.mainBarTintColor, NSFontAttributeName: UIFont.systemFont(ofSize: size) ] - let date = Date.iso8601Formatter.date(from: self.dateString)! - 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" - } + return NSAttributedString(string: Date.timeStringSince(fromConverted: date), attributes: attributes) } } diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableNodeCell.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableNodeCell.swift index 56e02588b..4ad4ea8ab 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableNodeCell.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableNodeCell.swift @@ -2,53 +2,60 @@ // PhotoTableNodeCell.swift // ASDKgram-Swift // -// Created by Calum Harris on 09/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. +// 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 // -// 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 AsyncDisplayKit class PhotoTableNodeCell: ASCellNode { - + + // MARK: Properties + let usernameLabel = ASTextNode() let timeIntervalLabel = ASTextNode() let photoLikesLabel = ASTextNode() let photoDescriptionLabel = ASTextNode() let avatarImageNode: ASNetworkImageNode = { - let imageNode = ASNetworkImageNode() - imageNode.contentMode = .scaleAspectFill - imageNode.imageModificationBlock = ASImageNodeRoundBorderModificationBlock(0, nil) - return imageNode + let node = ASNetworkImageNode() + node.contentMode = .scaleAspectFill + // Set the imageModificationBlock for a rounded avatar + node.imageModificationBlock = ASImageNodeRoundBorderModificationBlock(0, nil) + return node }() let photoImageNode: ASNetworkImageNode = { - let imageNode = ASNetworkImageNode() - imageNode.contentMode = .scaleAspectFill - return imageNode + let node = ASNetworkImageNode() + node.contentMode = .scaleAspectFill + return node }() + + // MARK: Lifecycle init(photoModel: PhotoModel) { super.init() - self.photoImageNode.url = URL(string: photoModel.url) - self.avatarImageNode.url = URL(string: photoModel.ownerPicURL) - self.usernameLabel.attributedText = photoModel.attrStringForUserName(withSize: Constants.CellLayout.FontSize) - self.timeIntervalLabel.attributedText = photoModel.attrStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) - self.photoLikesLabel.attributedText = photoModel.attrStringLikes(withSize: Constants.CellLayout.FontSize) - self.photoDescriptionLabel.attributedText = photoModel.attrStringForDescription(withSize: Constants.CellLayout.FontSize) - self.automaticallyManagesSubnodes = true + + automaticallyManagesSubnodes = true + photoImageNode.url = URL(string: photoModel.url) + avatarImageNode.url = URL(string: photoModel.user.profileImage) + usernameLabel.attributedText = photoModel.attributedStringForUserName(withSize: Constants.CellLayout.FontSize) + timeIntervalLabel.attributedText = photoModel.attributedStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) + photoLikesLabel.attributedText = photoModel.attributedStringLikes(withSize: Constants.CellLayout.FontSize) + photoDescriptionLabel.attributedText = photoModel.attributedStringForDescription(withSize: Constants.CellLayout.FontSize) } + + // MARK: ASDisplayNode override func layoutSpecThatFits(_ constrainedSize: ASSizeRange) -> ASLayoutSpec { @@ -58,9 +65,13 @@ class PhotoTableNodeCell: ASCellNode { let headerStack = ASStackLayoutSpec.horizontal() 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)) - usernameLabel.style.flexShrink = 1.0 + + usernameLabel.style.flexShrink = 1.0 headerChildren.append(usernameLabel) let spacer = ASLayoutSpec() @@ -76,9 +87,11 @@ class PhotoTableNodeCell: ASCellNode { headerStack.children = headerChildren let verticalStack = ASStackLayoutSpec.vertical() - - verticalStack.children = [ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForHeader, child: headerStack), ASRatioLayoutSpec(ratio: 1.0, child: photoImageNode), ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForFooter, child: footerStack)] - + verticalStack.children = [ + ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForHeader, child: headerStack), + ASRatioLayoutSpec(ratio: 1.0, child: photoImageNode), + ASInsetLayoutSpec(insets: Constants.CellLayout.InsetForFooter, child: footerStack) + ] return verticalStack } } diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableViewCell.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableViewCell.swift index b57beab9a..982f673d0 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableViewCell.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PhotoTableViewCell.swift @@ -2,19 +2,17 @@ // PhotoTableViewCell.swift // ASDKgram-Swift // -// Created by Calum Harris on 08/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import UIKit @@ -25,15 +23,15 @@ class PhotoTableViewCell: UITableViewCell { didSet { if let model = photoModel { photoImageView.loadImageUsingUrlString(urlString: model.url) - avatarImageView.loadImageUsingUrlString(urlString: model.ownerPicURL) - photoLikesLabel.attributedText = model.attrStringLikes(withSize: Constants.CellLayout.FontSize) - usernameLabel.attributedText = model.attrStringForUserName(withSize: Constants.CellLayout.FontSize) - timeIntervalLabel.attributedText = model.attrStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) - photoDescriptionLabel.attributedText = model.attrStringForDescription(withSize: Constants.CellLayout.FontSize) + avatarImageView.loadImageUsingUrlString(urlString: model.user.profileImage) + photoLikesLabel.attributedText = model.attributedStringLikes(withSize: Constants.CellLayout.FontSize) + usernameLabel.attributedText = model.attributedStringForUserName(withSize: Constants.CellLayout.FontSize) + timeIntervalLabel.attributedText = model.attributedStringForTimeSinceString(withSize: Constants.CellLayout.FontSize) + photoDescriptionLabel.attributedText = model.attributedStringForDescription(withSize: Constants.CellLayout.FontSize) photoDescriptionLabel.sizeToFit() var rect = photoDescriptionLabel.frame 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 } } @@ -133,7 +131,7 @@ class PhotoTableViewCell: UITableViewCell { let photoHeight = width let font = UIFont.systemFont(ofSize: Constants.CellLayout.FontSize) 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 descriptionHeight = descriptionAttrString.boundingRect(with: CGSize(width: availableWidth, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, context: nil).size.height diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PopularPageModel.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PopularPageModel.swift index 8aca2445f..0c32a2bb0 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PopularPageModel.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/PopularPageModel.swift @@ -2,36 +2,31 @@ // PopularPageModel.swift // ASDKgram-Swift // -// Created by Calum Harris on 08/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import Foundation -class PopularPageModel: NSObject { - - let page: Int - let totalPages: Int - let totalNumberOfItems: Int - let photos: [PhotoModel] - - init?(dictionary: JSONDictionary, photosArray: [PhotoModel]) { - 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.page = page - self.totalPages = totalPages - self.totalNumberOfItems = totalItems - self.photos = photosArray - } +struct PopularPageModel { + let page: Int + let totalPages: Int + let totalNumberOfItems: Int + let photos: [PhotoModel] + + init(metaData: ResponseMetadata, photos:[PhotoModel]) { + self.page = metaData.currentPage + self.totalPages = metaData.pagesTotal + self.totalNumberOfItems = metaData.itemsTotal + self.photos = photos + } } diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIColor.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIColor.swift index 5ab9d7e01..40cea6f91 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIColor.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIColor.swift @@ -2,25 +2,23 @@ // UIColor.swift // ASDKgram-Swift // -// Created by Calum Harris on 06/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. +// 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. // -// 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. +// 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 UIKit extension UIColor { - - class func mainBarTintColor() -> UIColor { + static var mainBarTintColor: UIColor { return UIColor(red: 69/255, green: 142/255, blue: 255/255, alpha: 1) } } diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIImage.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIImage.swift deleted file mode 100644 index 4eb233f37..000000000 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/UIImage.swift +++ /dev/null @@ -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 - } -} diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/URL.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/URL.swift index 596049815..cbdd5a942 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/URL.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/URL.swift @@ -2,66 +2,36 @@ // URL.swift // ASDKgram-Swift // -// Created by Calum Harris on 07/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // import UIKit extension URL { - static func URLForFeedModelType(feedModelType: PhotoFeedModelType) -> URL { switch feedModelType { case .photoFeedModelTypePopular: - return URL(string: assemble500PXURLString(endpoint: Constants.PX500.URLS.PopularEndpoint))! + return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.PopularEndpoint))! case .photoFeedModelTypeLocation: - return URL(string: assemble500PXURLString(endpoint: Constants.PX500.URLS.SearchEndpoint))! + return URL(string: assembleUnsplashURLString(endpoint: Constants.Unsplash.URLS.SearchEndpoint))! 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 { - return Constants.PX500.URLS.Host + endpoint + Constants.PX500.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)! - } - + + private static func assembleUnsplashURLString(endpoint: String) -> String { + return Constants.Unsplash.URLS.Host + endpoint + Constants.Unsplash.URLS.ConsumerKey + } } diff --git a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift index 830d78eb0..0718905d5 100644 --- a/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift +++ b/examples_extra/ASDKgram-Swift/ASDKgram-Swift/Webservice.swift @@ -2,33 +2,33 @@ // Webservice.swift // ASDKgram-Swift // -// Created by Calum Harris on 06/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. +// 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 // -// 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. +// http://www.apache.org/licenses/LICENSE-2.0 // // swiftlint:disable force_cast import UIKit final class WebService { + /// Load a new resource. Callback is called on main func load(resource: Resource, completion: @escaping (Result) -> ()) { URLSession.shared.dataTask(with: resource.url) { data, response, error in // Check for errors in responses. let result = self.checkForNetworkErrors(data, response, error) DispatchQueue.main.async { + // Parsing should happen off main switch result { case .success(let data): - completion(resource.parse(data)) + completion(resource.parse(data, response)) case .failure(let error): completion(.failure(error)) } @@ -38,54 +38,77 @@ final class WebService { } extension WebService { - + /// // Check for errors in responses. fileprivate func checkForNetworkErrors(_ data: Data?, _ response: URLResponse?, _ error: Error?) -> Result { - // Check for errors in responses. if let error = error { - let nsError = error as NSError - if nsError.domain == NSURLErrorDomain && (nsError.code == NSURLErrorNotConnectedToInternet || nsError.code == NSURLErrorTimedOut) { - return .failure(.noInternetConnection) - } else { - return .failure(.returnedError(error)) - } + switch error { + case URLError.notConnectedToInternet, URLError.timedOut: + return .failure(.noInternetConnection) + default: + return .failure(.returnedError(error)) + } } if let response = response as? HTTPURLResponse, response.statusCode <= 200 && response.statusCode >= 299 { 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) } } +struct ResponseMetadata { + let currentPage: Int + let itemsTotal: Int + let itemsPerPage: Int +} + +extension ResponseMetadata { + var pagesTotal: Int { + return itemsTotal / itemsPerPage + } +} + struct Resource { let url: URL - let parse: (Data) -> Result + let parse: (Data, URLResponse?) -> Result } extension Resource { - - init(url: URL, parseJSON: @escaping (Any) -> Result) { - self.url = url - self.parse = { data in - do { - let jsonData = try JSONSerialization.jsonObject(with: data, options: []) - return parseJSON(jsonData) - } catch { - fatalError("Error parsing data") - } + init(url: URL, page: Int, parseResponse: @escaping (ResponseMetadata, Data) -> Result) { + // Append extra data to url for paging + guard let url = URL(string: url.absoluteString.appending("&page=\(page)")) else { + fatalError("Malformed URL given"); + } + self.url = url + self.parse = { data, response in + // Parse out metadata from header + 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 { case success(T) - case failure(NetworkingErrors) + case failure(NetworkingError) } -enum NetworkingErrors: Error { +enum NetworkingError: Error { + case errorParsingResponse case errorParsingJSON case noInternetConnection case dataReturnedNil diff --git a/examples_extra/ASDKgram-Swift/Podfile b/examples_extra/ASDKgram-Swift/Podfile index 6ab85b36c..5510b59d9 100644 --- a/examples_extra/ASDKgram-Swift/Podfile +++ b/examples_extra/ASDKgram-Swift/Podfile @@ -1,8 +1,9 @@ - +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '9.0' target 'ASDKgram-Swift' do - use_frameworks! + inhibit_all_warnings! - pod 'Texture', '>= 2.0' + pod 'Texture/PINRemoteImage', :path => '../..' end