mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-10 08:20:16 +00:00
Rewrite Swift Example (#1002)
* Rewrite Swift Example * Add license header to OrderedDictionary
This commit is contained in:
parent
6c487dd26c
commit
c8b5a1b323
@ -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 */
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -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 = "<group>"; };
|
||||
3A2362FA1E2D33A0007E08F1 /* Date.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Date.swift; sourceTree = "<group>"; };
|
||||
3A7A28D81E2F7410003E2B8D /* UIImage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
||||
3AB33F5A1E1F94520039F711 /* ASDKgram-Swift.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ASDKgram-Swift.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3AB33F5D1E1F94530039F711 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
3AB33F641E1F94530039F711 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
@ -46,11 +48,15 @@
|
||||
3AB33F871E20ED460039F711 /* PhotoModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoModel.swift; sourceTree = "<group>"; };
|
||||
3AB33F8B1E2106F30039F711 /* URL.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
|
||||
3AB33F951E2269D40039F711 /* PopularPageModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopularPageModel.swift; sourceTree = "<group>"; };
|
||||
3AB33F971E22A0080039F711 /* PX500Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PX500Convenience.swift; sourceTree = "<group>"; };
|
||||
3AB33F971E22A0080039F711 /* ParseResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseResponse.swift; sourceTree = "<group>"; };
|
||||
3AB33F9D1E22D9DB0039F711 /* PhotoTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableViewCell.swift; sourceTree = "<group>"; };
|
||||
3AB33FA11E230A160039F711 /* NetworkImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NetworkImageView.swift; sourceTree = "<group>"; };
|
||||
3AB33FA31E2337850039F711 /* PhotoTableNodeCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PhotoTableNodeCell.swift; sourceTree = "<group>"; };
|
||||
4D7D664E4FF432C4AE232A56 /* Pods_ASDKgram_Swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ASDKgram_Swift.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
692CD06D20E1A40D00D9B963 /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = "<group>"; };
|
||||
9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Codable.swift"; sourceTree = "<group>"; };
|
||||
9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OrderedDictionary.swift; sourceTree = "<group>"; };
|
||||
9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "OrderedDictionary+Description.swift"; sourceTree = "<group>"; };
|
||||
A3A86E74A8C3F06D7688AACB /* Pods-ASDKgram-Swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ASDKgram-Swift.release.xcconfig"; path = "Pods/Target Support Files/Pods-ASDKgram-Swift/Pods-ASDKgram-Swift.release.xcconfig"; sourceTree = "<group>"; };
|
||||
/* 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 = "<group>";
|
||||
@ -141,7 +148,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3AB33F801E1FDE100039F711 /* Webservice.swift */,
|
||||
3AB33F971E22A0080039F711 /* PX500Convenience.swift */,
|
||||
3AB33F971E22A0080039F711 /* ParseResponse.swift */,
|
||||
);
|
||||
name = Client;
|
||||
sourceTree = "<group>";
|
||||
@ -191,6 +198,16 @@
|
||||
name = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9D4DFC5A20E1DF660067C960 /* OrderedDictionary */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9D4DFC5B20E1DF660067C960 /* OrderedDictionary+Codable.swift */,
|
||||
9D4DFC5C20E1DF660067C960 /* OrderedDictionary.swift */,
|
||||
9D4DFC5D20E1DF660067C960 /* OrderedDictionary+Description.swift */,
|
||||
);
|
||||
path = OrderedDictionary;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 */,
|
||||
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -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.
|
||||
//
|
||||
// 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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// 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<units>
|
||||
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<units>
|
||||
static let UserEndpoint = "photos?user_id="
|
||||
static let ConsumerKey = "&client_id=3b99a69cee09770a4a0bbb870b437dbda53efb22f6f6de63714b71c4df7c9642"
|
||||
static let ImagesPerPage = 30
|
||||
}
|
||||
}
|
||||
|
||||
struct CellLayout {
|
||||
static let FontSize: CGFloat = 14
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// 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 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// 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
|
||||
|
||||
@ -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
|
||||
}()
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
/**
|
||||
Copyright (c) 2015-2017 Lukas Kubanek
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#if swift(>=4.1)
|
||||
|
||||
extension OrderedDictionary: Encodable where Key: Encodable, Value: Encodable {
|
||||
|
||||
/// __inheritdoc__
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
// Encode the ordered dictionary as an array of alternating key-value pairs.
|
||||
var container = encoder.unkeyedContainer()
|
||||
|
||||
for (key, value) in self {
|
||||
try container.encode(key)
|
||||
try container.encode(value)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OrderedDictionary: Decodable where Key: Decodable, Value: Decodable {
|
||||
|
||||
/// __inheritdoc__
|
||||
public init(from decoder: Decoder) throws {
|
||||
// Decode the ordered dictionary from an array of alternating key-value pairs.
|
||||
self.init()
|
||||
|
||||
var container = try decoder.unkeyedContainer()
|
||||
|
||||
while !container.isAtEnd {
|
||||
let key = try container.decode(Key.self)
|
||||
guard !container.isAtEnd else { throw DecodingError.unkeyedContainerReachedEndBeforeValue(decoder.codingPath) }
|
||||
let value = try container.decode(Value.self)
|
||||
|
||||
self[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
extension OrderedDictionary: Encodable {
|
||||
|
||||
/// __inheritdoc__
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
// Since Swift 4.0 lacks the protocol conditional conformance support, we have to make the
|
||||
// whole OrderedDictionary type conform to Encodable and assert that the key and value
|
||||
// types conform to Encodable. Furthermore, we leverage a trick of super encoders to be
|
||||
// able to encode objects without knowing their exact types. This trick was used in the
|
||||
// standard library for encoding/decoding Dictionary before Swift 4.1.
|
||||
|
||||
_assertTypeIsEncodable(Key.self, in: type(of: self))
|
||||
_assertTypeIsEncodable(Value.self, in: type(of: self))
|
||||
|
||||
var container = encoder.unkeyedContainer()
|
||||
|
||||
for (key, value) in self {
|
||||
let keyEncoder = container.superEncoder()
|
||||
try (key as! Encodable).encode(to: keyEncoder)
|
||||
|
||||
let valueEncoder = container.superEncoder()
|
||||
try (value as! Encodable).encode(to: valueEncoder)
|
||||
}
|
||||
}
|
||||
|
||||
private func _assertTypeIsEncodable<T>(_ type: T.Type, in wrappingType: Any.Type) {
|
||||
guard T.self is Encodable.Type else {
|
||||
if T.self == Encodable.self || T.self == Codable.self {
|
||||
preconditionFailure("\(wrappingType) does not conform to Encodable because Encodable does not conform to itself. You must use a concrete type to encode or decode.")
|
||||
} else {
|
||||
preconditionFailure("\(wrappingType) does not conform to Encodable because \(T.self) does not conform to Encodable.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OrderedDictionary: Decodable {
|
||||
|
||||
/// __inheritdoc__
|
||||
public init(from decoder: Decoder) throws {
|
||||
// Since Swift 4.0 lacks the protocol conditional conformance support, we have to make the
|
||||
// whole OrderedDictionary type conform to Decodable and assert that the key and value
|
||||
// types conform to Decodable. Furthermore, we leverage a trick of super decoders to be
|
||||
// able to decode objects without knowing their exact types. This trick was used in the
|
||||
// standard library for encoding/decoding Dictionary before Swift 4.1.
|
||||
|
||||
self.init()
|
||||
|
||||
_assertTypeIsDecodable(Key.self, in: type(of: self))
|
||||
_assertTypeIsDecodable(Value.self, in: type(of: self))
|
||||
|
||||
var container = try decoder.unkeyedContainer()
|
||||
|
||||
let keyMetaType = (Key.self as! Decodable.Type)
|
||||
let valueMetaType = (Value.self as! Decodable.Type)
|
||||
|
||||
while !container.isAtEnd {
|
||||
let keyDecoder = try container.superDecoder()
|
||||
let key = try keyMetaType.init(from: keyDecoder) as! Key
|
||||
|
||||
guard !container.isAtEnd else { throw DecodingError.unkeyedContainerReachedEndBeforeValue(decoder.codingPath) }
|
||||
|
||||
let valueDecoder = try container.superDecoder()
|
||||
let value = try valueMetaType.init(from: valueDecoder) as! Value
|
||||
|
||||
self[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
private func _assertTypeIsDecodable<T>(_ type: T.Type, in wrappingType: Any.Type) {
|
||||
guard T.self is Decodable.Type else {
|
||||
if T.self == Decodable.self || T.self == Codable.self {
|
||||
preconditionFailure("\(wrappingType) does not conform to Decodable because Decodable does not conform to itself. You must use a concrete type to encode or decode.")
|
||||
} else {
|
||||
preconditionFailure("\(wrappingType) does not conform to Decodable because \(T.self) does not conform to Decodable.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
fileprivate extension DecodingError {
|
||||
|
||||
fileprivate static func unkeyedContainerReachedEndBeforeValue(_ codingPath: [CodingKey]) -> DecodingError {
|
||||
return DecodingError.dataCorrupted(
|
||||
DecodingError.Context(
|
||||
codingPath: codingPath,
|
||||
debugDescription: "Unkeyed container reached end before value in key-value pair."
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
/**
|
||||
Copyright (c) 2015-2017 Lukas Kubanek
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
extension OrderedDictionary: CustomStringConvertible {
|
||||
|
||||
/// A textual representation of the ordered dictionary.
|
||||
public var description: String {
|
||||
return makeDescription(debug: false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OrderedDictionary: CustomDebugStringConvertible {
|
||||
|
||||
/// A textual representation of the ordered dictionary, suitable for debugging.
|
||||
public var debugDescription: String {
|
||||
return makeDescription(debug: true)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OrderedDictionary {
|
||||
|
||||
fileprivate func makeDescription(debug: Bool) -> String {
|
||||
// The implementation of the description is inspired by zwaldowski's implementation of the
|
||||
// ordered dictionary. See http://bit.ly/2iqGhrb
|
||||
|
||||
if isEmpty { return "[:]" }
|
||||
|
||||
let printFunction: (Any, inout String) -> () = {
|
||||
if debug {
|
||||
return { debugPrint($0, separator: "", terminator: "", to: &$1) }
|
||||
} else {
|
||||
return { print($0, separator: "", terminator: "", to: &$1) }
|
||||
}
|
||||
}()
|
||||
|
||||
let descriptionForItem: (Any) -> String = { item in
|
||||
var description = ""
|
||||
printFunction(item, &description)
|
||||
return description
|
||||
}
|
||||
|
||||
let bodyComponents = map { element in
|
||||
return descriptionForItem(element.key) + ": " + descriptionForItem(element.value)
|
||||
}
|
||||
|
||||
let body = bodyComponents.joined(separator: ", ")
|
||||
|
||||
return "[\(body)]"
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,620 @@
|
||||
/**
|
||||
Copyright (c) 2015-2017 Lukas Kubanek
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
/// A generic collection for storing key-value pairs in an ordered manner.
|
||||
///
|
||||
/// Same as in a dictionary all keys in the collection are unique and have an associated value.
|
||||
/// Same as in an array, all key-value pairs (elements) are kept sorted and accessible by
|
||||
/// a zero-based integer index.
|
||||
public struct OrderedDictionary<Key: Hashable, Value>: BidirectionalCollection {
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Type Aliases
|
||||
// ======================================================= //
|
||||
|
||||
/// The type of the key-value pair stored in the ordered dictionary.
|
||||
public typealias Element = (key: Key, value: Value)
|
||||
|
||||
/// The type of the index.
|
||||
public typealias Index = Int
|
||||
|
||||
/// The type of the indices collection.
|
||||
public typealias Indices = CountableRange<Int>
|
||||
|
||||
/// The type of the contiguous subrange of the ordered dictionary's elements.
|
||||
///
|
||||
/// - SeeAlso: OrderedDictionarySlice
|
||||
public typealias SubSequence = OrderedDictionarySlice<Key, Value>
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Initialization
|
||||
// ======================================================= //
|
||||
|
||||
/// Creates an empty ordered dictionary.
|
||||
public init() {}
|
||||
|
||||
/// Creates an ordered dictionary from a sequence of values keyed by a key which gets extracted
|
||||
/// from the value in the provided closure.
|
||||
///
|
||||
/// - Parameter values: The sequence of values.
|
||||
/// - Parameter getKey: The closure which provides a key for the given value from the values
|
||||
/// sequence.
|
||||
public init<Values: Sequence>(values: Values, keyedBy getKey: (Value) -> Key) where Values.Element == Value {
|
||||
self.init(values.map { (getKey($0), $0) })
|
||||
}
|
||||
|
||||
/// Creates an ordered dictionary from a sequence of values keyed by a key loaded from the value
|
||||
/// at the given key path.
|
||||
///
|
||||
/// - Parameter values: The sequence of values.
|
||||
/// - Parameter keyPath: The key path for the value to locate its key at.
|
||||
public init(values: [Value], keyedBy keyPath: KeyPath<Value, Key>) {
|
||||
self.init(values.map { ($0[keyPath: keyPath], $0) })
|
||||
}
|
||||
|
||||
/// Creates an ordered dictionary from a regular unsorted dictionary by sorting it using the
|
||||
/// the given sort function.
|
||||
///
|
||||
/// - Parameter unsorted: The unsorted dictionary.
|
||||
/// - Parameter areInIncreasingOrder: The sort function which compares the key-value pairs.
|
||||
public init(unsorted: Dictionary<Key, Value>, areInIncreasingOrder: (Element, Element) -> Bool) {
|
||||
let elements = unsorted
|
||||
.map { (key: $0.key, value: $0.value) }
|
||||
.sorted(by: areInIncreasingOrder)
|
||||
|
||||
self.init(elements)
|
||||
}
|
||||
|
||||
/// Creates an ordered dictionary from a sequence of key-value pairs.
|
||||
///
|
||||
/// - Parameter elements: The key-value pairs that will make up the new ordered dictionary.
|
||||
/// Each key in `elements` must be unique.
|
||||
public init<S: Sequence>(_ elements: S) where S.Element == Element {
|
||||
for (key, value) in elements {
|
||||
precondition(!containsKey(key), "Elements sequence contains duplicate keys")
|
||||
self[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Ordered Keys & Values
|
||||
// ======================================================= //
|
||||
|
||||
/// A collection containing just the keys of the ordered dictionary in the correct order.
|
||||
public var orderedKeys: OrderedDictionaryKeys<Key, Value> {
|
||||
return self.lazy.map { $0.key }
|
||||
}
|
||||
|
||||
/// A collection containing just the values of the ordered dictionary in the correct order.
|
||||
public var orderedValues: OrderedDictionaryValues<Key, Value> {
|
||||
return self.lazy.map { $0.value }
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Dictionary
|
||||
// ======================================================= //
|
||||
|
||||
/// Converts itself to a common unsorted dictionary.
|
||||
public var unorderedDictionary: Dictionary<Key, Value> {
|
||||
return _keysToValues
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Key-based Access
|
||||
// ======================================================= //
|
||||
|
||||
/// Accesses the value associated with the given key for reading and writing.
|
||||
///
|
||||
/// This key-based subscript returns the value for the given key if the key is found in the
|
||||
/// ordered dictionary, or `nil` if the key is not found.
|
||||
///
|
||||
/// When you assign a value for a key and that key already exists, the ordered dictionary
|
||||
/// overwrites the existing value and preservers the index of the key-value pair. If the ordered
|
||||
/// dictionary does not contain the key, a new key-value pair is appended to the end of the
|
||||
/// ordered dictionary.
|
||||
///
|
||||
/// If you assign `nil` as the value for the given key, the ordered dictionary removes that key
|
||||
/// and its associated value if it exists.
|
||||
///
|
||||
/// - Parameter key: The key to find in the ordered dictionary.
|
||||
/// - Returns: The value associated with `key` if `key` is in the ordered dictionary; otherwise,
|
||||
/// `nil`.
|
||||
public subscript(key: Key) -> Value? {
|
||||
get {
|
||||
return value(forKey: key)
|
||||
}
|
||||
set(newValue) {
|
||||
if let newValue = newValue {
|
||||
updateValue(newValue, forKey: key)
|
||||
} else {
|
||||
removeValue(forKey: key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a Boolean value indicating whether the ordered dictionary contains the given key.
|
||||
///
|
||||
/// - Parameter key: The key to be looked up.
|
||||
/// - Returns: `true` if the ordered dictionary contains the given key; otherwise, `false`.
|
||||
public func containsKey(_ key: Key) -> Bool {
|
||||
return _keysToValues[key] != nil
|
||||
}
|
||||
|
||||
/// Returns the value associated with the given key if the key is found in the ordered
|
||||
/// dictionary, or `nil` if the key is not found.
|
||||
///
|
||||
/// - Parameter key: The key to find in the ordered dictionary.
|
||||
/// - Returns: The value associated with `key` if `key` is in the ordered dictionary; otherwise,
|
||||
/// `nil`.
|
||||
public func value(forKey key: Key) -> Value? {
|
||||
return _keysToValues[key]
|
||||
}
|
||||
|
||||
/// Updates the value stored in the ordered dictionary for the given key, or appends a new
|
||||
/// key-value pair if the key does not exist.
|
||||
///
|
||||
/// - Parameter value: The new value to add to the ordered dictionary.
|
||||
/// - Parameter key: The key to associate with `value`. If `key` already exists in the ordered
|
||||
/// dictionary, `value` replaces the existing associated value. If `key` is not already a key
|
||||
/// of the ordered dictionary, the `(key, value)` pair is appended at the end of the ordered
|
||||
/// dictionary.
|
||||
@discardableResult
|
||||
public mutating func updateValue(_ value: Value, forKey key: Key) -> Value? {
|
||||
if containsKey(key) {
|
||||
let currentValue = _unsafeValue(forKey: key)
|
||||
|
||||
_keysToValues[key] = value
|
||||
|
||||
return currentValue
|
||||
} else {
|
||||
_orderedKeys.append(key)
|
||||
_keysToValues[key] = value
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes the given key and its associated value from the ordered dictionary.
|
||||
///
|
||||
/// If the key is found in the ordered dictionary, this method returns the key's associated
|
||||
/// value. On removal, the indices of the ordered dictionary are invalidated. If the key is
|
||||
/// not found in the ordered dictionary, this method returns `nil`.
|
||||
///
|
||||
/// - Parameter key: The key to remove along with its associated value.
|
||||
/// - Returns: The value that was removed, or `nil` if the key was not present in the
|
||||
/// ordered dictionary.
|
||||
///
|
||||
/// - SeeAlso: remove(at:)
|
||||
@discardableResult
|
||||
public mutating func removeValue(forKey key: Key) -> Value? {
|
||||
guard let index = index(forKey: key) else { return nil }
|
||||
|
||||
let currentValue = self[index].value
|
||||
|
||||
_orderedKeys.remove(at: index)
|
||||
_keysToValues[key] = nil
|
||||
|
||||
return currentValue
|
||||
}
|
||||
|
||||
/// Removes all key-value pairs from the ordered dictionary and invalidates all indices.
|
||||
///
|
||||
/// - Parameter keepCapacity: Whether the ordered dictionary should keep its underlying storage.
|
||||
/// If you pass `true`, the operation preserves the storage capacity that the collection has,
|
||||
/// otherwise the underlying storage is released. The default is `false`.
|
||||
public mutating func removeAll(keepingCapacity keepCapacity: Bool = false) {
|
||||
_orderedKeys.removeAll(keepingCapacity: keepCapacity)
|
||||
_keysToValues.removeAll(keepingCapacity: keepCapacity)
|
||||
}
|
||||
|
||||
private func _unsafeValue(forKey key: Key) -> Value {
|
||||
let value = _keysToValues[key]
|
||||
precondition(value != nil, "Inconsistency error occurred in OrderedDictionary")
|
||||
return value!
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Index-based Access
|
||||
// ======================================================= //
|
||||
|
||||
/// Accesses the key-value pair at the specified position.
|
||||
///
|
||||
/// The specified position has to be a valid index of the ordered dictionary. The index-base
|
||||
/// subscript returns the key-value pair corresponding to the index.
|
||||
///
|
||||
/// - Parameter position: The position of the key-value pair to access. `position` must be
|
||||
/// a valid index of the ordered dictionary and not equal to `endIndex`.
|
||||
/// - Returns: A tuple containing the key-value pair corresponding to `position`.
|
||||
///
|
||||
/// - SeeAlso: update(:at:)
|
||||
public subscript(position: Index) -> Element {
|
||||
precondition(indices.contains(position), "OrderedDictionary index is out of range")
|
||||
|
||||
let key = _orderedKeys[position]
|
||||
let value = _unsafeValue(forKey: key)
|
||||
|
||||
return (key, value)
|
||||
}
|
||||
|
||||
/// Returns the index for the given key.
|
||||
///
|
||||
/// - Parameter key: The key to find in the ordered dictionary.
|
||||
/// - Returns: The index for `key` and its associated value if `key` is in the ordered
|
||||
/// dictionary; otherwise, `nil`.
|
||||
public func index(forKey key: Key) -> Index? {
|
||||
return _orderedKeys.index(of: key)
|
||||
}
|
||||
|
||||
/// Returns the key-value pair at the specified index, or `nil` if there is no key-value pair
|
||||
/// at that index.
|
||||
///
|
||||
/// - Parameter index: The index of the key-value pair to be looked up. `index` does not have to
|
||||
/// be a valid index.
|
||||
/// - Returns: A tuple containing the key-value pair corresponding to `index` if the index is
|
||||
/// valid; otherwise, `nil`.
|
||||
public func elementAt(_ index: Index) -> Element? {
|
||||
return indices.contains(index) ? self[index] : nil
|
||||
}
|
||||
|
||||
/// Checks whether the given key-value pair can be inserted into to ordered dictionary by
|
||||
/// validating the presence of the key.
|
||||
///
|
||||
/// - Parameter newElement: The key-value pair to be inserted into the ordered dictionary.
|
||||
/// - Returns: `true` if the key-value pair can be safely inserted; otherwise, `false`.
|
||||
///
|
||||
/// - SeeAlso: canInsert(key:)
|
||||
/// - SeeAlso: canInsert(at:)
|
||||
@available(*, deprecated, message: "Use canInsert(key:) with the element's key instead")
|
||||
public func canInsert(_ newElement: Element) -> Bool {
|
||||
return canInsert(key: newElement.key)
|
||||
}
|
||||
|
||||
/// Checks whether a key-value pair with the given key can be inserted into the ordered
|
||||
/// dictionary by validating its presence.
|
||||
///
|
||||
/// - Parameter key: The key to be inserted into the ordered dictionary.
|
||||
/// - Returns: `true` if the key can safely be inserted; ortherwise, `false`.
|
||||
///
|
||||
/// - SeeAlso: canInsert(at:)
|
||||
public func canInsert(key: Key) -> Bool {
|
||||
return !containsKey(key)
|
||||
}
|
||||
|
||||
/// Checks whether a new key-value pair can be inserted into the ordered dictionary at the
|
||||
/// given index.
|
||||
///
|
||||
/// - Parameter index: The index the new key-value pair should be inserted at.
|
||||
/// - Returns: `true` if a new key-value pair can be inserted at the specified index; otherwise,
|
||||
/// `false`.
|
||||
///
|
||||
/// - SeeAlso: canInsert(key:)
|
||||
public func canInsert(at index: Index) -> Bool {
|
||||
return index >= startIndex && index <= endIndex
|
||||
}
|
||||
|
||||
/// Inserts a new key-value pair at the specified position.
|
||||
///
|
||||
/// If the key of the inserted pair already exists in the ordered dictionary, a runtime error
|
||||
/// is triggered. Use `canInsert(_:)` for performing a check first, so that this method can
|
||||
/// be executed safely.
|
||||
///
|
||||
/// - Parameter newElement: The new key-value pair to insert into the ordered dictionary. The
|
||||
/// key contained in the pair must not be already present in the ordered dictionary.
|
||||
/// - Parameter index: The position at which to insert the new key-value pair. `index` must be
|
||||
/// a valid index of the ordered dictionary or equal to `endIndex` property.
|
||||
///
|
||||
/// - SeeAlso: canInsert(key:)
|
||||
/// - SeeAlso: canInsert(at:)
|
||||
/// - SeeAlso: update(:at:)
|
||||
public mutating func insert(_ newElement: Element, at index: Index) {
|
||||
precondition(canInsert(key: newElement.key), "Cannot insert duplicate key in OrderedDictionary")
|
||||
precondition(canInsert(at: index), "Cannot insert at invalid index in OrderedDictionary")
|
||||
|
||||
let (key, value) = newElement
|
||||
|
||||
_orderedKeys.insert(key, at: index)
|
||||
_keysToValues[key] = value
|
||||
}
|
||||
|
||||
/// Checks whether the key-value pair at the given index can be updated with the given key-value
|
||||
/// pair. This is not the case if the key of the updated element is already present in the
|
||||
/// ordered dictionary and located at another index than the updated one.
|
||||
///
|
||||
/// Although this is a checking method, a valid index has to be provided.
|
||||
///
|
||||
/// - Parameter newElement: The key-value pair to be set at the specified position.
|
||||
/// - Parameter index: The position at which to set the key-value pair. `index` must be a valid
|
||||
/// index of the ordered dictionary.
|
||||
public func canUpdate(_ newElement: Element, at index: Index) -> Bool {
|
||||
var keyPresentAtIndex = false
|
||||
return _canUpdate(newElement, at: index, keyPresentAtIndex: &keyPresentAtIndex)
|
||||
}
|
||||
|
||||
/// Updates the key-value pair located at the specified position.
|
||||
///
|
||||
/// If the key of the updated pair already exists in the ordered dictionary *and* is located at
|
||||
/// a different position than the specified one, a runtime error is triggered. Use
|
||||
/// `canUpdate(_:at:)` for performing a check first, so that this method can be executed safely.
|
||||
///
|
||||
/// - Parameter newElement: The key-value pair to be set at the specified position.
|
||||
/// - Parameter index: The position at which to set the key-value pair. `index` must be a valid
|
||||
/// index of the ordered dictionary.
|
||||
///
|
||||
/// - SeeAlso: canUpdate(_:at:)
|
||||
/// - SeeAlso: insert(:at:)
|
||||
@discardableResult
|
||||
public mutating func update(_ newElement: Element, at index: Index) -> Element? {
|
||||
// Store the flag indicating whether the key of the inserted element
|
||||
// is present at the updated index
|
||||
var keyPresentAtIndex = false
|
||||
|
||||
precondition(
|
||||
_canUpdate(newElement, at: index, keyPresentAtIndex: &keyPresentAtIndex),
|
||||
"OrderedDictionary update duplicates key"
|
||||
)
|
||||
|
||||
// Decompose the element
|
||||
let (key, value) = newElement
|
||||
|
||||
// Load the current element at the index
|
||||
let replacedElement = self[index]
|
||||
|
||||
// If its key differs, remove its associated value
|
||||
if (!keyPresentAtIndex) {
|
||||
_keysToValues.removeValue(forKey: replacedElement.key)
|
||||
}
|
||||
|
||||
// Store the new position of the key and the new value associated with the key
|
||||
_orderedKeys[index] = key
|
||||
_keysToValues[key] = value
|
||||
|
||||
return replacedElement
|
||||
}
|
||||
|
||||
/// Removes and returns the key-value pair at the specified position if there is any key-value
|
||||
/// pair, or `nil` if there is none.
|
||||
///
|
||||
/// - Parameter index: The position of the key-value pair to remove.
|
||||
/// - Returns: The element at the specified index, or `nil` if the position is not taken.
|
||||
///
|
||||
/// - SeeAlso: removeValue(forKey:)
|
||||
@discardableResult
|
||||
public mutating func remove(at index: Index) -> Element? {
|
||||
guard let element = elementAt(index) else { return nil }
|
||||
|
||||
_orderedKeys.remove(at: index)
|
||||
_keysToValues.removeValue(forKey: element.key)
|
||||
|
||||
return element
|
||||
}
|
||||
|
||||
private func _canUpdate(_ newElement: Element, at index: Index, keyPresentAtIndex: inout Bool) -> Bool {
|
||||
precondition(indices.contains(index), "OrderedDictionary index is out of range")
|
||||
|
||||
let currentIndexOfKey = self.index(forKey: newElement.key)
|
||||
|
||||
let keyNotPresent = currentIndexOfKey == nil
|
||||
keyPresentAtIndex = currentIndexOfKey == index
|
||||
|
||||
return keyNotPresent || keyPresentAtIndex
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Moving Elements
|
||||
// ======================================================= //
|
||||
|
||||
/// Moves an existing key-value pair specified by the given key to the new index by removing it
|
||||
/// from its original index first and inserting it at the new index. If the movement is
|
||||
/// actually performed, the previous index of the key-value pair is returned. Otherwise, `nil`
|
||||
/// is returned.
|
||||
///
|
||||
/// - Parameter key: The key specifying the key-value pair to move.
|
||||
/// - Parameter newIndex: The new index the key-value pair should be moved to.
|
||||
/// - Returns: The previous index of the key-value pair if it was sucessfully moved.
|
||||
@discardableResult
|
||||
public mutating func moveElement(forKey key: Key, to newIndex: Index) -> Index? {
|
||||
// Load the previous index and return nil if the index is not found.
|
||||
guard let previousIndex = index(forKey: key) else { return nil }
|
||||
|
||||
// If the previous and new indices match, threat it as if the movement was already
|
||||
// performed.
|
||||
guard previousIndex != newIndex else { return previousIndex }
|
||||
|
||||
// Remove the value for the key at its original index.
|
||||
let value = removeValue(forKey: key)!
|
||||
|
||||
// Validate the new index.
|
||||
precondition(canInsert(at: newIndex), "Cannot move to invalid index in OrderedDictionary")
|
||||
|
||||
// Insert the element at the new index.
|
||||
insert((key: key, value: value), at: newIndex)
|
||||
|
||||
return previousIndex
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Sorting Elements
|
||||
// ======================================================= //
|
||||
|
||||
/// Sorts the ordered dictionary in place, using the given predicate as the comparison between
|
||||
/// elements.
|
||||
///
|
||||
/// The predicate must be a *strict weak ordering* over the elements.
|
||||
///
|
||||
/// - Parameter areInIncreasingOrder: A predicate that returns `true` if its first argument
|
||||
/// should be ordered before its second argument; otherwise, `false`.
|
||||
///
|
||||
/// - SeeAlso: MutableCollection.sort(by:), sorted(by:)
|
||||
public mutating func sort(by areInIncreasingOrder: (Element, Element) -> Bool) {
|
||||
_orderedKeys = _sortedElements(by: areInIncreasingOrder).map { $0.key }
|
||||
}
|
||||
|
||||
/// Returns a new ordered dictionary, sorted using the given predicate as the comparison between
|
||||
/// elements.
|
||||
///
|
||||
/// The predicate must be a *strict weak ordering* over the elements.
|
||||
///
|
||||
/// - Parameter areInIncreasingOrder: A predicate that returns `true` if its first argument
|
||||
/// should be ordered before its second argument; otherwise, `false`.
|
||||
/// - Returns: A new ordered dictionary sorted according to the predicate.
|
||||
///
|
||||
/// - SeeAlso: MutableCollection.sorted(by:), sort(by:)
|
||||
/// - MutatingVariant: sort
|
||||
public func sorted(by areInIncreasingOrder: (Element, Element) -> Bool) -> OrderedDictionary<Key, Value> {
|
||||
return OrderedDictionary(_sortedElements(by: areInIncreasingOrder))
|
||||
}
|
||||
|
||||
private func _sortedElements(by areInIncreasingOrder: (Element, Element) -> Bool) -> [Element] {
|
||||
return sorted(by: areInIncreasingOrder)
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Slices
|
||||
// ======================================================= //
|
||||
|
||||
/// Accesses a contiguous subrange of the ordered dictionary.
|
||||
///
|
||||
/// - Parameter bounds: A range of the ordered dictionary's indices. The bounds of the range
|
||||
/// must be valid indices of the ordered dictionary.
|
||||
/// - Returns: The slice view at the ordered dictionary in the specified subrange.
|
||||
public subscript(bounds: Range<Index>) -> SubSequence {
|
||||
return OrderedDictionarySlice(base: self, bounds: bounds)
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Indices
|
||||
// ======================================================= //
|
||||
|
||||
/// The indices that are valid for subscripting the ordered dictionary.
|
||||
public var indices: Indices {
|
||||
return _orderedKeys.indices
|
||||
}
|
||||
|
||||
/// The position of the first key-value pair in a non-empty ordered dictionary.
|
||||
public var startIndex: Index {
|
||||
return _orderedKeys.startIndex
|
||||
}
|
||||
|
||||
/// The position which is one greater than the position of the last valid key-value pair in the
|
||||
/// ordered dictionary.
|
||||
public var endIndex: Index {
|
||||
return _orderedKeys.endIndex
|
||||
}
|
||||
|
||||
/// Returns the position immediately after the given index.
|
||||
public func index(after i: Index) -> Index {
|
||||
return _orderedKeys.index(after: i)
|
||||
}
|
||||
|
||||
/// Returns the position immediately before the given index.
|
||||
public func index(before i: Index) -> Index {
|
||||
return _orderedKeys.index(before: i)
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Internal Storage
|
||||
// ======================================================= //
|
||||
|
||||
/// The backing storage for the ordered keys.
|
||||
fileprivate var _orderedKeys = [Key]()
|
||||
|
||||
/// The backing storage for the mapping of keys to values.
|
||||
fileprivate var _keysToValues = [Key: Value]()
|
||||
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Subtypes
|
||||
// ======================================================= //
|
||||
|
||||
#if swift(>=4.1)
|
||||
|
||||
/// A view into an ordered dictionary whose indices are a subrange of the indices of the ordered
|
||||
/// dictionary.
|
||||
public typealias OrderedDictionarySlice<Key: Hashable, Value> = Slice<OrderedDictionary<Key, Value>>
|
||||
|
||||
/// A collection containing the keys of the ordered dictionary.
|
||||
///
|
||||
/// Under the hood this is a lazily evaluated bidirectional collection deriving the keys from
|
||||
/// the base ordered dictionary on-the-fly.
|
||||
public typealias OrderedDictionaryKeys<Key: Hashable, Value> = LazyMapCollection<OrderedDictionary<Key, Value>, Key>
|
||||
|
||||
/// A collection containing the values of the ordered dictionary.
|
||||
///
|
||||
/// Under the hood this is a lazily evaluated bidirectional collection deriving the values from
|
||||
/// the base ordered dictionary on-the-fly.
|
||||
public typealias OrderedDictionaryValues<Key: Hashable, Value> = LazyMapCollection<OrderedDictionary<Key, Value>, Value>
|
||||
|
||||
#else
|
||||
|
||||
/// A view into an ordered dictionary whose indices are a subrange of the indices of the ordered
|
||||
/// dictionary.
|
||||
public typealias OrderedDictionarySlice<Key: Hashable, Value> = Slice<OrderedDictionary<Key, Value>>
|
||||
|
||||
/// A collection containing the keys of the ordered dictionary.
|
||||
///
|
||||
/// Under the hood this is a lazily evaluated bidirectional collection deriving the keys from
|
||||
/// the base ordered dictionary on-the-fly.
|
||||
public typealias OrderedDictionaryKeys<Key: Hashable, Value> = LazyMapCollection<OrderedDictionary<Key, Value>, Key>
|
||||
|
||||
/// A collection containing the values of the ordered dictionary.
|
||||
///
|
||||
/// Under the hood this is a lazily evaluated bidirectional collection deriving the values from
|
||||
/// the base ordered dictionary on-the-fly.
|
||||
public typealias OrderedDictionaryValues<Key: Hashable, Value> = LazyMapCollection<OrderedDictionary<Key, Value>, Value>
|
||||
|
||||
#endif
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Literals
|
||||
// ======================================================= //
|
||||
|
||||
extension OrderedDictionary: ExpressibleByArrayLiteral {
|
||||
|
||||
/// Creates an ordered dictionary initialized from an array literal containing a list of
|
||||
/// key-value pairs.
|
||||
public init(arrayLiteral elements: Element...) {
|
||||
self.init(elements)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension OrderedDictionary: ExpressibleByDictionaryLiteral {
|
||||
|
||||
/// Creates an ordered dictionary initialized from a dictionary literal.
|
||||
public init(dictionaryLiteral elements: (Key, Value)...) {
|
||||
self.init(elements.map { element in
|
||||
let (key, value) = element
|
||||
return (key: key, value: value)
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ======================================================= //
|
||||
// MARK: - Equatable Conformance
|
||||
// ======================================================= //
|
||||
|
||||
#if swift(>=4.1)
|
||||
|
||||
extension OrderedDictionary: Equatable where Value: Equatable {}
|
||||
|
||||
#endif
|
||||
|
||||
extension OrderedDictionary where Value: Equatable {
|
||||
|
||||
/// Returns a Boolean value that indicates whether the two given ordered dictionaries with
|
||||
/// equatable values are equal.
|
||||
public static func == (lhs: OrderedDictionary, rhs: OrderedDictionary) -> Bool {
|
||||
return lhs._orderedKeys == rhs._orderedKeys
|
||||
&& lhs._keysToValues == rhs._keysToValues
|
||||
}
|
||||
|
||||
}
|
||||
@ -0,0 +1,31 @@
|
||||
//
|
||||
// ParseResponse.swift
|
||||
// ASDKgram-Swift
|
||||
//
|
||||
// Copyright (c) 2014-present, Facebook, Inc. All rights reserved.
|
||||
// This source code is licensed under the BSD-style license found in the
|
||||
// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional
|
||||
// grant of patent rights can be found in the PATENTS file in the same directory.
|
||||
//
|
||||
// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present,
|
||||
// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
func parsePopularPage(withURL: URL, page: Int) -> Resource<PopularPageModel> {
|
||||
let parse = Resource<PopularPageModel>(url: withURL, page: page) { metaData, jsonData in
|
||||
do {
|
||||
let photos = try JSONDecoder().decode([PhotoModel].self, from: jsonData)
|
||||
return .success(PopularPageModel(metaData: metaData, photos: photos))
|
||||
} catch {
|
||||
return .failure(.errorParsingJSON)
|
||||
}
|
||||
}
|
||||
|
||||
return parse
|
||||
}
|
||||
@ -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.
|
||||
//
|
||||
// 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
|
||||
|
||||
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<String, PhotoModel> = [:]
|
||||
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) -> ()) {
|
||||
|
||||
// For this example let's use the main thread as locking queue
|
||||
DispatchQueue.main.async {
|
||||
guard !self.fetchPageInProgress else {
|
||||
return
|
||||
}
|
||||
|
||||
guard !fetchPageInProgress else { return }
|
||||
self.fetchPageInProgress = true
|
||||
self.fetchNextPageOfPopularPhotos(replaceData: false) { [unowned self] additions, error in
|
||||
self.fetchPageInProgress = false
|
||||
|
||||
fetchPageInProgress = true
|
||||
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)
|
||||
}
|
||||
}
|
||||
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
|
||||
case .success(let itemsPage):
|
||||
// Update current state
|
||||
self.totalItems = itemsPage.totalNumberOfItems
|
||||
self.totalPages = itemsPage.totalPages
|
||||
self.currentPage = itemsPage.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)
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// 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 AsyncDisplayKit
|
||||
|
||||
class PhotoFeedTableNodeController: ASViewController<ASTableNode> {
|
||||
|
||||
// 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
|
||||
node.view.separatorStyle = .none
|
||||
|
||||
navigationController?.hidesBarsOnSwipe = true
|
||||
|
||||
node.view.addSubview(activityIndicatorView)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
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..<photoFeed.photos.count)
|
||||
let indexRange = (photoFeedModel.numberOfItems - newPhotos..<photoFeedModel.numberOfItems)
|
||||
let indexPaths = indexRange.map { IndexPath(row: $0, section: 0) }
|
||||
node.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: ASTableDataSource / ASTableDelegate
|
||||
|
||||
extension PhotoFeedTableNodeController: ASTableDataSource, ASTableDelegate {
|
||||
|
||||
func tableNode(_ tableNode: ASTableNode, numberOfRowsInSection section: Int) -> 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)
|
||||
}
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// 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
|
||||
@ -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..<photoFeed.photos.count)
|
||||
let indexRange = (photoFeed.numberOfItems - newPhotos..<photoFeed.numberOfItems)
|
||||
let indexPaths = indexRange.map { IndexPath(row: $0, section: 0) }
|
||||
tableView.insertRows(at: indexPaths, with: .none)
|
||||
}
|
||||
@ -95,24 +88,26 @@ extension PhotoFeedTableViewController {
|
||||
// TableView Data Source
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return photoFeed.photos.count
|
||||
return photoFeed.numberOfItems
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
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()
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// 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
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// 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
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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.
|
||||
//
|
||||
// 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 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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
//
|
||||
// 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
|
||||
//
|
||||
// swiftlint:disable force_cast
|
||||
|
||||
import UIKit
|
||||
|
||||
final class WebService {
|
||||
/// Load a new resource. Callback is called on main
|
||||
func load<A>(resource: Resource<A>, completion: @escaping (Result<A>) -> ()) {
|
||||
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<Data> {
|
||||
// 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<A> {
|
||||
let url: URL
|
||||
let parse: (Data) -> Result<A>
|
||||
let parse: (Data, URLResponse?) -> Result<A>
|
||||
}
|
||||
|
||||
extension Resource {
|
||||
|
||||
init(url: URL, parseJSON: @escaping (Any) -> Result<A>) {
|
||||
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<A>) {
|
||||
// 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<T> {
|
||||
case success(T)
|
||||
case failure(NetworkingErrors)
|
||||
case failure(NetworkingError)
|
||||
}
|
||||
|
||||
enum NetworkingErrors: Error {
|
||||
enum NetworkingError: Error {
|
||||
case errorParsingResponse
|
||||
case errorParsingJSON
|
||||
case noInternetConnection
|
||||
case dataReturnedNil
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user