├── .gitignore.swp ├── Pods ├── Target Support Files │ ├── Contentful │ │ ├── Contentful.modulemap │ │ ├── Contentful-dummy.m │ │ ├── Contentful-prefix.pch │ │ ├── Contentful-umbrella.h │ │ ├── Contentful.xcconfig │ │ └── Contentful-Info.plist │ └── Pods-Boilerplate │ │ ├── Pods-Boilerplate.modulemap │ │ ├── Pods-Boilerplate-dummy.m │ │ ├── Pods-Boilerplate-umbrella.h │ │ ├── Pods-Boilerplate.debug.xcconfig │ │ ├── Pods-Boilerplate.release.xcconfig │ │ ├── Pods-Boilerplate-Info.plist │ │ ├── Pods-Boilerplate-acknowledgements.markdown │ │ ├── Pods-Boilerplate-acknowledgements.plist │ │ └── Pods-Boilerplate-frameworks.sh ├── Manifest.lock ├── Contentful │ ├── Sources │ │ └── Contentful │ │ │ ├── Util.swift │ │ │ ├── Endpoints.swift │ │ │ ├── UIKit │ │ │ └── Client+UIKit.swift │ │ │ ├── Space.swift │ │ │ ├── ContentType.swift │ │ │ ├── DataCache.swift │ │ │ ├── Result.swift │ │ │ ├── Sys.swift │ │ │ ├── Date.swift │ │ │ ├── Link.swift │ │ │ ├── Field.swift │ │ │ ├── Persistence.swift │ │ │ ├── Entry.swift │ │ │ ├── ClientConfiguration.swift │ │ │ ├── Locale.swift │ │ │ ├── Asset.swift │ │ │ ├── QueryOperation.swift │ │ │ ├── SyncSpace.swift │ │ │ ├── StructuredText.swift │ │ │ ├── Error.swift │ │ │ ├── Resource.swift │ │ │ ├── Decodable.swift │ │ │ ├── ArrayResponse.swift │ │ │ └── ImageOptions.swift │ ├── LICENSE │ └── README.md └── Local Podspecs │ └── Contentful.podspec.json ├── Boilerplate-Swift.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata └── project.pbxproj ├── Boilerplate-Swift.xcworkspace └── contents.xcworkspacedata ├── .gitignore ├── Podfile.lock ├── Boilerplate ├── AppDelegate.swift ├── Assets.xcassets │ └── AppIcon.appiconset │ │ └── Contents.json ├── Info.plist ├── Base.lproj │ └── LaunchScreen.storyboard └── ViewController.swift ├── README.md ├── Podfile └── LICENSE /.gitignore.swp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/contentful/boilerplate-swift/master/.gitignore.swp -------------------------------------------------------------------------------- /Pods/Target Support Files/Contentful/Contentful.modulemap: -------------------------------------------------------------------------------- 1 | framework module Contentful { 2 | umbrella header "Contentful-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Contentful/Contentful-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Contentful : NSObject 3 | @end 4 | @implementation PodsDummy_Contentful 5 | @end 6 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Boilerplate/Pods-Boilerplate.modulemap: -------------------------------------------------------------------------------- 1 | framework module Pods_Boilerplate { 2 | umbrella header "Pods-Boilerplate-umbrella.h" 3 | 4 | export * 5 | module * { export * } 6 | } 7 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Boilerplate/Pods-Boilerplate-dummy.m: -------------------------------------------------------------------------------- 1 | #import 2 | @interface PodsDummy_Pods_Boilerplate : NSObject 3 | @end 4 | @implementation PodsDummy_Pods_Boilerplate 5 | @end 6 | -------------------------------------------------------------------------------- /Boilerplate-Swift.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Contentful/Contentful-prefix.pch: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | -------------------------------------------------------------------------------- /Boilerplate-Swift.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Contentful/Contentful-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double ContentfulVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char ContentfulVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Boilerplate/Pods-Boilerplate-umbrella.h: -------------------------------------------------------------------------------- 1 | #ifdef __OBJC__ 2 | #import 3 | #else 4 | #ifndef FOUNDATION_EXPORT 5 | #if defined(__cplusplus) 6 | #define FOUNDATION_EXPORT extern "C" 7 | #else 8 | #define FOUNDATION_EXPORT extern 9 | #endif 10 | #endif 11 | #endif 12 | 13 | 14 | FOUNDATION_EXPORT double Pods_BoilerplateVersionNumber; 15 | FOUNDATION_EXPORT const unsigned char Pods_BoilerplateVersionString[]; 16 | 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | 6 | # macOS 7 | .DS_Store 8 | .gutter.json 9 | *.gcda 10 | *.gcno 11 | *.xccheckout 12 | xcuserdata/ 13 | .idea 14 | 15 | 16 | ## Build generated 17 | #build/ 18 | DerivedData/ 19 | 20 | ## Other 21 | *.moved-aside 22 | *.xcuserstate 23 | 24 | ## Obj-C/Swift specific 25 | *.hmap 26 | *.ipa 27 | *.dSYM.zip 28 | *.dSYM 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Contentful/Contentful.xcconfig: -------------------------------------------------------------------------------- 1 | CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Contentful 2 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 3 | OTHER_LDFLAGS = $(inherited) -framework "CoreLocation" 4 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 5 | PODS_BUILD_DIR = ${BUILD_DIR} 6 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 7 | PODS_ROOT = ${SRCROOT} 8 | PODS_TARGET_SRCROOT = ${PODS_ROOT}/Contentful 9 | PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} 10 | SKIP_INSTALL = YES 11 | -------------------------------------------------------------------------------- /Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Contentful (4.0.0): 3 | - Contentful/ImageOptions (= 4.0.0) 4 | - Contentful/ImageOptions (4.0.0) 5 | 6 | DEPENDENCIES: 7 | - "Contentful (from `git@github.com:contentful/contentful.swift`, branch `master`)" 8 | 9 | EXTERNAL SOURCES: 10 | Contentful: 11 | :branch: master 12 | :git: "git@github.com:contentful/contentful.swift" 13 | 14 | CHECKOUT OPTIONS: 15 | Contentful: 16 | :commit: 697de08b5582b124c0524b71e10307178488d6d8 17 | :git: "git@github.com:contentful/contentful.swift" 18 | 19 | SPEC CHECKSUMS: 20 | Contentful: 5384e2bd357dabfa442bc6465aadc525dfb688ab 21 | 22 | PODFILE CHECKSUM: 133c32e5f8ce1742e4ee8caeed25f6ec2f646e91 23 | 24 | COCOAPODS: 1.6.0.beta.2 25 | -------------------------------------------------------------------------------- /Pods/Manifest.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Contentful (4.0.0): 3 | - Contentful/ImageOptions (= 4.0.0) 4 | - Contentful/ImageOptions (4.0.0) 5 | 6 | DEPENDENCIES: 7 | - "Contentful (from `git@github.com:contentful/contentful.swift`, branch `master`)" 8 | 9 | EXTERNAL SOURCES: 10 | Contentful: 11 | :branch: master 12 | :git: "git@github.com:contentful/contentful.swift" 13 | 14 | CHECKOUT OPTIONS: 15 | Contentful: 16 | :commit: 697de08b5582b124c0524b71e10307178488d6d8 17 | :git: "git@github.com:contentful/contentful.swift" 18 | 19 | SPEC CHECKSUMS: 20 | Contentful: 5384e2bd357dabfa442bc6465aadc525dfb688ab 21 | 22 | PODFILE CHECKSUM: 133c32e5f8ce1742e4ee8caeed25f6ec2f646e91 23 | 24 | COCOAPODS: 1.6.0.beta.2 25 | -------------------------------------------------------------------------------- /Boilerplate/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Boilerplate 4 | // 5 | // Created by JP Wright on 31/01/2017. 6 | // Copyright © 2017 Contentful. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | 11 | 12 | @UIApplicationMain 13 | class AppDelegate: UIResponder, UIApplicationDelegate { 14 | 15 | var window: UIWindow? 16 | 17 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 18 | 19 | let viewController = ViewController() 20 | 21 | window = UIWindow(frame: UIScreen.main.bounds) 22 | window?.rootViewController = viewController 23 | window?.makeKeyAndVisible() 24 | 25 | return true 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # boilerplate-swift 2 | 3 | This is a sample app demonstrating basic usage of the [contentful.swift](https://github.com/contentful/contentful.swift) Content Delivery API SDK. 4 | 5 | Upon launch, the app: 6 | 7 | - Initializes a `Client` object. 8 | - Fetches a `Space`. 9 | - Fetches up to 100 `Entry` objects from that `Space`. 10 | 11 | ## Using the app 12 | 13 | 1. Clone the repo, and open _Boilerplate-Swift.xcworkspace_. 14 | 2. Copy/paste your `space_id` and `access_token` into the global constants defined in the _ViewController_ file ([source here](https://github.com/contentful/boilerplate-swift/blob/master/Boilerplate/ViewController.swift)). 15 | 3. Hit "Run" in Xcode, follow along with the comments, and view the relevant output in the Xcode debug console. 16 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/ruby 2 | 3 | use_frameworks! 4 | 5 | # iOS 6 | target 'Boilerplate' do 7 | platform :ios, '10.0' 8 | 9 | pod 'Contentful', :git => 'git@github.com:contentful/contentful.swift', :branch => 'master' 10 | end 11 | 12 | # Use the commented examples below as a boilerplate 13 | # if you are using Contentful on other Cocoa platforms 14 | 15 | ## macOS 16 | #target 'Boilerplate_macOS' do 17 | # platform :osx, '10.12' 18 | # 19 | # pod 'Contentful', '~> 0.3.0' 20 | #end 21 | # 22 | ## tvOS 23 | #target 'Boilerplate_tvOS' do 24 | # platform :tvos, '9.0' 25 | # 26 | # pod 'Contentful', '~> 0.3.0' 27 | #end 28 | # 29 | ## watchOS 30 | #target 'Boilerplate_watchOS' do 31 | # platform :watchos, '2.0' 32 | # 33 | # pod 'Contentful', '~> 0.3.0' 34 | #end 35 | 36 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Boilerplate/Pods-Boilerplate.debug.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Contentful" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Contentful/Contentful.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "Contentful" -framework "CoreLocation" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Boilerplate/Pods-Boilerplate.release.xcconfig: -------------------------------------------------------------------------------- 1 | ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES 2 | FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Contentful" 3 | GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 4 | HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/Contentful/Contentful.framework/Headers" 5 | LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' 6 | OTHER_LDFLAGS = $(inherited) -framework "Contentful" -framework "CoreLocation" 7 | OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS 8 | PODS_BUILD_DIR = ${BUILD_DIR} 9 | PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) 10 | PODS_PODFILE_DIR_PATH = ${SRCROOT}/. 11 | PODS_ROOT = ${SRCROOT}/Pods 12 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Util.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Util.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 07.03.18. 6 | // Copyright © 2018 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | public func += (left: [K: V], right: [K: V]) -> [K: V] { 13 | var result = left 14 | right.forEach { (key, value) in result[key] = value } 15 | return result 16 | } 17 | 18 | public func + (left: [K: V], right: [K: V]) -> [K: V] { 19 | return left += right 20 | } 21 | 22 | 23 | // Convenience protocol and accompanying extension for extracting the type of data wrapped in an Optional. 24 | internal protocol OptionalProtocol { 25 | static func wrappedType() -> Any.Type 26 | } 27 | 28 | extension Optional: OptionalProtocol { 29 | static func wrappedType() -> Any.Type { 30 | return Wrapped.self 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Endpoints.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Endpoints.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 28.02.18. 6 | // Copyright © 2018 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Endpoints that are available for the Content Delivery and Preview APIs. 12 | public enum Endpoint: String { 13 | /// The spaces endpoint; also the base-path for all other endpoints. 14 | case spaces = "" 15 | /// The content types endpoint. 16 | case contentTypes = "content_types" 17 | /// The entries endpoint. 18 | case entries 19 | /// The assets endpoint. 20 | case assets 21 | /// The locales endpoint. 22 | case locales 23 | /// The synchronization endpoint. 24 | case sync 25 | 26 | /// The path component string for this endpoint. 27 | public var pathComponent: String { 28 | return rawValue 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Contentful/Contentful-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 4.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Boilerplate/Pods-Boilerplate-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | ${EXECUTABLE_NAME} 9 | CFBundleIdentifier 10 | ${PRODUCT_BUNDLE_IDENTIFIER} 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | ${PRODUCT_NAME} 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | ${CURRENT_PROJECT_VERSION} 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Contentful 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/UIKit/Client+UIKit.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Client+UIKit.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 15.05.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | #if os(iOS) || os(tvOS) || os(watchOS) 10 | import Foundation 11 | import UIKit 12 | 13 | extension Client { 14 | 15 | /** 16 | Fetch the underlying media file as `UIImage`. 17 | 18 | - returns: The signal for the `UIImage` result 19 | */ 20 | @discardableResult public func fetchImage(for asset: Asset, 21 | with imageOptions: [ImageOption] = [], 22 | then completion: @escaping ResultsHandler) -> URLSessionDataTask? { 23 | return fetchData(for: asset, with: imageOptions) { result in 24 | if let imageData = result.value, let image = UIImage(data: imageData) { 25 | completion(Result.success(image)) 26 | return 27 | } 28 | completion(Result.error(SDKError.unableToDecodeImageData)) 29 | } 30 | } 31 | } 32 | 33 | #endif 34 | -------------------------------------------------------------------------------- /Pods/Contentful/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Contentful GmbH - https://www.contentful.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Pods/Local Podspecs/Contentful.podspec.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Contentful", 3 | "version": "4.0.0", 4 | "summary": "Swift SDK for Contentful's Content Delivery API.", 5 | "homepage": "https://github.com/contentful/contentful.swift/", 6 | "social_media_url": "https://twitter.com/contentful", 7 | "license": { 8 | "type": "MIT", 9 | "file": "LICENSE" 10 | }, 11 | "authors": { 12 | "JP Wright": "jp@contentful.com", 13 | "Boris Bügling": "boris@buegling.com" 14 | }, 15 | "source": { 16 | "git": "https://github.com/contentful/contentful.swift.git", 17 | "tag": "4.0.0" 18 | }, 19 | "requires_arc": true, 20 | "source_files": "Sources/Contentful/*.swift", 21 | "frameworks": "CoreLocation", 22 | "ios": { 23 | "source_files": "Sources/Contentful/UIKit/*.swift" 24 | }, 25 | "watchos": { 26 | "source_files": "Sources/Contentful/UIKit/*.swift" 27 | }, 28 | "tvos": { 29 | "source_files": "Sources/Contentful/UIKit/*.swift" 30 | }, 31 | "osx": { 32 | "source_files": "Sources/Contentful/AppKit/*.swift" 33 | }, 34 | "platforms": { 35 | "ios": "8.0", 36 | "osx": "10.10", 37 | "watchos": "2.0", 38 | "tvos": "9.0" 39 | }, 40 | "subspecs": [ 41 | { 42 | "name": "ImageOptions", 43 | "source_files": "Sources/Contentful/ImageOptions.swift" 44 | } 45 | ] 46 | } 47 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Boilerplate/Pods-Boilerplate-acknowledgements.markdown: -------------------------------------------------------------------------------- 1 | # Acknowledgements 2 | This application makes use of the following third party libraries: 3 | 4 | ## Contentful 5 | 6 | Copyright (c) 2014 Contentful GmbH - https://www.contentful.com 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining 9 | a copy of this software and associated documentation files (the 10 | "Software"), to deal in the Software without restriction, including 11 | without limitation the rights to use, copy, modify, merge, publish, 12 | distribute, sublicense, and/or sell copies of the Software, and to 13 | permit persons to whom the Software is furnished to do so, subject to 14 | the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be 17 | included in all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 20 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 21 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 22 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 23 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 24 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 25 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 26 | 27 | Generated by CocoaPods - https://cocoapods.org 28 | -------------------------------------------------------------------------------- /Boilerplate/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "idiom" : "ipad", 35 | "size" : "29x29", 36 | "scale" : "1x" 37 | }, 38 | { 39 | "idiom" : "ipad", 40 | "size" : "29x29", 41 | "scale" : "2x" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "size" : "40x40", 46 | "scale" : "1x" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "size" : "40x40", 51 | "scale" : "2x" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "size" : "76x76", 56 | "scale" : "1x" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "size" : "76x76", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Space.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Space.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 18/08/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// A Space represents a collection of Content Types, Assets and Entries in Contentful 13 | public class Space: Resource, FlatResource, Decodable { 14 | 15 | /// System fields. 16 | public let sys: Sys 17 | 18 | /// Available Locales for this Space 19 | public let locales: [Locale] 20 | 21 | /// The name of this Space 22 | public let name: String 23 | 24 | /// Resource type ("Space"). 25 | public var type: String { 26 | return sys.type 27 | } 28 | 29 | // MARK: 30 | 31 | public required init(from decoder: Decoder) throws { 32 | let container = try decoder.container(keyedBy: CodingKeys.self) 33 | sys = try container.decode(Sys.self, forKey: .sys) 34 | name = try container.decode(String.self, forKey: .name) 35 | locales = try container.decode([Locale].self, forKey: .locales) 36 | } 37 | 38 | private enum CodingKeys: String, CodingKey { 39 | case sys 40 | case name 41 | case locales 42 | } 43 | } 44 | 45 | extension Space: EndpointAccessible { 46 | 47 | public static let endpoint = Endpoint.spaces 48 | } 49 | -------------------------------------------------------------------------------- /Boilerplate/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | LSRequiresIPhoneOS 22 | 23 | UILaunchStoryboardName 24 | LaunchScreen 25 | UIRequiredDeviceCapabilities 26 | 27 | armv7 28 | 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /Boilerplate/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/ContentType.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentType.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 18/08/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A Content Type represents your data model for Entries in a Contentful Space 12 | public class ContentType: Resource, FlatResource, Decodable, ResourceQueryable { 13 | 14 | /// The QueryType for ContentType is a ContentTypeQuery 15 | public typealias QueryType = ContentTypeQuery 16 | 17 | /// System fields. 18 | public let sys: Sys 19 | 20 | /// The fields which are part of this Content Type 21 | public let fields: [Field] 22 | 23 | /// The name of this Content Type 24 | public let name: String 25 | 26 | /// The description of this Content Type. 27 | public let description: String? 28 | 29 | /// Resource type ("ContentType") 30 | public var type: String { 31 | return sys.type 32 | } 33 | 34 | public required init(from decoder: Decoder) throws { 35 | let container = try decoder.container(keyedBy: CodingKeys.self) 36 | sys = try container.decode(Sys.self, forKey: .sys) 37 | fields = try container.decode([Field].self, forKey: .fields) 38 | name = try container.decode(String.self, forKey: .name) 39 | description = try container.decodeIfPresent(String.self, forKey: .description) 40 | } 41 | 42 | private enum CodingKeys: String, CodingKey { 43 | case sys, fields, name, description 44 | } 45 | 46 | /// The keys used when representing a resource in JSON. These can be used when constructing 47 | /// ContentTypeQuery instance to query against content types. 48 | public enum QueryableCodingKey: String, CodingKey { 49 | /// The name and description keys. 50 | case name, description 51 | } 52 | } 53 | 54 | extension ContentType: EndpointAccessible { 55 | 56 | public static let endpoint = Endpoint.contentTypes 57 | } 58 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/DataCache.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DataCache.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 31.07.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | internal class DataCache { 12 | 13 | static let cacheKeyDelimiter = "_" 14 | 15 | internal static func cacheKey(for link: Link) -> String { 16 | let linkType: String 17 | switch link { 18 | case .asset: linkType = "asset" 19 | case .entry: linkType = "entry" 20 | case .unresolved(let sys): linkType = sys.linkType 21 | } 22 | 23 | let cacheKey = DataCache.cacheKey(id: link.id, linkType: linkType) 24 | return cacheKey 25 | } 26 | 27 | private static func cacheKey(id: String, linkType: String) -> String { 28 | let delimeter = DataCache.cacheKeyDelimiter 29 | let cacheKey = id + delimeter + linkType.lowercased() + delimeter 30 | return cacheKey 31 | } 32 | 33 | var assetCache = Dictionary() 34 | var entryCache = Dictionary() 35 | 36 | internal func add(asset: Asset) { 37 | assetCache[DataCache.cacheKey(id: asset.id, linkType: "Asset")] = asset 38 | } 39 | 40 | internal func add(entry: EntryDecodable) { 41 | entryCache[DataCache.cacheKey(id: entry.id, linkType: "Entry")] = entry 42 | } 43 | 44 | internal func asset(for identifier: String) -> Asset? { 45 | return assetCache[identifier] 46 | } 47 | 48 | internal func entry(for identifier: String) -> EntryDecodable? { 49 | return entryCache[identifier] as? EntryDecodable 50 | } 51 | 52 | internal func item(for identifier: String) -> T? { 53 | return item(for: identifier) as? T 54 | } 55 | 56 | internal func item(for identifier: String) -> Any? { 57 | var target: Any? = self.asset(for: identifier) 58 | 59 | if target == nil { 60 | target = self.entry(for: identifier) 61 | } 62 | 63 | return target 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Boilerplate/Pods-Boilerplate-acknowledgements.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreferenceSpecifiers 6 | 7 | 8 | FooterText 9 | This application makes use of the following third party libraries: 10 | Title 11 | Acknowledgements 12 | Type 13 | PSGroupSpecifier 14 | 15 | 16 | FooterText 17 | Copyright (c) 2014 Contentful GmbH - https://www.contentful.com 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining 20 | a copy of this software and associated documentation files (the 21 | "Software"), to deal in the Software without restriction, including 22 | without limitation the rights to use, copy, modify, merge, publish, 23 | distribute, sublicense, and/or sell copies of the Software, and to 24 | permit persons to whom the Software is furnished to do so, subject to 25 | the following conditions: 26 | 27 | The above copyright notice and this permission notice shall be 28 | included in all copies or substantial portions of the Software. 29 | 30 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 31 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 32 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 33 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 34 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 35 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 36 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 37 | 38 | License 39 | MIT 40 | Title 41 | Contentful 42 | Type 43 | PSGroupSpecifier 44 | 45 | 46 | FooterText 47 | Generated by CocoaPods - https://cocoapods.org 48 | Title 49 | 50 | Type 51 | PSGroupSpecifier 52 | 53 | 54 | StringsTable 55 | Acknowledgements 56 | Title 57 | Acknowledgements 58 | 59 | 60 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Result.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 30/05/18. 6 | // Copyright © 2018 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Conform to ResultType to use your own result type, e.g. from other libraries with Contentful. 12 | public protocol ResultType { 13 | /// Describes the contained successful type of this result. 14 | associatedtype Value 15 | 16 | /// Return an error if the result is unsuccessful, otherwise nil. 17 | var error: Error? { get } 18 | 19 | /// Return the value if the result is successful, otherwise nil. 20 | var value: Value? { get } 21 | } 22 | 23 | 24 | /** 25 | A result contains the result of a computation or task. It might be either successfull 26 | with an attached value or a failure with an attached error (which conforms to Swift 2's 27 | ErrorType). You can read more about the implementation in 28 | [this blog post](http://jensravens.de/a-swifter-way-of-handling-errors/). 29 | */ 30 | public enum Result: ResultType { 31 | case success(T) 32 | case error(Error) 33 | 34 | /** 35 | Initialize a result containing a successful value. 36 | */ 37 | public init(success value: T) { 38 | self = Result.success(value) 39 | } 40 | 41 | /** 42 | Initialize a result containing an error 43 | */ 44 | public init(error: Error) { 45 | self = .error(error) 46 | } 47 | 48 | /** 49 | Direct access to the content of the result as an optional. If the result was a success, 50 | the optional will contain the value of the result. 51 | */ 52 | public var value: T? { 53 | switch self { 54 | case let .success(v): return v 55 | case .error: return nil 56 | } 57 | } 58 | 59 | /** 60 | Direct access to the error of the result as an optional. If the result was an error, 61 | the optional will contain the error of the result. 62 | */ 63 | public var error: Error? { 64 | switch self { 65 | case .success: return nil 66 | case .error(let x): return x 67 | } 68 | } 69 | } 70 | 71 | 72 | /** 73 | Provide a default value for failed results. 74 | */ 75 | public func ?? (result: Result, defaultValue: @autoclosure () -> T) -> T { 76 | switch result { 77 | case .success(let x): return x 78 | case .error: return defaultValue() 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Sys.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Sys.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 16/03/2017. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | The system fields available on all resources in Contentful. At minimum, 13 | all resources have an `id` and a `type` available. Entries and assets provide more information than 14 | */ 15 | public struct Sys { 16 | 17 | /// The unique id. 18 | public let id: String 19 | 20 | /// Resource type 21 | public let type: String 22 | 23 | /// Read-only property describing the date the `Resource` was created. 24 | public let createdAt: Date? 25 | 26 | /// Read-only property describing the date the `Resource` was last updated. 27 | public let updatedAt: Date? 28 | 29 | /// Currently selected locale 30 | public var locale: LocaleCode? // Not present when hitting /sync or using "*" wildcard locale in request. 31 | 32 | /// The identifier for the ContentType, if the Resource is an `Entry`. 33 | public var contentTypeId: String? { 34 | return contentTypeInfo?.sys.id 35 | } 36 | 37 | /// The number denoting what published version of the resource is. 38 | public let revision: Int? 39 | 40 | /// The link describing the resource type. Not present on `Asset` or `ContentType` resources. 41 | public let contentTypeInfo: Link? 42 | } 43 | 44 | extension Sys: Decodable { 45 | 46 | public init(from decoder: Decoder) throws { 47 | let container = try decoder.container(keyedBy: CodingKeys.self) 48 | 49 | id = try container.decode(String.self, forKey: .id) 50 | type = try container.decode(String.self, forKey: .type) 51 | createdAt = try container.decodeIfPresent(Date.self, forKey: .createdAt) 52 | updatedAt = try container.decodeIfPresent(Date.self, forKey: .updatedAt) 53 | locale = try container.decodeIfPresent(String.self, forKey: .locale) 54 | revision = try container.decodeIfPresent(Int.self, forKey: .revision) 55 | contentTypeInfo = try container.decodeIfPresent(Link.self, forKey: .contentType) 56 | } 57 | 58 | /// The keys used when representing a resource in JSON. 59 | public enum CodingKeys: String, CodingKey { 60 | /// The JSON keys for a Sys object. 61 | case id, type, createdAt, updatedAt, locale, revision, contentType 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Date.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Date.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 04/01/2017. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | // Formatter and extensions pulled from: https://stackoverflow.com/a/28016692/4068264 12 | // and https://stackoverflow.com/a/46538676/4068264 13 | public extension Date { 14 | 15 | // An array of 4 date formats: the format present on `sys` properties in Contentful, 16 | // and the 3 formats used when creating entries in the Contentful web app. See this reference 17 | // for date symbols: http://userguide.icu-project.org/formatparse/datetime 18 | internal static let supportedFormats: [String] = [ 19 | // Fractional seconds, as seen in `sys.updatedAt` and `sys.createdAt` 20 | "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX", 21 | "yyyy-MM-dd", 22 | "yyyy-MM-dd'T'HH:mm", 23 | // Handle UTC offsets. 24 | "yyyy-MM-dd'T'HH:mmxxx", 25 | "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 26 | ] 27 | 28 | /// A small error type thrown when the date found in JSON cannot be deserialized. 29 | public enum Error: String, Swift.Error { 30 | /// The error thrown when a date string returned in an API response cannot be parsed by the SDK. 31 | case unsupportedDateFormat 32 | } 33 | 34 | /// A formatter ready to handle iso8601 dates: normalized string output to an offset of 0 from UTC. 35 | public static func iso8601Formatter(timeZone: TimeZone? = nil) -> DateFormatter { 36 | let formatter = DateFormatter() 37 | formatter.calendar = Calendar(identifier: .iso8601) 38 | // The locale and timezone properties must be exactly as follows to have a true, time-zone agnostic (i.e. offset of 00:00 from UTC) ISO stamp. 39 | formatter.locale = Foundation.Locale(identifier: "en_US_POSIX") 40 | formatter.timeZone = timeZone ?? TimeZone(secondsFromGMT: 0) 41 | formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" 42 | return formatter 43 | } 44 | 45 | /// A custom for deserializing dates that come from Contentful 46 | /// This method first attempts to deserialize ISO8601 internetDateTime with fractional seconds 47 | /// then falls back to attempt the three other ISO8601 variants that the Contentful web app 48 | /// enables for editing date fields. 49 | /// - parameter decoder: The Decoder used to deserialize JSON from Contentful. 50 | /// - throws: Error.unsupportedDateFormat if the date isn't one of the three formats the web app supports 51 | /// or the format used by Contentful `sys` properties. 52 | public static func variableISO8601Strategy(_ decoder: Decoder) throws -> Date { 53 | let container = try decoder.singleValueContainer() 54 | let dateString = try container.decode(String.self) 55 | 56 | let timeZone = decoder.timeZone 57 | let formatter = Date.iso8601Formatter(timeZone: timeZone) 58 | 59 | for format in Date.supportedFormats { 60 | formatter.dateFormat = format 61 | if let date = formatter.date(from: dateString) { 62 | return date 63 | } 64 | } 65 | throw Error.unsupportedDateFormat 66 | } 67 | 68 | /// Returns a `String` in the ISO8601 format. 69 | public var iso8601String: String { 70 | return Date.iso8601Formatter().string(from: self) 71 | } 72 | } 73 | 74 | public extension String { 75 | 76 | /// Return a `Date` object if the current String is in the right format. 77 | public var iso8601StringDate: Date? { 78 | return Date.iso8601Formatter().date(from: self) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Link.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Link.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 16/03/2017. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | A representation of Linked Resources that a field may point to in your content model. 13 | This stateful type safely highlights links that have been resolved to Entries, Assets, or if they are 14 | still unresolved. 15 | */ 16 | public enum Link: Decodable { 17 | 18 | /// The system properties which describe the link. 19 | public struct Sys: Decodable { 20 | 21 | /// The identifier of the linked resource 22 | public let id: String 23 | 24 | /// The type of the linked resource: either "Entry" or "Asset". 25 | public let linkType: String 26 | 27 | /// The content type identifier for the linked resource. 28 | public let type: String 29 | } 30 | 31 | /// The Link points to an `Asset` 32 | case asset(Asset) 33 | 34 | /// The Link points to an `Entry` 35 | case entry(Entry) 36 | 37 | /// The Link is unresolved, and therefore contains a dictionary of metadata describing the linked resource. 38 | case unresolved(Link.Sys) 39 | 40 | /// The unique identifier of the linked asset or entry 41 | public var id: String { 42 | switch self { 43 | case .asset(let asset): return asset.id 44 | case .entry(let entry): return entry.id 45 | case .unresolved(let sys): return sys.id 46 | } 47 | } 48 | 49 | /// The linked Entry, if it exists. 50 | public var entry: Entry? { 51 | switch self { 52 | case .entry(let entry): return entry 53 | default: return nil 54 | } 55 | } 56 | 57 | /// The linked Asset, if it exists. 58 | public var asset: Asset? { 59 | switch self { 60 | case .asset(let asset): return asset 61 | default: return nil 62 | } 63 | } 64 | 65 | /// The system properties which describe the link. 66 | public var sys: Link.Sys { 67 | switch self { 68 | case .unresolved(let sys): 69 | return sys 70 | default: 71 | fatalError("Should not try to access sys properties on links that are resolved.") 72 | } 73 | } 74 | 75 | internal var isResolved: Bool { 76 | switch self { 77 | case .asset, .entry: return true 78 | case .unresolved: return false 79 | } 80 | } 81 | 82 | internal func resolve(against includedEntries: [Entry]?, and includedAssets: [Asset]?) -> Link { 83 | switch self { 84 | case .unresolved(let sys): 85 | switch sys.linkType { 86 | case "Entry": 87 | if let entry = (includedEntries?.filter { $0.sys.id == sys.id })?.first { 88 | return Link.entry(entry) 89 | } 90 | case "Asset": 91 | if let asset = (includedAssets?.filter { return $0.sys.id == sys.id })?.first { 92 | return Link.asset(asset) 93 | } 94 | default: 95 | fatalError() 96 | } 97 | 98 | default: 99 | fatalError() 100 | } 101 | return self 102 | } 103 | 104 | public init(from decoder: Decoder) throws { 105 | let container = try decoder.container(keyedBy: CodingKeys.self) 106 | let sys = try container.decode(Link.Sys.self, forKey: .sys) 107 | self = .unresolved(sys) 108 | } 109 | 110 | private enum CodingKeys: String, CodingKey { 111 | case sys 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /Boilerplate/ViewController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ViewController.swift 3 | // Boilerplate 4 | // 5 | // Created by JP Wright on 31/01/2017. 6 | // Copyright © 2017 Contentful. All rights reserved. 7 | // 8 | 9 | import UIKit 10 | import Contentful 11 | 12 | /** 13 | * The Space Id and access_token for your Contentful space. Copy paste your space id, 14 | * and your CDA API key (access token) to replace the variables below and see your data in the console. 15 | */ 16 | let SPACE_ID = "cfexampleapi" 17 | let ACCESS_TOKEN = "b4c0n73n7fu1" 18 | 19 | class ViewController: UIViewController { 20 | 21 | /** 22 | * We must initialize a Contentful.Client object to infterface with the Contentful CDA. 23 | * In this case, it's an instance variable on our viewController. 24 | */ 25 | let client: Client = Client(spaceId: SPACE_ID, accessToken: ACCESS_TOKEN) 26 | 27 | override func viewDidLoad() { 28 | super.viewDidLoad() 29 | view.backgroundColor = .white 30 | addCheckConsoleLabel() 31 | 32 | /** 33 | * All of the Client methods to retrieve content from the API are prefixed with `fetch` 34 | * They use asynchronous callbacks to handle the responses. The response has a Result enum 35 | * which encapsulates the returned value if the request was successful, or the error if the 36 | * request failed. 37 | * Don't forget to delegate to the main thread if you are going to update the UI with the content! 38 | */ 39 | client.fetchSpace { [weak self] (result: Result) in 40 | 41 | // This callback is where your network response will be handled 42 | // If you're going to update the UI...make sure to delegate back to the main thread. 43 | switch result { 44 | case .success(let space): 45 | print("==================Printing space==================") 46 | print("Fetched space with the name \(space.name)") 47 | print("===============Done Printing space================") 48 | case .error(let error): 49 | self?.handle(error: error) 50 | } 51 | } 52 | 53 | /** 54 | * Similar to `fetchSpace` fetchEntries returns a Result, but this time containing a Contentful 55 | * Array of Entry objects. 56 | */ 57 | client.fetchArray(of: Entry.self, matching: Query.limit(to: 100)) { [weak self] (result: Result>) in 58 | switch result { 59 | case .success(let entries): 60 | print("=================Printing Entries=================") 61 | entries.items.forEach { entry in 62 | print(entry.id) 63 | } 64 | print("==============Done Printing Entries===============") 65 | case .error(let error): 66 | self?.handle(error: error) 67 | } 68 | } 69 | } 70 | 71 | // MARK: Private 72 | 73 | func handle(error: Error) { 74 | print("Uh oh, something went wrong. You can do what you want with this \(error)") 75 | } 76 | 77 | func addCheckConsoleLabel() { 78 | let checkConsoleLabel = UILabel(frame: .zero) 79 | view.addSubview(checkConsoleLabel) 80 | checkConsoleLabel.translatesAutoresizingMaskIntoConstraints = false 81 | checkConsoleLabel.numberOfLines = 0 82 | checkConsoleLabel.lineBreakMode = .byWordWrapping 83 | checkConsoleLabel.textAlignment = .center 84 | checkConsoleLabel.text = "Check Xcode console for Contentful CDA Responses" 85 | checkConsoleLabel.font = UIFont.systemFont(ofSize: 28.0, weight: .medium) 86 | checkConsoleLabel.sizeToFit() 87 | 88 | let viewsDictionary = ["checkConsoleLabel": checkConsoleLabel] 89 | view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-15.0-[checkConsoleLabel]-15.0-|", options: [], metrics: nil, views: viewsDictionary)) 90 | view.addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-50.0-[checkConsoleLabel]->=100.0-|", options: [], metrics: nil, views: viewsDictionary)) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Field.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Field.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 30/09/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | /// An alias for String representing the id for a field. 10 | public typealias FieldName = String 11 | 12 | /// The possible Field types in Contentful 13 | public enum FieldType: String, Decodable { 14 | /// An array of links or symbols 15 | case array = "Array" 16 | /// A link to an Asset 17 | case asset = "Asset" 18 | /// A boolean value, true or false 19 | case boolean = "Boolean" 20 | /// A date value with optional time component 21 | case date = "Date" 22 | /// A link to an Entry 23 | case entry = "Entry" 24 | /// A numeric integer value 25 | case integer = "Integer" 26 | /// A link to an Asset or Entry 27 | case link = "Link" 28 | /// A location value, consists of latitude and longitude 29 | case location = "Location" 30 | /// A floating point number value 31 | case number = "Number" 32 | /// A JSON object value 33 | case object = "Object" 34 | /// A short text string, can be part of an array 35 | case symbol = "Symbol" 36 | /// A longer text string 37 | case text = "Text" 38 | /// An unknown kind of value 39 | case none = "None" 40 | /// The structured text field type. 41 | case structuredText = "StructuredText" 42 | } 43 | 44 | /// A Field describes a single value inside an Entry. 45 | public struct Field: Decodable { 46 | /// The unique identifier of this Field 47 | public let id: String 48 | /// The name of this Field 49 | public let name: String 50 | 51 | /// Whether this field is disabled (invisible by default in the UI) 52 | public let disabled: Bool 53 | /// Whether this field is localized (can have different values depending on locale) 54 | public let localized: Bool 55 | /// Whether this field is required (needs to have a value) 56 | public let required: Bool 57 | 58 | /// The type of this Field 59 | public let type: FieldType 60 | 61 | /** 62 | The item type of this Field (a subtype if `type` is `Array` or `Link`) 63 | For `Array`s, itemType is inferred via items.type. 64 | For `Link`s, itemType is inferred via "linkType". 65 | */ 66 | public let itemType: FieldType? 67 | 68 | public init(from decoder: Decoder) throws { 69 | let container = try decoder.container(keyedBy: CodingKeys.self) 70 | id = try container.decode(String.self, forKey: .id) 71 | name = try container.decode(String.self, forKey: .name) 72 | disabled = try container.decode(Bool.self, forKey: .disabled) 73 | localized = try container.decode(Bool.self, forKey: .localized) 74 | required = try container.decode(Bool.self, forKey: .required) 75 | 76 | type = try container.decode(FieldType.self, forKey: .type) 77 | 78 | var itemTypeString: String? 79 | 80 | if type == FieldType.array { 81 | if let items = try container.decodeIfPresent([String: Any].self, forKey: .items) { 82 | itemTypeString = items["type"] as? String 83 | if itemTypeString == FieldType.link.rawValue { 84 | itemTypeString = items["linkType"] as? String 85 | } 86 | } 87 | } else if type == FieldType.link { 88 | itemTypeString = try container.decode(String.self, forKey: .linkType) 89 | } 90 | self.itemType = FieldType(rawValue: itemTypeString ?? FieldType.none.rawValue) ?? .none 91 | } 92 | 93 | private enum CodingKeys: String, CodingKey { 94 | case id, name, disabled, localized, required 95 | case type 96 | case items 97 | case linkType 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Persistence.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Persistence.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 14.06.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Conform to this protocol and initialize your `Client` instance with the `persistenceIntegration` 13 | initialization parameter set to recieve messages about creation and deletion of `Entry`s and `Asset`s 14 | in your space when doing sync operations using the `Client.initialSync()` and Client.nextSync()` methods. 15 | Proper conformance to this protocol should enable persisting the state changes that happen in your Contentful 16 | space to a persistent store such as `CoreData`. 17 | */ 18 | public protocol PersistenceIntegration: Integration { 19 | 20 | /** 21 | Updates the PersistenceIntegration with information about the locales supported in the current space. 22 | */ 23 | func update(localeCodes: [LocaleCode]) 24 | 25 | /** 26 | Update the local datastore with the latest delta messages from the most recently fetched SyncSpace response. 27 | 28 | There is no guarantee which thread this will be called from, so it is your responsibility when implementing 29 | this method, to execute on whatever thread your local datastore may require operations to be executed on. 30 | */ 31 | func update(with syncSpace: SyncSpace) 32 | 33 | 34 | /** 35 | This is called whenever a new Asset was created or an existing one was updated. 36 | 37 | There is no guarantee which thread this will be called from, so it is your responsibility when implementing 38 | this method, to execute on whatever thread your local datastore may require operations to be executed on. 39 | 40 | - parameter asset: The created/updated Asset 41 | */ 42 | func create(asset: Asset) 43 | 44 | /** 45 | This is called whenever an Asset was deleted. 46 | 47 | There is no guarantee which thread this will be called from, so it is your responsibility when implementing 48 | this method, to execute on whatever thread your local datastore may require operations to be executed on. 49 | 50 | - parameter assetId: Identifier of the Asset that was deleted. 51 | */ 52 | func delete(assetWithId: String) 53 | 54 | /** 55 | This is called whenever a new Entry was created or an existing one was updated. 56 | 57 | There is no guarantee which thread this will be called from, so it is your responsibility when implementing 58 | this method, to execute on whatever thread your local datastore may require operations to be executed on. 59 | 60 | - parameter entry: The created/updated Entry 61 | */ 62 | func create(entry: Entry) 63 | 64 | /** 65 | This is called whenever an Entry was deleted. 66 | 67 | There is no guarantee which thread this will be called from, so it is your responsibility when implementing 68 | this method, to execute on whatever thread your local datastore may require operations to be executed on. 69 | 70 | - parameter entryId: Identifier of the Entry that was deleted. 71 | */ 72 | func delete(entryWithId: String) 73 | 74 | /** 75 | This method is called on completion of a successful `Client.initialSync` and `Client.nextSync` 76 | calls so that the sync token can be cached and future launches of your application can synchronize 77 | without doing an `initialSync`. 78 | 79 | There is no guarantee which thread this will be called from, so it is your responsibility when implementing 80 | this method, to execute on whatever thread your local datastore may require operations to be executed on. 81 | */ 82 | func update(syncToken: String) 83 | 84 | /** 85 | This method is called after all `Entry`s have been created and all links have been resolved. 86 | The implementation should map `Entry` fields that are `Link`s to persistent relationships in 87 | the underlying persistent data store. 88 | 89 | There is no guarantee which thread this will be called from, so it is your responsibility when implementing 90 | this method, to execute on whatever thread your local datastore may require operations to be executed on. 91 | */ 92 | func resolveRelationships() 93 | 94 | /** 95 | This method is called after all `Asset`s and `Entry`s have been tranformed to persistable data 96 | structures. The implementation should actually perform the save operation to the persisent database. 97 | 98 | There is no guarantee which thread this will be called from, so it is your responsibility when implementing 99 | this method, to execute on whatever thread your local datastore may require operations to be executed on. 100 | */ 101 | func save() 102 | } 103 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Entry.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Entry.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 18/08/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /** 12 | Classes conforming to this protocol can be passed into your Client instance so that fetch methods 13 | asynchronously returning MappedCollection can be used and classes of your own definition can be returned. 14 | 15 | It's important to note that there is no special handling of locales so if using the locale=* query parameter, 16 | you will need to implement the special handing in your `init(from decoder: Decoder) throws` initializer for your class. 17 | 18 | Example: 19 | 20 | ``` 21 | func fetchMappedEntries(with query: Query, 22 | then completion: @escaping ResultsHandler>) -> URLSessionDataTask? 23 | ``` 24 | */ 25 | public protocol EntryDecodable: FlatResource, Decodable, EndpointAccessible { 26 | /// The identifier of the Contentful content type that will map to this type of `EntryPersistable` 27 | static var contentTypeId: ContentTypeId { get } 28 | } 29 | 30 | public extension EndpointAccessible where Self: EntryDecodable { 31 | static var endpoint: Endpoint { 32 | return Endpoint.entries 33 | } 34 | } 35 | 36 | /// An Entry represents a typed collection of data in Contentful 37 | public class Entry: LocalizableResource { 38 | 39 | /// A convenience subscript operator to access the fields dictionary directly and return a String? 40 | public subscript(key: String) -> String? { 41 | return fields[key] as? String 42 | } 43 | 44 | /// A convenience subscript operator to access the fields dictionary directly and return an Int? 45 | public subscript(key: String) -> Int? { 46 | return fields[key] as? Int 47 | } 48 | 49 | // MARK: Internal 50 | 51 | internal func resolveLinks(against includedEntries: [Entry]?, and includedAssets: [Asset]?) { 52 | var localizableFields = [FieldName: [LocaleCode: Any]]() 53 | 54 | for (fieldName, localizableFieldMap) in self.localizableFields { 55 | // Mutable copy. 56 | var resolvedLocalizableFieldMap = localizableFieldMap 57 | 58 | for (localeCode, fieldValueForLocaleCode) in localizableFieldMap { 59 | 60 | if let unresolvedLink = fieldValueForLocaleCode as? Link, unresolvedLink.isResolved == false { 61 | let resolvedLink = unresolvedLink.resolve(against: includedEntries, and: includedAssets) 62 | // Technically it's possible that the link is still unresolved at this point: 63 | // for instance if a user specify type=Entry when using the '/sync' endpoint 64 | resolvedLocalizableFieldMap[localeCode] = resolvedLink 65 | } 66 | 67 | // Resolve one-to-many links. We need to account for links that might not have been 68 | // resolved because of a multiple page sync so we will store a dictionary rather 69 | // than a Swift object in the link body. The link will be resolved at a later time. 70 | 71 | if let mixedLinks = fieldValueForLocaleCode as? [Link] { 72 | 73 | // The conversion from dictionary representation should only ever happen once 74 | let alreadyResolvedLinks = mixedLinks.filter { $0.isResolved == true } 75 | 76 | let unresolvedLinks = mixedLinks.filter { $0.isResolved == false } 77 | let newlyResolvedLinks = unresolvedLinks.map { $0.resolve(against: includedEntries, and: includedAssets) } 78 | 79 | let resolvedLinks = alreadyResolvedLinks + newlyResolvedLinks 80 | resolvedLocalizableFieldMap[localeCode] = resolvedLinks 81 | } 82 | 83 | // Resolve links for structured text fields. 84 | if let value = fieldValueForLocaleCode as? Document { 85 | let embeddedEntryNodes: [Node] = value.content.map { node in 86 | if let blockNode = node as? EmbeddedEntryBlock { 87 | let resolvedTarget = blockNode.data.target.resolve(against: includedEntries, and: includedAssets) 88 | let newData = EmbeddedResourceData(resolvedTarget: resolvedTarget) 89 | let newBlockNode = EmbeddedEntryBlock(resolvedData: newData) 90 | return newBlockNode 91 | } 92 | return node 93 | } 94 | let newDocument = Document(content: embeddedEntryNodes) 95 | resolvedLocalizableFieldMap[localeCode] = newDocument 96 | } 97 | } 98 | localizableFields[fieldName] = resolvedLocalizableFieldMap 99 | } 100 | 101 | self.localizableFields = localizableFields 102 | } 103 | } 104 | 105 | extension Entry: EndpointAccessible { 106 | 107 | public static let endpoint = Endpoint.entries 108 | } 109 | 110 | extension Entry: ResourceQueryable { 111 | 112 | /// The QueryType for an EntryQuery is Query. 113 | public typealias QueryType = Query 114 | } 115 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/ClientConfiguration.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ClientConfiguration.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 18/08/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Some default values that the SDK uses. 12 | public struct Host { 13 | /// The path for the Contentful Delivery API. 14 | public static let delivery = "cdn.contentful.com" 15 | /// The path for the Contentful Preview API. 16 | public static let preview = "preview.contentful.com" 17 | } 18 | 19 | /** 20 | The `Integration` protocol describes the libary name and version number for external integrations 21 | to be used in conjunction with the contentful.swift SDK. 22 | */ 23 | public protocol Integration { 24 | 25 | /// The name of the integrated library. 26 | var name: String { get } 27 | 28 | /// The version number for the intergrated library. 29 | var version: String { get } 30 | } 31 | 32 | /// ClientConfiguration parameters for a client instance 33 | public struct ClientConfiguration { 34 | 35 | /// The default instance of ClientConfiguration which interfaces with the Content Delivery API. 36 | public static let `default` = ClientConfiguration() 37 | 38 | /// Whether or not to use HTTPS connections, defaults to `true` 39 | public var secure = true 40 | 41 | /// An optional configuration to override the date decoding strategy that is provided by the the SDK. 42 | public var dateDecodingStrategy: JSONDecoder.DateDecodingStrategy? 43 | 44 | /// An optional configuration to override the TimeZone the SDk will use to serialize Date instances. The SDK will 45 | /// use a TimeZone with 0 seconds offset from GMT if this configuration is omitted. 46 | public var timeZone: TimeZone? 47 | 48 | /// Computed version of the user agent, including OS name and version 49 | internal func userAgentString(with integration: Integration?) -> String { 50 | // Inspired by Alamofire https://github.com/Alamofire/Alamofire/blob/25d8fdd8a36f510a2bc4fe98289f367ec385d337/Source/SessionManager.swift 51 | 52 | var userAgentString = "" 53 | 54 | // Fail gracefully in case any information is inaccessible. 55 | // App info. 56 | if let appVersionString = appVersionString() { 57 | userAgentString = "app \(appVersionString); " 58 | } 59 | // SDK info. 60 | userAgentString += "sdk \(sdkVersionString());" 61 | // Platform/language info. 62 | if let platformVersionString = platformVersionString() { 63 | userAgentString += " platform \(platformVersionString);" 64 | } 65 | 66 | // Operating system info. 67 | if let operatingSystemVersionString = operatingSystemVersionString() { 68 | userAgentString += " os \(operatingSystemVersionString);" 69 | } 70 | // Integration 71 | if let integration = integration { 72 | userAgentString += " integration \(integration.name)/\(integration.version);" 73 | } 74 | 75 | return userAgentString 76 | } 77 | 78 | private func platformVersionString() -> String? { 79 | var swiftVersionString: String? 80 | 81 | // The project is only compatible with swift >=4.0 82 | #if swift(>=4.0) 83 | swiftVersionString = "4.0" 84 | #endif 85 | 86 | guard let swiftVersion = swiftVersionString else { return nil } 87 | return "Swift/\(swiftVersion)" 88 | } 89 | 90 | /** 91 | Initialize a clientConfiguration with default values 92 | 93 | - returns: An initialized clientConfiguration instance 94 | */ 95 | public init() {} 96 | 97 | // MARK: Private 98 | 99 | private func operatingSystemVersionString() -> String? { 100 | guard let osName = operatingSystemPlatform() else { return nil } 101 | 102 | let osVersion = ProcessInfo.processInfo.operatingSystemVersion 103 | let osVersionString = String(osVersion.majorVersion) + "." + String(osVersion.minorVersion) + "." + String(osVersion.patchVersion) 104 | return "\(osName)/\(osVersionString)" 105 | } 106 | 107 | private func operatingSystemPlatform() -> String? { 108 | let osName: String? = { 109 | 110 | #if os(iOS) 111 | return "iOS" 112 | #elseif os(OSX) 113 | return "macOS" 114 | #elseif os(tvOS) 115 | return "tvOS" 116 | #elseif os(watchOS) 117 | return "watchOS" 118 | #elseif os(Linux) 119 | return "Linux" 120 | #else 121 | return nil 122 | #endif 123 | }() 124 | return osName 125 | } 126 | 127 | private func sdkVersionString() -> String { 128 | guard 129 | let bundleInfo = Bundle(for: Client.self).infoDictionary, 130 | let versionNumberString = bundleInfo["CFBundleShortVersionString"] as? String 131 | else { return "Unknown" } 132 | 133 | return "contentful.swift/\(versionNumberString)" 134 | } 135 | 136 | private func appVersionString() -> String? { 137 | guard 138 | let bundleInfo = Bundle.main.infoDictionary, 139 | let versionNumberString = bundleInfo["CFBundleShortVersionString"] as? String, 140 | let appBundleId = Bundle.main.bundleIdentifier else { return nil } 141 | 142 | return appBundleId + "/" + versionNumberString 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Locale.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Decoding.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 29/09/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// An alias for String representing the code for a Contentful locale. 12 | public typealias LocaleCode = String 13 | 14 | extension Locale: EndpointAccessible { 15 | 16 | public static let endpoint = Endpoint.locales 17 | } 18 | 19 | /// A Locale represents possible translations for Entry Fields 20 | public class Locale: Resource, FlatResource, Decodable { 21 | 22 | /// System fields. 23 | public let sys: Sys 24 | 25 | /// Linked list accessor for going to the next fallback locale 26 | public let fallbackLocaleCode: LocaleCode? 27 | 28 | /// The unique identifier for this Locale 29 | public let code: LocaleCode 30 | /** 31 | Whether this Locale is the default (if a Field is not translated in a given Locale, the value of 32 | the default locale will be returned by the API) 33 | */ 34 | public let isDefault: Bool 35 | /// The name of this Locale 36 | public let name: String 37 | 38 | private enum CodingKeys: String, CodingKey { 39 | case sys 40 | case code 41 | case isDefault = "default" 42 | case name 43 | case fallbackLocaleCode = "fallbackCode" 44 | } 45 | 46 | public required init(from decoder: Decoder) throws { 47 | let container = try decoder.container(keyedBy: CodingKeys.self) 48 | name = try container.decode(String.self, forKey: .name) 49 | code = try container.decode(LocaleCode.self, forKey: .code) 50 | isDefault = try container.decode(Bool.self, forKey: .isDefault) 51 | fallbackLocaleCode = try container.decodeIfPresent(LocaleCode.self, forKey: .fallbackLocaleCode) 52 | 53 | // If we get locales as an array within a space, the sys property will not be present. 54 | // Check if present, and if not, then manually construct a Sys object. 55 | if let sys = try container.decodeIfPresent(Sys.self, forKey: .sys) { 56 | self.sys = sys 57 | } else { 58 | self.sys = Sys(id: code, type: "Locale", createdAt: nil, updatedAt: nil, 59 | locale: nil, revision: nil, contentTypeInfo: nil) 60 | } 61 | } 62 | } 63 | 64 | /** 65 | The `LocalizationContext` contains meta information about a Space about locales including 66 | information about which locale is the default, and what the fallback locale chain is. 67 | 68 | This contextual information is necessary to intiialize `Entry` and `Asset` instances properly so that 69 | the correct data is displayed for the currently selected locale. For instance, if a particular field 70 | for an `Entry` does not have data for the currently selected locale, the SDK will walk the fallback 71 | chain for this field until a non-null value is found, or full chain has been walked. 72 | */ 73 | public class LocalizationContext { 74 | 75 | /// An ordered collection of locales representing the fallback chain. 76 | public let locales: [LocaleCode: Locale] 77 | 78 | /// The default locale of the space. 79 | public let `default`: Locale 80 | 81 | /// Initialize a new LocalizationContext with the relevant locales. 82 | public init?(locales: [Locale]) { 83 | 84 | guard let defaultLocale = locales.filter({ $0.isDefault }).first else { 85 | return nil 86 | } 87 | self.`default` = defaultLocale 88 | 89 | var localeMap = [LocaleCode: Locale]() 90 | locales.forEach { localeMap[$0.code] = $0 } 91 | self.locales = localeMap 92 | } 93 | 94 | } 95 | 96 | internal struct Localization { 97 | 98 | // Walks down the fallback chain and returns the field values for the specified locale. 99 | internal static func fields(forLocale locale: Locale?, 100 | localizableFields: [FieldName: [LocaleCode: Any]], 101 | localizationContext: LocalizationContext) -> [FieldName: Any] { 102 | 103 | // If no locale passed in, use the default. 104 | let originalLocale = locale ?? localizationContext.default 105 | 106 | var fields = [FieldName: Any]() 107 | for (fieldName, localesToFieldValues) in localizableFields { 108 | 109 | // Reset to the original locale. 110 | var currentLocale = originalLocale 111 | 112 | // While there is no localized value for a particular locale, get the next locale. 113 | while localesToFieldValues[currentLocale.code] == nil { 114 | 115 | // Break loops if we've walked through all of the locales. 116 | guard let fallbackLocaleCode = currentLocale.fallbackLocaleCode else { break } 117 | 118 | // Go to the next locale. 119 | if let fallbackLocale = localizationContext.locales[fallbackLocaleCode] { 120 | currentLocale = fallbackLocale 121 | } 122 | } 123 | // Assign the value, if it exists. 124 | if let fieldValue = localesToFieldValues[currentLocale.code] { 125 | fields[fieldName] = fieldValue 126 | } 127 | } 128 | return fields 129 | } 130 | 131 | // Normalizes fields to have a value for every locale in the space. 132 | internal static func fieldsInMultiLocaleFormat(from fields: [FieldName: Any], 133 | selectedLocale: Locale, 134 | wasSelectedOnAPILevel: Bool) throws -> [FieldName: [LocaleCode: Any]] { 135 | 136 | if wasSelectedOnAPILevel == false { // sanitize. 137 | // If there was no locale it the response, then we have the format with all locales present and we can simply map from localecode to locale and exit 138 | guard let fields = fields as? [FieldName: [LocaleCode: Any]] else { 139 | throw SDKError.localeHandlingError(message: "Unexpected response format: 'sys.locale' not present, and" 140 | + "individual fields dictionary is not in localizable format. i.e. 'title: { en-US: englishValue, de-DE: germanValue }'") 141 | } 142 | return fields 143 | } 144 | 145 | // Init container for our own format. 146 | var localizableFields = [FieldName: [LocaleCode: Any]]() 147 | 148 | for (fieldName, fieldValue) in fields { 149 | localizableFields[fieldName] = [selectedLocale.code: fieldValue] 150 | } 151 | return localizableFields 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Asset.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Asset.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 18/08/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A simple protocol to bridge `Contentful.Asset` and other formats for storing asset information. 12 | public protocol AssetProtocol: FlatResource { 13 | 14 | /// String representation for the URL of the media file associated with this asset. 15 | var urlString: String? { get } 16 | } 17 | 18 | /// Classes conforming to this protocol can be decoded during JSON deserialization as reprsentations 19 | /// of Contentful assets. 20 | public protocol AssetDecodable: AssetProtocol, Decodable {} 21 | 22 | /// An asset represents a media file in Contentful. 23 | public class Asset: LocalizableResource, AssetDecodable { 24 | 25 | /// The key paths for member fields of an Asset 26 | public enum Fields: String, CodingKey { 27 | /// Title description and file keys. 28 | case title, description, file 29 | } 30 | 31 | /// The URL for the underlying media file. Returns nil if the url was omitted from the response (i.e. `select` operation in query) 32 | /// or if the underlying media file is still processing with Contentful. 33 | public var url: URL? { 34 | 35 | guard let url = file?.url else { return nil } 36 | return url 37 | } 38 | 39 | /// String representation for the URL of the media file associated with this asset. Optional for compatibility with `select` operator queries. 40 | /// Also, If the media file is still being processed, as the final stage of uploading to your space, this property will be nil. 41 | public var urlString: String? { 42 | guard let urlString = url?.absoluteString else { return nil } 43 | return urlString 44 | } 45 | 46 | /// The title of the asset. Optional for compatibility with `select` operator queries. 47 | public var title: String? { 48 | return fields["title"] as? String 49 | } 50 | 51 | /// Description of the asset. Optional for compatibility with `select` operator queries. 52 | public var description: String? { 53 | return fields["description"] as? String 54 | } 55 | 56 | /// Metadata describing the file associated with the asset. Optional for compatibility with `select` operator queries. 57 | public var file: FileMetadata? { 58 | return fields["file"] as? FileMetadata 59 | } 60 | } 61 | 62 | public extension AssetProtocol { 63 | /** 64 | The URL for the underlying media file with additional options for server side manipulations 65 | such as format changes, resizing, cropping, and focusing on different areas including on faces, 66 | among others. 67 | 68 | - Parameter imageOptions: An array of `ImageOption` that will be used for server side manipulations. 69 | - Throws: Will throw SDKError if the SDK is unable to generate a valid URL with the desired ImageOptions. 70 | */ 71 | public func url(with imageOptions: [ImageOption] = []) throws -> URL { 72 | guard let url = try urlString?.url(with: imageOptions) else { 73 | throw SDKError.invalidURL(string: urlString ?? "No url string is stored for Asset: \(id)") 74 | } 75 | return url 76 | } 77 | } 78 | 79 | extension Asset { 80 | 81 | /// Metadata describing underlying media file. 82 | public struct FileMetadata: Decodable { 83 | 84 | /// Original filename of the file. 85 | public let fileName: String 86 | 87 | /// Content type of the file. 88 | public let contentType: String 89 | 90 | /// Details of the file, depending on it's MIME type. 91 | public let details: Details? 92 | 93 | /// The remote URL for the binary data for this Asset. 94 | /// If the media file is still being processed, as the final stage of uploading to your space, this property will be nil. 95 | public let url: URL? 96 | 97 | /// The size and dimensions of the underlying media file if it is an image. 98 | public struct Details: Decodable { 99 | /// The size of the file in bytes. 100 | public let size: Int 101 | 102 | /// Additional information describing the image the asset references. 103 | public let imageInfo: ImageInfo? 104 | 105 | /// A lightweight struct to hold the dimensions information for the this file, if it is an image type. 106 | public struct ImageInfo: Decodable { 107 | /// The width of the image. 108 | public let width: Double 109 | /// The height of the image. 110 | public let height: Double 111 | 112 | public init(from decoder: Decoder) throws { 113 | let container = try decoder.container(keyedBy: CodingKeys.self) 114 | width = try container.decode(Double.self, forKey: .width) 115 | height = try container.decode(Double.self, forKey: .height) 116 | } 117 | 118 | private enum CodingKeys: String, CodingKey { 119 | case width, height 120 | } 121 | } 122 | 123 | public init(from decoder: Decoder) throws { 124 | let container = try decoder.container(keyedBy: CodingKeys.self) 125 | size = try container.decode(Int.self, forKey: .size) 126 | imageInfo = try container.decodeIfPresent(ImageInfo.self, forKey: .image) 127 | } 128 | 129 | private enum CodingKeys: String, CodingKey { 130 | case size, image 131 | } 132 | } 133 | 134 | public init(from decoder: Decoder) throws { 135 | let container = try decoder.container(keyedBy: CodingKeys.self) 136 | fileName = try container.decode(String.self, forKey: .fileName) 137 | contentType = try container.decode(String.self, forKey: .contentType) 138 | details = try container.decode(Details.self, forKey: .details) 139 | // Decodable handles URL's automatically but we need to prepend the https protocol. 140 | let urlString = try container.decode(String.self, forKey: .url) 141 | guard let url = URL(string: "https:" + urlString) else { 142 | throw SDKError.invalidURL(string: "Asset had urlString incapable of being made into a Foundation.URL object \(urlString)") 143 | } 144 | self.url = url 145 | } 146 | 147 | private enum CodingKeys: String, CodingKey { 148 | case fileName, contentType, url, details 149 | } 150 | } 151 | } 152 | 153 | extension Asset: EndpointAccessible { 154 | 155 | public static let endpoint = Endpoint.assets 156 | } 157 | 158 | extension Asset: ResourceQueryable { 159 | 160 | /// The QueryType for an Asset is AssetQuery 161 | public typealias QueryType = AssetQuery 162 | } 163 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/QueryOperation.swift: -------------------------------------------------------------------------------- 1 | // 2 | // QueryOperation.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 16.10.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public extension Query { 12 | /** 13 | Property-value query operations used for matching patterns on either "sys" or "fields" properties of `Asset`s and `Entry`s. 14 | Each operation specifies a property name on the left-hand side, with a value to match on the right. 15 | For instance, using the doesNotEqual operation in an a concrete Query like: 16 | 17 | ``` 18 | Query(where:"fields.name", .doesNotEqual("Happy Cat")) 19 | ``` 20 | 21 | would append the following to the http URL: 22 | 23 | ``` 24 | "fields.name[ne]=Happy%20Cat" 25 | ``` 26 | 27 | Refer to the documentation for the various Query classes for more information. 28 | */ 29 | public enum Operation { 30 | 31 | /// The equality operator: 32 | case equals(String) 33 | /// The inequality operator: 34 | case doesNotEqual(String) 35 | /// Query by matching all of the values in the set: 36 | case hasAll([String]) 37 | /// The inclusion operator: 38 | case includes([String]) 39 | /// The exclusion operator: 40 | case excludes([String]) 41 | /// The existence operator: 42 | case exists(Bool) 43 | 44 | /// Full text search on a field. 45 | case matches(String) 46 | 47 | /// MARK: Ranges 48 | 49 | /// Less-than operator: 50 | case isLessThan(QueryableRange) 51 | /// Less-than-or-equal-to operator: 52 | case isLessThanOrEqualTo(QueryableRange) 53 | /// Greater-than operator: 54 | case isGreaterThan(QueryableRange) 55 | /// Greater-than-or-equal-to operator: 56 | case isGreaterThanOrEqualTo(QueryableRange) 57 | /// Equivalent to the less-than operator: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/ranges 58 | case isBefore(QueryableRange) 59 | /// Equivalent to the greater-than operator: https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/ranges 60 | case isAfter(QueryableRange) 61 | 62 | /// Location proximity search: 65 | case isWithin(Bounds) 66 | 67 | internal var string: String { 68 | switch self { 69 | case .equals: return "" 70 | case .doesNotEqual: return "[ne]" 71 | case .hasAll: return "[all]" 72 | case .includes: return "[in]" 73 | case .excludes: return "[nin]" 74 | case .exists: return "[exists]" 75 | case .matches: return "[match]" 76 | 77 | case .isLessThan: return "[lt]" 78 | case .isLessThanOrEqualTo: return "[lte]" 79 | case .isGreaterThan: return "[gt]" 80 | case .isGreaterThanOrEqualTo: return "[gte]" 81 | case .isBefore: return "[lte]" 82 | case .isAfter: return "[gte]" 83 | 84 | case .isNear: return "[near]" 85 | case .isWithin: return "[within]" 86 | } 87 | } 88 | 89 | internal var values: String { 90 | switch self { 91 | case .equals(let value): return value 92 | case .doesNotEqual(let value): return value 93 | case .hasAll(let values): return values.joined(separator: ",") 94 | case .includes(let values): return values.joined(separator: ",") 95 | case .excludes(let values): return values.joined(separator: ",") 96 | case .exists(let value): return String(value) 97 | case .matches(let value): return value 98 | 99 | case .isLessThan(let queryableRange): return queryableRange.stringValue 100 | case .isLessThanOrEqualTo(let queryableRange): return queryableRange.stringValue 101 | case .isGreaterThan(let queryableRange): return queryableRange.stringValue 102 | case .isGreaterThanOrEqualTo(let queryableRange): return queryableRange.stringValue 103 | case .isBefore(let queryableRange): return queryableRange.stringValue 104 | case .isAfter(let queryableRange): return queryableRange.stringValue 105 | 106 | case .isNear(let coordinates): return "\(coordinates.latitude),\(coordinates.longitude)" 107 | case .isWithin(let bounds): return string(for: bounds) 108 | } 109 | } 110 | 111 | private func string(for bounds: Bounds) -> String { 112 | switch bounds { 113 | case .box(let bottomLeft, let topRight): 114 | return "\(bottomLeft.latitude),\(bottomLeft.longitude),\(topRight.latitude),\(topRight.longitude)" 115 | 116 | case .circle(let center, let radius): 117 | return "\(center.latitude),\(center.longitude),\(radius)" 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Pods/Target Support Files/Pods-Boilerplate/Pods-Boilerplate-frameworks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | set -u 4 | set -o pipefail 5 | 6 | function on_error { 7 | echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" 8 | } 9 | trap 'on_error $LINENO' ERR 10 | 11 | if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then 12 | # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy 13 | # frameworks to, so exit 0 (signalling the script phase was successful). 14 | exit 0 15 | fi 16 | 17 | echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 18 | mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 19 | 20 | COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" 21 | SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" 22 | 23 | # Used as a return value for each invocation of `strip_invalid_archs` function. 24 | STRIP_BINARY_RETVAL=0 25 | 26 | # This protects against multiple targets copying the same framework dependency at the same time. The solution 27 | # was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html 28 | RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") 29 | 30 | # Copies and strips a vendored framework 31 | install_framework() 32 | { 33 | if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then 34 | local source="${BUILT_PRODUCTS_DIR}/$1" 35 | elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then 36 | local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" 37 | elif [ -r "$1" ]; then 38 | local source="$1" 39 | fi 40 | 41 | local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" 42 | 43 | if [ -L "${source}" ]; then 44 | echo "Symlinked..." 45 | source="$(readlink "${source}")" 46 | fi 47 | 48 | # Use filter instead of exclude so missing patterns don't throw errors. 49 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" 50 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" 51 | 52 | local basename 53 | basename="$(basename -s .framework "$1")" 54 | binary="${destination}/${basename}.framework/${basename}" 55 | 56 | if ! [ -r "$binary" ]; then 57 | binary="${destination}/${basename}" 58 | elif [ -L "${binary}" ]; then 59 | echo "Destination binary is symlinked..." 60 | dirname="$(dirname "${binary}")" 61 | binary="${dirname}/$(readlink "${binary}")" 62 | fi 63 | 64 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 65 | if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then 66 | strip_invalid_archs "$binary" 67 | fi 68 | 69 | # Resign the code if required by the build settings to avoid unstable apps 70 | code_sign_if_enabled "${destination}/$(basename "$1")" 71 | 72 | # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. 73 | if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then 74 | local swift_runtime_libs 75 | swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) 76 | for lib in $swift_runtime_libs; do 77 | echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" 78 | rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" 79 | code_sign_if_enabled "${destination}/${lib}" 80 | done 81 | fi 82 | } 83 | 84 | # Copies and strips a vendored dSYM 85 | install_dsym() { 86 | local source="$1" 87 | if [ -r "$source" ]; then 88 | # Copy the dSYM into a the targets temp dir. 89 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${DERIVED_FILES_DIR}\"" 90 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${DERIVED_FILES_DIR}" 91 | 92 | local basename 93 | basename="$(basename -s .framework.dSYM "$source")" 94 | binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" 95 | 96 | # Strip invalid architectures so "fat" simulator / device frameworks work on device 97 | if [[ "$(file "$binary")" == *"Mach-O dSYM companion"* ]]; then 98 | strip_invalid_archs "$binary" 99 | fi 100 | 101 | if [[ $STRIP_BINARY_RETVAL == 1 ]]; then 102 | # Move the stripped file into its final destination. 103 | echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${DERIVED_FILES_DIR}/${basename}.framework.dSYM\" \"${DWARF_DSYM_FOLDER_PATH}\"" 104 | rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.framework.dSYM" "${DWARF_DSYM_FOLDER_PATH}" 105 | else 106 | # The dSYM was not stripped at all, in this case touch a fake folder so the input/output paths from Xcode do not reexecute this script because the file is missing. 107 | touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" 108 | fi 109 | fi 110 | } 111 | 112 | # Signs a framework with the provided identity 113 | code_sign_if_enabled() { 114 | if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then 115 | # Use the current code_sign_identity 116 | echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" 117 | local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" 118 | 119 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 120 | code_sign_cmd="$code_sign_cmd &" 121 | fi 122 | echo "$code_sign_cmd" 123 | eval "$code_sign_cmd" 124 | fi 125 | } 126 | 127 | # Strip invalid architectures 128 | strip_invalid_archs() { 129 | binary="$1" 130 | # Get architectures for current target binary 131 | binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" 132 | # Intersect them with the architectures we are building for 133 | intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" 134 | # If there are no archs supported by this binary then warn the user 135 | if [[ -z "$intersected_archs" ]]; then 136 | echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." 137 | STRIP_BINARY_RETVAL=0 138 | return 139 | fi 140 | stripped="" 141 | for arch in $binary_archs; do 142 | if ! [[ "${ARCHS}" == *"$arch"* ]]; then 143 | # Strip non-valid architectures in-place 144 | lipo -remove "$arch" -output "$binary" "$binary" 145 | stripped="$stripped $arch" 146 | fi 147 | done 148 | if [[ "$stripped" ]]; then 149 | echo "Stripped $binary of architectures:$stripped" 150 | fi 151 | STRIP_BINARY_RETVAL=1 152 | } 153 | 154 | 155 | if [[ "$CONFIGURATION" == "Debug" ]]; then 156 | install_framework "${BUILT_PRODUCTS_DIR}/Contentful/Contentful.framework" 157 | fi 158 | if [[ "$CONFIGURATION" == "Release" ]]; then 159 | install_framework "${BUILT_PRODUCTS_DIR}/Contentful/Contentful.framework" 160 | fi 161 | if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then 162 | wait 163 | fi 164 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/SyncSpace.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SyncSpace.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 20/01/16. 6 | // Copyright © 2016 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// A container for the synchronized state of a Space 12 | public final class SyncSpace: Decodable { 13 | 14 | /// The url parameters relevant for the next sync operation that this `SyncSpace` can perform. 15 | public var parameters: [String: String] { 16 | 17 | if syncToken.isEmpty { 18 | return ["initial": true.description] 19 | } else { 20 | return ["sync_token": syncToken] 21 | } 22 | } 23 | 24 | /// The entity types in Contentful that a sync can be restricted to. 25 | public enum SyncableTypes { 26 | /// Sync all assets and all entries of all content types. 27 | case all 28 | /// Sync only entities which are entries (i.e. instances of your content types(s)). 29 | case entries 30 | /// Sync only assets. 31 | case assets 32 | /// Sync only entities of a specific content type. 33 | case entriesOfContentType(withId: String) 34 | /// Sync only deleted entries or assets. 35 | case allDeletions 36 | /// Sync only deleted entries. 37 | case deletedEntries 38 | /// Sync only deleted assets. 39 | case deletedAssets 40 | 41 | // Query parameters. 42 | public var parameters: [String: String] { 43 | 44 | let typeParameter = "type" 45 | switch self { 46 | case .all: 47 | // Return empty dictionary to specify that all content should be sync'ed. 48 | return [:] 49 | case .entries: 50 | return [typeParameter: "Entry"] 51 | case .assets: 52 | return [typeParameter: "Asset"] 53 | case .allDeletions: 54 | return [typeParameter: "Deletion"] 55 | case .deletedEntries: 56 | return [typeParameter: "DeletedEntry"] 57 | case .deletedAssets: 58 | return [typeParameter: "DeletedAsset"] 59 | case .entriesOfContentType(let contentTypeId): 60 | return [typeParameter: "Entry", QueryParameter.contentType: contentTypeId] 61 | } 62 | } 63 | } 64 | 65 | internal var assetsMap = [String: Asset]() 66 | internal var entriesMap = [String: Entry]() 67 | 68 | /// An array of identifiers for assets that were deleted after the last sync operations. 69 | public var deletedAssetIds = [String]() 70 | /// An array of identifiers for entries that were deleted after the last sync operations. 71 | public var deletedEntryIds = [String]() 72 | 73 | internal var hasMorePages: Bool 74 | 75 | /// A token which needs to be present to perform a subsequent synchronization operation 76 | internal(set) public var syncToken = "" 77 | 78 | /// List of Assets currently published on the Space being synchronized 79 | public var assets: [Asset] { 80 | return Array(assetsMap.values) 81 | } 82 | 83 | /// List of Entries currently published on the Space being synchronized 84 | public var entries: [Entry] { 85 | return Array(entriesMap.values) 86 | } 87 | 88 | /** 89 | Continue a synchronization with previous data. 90 | 91 | - parameter syncToken: The sync token from a previous synchronization 92 | 93 | - returns: An initialized synchronized space instance 94 | */ 95 | public init(syncToken: String = "") { 96 | self.hasMorePages = false 97 | self.syncToken = syncToken 98 | } 99 | 100 | internal static func syncToken(from urlString: String) -> String { 101 | guard let components = URLComponents(string: urlString)?.queryItems else { return "" } 102 | for component in components { 103 | if let value = component.value, component.name == "sync_token" { 104 | return value 105 | } 106 | } 107 | return "" 108 | } 109 | 110 | public required init(from decoder: Decoder) throws { 111 | let container = try decoder.container(keyedBy: CodingKeys.self) 112 | var syncUrl = try container.decodeIfPresent(String.self, forKey: .nextPageUrl) 113 | 114 | var hasMorePages = true 115 | if syncUrl == nil { 116 | hasMorePages = false 117 | syncUrl = try container.decodeIfPresent(String.self, forKey: .nextSyncUrl) 118 | } 119 | 120 | guard let nextSyncUrl = syncUrl else { 121 | throw SDKError.unparseableJSON(data: nil, errorMessage: "No sync url for future sync operations was serialized from the response.") 122 | } 123 | 124 | self.syncToken = SyncSpace.syncToken(from: nextSyncUrl) 125 | self.hasMorePages = hasMorePages 126 | 127 | // A copy as an array of dictionaries just to extract "sys.type" field. 128 | guard let items = try container.decode(Array.self, forKey: .items) as? [[String: Any]] else { 129 | throw SDKError.unparseableJSON(data: nil, errorMessage: "SDK was unable to serialize returned resources") 130 | } 131 | var itemsArrayContainer = try container.nestedUnkeyedContainer(forKey: .items) 132 | 133 | var resources = [Resource]() 134 | while itemsArrayContainer.isAtEnd == false { 135 | 136 | guard let sys = items[itemsArrayContainer.currentIndex]["sys"] as? [String: Any], let type = sys["type"] as? String else { 137 | let errorMessage = "SDK was unable to parse sys.type property necessary to finish resource serialization." 138 | throw SDKError.unparseableJSON(data: nil, errorMessage: errorMessage) 139 | } 140 | let item: Resource 141 | switch type { 142 | case "Asset": item = try itemsArrayContainer.decode(Asset.self) 143 | case "Entry": item = try itemsArrayContainer.decode(Entry.self) 144 | case "DeletedAsset": item = try itemsArrayContainer.decode(DeletedResource.self) 145 | case "DeletedEntry": item = try itemsArrayContainer.decode(DeletedResource.self) 146 | default: fatalError("Unsupported resource type '\(type)'") 147 | } 148 | resources.append(item) 149 | } 150 | 151 | cache(resources: resources) 152 | } 153 | 154 | private enum CodingKeys: String, CodingKey { 155 | case nextSyncUrl 156 | case nextPageUrl 157 | case items 158 | } 159 | 160 | internal func updateWithDiffs(from syncSpace: SyncSpace) { 161 | 162 | for asset in syncSpace.assets { 163 | assetsMap[asset.sys.id] = asset 164 | } 165 | 166 | // Update and deduplicate all entries. 167 | for entry in syncSpace.entries { 168 | entriesMap[entry.sys.id] = entry 169 | } 170 | 171 | // Resolve all entries in-memory. 172 | for entry in entries { 173 | entry.resolveLinks(against: entries, and: assets) 174 | } 175 | 176 | for deletedAssetId in syncSpace.deletedAssetIds { 177 | assetsMap.removeValue(forKey: deletedAssetId) 178 | } 179 | 180 | for deletedEntryId in syncSpace.deletedEntryIds { 181 | entriesMap.removeValue(forKey: deletedEntryId) 182 | } 183 | 184 | syncToken = syncSpace.syncToken 185 | } 186 | 187 | internal func cache(resources: [Resource]) { 188 | for resource in resources { 189 | switch resource { 190 | case let asset as Asset: 191 | self.assetsMap[asset.sys.id] = asset 192 | 193 | case let entry as Entry: 194 | self.entriesMap[entry.sys.id] = entry 195 | 196 | case let deletedResource as DeletedResource: 197 | switch deletedResource.sys.type { 198 | case "DeletedAsset": self.deletedAssetIds.append(deletedResource.sys.id) 199 | case "DeletedEntry": self.deletedEntryIds.append(deletedResource.sys.id) 200 | default: break 201 | } 202 | default: break 203 | } 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/StructuredText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // StructuredText.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 26.08.18. 6 | // Copyright © 2018 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// The base protocol which all node types that may be present in a tree of Structured text. 12 | /// See: for more information. 13 | public protocol Node: Decodable { 14 | /// The type of node which should be rendered. 15 | var nodeType: NodeType { get } 16 | var nodeClass: NodeClass { get } 17 | } 18 | 19 | /// A node which contains an array of child nodes. 20 | public protocol BlockNode: Node { 21 | /// An array of child nodes. 22 | var content: [Node] { get } 23 | } 24 | 25 | /// A node which contains a linked entry or asset. 26 | public protocol EmbeddedResourceNode: Node { 27 | var data: EmbeddedResourceData { get } 28 | } 29 | 30 | /// The data describing the linked entry or asset for an `EmbeddedResouceNode` 31 | public class EmbeddedResourceData: Decodable { 32 | // TODO: Add initializers to make immutable. 33 | /// The raw link object which describes the target entry or asset. 34 | public let target: Link 35 | 36 | /// When using the SDK in conjunction with your own `EntryDecodable` classes, this property will 37 | /// be to the resolved `EntryDecodable` instance. 38 | public var resolvedEntryDecodable: EntryDecodable? 39 | 40 | public required init(from decoder: Decoder) throws { 41 | let container = try decoder.container(keyedBy: JSONCodingKeys.self) 42 | target = try container.decode(Link.self, forKey: JSONCodingKeys(stringValue: "target")!) 43 | 44 | try container.resolveLink(forKey: JSONCodingKeys(stringValue: "target")!, decoder: decoder) { [weak self] decodable in 45 | // Workaroudn for bug in the Swift compiler: https://bugs.swift.org/browse/SR-3871 46 | self?.resolvedEntryDecodable = decodable as? EntryDecodable 47 | } 48 | } 49 | internal init(resolvedTarget: Link) { 50 | target = resolvedTarget 51 | resolvedEntryDecodable = nil 52 | } 53 | } 54 | 55 | /// A node modifying the current node with marks. 56 | public protocol InlineNode: Node { 57 | /// The textual value for this inline node. 58 | var value: String { get } 59 | /// The marks that describe the markup for this inline node. 60 | var marks: [Text.Mark] { get} 61 | } 62 | 63 | internal enum NodeContentCodingKeys: String, CodingKey { 64 | case nodeType, nodeClass, content, data 65 | } 66 | 67 | /// A descriptor of the node's position and utility within a structured text tree. 68 | public enum NodeClass: String, Decodable { 69 | /// The top-level node which is the beginning of the tree. 70 | case document 71 | /// A block node can contain child nodes within the tree structure. 72 | case block 73 | /// An inline node modifies the current node. 74 | case inline 75 | /// A text node is a leaf node that cannot have any children. 76 | case text 77 | } 78 | 79 | /// A descriptor of the node's type, which can be used to determine rendering heuristics. 80 | public enum NodeType: String, Decodable { 81 | /// The top-level node type. 82 | case document 83 | /// A block of text, the parent node for inline text nodes. 84 | case paragraph 85 | /// A string of text which may contain marks. 86 | case text 87 | /// A block with another Contentful entry embedded inside. 88 | case embeddedEntryBlock = "embedded-entry-block" 89 | /// A large heading. 90 | case h1 = "heading-1" 91 | /// A sub-heading. 92 | case h2 = "heading-2" 93 | 94 | internal var type: Node.Type { 95 | switch self { 96 | case .paragraph: 97 | return Paragraph.self 98 | case .text: 99 | return Text.self 100 | case .h1: 101 | return H1.self 102 | case .h2: 103 | return H2.self 104 | case .embeddedEntryBlock: 105 | return EmbeddedEntryBlock.self 106 | case .document: 107 | return Document.self 108 | } 109 | } 110 | } 111 | 112 | /// The top level node which contains all other nodes. 113 | public struct Document: BlockNode, Decodable { 114 | public let nodeType: NodeType 115 | public let nodeClass: NodeClass 116 | public let content: [Node] 117 | 118 | public init(from decoder: Decoder) throws { 119 | let container = try decoder.container(keyedBy: NodeContentCodingKeys.self) 120 | nodeType = try container.decode(NodeType.self, forKey: .nodeType) 121 | nodeClass = try container.decode(NodeClass.self, forKey: .nodeClass) 122 | content = try container.decodeContent(forKey: .content) 123 | } 124 | internal init(content: [Node]) { 125 | self.content = content 126 | nodeType = .document 127 | nodeClass = .document 128 | } 129 | } 130 | 131 | /// A block of text, containing child `Text` nodes. 132 | public struct Paragraph: BlockNode { 133 | public let nodeType: NodeType 134 | public let nodeClass: NodeClass 135 | public let content: [Node] 136 | 137 | public init(from decoder: Decoder) throws { 138 | let container = try decoder.container(keyedBy: NodeContentCodingKeys.self) 139 | nodeType = try container.decode(NodeType.self, forKey: .nodeType) 140 | nodeClass = try container.decode(NodeClass.self, forKey: .nodeClass) 141 | content = try container.decodeContent(forKey: .content) 142 | } 143 | } 144 | 145 | /// A heading for the document. 146 | public struct H1: BlockNode { 147 | public let nodeType: NodeType 148 | public let nodeClass: NodeClass 149 | public let content: [Node] 150 | 151 | public init(from decoder: Decoder) throws { 152 | let container = try decoder.container(keyedBy: NodeContentCodingKeys.self) 153 | nodeType = try container.decode(NodeType.self, forKey: .nodeType) 154 | nodeClass = try container.decode(NodeClass.self, forKey: .nodeClass) 155 | content = try container.decodeContent(forKey: .content) 156 | } 157 | } 158 | 159 | /// A sub-heading. 160 | public struct H2: BlockNode { 161 | public let nodeType: NodeType 162 | public let nodeClass: NodeClass 163 | public let content: [Node] 164 | 165 | public init(from decoder: Decoder) throws { 166 | let container = try decoder.container(keyedBy: NodeContentCodingKeys.self) 167 | nodeType = try container.decode(NodeType.self, forKey: .nodeType) 168 | nodeClass = try container.decode(NodeClass.self, forKey: .nodeClass) 169 | content = try container.decodeContent(forKey: .content) 170 | } 171 | } 172 | 173 | /// A block containing data for a linked entry. 174 | public class EmbeddedEntryBlock: EmbeddedResourceNode { 175 | public let nodeType: NodeType 176 | public let nodeClass: NodeClass 177 | public let data: EmbeddedResourceData 178 | 179 | public required init(from decoder: Decoder) throws { 180 | let container = try decoder.container(keyedBy: NodeContentCodingKeys.self) 181 | nodeType = try container.decode(NodeType.self, forKey: .nodeType) 182 | nodeClass = try container.decode(NodeClass.self, forKey: .nodeClass) 183 | data = try container.decode(EmbeddedResourceData.self, forKey: .data) 184 | } 185 | 186 | internal init(resolvedData: EmbeddedResourceData) { 187 | nodeClass = .block 188 | nodeType = .embeddedEntryBlock 189 | data = resolvedData 190 | } 191 | } 192 | 193 | /// A node containing text with marks. 194 | public struct Text: InlineNode { 195 | public let nodeType: NodeType 196 | public let nodeClass: NodeClass 197 | /// The string value of the text. 198 | public let value: String 199 | /// An array of the markup styles which should be applied to the text. 200 | public let marks: [Mark] 201 | 202 | /// THe markup styling which should be applied to the text. 203 | public struct Mark: Decodable { 204 | public let type: MarkType 205 | } 206 | 207 | /// A type of the markup styling which should be applied to the text. 208 | public enum MarkType: String, Decodable { 209 | /// Bold text. 210 | case bold 211 | /// Italicized text. 212 | case italic 213 | /// Underlined text. 214 | case underline 215 | /// Text formatted as code; presumably with monospaced font. 216 | case code 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Error.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Error.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 29/09/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Possible errors being thrown by the SDK 12 | public enum SDKError: Error { 13 | /// Thrown when no valid client is available during sync 14 | case invalidClient() 15 | 16 | /** 17 | Thrown when receiving an invalid HTTP response. 18 | - Parameter response: Optional URL response that has triggered the error. 19 | */ 20 | case invalidHTTPResponse(response: URLResponse?) 21 | 22 | /** 23 | Thrown when attempting to construct an invalid URL. 24 | - Parameter string: The invalid URL string. 25 | */ 26 | case invalidURL(string: String) 27 | 28 | /// Thrown if the subsequent sync operations are executed in preview mode. 29 | case previewAPIDoesNotSupportSync() 30 | 31 | /// Thrown if a resource returned in the multi-locale format does not have any value for the given 32 | /// fallback chain. 33 | case noValuePresent(fieldKey: CodingKey) 34 | 35 | /** 36 | Thrown when receiving unparseable JSON responses. 37 | - Parameter data: The data being parsed. 38 | - Parameter errorMessage: The message from the error which occured during parsing. 39 | */ 40 | case unparseableJSON(data: Data?, errorMessage: String) 41 | 42 | /// Thrown when no entry is found matching a specified Entry id 43 | case noEntryFoundFor(id: String) 44 | 45 | /// Thrown when no resource is found matching a specified id 46 | case noResourceFoundFor(id: String) 47 | 48 | /// Thrown when a `Foundation.Data` object is unable to be transformed to a `UIImage` or an `NSImage` object. 49 | case unableToDecodeImageData 50 | 51 | /** 52 | Thrown when the SDK has issues mapping responses with the necessary locale information. 53 | - Parameter message: The message from the erorr which occured during parsing. 54 | */ 55 | case localeHandlingError(message: String) 56 | } 57 | 58 | /// Errors thrown for queries which have invalid construction. 59 | public enum QueryError: Error, CustomDebugStringConvertible { 60 | 61 | public var debugDescription: String { 62 | return message 63 | } 64 | 65 | internal var message: String { 66 | switch self { 67 | case .invalidSelection(let fieldKeyPath): 68 | return "Selection for \(fieldKeyPath) is invalid. Make sure it has at most 1 '.' character in it." 69 | case .maxSelectionLimitExceeded: 70 | return "Can select at most 99 key paths when using the select operator on a content type." 71 | case .invalidOrderProperty: 72 | return "Either 'sys' or 'fields' properties must be specified. Prefix your propety name with 'fields.' or 'sys.'." 73 | case .textSearchTooShort: 74 | return "Full text search must have a string with more than 1 character." 75 | } 76 | } 77 | 78 | /// Thrown if the query string for a full-text search query only has less than 2 characters. 79 | case textSearchTooShort 80 | 81 | /// Thrown when attempting to order query results with a property that is not prefixed with "fields." or "sys.". 82 | case invalidOrderProperty 83 | 84 | /// Thrown when a selection for the `select` operator is constructed in a way that is invalid. 85 | case invalidSelection(fieldKeyPath: String) 86 | 87 | /// Thrown when over 99 properties have been selected. The CDA only supports 100 selections 88 | /// and the SDK always includes "sys" as one of them. 89 | case maxSelectionLimitExceeded 90 | } 91 | 92 | 93 | /// Information regarding an error received from Contentful's API. 94 | public class APIError: Decodable, Error, CustomDebugStringConvertible { 95 | 96 | public var debugDescription: String { 97 | let statusCodeString = "HTTP status code " + String(statusCode) 98 | let detailsStrings = details?.errors.compactMap({ $0.details }).joined(separator: "\n") ?? "" 99 | let debugDescription = 100 | """ 101 | \(statusCodeString): \(message!) 102 | \(detailsStrings). 103 | Contentful Request ID: \(requestId!) 104 | """ 105 | return debugDescription 106 | } 107 | 108 | /// System fields for the error. 109 | public struct Sys: Decodable { 110 | /// The identifier for the error. 111 | let id: String 112 | /// The type of the error. 113 | let type: String 114 | } 115 | 116 | /// System fields. 117 | public let sys: Sys 118 | 119 | /// Human readable error message. 120 | public let message: String! 121 | 122 | /// The identifier of the request, can be useful when making support requests. 123 | public let requestId: String! 124 | 125 | /// The HTTP status code. 126 | public var statusCode: Int! 127 | 128 | /// More details about the error. 129 | public let details: Details? 130 | 131 | /// A lightweight struct describing other details about the error. 132 | public struct Details: Decodable { 133 | 134 | /// All the errors, enumerated. 135 | public let errors: [Details.Error] 136 | 137 | /// All the errors, enumerated. 138 | public struct Error: Decodable { 139 | /// The `name` property of the error. 140 | public let name: String 141 | /// The `path` property of the error. 142 | public let path: [String] 143 | /// The `details` property of the error. 144 | public let details: String? 145 | } 146 | } 147 | 148 | /// A psuedo identifier for the error returned by the API(s). 149 | /// "BadRequest", "InvalidQuery" and "InvalidEntry" are all examples. 150 | public var id: String { 151 | return sys.id 152 | } 153 | 154 | /// Resource type ("Error"). 155 | public var type: String { 156 | return sys.type 157 | } 158 | 159 | public required init(from decoder: Decoder) throws { 160 | let container = try decoder.container(keyedBy: CodingKeys.self) 161 | sys = try container.decode(Sys.self, forKey: .sys) 162 | message = try container.decodeIfPresent(String.self, forKey: .message) 163 | requestId = try container.decodeIfPresent(String.self, forKey: .requestId) 164 | details = try container.decodeIfPresent(Details.self, forKey: .details) 165 | } 166 | 167 | /** 168 | * API Errors from the Contentful Delivery API are special cased for JSON deserialization: 169 | * Rather than throw an error and trigger a Swift error breakpoint in Xcode, 170 | * we use failable initializers so that consumers don't experience error breakpoints when 171 | * no error was returned from the API. 172 | */ 173 | internal static func error(with decoder: JSONDecoder, data: Data, statusCode: Int) -> APIError? { 174 | if let error = try? decoder.decode(APIError.self, from: data) { 175 | // An error must have these things. 176 | guard error.message != nil && error.requestId != nil else { 177 | return nil 178 | } 179 | error.statusCode = statusCode 180 | return error 181 | } 182 | return nil 183 | } 184 | 185 | private enum CodingKeys: String, CodingKey { 186 | case sys, message, requestId, details 187 | } 188 | } 189 | 190 | /** 191 | For requests that do hit the Contentful Delivery API enforces rate limits of 78 requests per second 192 | and 280800 requests per hour by default. Higher rate limits may apply depending on your current plan. 193 | */ 194 | public final class RateLimitError: APIError { 195 | /** 196 | An integer specifying the time before one of the two limits resets and another request 197 | to the API will be accepted. If the client is rate limited per second, the header will return 1, 198 | which means the next second. If the client is rate limited per hour, the next reset will be 199 | determined like this: Every request which was made in the last hour gets counted in one of four 200 | 15 minute buckets. Every time a request comes in, the API calculates how many seconds remain 201 | until the sum of all bucket counts will be below the hourly limit. 202 | See [the API Rate Limit docs](https://www.contentful.com/developers/docs/references/content-delivery-api/#/introduction/api-rate-limits) 203 | for more information. 204 | */ 205 | public internal(set) var timeBeforeLimitReset: Int? 206 | 207 | /// A textual representation of this instance, suitable for debugging. 208 | override public var debugDescription: String { 209 | let debugDescription = super.debugDescription 210 | let timeInfoString = ( timeBeforeLimitReset == nil ? "" : "Wait " + String(timeBeforeLimitReset!)) + " seconds before making more requests." 211 | return """ 212 | \(debugDescription) 213 | \(timeInfoString) 214 | """ 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Resource.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Resource.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 18/08/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | /// Protocol for resources inside Contentful 12 | public protocol Resource { 13 | 14 | /// System fields 15 | var sys: Sys { get } 16 | } 17 | 18 | /// A protocol signifying that a resource's Sys property are accessible for lookup from the top level. 19 | public protocol FlatResource { 20 | /// The unique identifier of the Resource. 21 | var id: String { get } 22 | 23 | /// The date representing the last time the Contentful Resource was updated. 24 | var updatedAt: Date? { get } 25 | 26 | /// The date that the Contentful Resource was first created. 27 | var createdAt: Date? { get } 28 | 29 | /// The code which represents which locale the Resource of interest contains data for. 30 | var localeCode: String? { get } 31 | } 32 | 33 | public extension FlatResource where Self: Resource { 34 | public var id: String { 35 | return sys.id 36 | } 37 | 38 | public var type: String { 39 | return sys.type 40 | } 41 | 42 | public var updatedAt: Date? { 43 | return sys.updatedAt 44 | } 45 | 46 | public var createdAt: Date? { 47 | return sys.createdAt 48 | } 49 | 50 | public var localeCode: String? { 51 | return sys.locale 52 | } 53 | } 54 | 55 | /// A protocol enabling strongly typed queries to the Contentful Delivery API via the SDK. 56 | public protocol FieldKeysQueryable { 57 | 58 | /// The CodingKey representing the names of each of the fields for the corresponding content type. 59 | /// These coding keys should be the same as those used when implementing Decodable. 60 | associatedtype FieldKeys: CodingKey 61 | } 62 | 63 | 64 | /// Classes conforming to this protocol are accessible via an `Endpoint`. 65 | public protocol EndpointAccessible { 66 | static var endpoint: Endpoint { get } 67 | } 68 | 69 | /// Entities conforming to this protocol have a QueryType that the SDK can use to make generic fetch requests. 70 | public protocol ResourceQueryable { 71 | 72 | associatedtype QueryType: AbstractQuery 73 | } 74 | 75 | public typealias ContentTypeId = String 76 | 77 | 78 | /// Class to represent the information describing a resource that has been deleted from Contentful. 79 | public class DeletedResource: Resource, FlatResource, Decodable { 80 | 81 | public let sys: Sys 82 | 83 | init(sys: Sys) { 84 | self.sys = sys 85 | } 86 | } 87 | 88 | /** 89 | LocalizableResource 90 | 91 | Base class for any Resource that has the capability of carrying information for multiple locales. 92 | If more than one locale is fetched using either `/sync` endpoint, or specifying the wildcard value 93 | for the locale paramater (i.e ["locale": "*"]) during a fetch, the SDK will cache returned values for 94 | all locales. This class gives an interface to specify which locale should be used when fetching data 95 | from `Resource` instances that are in memory. 96 | */ 97 | public class LocalizableResource: Resource, FlatResource, Decodable { 98 | 99 | /// System fields 100 | public let sys: Sys 101 | 102 | /// Currently selected locale to use when reading data from the `fields` dictionary. 103 | public var currentlySelectedLocale: Locale 104 | 105 | /** 106 | Content fields. If there is no value for a field associated with the currently selected `Locale`, 107 | the SDK will walk down fallback chain until a value is found. If there is still no value after 108 | walking the full chain, the field will be omitted from the `fields` dictionary. 109 | */ 110 | public var fields: [FieldName: Any] { 111 | return Localization.fields(forLocale: currentlySelectedLocale, localizableFields: localizableFields, localizationContext: localizationContext) 112 | } 113 | 114 | /** Set's the locale on the Localizable Resource (i.e. an instance of `Asset` or `Entry`) 115 | so that future reads from the `fields` property will return data corresponding 116 | to the specified locale code. 117 | */ 118 | @discardableResult public func setLocale(withCode code: LocaleCode) -> Bool { 119 | guard let newLocale = localizationContext.locales[code] else { 120 | return false 121 | } 122 | currentlySelectedLocale = newLocale 123 | return true 124 | } 125 | 126 | // Locale to Field mapping. 127 | internal var localizableFields: [FieldName: [LocaleCode: Any]] 128 | 129 | // Context used for handling locales during decoding of `Asset` and `Entry` instances. 130 | internal let localizationContext: LocalizationContext 131 | 132 | public required init(from decoder: Decoder) throws { 133 | 134 | let container = try decoder.container(keyedBy: CodingKeys.self) 135 | let sys = try container.decode(Sys.self, forKey: .sys) 136 | 137 | guard let localizationContext = decoder.userInfo[.localizationContextKey] as? LocalizationContext else { 138 | throw SDKError.localeHandlingError(message: """ 139 | SDK failed to find the necessary LocalizationContext 140 | necessary to properly map API responses to internal format. 141 | """ 142 | ) 143 | } 144 | 145 | self.localizationContext = localizationContext 146 | // Get currently selected locale. 147 | if let localeCode = sys.locale, let locale = localizationContext.locales[localeCode] { 148 | currentlySelectedLocale = locale 149 | } else { 150 | currentlySelectedLocale = localizationContext.default 151 | } 152 | self.sys = sys 153 | 154 | let fieldsDictionary = try container.decode(Dictionary.self, forKey: .fields) 155 | localizableFields = try Localization.fieldsInMultiLocaleFormat(from: fieldsDictionary, 156 | selectedLocale: currentlySelectedLocale, 157 | wasSelectedOnAPILevel: sys.locale != nil) 158 | } 159 | 160 | /// The keys used when representing a resource in JSON. 161 | public enum CodingKeys: String, CodingKey { 162 | /// The JSON key for the sys object. 163 | case sys 164 | /// The JSON key for the fields object. 165 | case fields 166 | } 167 | } 168 | 169 | 170 | /// Convenience methods for reading from dictionaries without conditional casts. 171 | public extension Dictionary where Key: ExpressibleByStringLiteral { 172 | 173 | /** 174 | Extract the String at the specified fieldName. 175 | 176 | - Parameter key: The name of the field to extract the `String` from 177 | - Returns: The `String` value, or `nil` if data contained is not convertible to a `String`. 178 | */ 179 | public func string(at key: Key) -> String? { 180 | return self[key] as? String 181 | } 182 | 183 | /** 184 | Extract the array of `String` at the specified fieldName. 185 | 186 | - Parameter key: The name of the field to extract the `[String]` from 187 | - Returns: The `[String]`, or nil if data contained is not convertible to an `[String]`. 188 | */ 189 | public func strings(at key: Key) -> [String]? { 190 | return self[key] as? [String] 191 | } 192 | 193 | /** 194 | Extract the `Int` at the specified fieldName. 195 | 196 | - Parameter key: The name of the field to extract the `Int` value from. 197 | - Returns: The `Int` value, or `nil` if data contained is not convertible to an `Int`. 198 | */ 199 | public func int(at key: Key) -> Int? { 200 | return self[key] as? Int 201 | } 202 | 203 | /** 204 | Extract the `Date` at the specified fieldName. 205 | 206 | - Parameter key: The name of the field to extract the `Date` value from. 207 | - Returns: The `Date` value, or `nil` if data contained is not convertible to a `Date`. 208 | */ 209 | public func int(at key: Key) -> Date? { 210 | let dateString = self[key] as? String 211 | let date = dateString?.iso8601StringDate 212 | return date 213 | } 214 | 215 | /** 216 | Extract the `Entry` at the specified fieldName. 217 | 218 | - Parameter key: The name of the field to extract the `Entry` from. 219 | - Returns: The `Entry` value, or `nil` if data contained does not have contain a Link referencing an `Entry`. 220 | */ 221 | public func linkedEntry(at key: Key) -> Entry? { 222 | let link = self[key] as? Link 223 | let entry = link?.entry 224 | return entry 225 | } 226 | 227 | /** 228 | Extract the `Asset` at the specified fieldName. 229 | 230 | - Parameter key: The name of the field to extract the `Asset` from. 231 | - Returns: The `Asset` value, or `nil` if data contained does not have contain a Link referencing an `Asset`. 232 | */ 233 | public func linkedAsset(at key: Key) -> Asset? { 234 | let link = self[key] as? Link 235 | let asset = link?.asset 236 | return asset 237 | } 238 | 239 | /** 240 | Extract the `[Entry]` at the specified fieldName. 241 | 242 | - Parameter key: The name of the field to extract the `[Entry]` from. 243 | - Returns: The `[Entry]` value, or `nil` if data contained does not have contain a Link referencing an `Entry`. 244 | */ 245 | public func linkedEntries(at key: Key) -> [Entry]? { 246 | let links = self[key] as? [Link] 247 | let entries = links?.compactMap { $0.entry } 248 | return entries 249 | } 250 | 251 | /** 252 | Extract the `[Asset]` at the specified fieldName. 253 | 254 | - Parameter key: The name of the field to extract the `[Asset]` from. 255 | - Returns: The `[Asset]` value, or `nil` if data contained does not have contain a Link referencing an `[Asset]`. 256 | */ 257 | public func linkedAssets(at key: Key) -> [Asset]? { 258 | let links = self[key] as? [Link] 259 | let assets = links?.compactMap { $0.asset } 260 | return assets 261 | } 262 | 263 | /** 264 | Extract the `CLLocationCoordinate2D` at the specified fieldName. 265 | 266 | - Parameter key: The name of the field to extract the `CLLocationCoordinate2D` value from. 267 | - Returns: The `Bool` value, or `nil` if data contained is not convertible to a `Bool`. 268 | */ 269 | public func bool(at key: Key) -> Bool? { 270 | return self[key] as? Bool 271 | } 272 | 273 | /** 274 | Extract the `Bool` at the specified fieldName. 275 | 276 | - Parameter key: The name of the field to extract the `Bool` value from. 277 | - Returns: The `Bool` value, or `nil` if data contained is not convertible to a `Bool`. 278 | */ 279 | public func location(at key: Key) -> Location? { 280 | let coordinateJSON = self[key] as? [String: Any] 281 | guard let longitude = coordinateJSON?["lon"] as? Double else { return nil } 282 | guard let latitude = coordinateJSON?["lat"] as? Double else { return nil } 283 | let location = Location(latitude: latitude, longitude: longitude) 284 | return location 285 | } 286 | } 287 | 288 | // MARK: Internal 289 | 290 | extension LocalizableResource: Hashable { 291 | 292 | public var hashValue: Int { 293 | return id.hashValue 294 | } 295 | } 296 | 297 | /// Equatable implementation for `LocalizableResource` 298 | public func == (lhs: LocalizableResource, rhs: LocalizableResource) -> Bool { 299 | return lhs.id == rhs.id && lhs.sys.updatedAt == rhs.sys.updatedAt 300 | } 301 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/Decodable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Decodable.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 05.09.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | 12 | /// Helper methods for decoding instances of the various types in your content model. 13 | public extension Decoder { 14 | 15 | internal var linkResolver: LinkResolver { 16 | return userInfo[.linkResolverContextKey] as! LinkResolver 17 | } 18 | 19 | /// The `TimeZone` the Decoder is using to offset dates by. 20 | /// Set through `ClientConfiguration`. 21 | public var timeZone: TimeZone? { 22 | return userInfo[.timeZoneContextKey] as? TimeZone 23 | } 24 | 25 | internal var contentTypes: [ContentTypeId: EntryDecodable.Type] { 26 | guard let contentTypes = userInfo[.contentTypesContextKey] as? [ContentTypeId: EntryDecodable.Type] else { 27 | fatalError( 28 | """ 29 | Make sure to pass your content types into the `Client` intializer 30 | so the SDK can properly deserializer your own types if you are using the `fetchMappedEntries` methods 31 | """) 32 | } 33 | return contentTypes 34 | } 35 | 36 | /// The localization context of the connected Contentful space necessary to properly serialize 37 | /// entries and assets to Swift models from Contentful API responses. 38 | public var localizationContext: LocalizationContext { 39 | return userInfo[.localizationContextKey] as! LocalizationContext 40 | } 41 | 42 | /// Helper method to extract the sys property of a Contentful resource. 43 | public func sys() throws -> Sys { 44 | let container = try self.container(keyedBy: LocalizableResource.CodingKeys.self) 45 | let sys = try container.decode(Sys.self, forKey: .sys) 46 | return sys 47 | } 48 | 49 | /// Extract the nested JSON container for the "fields" dictionary present in Entry and Asset resources. 50 | public func contentfulFieldsContainer(keyedBy keyType: NestedKey.Type) throws -> KeyedDecodingContainer { 51 | let container = try self.container(keyedBy: LocalizableResource.CodingKeys.self) 52 | let fieldsContainer = try container.nestedContainer(keyedBy: keyType, forKey: .fields) 53 | return fieldsContainer 54 | } 55 | } 56 | 57 | extension JSONDecoder { 58 | 59 | /** 60 | Returns the JSONDecoder owned by the Client. Until the first request to the CDA is made, this 61 | decoder won't have the necessary localization content required to properly deserialize resources 62 | returned in the multi-locale format. 63 | */ 64 | public static func withoutLocalizationContext() -> JSONDecoder { 65 | let jsonDecoder = JSONDecoder() 66 | jsonDecoder.dateDecodingStrategy = .custom(Date.variableISO8601Strategy) 67 | return jsonDecoder 68 | } 69 | 70 | /** 71 | Updates the JSONDecoder provided by the client with the localization context necessary to deserialize 72 | resources returned in the multi-locale format with the locale information provided by the space. 73 | */ 74 | public func update(with localizationContext: LocalizationContext) { 75 | userInfo[.localizationContextKey] = localizationContext 76 | } 77 | } 78 | 79 | internal extension Decodable where Self: EntryDecodable { 80 | // This is a magic workaround for the fact that dynamic metatypes cannot be passed into 81 | // initializers such as UnkeyedDecodingContainer.decode(Decodable.Type), yet static methods CAN 82 | // be called on metatypes. 83 | static func popEntryDecodable(from container: inout UnkeyedDecodingContainer) throws -> Self { 84 | let entryDecodable = try container.decode(self) 85 | return entryDecodable 86 | } 87 | } 88 | 89 | internal extension Decodable where Self: AssetDecodable { 90 | static func popAssetDecodable(from container: inout UnkeyedDecodingContainer) throws -> Self { 91 | let assetDecodable = try container.decode(self) 92 | return assetDecodable 93 | } 94 | } 95 | 96 | internal extension Decodable where Self: Node { 97 | static func popNodeDecodable(from container: inout UnkeyedDecodingContainer) throws -> Self { 98 | let contentDecodable = try container.decode(self) 99 | return contentDecodable 100 | } 101 | } 102 | 103 | internal extension CodingUserInfoKey { 104 | internal static let linkResolverContextKey = CodingUserInfoKey(rawValue: "linkResolverContext")! 105 | internal static let timeZoneContextKey = CodingUserInfoKey(rawValue: "timeZoneContext")! 106 | internal static let contentTypesContextKey = CodingUserInfoKey(rawValue: "contentTypesContext")! 107 | internal static let localizationContextKey = CodingUserInfoKey(rawValue: "localizationContext")! 108 | } 109 | 110 | // Fields JSON container. 111 | public extension KeyedDecodingContainer { 112 | 113 | /** 114 | Caches a link to be resolved once all resources in the response have been serialized. 115 | 116 | - Parameter key: The KeyedDecodingContainer.Key representing the JSON key were the related resource is found 117 | - Parameter localeCode: The locale of the link source to be used when caching the relationship for future resolving 118 | - Parameter decoder: The Decoder being used to deserialize the JSON to a user-defined class 119 | - Parameter callback: The callback used to assign the linked item at a later time. 120 | - Throws: Forwards the error if no link object is in the JSON at the specified key. 121 | */ 122 | public func resolveLink(forKey key: KeyedDecodingContainer.Key, 123 | decoder: Decoder, 124 | callback: @escaping (AnyObject) -> Void) throws { 125 | 126 | let linkResolver = decoder.linkResolver 127 | if let link = try decodeIfPresent(Link.self, forKey: key) { 128 | linkResolver.resolve(link, callback: callback) 129 | } 130 | } 131 | 132 | /** 133 | Caches an array of linked entries to be resolved once all resources in the response have been serialized. 134 | 135 | - Parameter key: The KeyedDecodingContainer.Key representing the JSON key were the related resources arem found 136 | - Parameter localeCode: The locale of the link source to be used when caching the relationship for future resolving 137 | - Parameter decoder: The Decoder being used to deserialize the JSON to a user-defined class 138 | - Parameter callback: The callback used to assign the linked item at a later time. 139 | - Throws: Forwards the error if no link object is in the JSON at the specified key. 140 | */ 141 | public func resolveLinksArray(forKey key: KeyedDecodingContainer.Key, 142 | decoder: Decoder, 143 | callback: @escaping (AnyObject) -> Void) throws { 144 | 145 | let linkResolver = decoder.linkResolver 146 | if let links = try decodeIfPresent(Array.self, forKey: key) { 147 | linkResolver.resolve(links, callback: callback) 148 | } 149 | } 150 | } 151 | 152 | internal class LinkResolver { 153 | 154 | private var dataCache: DataCache = DataCache() 155 | 156 | private var callbacks: [String: [(AnyObject) -> Void]] = [:] 157 | 158 | private static let linksArrayPrefix = "linksArrayPrefix" 159 | 160 | internal func cache(assets: [Asset]) { 161 | for asset in assets { 162 | dataCache.add(asset: asset) 163 | } 164 | } 165 | 166 | internal func cache(entryDecodables: [EntryDecodable]) { 167 | for entryDecodable in entryDecodables { 168 | dataCache.add(entry: entryDecodable) 169 | } 170 | } 171 | 172 | // Caches the callback to resolve the relationship represented by a Link at a later time. 173 | internal func resolve(_ link: Link, callback: @escaping (AnyObject) -> Void) { 174 | let key = DataCache.cacheKey(for: link) 175 | // Swift 4 API enables setting a default value, if none exists for the given key. 176 | callbacks[key, default: []] += [callback] 177 | } 178 | 179 | internal func resolve(_ links: [Link], callback: @escaping (AnyObject) -> Void) { 180 | let linksIdentifier: String = links.reduce(into: LinkResolver.linksArrayPrefix) { (id, link) in 181 | id += "," + DataCache.cacheKey(for: link) 182 | } 183 | callbacks[linksIdentifier, default: []] += [callback] 184 | } 185 | 186 | // Executes all cached callbacks to resolve links and then clears the callback cache and the data cache 187 | // where resources are cached before being resolved. 188 | internal func churnLinks() { 189 | for (linkKey, callbacksList) in callbacks { 190 | if linkKey.hasPrefix(LinkResolver.linksArrayPrefix) { 191 | let firstKeyIndex = linkKey.index(linkKey.startIndex, offsetBy: LinkResolver.linksArrayPrefix.count) 192 | let onlyKeysString = linkKey[firstKeyIndex ..< linkKey.endIndex] 193 | // Split creates a [Substring] array, but we need [String] to index the cache 194 | let keys = onlyKeysString.split(separator: ",").map { String($0) } 195 | let items: [AnyObject] = keys.compactMap { dataCache.item(for: $0) } 196 | for callback in callbacksList { 197 | callback(items as AnyObject) 198 | } 199 | } else { 200 | let item = dataCache.item(for: linkKey) 201 | for callback in callbacksList { 202 | callback(item as AnyObject) 203 | } 204 | } 205 | } 206 | self.callbacks = [:] 207 | self.dataCache = DataCache() 208 | } 209 | } 210 | 211 | 212 | // Inspired by https://gist.github.com/mbuchetics/c9bc6c22033014aa0c550d3b4324411a 213 | internal struct JSONCodingKeys: CodingKey { 214 | internal var stringValue: String 215 | 216 | internal init?(stringValue: String) { 217 | self.stringValue = stringValue 218 | } 219 | 220 | internal var intValue: Int? 221 | 222 | internal init?(intValue: Int) { 223 | self.init(stringValue: "\(intValue)") 224 | self.intValue = intValue 225 | } 226 | } 227 | 228 | internal extension KeyedDecodingContainer { 229 | 230 | internal func decode(_ type: Dictionary.Type, forKey key: K) throws -> Dictionary { 231 | let container = try self.nestedContainer(keyedBy: JSONCodingKeys.self, forKey: key) 232 | return try container.decode(type) 233 | } 234 | 235 | internal func decodeIfPresent(_ type: Dictionary.Type, forKey key: K) throws -> Dictionary? { 236 | guard contains(key) else { return nil } 237 | guard try decodeNil(forKey: key) == false else { return nil } 238 | return try decode(type, forKey: key) 239 | } 240 | 241 | internal func decode(_ type: Array.Type, forKey key: K) throws -> Array { 242 | var container = try self.nestedUnkeyedContainer(forKey: key) 243 | return try container.decode(type) 244 | } 245 | 246 | internal func decodeIfPresent(_ type: Array.Type, forKey key: K) throws -> Array? { 247 | guard contains(key) else { return nil } 248 | guard try decodeNil(forKey: key) == false else { return nil } 249 | return try decode(type, forKey: key) 250 | } 251 | 252 | internal func decode(_ type: Dictionary.Type) throws -> Dictionary { 253 | var dictionary = Dictionary() 254 | 255 | for key in allKeys { 256 | if let boolValue = try? decode(Bool.self, forKey: key) { 257 | dictionary[key.stringValue] = boolValue 258 | } else if let stringValue = try? decode(String.self, forKey: key) { 259 | dictionary[key.stringValue] = stringValue 260 | } else if let intValue = try? decode(Int.self, forKey: key) { 261 | dictionary[key.stringValue] = intValue 262 | } else if let doubleValue = try? decode(Double.self, forKey: key) { 263 | dictionary[key.stringValue] = doubleValue 264 | } 265 | // Custom contentful types. 266 | else if let fileMetaData = try? decode(Asset.FileMetadata.self, forKey: key) { 267 | dictionary[key.stringValue] = fileMetaData 268 | } else if let link = try? decode(Link.self, forKey: key) { 269 | dictionary[key.stringValue] = link 270 | } else if let location = try? decode(Location.self, forKey: key) { 271 | dictionary[key.stringValue] = location 272 | } else if let document = try? decode(Document.self, forKey: key) { 273 | dictionary[key.stringValue] = document 274 | } 275 | // These must be called after attempting to decode all other custom types. 276 | else if let nestedDictionary = try? decode(Dictionary.self, forKey: key) { 277 | dictionary[key.stringValue] = nestedDictionary 278 | } else if let nestedArray = try? decode(Array.self, forKey: key) { 279 | dictionary[key.stringValue] = nestedArray 280 | } 281 | } 282 | return dictionary 283 | } 284 | } 285 | 286 | internal extension UnkeyedDecodingContainer { 287 | 288 | internal mutating func decode(_ type: Array.Type) throws -> Array { 289 | var array: [Any] = [] 290 | while isAtEnd == false { 291 | if try decodeNil() { 292 | continue 293 | } else if let value = try? decode(Bool.self) { 294 | array.append(value) 295 | } else if let value = try? decode(Int.self) { 296 | array.append(value) 297 | } else if let value = try? decode(Double.self) { 298 | array.append(value) 299 | } else if let value = try? decode(String.self) { 300 | array.append(value) 301 | } 302 | // Custom contentful types. 303 | else if let fileMetaData = try? decode(Asset.FileMetadata.self) { 304 | array.append(fileMetaData) // Custom contentful type. 305 | } else if let link = try? decode(Link.self) { 306 | array.append(link) // Custom contentful type. 307 | } 308 | // These must be called after attempting to decode all other custom types. 309 | else if let nestedDictionary = try? decode(Dictionary.self) { 310 | array.append(nestedDictionary) 311 | } else if let nestedArray = try? decode(Array.self) { 312 | array.append(nestedArray) 313 | } else if let location = try? decode(Location.self) { 314 | array.append(location) 315 | } 316 | 317 | } 318 | return array 319 | } 320 | 321 | internal mutating func decode(_ type: Dictionary.Type) throws -> Dictionary { 322 | 323 | let nestedContainer = try self.nestedContainer(keyedBy: JSONCodingKeys.self) 324 | return try nestedContainer.decode(type) 325 | } 326 | } 327 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/ArrayResponse.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ArrayResponse.swift 3 | // Contentful 4 | // 5 | // Created by Boris Bügling on 18/08/15. 6 | // Copyright © 2015 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | private protocol Array { 10 | 11 | var limit: UInt { get } 12 | 13 | var skip: UInt { get } 14 | 15 | var total: UInt { get } 16 | 17 | var errors: [ArrayResponseError]? { get } 18 | } 19 | 20 | internal enum ArrayCodingKeys: String, CodingKey { 21 | case items, includes, skip, limit, total, errors 22 | } 23 | 24 | private protocol HomogeneousArray: Array { 25 | 26 | associatedtype ItemType 27 | 28 | var items: [ItemType] { get } 29 | } 30 | 31 | /** 32 | Sometimes, when links are unresolvable (for instance, when a linked entry is not published), the API 33 | will return an array of errors, one for each unresolvable link. 34 | */ 35 | public struct ArrayResponseError: Decodable { 36 | /// The system fields of the error. 37 | public struct Sys: Decodable { 38 | let id: String 39 | let type: String 40 | } 41 | 42 | /// System fields for the unresolvable link. 43 | public let details: Link.Sys 44 | /// System fields describing the type of this object ("error") and the error message: generally "notResolvable". 45 | public let sys: ArrayResponseError.Sys 46 | } 47 | 48 | /** 49 | A list of resources in Contentful 50 | 51 | This is the result type for any request of a collection of resources. 52 | See: 53 | */ 54 | public struct ArrayResponse: HomogeneousArray where ItemType: Decodable & EndpointAccessible { 55 | 56 | /// The resources which are part of the given array 57 | public let items: [ItemType] 58 | 59 | /// The maximum number of resources originally requested 60 | public let limit: UInt 61 | 62 | /// The number of elements skipped when performing the request 63 | public let skip: UInt 64 | 65 | /// The total number of resources which matched the original request 66 | public let total: UInt 67 | 68 | /// An array of errors, or partial errors, which describe links which were returned in the response that 69 | /// cannot be resolved. 70 | public let errors: [ArrayResponseError]? 71 | 72 | internal let includes: Includes? 73 | internal let mappedIncludes: MappedIncludes? 74 | 75 | internal var includedAssets: [Asset]? { 76 | return includes?.assets 77 | } 78 | internal var includedEntries: [Entry]? { 79 | return includes?.entries 80 | } 81 | 82 | internal struct Includes: Decodable { 83 | let assets: [Asset]? 84 | let entries: [Entry]? 85 | 86 | private enum CodingKeys: String, CodingKey { 87 | case assets = "Asset" 88 | case entries = "Entry" 89 | } 90 | 91 | init(from decoder: Decoder) throws { 92 | let values = try decoder.container(keyedBy: CodingKeys.self) 93 | assets = try values.decodeIfPresent([Asset].self, forKey: .assets) 94 | entries = try values.decodeIfPresent([Entry].self, forKey: .entries) 95 | } 96 | } 97 | } 98 | 99 | extension ArrayResponse: Decodable { 100 | public init(from decoder: Decoder) throws { 101 | let container = try decoder.container(keyedBy: ArrayCodingKeys.self) 102 | 103 | skip = try container.decode(UInt.self, forKey: .skip) 104 | total = try container.decode(UInt.self, forKey: .total) 105 | limit = try container.decode(UInt.self, forKey: .limit) 106 | errors = try container.decodeIfPresent([ArrayResponseError].self, forKey: .errors) 107 | 108 | // First see if we can decode an array of user-defined types. 109 | if ItemType.self is EntryDecodable.Type { 110 | 111 | // All items and includes. 112 | includes = nil 113 | mappedIncludes = try container.decodeIfPresent(MappedIncludes.self, forKey: .includes) 114 | 115 | // A copy as an array of dictionaries just to extract "sys.type" field. 116 | guard let jsonItems = try container.decode(Swift.Array.self, forKey: .items) as? [[String: Any]] else { 117 | throw SDKError.unparseableJSON(data: nil, errorMessage: "SDK was unable to serialize returned resources") 118 | } 119 | var entriesJSONContainer = try container.nestedUnkeyedContainer(forKey: .items) 120 | var entries: [EntryDecodable] = [] 121 | let contentTypes = decoder.userInfo[.contentTypesContextKey] as! [ContentTypeId: EntryDecodable.Type] 122 | 123 | while !entriesJSONContainer.isAtEnd { 124 | guard let contentTypeInfo = jsonItems.contentTypeInfo(at: entriesJSONContainer.currentIndex) else { 125 | let errorMessage = "SDK was unable to parse sys.type property necessary to finish resource serialization." 126 | throw SDKError.unparseableJSON(data: nil, errorMessage: errorMessage) 127 | } 128 | 129 | // Throw an error in this case as if there is no matching content type for the current id, then 130 | // we can't serialize any of the entries. The type must match ItemType as this is a homogenous array. 131 | guard let entryDecodableType = contentTypes[contentTypeInfo.id], entryDecodableType == ItemType.self else { 132 | let errorMessage = """ 133 | A response for the QueryOn<\(ItemType.self)> did return successfully, but a serious error 134 | occurred when decoding the array of \(ItemType.self). 135 | """ 136 | throw SDKError.unparseableJSON(data: nil, errorMessage: errorMessage) 137 | } 138 | let entryDecodable = try entryDecodableType.popEntryDecodable(from: &entriesJSONContainer) 139 | entries.append(entryDecodable) 140 | } 141 | 142 | // Workaround for type system not allowing cast of items to [ItemType]. 143 | self.items = entries.compactMap { $0 as? ItemType } 144 | 145 | // Cache to enable link resolution. 146 | decoder.linkResolver.cache(entryDecodables: self.items as! [EntryDecodable]) 147 | 148 | // Resolve links. 149 | decoder.linkResolver.churnLinks() 150 | } else { 151 | mappedIncludes = nil 152 | includes = try container.decodeIfPresent(ArrayResponse.Includes.self, forKey: .includes) 153 | items = try container.decode([ItemType].self, forKey: .items) 154 | 155 | // If the ItemType was Entry, filter those entries so we can resolve their links. 156 | let entries: [Entry] = items.compactMap { $0 as? Entry } 157 | 158 | let allIncludedEntries = entries + (includedEntries ?? []) 159 | 160 | // Rememember `Entry`s are classes (passed by reference) so we can change them in place 161 | for entry in allIncludedEntries { 162 | entry.resolveLinks(against: allIncludedEntries, and: (includedAssets ?? [])) 163 | } 164 | } 165 | } 166 | fileprivate enum CodingKeys: String, CodingKey { 167 | case items, includes, skip, limit, total, errors 168 | } 169 | } 170 | 171 | internal struct MappedIncludes: Decodable { 172 | let assets: [Asset]? 173 | let entries: [EntryDecodable]? 174 | 175 | private enum CodingKeys: String, CodingKey { 176 | case assets = "Asset" 177 | case entries = "Entry" 178 | } 179 | 180 | init(from decoder: Decoder) throws { 181 | let container = try decoder.container(keyedBy: CodingKeys.self) 182 | assets = try container.decodeIfPresent([Asset].self, forKey: .assets) 183 | entries = try container.decodeHeterogeneousEntries(forKey: .entries, 184 | contentTypes: decoder.contentTypes, 185 | throwIfNotPresent: false) 186 | // Cache to enable link resolution. 187 | if let assets = assets { 188 | decoder.linkResolver.cache(assets: assets) 189 | } 190 | // Cache to enable link resolution. 191 | if let entries = entries { 192 | decoder.linkResolver.cache(entryDecodables: entries) 193 | } 194 | } 195 | } 196 | 197 | 198 | /** 199 | A list of Contentful entries that have been mapped to types conforming to `EntryDecodable` instances. 200 | A MixedArrayResponse respresents a heterogeneous collection of EntryDecodables being returned, 201 | for instance if hitting the base /entries endpoint with no additional query parameters. If there is no 202 | user-defined type for a particular entry, that entry will not be serialized at all. It is up to you to 203 | introspect the type of each element in the items array to handle the response data properly. 204 | 205 | See: 206 | */ 207 | public struct MixedArrayResponse: Array { 208 | 209 | /// The resources which are part of the given array 210 | public let items: [EntryDecodable] 211 | 212 | /// The maximum number of resources originally requested 213 | public let limit: UInt 214 | 215 | /// The number of elements skipped when performing the request 216 | public let skip: UInt 217 | 218 | /// The total number of resources which matched the original request 219 | public let total: UInt 220 | 221 | /// An array of errors, or partial errors, which describe links which were returned in the response that 222 | /// cannot be resolved. 223 | public let errors: [ArrayResponseError]? 224 | 225 | internal let includes: MappedIncludes? 226 | 227 | internal var includedAssets: [Asset]? { 228 | return includes?.assets 229 | } 230 | internal var includedEntries: [EntryDecodable]? { 231 | return includes?.entries 232 | } 233 | } 234 | 235 | extension MixedArrayResponse: Decodable { 236 | 237 | public init(from decoder: Decoder) throws { 238 | let container = try decoder.container(keyedBy: ArrayCodingKeys.self) 239 | skip = try container.decode(UInt.self, forKey: .skip) 240 | total = try container.decode(UInt.self, forKey: .total) 241 | limit = try container.decode(UInt.self, forKey: .limit) 242 | errors = try container.decodeIfPresent([ArrayResponseError].self, forKey: .errors) 243 | 244 | // All items and includes. 245 | includes = try container.decodeIfPresent(MappedIncludes.self, forKey: .includes) 246 | items = try container.decodeHeterogeneousEntries(forKey: .items, 247 | contentTypes: decoder.contentTypes, 248 | throwIfNotPresent: true) ?? [] 249 | 250 | // Cache to enable link resolution. 251 | decoder.linkResolver.cache(entryDecodables: self.items) 252 | 253 | // Resolve links. 254 | decoder.linkResolver.churnLinks() 255 | } 256 | } 257 | 258 | // Convenience method for grabbing the content type information of a json item in an array of resources. 259 | internal extension Swift.Array where Element == Dictionary { 260 | 261 | func contentTypeInfo(at index: Int) -> Link? { 262 | guard let sys = self[index]["sys"] as? [String: Any], let contentTypeInfo = sys["contentType"] as? Link else { 263 | return nil 264 | } 265 | return contentTypeInfo 266 | } 267 | 268 | func nodeTypeInfo(at index: Int) -> NodeType? { 269 | guard let nodeTypeString = self[index]["nodeType"] as? String, let nodeType = NodeType(rawValue: nodeTypeString) else { 270 | return nil 271 | } 272 | return nodeType 273 | } 274 | } 275 | 276 | // Empty type so that we can continue to the end of a UnkeyedContainer 277 | internal struct EmptyDecodable: Decodable {} 278 | 279 | extension KeyedDecodingContainer { 280 | 281 | internal func decodeHeterogeneousEntries(forKey key: K, 282 | contentTypes: [ContentTypeId: EntryDecodable.Type], 283 | throwIfNotPresent: Bool) throws -> [EntryDecodable]? { 284 | 285 | 286 | guard let itemsAsDictionaries = try self.decodeIfPresent(Swift.Array.self, forKey: key) as? [[String: Any]] else { 287 | if throwIfNotPresent { 288 | throw SDKError.unparseableJSON(data: nil, errorMessage: "SDK was unable to serialize returned resources") 289 | } else { 290 | return nil 291 | } 292 | } 293 | var entriesJSONContainer = try self.nestedUnkeyedContainer(forKey: key) 294 | 295 | var entries: [EntryDecodable] = [] 296 | while !entriesJSONContainer.isAtEnd { 297 | 298 | guard let contentTypeInfo = itemsAsDictionaries.contentTypeInfo(at: entriesJSONContainer.currentIndex) else { 299 | let errorMessage = "SDK was unable to parse sys.type property necessary to finish resource serialization." 300 | throw SDKError.unparseableJSON(data: nil, errorMessage: errorMessage) 301 | } 302 | 303 | // For includes, if the type of this entry isn't defined by the user, we skip serialization. 304 | if let type = contentTypes[contentTypeInfo.id] { 305 | let entryModellable = try type.popEntryDecodable(from: &entriesJSONContainer) 306 | entries.append(entryModellable) 307 | } else { 308 | // Another annoying workaround: there is no mechanism for incrementing the `currentIndex` of an 309 | // UnkeyedCodingContainer other than actually decoding an item 310 | _ = try? entriesJSONContainer.decode(EmptyDecodable.self) 311 | } 312 | } 313 | return entries 314 | } 315 | 316 | internal func decodeContent(forKey key: K) throws -> [Node] { 317 | 318 | // A copy as an array of dictionaries just to extract "nodeType" field. 319 | guard let jsonContent = try decode(Swift.Array.self, forKey: key) as? [[String: Any]] else { 320 | throw SDKError.unparseableJSON(data: nil, errorMessage: "SDK was unable to serialize returned resources") 321 | } 322 | 323 | var contentJSONContainer = try nestedUnkeyedContainer(forKey: key) 324 | var content: [Node] = [] 325 | 326 | while !contentJSONContainer.isAtEnd { 327 | guard let nodeType = jsonContent.nodeTypeInfo(at: contentJSONContainer.currentIndex) else { 328 | let errorMessage = "SDK was unable to parse nodeType property necessary to finish resource serialization." 329 | throw SDKError.unparseableJSON(data: nil, errorMessage: errorMessage) 330 | } 331 | let element = try nodeType.type.popNodeDecodable(from: &contentJSONContainer) 332 | content.append(element) 333 | } 334 | return content 335 | } 336 | } 337 | -------------------------------------------------------------------------------- /Boilerplate-Swift.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | DCD9ABE024974583C11DE013 /* Pods_Boilerplate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 13A41D658327E3AF1C5C5E30 /* Pods_Boilerplate.framework */; }; 11 | ED95DF511E40BA8800F2F8F9 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED95DF501E40BA8800F2F8F9 /* AppDelegate.swift */; }; 12 | ED95DF531E40BA8800F2F8F9 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED95DF521E40BA8800F2F8F9 /* ViewController.swift */; }; 13 | ED95DF581E40BA8800F2F8F9 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = ED95DF571E40BA8800F2F8F9 /* Assets.xcassets */; }; 14 | ED95DF5B1E40BA8800F2F8F9 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = ED95DF591E40BA8800F2F8F9 /* LaunchScreen.storyboard */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXFileReference section */ 18 | 0463465793B536EF70F077F6 /* Pods-Boilerplate.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Boilerplate.release.xcconfig"; path = "Target Support Files/Pods-Boilerplate/Pods-Boilerplate.release.xcconfig"; sourceTree = ""; }; 19 | 13A41D658327E3AF1C5C5E30 /* Pods_Boilerplate.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Boilerplate.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 20 | 8DA66C9CD0AAA3E7C55CE751 /* Pods-Boilerplate.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Boilerplate.debug.xcconfig"; path = "Target Support Files/Pods-Boilerplate/Pods-Boilerplate.debug.xcconfig"; sourceTree = ""; }; 21 | ED95DF4E1E40BA8800F2F8F9 /* Boilerplate.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Boilerplate.app; sourceTree = BUILT_PRODUCTS_DIR; }; 22 | ED95DF501E40BA8800F2F8F9 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 23 | ED95DF521E40BA8800F2F8F9 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; 24 | ED95DF571E40BA8800F2F8F9 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 25 | ED95DF5A1E40BA8800F2F8F9 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 26 | ED95DF5C1E40BA8800F2F8F9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 27 | /* End PBXFileReference section */ 28 | 29 | /* Begin PBXFrameworksBuildPhase section */ 30 | ED95DF4B1E40BA8800F2F8F9 /* Frameworks */ = { 31 | isa = PBXFrameworksBuildPhase; 32 | buildActionMask = 2147483647; 33 | files = ( 34 | DCD9ABE024974583C11DE013 /* Pods_Boilerplate.framework in Frameworks */, 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | 058F1DD3438058E16EF3D616 /* Frameworks */ = { 42 | isa = PBXGroup; 43 | children = ( 44 | 13A41D658327E3AF1C5C5E30 /* Pods_Boilerplate.framework */, 45 | ); 46 | name = Frameworks; 47 | sourceTree = ""; 48 | }; 49 | 23F494B8B4E777E1C434DAF2 /* Pods */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | 8DA66C9CD0AAA3E7C55CE751 /* Pods-Boilerplate.debug.xcconfig */, 53 | 0463465793B536EF70F077F6 /* Pods-Boilerplate.release.xcconfig */, 54 | ); 55 | path = Pods; 56 | sourceTree = ""; 57 | }; 58 | ED2FE21B1E3B81A100B7B658 = { 59 | isa = PBXGroup; 60 | children = ( 61 | ED95DF4F1E40BA8800F2F8F9 /* Boilerplate */, 62 | ED2FE2251E3B81A100B7B658 /* Products */, 63 | 23F494B8B4E777E1C434DAF2 /* Pods */, 64 | 058F1DD3438058E16EF3D616 /* Frameworks */, 65 | ); 66 | sourceTree = ""; 67 | }; 68 | ED2FE2251E3B81A100B7B658 /* Products */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | ED95DF4E1E40BA8800F2F8F9 /* Boilerplate.app */, 72 | ); 73 | name = Products; 74 | sourceTree = ""; 75 | }; 76 | ED95DF4F1E40BA8800F2F8F9 /* Boilerplate */ = { 77 | isa = PBXGroup; 78 | children = ( 79 | ED95DF501E40BA8800F2F8F9 /* AppDelegate.swift */, 80 | ED95DF521E40BA8800F2F8F9 /* ViewController.swift */, 81 | ED95DF571E40BA8800F2F8F9 /* Assets.xcassets */, 82 | ED95DF591E40BA8800F2F8F9 /* LaunchScreen.storyboard */, 83 | ED95DF5C1E40BA8800F2F8F9 /* Info.plist */, 84 | ); 85 | path = Boilerplate; 86 | sourceTree = ""; 87 | }; 88 | /* End PBXGroup section */ 89 | 90 | /* Begin PBXNativeTarget section */ 91 | ED95DF4D1E40BA8800F2F8F9 /* Boilerplate */ = { 92 | isa = PBXNativeTarget; 93 | buildConfigurationList = ED95DF5D1E40BA8800F2F8F9 /* Build configuration list for PBXNativeTarget "Boilerplate" */; 94 | buildPhases = ( 95 | 1F8AC1622E5265206CBFFD57 /* [CP] Check Pods Manifest.lock */, 96 | ED95DF4A1E40BA8800F2F8F9 /* Sources */, 97 | ED95DF4B1E40BA8800F2F8F9 /* Frameworks */, 98 | ED95DF4C1E40BA8800F2F8F9 /* Resources */, 99 | 7246A93DE6390D6C756992AD /* [CP] Embed Pods Frameworks */, 100 | ); 101 | buildRules = ( 102 | ); 103 | dependencies = ( 104 | ); 105 | name = Boilerplate; 106 | productName = Boilerplate; 107 | productReference = ED95DF4E1E40BA8800F2F8F9 /* Boilerplate.app */; 108 | productType = "com.apple.product-type.application"; 109 | }; 110 | /* End PBXNativeTarget section */ 111 | 112 | /* Begin PBXProject section */ 113 | ED2FE21C1E3B81A100B7B658 /* Project object */ = { 114 | isa = PBXProject; 115 | attributes = { 116 | LastSwiftUpdateCheck = 0820; 117 | LastUpgradeCheck = 0820; 118 | ORGANIZATIONNAME = Contentful; 119 | TargetAttributes = { 120 | ED95DF4D1E40BA8800F2F8F9 = { 121 | CreatedOnToolsVersion = 8.2.1; 122 | ProvisioningStyle = Automatic; 123 | }; 124 | }; 125 | }; 126 | buildConfigurationList = ED2FE21F1E3B81A100B7B658 /* Build configuration list for PBXProject "Boilerplate-Swift" */; 127 | compatibilityVersion = "Xcode 3.2"; 128 | developmentRegion = English; 129 | hasScannedForEncodings = 0; 130 | knownRegions = ( 131 | en, 132 | Base, 133 | ); 134 | mainGroup = ED2FE21B1E3B81A100B7B658; 135 | productRefGroup = ED2FE2251E3B81A100B7B658 /* Products */; 136 | projectDirPath = ""; 137 | projectRoot = ""; 138 | targets = ( 139 | ED95DF4D1E40BA8800F2F8F9 /* Boilerplate */, 140 | ); 141 | }; 142 | /* End PBXProject section */ 143 | 144 | /* Begin PBXResourcesBuildPhase section */ 145 | ED95DF4C1E40BA8800F2F8F9 /* Resources */ = { 146 | isa = PBXResourcesBuildPhase; 147 | buildActionMask = 2147483647; 148 | files = ( 149 | ED95DF5B1E40BA8800F2F8F9 /* LaunchScreen.storyboard in Resources */, 150 | ED95DF581E40BA8800F2F8F9 /* Assets.xcassets in Resources */, 151 | ); 152 | runOnlyForDeploymentPostprocessing = 0; 153 | }; 154 | /* End PBXResourcesBuildPhase section */ 155 | 156 | /* Begin PBXShellScriptBuildPhase section */ 157 | 1F8AC1622E5265206CBFFD57 /* [CP] Check Pods Manifest.lock */ = { 158 | isa = PBXShellScriptBuildPhase; 159 | buildActionMask = 2147483647; 160 | files = ( 161 | ); 162 | inputFileListPaths = ( 163 | ); 164 | inputPaths = ( 165 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 166 | "${PODS_ROOT}/Manifest.lock", 167 | ); 168 | name = "[CP] Check Pods Manifest.lock"; 169 | outputFileListPaths = ( 170 | ); 171 | outputPaths = ( 172 | "$(DERIVED_FILE_DIR)/Pods-Boilerplate-checkManifestLockResult.txt", 173 | ); 174 | runOnlyForDeploymentPostprocessing = 0; 175 | shellPath = /bin/sh; 176 | 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"; 177 | showEnvVarsInLog = 0; 178 | }; 179 | 7246A93DE6390D6C756992AD /* [CP] Embed Pods Frameworks */ = { 180 | isa = PBXShellScriptBuildPhase; 181 | buildActionMask = 2147483647; 182 | files = ( 183 | ); 184 | inputFileListPaths = ( 185 | ); 186 | inputPaths = ( 187 | "${PODS_ROOT}/Target Support Files/Pods-Boilerplate/Pods-Boilerplate-frameworks.sh", 188 | "${BUILT_PRODUCTS_DIR}/Contentful/Contentful.framework", 189 | ); 190 | name = "[CP] Embed Pods Frameworks"; 191 | outputFileListPaths = ( 192 | ); 193 | outputPaths = ( 194 | "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Contentful.framework", 195 | ); 196 | runOnlyForDeploymentPostprocessing = 0; 197 | shellPath = /bin/sh; 198 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Boilerplate/Pods-Boilerplate-frameworks.sh\"\n"; 199 | showEnvVarsInLog = 0; 200 | }; 201 | /* End PBXShellScriptBuildPhase section */ 202 | 203 | /* Begin PBXSourcesBuildPhase section */ 204 | ED95DF4A1E40BA8800F2F8F9 /* Sources */ = { 205 | isa = PBXSourcesBuildPhase; 206 | buildActionMask = 2147483647; 207 | files = ( 208 | ED95DF531E40BA8800F2F8F9 /* ViewController.swift in Sources */, 209 | ED95DF511E40BA8800F2F8F9 /* AppDelegate.swift in Sources */, 210 | ); 211 | runOnlyForDeploymentPostprocessing = 0; 212 | }; 213 | /* End PBXSourcesBuildPhase section */ 214 | 215 | /* Begin PBXVariantGroup section */ 216 | ED95DF591E40BA8800F2F8F9 /* LaunchScreen.storyboard */ = { 217 | isa = PBXVariantGroup; 218 | children = ( 219 | ED95DF5A1E40BA8800F2F8F9 /* Base */, 220 | ); 221 | name = LaunchScreen.storyboard; 222 | sourceTree = ""; 223 | }; 224 | /* End PBXVariantGroup section */ 225 | 226 | /* Begin XCBuildConfiguration section */ 227 | ED2FE2291E3B81A100B7B658 /* Debug */ = { 228 | isa = XCBuildConfiguration; 229 | buildSettings = { 230 | ALWAYS_SEARCH_USER_PATHS = NO; 231 | CLANG_ANALYZER_NONNULL = YES; 232 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 233 | CLANG_CXX_LIBRARY = "libc++"; 234 | CLANG_ENABLE_MODULES = YES; 235 | CLANG_ENABLE_OBJC_ARC = YES; 236 | CLANG_WARN_BOOL_CONVERSION = YES; 237 | CLANG_WARN_CONSTANT_CONVERSION = YES; 238 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 239 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 240 | CLANG_WARN_EMPTY_BODY = YES; 241 | CLANG_WARN_ENUM_CONVERSION = YES; 242 | CLANG_WARN_INFINITE_RECURSION = YES; 243 | CLANG_WARN_INT_CONVERSION = YES; 244 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 245 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 246 | CLANG_WARN_UNREACHABLE_CODE = YES; 247 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 248 | CODE_SIGN_IDENTITY = "-"; 249 | COPY_PHASE_STRIP = NO; 250 | DEBUG_INFORMATION_FORMAT = dwarf; 251 | ENABLE_STRICT_OBJC_MSGSEND = YES; 252 | ENABLE_TESTABILITY = YES; 253 | GCC_C_LANGUAGE_STANDARD = gnu99; 254 | GCC_DYNAMIC_NO_PIC = NO; 255 | GCC_NO_COMMON_BLOCKS = YES; 256 | GCC_OPTIMIZATION_LEVEL = 0; 257 | GCC_PREPROCESSOR_DEFINITIONS = ( 258 | "DEBUG=1", 259 | "$(inherited)", 260 | ); 261 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 262 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 263 | GCC_WARN_UNDECLARED_SELECTOR = YES; 264 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 265 | GCC_WARN_UNUSED_FUNCTION = YES; 266 | GCC_WARN_UNUSED_VARIABLE = YES; 267 | MACOSX_DEPLOYMENT_TARGET = 10.12; 268 | MTL_ENABLE_DEBUG_INFO = YES; 269 | ONLY_ACTIVE_ARCH = YES; 270 | SDKROOT = macosx; 271 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 272 | }; 273 | name = Debug; 274 | }; 275 | ED2FE22A1E3B81A100B7B658 /* Release */ = { 276 | isa = XCBuildConfiguration; 277 | buildSettings = { 278 | ALWAYS_SEARCH_USER_PATHS = NO; 279 | CLANG_ANALYZER_NONNULL = YES; 280 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 281 | CLANG_CXX_LIBRARY = "libc++"; 282 | CLANG_ENABLE_MODULES = YES; 283 | CLANG_ENABLE_OBJC_ARC = YES; 284 | CLANG_WARN_BOOL_CONVERSION = YES; 285 | CLANG_WARN_CONSTANT_CONVERSION = YES; 286 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 287 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 288 | CLANG_WARN_EMPTY_BODY = YES; 289 | CLANG_WARN_ENUM_CONVERSION = YES; 290 | CLANG_WARN_INFINITE_RECURSION = YES; 291 | CLANG_WARN_INT_CONVERSION = YES; 292 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 293 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 294 | CLANG_WARN_UNREACHABLE_CODE = YES; 295 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 296 | CODE_SIGN_IDENTITY = "-"; 297 | COPY_PHASE_STRIP = NO; 298 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 299 | ENABLE_NS_ASSERTIONS = NO; 300 | ENABLE_STRICT_OBJC_MSGSEND = YES; 301 | GCC_C_LANGUAGE_STANDARD = gnu99; 302 | GCC_NO_COMMON_BLOCKS = YES; 303 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 304 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 305 | GCC_WARN_UNDECLARED_SELECTOR = YES; 306 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 307 | GCC_WARN_UNUSED_FUNCTION = YES; 308 | GCC_WARN_UNUSED_VARIABLE = YES; 309 | MACOSX_DEPLOYMENT_TARGET = 10.12; 310 | MTL_ENABLE_DEBUG_INFO = NO; 311 | SDKROOT = macosx; 312 | }; 313 | name = Release; 314 | }; 315 | ED95DF5E1E40BA8800F2F8F9 /* Debug */ = { 316 | isa = XCBuildConfiguration; 317 | baseConfigurationReference = 8DA66C9CD0AAA3E7C55CE751 /* Pods-Boilerplate.debug.xcconfig */; 318 | buildSettings = { 319 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 320 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 321 | INFOPLIST_FILE = Boilerplate/Info.plist; 322 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 323 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 324 | PRODUCT_BUNDLE_IDENTIFIER = com.contentful.Boilerplate; 325 | PRODUCT_NAME = "$(TARGET_NAME)"; 326 | SDKROOT = iphoneos; 327 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 328 | SWIFT_VERSION = 4.2; 329 | TARGETED_DEVICE_FAMILY = "1,2"; 330 | }; 331 | name = Debug; 332 | }; 333 | ED95DF5F1E40BA8800F2F8F9 /* Release */ = { 334 | isa = XCBuildConfiguration; 335 | baseConfigurationReference = 0463465793B536EF70F077F6 /* Pods-Boilerplate.release.xcconfig */; 336 | buildSettings = { 337 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 338 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 339 | INFOPLIST_FILE = Boilerplate/Info.plist; 340 | IPHONEOS_DEPLOYMENT_TARGET = 10.2; 341 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 342 | PRODUCT_BUNDLE_IDENTIFIER = com.contentful.Boilerplate; 343 | PRODUCT_NAME = "$(TARGET_NAME)"; 344 | SDKROOT = iphoneos; 345 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 346 | SWIFT_VERSION = 4.2; 347 | TARGETED_DEVICE_FAMILY = "1,2"; 348 | VALIDATE_PRODUCT = YES; 349 | }; 350 | name = Release; 351 | }; 352 | /* End XCBuildConfiguration section */ 353 | 354 | /* Begin XCConfigurationList section */ 355 | ED2FE21F1E3B81A100B7B658 /* Build configuration list for PBXProject "Boilerplate-Swift" */ = { 356 | isa = XCConfigurationList; 357 | buildConfigurations = ( 358 | ED2FE2291E3B81A100B7B658 /* Debug */, 359 | ED2FE22A1E3B81A100B7B658 /* Release */, 360 | ); 361 | defaultConfigurationIsVisible = 0; 362 | defaultConfigurationName = Release; 363 | }; 364 | ED95DF5D1E40BA8800F2F8F9 /* Build configuration list for PBXNativeTarget "Boilerplate" */ = { 365 | isa = XCConfigurationList; 366 | buildConfigurations = ( 367 | ED95DF5E1E40BA8800F2F8F9 /* Debug */, 368 | ED95DF5F1E40BA8800F2F8F9 /* Release */, 369 | ); 370 | defaultConfigurationIsVisible = 0; 371 | defaultConfigurationName = Release; 372 | }; 373 | /* End XCConfigurationList section */ 374 | }; 375 | rootObject = ED2FE21C1E3B81A100B7B658 /* Project object */; 376 | } 377 | -------------------------------------------------------------------------------- /Pods/Contentful/Sources/Contentful/ImageOptions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Image.swift 3 | // Contentful 4 | // 5 | // Created by JP Wright on 24.05.17. 6 | // Copyright © 2017 Contentful GmbH. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import CoreGraphics 11 | 12 | #if os(iOS) || os(tvOS) || os(watchOS) 13 | import UIKit 14 | #elseif os(macOS) 15 | import Cocoa 16 | #endif 17 | 18 | public extension String { 19 | 20 | /** 21 | Will make a `URL` from the current `String` instance if possible. 22 | */ 23 | internal func toURL() throws -> URL { 24 | guard var urlComponents = URLComponents(string: self) else { 25 | throw ImageOptionError(message: "Invalid URL String: \(self)") 26 | } 27 | 28 | // Append https scheme if not present. 29 | if urlComponents.scheme == nil { 30 | urlComponents.scheme = "https" 31 | } 32 | 33 | guard let url = urlComponents.url else { 34 | throw ImageOptionError(message: "Invalid URL String: \(self)") 35 | } 36 | return url 37 | } 38 | } 39 | 40 | public extension String { 41 | 42 | /** 43 | The URL for the underlying media file with additional options for server side manipulations 44 | such as format changes, resizing, cropping, and focusing on different areas including on faces, 45 | among others. 46 | 47 | - Parameter imageOptions: An array of `ImageOption` that will be used for server side manipulations. 48 | - Throws: Will throw an `ImageQueryError` if the SDK is unable to generate a valid URL with the desired ImageOptions. 49 | */ 50 | public func url(with imageOptions: [ImageOption]) throws -> URL { 51 | 52 | // Check that there are no two image options that specifiy the same query parameter. 53 | // https://stackoverflow.com/a/27624476/4068264z 54 | // A Set is a collection of unique elements, so constructing them will invoke the Equatable implementation 55 | // and unique'ify the elements in the array. 56 | let uniqueImageOptions = Array(Set(imageOptions)) 57 | guard uniqueImageOptions.count == imageOptions.count else { 58 | throw ImageOptionError(message: "Cannot specify two instances of ImageOption of the same case." 59 | + "i.e. `[.formatAs(.png), .formatAs(.jpg(withQuality: .unspecified)]` is invalid.") 60 | } 61 | guard imageOptions.count > 0 else { 62 | return try toURL() 63 | } 64 | 65 | let urlString = try toURL().absoluteString 66 | guard var urlComponents = URLComponents(string: urlString) else { 67 | throw ImageOptionError(message: "The url string is not valid: \(urlString)") 68 | } 69 | 70 | urlComponents.queryItems = try imageOptions.flatMap { option in 71 | try option.urlQueryItems() 72 | } 73 | 74 | guard let url = urlComponents.url else { 75 | let message = """ 76 | The SDK was unable to generate a valid URL for the given ImageOptions. 77 | Please contact the maintainer on Github with a copy of the query \(urlString) 78 | """ 79 | throw ImageOptionError(message: message) 80 | } 81 | return url 82 | } 83 | } 84 | /** 85 | An enum-based API for specifying retrieval and server-side manipulation of images referenced by Contentful assets. 86 | 87 | See [Images API Reference](https://www.contentful.com/developers/docs/references/images-api/) 88 | */ 89 | public enum ImageOption: Equatable, Hashable { 90 | 91 | /// Specify the height of the image in pixels to be returned from the API. Valid ranges for height are between 0 and 4000. 92 | case height(UInt) 93 | 94 | /// Specify the width of the image in pixels to be returned from the API. Valid ranges for width are between 0 and 4000. 95 | case width(UInt) 96 | 97 | /// Specify the desired image filetype extension to be returned from the API. 98 | case formatAs(Format) 99 | 100 | /// Specify options for resizing behavior including . See `Fit` for available options. 101 | case fit(for: Fit) 102 | 103 | /// Specify the radius for rounded corners for an image. 104 | case withCornerRadius(Float) 105 | 106 | internal func urlQueryItems() throws -> [URLQueryItem] { 107 | switch self { 108 | case .height(let height) where height > 0 && height <= 4000: 109 | return [URLQueryItem(name: ImageParameters.height, value: String(height))] 110 | 111 | case .width(let width) where width > 0 && width <= 4000: 112 | return [URLQueryItem(name: ImageParameters.width, value: String(width))] 113 | 114 | case .width, .height: 115 | throw ImageOptionError(message: "The specified width or height parameters are not within the acceptable range") 116 | 117 | case .formatAs(let format): 118 | return try format.urlQueryItems() 119 | 120 | case .fit(let fit): 121 | return try fit.urlQueryItems() 122 | 123 | case .withCornerRadius(let radius): 124 | return [URLQueryItem(name: ImageParameters.radius, value: String(radius))] 125 | } 126 | } 127 | 128 | // MARK: 129 | 130 | // Used to unique'ify an Array of ImageOption instances by converting to a Set. 131 | public var hashValue: Int { 132 | switch self { 133 | case .width: return 0 134 | case .height: return 1 135 | case .formatAs: return 2 136 | case .fit: return 3 137 | case .withCornerRadius: return 4 138 | } 139 | } 140 | } 141 | 142 | /// Equatable implementation for `ImageOption` 143 | public func == (lhs: ImageOption, rhs: ImageOption) -> Bool { 144 | // We don't need to check associated values, we only implement equatable to validate that 145 | // two ImageOptions of the same case can't be used in one request. 146 | switch (lhs, rhs) { 147 | case (.width, .width): 148 | return true 149 | case (.height, .height): 150 | return true 151 | case (.formatAs, .formatAs): 152 | return true 153 | case (.fit, .fit): 154 | return true 155 | case (.withCornerRadius, .withCornerRadius): 156 | return true 157 | default: 158 | return false 159 | } 160 | } 161 | 162 | /** 163 | Quality options for JPG images to be used when specifying `.jpg` as the desired image format. 164 | Example usage 165 | 166 | ``` 167 | let imageOptions = [.formatAs(.jpg(withQuality: .asPercent(50)))] 168 | ``` 169 | */ 170 | public enum JPGQuality { 171 | 172 | /// Don't specify any quality for the JPG image. 173 | case unspecified 174 | 175 | /// Specify the JPG quality as a percentage. Valid ranges are 0-100 (inclusive). 176 | case asPercent(UInt) 177 | 178 | /// Specify that the API should return a progressive JPG. 179 | /// The progressive JPEG format stores multiple passes of an image in progressively higher detail. 180 | case progressive 181 | 182 | fileprivate func urlQueryItem() throws -> URLQueryItem? { 183 | switch self { 184 | case .unspecified: 185 | return nil 186 | case .asPercent(let quality): 187 | if quality > 100 { 188 | throw ImageOptionError(message: "JPG quality must be between 0 and 100 (inclusive).") 189 | } 190 | return URLQueryItem(name: ImageParameters.quality, value: String(quality)) 191 | case .progressive: 192 | return URLQueryItem(name: ImageParameters.formatFlag, value: "progressive") 193 | } 194 | } 195 | } 196 | 197 | /** 198 | Quality options for PNG images to be used when specifying `.png` as the desired image format. 199 | Example usage 200 | 201 | ``` 202 | let imageOptions = [.formatAs(.png(bits: .standard))] 203 | ``` 204 | */ 205 | public enum PngBits { 206 | 207 | /// Specify that the PNG should be represented with standard bit-depth. 208 | case standard 209 | 210 | /// Specify that the PNG should be represented with only 8 bits. 211 | case eight 212 | 213 | fileprivate func urlQueryItem() -> URLQueryItem? { 214 | switch self { 215 | case .standard: 216 | return nil 217 | case .eight: 218 | return URLQueryItem(name: ImageParameters.formatFlag, value: "png8") 219 | } 220 | } 221 | } 222 | 223 | /** 224 | Use `Format` to specify the image file formats supported by Contentful's Images API. 225 | Supported formats are `jpg` `png` and `webp`. 226 | */ 227 | public enum Format: URLImageQueryExtendable { 228 | 229 | internal var imageQueryParameter: String { 230 | return ImageParameters.format 231 | } 232 | 233 | /// Specify that the API should return the image as a jpg. Additionally, you can choose to specify 234 | /// a quality, or you can choose `jpg(withQuality: .unspecified). 235 | case jpg(withQuality: JPGQuality) 236 | 237 | /// Specify that the API should return the image as a png. 238 | case png(bits: PngBits) 239 | 240 | /// Specify that the API should return the image as a webp file. 241 | case webp 242 | 243 | fileprivate func urlArgument() -> String { 244 | switch self { 245 | case .jpg: return "jpg" 246 | case .png: return "png" 247 | case .webp: return "webp" 248 | } 249 | } 250 | 251 | fileprivate func additionalQueryItem() throws -> URLQueryItem? { 252 | switch self { 253 | case .jpg(let quality): 254 | return try quality.urlQueryItem() 255 | case .png(let bits): 256 | return bits.urlQueryItem() 257 | default: 258 | return nil 259 | } 260 | } 261 | } 262 | 263 | /** 264 | Use `Focus` to specify the focus area when resizing an image using either the `Fit.thumb`, `Fit.fill` 265 | and `Fit.crop` options. 266 | See [Contentful's Images API Reference Docs](https://www.contentful.com/developers/docs/references/images-api/#/reference/resizing-&-cropping/specify-focus-area-for-resizing) 267 | for more information. 268 | */ 269 | public enum Focus: String { 270 | /// Focus on the top of the image. 271 | case top 272 | /// Focus on the bottom of the image. 273 | case bottom 274 | /// Focus on the left of the image. 275 | case left 276 | /// Focus on the right of the image. 277 | case right 278 | /// Focus on the top left of the image. 279 | case topLeft = "top_left" 280 | /// Focus on the top right of the image. 281 | case topRight = "top_right" 282 | /// Focus on the bottom left of the image. 283 | case bottomLeft = "bottom_left" 284 | /// Focus on the bottom right of the image. 285 | case bottomRight = "bottom_right" 286 | /// Focus on a face in the image, if detected. 287 | case face 288 | /// Focus on a collection of faces in the image, if detected. 289 | case faces 290 | } 291 | 292 | /** 293 | The various options available within Fit specify different resizing behaviors for use in 294 | conjunction with the `ImageOption.fit(for: Fit)` option. By default, images are resized to fit 295 | inside the bounding box given by `w and `h while retaining their aspect ratio. 296 | Using the `Fit` options, you can change this behavior. 297 | */ 298 | public enum Fit: URLImageQueryExtendable { 299 | 300 | #if os(iOS) || os(tvOS) || os(watchOS) 301 | /// If building for iOS, tvOS, or watchOS, `Color` aliases to `UIColor`. If building for macOS 302 | /// `Color` aliases to `NSColor` 303 | public typealias Color = UIColor 304 | #else 305 | /// If building for iOS, tvOS, or watchOS, `Color` aliases to `UIColor`. If building for macOS 306 | /// `Color` aliases to `NSColor` 307 | public typealias Color = NSColor 308 | #endif 309 | 310 | /** 311 | If specifying an optional `UIColor` or `NSColor` make sure to also provide a custom width and height 312 | or else you may receive an error from the server. If the color cannot be resolved to a hex string by the SDK, 313 | an error will be thrown. 314 | */ 315 | case pad(withBackgroundColor: Color?) 316 | /// Specify that the image should be cropped, with an optional focus parameter. 317 | case crop(focusingOn: Focus?) 318 | /// Crop to the specified dimensions; if the original image is smaller than those specified, the image will be upscaled. 319 | case fill(focusingOn: Focus?) 320 | /// Creates a thumbnail with the specified focus. 321 | case thumb(focusingOn: Focus?) 322 | /// Scale the image regardless of the original aspect ratio. 323 | case scale 324 | 325 | // Enums that have cases with associated values in swift can't be backed by 326 | // String so we must reimplement returning the raw case value. 327 | fileprivate func urlArgument() -> String { 328 | switch self { 329 | case .pad: return "pad" 330 | case .crop: return "crop" 331 | case .fill: return "fill" 332 | case .thumb: return "thumb" 333 | case .scale: return "scale" 334 | } 335 | } 336 | 337 | fileprivate var imageQueryParameter: String { 338 | return ImageParameters.fit 339 | } 340 | 341 | fileprivate func additionalQueryItem() throws -> URLQueryItem? { 342 | switch self { 343 | case .pad(let .some(color)): 344 | let cgColor = color.cgColor 345 | let hexRepresentation = cgColor.hexRepresentation() 346 | return URLQueryItem(name: ImageParameters.backgroundColor, value: "rgb:" + hexRepresentation) 347 | 348 | case .thumb(let .some(focus)): 349 | return URLQueryItem(name: ImageParameters.focus, value: focus.rawValue) 350 | 351 | case .fill(let .some(focus)): 352 | return URLQueryItem(name: ImageParameters.focus, value: focus.rawValue) 353 | 354 | case .crop(let .some(focus)): 355 | return URLQueryItem(name: ImageParameters.focus, value: focus.rawValue) 356 | 357 | default: 358 | return nil 359 | } 360 | } 361 | } 362 | 363 | public struct ImageOptionError: Error, CustomDebugStringConvertible { 364 | 365 | let message: String 366 | 367 | public var debugDescription: String { 368 | return message 369 | } 370 | } 371 | 372 | // MARK: - Private 373 | 374 | private protocol URLImageQueryExtendable { 375 | 376 | var imageQueryParameter: String { get } 377 | 378 | func additionalQueryItem() throws -> URLQueryItem? 379 | 380 | func urlArgument() -> String 381 | } 382 | 383 | extension URLImageQueryExtendable { 384 | 385 | fileprivate func urlQueryItems() throws -> [URLQueryItem] { 386 | var urlQueryItems = [URLQueryItem]() 387 | 388 | let firstItem = URLQueryItem(name: imageQueryParameter, value: urlArgument()) 389 | urlQueryItems.append(firstItem) 390 | 391 | if let item = try additionalQueryItem() { 392 | urlQueryItems.append(item) 393 | } 394 | 395 | return urlQueryItems 396 | } 397 | } 398 | 399 | private struct ImageParameters { 400 | 401 | static let width = "w" 402 | static let height = "h" 403 | static let radius = "r" 404 | static let focus = "f" 405 | static let backgroundColor = "bg" 406 | static let fit = "fit" 407 | static let format = "fm" 408 | static let formatFlag = "fl" 409 | static let quality = "q" 410 | } 411 | 412 | 413 | // Use CGColor instead of UIColor to enable cross-platform compatibility: macOS, iOS, tvOS, watchOS. 414 | internal extension CGColor { 415 | 416 | // If for some reason the following code fails to create a hex string, the color black will be 417 | // returned. 418 | internal func hexRepresentation() -> String { 419 | let hexForBlack = "000000" 420 | guard let colorComponents = components else { return hexForBlack } 421 | guard let colorSpace = colorSpace else { return hexForBlack } 422 | 423 | let r, g, b: Float 424 | 425 | switch colorSpace.model { 426 | case .monochrome: 427 | // In this case, we're assigning the single shade of gray to all of r, g, and b. 428 | r = Float(colorComponents[0]) 429 | g = Float(colorComponents[0]) 430 | b = Float(colorComponents[0]) 431 | 432 | case .rgb: 433 | r = Float(colorComponents[0]) 434 | g = Float(colorComponents[1]) 435 | b = Float(colorComponents[2]) 436 | default: 437 | return hexForBlack 438 | } 439 | 440 | // Search the web for Swift UIColor to hex. 441 | // This answer helped: https://stackoverflow.com/a/30967091/4068264 442 | let hexString = String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255)) 443 | return hexString 444 | } 445 | } 446 | -------------------------------------------------------------------------------- /Pods/Contentful/README.md: -------------------------------------------------------------------------------- 1 | ![header](./.github/header-swift.png) 2 |

3 | 4 | Join Contentful Community Slack 5 | 6 |   7 | 8 | Join Contentful Community Forum 10 | 11 |

12 | 13 | # contentful.swift - Swift Delivery SDK for Contentful 14 | 15 | > Swift SDK for the Contentful [Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/) and [Content Preview API](https://www.contentful.com/developers/docs/references/content-preview-api/). It helps you to easily access your Content stored in Contentful with your Swift applications. 16 | 17 |

18 | This repository is actively maintained 19 |   20 | 21 | MIT License 22 | 23 |   24 | 25 | Build Status 26 | 27 |   28 | 29 | Coverage Status 30 | 31 |   32 | 33 | Codebeat badge 34 | 35 |

36 | 37 | 38 |

39 | 40 | Version 41 | 42 |   43 | 44 | Carthage compatible 45 | 46 |   47 | 48 | Swift Package Manager compatible 49 | 50 |   51 | 52 | iOS | macOS | watchOS | tvOS 53 | 54 |   55 |

56 | 57 | **What is Contentful?** 58 | 59 | [Contentful](https://www.contentful.com/) provides content infrastructure for digital teams to power websites, apps, and devices. Unlike a CMS, Contentful was built to integrate with the modern software stack. It offers a central hub for structured content, powerful management and delivery APIs, and a customizable web app that enable developers and content creators to ship their products faster. 60 | 61 |
62 | Table of contents 63 | 64 | 65 | - [contentful.swift - Swift Delivery SDK for Contentful](#contentfulswift---swift-delivery-sdk-for-contentful) 66 | - [Core Features](#core-features) 67 | - [Getting started](#getting-started) 68 | - [Installation](#installation) 69 | - [CocoaPods installation](#cocoapods-installation) 70 | - [Carthage installation](#carthage-installation) 71 | - [Swift Package Manager [swift-tools-version 4.2]](#swift-package-manager-swift-tools-version-42) 72 | - [Your first request](#your-first-request) 73 | - [Accessing the Preview API](#accessing-the-preview-api) 74 | - [Authorization](#authorization) 75 | - [Map Contentful entries to Swift classes via `EntryDecodable`](#map-contentful-entries-to-swift-classes-via-entrydecodable) 76 | - [Documentation & References](#documentation--references) 77 | - [Reference Documentation](#reference-documentation) 78 | - [Tutorials & other resources](#tutorials--other-resources) 79 | - [Swift playground](#swift-playground) 80 | - [Example application](#example-application) 81 | - [Migration](#migration) 82 | - [Swift Versioning](#swift-versioning) 83 | - [Reach out to us](#reach-out-to-us) 84 | - [You have questions about how to use this library?](#you-have-questions-about-how-to-use-this-library) 85 | - [You found a bug or want to propose a feature?](#you-found-a-bug-or-want-to-propose-a-feature) 86 | - [You need to share confidential information or have other questions?](#you-need-to-share-confidential-information-or-have-other-questions) 87 | - [Get involved](#get-involved) 88 | - [License](#license) 89 | - [Code of Conduct](#code-of-conduct) 90 | 91 | 92 | 93 |
94 | 95 | ## Core Features 96 | 97 | - Content retrieval through [Content Delivery API](https://www.contentful.com/developers/docs/references/content-delivery-api/) and [Content Preview API](https://www.contentful.com/developers/docs/references/content-preview-api/). 98 | - [Link resolution](https://www.contentful.com/developers/docs/concepts/links/) 99 | - Rich query syntax for type-safe queries 100 | - [Synchronization](https://www.contentful.com/developers/docs/concepts/sync/) 101 | - [Localization support](https://www.contentful.com/developers/docs/concepts/locales/) 102 | - Up-to-date with the latest Swift development stack: Swift 4.x | Xcode 10.x 103 | - Supports [Environments](https://www.contentful.com/developers/docs/concepts/multiple-environments/) (**v2.0.0+**) 104 | 105 | ## Getting started 106 | 107 | In order to get started with the Contentful Swift SDK you'll need not only to install it, but also to get credentials which will allow you to have access to your content in Contentful. 108 | 109 | - [Installation](#installation) 110 | - [Your first request](#your-first-request) 111 | - [Accessing Preview API](#accessing-the-preview-api) 112 | - [Authorization](#authorization) 113 | 114 | ### Installation 115 | 116 | ##### CocoaPods installation 117 | 118 | ```ruby 119 | platform :ios, '9.0' 120 | use_frameworks! 121 | pod 'Contentful' 122 | ``` 123 | 124 | You can specify a specific version of Contentful depending on your needs. To learn more about operators for dependency versioning within a Podfile, see the [CocoaPods doc on the Podfile](https://guides.cocoapods.org/using/the-podfile.html). 125 | 126 | ```ruby 127 | pod 'Contentful', '~> 4.0.0' 128 | ``` 129 | 130 | #### Carthage installation 131 | 132 | You can also use [Carthage](https://github.com/Carthage/Carthage) for integration by adding the following to your `Cartfile`: 133 | 134 | ``` 135 | github "contentful/contentful.swift" ~> 4.0.0 136 | ``` 137 | 138 | #### Swift Package Manager [swift-tools-version 4.2] 139 | 140 | Add the following line to your array of dependencies: 141 | 142 | ```swift 143 | .package(url: "https://github.com/contentful/contentful.swift", .upToNextMajor(from: "4.0.0")) 144 | ``` 145 | 146 | ### Your first request 147 | 148 | The following code snippet is the most basic one you can use to fetch content from Contentful with this SDK: 149 | 150 | ```swift 151 | import Contentful 152 | 153 | let client = Client(spaceId: "cfexampleapi", 154 | environmentId: "master", // Defaults to "master" if omitted. 155 | accessToken: "b4c0n73n7fu1") 156 | 157 | client.fetch(Entry.self, id: "nyancat") { (result: Result) in 158 | switch result { 159 | case .success(let entry): 160 | print(entry) 161 | case .error(let error): 162 | print("Error \(error)!") 163 | } 164 | } 165 | ``` 166 | 167 | ### Accessing the Preview API 168 | 169 | To access the Content Preview API, use your preview access token and set your client configuration to use preview as shown below. 170 | 171 | ```swift 172 | 173 | let client = Client(spaceId: "cfexampleapi", 174 | accessToken: "e5e8d4c5c122cf28fc1af3ff77d28bef78a3952957f15067bbc29f2f0dde0b50", 175 | host: Host.preview) // Defaults to Host.delivery if omitted. 176 | ``` 177 | 178 | ### Authorization 179 | 180 | Grab credentials for your Contentful space by [navigating to the "APIs" section of the Contentful Web App](https://app.contentful.com/deeplink?link=api). 181 | If you don't have access tokens for your app, create a new set for the Delivery and Preview APIs. 182 | Next, pass the id of your space and delivery access token into the initializer like so: 183 | 184 | ### Map Contentful entries to Swift classes via `EntryDecodable` 185 | 186 | The `EntryDecodable` protocol allows you to define a mapping between your content types and your Swift classes that entries will be serialized to. When using methods such as: 187 | 188 | ```swift 189 | let query = QueryOn.where(field: .color, .equals("gray")) 190 | 191 | client.fetchArray(of: Cat.self, matching: query) { (result: Result>) in 192 | guard let cats = result.value?.items else { return } 193 | print(cats) 194 | } 195 | ``` 196 | 197 | The asynchronously returned result will be an instance of `ArrayResponse` in which the generic type parameter is the same type you've passed into the `fetch` method. If you are using a `Query` that does not restrict the response to contain entries of one content type, you will use methods that return `MixedArrayResponse` instead of `ArrayResponse`. The `EntryDecodable` protocol extends the `Decodable` protocol in Swift 4's Foundation standard library. The SDK provides helper methods for resolving relationships between `EntryDecodable`s and also for grabbing values from the fields container in the JSON for each resource. 198 | 199 | In the example above, `Cat` is a type of our own definition conforming to `EntryDecodable` and `FieldKeysQueryable`. In order for the SDK to properly create your model types when receiving JSON, you must pass in these types to your `Client` instance: 200 | 201 | ```swift 202 | let contentTypeClasses: [EntryDecodable.Type] = [ 203 | Cat.self 204 | Dog.self, 205 | Human.self 206 | ] 207 | 208 | let client = Client(spaceId: spaceId, 209 | accessToken: deliveryAPIAccessToken, 210 | contentTypeClasses: contentTypeClasses) 211 | ``` 212 | 213 | The source for the `Cat` model class is below; note the helper methods the SDK adds to Swift 4's `Decoder` type to simplify for parsing JSON returned by Contentful. You also need to pass in these types to your `Client` instance in order to use the fetch methods which take `EntryDecodable` type references: 214 | 215 | ```swift 216 | final class Cat: EntryDecodable, FieldKeysQueryable { 217 | 218 | static let contentTypeId: String = "cat" 219 | 220 | let sys: Sys 221 | let color: String? 222 | let name: String? 223 | let lives: Int? 224 | let likes: [String]? 225 | 226 | // Relationship fields. 227 | var bestFriend: Cat? 228 | 229 | public required init(from decoder: Decoder) throws { 230 | sys = try decoder.sys() 231 | let fields = try decoder.contentfulFieldsContainer(keyedBy: Cat.Fields.self) 232 | 233 | self.name = try fields.decodeIfPresent(String.self, forKey: .name) 234 | self.color = try fields.decodeIfPresent(String.self, forKey: .color) 235 | self.likes = try fields.decodeIfPresent(Array.self, forKey: .likes) 236 | self.lives = try fields.decodeIfPresent(Int.self, forKey: .lives) 237 | 238 | try fields.resolveLink(forKey: .bestFriend, decoder: decoder) { [weak self] linkedCat in 239 | self?.bestFriend = linkedCat as? Cat 240 | } 241 | } 242 | 243 | enum FieldKeys: String, CodingKey { 244 | case bestFriend 245 | case name, color, likes, lives 246 | } 247 | } 248 | ``` 249 | 250 | ## Documentation & References 251 | 252 | ### Reference Documentation 253 | 254 | The SDK has 100% documentation coverage of all public variables, types, and functions. You can view the docs on the [web](https://contentful.github.io/contentful.swift/docs/index.html) or browse them in Xcode. For further information about the Content Delivery API, check out the [Content Delivery API Reference Documentation](https://www.contentful.com/developers/documentation/content-delivery-api/). 255 | 256 | ### Tutorials & other resources 257 | 258 | * This library is a wrapper around our Contentful Delivery REST API. Some more specific details such as search parameters and pagination are better explained on the [REST API reference](https://www.contentful.com/developers/docs/references/content-delivery-api/), and you can also get a better understanding of how the requests look under the hood. 259 | * Check the [Contentful for Swift](https://www.contentful.com/developers/docs/ios/tutorials/) page for Tutorials, Demo Apps, and more information on other ways of using Swift with Contentful 260 | 261 | #### Swift playground 262 | 263 | If you'd like to try an interactive demo of the API via a Swift Playground, do the following: 264 | 265 | ```bash 266 | git clone --recursive https://github.com/contentful/contentful.swift.git 267 | cd contentful.swift 268 | make open 269 | ``` 270 | 271 | Then build the "Contentful_macOS" scheme, open the playground file and go! Note: make sure the "Render Documentation" button is switched on in the Utilities menu on the right of Xcode, and also open up the console to see the outputs of the calls to `print`. 272 | 273 | #### Example application 274 | 275 | See the [Swift iOS app on Github](https://github.com/contentful/the-example-app.swift) and follow the instructions on the README to get a copy of the space so you can see how changing content in Contentful affects the presentation of the app. 276 | 277 | ### Migration 278 | 279 | We gathered all information related to migrating from older versions of the library in our [Migrations.md](Migrations.md) document. 280 | 281 | ## Swift Versioning 282 | 283 | It is recommended to use Swift 4.2, as older versions of the SDK will not have fixes backported. If you must use older Swift versions, see the compatible tags below. 284 | 285 | Swift version | Compatible Contentful tag | 286 | | --- | --- | 287 | | Swift 4.2 | [ ≥ `4.0.0` ] | 288 | | Swift 4.1 | [`2.0.0` - `3.1.2`] | 289 | | Swift 4.0 | [`0.10.0` - `1.0.1`] | 290 | | Swift 3.x | [`0.3.0` - `0.9.3`] | 291 | | Swift 2.3 | `0.2.3` | 292 | | Swift 2.2 | `0.2.1` | 293 | 294 | 295 | ## Reach out to us 296 | 297 | ### You have questions about how to use this library? 298 | * Reach out to our community forum: [![Contentful Community Forum](https://img.shields.io/badge/-Join%20Community%20Forum-3AB2E6.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MiA1OSI+CiAgPHBhdGggZmlsbD0iI0Y4RTQxOCIgZD0iTTE4IDQxYTE2IDE2IDAgMCAxIDAtMjMgNiA2IDAgMCAwLTktOSAyOSAyOSAwIDAgMCAwIDQxIDYgNiAwIDEgMCA5LTkiIG1hc2s9InVybCgjYikiLz4KICA8cGF0aCBmaWxsPSIjNTZBRUQyIiBkPSJNMTggMThhMTYgMTYgMCAwIDEgMjMgMCA2IDYgMCAxIDAgOS05QTI5IDI5IDAgMCAwIDkgOWE2IDYgMCAwIDAgOSA5Ii8+CiAgPHBhdGggZmlsbD0iI0UwNTM0RSIgZD0iTTQxIDQxYTE2IDE2IDAgMCAxLTIzIDAgNiA2IDAgMSAwLTkgOSAyOSAyOSAwIDAgMCA0MSAwIDYgNiAwIDAgMC05LTkiLz4KICA8cGF0aCBmaWxsPSIjMUQ3OEE0IiBkPSJNMTggMThhNiA2IDAgMSAxLTktOSA2IDYgMCAwIDEgOSA5Ii8+CiAgPHBhdGggZmlsbD0iI0JFNDMzQiIgZD0iTTE4IDUwYTYgNiAwIDEgMS05LTkgNiA2IDAgMCAxIDkgOSIvPgo8L3N2Zz4K&maxAge=31557600)](https://support.contentful.com/) 299 | * Jump into our community slack channel: [![Contentful Community Slack](https://img.shields.io/badge/-Join%20Community%20Slack-2AB27B.svg?logo=slack&maxAge=31557600)](https://www.contentful.com/slack/) 300 | 301 | ### You found a bug or want to propose a feature? 302 | 303 | * File an issue here on GitHub: [![File an issue](https://img.shields.io/badge/-Create%20Issue-6cc644.svg?logo=github&maxAge=31557600)](https://github.com/contentful/contentful.swift/issues/new). Make sure to remove any credential from your code before sharing it. 304 | 305 | ### You need to share confidential information or have other questions? 306 | 307 | * File a support ticket at our Contentful Customer Support: [![File support ticket](https://img.shields.io/badge/-Submit%20Support%20Ticket-3AB2E6.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MiA1OSI+CiAgPHBhdGggZmlsbD0iI0Y4RTQxOCIgZD0iTTE4IDQxYTE2IDE2IDAgMCAxIDAtMjMgNiA2IDAgMCAwLTktOSAyOSAyOSAwIDAgMCAwIDQxIDYgNiAwIDEgMCA5LTkiIG1hc2s9InVybCgjYikiLz4KICA8cGF0aCBmaWxsPSIjNTZBRUQyIiBkPSJNMTggMThhMTYgMTYgMCAwIDEgMjMgMCA2IDYgMCAxIDAgOS05QTI5IDI5IDAgMCAwIDkgOWE2IDYgMCAwIDAgOSA5Ii8+CiAgPHBhdGggZmlsbD0iI0UwNTM0RSIgZD0iTTQxIDQxYTE2IDE2IDAgMCAxLTIzIDAgNiA2IDAgMSAwLTkgOSAyOSAyOSAwIDAgMCA0MSAwIDYgNiAwIDAgMC05LTkiLz4KICA8cGF0aCBmaWxsPSIjMUQ3OEE0IiBkPSJNMTggMThhNiA2IDAgMSAxLTktOSA2IDYgMCAwIDEgOSA5Ii8+CiAgPHBhdGggZmlsbD0iI0JFNDMzQiIgZD0iTTE4IDUwYTYgNiAwIDEgMS05LTkgNiA2IDAgMCAxIDkgOSIvPgo8L3N2Zz4K&maxAge=31557600)](https://www.contentful.com/support/) 308 | 309 | 310 | ## Get involved 311 | 312 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?maxAge=31557600)](http://makeapullrequest.com) 313 | 314 | We appreciate any help on our repositories. For more details about how to contribute see our [Contributing.md](Contributing.md) document. 315 | 316 | ## License 317 | 318 | This repository is published under the [MIT](LICENSE) license. 319 | 320 | ## Code of Conduct 321 | 322 | We want to provide a safe, inclusive, welcoming, and harassment-free space and experience for all participants, regardless of gender identity and expression, sexual orientation, disability, physical appearance, socioeconomic status, body size, ethnicity, nationality, level of experience, age, religion (or lack thereof), or other identity markers. 323 | 324 | [Read our full Code of Conduct](https://github.com/contentful-developer-relations/community-code-of-conduct). 325 | 326 | --------------------------------------------------------------------------------