Rewrite Swift Example (#1002)

* Rewrite Swift Example

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

View File

@ -278,8 +278,6 @@
05E2127D19D4DB510098F589 /* Sources */,
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 */

View File

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

View File

@ -8,7 +8,6 @@
/* Begin PBXBuildFile section */
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 */,

View File

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

View File

@ -2,19 +2,17 @@
// AppDelegate.swift
// 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

View File

@ -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

View File

@ -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"
}
}
}

View File

@ -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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -2,116 +2,114 @@
// PhotoFeedModel.swift
// 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)
}
}
}
}

View File

@ -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)
}

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}
}

View File

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

View File

@ -2,66 +2,36 @@
// URL.swift
// 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
}
}

View File

@ -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

View File

@ -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