diff --git a/Podfile b/Podfile new file mode 100644 index 0000000..1ef78bb --- /dev/null +++ b/Podfile @@ -0,0 +1,12 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'rss-reader' do + use_frameworks! + inhibit_all_warnings! + + pod 'FeedKit', '~> 8.1' + pod 'Kingfisher', '~> 5.7' + pod 'SVProgressHUD', '~> 2.2' + +end diff --git a/Podfile.lock b/Podfile.lock new file mode 100644 index 0000000..ddc934c --- /dev/null +++ b/Podfile.lock @@ -0,0 +1,24 @@ +PODS: + - FeedKit (8.1.1) + - Kingfisher (5.7.0) + - SVProgressHUD (2.2.5) + +DEPENDENCIES: + - FeedKit (~> 8.1) + - Kingfisher (~> 5.7) + - SVProgressHUD (~> 2.2) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - FeedKit + - Kingfisher + - SVProgressHUD + +SPEC CHECKSUMS: + FeedKit: 3418eed25f0b493b205b4de1b8511ac21d413fa9 + Kingfisher: c7d211b54f1f30d8060aadab177d52b4349c825b + SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 + +PODFILE CHECKSUM: 3412147b32c5fcffbdb5018d5e3a38ea8cc2b8d4 + +COCOAPODS: 1.7.2 diff --git a/Pods/FeedKit/LICENSE b/Pods/FeedKit/LICENSE new file mode 100644 index 0000000..263f199 --- /dev/null +++ b/Pods/FeedKit/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 - 2018 Nuno Manuel Dias + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Pods/FeedKit/README.md b/Pods/FeedKit/README.md new file mode 100644 index 0000000..636d691 --- /dev/null +++ b/Pods/FeedKit/README.md @@ -0,0 +1,233 @@ +![FeedKit](/FeedKit.png?raw=true) + +[![build status](https://travis-ci.org/nmdias/FeedKit.svg)](https://travis-ci.org/nmdias/FeedKit) +[![cocoapods compatible](https://img.shields.io/badge/cocoapods-compatible-brightgreen.svg)](https://cocoapods.org/pods/FeedKit) +[![carthage compatible](https://img.shields.io/badge/carthage-compatible-brightgreen.svg)](https://github.com/Carthage/Carthage) +[![language](https://img.shields.io/badge/spm-compatible-brightgreen.svg)](https://swift.org) +[![swift](https://img.shields.io/badge/swift-4.2-orange.svg)](https://github.com/nmdias/DefaultsKit/releases) + +## Features + +- [x] [Atom](https://tools.ietf.org/html/rfc4287) +- [x] RSS [0.90](http://www.rssboard.org/rss-0-9-0), [0.91](http://www.rssboard.org/rss-0-9-1), [1.00](http://web.resource.org/rss/1.0/spec), [2.00](http://cyber.law.harvard.edu/rss/rss.html) +- [x] [JSON](https://jsonfeed.org/version/1) +- [x] Namespaces + - [x] [Dublin Core](http://web.resource.org/rss/1.0/modules/dc/) + - [x] [Syndication](http://web.resource.org/rss/1.0/modules/syndication/) + - [x] [Content](http://web.resource.org/rss/1.0/modules/content/) + - [x] [Media RSS](http://www.rssboard.org/media-rss) + - [x] [iTunes Podcasting Tags](https://help.apple.com/itc/podcasts_connect/#/itcb54353390) +- [x] [Documentation](http://cocoadocs.org/docsets/FeedKit) +- [x] Unit Test Coverage + +## Requirements + +![xcode](https://img.shields.io/badge/xcode-10.1%2b-lightgrey.svg) +![ios](https://img.shields.io/badge/ios-8.0%2b-lightgrey.svg) +![tvos](https://img.shields.io/badge/tvos-9.0%2b-lightgrey.svg) +![watchos](https://img.shields.io/badge/watchos-2.0%2b-lightgrey.svg) +![mac os](https://img.shields.io/badge/mac%20os-10.10%2b-lightgrey.svg) +![mac os](https://img.shields.io/badge/ubuntu-16.04+-lightgrey.svg) + +Installation >> [`instructions`](https://github.com/nmdias/FeedKit/blob/master/INSTALL.md) << + +## Usage + +Build a URL pointing to an RSS, Atom or JSON Feed. +```swift +let feedURL = URL(string: "http://images.apple.com/main/rss/hotnews/hotnews.rss")! +``` + +Get an instance of `FeedParser` +```swift +let parser = FeedParser(URL: feedURL) // or FeedParser(data: data) or FeedParser(xmlStream: stream) +``` + +Then call `parse` or `parseAsync` to start parsing the feed... + +> A **common scenario** in UI environments would be parsing a feed **asynchronously** from a user initiated action, such as the touch of a button. e.g. + +```swift +// Parse asynchronously, not to block the UI. +parser.parseAsync(queue: DispatchQueue.global(qos: .userInitiated)) { (result) in + // Do your thing, then back to the Main thread + DispatchQueue.main.async { + // ..and update the UI + } +} +``` + +Remember, you are responsible to manually bring the result closure to whichever queue is apropriate. Usually to the Main thread, for UI apps, by calling `DispatchQueue.main.async` . + +Alternatively, you can also parse synchronously. + +```swift +let result = parser.parse() +``` + +## Parse Result + +Whichever the case, if parsing succeeds you should now have a `Strongly Typed Model` of an `RSS`, `Atom` or `JSON Feed`. +```swift +switch result { +case let .atom(feed): // Atom Syndication Format Feed Model +case let .rss(feed): // Really Simple Syndication Feed Model +case let .json(feed): // JSON Feed Model +case let .failure(error): +} +``` + + +#### Parse Success +You can check if a Feed was `successfully` parsed or not. +```swift +result.isSuccess // If parsing was a success +result.isFailure // If parsing failed +result.error // An error, if any +``` + +## Model Preview +Safely bind a feed of your choosing: +> You may find the example bellow useful, if you're dealing with only a single type of feed. +```swift +guard let feed = result.rssFeed, result.isSuccess else { + print(result.error) + return +} +``` +Then go through it's properties: + +> The RSS and Atom feed Models are rather extensive throughout the supported namespaces. These are just a preview of what's available. + +#### RSS + +```swift +feed.title +feed.link +feed.description +feed.language +feed.copyright +feed.managingEditor +feed.webMaster +feed.pubDate +feed.lastBuildDate +feed.categories +feed.generator +feed.docs +feed.cloud +feed.rating +feed.ttl +feed.image +feed.textInput +feed.skipHours +feed.skipDays +//... +feed.dublinCore +feed.syndication +feed.iTunes +// ... + +let item = feed.items?.first + +item?.title +item?.link +item?.description +item?.author +item?.categories +item?.comments +item?.enclosure +item?.guid +item?.pubDate +item?.source +//... +item?.dublinCore +item?.content +item?.iTunes +item?.media +// ... +``` + +> Refer to the [`documentation`](http://cocoadocs.org/docsets/FeedKit) for the complete model properties and descriptions + +#### Atom + +```swift +feed.title +feed.subtitle +feed.links +feed.updated +feed.authors +feed.contributors +feed.id +feed.generator +feed.icon +feed.logo +feed.rights +// ... + +let entry = feed.entries?.first + +entry?.title +entry?.summary +entry?.authors +entry?.contributors +entry?.links +entry?.updated +entry?.categories +entry?.id +entry?.content +entry?.published +entry?.source +entry?.rights +// ... +``` + +> Refer to the [`documentation`](http://cocoadocs.org/docsets/FeedKit) for the complete model properties and descriptions + +#### JSON + +```swift +feed.version +feed.title +feed.homePageURL +feed.feedUrl +feed.description +feed.userComment +feed.nextUrl +feed.icon +feed.favicon +feed.author +feed.expired +feed.hubs +feed.extensions +// ... + +let item = feed.items?.first + +item?.id +item?.url +item?.externalUrl +item?.title +item?.contentText +item?.contentHtml +item?.summary +item?.image +item?.bannerImage +item?.datePublished +item?.dateModified +item?.author +item?.url +item?.tags +item?.attachments +item?.extensions +// ... +``` + +> Refer to the [`documentation`](http://cocoadocs.org/docsets/FeedKit) for the complete model properties and descriptions + +## License + +FeedKit is released under the MIT license. See [LICENSE](https://github.com/nmdias/FeedKit/blob/master/LICENSE) for details. + + + diff --git a/Pods/FeedKit/Sources/FeedKit/Dates/DateSpec.swift b/Pods/FeedKit/Sources/FeedKit/Dates/DateSpec.swift new file mode 100644 index 0000000..9f7c053 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Dates/DateSpec.swift @@ -0,0 +1,39 @@ +// +// DateSpec.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Date specifications +/// +/// - rfc822: The `Standard for the format of arpa internet text messages`. +/// See https://www.ietf.org/rfc/rfc0822.txt +/// - rfc3999: The `Date and Time on the Internet: Timestamps`. +/// See https://www.ietf.org/rfc/rfc3339.txt +/// - iso8601: The `W3CDTF` date time format specification +/// See http://www.w3.org/TR/NOTE-datetime +enum DateSpec { + case rfc822 + case rfc3999 + case iso8601 +} diff --git a/Pods/FeedKit/Sources/FeedKit/Dates/ISO8601DateFormatter.swift b/Pods/FeedKit/Sources/FeedKit/Dates/ISO8601DateFormatter.swift new file mode 100644 index 0000000..c48d879 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Dates/ISO8601DateFormatter.swift @@ -0,0 +1,59 @@ +// +// ISO8601DateFormatter.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Converts date and time textual representations within the ISO8601 +/// date specification into `Date` objects +class ISO8601DateFormatter: DateFormatter { + + let dateFormats = [ + "yyyy-MM-dd'T'HH:mm:ss.SSZZZZZ", + "yyyy-MM-dd'T'HH:mm:ssZZZZZ", + "yyyy-MM-dd'T'HH:mmSSZZZZZ", + "yyyy-MM-dd'T'HH:mm" + ] + + override init() { + super.init() + self.timeZone = TimeZone(secondsFromGMT: 0) + self.locale = Locale(identifier: "en_US_POSIX") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) not supported") + } + + override func date(from string: String) -> Date? { + let string = string.trimmingCharacters(in: .whitespacesAndNewlines) + for dateFormat in self.dateFormats { + self.dateFormat = dateFormat + if let date = super.date(from: string) { + return date + } + } + return nil + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Dates/RFC3339DateFormatter.swift b/Pods/FeedKit/Sources/FeedKit/Dates/RFC3339DateFormatter.swift new file mode 100644 index 0000000..e410e08 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Dates/RFC3339DateFormatter.swift @@ -0,0 +1,58 @@ +// +// RFC3339DateFormatter.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Converts date and time textual representations within the RFC3339 +/// date specification into `Date` objects +class RFC3339DateFormatter: DateFormatter { + + let dateFormats = [ + "yyyy-MM-dd'T'HH:mm:ssZZZZZ", + "yyyy-MM-dd'T'HH:mm:ss.SSZZZZZ", + "yyyy-MM-dd'T'HH:mm:ss-SS:ZZ" + ] + + override init() { + super.init() + self.timeZone = TimeZone(secondsFromGMT: 0) + self.locale = Locale(identifier: "en_US_POSIX") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) not supported") + } + + override func date(from string: String) -> Date? { + let string = string.trimmingCharacters(in: .whitespacesAndNewlines) + for dateFormat in self.dateFormats { + self.dateFormat = dateFormat + if let date = super.date(from: string) { + return date + } + } + return nil + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Dates/RFC822DateFormatter.swift b/Pods/FeedKit/Sources/FeedKit/Dates/RFC822DateFormatter.swift new file mode 100644 index 0000000..40625a7 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Dates/RFC822DateFormatter.swift @@ -0,0 +1,77 @@ +// +// RFC822DateFormatter.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Converts date and time textual representations within the RFC822 +/// date specification into `Date` objects +class RFC822DateFormatter: DateFormatter { + + let dateFormats = [ + "EEE, d MMM yyyy HH:mm:ss zzz", + "EEE, d MMM yyyy HH:mm zzz", + "d MMM yyyy HH:mm:ss Z" + ] + + let backupFormats = [ + "d MMM yyyy HH:mm:ss zzz", + "d MMM yyyy HH:mm zzz" + ] + + override init() { + super.init() + self.timeZone = TimeZone(secondsFromGMT: 0) + self.locale = Locale(identifier: "en_US_POSIX") + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) not supported") + } + + private func attemptParsing(from string: String, formats: [String]) -> Date? { + for dateFormat in formats { + self.dateFormat = dateFormat + if let date = super.date(from: string) { + return date + } + } + return nil + } + + override func date(from string: String) -> Date? { + let string = string.trimmingCharacters(in: .whitespacesAndNewlines) + if let parsedDate = attemptParsing(from: string, formats: dateFormats) { + return parsedDate + } + // See if we can lop off a text weekday, as DateFormatter does not + // handle these in full compliance with Unicode tr35-31. For example, + // "Tues, 6 November 2007 12:00:00 GMT" is rejected because of the "Tues", + // even though "Tues" is used as an example for EEE in tr35-31. + let trimRegEx = try! NSRegularExpression(pattern: "^[a-zA-Z]+, ([\\w :+-]+)$") + let trimmed = trimRegEx.stringByReplacingMatches(in: string, options: [], + range: NSMakeRange(0, string.count), withTemplate: "$1") + return attemptParsing(from: trimmed, formats: backupFormats) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/Array + Equatable.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/Array + Equatable.swift new file mode 100644 index 0000000..f1b4145 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/Array + Equatable.swift @@ -0,0 +1,42 @@ +// +// Array + Equatable.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional arrays equatable +/// +/// - Parameters: +/// - lhs: The left-hand side +/// - rhs: The right-hand side +/// - Returns: A boolean value +public func ==(lhs: [T]?, rhs: [T]?) -> Bool { + switch (lhs,rhs) { + case (.some(let lhs), .some(let rhs)): + return lhs == rhs + case (.none, .none): + return true + default: + return false + } +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/Date + codingStrategy.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/Date + codingStrategy.swift new file mode 100644 index 0000000..988cc7f --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/Date + codingStrategy.swift @@ -0,0 +1,69 @@ +// +// Date + codingStrategy.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension Date { + + public static var encodingStrategy: JSONEncoder.DateEncodingStrategy { + return JSONEncoder.DateEncodingStrategy.custom({ (date, encoder) in + let formatter = DateFormatter() + formatter.dateFormat = "YYYY-MM-dd'T'HH:mm:ss" + let stringData = formatter.string(from: date) + var container = encoder.singleValueContainer() + try container.encode(stringData) + }) + } + + public static var decodingStrategy: JSONDecoder.DateDecodingStrategy { + + return JSONDecoder.DateDecodingStrategy.custom({ (decoder: Decoder) -> Date in + let container = try decoder.singleValueContainer() + let dateString = try container.decode(String.self) + + func from(_ string: String) -> Date? { + if string.isEmpty { + return nil + } + for dateFormat in [ + "yyyy-MM-dd'T'HH:mm:ssZZZZZ", + "yyyy-MM-dd'T'HH:mm:ss.SSZZZZZ", + "yyyy-MM-dd'T'HH:mm:ss-SS:ZZ"] { + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = dateFormat + if let date = dateFormatter.date(from: string) { + return date + } + } + return nil + } + + guard let date = from(dateString) else { + fatalError("Date decoding strategy failed.") + } + return date + }) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/String + toBool.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toBool.swift new file mode 100644 index 0000000..d1dc038 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toBool.swift @@ -0,0 +1,39 @@ +// +// String + toBool.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension String { + + /// Convert a string representation of a logical value to it's `Bool`. + /// equivalent + func toBool() -> Bool? { + switch self { + case "True", "true", "Yes", "yes", "1": return true + case "False", "false", "No", "no", "0": return false + default: return nil + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDate.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDate.swift new file mode 100644 index 0000000..2120f54 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDate.swift @@ -0,0 +1,52 @@ +// +// String + toDate.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension String { + + /// Attempts to convert the textual representation of a date with + /// the specified `DateSpec` to a `Date` object. + /// + /// - Parameter spec: The `DateSpec` to interpert the string. + /// - Returns: A `Date` object, or nil if the conversion failed. + func toDate(from spec: DateSpec) -> Date? { + switch spec { + case .rfc822: return RFC822DateFormatter().date(from: self) + case .rfc3999: return RFC3339DateFormatter().date(from: self) + case .iso8601: return ISO8601DateFormatter().date(from: self) + } + } + + /// Attempts to convert the textual representation of a date to a + /// `Date` object according to several common schemes. + /// + /// - Returns: A `Date` object, or nil if the conversion failed. + func toPermissiveDate() -> Date? { + return RFC822DateFormatter().date(from: self) ?? + (RFC3339DateFormatter().date(from: self) ?? + ISO8601DateFormatter().date(from: self)) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDuration.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDuration.swift new file mode 100644 index 0000000..b22bbc9 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/String + toDuration.swift @@ -0,0 +1,54 @@ +// +// String + toDuration.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension String { + + /// Convert the string representation of a time duration to a Time Interval. + /// + /// - Returns: A TimeInterval. + func toDuration() -> TimeInterval? { + let comps = self.components(separatedBy: ":") + + guard + !comps.contains(where: { Int($0) == nil }), + !comps.contains(where: { Int($0)! < 0 }) + else { return nil } + + return comps + .reversed() + .enumerated() + .map { i, e in + (Double(e) ?? 0) + * + pow(Double(60), Double(i)) + } + .reduce(0, +) + + } + +} + + diff --git a/Pods/FeedKit/Sources/FeedKit/Extensions/URL + replacingScheme.swift b/Pods/FeedKit/Sources/FeedKit/Extensions/URL + replacingScheme.swift new file mode 100644 index 0000000..072f275 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Extensions/URL + replacingScheme.swift @@ -0,0 +1,47 @@ +// +// URL + replacingScheme.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension URL { + /// Returns a new `URL` in which the target scheme is replaced by another given + /// scheme. Returns `self` if the scheme already matches the target scheme. + /// + /// - Parameters: + /// - target: The target scheme + /// - replacement: The replacement scheme + func replacing( + scheme target: Target, + with replacement: Replacement) + -> URL? where Target : StringProtocol, Replacement : StringProtocol + { + var urlComponents = URLComponents(url: self, resolvingAgainstBaseURL: true) + let isTargetScheme = urlComponents?.scheme?.caseInsensitiveCompare(target) == ComparisonResult.orderedSame + if isTargetScheme { + urlComponents?.scheme = "\(replacement)" + return urlComponents?.url + } + return self + } +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapAttributes.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapAttributes.swift new file mode 100644 index 0000000..915e029 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapAttributes.swift @@ -0,0 +1,354 @@ +// +// AtomFeed + mapAttributes.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension AtomFeed { + + /// Maps the attributes of the specified dictionary for a given `AtomPath` + /// to the `AtomFeed` model + /// + /// - Parameters: + /// - attributes: The attribute dictionary to map to the model. + /// - path: The path of feed's element. + func map(_ attributes: [String : String], for path: AtomPath) { + + switch path { + + case .feedSubtitle: + + if self.subtitle == nil { + self.subtitle = AtomFeedSubtitle(attributes: attributes) + } + + case .feedLink: + + if self.links == nil { + self.links = [] + } + + self.links?.append(AtomFeedLink(attributes: attributes)) + + case .feedCategory: + + if self.categories == nil { + self.categories = [] + } + + self.categories?.append(AtomFeedCategory(attributes: attributes)) + + case .feedAuthor: + + if self.authors == nil { + self.authors = [] + } + + self.authors?.append(AtomFeedAuthor()) + + case .feedContributor: + + if self.contributors == nil { + self.contributors = [] + } + + self.contributors?.append(AtomFeedContributor()) + + case .feedGenerator: + + if self.generator == nil { + self.generator = AtomFeedGenerator(attributes: attributes) + } + + case .feedEntry: + + if self.entries == nil { + self.entries = [] + } + + self.entries?.append(AtomFeedEntry()) + + case .feedEntrySummary: + + if self.entries?.last?.summary == nil { + self.entries?.last?.summary = AtomFeedEntrySummary(attributes: attributes) + } + + case .feedEntryAuthor: + + if self.entries?.last?.authors == nil { + self.entries?.last?.authors = [] + } + + self.entries?.last?.authors?.append(AtomFeedEntryAuthor()) + + case .feedEntryContributor: + + if self.entries?.last?.contributors == nil { + self.entries?.last?.contributors = [] + } + + self.entries?.last?.contributors?.append(AtomFeedEntryContributor()) + + case .feedEntryLink: + + if self.entries?.last?.links == nil { + self.entries?.last?.links = [] + } + + self.entries?.last?.links?.append(AtomFeedEntryLink(attributes: attributes)) + + case .feedEntryCategory: + + if self.entries?.last?.categories == nil { + self.entries?.last?.categories = [] + } + + self.entries?.last?.categories?.append(AtomFeedEntryCategory(attributes: attributes)) + + case .feedEntryContent: + + if self.entries?.last?.content == nil { + self.entries?.last?.content = AtomFeedEntryContent(attributes: attributes) + } + + case .feedEntrySource: + + if self.entries?.last?.source == nil { + self.entries?.last?.source = AtomFeedEntrySource() + } + + // MARK: Media + + case + .feedEntryMediaThumbnail, + .feedEntryMediaContent, + .feedEntryMediaCommunity, + .feedEntryMediaCommunityMediaStarRating, + .feedEntryMediaCommunityMediaStatistics, + .feedEntryMediaCommunityMediaTags, + .feedEntryMediaComments, + .feedEntryMediaCommentsMediaComment, + .feedEntryMediaEmbed, + .feedEntryMediaEmbedMediaParam, + .feedEntryMediaResponses, + .feedEntryMediaResponsesMediaResponse, + .feedEntryMediaBackLinks, + .feedEntryMediaBackLinksBackLink, + .feedEntryMediaStatus, + .feedEntryMediaPrice, + .feedEntryMediaLicense, + .feedEntryMediaSubTitle, + .feedEntryMediaPeerLink, + .feedEntryMediaLocation, + .feedEntryMediaLocationPosition, + .feedEntryMediaRestriction, + .feedEntryMediaScenes, + .feedEntryMediaScenesMediaScene, + .feedEntryMediaGroup, + .feedEntryMediaGroupMediaCategory, + .feedEntryMediaGroupMediaCredit, + .feedEntryMediaGroupMediaRating, + .feedEntryMediaGroupMediaContent: + + if self.entries?.last?.media == nil { + self.entries?.last?.media = MediaNamespace() + } + + switch path { + + case .feedEntryMediaThumbnail: + + if self.entries?.last?.media?.mediaThumbnails == nil { + self.entries?.last?.media?.mediaThumbnails = [] + } + + self.entries?.last?.media?.mediaThumbnails?.append(MediaThumbnail(attributes: attributes)) + + case .feedEntryMediaContent: + + if self.entries?.last?.media?.mediaContents == nil { + self.entries?.last?.media?.mediaContents = [] + } + + self.entries?.last?.media?.mediaContents?.append(MediaContent(attributes: attributes)) + + case .feedEntryMediaCommunity: + + if self.entries?.last?.media?.mediaCommunity == nil { + self.entries?.last?.media?.mediaCommunity = MediaCommunity() + } + + case .feedEntryMediaCommunityMediaStarRating: + + if self.entries?.last?.media?.mediaCommunity?.mediaStarRating == nil { + self.entries?.last?.media?.mediaCommunity?.mediaStarRating = MediaStarRating(attributes: attributes) + } + + case .feedEntryMediaCommunityMediaStatistics: + + if self.entries?.last?.media?.mediaCommunity?.mediaStatistics == nil { + self.entries?.last?.media?.mediaCommunity?.mediaStatistics = MediaStatistics(attributes: attributes) + } + + case .feedEntryMediaCommunityMediaTags: + + if self.entries?.last?.media?.mediaCommunity?.mediaTags == nil { + self.entries?.last?.media?.mediaCommunity?.mediaTags = [] + } + + case .feedEntryMediaComments: + + if self.entries?.last?.media?.mediaComments == nil { + self.entries?.last?.media?.mediaComments = [] + } + + case .feedEntryMediaEmbed: + + if self.entries?.last?.media?.mediaEmbed == nil { + self.entries?.last?.media?.mediaEmbed = MediaEmbed(attributes: attributes) + } + + case .feedEntryMediaEmbedMediaParam: + + if self.entries?.last?.media?.mediaEmbed?.mediaParams == nil { + self.entries?.last?.media?.mediaEmbed?.mediaParams = [] + } + + self.entries?.last?.media?.mediaEmbed?.mediaParams?.append(MediaParam(attributes: attributes)) + + case .feedEntryMediaResponses: + + if self.entries?.last?.media?.mediaResponses == nil { + self.entries?.last?.media?.mediaResponses = [] + } + + case .feedEntryMediaBackLinks: + + if self.entries?.last?.media?.mediaBackLinks == nil { + self.entries?.last?.media?.mediaBackLinks = [] + } + + case .feedEntryMediaStatus: + + if self.entries?.last?.media?.mediaStatus == nil { + self.entries?.last?.media?.mediaStatus = MediaStatus(attributes: attributes) + } + + case .feedEntryMediaPrice: + + if self.entries?.last?.media?.mediaPrices == nil { + self.entries?.last?.media?.mediaPrices = [] + } + + self.entries?.last?.media?.mediaPrices?.append(MediaPrice(attributes: attributes)) + + case .feedEntryMediaLicense: + + if self.entries?.last?.media?.mediaLicense == nil { + self.entries?.last?.media?.mediaLicense = MediaLicence(attributes: attributes) + } + + case .feedEntryMediaSubTitle: + + if self.entries?.last?.media?.mediaSubTitle == nil { + self.entries?.last?.media?.mediaSubTitle = MediaSubTitle(attributes: attributes) + } + + case .feedEntryMediaPeerLink: + + if self.entries?.last?.media?.mediaPeerLink == nil { + self.entries?.last?.media?.mediaPeerLink = MediaPeerLink(attributes: attributes) + } + + case .feedEntryMediaLocation: + + if self.entries?.last?.media?.mediaLocation == nil { + self.entries?.last?.media?.mediaLocation = MediaLocation(attributes: attributes) + } + + case .feedEntryMediaRestriction: + + if self.entries?.last?.media?.mediaRestriction == nil { + self.entries?.last?.media?.mediaRestriction = MediaRestriction(attributes: attributes) + } + + case .feedEntryMediaScenes: + + if self.entries?.last?.media?.mediaScenes == nil { + self.entries?.last?.media?.mediaScenes = [] + } + + case .feedEntryMediaScenesMediaScene: + + if self.entries?.last?.media?.mediaScenes == nil { + self.entries?.last?.media?.mediaScenes = [] + } + + self.entries?.last?.media?.mediaScenes?.append(MediaScene()) + + case .feedEntryMediaGroup: + + if self.entries?.last?.media?.mediaGroup == nil { + self.entries?.last?.media?.mediaGroup = MediaGroup() + } + + case .feedEntryMediaGroupMediaCategory: + + if self.entries?.last?.media?.mediaGroup?.mediaCategory == nil { + self.entries?.last?.media?.mediaGroup?.mediaCategory = MediaCategory(attributes: attributes) + } + + case .feedEntryMediaGroupMediaCredit: + + if self.entries?.last?.media?.mediaGroup?.mediaCredits == nil { + self.entries?.last?.media?.mediaGroup?.mediaCredits = [] + } + + self.entries?.last?.media?.mediaGroup?.mediaCredits?.append(MediaCredit(attributes: attributes)) + + case .feedEntryMediaGroupMediaRating: + + if self.entries?.last?.media?.mediaGroup?.mediaRating == nil { + self.entries?.last?.media?.mediaGroup?.mediaRating = MediaRating(attributes: attributes) + } + + case .feedEntryMediaGroupMediaContent: + + if self.entries?.last?.media?.mediaGroup?.mediaContents == nil { + self.entries?.last?.media?.mediaGroup?.mediaContents = [] + } + + self.entries?.last?.media?.mediaGroup?.mediaContents?.append(MediaContent(attributes: attributes)) + + default: break + + } + + default: break + + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapCharacters.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapCharacters.swift new file mode 100644 index 0000000..73f1c4a --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed + mapCharacters.swift @@ -0,0 +1,86 @@ +// +// AtomFeed + mapCharacters.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension AtomFeed { + + /// Maps the characters in the specified string to the `AtomFeed` model. + /// + /// - Parameters: + /// - string: The string to map to the model. + /// - path: The path of feed's element. + func map(_ string: String, for path: AtomPath) { + switch path { + case .feedTitle: self.title = self.title?.appending(string) ?? string + case .feedSubtitle: self.subtitle?.value = self.subtitle?.value?.appending(string) ?? string + case .feedUpdated: self.updated = string.toPermissiveDate() + case .feedAuthorName: self.authors?.last?.name = self.authors?.last?.name?.appending(string) ?? string + case .feedAuthorEmail: self.authors?.last?.email = self.authors?.last?.email?.appending(string) ?? string + case .feedAuthorUri: self.authors?.last?.uri = self.authors?.last?.uri?.appending(string) ?? string + case .feedContributorName: self.contributors?.last?.name = self.contributors?.last?.name?.appending(string) ?? string + case .feedContributorEmail: self.contributors?.last?.email = self.contributors?.last?.email?.appending(string) ?? string + case .feedContributorUri: self.contributors?.last?.uri = self.contributors?.last?.uri?.appending(string) ?? string + case .feedID: self.id = self.id?.appending(string) ?? string + case .feedGenerator: self.generator?.value = self.generator?.value?.appending(string) ?? string + case .feedIcon: self.icon = self.icon?.appending(string) ?? string + case .feedLogo: self.logo = self.logo?.appending(string) ?? string + case .feedRights: self.rights = self.rights?.appending(string) ?? string + case .feedEntryTitle: self.entries?.last?.title = self.entries?.last?.title?.appending(string) ?? string + case .feedEntrySummary: self.entries?.last?.summary?.value = self.entries?.last?.summary?.value?.appending(string) ?? string + case .feedEntryUpdated: self.entries?.last?.updated = string.toPermissiveDate() + case .feedEntryID: self.entries?.last?.id = self.entries?.last?.id?.appending(string) ?? string + case .feedEntryContent: self.entries?.last?.content?.value = self.entries?.last?.content?.value?.appending(string) ?? string + case .feedEntryPublished: self.entries?.last?.published = string.toPermissiveDate() + case .feedEntrySourceID: self.entries?.last?.source?.id = self.entries?.last?.source?.id?.appending(string) ?? string + case .feedEntrySourceTitle: self.entries?.last?.source?.title = self.entries?.last?.source?.title?.appending(string) ?? string + case .feedEntrySourceUpdated: self.entries?.last?.source?.updated = string.toPermissiveDate() + case .feedEntryRights: self.entries?.last?.rights = self.entries?.last?.rights?.appending(string) ?? string + case .feedEntryAuthorName: self.entries?.last?.authors?.last?.name = self.entries?.last?.authors?.last?.name?.appending(string) ?? string + case .feedEntryAuthorEmail: self.entries?.last?.authors?.last?.email = self.entries?.last?.authors?.last?.email?.appending(string) ?? string + case .feedEntryAuthorUri: self.entries?.last?.authors?.last?.uri = self.entries?.last?.authors?.last?.uri?.appending(string) ?? string + case .feedEntryContributorName: self.entries?.last?.contributors?.last?.name = self.entries?.last?.contributors?.last?.name?.appending(string) ?? string + case .feedEntryContributorEmail: self.entries?.last?.contributors?.last?.email = self.entries?.last?.contributors?.last?.email?.appending(string) ?? string + case .feedEntryContributorUri: self.entries?.last?.contributors?.last?.uri = self.entries?.last?.contributors?.last?.uri?.appending(string) ?? string + case .feedEntryMediaThumbnail: self.entries?.last?.media?.mediaThumbnails?.last?.value = self.entries?.last?.media?.mediaThumbnails?.last?.value?.appending(string) ?? string + case .feedEntryMediaLicense: self.entries?.last?.media?.mediaLicense?.value = self.entries?.last?.media?.mediaLicense?.value?.appending(string) ?? string + case .feedEntryMediaRestriction: self.entries?.last?.media?.mediaRestriction?.value = self.entries?.last?.media?.mediaRestriction?.value?.appending(string) ?? string + case .feedEntryMediaCommunityMediaTags: self.entries?.last?.media?.mediaCommunity?.mediaTags = MediaTag.tagsFrom(string: string) + case .feedEntryMediaCommentsMediaComment: self.entries?.last?.media?.mediaComments?.append(string) + case .feedEntryMediaEmbedMediaParam: self.entries?.last?.media?.mediaEmbed?.mediaParams?.last?.value = self.entries?.last?.media?.mediaEmbed?.mediaParams?.last?.value?.appending(string) ?? string + case .feedEntryMediaGroupMediaCredit: self.entries?.last?.media?.mediaGroup?.mediaCredits?.last?.value = self.entries?.last?.media?.mediaGroup?.mediaCredits?.last?.value?.appending(string) ?? string + case .feedEntryMediaGroupMediaCategory: self.entries?.last?.media?.mediaGroup?.mediaCategory?.value = self.entries?.last?.media?.mediaGroup?.mediaCategory?.value?.appending(string) ?? string + case .feedEntryMediaGroupMediaRating: self.entries?.last?.media?.mediaGroup?.mediaRating?.value = self.entries?.last?.media?.mediaGroup?.mediaRating?.value?.appending(string) ?? string + case .feedEntryMediaResponsesMediaResponse: self.entries?.last?.media?.mediaResponses?.append(string) + case .feedEntryMediaBackLinksBackLink: self.entries?.last?.media?.mediaBackLinks?.append(string) + case .feedEntryMediaLocationPosition: self.entries?.last?.media?.mediaLocation?.mapFrom(latLng: string) + case .feedEntryMediaScenesMediaSceneSceneTitle: self.entries?.last?.media?.mediaScenes?.last?.sceneTitle = self.entries?.last?.media?.mediaScenes?.last?.sceneTitle?.appending(string) ?? string + case .feedEntryMediaScenesMediaSceneSceneDescription: self.entries?.last?.media?.mediaScenes?.last?.sceneDescription = self.entries?.last?.media?.mediaScenes?.last?.sceneDescription?.appending(string) ?? string + case .feedEntryMediaScenesMediaSceneSceneStartTime: self.entries?.last?.media?.mediaScenes?.last?.sceneStartTime = string.toDuration() + case .feedEntryMediaScenesMediaSceneSceneEndTime: self.entries?.last?.media?.mediaScenes?.last?.sceneEndTime = string.toDuration() + default: break + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed.swift new file mode 100644 index 0000000..dcc744d --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeed.swift @@ -0,0 +1,190 @@ +// +// AtomFeed.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Data model for the XML DOM of the Atom Specification +/// See https://tools.ietf.org/html/rfc4287 +/// +/// The "atom:feed" element is the document (i.e., top-level) element of +/// an Atom Feed Document, acting as a container for metadata and data +/// associated with the feed. Its element children consist of metadata +/// elements followed by zero or more atom:entry child elements. +open class AtomFeed { + + /// The "atom:title" element is a Text construct that conveys a human- + /// readable title for an entry or feed. + public var title: String? + + /// The "atom:subtitle" element is a Text construct that conveys a human- + /// readable description or subtitle for a feed. + public var subtitle: AtomFeedSubtitle? + + /// The "atom:link" element defines a reference from an entry or feed to + /// a Web resource. This specification assigns no meaning to the content + /// (if any) of this element. + public var links: [AtomFeedLink]? + + /// The "atom:updated" element is a Date construct indicating the most + /// recent instant in time when an entry or feed was modified in a way + /// the publisher considers significant. Therefore, not all + /// modifications necessarily result in a changed atom:updated value. + public var updated: Date? + + /// The "atom:category" element conveys information about a category + /// associated with an entry or feed. This specification assigns no + /// meaning to the content (if any) of this element. + public var categories: [AtomFeedCategory]? + + /// The "atom:author" element is a Person construct that indicates the + /// author of the entry or feed. + /// + /// If an atom:entry element does not contain atom:author elements, then + /// the atom:author elements of the contained atom:source element are + /// considered to apply. In an Atom Feed Document, the atom:author + /// elements of the containing atom:feed element are considered to apply + /// to the entry if there are no atom:author elements in the locations + /// described above. + public var authors: [AtomFeedAuthor]? + + /// The "atom:contributor" element is a Person construct that indicates a + /// person or other entity who contributed to the entry or feed. + public var contributors: [AtomFeedContributor]? + + /// The "atom:id" element conveys a permanent, universally unique + /// identifier for an entry or feed. + /// + /// Its content MUST be an IRI, as defined by [RFC3987]. Note that the + /// definition of "IRI" excludes relative references. Though the IRI + /// might use a dereferencable scheme, Atom Processors MUST NOT assume it + /// can be dereferenced. + /// + /// When an Atom Document is relocated, migrated, syndicated, + /// republished, exported, or imported, the content of its atom:id + /// element MUST NOT change. Put another way, an atom:id element + /// pertains to all instantiations of a particular Atom entry or feed; + /// revisions retain the same content in their atom:id elements. It is + /// suggested that the atom:id element be stored along with the + /// associated resource. + /// + /// The content of an atom:id element MUST be created in a way that + /// assures uniqueness. + /// + /// Because of the risk of confusion between IRIs that would be + /// equivalent if they were mapped to URIs and dereferenced, the + /// following normalization strategy SHOULD be applied when generating + /// atom:id elements: + /// + /// - Provide the scheme in lowercase characters. + /// - Provide the host, if any, in lowercase characters. + /// - Only perform percent-encoding where it is essential. + /// - Use uppercase A through F characters when percent-encoding. + /// - Prevent dot-segments from appearing in paths. + /// - For schemes that define a default authority, use an empty + /// authority if the default is desired. + /// - For schemes that define an empty path to be equivalent to a path + /// of "/", use "/". + /// - For schemes that define a port, use an empty port if the default + /// is desired. + /// - Preserve empty fragment identifiers and queries. + /// - Ensure that all components of the IRI are appropriately character + /// normalized, e.g., by using NFC or NFKC. + public var id: String? + + /// The "atom:generator" element's content identifies the agent used to + /// generate a feed, for debugging and other purposes. + /// + /// The content of this element, when present, MUST be a string that is a + /// human-readable name for the generating agent. Entities such as + /// "&" and "<" represent their corresponding characters ("&" and + /// "<" respectively), not markup. + /// + /// The atom:generator element MAY have a "uri" attribute whose value + /// MUST be an IRI reference [RFC3987]. When dereferenced, the resulting + /// URI (mapped from an IRI, if necessary) SHOULD produce a + /// representation that is relevant to that agent. + /// + /// The atom:generator element MAY have a "version" attribute that + /// indicates the version of the generating agent. + public var generator: AtomFeedGenerator? + + /// The "atom:icon" element's content is an IRI reference [RFC3987] that + /// identifies an image that provides iconic visual identification for a + /// feed. + /// + /// The image SHOULD have an aspect ratio of one (horizontal) to one + /// (vertical) and SHOULD be suitable for presentation at a small size. + public var icon: String? + + /// The "atom:logo" element's content is an IRI reference [RFC3987] that + /// identifies an image that provides visual identification for a feed. + /// + /// The image SHOULD have an aspect ratio of 2 (horizontal) to 1 + /// (vertical). + public var logo: String? + + /// The "atom:rights" element is a Text construct that conveys + /// information about rights held in and over an entry or feed. + /// + /// The atom:rights element SHOULD NOT be used to convey machine-readable + /// licensing information. + /// + /// If an atom:entry element does not contain an atom:rights element, + /// then the atom:rights element of the containing atom:feed element, if + /// present, is considered to apply to the entry. + public var rights: String? + + /// The "atom:entry" element represents an individual entry, acting as a + /// container for metadata and data associated with the entry. This + /// element can appear as a child of the atom:feed element, or it can + /// appear as the document (i.e., top-level) element of a stand-alone + /// Atom Entry Document. + public var entries: [AtomFeedEntry]? + + public init() { } + +} + +// MARK: - Equatable + +extension AtomFeed: Equatable { + + public static func ==(lhs: AtomFeed, rhs: AtomFeed) -> Bool { + return + lhs.title == rhs.title && + lhs.subtitle == rhs.subtitle && + lhs.links == rhs.links && + lhs.updated == rhs.updated && + lhs.categories == rhs.categories && + lhs.authors == rhs.authors && + lhs.contributors == rhs.contributors && + lhs.id == rhs.id && + lhs.generator == rhs.generator && + lhs.icon == rhs.icon && + lhs.logo == rhs.logo && + lhs.rights == rhs.rights && + lhs.entries == rhs.entries + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedAuthor.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedAuthor.swift new file mode 100644 index 0000000..c374319 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedAuthor.swift @@ -0,0 +1,70 @@ +// +// AtomFeedAuthor.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:author" element is a Person construct that indicates the +/// author of the entry or feed. +/// +/// If an atom:entry element does not contain atom:author elements, then +/// the atom:author elements of the contained atom:source element are +/// considered to apply. In an Atom Feed Document, the atom:author +/// elements of the containing atom:feed element are considered to apply +/// to the entry if there are no atom:author elements in the locations +/// described above. +public class AtomFeedAuthor { + + /// The "atom:name" element's content conveys a human-readable name for + /// the person. The content of atom:name is Language-Sensitive. Person + /// constructs MUST contain exactly one "atom:name" element. + public var name: String? + + /// The "atom:email" element's content conveys an e-mail address + /// associated with the person. Person constructs MAY contain an + /// atom:email element, but MUST NOT contain more than one. Its content + /// MUST conform to the "addr-spec" production in [RFC2822]. + public var email: String? + + /// The "atom:uri" element's content conveys an IRI associated with the + /// person. Person constructs MAY contain an atom:uri element, but MUST + /// NOT contain more than one. The content of atom:uri in a Person + /// construct MUST be an IRI reference [RFC3987]. + public var uri: String? + + public init() { } + +} + +// MARK: - Equatable + +extension AtomFeedAuthor: Equatable { + + public static func ==(lhs: AtomFeedAuthor, rhs: AtomFeedAuthor) -> Bool { + return + lhs.name == rhs.name && + lhs.email == rhs.email && + lhs.uri == rhs.uri + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedCategory.swift new file mode 100644 index 0000000..f55372e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedCategory.swift @@ -0,0 +1,109 @@ +// +// AtomFeedCategory.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:category" element conveys information about a category +/// associated with an entry or feed. This specification assigns no +/// meaning to the content (if any) of this element. +public class AtomFeedCategory { + + /// The element's attributes. + public class Attributes { + + /// The "term" attribute is a string that identifies the category to + /// which the entry or feed belongs. Category elements MUST have a + /// "term" attribute. + public var term: String? + + /// The "scheme" attribute is an IRI that identifies a categorization + /// scheme. Category elements MAY have a "scheme" attribute. + public var scheme: String? + + /// The "label" attribute provides a human-readable label for display in + /// end-user applications. The content of the "label" attribute is + /// Language-Sensitive. Entities such as "&" and "<" represent + /// their corresponding characters ("&" and "<", respectively), not + /// markup. Category elements MAY have a "label" attribute. + public var label: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension AtomFeedCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedCategory.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.term = attributeDict["term"] + self.scheme = attributeDict["scheme"] + self.label = attributeDict["label"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedCategory: Equatable { + + public static func ==(lhs: AtomFeedCategory, rhs: AtomFeedCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedCategory.Attributes: Equatable { + + public static func ==(lhs: AtomFeedCategory.Attributes, rhs: AtomFeedCategory.Attributes) -> Bool { + return + lhs.term == rhs.term && + lhs.scheme == rhs.scheme && + lhs.label == rhs.label + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedContributor.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedContributor.swift new file mode 100644 index 0000000..5ca1d8b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedContributor.swift @@ -0,0 +1,63 @@ +// +// AtomFeedContributor.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:contributor" element is a Person construct that indicates a +/// person or other entity who contributed to the entry or feed. +public class AtomFeedContributor { + + /// The "atom:name" element's content conveys a human-readable name for + /// the person. The content of atom:name is Language-Sensitive. Person + /// constructs MUST contain exactly one "atom:name" element. + public var name: String? + + /// The "atom:email" element's content conveys an e-mail address + /// associated with the person. Person constructs MAY contain an + /// atom:email element, but MUST NOT contain more than one. Its content + /// MUST conform to the "addr-spec" production in [RFC2822]. + public var email: String? + + /// The "atom:uri" element's content conveys an IRI associated with the + /// person. Person constructs MAY contain an atom:uri element, but MUST + /// NOT contain more than one. The content of atom:uri in a Person + /// construct MUST be an IRI reference [RFC3987]. + public var uri: String? + + public init() { } + +} + +// MARK: - Equatable + +extension AtomFeedContributor: Equatable { + + public static func ==(lhs: AtomFeedContributor, rhs: AtomFeedContributor) -> Bool { + return + lhs.name == rhs.name && + lhs.email == rhs.email && + lhs.uri == rhs.uri + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntry.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntry.swift new file mode 100644 index 0000000..35fd935 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntry.swift @@ -0,0 +1,191 @@ +// +// AtomFeedEntry.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:entry" element represents an individual entry, acting as a +/// container for metadata and data associated with the entry. This +/// element can appear as a child of the atom:feed element, or it can +/// appear as the document (i.e., top-level) element of a stand-alone +/// Atom Entry Document. +public class AtomFeedEntry { + + /// The "atom:title" element is a Text construct that conveys a human- + /// readable title for an entry or feed. + public var title: String? + + /// The "atom:summary" element is a Text construct that conveys a short + /// summary, abstract, or excerpt of an entry. + /// + /// atomSummary = element atom:summary { atomTextConstruct } + /// + /// It is not advisable for the atom:summary element to duplicate + /// atom:title or atom:content because Atom Processors might assume there + /// is a useful summary when there is none. + public var summary: AtomFeedEntrySummary? + + /// The "atom:author" element is a Person construct that indicates the + /// author of the entry or feed. + /// + /// If an atom:entry element does not contain atom:author elements, then + /// the atom:author elements of the contained atom:source element are + /// considered to apply. In an Atom Feed Document, the atom:author + /// elements of the containing atom:feed element are considered to apply + /// to the entry if there are no atom:author elements in the locations + /// described above. + public var authors: [AtomFeedEntryAuthor]? + + /// The "atom:contributor" element is a Person construct that indicates a + /// person or other entity who contributed to the entry or feed. + public var contributors: [AtomFeedEntryContributor]? + + /// The "atom:link" element defines a reference from an entry or feed to + /// a Web resource. This specification assigns no meaning to the content + /// (if any) of this element. + public var links: [AtomFeedEntryLink]? + + /// The "atom:updated" element is a Date construct indicating the most + /// recent instant in time when an entry or feed was modified in a way + /// the publisher considers significant. Therefore, not all + /// modifications necessarily result in a changed atom:updated value. + /// + /// Publishers MAY change the value of this element over time. + public var updated: Date? + + /// The "atom:category" element conveys information about a category + /// associated with an entry or feed. This specification assigns no + /// meaning to the content (if any) of this element. + public var categories: [AtomFeedEntryCategory]? + + /// The "atom:id" element conveys a permanent, universally unique + /// identifier for an entry or feed. + /// + /// Its content MUST be an IRI, as defined by [RFC3987]. Note that the + /// definition of "IRI" excludes relative references. Though the IRI + /// might use a dereferencable scheme, Atom Processors MUST NOT assume it + /// can be dereferenced. + /// + /// When an Atom Document is relocated, migrated, syndicated, + /// republished, exported, or imported, the content of its atom:id + /// element MUST NOT change. Put another way, an atom:id element + /// pertains to all instantiations of a particular Atom entry or feed; + /// revisions retain the same content in their atom:id elements. It is + /// suggested that the atom:id element be stored along with the + /// associated resource. + /// + /// The content of an atom:id element MUST be created in a way that + /// assures uniqueness. + /// + /// Because of the risk of confusion between IRIs that would be + /// equivalent if they were mapped to URIs and dereferenced, the + /// following normalization strategy SHOULD be applied when generating + /// atom:id elements: + /// + /// - Provide the scheme in lowercase characters. + /// - Provide the host, if any, in lowercase characters. + /// - Only perform percent-encoding where it is essential. + /// - Use uppercase A through F characters when percent-encoding. + /// - Prevent dot-segments from appearing in paths. + /// - For schemes that define a default authority, use an empty + /// authority if the default is desired. + /// - For schemes that define an empty path to be equivalent to a path + /// of "/", use "/". + /// - For schemes that define a port, use an empty port if the default + /// is desired. + /// - Preserve empty fragment identifiers and queries. + /// - Ensure that all components of the IRI are appropriately character + /// normalized, e.g., by using NFC or NFKC. + public var id: String? + + /// The "atom:content" element either contains or links to the content of + /// the entry. The content of atom:content is Language-Sensitive. + public var content: AtomFeedEntryContent? + + /// The "atom:published" element is a Date construct indicating an + /// instant in time associated with an event early in the life cycle of + /// the entry. + /// + /// Typically, atom:published will be associated with the initial + /// creation or first availability of the resource. + public var published: Date? + + /// If an atom:entry is copied from one feed into another feed, then the + /// source atom:feed's metadata (all child elements of atom:feed other + /// than the atom:entry elements) MAY be preserved within the copied + /// entry by adding an atom:source child element, if it is not already + /// present in the entry, and including some or all of the source feed's + /// Metadata elements as the atom:source element's children. Such + /// metadata SHOULD be preserved if the source atom:feed contains any of + /// the child elements atom:author, atom:contributor, atom:rights, or + /// atom:category and those child elements are not present in the source + /// atom:entry. + /// + /// The atom:source element is designed to allow the aggregation of + /// entries from different feeds while retaining information about an + /// entry's source feed. For this reason, Atom Processors that are + /// performing such aggregation SHOULD include at least the required + /// feed-level Metadata elements (atom:id, atom:title, and atom:updated) + /// in the atom:source element. + public var source: AtomFeedEntrySource? + + /// The "atom:rights" element is a Text construct that conveys + /// information about rights held in and over an entry or feed. + /// + /// The atom:rights element SHOULD NOT be used to convey machine-readable + /// licensing information. + /// + /// If an atom:entry element does not contain an atom:rights element, + /// then the atom:rights element of the containing atom:feed element, if + /// present, is considered to apply to the entry. + public var rights: String? + + /// Media RSS is a new RSS module that supplements the + /// capabilities of RSS 2.0. + public var media: MediaNamespace? + + public init() { } + +} + +// MARK: - Equatable + +extension AtomFeedEntry: Equatable { + + public static func ==(lhs: AtomFeedEntry, rhs: AtomFeedEntry) -> Bool { + return + lhs.title == rhs.title && + lhs.summary == rhs.summary && + lhs.authors == rhs.authors && + lhs.contributors == rhs.contributors && + lhs.links == rhs.links && + lhs.updated == rhs.updated && + lhs.categories == rhs.categories && + lhs.id == rhs.id && + lhs.content == rhs.content && + lhs.published == rhs.published && + lhs.source == rhs.source && + lhs.rights == rhs.rights + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryAuthor.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryAuthor.swift new file mode 100644 index 0000000..26c5564 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryAuthor.swift @@ -0,0 +1,70 @@ +// +// AtomFeedEntryAuthor.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:author" element is a Person construct that indicates the +/// author of the entry or feed. +/// +/// If an atom:entry element does not contain atom:author elements, then +/// the atom:author elements of the contained atom:source element are +/// considered to apply. In an Atom Feed Document, the atom:author +/// elements of the containing atom:feed element are considered to apply +/// to the entry if there are no atom:author elements in the locations +/// described above. +public class AtomFeedEntryAuthor { + + /// The "atom:name" element's content conveys a human-readable name for + /// the person. The content of atom:name is Language-Sensitive. Person + /// constructs MUST contain exactly one "atom:name" element. + public var name: String? + + /// The "atom:email" element's content conveys an e-mail address + /// associated with the person. Person constructs MAY contain an + /// atom:email element, but MUST NOT contain more than one. Its content + /// MUST conform to the "addr-spec" production in [RFC2822]. + public var email: String? + + /// The "atom:uri" element's content conveys an IRI associated with the + /// person. Person constructs MAY contain an atom:uri element, but MUST + /// NOT contain more than one. The content of atom:uri in a Person + /// construct MUST be an IRI reference [RFC3987]. + public var uri: String? + + public init() { } + +} + +// MARK: - Equatable + +extension AtomFeedEntryAuthor: Equatable { + + public static func ==(lhs: AtomFeedEntryAuthor, rhs: AtomFeedEntryAuthor) -> Bool { + return + lhs.name == rhs.name && + lhs.email == rhs.email && + lhs.uri == rhs.uri + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryCategory.swift new file mode 100644 index 0000000..943ac5b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryCategory.swift @@ -0,0 +1,108 @@ +// +// AtomFeedEntryCategory.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:category" element conveys information about a category +/// associated with an entry or feed. This specification assigns no +/// meaning to the content (if any) of this element. +public class AtomFeedEntryCategory { + + /// The element's attributes + public class Attributes { + + /// The "term" attribute is a string that identifies the category to + /// which the entry or feed belongs. Category elements MUST have a + /// "term" attribute. + public var term: String? + + /// The "scheme" attribute is an IRI that identifies a categorization + /// scheme. Category elements MAY have a "scheme" attribute. + public var scheme: String? + + /// The "label" attribute provides a human-readable label for display in + /// end-user applications. The content of the "label" attribute is + /// Language-Sensitive. Entities such as "&" and "<" represent + /// their corresponding characters ("&" and "<", respectively), not + /// markup. Category elements MAY have a "label" attribute. + public var label: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension AtomFeedEntryCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedEntryCategory.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedEntryCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.term = attributeDict["term"] + self.scheme = attributeDict["scheme"] + self.label = attributeDict["label"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedEntryCategory: Equatable { + + public static func ==(lhs: AtomFeedEntryCategory, rhs: AtomFeedEntryCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedEntryCategory.Attributes: Equatable { + + public static func ==(lhs: AtomFeedEntryCategory.Attributes, rhs: AtomFeedEntryCategory.Attributes) -> Bool { + return + lhs.term == rhs.term && + lhs.scheme == rhs.scheme && + lhs.label == rhs.label + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContent.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContent.swift new file mode 100644 index 0000000..0a1c70b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContent.swift @@ -0,0 +1,116 @@ +// +// AtomFeedEntryContent.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:content" element either contains or links to the content of +/// the entry. The content of atom:content is Language-Sensitive. +public class AtomFeedEntryContent { + + /// The element's attributes. + public class Attributes { + + /// On the atom:content element, the value of the "type" attribute MAY be + /// one of "text", "html", or "xhtml". Failing that, it MUST conform to + /// the syntax of a MIME media type, but MUST NOT be a composite type + /// (see Section 4.2.6 of [MIMEREG]). If neither the type attribute nor + /// the src attribute is provided, Atom Processors MUST behave as though + /// the type attribute were present with a value of "text". + public var type: String? + + /// The atom:content MAY have a "src" attribute, whose value MUST be an IRI + /// reference [RFC3987]. If the "src" attribute is present, atom:content + /// MUST be empty. Atom Processors MAY use the IRI to retrieve the + /// content and MAY choose to ignore remote content or to present it in a + /// different manner than local content. + /// + /// If the "src" attribute is present, the "type" attribute SHOULD be + /// provided and MUST be a MIME media type [MIMEREG], rather than "text", + /// "html", or "xhtml". The value is advisory; that is to say, when the + /// corresponding URI (mapped from an IRI, if necessary) is dereferenced, + /// if the server providing that content also provides a media type, the + /// server-provided media type is authoritative. + public var src: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension AtomFeedEntryContent { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedEntryContent.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedEntryContent.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.src = attributeDict["src"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedEntryContent: Equatable { + + public static func ==(lhs: AtomFeedEntryContent, rhs: AtomFeedEntryContent) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedEntryContent.Attributes: Equatable { + + public static func ==(lhs: AtomFeedEntryContent.Attributes, rhs: AtomFeedEntryContent.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.src == rhs.src + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContributor.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContributor.swift new file mode 100644 index 0000000..b7e5e62 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryContributor.swift @@ -0,0 +1,63 @@ +// +// AtomFeedEntryContributor.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:contributor" element is a Person construct that indicates a +/// person or other entity who contributed to the entry or feed. +public class AtomFeedEntryContributor { + + /// The "atom:name" element's content conveys a human-readable name for + /// the person. The content of atom:name is Language-Sensitive. Person + /// constructs MUST contain exactly one "atom:name" element. + public var name: String? + + /// The "atom:email" element's content conveys an e-mail address + /// associated with the person. Person constructs MAY contain an + /// atom:email element, but MUST NOT contain more than one. Its content + /// MUST conform to the "addr-spec" production in [RFC2822]. + public var email: String? + + /// The "atom:uri" element's content conveys an IRI associated with the + /// person. Person constructs MAY contain an atom:uri element, but MUST + /// NOT contain more than one. The content of atom:uri in a Person + /// construct MUST be an IRI reference [RFC3987]. + public var uri: String? + + public init() { } + +} + +// MARK: - Equatable + +extension AtomFeedEntryContributor: Equatable { + + public static func ==(lhs: AtomFeedEntryContributor, rhs: AtomFeedEntryContributor) -> Bool { + return + lhs.name == rhs.name && + lhs.email == rhs.email && + lhs.uri == rhs.uri + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryLink.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryLink.swift new file mode 100644 index 0000000..1b30ff8 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntryLink.swift @@ -0,0 +1,185 @@ +// +// AtomFeedEntryLink.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:link" element defines a reference from an entry or feed to +/// a Web resource. This specification assigns no meaning to the content +/// (if any) of this element. +public class AtomFeedEntryLink { + + /// The element's attributes + public class Attributes { + + /// The "href" attribute contains the link's IRI. atom:link elements MUST + /// have an href attribute, whose value MUST be a IRI reference + /// [RFC3987]. + public var href: String? + + /// The atom:link elements MAY have a "rel" attribute that indicates the link + /// relation type. If the "rel" attribute is not present, the link + /// element MUST be interpreted as if the link relation type is + /// "alternate". + /// + /// The value of "rel" MUST be a string that is non-empty and matches + /// either the "isegment-nz-nc" or the "IRI" production in [RFC3987]. + /// Note that use of a relative reference other than a simple name is not + /// allowed. If a name is given, implementations MUST consider the link + /// relation type equivalent to the same name registered within the IANA + /// Registry of Link Relations (Section 7), and thus to the IRI that + /// would be obtained by appending the value of the rel attribute to the + /// string "http://www.iana.org/assignments/relation/". The value of + /// "rel" describes the meaning of the link, but does not impose any + /// behavioral requirements on Atom Processors. + /// + /// This document defines five initial values for the Registry of Link + /// Relations: + /// + /// 1. The value "alternate" signifies that the IRI in the value of the + /// href attribute identifies an alternate version of the resource + /// described by the containing element. + /// + /// 2. The value "related" signifies that the IRI in the value of the + /// href attribute identifies a resource related to the resource + /// described by the containing element. For example, the feed for a + /// site that discusses the performance of the search engine at + /// "http://search.example.com" might contain, as a child of + /// atom:feed: + /// + /// + /// + /// An identical link might appear as a child of any atom:entry whose + /// content contains a discussion of that same search engine. + /// + /// 3. The value "self" signifies that the IRI in the value of the href + /// attribute identifies a resource equivalent to the containing + /// element. + /// + /// 4. The value "enclosure" signifies that the IRI in the value of the + /// href attribute identifies a related resource that is potentially + /// large in size and might require special handling. For atom:link + /// elements with rel="enclosure", the length attribute SHOULD be + /// provided. + /// + /// 5. The value "via" signifies that the IRI in the value of the href + /// attribute identifies a resource that is the source of the + /// information provided in the containing element. + public var rel: String? + + /// On the link element, the "type" attribute's value is an advisory + /// media type: it is a hint about the type of the representation that is + /// expected to be returned when the value of the href attribute is + /// dereferenced. Note that the type attribute does not override the + /// actual media type returned with the representation. Link elements + /// MAY have a type attribute, whose value MUST conform to the syntax of + /// a MIME media type [MIMEREG]. + public var type: String? + + /// The "hreflang" attribute's content describes the language of the + /// resource pointed to by the href attribute. When used together with + /// the rel="alternate", it implies a translated version of the entry. + /// Link elements MAY have an hreflang attribute, whose value MUST be a + /// language tag [RFC3066]. + public var hreflang: String? + + /// The "title" attribute conveys human-readable information about the + /// link. The content of the "title" attribute is Language-Sensitive. + /// Entities such as "&" and "<" represent their corresponding + /// characters ("&" and "<", respectively), not markup. Link elements + /// MAY have a title attribute. + public var title: String? + + /// The "length" attribute indicates an advisory length of the linked + /// content in octets; it is a hint about the content length of the + /// representation returned when the IRI in the href attribute is mapped + /// to a URI and dereferenced. Note that the length attribute does not + /// override the actual content length of the representation as reported + /// by the underlying protocol. Link elements MAY have a length + /// attribute. + public var length: Int64? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension AtomFeedEntryLink { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedEntryLink.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedEntryLink.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.href = attributeDict["href"] + self.hreflang = attributeDict["hreflang"] + self.type = attributeDict["type"] + self.rel = attributeDict["rel"] + self.title = attributeDict["title"] + self.length = Int64(attributeDict["length"] ?? "") + + } + +} + +// MARK: - Equatable + +extension AtomFeedEntryLink: Equatable { + + public static func ==(lhs: AtomFeedEntryLink, rhs: AtomFeedEntryLink) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedEntryLink.Attributes: Equatable { + + public static func ==(lhs: AtomFeedEntryLink.Attributes, rhs: AtomFeedEntryLink.Attributes) -> Bool { + return + lhs.href == rhs.href && + lhs.hreflang == rhs.hreflang && + lhs.type == rhs.type && + lhs.rel == rhs.rel && + lhs.title == rhs.title && + lhs.length == rhs.length + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySource.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySource.swift new file mode 100644 index 0000000..a693c2b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySource.swift @@ -0,0 +1,75 @@ +// +// AtomFeedEntrySource.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// If an atom:entry is copied from one feed into another feed, then the +/// source atom:feed's metadata (all child elements of atom:feed other +/// than the atom:entry elements) MAY be preserved within the copied +/// entry by adding an atom:source child element, if it is not already +/// present in the entry, and including some or all of the source feed's +/// Metadata elements as the atom:source element's children. Such +/// metadata SHOULD be preserved if the source atom:feed contains any of +/// the child elements atom:author, atom:contributor, atom:rights, or +/// atom:category and those child elements are not present in the source +/// atom:entry. +/// +/// The atom:source element is designed to allow the aggregation of +/// entries from different feeds while retaining information about an +/// entry's source feed. For this reason, Atom Processors that are +/// performing such aggregation SHOULD include at least the required +/// feed-level Metadata elements (atom:id, atom:title, and atom:updated) +/// in the atom:source element. +public class AtomFeedEntrySource { + + /// The "atom:id" element conveys a permanent, universally unique + /// identifier for an entry or feed. + public var id: String? + + /// The "atom:title" element is a Text construct that conveys a human- + /// readable title for an entry or feed. + public var title: String? + + /// The "atom:updated" element is a Date construct indicating the most + /// recent instant in time when an entry or feed was modified in a way + /// the publisher considers significant. Therefore, not all + /// modifications necessarily result in a changed atom:updated value. + public var updated: Date? + + public init() { } + +} + +// MARK: - Equatable + +extension AtomFeedEntrySource: Equatable { + + public static func ==(lhs: AtomFeedEntrySource, rhs: AtomFeedEntrySource) -> Bool { + return + lhs.id == rhs.id && + lhs.title == rhs.title && + lhs.updated == rhs.updated + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySummary.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySummary.swift new file mode 100644 index 0000000..a30e7dd --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedEntrySummary.swift @@ -0,0 +1,103 @@ +// +// AtomFeedEntrySummary.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:summary" element is a Text construct that conveys a short +/// summary, abstract, or excerpt of an enactry. +/// +/// atomSummary = element atom:summary { atomTextConstruct } +/// +/// It is not advisable for the atom:summary element to duplicate +/// atom:title or atom:content because Atom Processors might assume there +/// is a useful summary when there is none. +public class AtomFeedEntrySummary { + + /// The element's attributes. + public class Attributes { + + /// Text constructs MAY have a "type" attribute. When present, the value + /// MUST be one of "text", "html", or "xhtml". If the "type" attribute + /// is not provided, Atom Processors MUST behave as though it were + /// present with a value of "text". + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension AtomFeedEntrySummary { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedEntrySummary.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedEntrySummary.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedEntrySummary: Equatable { + + public static func ==(lhs: AtomFeedEntrySummary, rhs: AtomFeedEntrySummary) -> Bool { + return + lhs.attributes == rhs.attributes && + lhs.value == rhs.value + } + +} + +extension AtomFeedEntrySummary.Attributes: Equatable { + + public static func ==(lhs: AtomFeedEntrySummary.Attributes, rhs: AtomFeedEntrySummary.Attributes) -> Bool { + return lhs.type == rhs.type + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedGenerator.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedGenerator.swift new file mode 100644 index 0000000..5238543 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedGenerator.swift @@ -0,0 +1,117 @@ +// +// AtomFeedGenerator.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:generator" element's content identifies the agent used to +/// generate a feed, for debugging and other purposes. +/// +/// The content of this element, when present, MUST be a string that is a +/// human-readable name for the generating agent. Entities such as +/// "&" and "<" represent their corresponding characters ("&" and +/// "<" respectively), not markup. +/// +/// The atom:generator element MAY have a "uri" attribute whose value +/// MUST be an IRI reference [RFC3987]. When dereferenced, the resulting +/// URI (mapped from an IRI, if necessary) SHOULD produce a +/// representation that is relevant to that agent. +/// +/// The atom:generator element MAY have a "version" attribute that +/// indicates the version of the generating agent. +public class AtomFeedGenerator { + + /// The element's attributes. + public class Attributes { + + /// The atom:generator element MAY have a "uri" attribute whose value + /// MUST be an IRI reference [RFC3987]. When dereferenced, the resulting + /// URI (mapped from an IRI, if necessary) SHOULD produce a + /// representation that is relevant to that agent. + public var uri: String? + + /// The atom:generator element MAY have a "version" attribute that + /// indicates the version of the generating agent. + public var version: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension AtomFeedGenerator { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedGenerator.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedGenerator.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.uri = attributeDict["uri"] + self.version = attributeDict["version"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedGenerator: Equatable { + + public static func ==(lhs: AtomFeedGenerator, rhs: AtomFeedGenerator) -> Bool { + return + lhs.attributes == rhs.attributes && + lhs.value == rhs.value + } + +} + +extension AtomFeedGenerator.Attributes: Equatable { + + public static func ==(lhs: AtomFeedGenerator.Attributes, rhs: AtomFeedGenerator.Attributes) -> Bool { + return + lhs.uri == rhs.uri && + lhs.version == rhs.version + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedLink.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedLink.swift new file mode 100644 index 0000000..a353dac --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedLink.swift @@ -0,0 +1,185 @@ +// +// AtomFeedLink.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:link" element defines a reference from an entry or feed to +/// a Web resource. This specification assigns no meaning to the content +/// (if any) of this element. +public class AtomFeedLink { + + /// The element's attributes. + public class Attributes { + + /// The "href" attribute contains the link's IRI. atom:link elements MUST + /// have an href attribute, whose value MUST be a IRI reference + /// [RFC3987]. + public var href: String? + + /// The atom:link elements MAY have a "rel" attribute that indicates the link + /// relation type. If the "rel" attribute is not present, the link + /// element MUST be interpreted as if the link relation type is + /// "alternate". + /// + /// The value of "rel" MUST be a string that is non-empty and matches + /// either the "isegment-nz-nc" or the "IRI" production in [RFC3987]. + /// Note that use of a relative reference other than a simple name is not + /// allowed. If a name is given, implementations MUST consider the link + /// relation type equivalent to the same name registered within the IANA + /// Registry of Link Relations (Section 7), and thus to the IRI that + /// would be obtained by appending the value of the rel attribute to the + /// string "http://www.iana.org/assignments/relation/". The value of + /// "rel" describes the meaning of the link, but does not impose any + /// behavioral requirements on Atom Processors. + /// + /// This document defines five initial values for the Registry of Link + /// Relations: + /// + /// 1. The value "alternate" signifies that the IRI in the value of the + /// href attribute identifies an alternate version of the resource + /// described by the containing element. + /// + /// 2. The value "related" signifies that the IRI in the value of the + /// href attribute identifies a resource related to the resource + /// described by the containing element. For example, the feed for a + /// site that discusses the performance of the search engine at + /// "http://search.example.com" might contain, as a child of + /// atom:feed: + /// + /// + /// + /// An identical link might appear as a child of any atom:entry whose + /// content contains a discussion of that same search engine. + /// + /// 3. The value "self" signifies that the IRI in the value of the href + /// attribute identifies a resource equivalent to the containing + /// element. + /// + /// 4. The value "enclosure" signifies that the IRI in the value of the + /// href attribute identifies a related resource that is potentially + /// large in size and might require special handling. For atom:link + /// elements with rel="enclosure", the length attribute SHOULD be + /// provided. + /// + /// 5. The value "via" signifies that the IRI in the value of the href + /// attribute identifies a resource that is the source of the + /// information provided in the containing element. + public var rel: String? + + /// On the link element, the "type" attribute's value is an advisory + /// media type: it is a hint about the type of the representation that is + /// expected to be returned when the value of the href attribute is + /// dereferenced. Note that the type attribute does not override the + /// actual media type returned with the representation. Link elements + /// MAY have a type attribute, whose value MUST conform to the syntax of + /// a MIME media type [MIMEREG]. + public var type: String? + + /// The "hreflang" attribute's content describes the language of the + /// resource pointed to by the href attribute. When used together with + /// the rel="alternate", it implies a translated version of the entry. + /// Link elements MAY have an hreflang attribute, whose value MUST be a + /// language tag [RFC3066]. + public var hreflang: String? + + /// The "title" attribute conveys human-readable information about the + /// link. The content of the "title" attribute is Language-Sensitive. + /// Entities such as "&" and "<" represent their corresponding + /// characters ("&" and "<", respectively), not markup. Link elements + /// MAY have a title attribute. + public var title: String? + + /// The "length" attribute indicates an advisory length of the linked + /// content in octets; it is a hint about the content length of the + /// representation returned when the IRI in the href attribute is mapped + /// to a URI and dereferenced. Note that the length attribute does not + /// override the actual content length of the representation as reported + /// by the underlying protocol. Link elements MAY have a length + /// attribute. + public var length: Int64? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension AtomFeedLink { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedLink.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedLink.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.href = attributeDict["href"] + self.hreflang = attributeDict["hreflang"] + self.type = attributeDict["type"] + self.rel = attributeDict["rel"] + self.title = attributeDict["title"] + self.length = Int64(attributeDict["length"] ?? "") + + } + +} + +// MARK: - Equatable + +extension AtomFeedLink: Equatable { + + public static func ==(lhs: AtomFeedLink, rhs: AtomFeedLink) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension AtomFeedLink.Attributes: Equatable { + + public static func ==(lhs: AtomFeedLink.Attributes, rhs: AtomFeedLink.Attributes) -> Bool { + return + lhs.href == rhs.href && + lhs.hreflang == rhs.hreflang && + lhs.type == rhs.type && + lhs.rel == rhs.rel && + lhs.title == rhs.title && + lhs.length == rhs.length + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedSubtitle.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedSubtitle.swift new file mode 100644 index 0000000..ec17202 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomFeedSubtitle.swift @@ -0,0 +1,97 @@ +// +// AtomFeedSubtitle.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The "atom:subtitle" element is a Text construct that conveys a human- +/// readable description or subtitle for a feed. +public class AtomFeedSubtitle { + + /// The element's attributes. + public class Attributes { + + /// Text constructs MAY have a "type" attribute. When present, the value + /// MUST be one of "text", "html", or "xhtml". If the "type" attribute + /// is not provided, Atom Processors MUST behave as though it were + /// present with a value of "text". + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension AtomFeedSubtitle { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = AtomFeedSubtitle.Attributes(attributes: attributeDict) + } + +} + +extension AtomFeedSubtitle.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension AtomFeedSubtitle: Equatable { + + public static func ==(lhs: AtomFeedSubtitle, rhs: AtomFeedSubtitle) -> Bool { + return + lhs.attributes == rhs.attributes && + lhs.value == rhs.value + } + +} + +extension AtomFeedSubtitle.Attributes: Equatable { + + public static func ==(lhs: AtomFeedSubtitle.Attributes, rhs: AtomFeedSubtitle.Attributes) -> Bool { + return lhs.type == rhs.type + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomPath.swift b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomPath.swift new file mode 100644 index 0000000..393bcf8 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Atom/AtomPath.swift @@ -0,0 +1,110 @@ +// +// AtomPath.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes the individual path for each XML DOM element of an Atom feed. +/// +/// See https://tools.ietf.org/html/rfc4287 +enum AtomPath: String { + + case feed = "/feed" + case feedTitle = "/feed/title" + case feedSubtitle = "/feed/subtitle" + case feedLink = "/feed/link" + case feedUpdated = "/feed/updated" + case feedCategory = "/feed/category" + case feedAuthor = "/feed/author" + case feedAuthorName = "/feed/author/name" + case feedAuthorEmail = "/feed/author/email" + case feedAuthorUri = "/feed/author/uri" + case feedContributor = "/feed/contributor" + case feedContributorName = "/feed/contributor/name" + case feedContributorEmail = "/feed/contributor/email" + case feedContributorUri = "/feed/contributor/uri" + case feedID = "/feed/id" + case feedGenerator = "/feed/generator" + case feedIcon = "/feed/icon" + case feedLogo = "/feed/logo" + case feedRights = "/feed/rights" + case feedEntry = "/feed/entry" + case feedEntryTitle = "/feed/entry/title" + case feedEntrySummary = "/feed/entry/summary" + case feedEntryLink = "/feed/entry/link" + case feedEntryUpdated = "/feed/entry/updated" + case feedEntryCategory = "/feed/entry/category" + case feedEntryID = "/feed/entry/id" + case feedEntryContent = "/feed/entry/content" + case feedEntryPublished = "/feed/entry/published" + case feedEntrySource = "/feed/entry/source" + case feedEntrySourceID = "/feed/entry/source/id" + case feedEntrySourceTitle = "/feed/entry/source/title" + case feedEntrySourceUpdated = "/feed/entry/source/updated" + case feedEntryRights = "/feed/entry/rights" + case feedEntryAuthor = "/feed/entry/author" + case feedEntryAuthorName = "/feed/entry/author/name" + case feedEntryAuthorEmail = "/feed/entry/author/email" + case feedEntryAuthorUri = "/feed/entry/author/uri" + case feedEntryContributor = "/feed/entry/contributor" + case feedEntryContributorName = "/feed/entry/contributor/name" + case feedEntryContributorEmail = "/feed/entry/contributor/email" + case feedEntryContributorUri = "/feed/entry/contributor/uri" + + // MARK: Media + + case feedEntryMediaThumbnail = "/feed/entry/media:thumbnail" + case feedEntryMediaContent = "/feed/entry/media:content" + case feedEntryMediaCommunity = "/feed/entry/media:community" + case feedEntryMediaCommunityMediaStarRating = "/feed/entry/media:community/media:starRating" + case feedEntryMediaCommunityMediaStatistics = "/feed/entry/media:community/media:statistics" + case feedEntryMediaCommunityMediaTags = "/feed/entry/media:community/media:tags" + case feedEntryMediaComments = "/feed/entry/media:comments" + case feedEntryMediaCommentsMediaComment = "/feed/entry/media:comments/media:comment" + case feedEntryMediaEmbed = "/feed/entry/media:embed" + case feedEntryMediaEmbedMediaParam = "/feed/entry/media:embed/media:param" + case feedEntryMediaResponses = "/feed/entry/media:responses" + case feedEntryMediaResponsesMediaResponse = "/feed/entry/media:responses/media:response" + case feedEntryMediaBackLinks = "/feed/entry/media:backLinks" + case feedEntryMediaBackLinksBackLink = "/feed/entry/media:backLinks/media:backLink" + case feedEntryMediaStatus = "/feed/entry/media:status" + case feedEntryMediaPrice = "/feed/entry/media:price" + case feedEntryMediaLicense = "/feed/entry/media:license" + case feedEntryMediaSubTitle = "/feed/entry/media:subTitle" + case feedEntryMediaPeerLink = "/feed/entry/media:peerLink" + case feedEntryMediaLocation = "/feed/entry/media:location" + case feedEntryMediaLocationPosition = "/feed/entry/media:location/georss:where/gml:Point/gml:pos" + case feedEntryMediaRestriction = "/feed/entry/media:restriction" + case feedEntryMediaScenes = "/feed/entry/media:scenes" + case feedEntryMediaScenesMediaScene = "/feed/entry/media:scenes/media:scene" + case feedEntryMediaScenesMediaSceneSceneTitle = "/feed/entry/media:scenes/media:scene/sceneTitle" + case feedEntryMediaScenesMediaSceneSceneDescription = "/feed/entry/media:scenes/media:scene/sceneDescription" + case feedEntryMediaScenesMediaSceneSceneStartTime = "/feed/entry/media:scenes/media:scene/sceneStartTime" + case feedEntryMediaScenesMediaSceneSceneEndTime = "/feed/entry/media:scenes/media:scene/sceneEndTime" + case feedEntryMediaGroup = "/feed/entry/media:group" + case feedEntryMediaGroupMediaCredit = "/feed/entry/media:group/media:credit" + case feedEntryMediaGroupMediaCategory = "/feed/entry/media:group/media:category" + case feedEntryMediaGroupMediaRating = "/feed/entry/media:group/media:rating" + case feedEntryMediaGroupMediaContent = "/feed/entry/media:group/media:content" + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeed.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeed.swift new file mode 100644 index 0000000..228cf53 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeed.swift @@ -0,0 +1,165 @@ +// +// JSONFeed.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The JSON Feed format is a pragmatic syndication format, like RSS and Atom, +/// but with one big difference: it's JSON instead of XML. +/// See https://jsonfeed.org/version/1 +public struct JSONFeed { + + /// (required, string) is the URL of the version of the format the feed + /// uses. This should appear at the very top, though we recognize that not all + /// JSON generators allow for ordering. + public var version: String? + + /// (required, string) is the name of the feed, which will often correspond to + /// the name of the website (blog, for instance), though not necessarily. + public var title: String? + + /// (optional but strongly recommended, string) is the URL of the resource that + /// the feed describes. This resource may or may not actually be a "home" page, + /// but it should be an HTML page. If a feed is published on the public web, + /// this should be considered as required. But it may not make sense in the + /// case of a file created on a desktop computer, when that file is not shared + /// or is shared only privately. + public var homePageURL: String? + + /// (optional but strongly recommended, string) is the URL of the feed, and + /// serves as the unique identifier for the feed. As with home_page_url, this + /// should be considered required for feeds on the public web. + public var feedUrl: String? + + /// (optional, string) provides more detail, beyond the title, on what the feed + /// is about. A feed reader may display this text. + public var description: String? + + /// (optional, string) is a description of the purpose of the feed. This is for + /// the use of people looking at the raw JSON, and should be ignored by feed + /// readers. + public var userComment: String? + + /// (optional, string) is the URL of a feed that provides the next n items, + /// where n is determined by the publisher. This allows for pagination, but + /// with the expectation that reader software is not required to use it and + /// probably won't use it very often. next_url must not be the same as + /// feed_url, and it must not be the same as a previous next_url (to avoid + /// infinite loops). + public var nextUrl: String? + + /// (optional, string) is the URL of an image for the feed suitable to be used + /// in a timeline, much the way an avatar might be used. It should be square + /// and relatively large - such as 512 x 512 - so that it can be scaled-down + /// and so that it can look good on retina displays. It should use transparency + /// where appropriate, since it may be rendered on a non-white background. + public var icon: String? + + /// (optional, string) is the URL of an image for the feed suitable to be used + /// in a source list. It should be square and relatively small, but not smaller + /// than 64 x 64 (so that it can look good on retina displays). As with icon, + /// this image should use transparency where appropriate, since it may be + /// rendered on a non-white background. + public var favicon: String? + + /// (optional, object) specifies the feed author. The author object has + /// several members. These are all optional - but if you provide an author + /// object, then at least one is required. + public var author: JSONFeedAuthor? + + /// (optional, boolean) says whether or not the feed is finished - that is, + /// whether or not it will ever update again. A feed for a temporary event, + /// such as an instance of the Olympics, could expire. If the value is true, + /// then it's expired. Any other value, or the absence of expired, means the + /// feed may continue to update. + public var expired: Bool? + + /// (very optional, array of objects) describes endpoints that can be used to + /// subscribe to real-time notifications from the publisher of this feed. Each + /// object has a type and url, both of which are required. + public var hubs: [JSONFeedHub]? + + /// The JSONFeed items. + public var items: [JSONFeedItem]? + +} + +// MARK: - Equatable + +extension JSONFeed: Equatable {} + +// MARK: - Codable + +extension JSONFeed: Codable { + + enum CodingKeys: String, CodingKey { + case version + case title + case user_comment + case home_page_url + case description + case feed_url + case next_url + case icon + case favicon + case expired + case author + case hubs + case items + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(version, forKey: .version) + try container.encode(title, forKey: .title) + try container.encode(userComment, forKey: .user_comment) + try container.encode(homePageURL, forKey: .home_page_url) + try container.encode(description, forKey: .description) + try container.encode(feedUrl, forKey: .feed_url) + try container.encode(nextUrl, forKey: .next_url) + try container.encode(icon, forKey: .icon) + try container.encode(favicon, forKey: .favicon) + try container.encode(expired, forKey: .expired) + try container.encode(author, forKey: .expired) + try container.encode(hubs, forKey: .hubs) + try container.encode(items, forKey: .items) + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + version = try values.decodeIfPresent(String.self, forKey: .version) + title = try values.decodeIfPresent(String.self, forKey: .title) + userComment = try values.decodeIfPresent(String.self, forKey: .user_comment) + homePageURL = try values.decodeIfPresent(String.self, forKey: .home_page_url) + description = try values.decodeIfPresent(String.self, forKey: .description) + feedUrl = try values.decodeIfPresent(String.self, forKey: .feed_url) + nextUrl = try values.decodeIfPresent(String.self, forKey: .next_url) + icon = try values.decodeIfPresent(String.self, forKey: .icon) + favicon = try values.decodeIfPresent(String.self, forKey: .favicon) + expired = try values.decodeIfPresent(Bool.self, forKey: .expired) + author = try values.decodeIfPresent(JSONFeedAuthor.self, forKey: .author) + hubs = try values.decodeIfPresent([JSONFeedHub].self, forKey: .hubs) + items = try values.decodeIfPresent([JSONFeedItem].self, forKey: .items) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAttachment.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAttachment.swift new file mode 100644 index 0000000..fb7694b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAttachment.swift @@ -0,0 +1,87 @@ +// +// JSONFeedAttachment.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes optional attatchments of a JSON Feed item. +public struct JSONFeedAttachment { + + /// (required, string) specifies the location of the attachment. + public var url: String? + + /// (required, string) specifies the type of the attachment, such as + /// "audio/mpeg." + public var mimeType: String? + + /// (optional, string) is a name for the attachment. Important: if there are + /// multiple attachments, and two or more have the exact same title (when title + /// is present), then they are considered as alternate representations of the + /// same thing. In this way a podcaster, for instance, might provide an audio + /// recording in different formats. + public var title: String? + + /// (optional, number) specifies how large the file is. + public var sizeInBytes: Int? + + /// (optional, number) specifies how long it takes to listen to or watch, when + /// played at normal speed. + public var durationInSeconds: TimeInterval? + +} + +// MARK: - Equatable + +extension JSONFeedAttachment: Equatable {} + +// MARK: - Codable + +extension JSONFeedAttachment: Codable { + + enum CodingKeys: String, CodingKey { + case title + case url + case mime_type + case size_in_bytes + case duration_in_seconds + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(title, forKey: .title) + try container.encode(url, forKey: .url) + try container.encode(mimeType, forKey: .mime_type) + try container.encode(sizeInBytes, forKey: .size_in_bytes) + try container.encode(durationInSeconds, forKey: .duration_in_seconds) + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + title = try values.decodeIfPresent(String.self, forKey: .title) + url = try values.decodeIfPresent(String.self, forKey: .url) + mimeType = try values.decodeIfPresent(String.self, forKey: .mime_type) + sizeInBytes = try values.decodeIfPresent(Int.self, forKey: .size_in_bytes) + durationInSeconds = try values.decodeIfPresent(TimeInterval.self, forKey: .duration_in_seconds) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAuthor.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAuthor.swift new file mode 100644 index 0000000..8f0db62 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedAuthor.swift @@ -0,0 +1,77 @@ +// +// JSONFeedAuthor.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// (optional, object) specifies the feed author. The author object has several +/// members. These are all optional - but if you provide an author object, then at +/// least one is required: +public struct JSONFeedAuthor { + + /// (optional, string) is the author's name. + public var name: String? + + /// (optional, string) is the URL of a site owned by the author. It could be a + /// blog, micro-blog, Twitter account, and so on. Ideally the linked-to page + /// provides a way to contact the author, but that's not required. The URL + /// could be a mailto: link, though we suspect that will be rare. + public var url: String? + + /// (optional, string) is the URL for an image for the author. As with icon, + /// it should be square and relatively large - such as 512 x 512 - and should + /// use transparency where appropriate, since it may be rendered on a non-white + /// background. + public var avatar: String? + +} + +// MARK: - Equatable + +extension JSONFeedAuthor: Equatable {} + +// MARK: - Codable + +extension JSONFeedAuthor: Codable { + + enum CodingKeys: String, CodingKey { + case name + case url + case avatar + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(name, forKey: .name) + try container.encode(url, forKey: .url) + try container.encode(avatar, forKey: .avatar) + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + name = try values.decode(String.self, forKey: .name) + url = try values.decodeIfPresent(String.self, forKey: .url) + avatar = try values.decodeIfPresent(String.self, forKey: .avatar) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedHub.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedHub.swift new file mode 100644 index 0000000..c8e1ab4 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedHub.swift @@ -0,0 +1,65 @@ +// +// JSONFeedHub.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes an endpoints that can be used to subscribe to real-time notifications +/// from the publisher of this feed. Each object has a type and url, both of which +/// are required. +public struct JSONFeedHub { + + /// The protocol used to talk with the hub, such as "rssCloud" or "WebSub." + public var type: String? + + /// The hub's url. + public var url: String? + +} + +// MARK: - Equatable + +extension JSONFeedHub: Equatable {} + +// MARK: - Codable + +extension JSONFeedHub: Codable { + + enum CodingKeys: String, CodingKey { + case type + case url + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(type, forKey: .type) + try container.encode(url, forKey: .url) + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + type = try values.decode(String.self, forKey: .type) + url = try values.decodeIfPresent(String.self, forKey: .url) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedItem.swift b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedItem.swift new file mode 100644 index 0000000..89c38d4 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/JSON/JSONFeedItem.swift @@ -0,0 +1,171 @@ +// +// JSONFeedItem.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// An individual item of a JSON Feed, acting as a container for metadata and data +/// associated with the item. +public struct JSONFeedItem { + + /// (required, string) is unique for that item for that feed over time. If an + /// item is ever updated, the id should be unchanged. New items should never + /// use a previously-used id. If an id is presented as a number or other type, + /// a JSON Feed reader must coerce it to a string. Ideally, the id is the full + /// URL of the resource described by the item, since URLs make great unique + /// identifiers. + public var id: String? + + /// (optional, string) is the URL of the resource described by the item. It's + /// the permalink. This may be the same as the id - but should be present + /// regardless. + public var url: String? + + /// (very optional, string) is the URL of a page elsewhere. This is especially + /// useful for linkblogs. If url links to where you're talking about a thing, + /// then external_url links to the thing you're talking about. + public var externalUrl: String? + + /// (optional, string) is plain text. Microblog items in particular may omit + /// titles. + public var title: String? + + /// content_html and content_text are each optional strings - but one or both + /// must be present. This is the HTML or plain text of the item. Important: + /// the only place HTML is allowed in this format is in content_html. A + /// Twitter-like service might use content_text, while a blog might use + /// content_html. Use whichever makes sense for your resource. (It doesn't + /// even have to be the same for each item in a feed.) + public var contentText: String? + + /// content_html and content_text are each optional strings - but one or both + /// must be present. This is the HTML or plain text of the item. Important: + /// the only place HTML is allowed in this format is in content_html. A + /// Twitter-like service might use content_text, while a blog might use + /// content_html. Use whichever makes sense for your resource. (It doesn't + /// even have to be the same for each item in a feed.) + public var contentHtml: String? + + /// (optional, string) is a plain text sentence or two describing the item. + /// This might be presented in a timeline, for instance, where a detail view + /// would display all of content_html or content_text. + public var summary: String? + + /// (optional, string) is the URL of the main image for the item. This image + /// may also appear in the content_html - if so, it's a hint to the feed reader + /// that this is the main, featured image. Feed readers may use the image as a + /// preview (probably resized as a thumbnail and placed in a timeline). + public var image: String? + + /// (optional, string) is the URL of an image to use as a banner. Some blogging + /// systems (such as Medium) display a different banner image chosen to go with + /// each post, but that image wouldn't otherwise appear in the content_html. + /// A feed reader with a detail view may choose to show this banner image at + /// the top of the detail view, possibly with the title overlaid. + public var bannerImage: String? + + /// (optional, string) specifies the date in RFC 3339 format. + /// (Example: 2010-02-07T14:04:00-05:00.) + public var datePublished: Date? + + /// (optional, string) specifies the modification date in RFC 3339 format. + public var dateModified: Date? + + /// (optional, object) has the same structure as the top-level author. + /// If not specified in an item, then the top-level author, if present, is the + /// author of the item. + public var author: JSONFeedAuthor? + + /// (optional, array of strings) can have any plain text values you want. Tags + /// tend to be just one word, but they may be anything. Note: they are not the + /// equivalent of Twitter hashtags. Some blogging systems and other feed + /// formats call these categories. + public var tags: [String]? + + /// (optional, array) lists related resources. + public var attachments: [JSONFeedAttachment]? + +} + +// MARK: - Equatable + +extension JSONFeedItem: Equatable {} + +// MARK: - Codable + +extension JSONFeedItem: Codable { + + enum CodingKeys: String, CodingKey { + case id + case title + case url + case external_url + case content_text + case content_html + case summary + case image + case banner_image + case date_published + case date_modified + case tags + case author + case attachments + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(id, forKey: .id) + try container.encode(title, forKey: .title) + try container.encode(url, forKey: .url) + try container.encode(externalUrl, forKey: .external_url) + try container.encode(contentText, forKey: .content_text) + try container.encode(contentHtml, forKey: .content_html) + try container.encode(summary, forKey: .summary) + try container.encode(image, forKey: .image) + try container.encode(bannerImage, forKey: .banner_image) + try container.encode(datePublished, forKey: .date_published) + try container.encode(dateModified, forKey: .date_modified) + try container.encode(tags, forKey: .tags) + try container.encode(author, forKey: .author) + try container.encode(attachments, forKey: .attachments) + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + id = try values.decodeIfPresent(String.self, forKey: .id) + title = try values.decodeIfPresent(String.self, forKey: .title) + url = try values.decodeIfPresent(String.self, forKey: .url) + externalUrl = try values.decodeIfPresent(String.self, forKey: .external_url) + contentText = try values.decodeIfPresent(String.self, forKey: .content_text) + contentHtml = try values.decodeIfPresent(String.self, forKey: .content_html) + summary = try values.decodeIfPresent(String.self, forKey: .summary) + image = try values.decodeIfPresent(String.self, forKey: .image) + bannerImage = try values.decodeIfPresent(String.self, forKey: .banner_image) + datePublished = try values.decodeIfPresent(Date.self, forKey: .date_published) + dateModified = try values.decodeIfPresent(Date.self, forKey: .date_modified) + tags = try values.decodeIfPresent([String].self, forKey: .tags) + author = try values.decodeIfPresent(JSONFeedAuthor.self, forKey: .author) + attachments = try values.decodeIfPresent([JSONFeedAttachment].self, forKey: .attachments) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Content/ContentNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Content/ContentNamespace.swift new file mode 100644 index 0000000..28c767c --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Content/ContentNamespace.swift @@ -0,0 +1,51 @@ +// +// ContentNamespace.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A module for the actual content of websites, in multiple formats. +/// See http://web.resource.org/rss/1.0/modules/content/ +public class ContentNamespace { + + /// An element whose contents are the entity-encoded or CDATA-escaped version + /// of the content of the item. + /// + /// Example: + /// What a beautiful day!

]]> + ///
+ public var contentEncoded: String? + + public init() { } + +} + +// MARK: - Equatable + +extension ContentNamespace: Equatable { + + public static func ==(lhs: ContentNamespace, rhs: ContentNamespace) -> Bool { + return lhs.contentEncoded == rhs.contentEncoded + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Dublin Core/DublinCoreNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Dublin Core/DublinCoreNamespace.swift new file mode 100644 index 0000000..2646e54 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Dublin Core/DublinCoreNamespace.swift @@ -0,0 +1,165 @@ +// +// DublinCoreNamespace.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The Dublin Core Metadata Element Set is a standard for cross-domain +/// resource description. +/// +/// See https://tools.ietf.org/html/rfc5013 +public class DublinCoreNamespace { + + /// A name given to the resource. + public var dcTitle: String? + + /// An entity primarily responsible for making the content of the resource + /// + /// Examples of a Creator include a person, an organization, or a service. + /// Typically, the name of a Creator should be used to indicate the entity. + public var dcCreator: String? + + /// The topic of the content of the resource + /// + /// Typically, the subject will be represented using keywords, key phrases, + /// or classification codes. Recommended best practice is to use a controlled + /// vocabulary. To describe the spatial or temporal topic of the resource, + /// use the Coverage element. + public var dcSubject: String? + + /// An account of the content of the resource + /// + /// Description may include but is not limited to: an abstract, a table of + /// contents, a graphical representation, or a free-text account of the + /// resource. + public var dcDescription: String? + + /// An entity responsible for making the resource available + /// + /// Examples of a Publisher include a person, an organization, or a service. + /// Typically, the name of a Publisher should be used to indicate the entity. + public var dcPublisher: String? + + /// An entity responsible for making contributions to the content of the + /// resource + /// + /// Examples of a Contributor include a person, an organization, or a service. + /// Typically, the name of a Contributor should be used to indicate the entity. + public var dcContributor: String? + + /// A point or period of time associated with an event in the lifecycle of the + /// resource. + /// + /// Date may be used to express temporal information at any level of + /// granularity. Recommended best practice is to use an encoding scheme, such + /// as the W3CDTF profile of ISO 8601 [W3CDTF]. + public var dcDate: Date? + + /// The nature or genre of the content of the resource + /// + /// Recommended best practice is to use a controlled vocabulary such as the + /// DCMI Type Vocabulary [DCTYPE]. To describe the file format, physical + /// medium, or dimensions of the resource, use the Format element. + public var dcType: String? + + /// The file format, physical medium, or dimensions of the resource. + /// + /// Examples of dimensions include size and duration. Recommended best + /// practice is to use a controlled vocabulary such as the list of Internet + /// Media Types [MIME]. + public var dcFormat: String? + + /// An unambiguous reference to the resource within a given context. + /// + /// Recommended best practice is to identify the resource by means of a string + /// conforming to a formal identification system. + public var dcIdentifier: String? + + /// A Reference to a resource from which the present resource is derived + /// + /// The described resource may be derived from the related resource in whole + /// or in part. Recommended best practice is to identify the related resource + /// by means of a string conforming to a formal identification system. + public var dcSource: String? + + /// A language of the resource. + /// + /// Recommended best practice is to use a controlled vocabulary such as + /// RFC 4646 [RFC4646]. + public var dcLanguage: String? + + /// A related resource. + /// + /// Recommended best practice is to identify the related resource by means of + /// a string conforming to a formal identification system. + public var dcRelation: String? + + /// The spatial or temporal topic of the resource, the spatial applicability + /// of the resource, or the jurisdiction under which the resource is + /// relevant. + /// + /// Spatial topic and spatial applicability may be a named place or a location + /// specified by its geographic coordinates. Temporal topic may be a named + /// period, date, or date range. A jurisdiction may be a named administrative + /// entity or a geographic place to which the resource applies. Recommended + /// best practice is to use a controlled vocabulary such as the Thesaurus of + /// Geographic Names [TGN]. Where appropriate, named places or time periods + /// can be used in preference to numeric identifiers such as sets of + /// coordinates or date ranges. + public var dcCoverage: String? + + /// Information about rights held in and over the resource. + /// + /// Typically, rights information includes a statement about various property + /// rights associated with the resource, including intellectual property + /// rights. + public var dcRights: String? + + public init() { } + +} + +// MARK: - Equatable + +extension DublinCoreNamespace: Equatable { + + public static func ==(lhs: DublinCoreNamespace, rhs: DublinCoreNamespace) -> Bool { + return + lhs.dcTitle == rhs.dcTitle && + lhs.dcCreator == rhs.dcCreator && + lhs.dcSubject == rhs.dcSubject && + lhs.dcDescription == rhs.dcDescription && + lhs.dcPublisher == rhs.dcPublisher && + lhs.dcContributor == rhs.dcContributor && + lhs.dcDate == rhs.dcDate && + lhs.dcType == rhs.dcType && + lhs.dcFormat == rhs.dcFormat && + lhs.dcIdentifier == rhs.dcIdentifier && + lhs.dcSource == rhs.dcSource && + lhs.dcLanguage == rhs.dcLanguage && + lhs.dcRelation == rhs.dcRelation && + lhs.dcCoverage == rhs.dcCoverage && + lhs.dcRights == rhs.dcRights + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCategory.swift new file mode 100644 index 0000000..81cca37 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCategory.swift @@ -0,0 +1,104 @@ +// +// MediaCategory.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows a taxonomy to be set that gives an indication of the type of media +/// content, and its particular contents. It has two optional attributes. +public class MediaCategory { + + /// The element's attributes. + public class Attributes { + + /// The URI that identifies the categorization scheme. It is an optional + /// attribute. If this attribute is not included, the default scheme + /// is "http://search.yahoo.com/mrss/category_schema". + public var scheme: String? + + /// The human readable label that can be displayed in end user + /// applications. It is an optional attribute. + public var label: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaCategory.Attributes(attributes: attributeDict) + } + +} + + +extension MediaCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.scheme = attributeDict["scheme"] + self.label = attributeDict["label"] + + } + +} + +// MARK: - Equatable + +extension MediaCategory: Equatable { + + public static func ==(lhs: MediaCategory, rhs: MediaCategory) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaCategory.Attributes: Equatable { + + public static func ==(lhs: MediaCategory.Attributes, rhs: MediaCategory.Attributes) -> Bool { + return + lhs.scheme == rhs.scheme && + lhs.label == rhs.label + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCommunity.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCommunity.swift new file mode 100644 index 0000000..4eaac56 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCommunity.swift @@ -0,0 +1,62 @@ +// +// MediaCommunity.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This element stands for the community related content. This allows +/// inclusion of the user perception about a media object in the form of view +/// count, ratings and tags. +public class MediaCommunity { + + /// This element specifies the rating-related information about a media object. + /// Valid attributes are average, count, min and max. + public var mediaStarRating: MediaStarRating? + + /// This element specifies various statistics about a media object like the + /// view count and the favorite count. Valid attributes are views and favorites. + public var mediaStatistics: MediaStatistics? + + /// This element contains user-generated tags separated by commas in the + /// decreasing order of each tag's weight. Each tag can be assigned an integer + /// weight in tag_name:weight format. It's up to the provider to choose the way + /// weight is determined for a tag; for example, number of occurences can be + /// one way to decide weight of a particular tag. Default weight is 1. + public var mediaTags: [MediaTag]? + + public init() { } + +} + +// MARK: - Equatable + +extension MediaCommunity: Equatable { + + public static func ==(lhs: MediaCommunity, rhs: MediaCommunity) -> Bool { + return + lhs.mediaStarRating == rhs.mediaStarRating && + lhs.mediaStatistics == rhs.mediaStatistics && + lhs.mediaTags == rhs.mediaTags + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaContent.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaContent.swift new file mode 100644 index 0000000..42ba5ba --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaContent.swift @@ -0,0 +1,198 @@ +// +// MediaContent.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// is a sub-element of either or . +/// Media objects that are not the same content should not be included +/// in the same element. The sequence of these items implies +/// the order of presentation. While many of the attributes appear to be +/// audio/video specific, this element can be used to publish any type of +/// media. It contains 14 attributes, most of which are optional. +public class MediaContent { + + /// The title of the particular media object. It has one optional attribute. + public var mediaTitle: MediaTitle? + + /// Short description describing the media object typically a sentence in + /// length. It has one optional attribute. + public var mediaDescription: MediaDescription? + + /// Allows the media object to be accessed through a web browser media player + /// console. This element is required only if a direct media url attribute is + /// not specified in the element. It has one required attribute + /// and two optional attributes. + public var mediaPlayer: MediaPlayer? + + /// Allows particular images to be used as representative images for the + /// media object. If multiple thumbnails are included, and time coding is not + /// at play, it is assumed that the images are in order of importance. It has + /// one required attribute and three optional attributes. + public var mediaThumbnails: [MediaThumbnail]? + + + /// The element's attributes. + public class Attributes { + + /// Should specify the direct URL to the media object. If not included, + /// a element must be specified. + public var url: String? + + /// The number of bytes of the media object. It is an optional + /// attribute. + public var fileSize: Int? + + /// The standard MIME type of the object. It is an optional attribute. + public var type: String? + + /// Tpe of object (image | audio | video | document | executable). + /// While this attribute can at times seem redundant if type is supplied, + /// it is included because it simplifies decision making on the reader + /// side, as well as flushes out any ambiguities between MIME type and + /// object type. It is an optional attribute. + public var medium: String? + + /// Determines if this is the default object that should be used for + /// the . There should only be one default object per + /// . It is an optional attribute. + public var isDefault: Bool? + + /// Determines if the object is a sample or the full version of the + /// object, or even if it is a continuous stream (sample | full | nonstop). + /// Default value is "full". It is an optional attribute. + public var expression: String? + + /// The kilobits per second rate of media. It is an optional attribute. + public var bitrate: Int? + + /// The number of frames per second for the media object. It is an + /// optional attribute. + public var framerate: Double? + + /// The number of samples per second taken to create the media object. + /// It is expressed in thousands of samples per second (kHz). + /// It is an optional attribute. + public var samplingrate: Double? + + /// The number of audio channels in the media object. It is an + /// optional attribute. + public var channels: Int? + + /// The number of seconds the media object plays. It is an + /// optional attribute. + public var duration: Int? + + /// The height of the media object. It is an optional attribute. + public var height: Int? + + /// The width of the media object. It is an optional attribute. + public var width: Int? + + /// The primary language encapsulated in the media object. + /// Language codes possible are detailed in RFC 3066. This attribute + /// is used similar to the xml:lang attribute detailed in the + /// XML 1.0 Specification (Third Edition). It is an optional + /// attribute. + public var lang: String? + + } + + /// The element's attributes + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaContent { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaContent.Attributes(attributes: attributeDict) + } + +} + +extension MediaContent.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + self.fileSize = Int(attributeDict["fileSize"] ?? "") + self.type = attributeDict["type"] + self.medium = attributeDict["medium"] + self.isDefault = attributeDict["isDefault"]?.toBool() + self.expression = attributeDict["expression"] + self.bitrate = Int(attributeDict["bitrate"] ?? "") + self.framerate = Double(attributeDict["framerate"] ?? "") + self.samplingrate = Double(attributeDict["samplingrate"] ?? "") + self.channels = Int(attributeDict["channels"] ?? "") + self.duration = Int(attributeDict["duration"] ?? "") + self.height = Int(attributeDict["height"] ?? "") + self.width = Int(attributeDict["width"] ?? "") + self.lang = attributeDict["lang"] + + } + +} + +// MARK: - Equatable + +extension MediaContent: Equatable { + + public static func ==(lhs: MediaContent, rhs: MediaContent) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaContent.Attributes: Equatable { + + public static func ==(lhs: MediaContent.Attributes, rhs: MediaContent.Attributes) -> Bool { + return + lhs.bitrate == rhs.bitrate && + lhs.channels == rhs.channels && + lhs.duration == rhs.duration && + lhs.expression == rhs.expression && + lhs.isDefault == rhs.isDefault && + lhs.fileSize == rhs.fileSize && + lhs.framerate == rhs.framerate && + lhs.height == rhs.height && + lhs.lang == rhs.lang && + lhs.medium == rhs.medium && + lhs.samplingrate == rhs.samplingrate && + lhs.type == rhs.type && + lhs.url == rhs.url && + lhs.width == rhs.width + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCopyright.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCopyright.swift new file mode 100644 index 0000000..503bc2c --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCopyright.swift @@ -0,0 +1,97 @@ +// +// MediaCopyright.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Copyright information for the media object. It has one optional attribute. +public class MediaCopyright { + + /// The element's attributes. + public class Attributes { + + /// The URL for a terms of use page or additional copyright information. + /// If the media is operating under a Creative Commons license, the + /// Creative Commons module should be used instead. It is an optional + /// attribute. + public var url: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaCopyright { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaCopyright.Attributes(attributes: attributeDict) + } + +} + + +extension MediaCopyright.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + + } + +} + +// MARK: - Equatable + +extension MediaCopyright: Equatable { + + public static func ==(lhs: MediaCopyright, rhs: MediaCopyright) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaCopyright.Attributes: Equatable { + + public static func ==(lhs: MediaCopyright.Attributes, rhs: MediaCopyright.Attributes) -> Bool { + return lhs.url == rhs.url + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCredit.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCredit.swift new file mode 100644 index 0000000..5f6939e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaCredit.swift @@ -0,0 +1,109 @@ +// +// MediaCredit.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Notable entity and the contribution to the creation of the media object. +/// Current entities can include people, companies, locations, etc. Specific +/// entities can have multiple roles, and several entities can have the same +/// role. These should appear as distinct elements. It has two +/// optional attributes. +public class MediaCredit { + + /// The element's attributes. + public class Attributes { + + /// Specifies the role the entity played. Must be lowercase. It is an + /// optional attribute. + public var role: String? + + /// The URI that identifies the role scheme. It is an optional attribute + /// and possible values for this attribute are ( urn:ebu | urn:yvs ) . The + /// default scheme is "urn:ebu". The list of roles supported under urn:ebu + /// scheme can be found at European Broadcasting Union Role Codes. The + /// roles supported under urn:yvs scheme are ( uploader | owner ). + public var scheme: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaCredit { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaCredit.Attributes(attributes: attributeDict) + } + +} + +extension MediaCredit.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.role = attributeDict["role"] + self.scheme = attributeDict["scheme"] + + } + +} + +// MARK: - Equatable + +extension MediaCredit: Equatable { + + public static func ==(lhs: MediaCredit, rhs: MediaCredit) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaCredit.Attributes: Equatable { + + public static func ==(lhs: MediaCredit.Attributes, rhs: MediaCredit.Attributes) -> Bool { + return + lhs.role == rhs.role && + lhs.scheme == rhs.scheme + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaDescription.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaDescription.swift new file mode 100644 index 0000000..b363ff3 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaDescription.swift @@ -0,0 +1,96 @@ +// +// MediaDescription.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Short description describing the media object typically a sentence in +/// length. It has one optional attribute. +public class MediaDescription { + + /// The element's attributes. + public class Attributes { + + /// Specifies the type of text embedded. Possible values are either "plain" or "html". + /// Default value is "plain". All HTML must be entity-encoded. It is an optional attribute. + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaDescription { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaDescription.Attributes(attributes: attributeDict) + } + +} + + +extension MediaDescription.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension MediaDescription: Equatable { + + public static func ==(lhs: MediaDescription, rhs: MediaDescription) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaDescription.Attributes: Equatable { + + public static func ==(lhs: MediaDescription.Attributes, rhs: MediaDescription.Attributes) -> Bool { + return lhs.type == rhs.type + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaEmbed.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaEmbed.swift new file mode 100644 index 0000000..280db01 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaEmbed.swift @@ -0,0 +1,106 @@ +// +// MediaEmbed.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Sometimes player-specific embed code is needed for a player to play any +/// video. allows inclusion of such information in the form of +/// key-value pairs. +public class MediaEmbed { + + /// The element's attributes. + public class Attributes { + + /// The location of the embeded media. + public var url: String? + + /// The width size for the embeded Media. + public var width: Int? + + /// The height size for the embeded Media. + public var height: Int? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// Key-Value pairs with aditional parameters for the embeded Media. + public var mediaParams: [MediaParam]? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaEmbed { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaEmbed.Attributes(attributes: attributeDict) + } + +} + +extension MediaEmbed.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + self.width = Int(attributeDict["width"] ?? "") + self.height = Int(attributeDict["height"] ?? "") + + } + +} + +// MARK: - Equatable + +extension MediaEmbed: Equatable { + + public static func ==(lhs: MediaEmbed, rhs: MediaEmbed) -> Bool { + return + lhs.mediaParams == rhs.mediaParams && + lhs.attributes == rhs.attributes + } + +} + +extension MediaEmbed.Attributes: Equatable { + + public static func ==(lhs: MediaEmbed.Attributes, rhs: MediaEmbed.Attributes) -> Bool { + return + lhs.url == rhs.url && + lhs.width == rhs.width && + lhs.height == rhs.height + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaGroup.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaGroup.swift new file mode 100644 index 0000000..e923478 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaGroup.swift @@ -0,0 +1,74 @@ +// +// MediaGroup.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The element is a sub-element of . It allows grouping +/// of elements that are effectively the same content, +/// yet different representations. For instance: the same song recorded +/// in both the WAV and MP3 format. It's an optional element that must +/// only be used for this purpose. +public class MediaGroup { + + /// is a sub-element of either or . + /// Media objects that are not the same content should not be included + /// in the same element. The sequence of these items implies + /// the order of presentation. While many of the attributes appear to be + /// audio/video specific, this element can be used to publish any type of + /// media. It contains 14 attributes, most of which are optional. + public var mediaContents: [MediaContent]? + + /// Notable entity and the contribution to the creation of the media object. + /// Current entities can include people, companies, locations, etc. Specific + /// entities can have multiple roles, and several entities can have the same + /// role. These should appear as distinct elements. It has two + /// optional attributes. + public var mediaCredits: [MediaCredit]? + + /// Allows a taxonomy to be set that gives an indication of the type of media + /// content, and its particular contents. It has two optional attributes. + public var mediaCategory: MediaCategory? + + /// This allows the permissible audience to be declared. If this element is not + /// included, it assumes that no restrictions are necessary. It has one + /// optional attribute. + public var mediaRating: MediaRating? + + public init() { } + +} + +// MARK: - Equatable + +extension MediaGroup: Equatable { + + public static func ==(lhs: MediaGroup, rhs: MediaGroup) -> Bool { + return + lhs.mediaContents == rhs.mediaContents && + lhs.mediaCredits == rhs.mediaCredits && + lhs.mediaCategory == rhs.mediaCategory && + lhs.mediaRating == rhs.mediaRating + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaHash.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaHash.swift new file mode 100644 index 0000000..eb8a959 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaHash.swift @@ -0,0 +1,97 @@ +// +// MediaHash.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This is the hash of the binary media file. It can appear multiple times as +/// long as each instance is a different algo. It has one optional attribute. +public class MediaHash { + + /// The element's attributes. + public class Attributes { + + /// This is the hash of the binary media file. It can appear multiple times as long as + /// each instance is a different algo. It has one optional attribute. + public var algo: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaHash { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaHash.Attributes(attributes: attributeDict) + } + +} + + +extension MediaHash.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.algo = attributeDict["algo"] + + } + +} + +// MARK: - Equatable + +extension MediaHash: Equatable { + + public static func ==(lhs: MediaHash, rhs: MediaHash) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaHash.Attributes: Equatable { + + public static func ==(lhs: MediaHash.Attributes, rhs: MediaHash.Attributes) -> Bool { + return lhs.algo == rhs.algo + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLicence.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLicence.swift new file mode 100644 index 0000000..60a97a0 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLicence.swift @@ -0,0 +1,101 @@ +// +// MediaLicence.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional link to specify the machine-readable license associated with the +/// content. +public class MediaLicence { + + /// The element's attributes. + public class Attributes { + + /// The licence type. + public var type: String? + + /// The location of the licence. + public var href: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaLicence { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaLicence.Attributes(attributes: attributeDict) + } + +} + +extension MediaLicence.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.href = attributeDict["href"] + + } + +} + +// MARK: - Equatable + +extension MediaLicence: Equatable { + + public static func ==(lhs: MediaLicence, rhs: MediaLicence) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaLicence.Attributes: Equatable { + + public static func ==(lhs: MediaLicence.Attributes, rhs: MediaLicence.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.href == rhs.href + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLocation.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLocation.swift new file mode 100644 index 0000000..e3cd901 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaLocation.swift @@ -0,0 +1,128 @@ +// +// MediaLocation.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional element to specify geographical information about various +/// locations captured in the content of a media object. The format conforms +/// to geoRSS. +public class MediaLocation { + + /// The element's attributes. + public class Attributes { + + /// Description of the place whose location is being specified. + public var description: String? + + /// Time at which the reference to a particular location starts in the + /// media object. + public var start: TimeInterval? + + /// Time at which the reference to a particular location ends in the media + /// object. + public var end: TimeInterval? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The geoRSS's location latitude. + public var latitude: Double? + + /// The geoRSS's location longitude. + public var longitude: Double? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaLocation { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaLocation.Attributes(attributes: attributeDict) + } + +} + +extension MediaLocation.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.description = attributeDict["description"] + self.start = attributeDict["start"]?.toDuration() + self.end = attributeDict["end"]?.toDuration() + + } + +} + +// MARK: - Equatable + +extension MediaLocation: Equatable { + + public static func ==(lhs: MediaLocation, rhs: MediaLocation) -> Bool { + return + lhs.latitude == rhs.latitude && + lhs.longitude == rhs.longitude && + lhs.attributes == rhs.attributes + } + +} + +extension MediaLocation.Attributes: Equatable { + + public static func ==(lhs: MediaLocation.Attributes, rhs: MediaLocation.Attributes) -> Bool { + return + lhs.description == rhs.description && + lhs.start == rhs.start && + lhs.end == rhs.end + } + +} + +// MARK: - Helpers + +extension MediaLocation { + + func mapFrom(latLng: String) { + + let components = latLng.components(separatedBy: " ") + if components.count == 2 { + self.latitude = Double(components.first ?? "") + self.longitude = Double(components.last ?? "") + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaNamespace.swift new file mode 100644 index 0000000..3e0dc6f --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaNamespace.swift @@ -0,0 +1,216 @@ +// +// MediaNamespace.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Media RSS is a new RSS module that supplements the +/// capabilities of RSS 2.0. RSS enclosures are already being used to +/// syndicate audio files and images. Media RSS extends enclosures to +/// handle other media types, such as short films or TV, as well as +/// provide additional metadata with the media. Media RSS enables +/// content publishers and bloggers to syndicate multimedia content +/// such as TV and video clips, movies, images and audio. +public class MediaNamespace { + + /// The element is a sub-element of . It allows grouping + /// of elements that are effectively the same content, + /// yet different representations. For instance: the same song recorded + /// in both the WAV and MP3 format. It's an optional element that must + /// only be used for this purpose. + public var mediaGroup: MediaGroup? + + /// is a sub-element of either or . + /// Media objects that are not the same content should not be included + /// in the same element. The sequence of these items implies + /// the order of presentation. While many of the attributes appear to be + /// audio/video specific, this element can be used to publish any type of + /// media. It contains 14 attributes, most of which are optional. + public var mediaContents: [MediaContent]? + + /// This allows the permissible audience to be declared. If this element is not + /// included, it assumes that no restrictions are necessary. It has one + /// optional attribute. + public var mediaRating: MediaRating? + + /// The title of the particular media object. It has one optional attribute. + public var mediaTitle: MediaTitle? + + /// Short description describing the media object typically a sentence in + /// length. It has one optional attribute. + public var mediaDescription: MediaDescription? + + /// Highly relevant keywords describing the media object with typically a + /// maximum of 10 words. The keywords and phrases should be comma-delimited. + public var mediaKeywords: [String]? + + /// Allows particular images to be used as representative images for the + /// media object. If multiple thumbnails are included, and time coding is not + /// at play, it is assumed that the images are in order of importance. It has + /// one required attribute and three optional attributes. + public var mediaThumbnails: [MediaThumbnail]? + + /// Allows a taxonomy to be set that gives an indication of the type of media + /// content, and its particular contents. It has two optional attributes. + public var mediaCategory: MediaCategory? + + /// This is the hash of the binary media file. It can appear multiple times as + /// long as each instance is a different algo. It has one optional attribute. + public var mediaHash: MediaHash? + + /// Allows the media object to be accessed through a web browser media player + /// console. This element is required only if a direct media url attribute is + /// not specified in the element. It has one required attribute + /// and two optional attributes. + public var mediaPlayer: MediaPlayer? + + /// Notable entity and the contribution to the creation of the media object. + /// Current entities can include people, companies, locations, etc. Specific + /// entities can have multiple roles, and several entities can have the same + /// role. These should appear as distinct elements. It has two + /// optional attributes. + public var mediaCredits: [MediaCredit]? + + /// Copyright information for the media object. It has one optional attribute. + public var mediaCopyright: MediaCopyright? + + /// Allows the inclusion of a text transcript, closed captioning or lyrics of + /// the media content. Many of these elements are permitted to provide a time + /// series of text. In such cases, it is encouraged, but not required, that the + /// elements be grouped by language and appear in time sequence order based on + /// the start time. Elements can have overlapping start and end times. It has + /// four optional attributes. + public var mediaText: MediaText? + + /// Allows restrictions to be placed on the aggregator rendering the media in + /// the feed. Currently, restrictions are based on distributor (URI), country + /// codes and sharing of a media object. This element is purely informational + /// and no obligation can be assumed or implied. Only one + /// element of the same type can be applied to a media object -- all others + /// will be ignored. Entities in this element should be space-separated. + /// To allow the producer to explicitly declare his/her intentions, two + /// literals are reserved: "all", "none". These literals can only be used once. + /// This element has one required attribute and one optional attribute (with + /// strict requirements for its exclusion). + public var mediaRestriction: MediaRestriction? + + /// This element stands for the community related content. This allows + /// inclusion of the user perception about a media object in the form of view + /// count, ratings and tags. + public var mediaCommunity: MediaCommunity? + + /// Allows inclusion of all the comments a media object has received. + public var mediaComments: [String]? + + /// Sometimes player-specific embed code is needed for a player to play any + /// video. allows inclusion of such information in the form of + /// key-value pairs. + public var mediaEmbed: MediaEmbed? + + /// Allows inclusion of a list of all media responses a media object has + /// received. + public var mediaResponses: [String]? + + /// Allows inclusion of all the URLs pointing to a media object. + public var mediaBackLinks: [String]? + + /// Optional tag to specify the status of a media object -- whether it's still + /// active or it has been blocked/deleted. + public var mediaStatus: MediaStatus? + + /// Optional tag to include pricing information about a media object. If this + /// tag is not present, the media object is supposed to be free. One media + /// object can have multiple instances of this tag for including different + /// pricing structures. The presence of this tag would mean that media object + /// is not free. + public var mediaPrices: [MediaPrice]? + + /// Optional link to specify the machine-readable license associated with the + /// content. + public var mediaLicense: MediaLicence? + + /// Optional link to specify the machine-readable license associated with the + /// content. + public var mediaSubTitle: MediaSubTitle? + + /// Optional element for P2P link. + public var mediaPeerLink: MediaPeerLink? + + /// Optional element to specify geographical information about various + /// locations captured in the content of a media object. The format conforms + /// to geoRSS. + public var mediaLocation: MediaLocation? + + /// Optional element to specify the rights information of a media object. + public var mediaRights: MediaRights? + + /// Optional element to specify various scenes within a media object. It can + /// have multiple child elements, where each + /// element contains information about a particular scene. has + /// the optional sub-elements , , + /// and , which contains title, description, + /// start and end time of a particular scene in the media, respectively. + public var mediaScenes: [MediaScene]? + + public init() { } + +} + +// MARK: - Equatable + +extension MediaNamespace: Equatable { + + public static func ==(lhs: MediaNamespace, rhs: MediaNamespace) -> Bool { + return + lhs.mediaGroup == rhs.mediaGroup && + lhs.mediaContents == rhs.mediaContents && + lhs.mediaRating == rhs.mediaRating && + lhs.mediaTitle == rhs.mediaTitle && + lhs.mediaDescription == rhs.mediaDescription && + lhs.mediaKeywords == rhs.mediaKeywords && + lhs.mediaThumbnails == rhs.mediaThumbnails && + lhs.mediaCategory == rhs.mediaCategory && + lhs.mediaHash == rhs.mediaHash && + lhs.mediaPlayer == rhs.mediaPlayer && + lhs.mediaCredits == rhs.mediaCredits && + lhs.mediaCopyright == rhs.mediaCopyright && + lhs.mediaText == rhs.mediaText && + lhs.mediaRestriction == rhs.mediaRestriction && + lhs.mediaCommunity == rhs.mediaCommunity && + lhs.mediaComments == rhs.mediaComments && + lhs.mediaEmbed == rhs.mediaEmbed && + lhs.mediaResponses == rhs.mediaResponses && + lhs.mediaBackLinks == rhs.mediaBackLinks && + lhs.mediaStatus == rhs.mediaStatus && + lhs.mediaPrices == rhs.mediaPrices && + lhs.mediaLicense == rhs.mediaLicense && + lhs.mediaSubTitle == rhs.mediaSubTitle && + lhs.mediaPeerLink == rhs.mediaPeerLink && + lhs.mediaLocation == rhs.mediaLocation && + lhs.mediaRights == rhs.mediaRights && + lhs.mediaScenes == rhs.mediaScenes + } + +} + + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaParam.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaParam.swift new file mode 100644 index 0000000..11966a5 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaParam.swift @@ -0,0 +1,94 @@ +// +// MediaParam.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Key-Value pairs with aditional parameters for the embeded Media. +public class MediaParam { + + /// The element's attributes. + public class Attributes { + + /// The parameter's key name. + public var name: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaParam { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaParam.Attributes(attributes: attributeDict) + } + +} + +extension MediaParam.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.name = attributeDict["name"] + + } + +} + +// MARK: - Equatable + +extension MediaParam: Equatable { + + public static func ==(lhs: MediaParam, rhs: MediaParam) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaParam.Attributes: Equatable { + + public static func ==(lhs: MediaParam.Attributes, rhs: MediaParam.Attributes) -> Bool { + return lhs.name == rhs.name + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPeerLink.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPeerLink.swift new file mode 100644 index 0000000..4b92b2c --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPeerLink.swift @@ -0,0 +1,100 @@ +// +// MediaPeerLink.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional element for P2P link. +public class MediaPeerLink { + + /// The element's attributes. + public class Attributes { + + /// The peer link's type. + public var type: String? + + /// The location of the peer link provider. + public var href: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaPeerLink { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaPeerLink.Attributes(attributes: attributeDict) + } + +} + +extension MediaPeerLink.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.href = attributeDict["href"] + + } + +} + +// MARK: - Equatable + +extension MediaPeerLink: Equatable { + + public static func ==(lhs: MediaPeerLink, rhs: MediaPeerLink) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaPeerLink.Attributes: Equatable { + + public static func ==(lhs: MediaPeerLink.Attributes, rhs: MediaPeerLink.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.href == rhs.href + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPlayer.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPlayer.swift new file mode 100644 index 0000000..6284642 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPlayer.swift @@ -0,0 +1,110 @@ +// +// MediaPlayer.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows the media object to be accessed through a web browser media player +/// console. This element is required only if a direct media url attribute is +/// not specified in the element. It has one required attribute +/// and two optional attributes. +public class MediaPlayer { + + /// The element's attributes. + public class Attributes { + + /// The URL of the player console that plays the media. It is a required attribute. + public var url: String? + + /// The width of the browser window that the URL should be opened in. It is + /// an optional attribute. + public var width: Int? + + /// The height of the browser window that the URL should be opened in. It is an + /// optional attribute. + public var height: Int? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaPlayer { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaPlayer.Attributes(attributes: attributeDict) + } + +} + + +extension MediaPlayer.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + self.height = Int(attributeDict["height"] ?? "") + self.width = Int(attributeDict["width"] ?? "") + + } + +} + +// MARK: - Equatable + +extension MediaPlayer: Equatable { + + public static func ==(lhs: MediaPlayer, rhs: MediaPlayer) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaPlayer.Attributes: Equatable { + + public static func ==(lhs: MediaPlayer.Attributes, rhs: MediaPlayer.Attributes) -> Bool { + return + lhs.width == rhs.width && + lhs.height == rhs.height && + lhs.url == rhs.url + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPrice.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPrice.swift new file mode 100644 index 0000000..e13503a --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaPrice.swift @@ -0,0 +1,116 @@ +// +// MediaPrice.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional tag to include pricing information about a media object. If this +/// tag is not present, the media object is supposed to be free. One media +/// object can have multiple instances of this tag for including different +/// pricing structures. The presence of this tag would mean that media object +/// is not free. +public class MediaPrice { + + /// The element's attributes. + public class Attributes { + + /// Valid values are "rent", "purchase", "package" or "subscription". If + /// nothing is specified, then the media is free. + public var type: String? + + /// The price of the media object. This is an optional attribute. + public var price: Double? + + /// If the type is "package" or "subscription", then info is a URL pointing + /// to package or subscription information. This is an optional attribute. + public var info: String? + + /// Use [ISO 4217] for currency codes. This is an optional attribute. + public var currency: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaPrice { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaPrice.Attributes(attributes: attributeDict) + } + +} + +extension MediaPrice.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.price = Double(attributeDict["price"] ?? "") + self.info = attributeDict["info"] + self.currency = attributeDict["currency"] + + } + +} + +// MARK: - Equatable + +extension MediaPrice: Equatable { + + public static func ==(lhs: MediaPrice, rhs: MediaPrice) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaPrice.Attributes: Equatable { + + public static func ==(lhs: MediaPrice.Attributes, rhs: MediaPrice.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.price == rhs.price && + lhs.info == rhs.info && + lhs.currency == rhs.currency + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRating.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRating.swift new file mode 100644 index 0000000..a6d75d4 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRating.swift @@ -0,0 +1,98 @@ +// +// MediaRating.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This allows the permissible audience to be declared. If this element is not +/// included, it assumes that no restrictions are necessary. It has one optional +/// attribute. +public class MediaRating { + + /// The element's attributes. + public class Attributes { + + /// The URI that identifies the rating scheme. It is an optional attribute. + /// If this attribute is not included, the default scheme is urn:simple (adult | nonadult). + public var scheme: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaRating { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaRating.Attributes(attributes: attributeDict) + } + +} + + +extension MediaRating.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.scheme = attributeDict["scheme"] + + } + +} + +// MARK: - Equatable + +extension MediaRating: Equatable { + + public static func ==(lhs: MediaRating, rhs: MediaRating) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaRating.Attributes: Equatable { + + public static func ==(lhs: MediaRating.Attributes, rhs: MediaRating.Attributes) -> Bool { + return lhs.scheme == rhs.scheme + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRestriction.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRestriction.swift new file mode 100644 index 0000000..6eaed04 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRestriction.swift @@ -0,0 +1,118 @@ +// +// MediaRestriction.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows restrictions to be placed on the aggregator rendering the media in +/// the feed. Currently, restrictions are based on distributor (URI), country +/// codes and sharing of a media object. This element is purely informational +/// and no obligation can be assumed or implied. Only one +/// element of the same type can be applied to a media object -- all others +/// will be ignored. Entities in this element should be space-separated. +/// To allow the producer to explicitly declare his/her intentions, two +/// literals are reserved: "all", "none". These literals can only be used once. +/// This element has one required attribute and one optional attribute (with +/// strict requirements for its exclusion). +public class MediaRestriction { + + /// The element's attributes. + public class Attributes { + + /// Indicates the type of relationship that the restriction represents + /// (allow | deny). In the example above, the media object should only be + /// syndicated in Australia and the United States. It is a required + /// attribute. + /// + /// Note: If the "allow" element is empty and the type of relationship is + /// "allow", it is assumed that the empty list means "allow nobody" and + /// the media should not be syndicated. + public var relationship: String? + + /// Specifies the type of restriction (country | uri | sharing ) that the + /// media can be syndicated. It is an optional attribute; however can only + /// be excluded when using one of the literal values "all" or "none". + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaRestriction { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaRestriction.Attributes(attributes: attributeDict) + } + +} + + +extension MediaRestriction.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.relationship = attributeDict["relationship"] + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension MediaRestriction: Equatable { + + public static func ==(lhs: MediaRestriction, rhs: MediaRestriction) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaRestriction.Attributes: Equatable { + + public static func ==(lhs: MediaRestriction.Attributes, rhs: MediaRestriction.Attributes) -> Bool { + return + lhs.relationship == rhs.relationship && + lhs.type == rhs.type + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRights.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRights.swift new file mode 100644 index 0000000..d6e352e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaRights.swift @@ -0,0 +1,90 @@ +// +// MediaRights.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional element to specify the rights information of a media object. +public class MediaRights { + + /// The element's attributes. + public class Attributes { + + /// Is the status of the media object saying whether a media object has + /// been created by the publisher or they have rights to circulate it. + /// Supported values are "userCreated" and "official". + public var status: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaRights { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaRights.Attributes(attributes: attributeDict) + } + +} + +extension MediaRights.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.status = attributeDict["status"] + + } + +} + +// MARK: - Equatable + +extension MediaRights: Equatable { + + public static func ==(lhs: MediaRights, rhs: MediaRights) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaRights.Attributes: Equatable { + + public static func ==(lhs: MediaRights.Attributes, rhs: MediaRights.Attributes) -> Bool { + return lhs.status == rhs.status + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaScene.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaScene.swift new file mode 100644 index 0000000..864e79b --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaScene.swift @@ -0,0 +1,63 @@ +// +// MediaScene.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional element to specify various scenes within a media object. It can +/// have multiple child elements, where each +/// element contains information about a particular scene. has +/// the optional sub-elements , , +/// and , which contains title, description, +/// start and end time of a particular scene in the media, respectively. +public class MediaScene { + + /// The scene's title. + public var sceneTitle: String? + + /// The scene's description. + public var sceneDescription: String? + + /// The scene's start time. + public var sceneStartTime: TimeInterval? + + /// The scene's end time. + public var sceneEndTime: TimeInterval? + + public init() { } + +} + +// MARK: - Equatable + +extension MediaScene: Equatable { + + public static func ==(lhs: MediaScene, rhs: MediaScene) -> Bool { + return + lhs.sceneTitle == rhs.sceneTitle && + lhs.sceneDescription == rhs.sceneDescription && + lhs.sceneStartTime == rhs.sceneStartTime && + lhs.sceneEndTime == rhs.sceneEndTime + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStarRating.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStarRating.swift new file mode 100644 index 0000000..90cf735 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStarRating.swift @@ -0,0 +1,106 @@ +// +// MediaStarRating.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This element specifies the rating-related information about a media object. +/// Valid attributes are average, count, min and max. +public class MediaStarRating { + + /// The element's attributes. + public class Attributes { + + /// The star rating's average. + public var average: Double? + + /// The star rating's total count. + public var count: Int? + + /// The star rating's minimum value. + public var min: Int? + + /// The star rating's maximum value. + public var max: Int? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaStarRating { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaStarRating.Attributes(attributes: attributeDict) + } + +} + +extension MediaStarRating.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.average = Double(attributeDict["average"] ?? "") + self.count = Int(attributeDict["count"] ?? "") + self.min = Int(attributeDict["min"] ?? "") + self.max = Int(attributeDict["max"] ?? "") + + } + +} + +// MARK: - Equatable + +extension MediaStarRating: Equatable { + + public static func ==(lhs: MediaStarRating, rhs: MediaStarRating) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaStarRating.Attributes: Equatable { + + public static func ==(lhs: MediaStarRating.Attributes, rhs: MediaStarRating.Attributes) -> Bool { + return + lhs.average == rhs.average && + lhs.count == rhs.count && + lhs.min == rhs.min && + lhs.max == rhs.max + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatistics.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatistics.swift new file mode 100644 index 0000000..ac3145a --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatistics.swift @@ -0,0 +1,96 @@ +// +// MediaStatistics.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This element specifies various statistics about a media object like the +/// view count and the favorite count. Valid attributes are views and favorites. +public class MediaStatistics { + + /// The element's attributes. + public class Attributes { + + /// The number of views. + public var views: Int? + + /// The number fo favorites. + public var favorites: Int? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaStatistics { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaStatistics.Attributes(attributes: attributeDict) + } + +} + +extension MediaStatistics.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.views = Int(attributeDict["views"] ?? "") + self.favorites = Int(attributeDict["favorites"] ?? "") + + } + +} + +// MARK: - Equatable + +extension MediaStatistics: Equatable { + + public static func ==(lhs: MediaStatistics, rhs: MediaStatistics) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaStatistics.Attributes: Equatable { + + public static func ==(lhs: MediaStatistics.Attributes, rhs: MediaStatistics.Attributes) -> Bool { + return + lhs.views == rhs.views && + lhs.favorites == rhs.favorites + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatus.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatus.swift new file mode 100644 index 0000000..9593873 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaStatus.swift @@ -0,0 +1,100 @@ +// +// MediaStatus.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional tag to specify the status of a media object -- whether it's still +/// active or it has been blocked/deleted. +public class MediaStatus { + + /// The element's attributes. + public class Attributes { + + /// State can have values "active", "blocked" or "deleted". "active" means + /// a media object is active in the system, "blocked" means a media object + /// is blocked by the publisher, "deleted" means a media object has been + /// deleted by the publisher. + public var state: String? + + /// A reason explaining why a media object has been blocked/deleted. It can + /// be plain text or a URL. + public var reason: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaStatus { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaStatus.Attributes(attributes: attributeDict) + } + +} + +extension MediaStatus.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.state = attributeDict["state"] + self.reason = attributeDict["reason"] + + } + +} + +// MARK: - Equatable + +extension MediaStatus: Equatable { + + public static func ==(lhs: MediaStatus, rhs: MediaStatus) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaStatus.Attributes: Equatable { + + public static func ==(lhs: MediaStatus.Attributes, rhs: MediaStatus.Attributes) -> Bool { + return + lhs.state == rhs.state && + lhs.reason == rhs.reason + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaSubTitle.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaSubTitle.swift new file mode 100644 index 0000000..76cf0d1 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaSubTitle.swift @@ -0,0 +1,100 @@ +// +// MediaSubTitle.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Optional link to specify the machine-readable license associated with the +/// content. +public class MediaSubTitle { + + /// The element's attributes. + public class Attributes { + + /// The type of the subtitle. + public var type: String? + + /// The subtitle language based on the RFC 3066. + public var lang: String? + + /// The location of the subtitle. + public var href: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaSubTitle { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaSubTitle.Attributes(attributes: attributeDict) + } + +} + +extension MediaSubTitle.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.lang = attributeDict["lang"] + self.href = attributeDict["href"] + + } + +} + +// MARK: - Equatable + +extension MediaSubTitle: Equatable { + + public static func ==(lhs: MediaSubTitle, rhs: MediaSubTitle) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension MediaSubTitle.Attributes: Equatable { + + public static func ==(lhs: MediaSubTitle.Attributes, rhs: MediaSubTitle.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.lang == rhs.lang && + lhs.href == rhs.href + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTag.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTag.swift new file mode 100644 index 0000000..788bd48 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTag.swift @@ -0,0 +1,90 @@ +// +// MediaTag.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// This element contains user-generated tags separated by commas in the decreasing +/// order of each tag's weight. Each tag can be assigned an integer weight in +/// tag_name:weight format. It's up to the provider to choose the way weight is +/// determined for a tag; for example, number of occurences can be one way to +/// decide weight of a particular tag. Default weight is 1. +public class MediaTag { + + /// The tag name. + public var tag: String? + + /// The tag weight. Default to 1 if not specified. + public var weight: Int? = 1 + + public init() { } + +} + +// MARK: - Initializers + +extension MediaTag { + + convenience init(tag: String, weight: Int = 1) { + + self.init() + + self.tag = tag + self.weight = weight + + } + + static func tagsFrom(string: String) -> [MediaTag]? { + + return string.components(separatedBy: ",").compactMap({ (value) -> MediaTag? in + + let mediaTag = MediaTag() + let components = value.components(separatedBy: ":") + + if components.count > 0 { + mediaTag.tag = components.first?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) + } + + if components.count > 1 { + mediaTag.weight = Int(components.last?.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines) ?? "") + } + + return mediaTag + + }) + + } + +} + +// MARK: - Equatable + +extension MediaTag: Equatable { + + public static func ==(lhs: MediaTag, rhs: MediaTag) -> Bool { + return + lhs.tag == rhs.tag && + lhs.weight == rhs.weight + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaText.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaText.swift new file mode 100644 index 0000000..52338d5 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaText.swift @@ -0,0 +1,126 @@ +// +// MediaText.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows the inclusion of a text transcript, closed captioning or lyrics of +/// the media content. Many of these elements are permitted to provide a time +/// series of text. In such cases, it is encouraged, but not required, that the +/// elements be grouped by language and appear in time sequence order based on +/// the start time. Elements can have overlapping start and end times. It has +/// four optional attributes. +public class MediaText { + + /// The element's attributes. + public class Attributes { + + /// Specifies the type of text embedded. Possible values are either "plain" + /// or "html". Default value is "plain". All HTML must be entity-encoded. + /// It is an optional attribute. + public var type: String? + + /// The primary language encapsulated in the media object. Language codes + /// possible are detailed in RFC 3066. This attribute is used similar to + /// the xml:lang attribute detailed in the XML 1.0 Specification (Third + /// Edition). It is an optional attribute. + public var lang: String? + + /// Specifies the start time offset that the text starts being relevant to + /// the media object. An example of this would be for closed captioning. + /// It uses the NTP time code format (see: the time attribute used in + /// ). It is an optional attribute. + public var start: String? + + /// Specifies the end time that the text is relevant. If this attribute is + /// not provided, and a start time is used, it is expected that the end + /// time is either the end of the clip or the start of the next + /// element. + public var end: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaText { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaText.Attributes(attributes: attributeDict) + } + +} + + +extension MediaText.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + self.lang = attributeDict["lang"] + self.start = attributeDict["start"] + self.end = attributeDict["end"] + + } + +} + +// MARK: - Equatable + +extension MediaText: Equatable { + + public static func ==(lhs: MediaText, rhs: MediaText) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaText.Attributes: Equatable { + + public static func ==(lhs: MediaText.Attributes, rhs: MediaText.Attributes) -> Bool { + return + lhs.type == rhs.type && + lhs.lang == rhs.lang && + lhs.start == rhs.start && + lhs.end == rhs.end + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaThumbnail.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaThumbnail.swift new file mode 100644 index 0000000..0f6eade --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaThumbnail.swift @@ -0,0 +1,116 @@ +// +// MediaThumbnail.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows particular images to be used as representative images for the +/// media object. If multiple thumbnails are included, and time coding is not +/// at play, it is assumed that the images are in order of importance. It has +/// one required attribute and three optional attributes. +public class MediaThumbnail { + + /// The element's attributes. + public class Attributes { + + /// Specifies the url of the thumbnail. It is a required attribute. + public var url: String? + + /// Specifies the height of the thumbnail. It is an optional attribute. + public var width: String? + + /// Specifies the width of the thumbnail. It is an optional attribute. + public var height: String? + + /// Specifies the time offset in relation to the media object. Typically this + /// is used when creating multiple keyframes within a single video. The format + /// for this attribute should be in the DSM-CC's Normal Play Time (NTP) as used in + /// RTSP [RFC 2326 3.6 Normal Play Time]. It is an optional attribute. + public var time: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaThumbnail { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaThumbnail.Attributes(attributes: attributeDict) + } + +} + + +extension MediaThumbnail.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + self.height = attributeDict["height"] + self.width = attributeDict["width"] + self.time = attributeDict["time"] + + } + +} + +// MARK: - Equatable + +extension MediaThumbnail: Equatable { + + public static func ==(lhs: MediaThumbnail, rhs: MediaThumbnail) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaThumbnail.Attributes: Equatable { + + public static func ==(lhs: MediaThumbnail.Attributes, rhs: MediaThumbnail.Attributes) -> Bool { + return + lhs.url == rhs.url && + lhs.height == rhs.height && + lhs.width == rhs.height && + lhs.time == rhs.time + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTitle.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTitle.swift new file mode 100644 index 0000000..2e1b7cd --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Media/MediaTitle.swift @@ -0,0 +1,96 @@ +// +// MediaTitle.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The title of the particular media object. It has one optional attribute. +public class MediaTitle { + + /// The element's attributes. + public class Attributes { + + /// Specifies the type of text embedded. Possible values are either "plain" or "html". + /// Default value is "plain". All HTML must be entity-encoded. It is an optional attribute. + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension MediaTitle { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = MediaTitle.Attributes(attributes: attributeDict) + } + +} + + +extension MediaTitle.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.type = attributeDict["type"] + + } + +} + +// MARK: - Equatable + +extension MediaTitle: Equatable { + + public static func ==(lhs: MediaTitle, rhs: MediaTitle) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension MediaTitle.Attributes: Equatable { + + public static func ==(lhs: MediaTitle.Attributes, rhs: MediaTitle.Attributes) -> Bool { + return lhs.type == rhs.type + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationNamespace.swift new file mode 100644 index 0000000..f2e43e8 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationNamespace.swift @@ -0,0 +1,69 @@ +// +// SyndicationNamespace.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Provides syndication hints to aggregators and others picking up this RDF Site +/// Summary (RSS) feed regarding how often it is updated. For example, if you +/// updated your file twice an hour, updatePeriod would be "hourly" and +/// updateFrequency would be "2". The syndication module borrows from Ian Davis's +/// Open Content Syndication (OCS) directory format. It supercedes the RSS 0.91 +/// skipDay and skipHour elements. +/// +/// See http://web.resource.org/rss/1.0/modules/syndication/ +public class SyndicationNamespace { + + /// Describes the period over which the channel format is updated. Acceptable + /// values are: hourly, daily, weekly, monthly, yearly. If omitted, daily is + /// assumed. + public var syUpdatePeriod: SyndicationUpdatePeriod? + + /// Used to describe the frequency of updates in relation to the update period. + /// A positive integer indicates how many times in that period the channel is + /// updated. For example, an updatePeriod of daily, and an updateFrequency of + /// 2 indicates the channel format is updated twice daily. If omitted a value + /// of 1 is assumed. + public var syUpdateFrequency: Int? + + /// Defines a base date to be used in concert with updatePeriod and + /// updateFrequency to calculate the publishing schedule. The date format takes + /// the form: yyyy-mm-ddThh:mm + public var syUpdateBase: Date? + + public init() { } + +} + +// MARK: - Equatable + +extension SyndicationNamespace: Equatable { + + public static func ==(lhs: SyndicationNamespace, rhs: SyndicationNamespace) -> Bool { + return + lhs.syUpdatePeriod == rhs.syUpdatePeriod && + lhs.syUpdateFrequency == rhs.syUpdateFrequency && + lhs.syUpdateBase == rhs.syUpdateBase + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationUpdatePeriod.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationUpdatePeriod.swift new file mode 100644 index 0000000..229c609 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/Syndication/SyndicationUpdatePeriod.swift @@ -0,0 +1,69 @@ +// +// SyndicationUpdatePeriod.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes the period over which the channel format is updated. Acceptable +/// values are: hourly, daily, weekly, monthly, yearly. If omitted, daily is +/// assumed. +/// +/// - hourly: Every hour, the channel is updated the number of times specified +/// by `syUpdateFrequency` +/// +/// - daily: Every day, the channel is updated the number of times specified +/// by `syUpdateFrequency` +/// +/// - weekly: Every week, the channel is updated the number of times specified +/// by `syUpdateFrequency` +/// +/// - monthly: Every month, the channel is updated the number of times specified +/// by `syUpdateFrequency` +/// +/// - yearly: Every year, the channel is updated the number of times specified +public enum SyndicationUpdatePeriod: String { + case hourly = "hourly" + case daily = "daily" + case weekly = "weekly" + case monthly = "monthly" + case yearly = "yearly" +} + +extension SyndicationUpdatePeriod { + + /// Lowercase the incoming `rawValue` string to try and match the + /// `SyUpdatePeriod`'s `rawValue` + /// + /// - Parameter rawValue: The raw value. + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "hourly": self = .hourly + case "daily": self = .daily + case "weekly": self = .weekly + case "monthly": self = .monthly + case "yearly": self = .yearly + default: return nil + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesCategory.swift new file mode 100644 index 0000000..462bb33 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesCategory.swift @@ -0,0 +1,121 @@ +// +// iTunesCategory.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Users can browse podcast subject categories in the iTunes Store by choosing +/// a category from the Podcasts pop-up menu in the navigation bar. Use the +/// tag to specify the browsing category for your podcast. +/// +/// You can also define a subcategory if one is available within your category. +/// Although you can specify more than one category and subcategory in your +/// feed, the iTunes Store only recognizes the first category and subcategory. +/// For a complete list of categories and subcategories, see Podcasts Connect +/// categories. +/// +/// Note: When specifying categories and subcategories, be sure to properly +/// escape ampersands: +/// +/// Single category: +/// +/// +/// Category with ampersand: +/// +/// +/// Category with subcategory: +/// +/// +/// +/// +/// Multiple categories: +/// +/// +/// +/// +/// +/// +public class ITunesCategory { + + /// The attributes of the element. + public class Attributes { + + /// The primary iTunes Category. + public var text: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The iTunes SubCategory. + public var subcategory: ITunesSubCategory? + + public init() { } + +} + +// MARK: - Initializers + +extension ITunesCategory { + + convenience init(attributes attributesDict: [String: String]) { + self.init() + self.attributes = ITunesCategory.Attributes(attributes: attributesDict) + } +} + +extension ITunesCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.text = attributeDict["text"] + + } + +} + +// MARK: - Equatable + +extension ITunesCategory: Equatable { + + public static func ==(lhs: ITunesCategory, rhs: ITunesCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension ITunesCategory.Attributes: Equatable { + + public static func ==(lhs: ITunesCategory.Attributes, rhs: ITunesCategory.Attributes) -> Bool { + return lhs.text == rhs.text + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesImage.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesImage.swift new file mode 100644 index 0000000..b2bc5f1 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesImage.swift @@ -0,0 +1,112 @@ +// +// ITunesImage.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Specify your podcast artwork using the attribute in the +/// tag. If you do not specify the tag, the +/// iTunes Store uses the content specified in the RSS feed image tag and Apple +/// does not consider your podcast for feature placement on the iTunes Store or +/// Podcasts. +/// +/// Depending on their device, subscribers see your podcast artwork in varying +/// sizes. Therefore, make sure your design is effective at both its original +/// size and at thumbnail size. Apple recommends including a title, brand, or +/// source name as part of your podcast artwork. For examples of podcast +/// artwork, see the Top Podcasts. To avoid technical issues when you update +/// your podcast artwork, be sure to: +/// +/// Change the artwork file name and URL at the same time +/// Verify the web server hosting your artwork allows HTTP head requests +/// The tag is also supported at the (episode) level. +/// For best results, Apple recommends embedding the same artwork within the +/// metadata for that episode's media file prior to uploading to your host +/// server; using Garageband or another content-creation tool to edit your +/// media file if needed. +/// +/// Note: Artwork must be a minimum size of 1400 x 1400 pixels and a maximum +/// size of 3000 x 3000 pixels, in JPEG or PNG format, 72 dpi, with appropriate +/// file extensions (.jpg, .png), and in the RGB colorspace. These requirements +/// are different from the standard RSS image tag specifications. +public class ITunesImage { + + /// The attributes of the element. + public class Attributes { + + /// The image's url. + public var href: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension ITunesImage { + + convenience init(attributes attributesDict: [String: String]) { + self.init() + self.attributes = ITunesImage.Attributes(attributes: attributesDict) + } +} + +extension ITunesImage.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.href = attributeDict["href"] + + } + +} + +// MARK: - Equatable + +extension ITunesImage: Equatable { + + public static func ==(lhs: ITunesImage, rhs: ITunesImage) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension ITunesImage.Attributes: Equatable { + + public static func ==(lhs: ITunesImage.Attributes, rhs: ITunesImage.Attributes) -> Bool { + return lhs.href == rhs.href + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesNamespace.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesNamespace.swift new file mode 100644 index 0000000..f563896 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesNamespace.swift @@ -0,0 +1,269 @@ +// +// iTunesNamespace.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// iTunes Podcasting Tags are de facto standard for podcast syndication. For more +/// information see https://help.apple.com/itc/podcasts_connect/#/itcb54353390 +public class ITunesNamespace { + + /// The content you specify in the tag appears in the Artist + /// column on the iTunes Store. If the tag is not present, the iTunes Store + /// uses the contents of the tag. If is not present + /// at the RSS feed level, the iTunes Store uses the contents of the + /// tag. + public var iTunesAuthor: String? + + /// Specifying the tag with a Yes value in: + /// + /// - A tag (podcast), prevents the entire podcast from appearing on + /// the iTunes Store podcast directory + /// + /// - An tag (episode), prevents that episode from appearing on the + /// iTunes Store podcast directory + /// + /// For example, you might want to block a specific episode if you know that + /// its content would otherwise cause the entire podcast to be removed from + /// the iTunes Store. Specifying any value other than Yes has no effect. + public var iTunesBlock: String? + + /// Users can browse podcast subject categories in the iTunes Store by choosing + /// a category from the Podcasts pop-up menu in the navigation bar. Use the + /// tag to specify the browsing category for your podcast. + /// + /// You can also define a subcategory if one is available within your category. + /// Although you can specify more than one category and subcategory in your + /// feed, the iTunes Store only recognizes the first category and subcategory. + /// For a complete list of categories and subcategories, see Podcasts Connect + /// categories. + /// + /// Note: When specifying categories and subcategories, be sure to properly + /// escape ampersands: + /// + /// Single category: + /// + /// + /// Category with ampersand: + /// + /// + /// Category with subcategory: + /// + /// + /// + /// + /// Multiple categories: + /// + /// + /// + /// + /// + /// + public var iTunesCategories: [ITunesCategory]? + + /// Specify your podcast artwork using the attribute in the + /// tag. If you do not specify the tag, the + /// iTunes Store uses the content specified in the RSS feed image tag and Apple + /// does not consider your podcast for feature placement on the iTunes Store or + /// Podcasts. + /// + /// Depending on their device, subscribers see your podcast artwork in varying + /// sizes. Therefore, make sure your design is effective at both its original + /// size and at thumbnail size. Apple recommends including a title, brand, or + /// source name as part of your podcast artwork. For examples of podcast + /// artwork, see the Top Podcasts. To avoid technical issues when you update + /// your podcast artwork, be sure to: + /// + /// Change the artwork file name and URL at the same time + /// Verify the web server hosting your artwork allows HTTP head requests + /// The tag is also supported at the (episode) level. + /// For best results, Apple recommends embedding the same artwork within the + /// metadata for that episode's media file prior to uploading to your host + /// server; using Garageband or another content-creation tool to edit your + /// media file if needed. + /// + /// Note: Artwork must be a minimum size of 1400 x 1400 pixels and a maximum + /// size of 3000 x 3000 pixels, in JPEG or PNG format, 72 dpi, with appropriate + /// file extensions (.jpg, .png), and in the RGB colorspace. These requirements + /// are different from the standard RSS image tag specifications. + public var iTunesImage: ITunesImage? + + /// The content you specify in the tag appears in the Time + /// column in the List View on the iTunes Store. + /// + /// Specify one of the following formats for the tag value: + /// + /// HH:MM:SS + /// H:MM:SS + /// MM:SS + /// M:SS + /// + /// Where H = hours, M = minutes, and S = seconds. + /// + /// If you specify a single number as a value (without colons), the iTunes + /// Store displays the value as seconds. If you specify one colon, the iTunes + /// Store displays the number to the left as minutes and the number to the + /// right as seconds. If you specify more then two colons, the iTunes Store + /// ignores the numbers farthest to the right. + public var iTunesDuration: TimeInterval? + + /// The tag indicates whether your podcast contains explicit + /// material. You can specify the following values: + /// + /// Yes | Explicit | True. If you specify yes, explicit, or true, indicating + /// the presence of explicit content, the iTunes Store displays an Explicit + /// parental advisory graphic for your podcast. + /// Clean | No | False. If you specify clean, no, or false, indicating that + /// none of your podcast episodes contain explicit language or adult content, + /// the iTunes Store displays a Clean parental advisory graphic for your + /// podcast. + /// + /// Note: Podcasts containing explicit material are not available in some + /// iTunes Store territories. + public var iTunesExplicit: String? + + /// Specifying the tag with a Yes value indicates + /// that the video podcast episode is embedded with closed captioning and the + /// iTunes Store should display a closed-caption icon next to the corresponding + /// episode. This tag is only supported at the level (episode). + /// + /// Note: If you specify a value other than Yes, no closed-caption indicator + /// appears. + public var isClosedCaptioned: String? + + /// Use the tag to specify the number value in which you would + /// like the episode to appear and override the default ordering of episodes + /// on the iTunes Store. + /// + /// For example, if you want an to appear as the first episode of your + /// podcast, specify the tag with 1. If conflicting order + /// values are present in multiple episodes, the iTunes Store uses . + public var iTunesOrder: Int? + + /// Specifying the tag with a Yes value indicates that a + /// podcast is complete and you will not post any more episodes in the future. + /// This tag is only supported at the level (podcast). + /// + /// Note: If you specify a value other than Yes, nothing happens. + public var iTunesComplete: String? + + /// Use the tag to manually change the URL where your + /// podcast is located. This tag is only supported at a level + /// (podcast). + /// + /// http://newlocation.com/example.rss + /// Note: You should maintain your old feed until you have migrated your e + /// xisting subscribers. For more information, see Update your RSS feed URL. + public var iTunesNewFeedURL: String? + + /// Use the tag to specify contact information for the podcast + /// owner. Include the email address of the owner in a nested + /// tag and the name of the owner in a nested tag. + /// + /// The tag information is for administrative communication + /// about the podcast and is not displayed on the iTunes Store. + public var iTunesOwner: ITunesOwner? + + /// The content you specify in the tag appears in the + /// Description column on the iTunes Store. For best results, choose a subtitle + /// that is only a few words long. + public var iTunesSubtitle: String? + + /// The content you specify in the tag appears on the iTunes + /// Store page for your podcast. You can specify up to 4000 characters. The + /// information also appears in a separate window if a users clicks the + /// Information icon (Information icon) in the Description column. If you do + /// not specify a tag, the iTunes Store uses the information + /// in the tag. + public var iTunesSummary: String? + + /// Note: The keywords tag is deprecated by Apple and no longer documented in + /// the official list of tags. However many podcasts still use the tags and it + /// may be of use for developers building directory or search functionality so + /// it is included. + /// + /// + /// This tag allows users to search on text keywords. + /// Limited to 255 characters or less, plain text, no HTML, words must be + /// separated by spaces. + /// This tag is applicable to the Item element only. + public var iTunesKeywords: String? + + /// Use the tag to indicate how you intend for episodes to be + /// presented. You can specify the following values: + /// + /// episodic | serial. If you specify episodic it means you intend for + /// episodes to be presented newest-to-oldest. This is the default behavior + /// in the iTunes Store if the tag is excluded. If you specify serial it + /// means you intend for episodes to be presented oldest-to-newest. + public var iTunesType: String? + + /// Use the tag to indicate what type of show item the + /// entry is. You can specify the following values: + /// + /// full | trailer | bonus. If you specify full, it means this is the full + /// content of a show. Trailer means this is a preview of the show. Bonus + /// means it is extra content for a show. + public var iTunesEpisodeType: String? + + /// Use the tag to indicate which season the item is part of. + /// + /// Note: The iTunes Store & Apple Podcasts does not show the season number + /// until a feed contains at least two seasons. + public var iTunesSeason: Int? + + /// Use the tag in conjunction with the tag + /// to indicate the order an episode should be presented within a season. + public var iTunesEpisode: Int? + + public init() { } + +} + +// MARK: - Equatable + +extension ITunesNamespace: Equatable { + + public static func ==(lhs: ITunesNamespace, rhs: ITunesNamespace) -> Bool { + return + lhs.iTunesAuthor == rhs.iTunesAuthor && + lhs.iTunesBlock == rhs.iTunesBlock && + lhs.iTunesCategories == rhs.iTunesCategories && + lhs.iTunesImage == rhs.iTunesImage && + lhs.iTunesDuration == rhs.iTunesDuration && + lhs.iTunesExplicit == rhs.iTunesExplicit && + lhs.isClosedCaptioned == rhs.isClosedCaptioned && + lhs.iTunesOrder == rhs.iTunesOrder && + lhs.iTunesComplete == rhs.iTunesComplete && + lhs.iTunesNewFeedURL == rhs.iTunesNewFeedURL && + lhs.iTunesOwner == rhs.iTunesOwner && + lhs.iTunesSubtitle == rhs.iTunesSubtitle && + lhs.iTunesSummary == rhs.iTunesSummary && + lhs.iTunesKeywords == rhs.iTunesKeywords && + lhs.iTunesType == rhs.iTunesType && + lhs.iTunesEpisodeType == rhs.iTunesEpisodeType && + lhs.iTunesSeason == rhs.iTunesSeason && + lhs.iTunesEpisode == rhs.iTunesEpisode + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesOwner.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesOwner.swift new file mode 100644 index 0000000..356ba5c --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesOwner.swift @@ -0,0 +1,55 @@ +// +// iTunesOwner.swift +// +// Copyright (c) 2017 Ben Murphy +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Use the tag to specify contact information for the podcast +/// owner. Include the email address of the owner in a nested tag +/// and the name of the owner in a nested tag. +/// +/// The tag information is for administrative communication about +/// the podcast and is not displayed on the iTunes Store. +public class ITunesOwner { + + /// The email address of the owner. + public var email: String? + + /// The name of the owner. + public var name: String? + + public init() { } + +} + +// MARK: - Equatable + +extension ITunesOwner: Equatable { + + public static func ==(lhs: ITunesOwner, rhs: ITunesOwner) -> Bool { + return + lhs.email == rhs.email && + lhs.name == rhs.name + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesSubCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesSubCategory.swift new file mode 100644 index 0000000..bf15827 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/Namespaces/iTunes/iTunesSubCategory.swift @@ -0,0 +1,118 @@ +// +// ITunesSubCategory.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Users can browse podcast subject categories in the iTunes Store by choosing +/// a category from the Podcasts pop-up menu in the navigation bar. Use the +/// tag to specify the browsing category for your podcast. +/// +/// You can also define a subcategory if one is available within your category. +/// Although you can specify more than one category and subcategory in your +/// feed, the iTunes Store only recognizes the first category and subcategory. +/// For a complete list of categories and subcategories, see Podcasts Connect +/// categories. +/// +/// Note: When specifying categories and subcategories, be sure to properly +/// escape ampersands: +/// +/// Single category: +/// +/// +/// Category with ampersand: +/// +/// +/// Category with subcategory: +/// +/// +/// +/// +/// Multiple categories: +/// +/// +/// +/// +/// +/// +public class ITunesSubCategory { + + /// The attributes of the element. + public class Attributes { + + /// The primary iTunes Category. + public var text: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension ITunesSubCategory { + + convenience init(attributes attributesDict: [String: String]) { + self.init() + self.attributes = ITunesSubCategory.Attributes(attributes: attributesDict) + } +} + +extension ITunesSubCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.text = attributeDict["text"] + + } + +} + +// MARK: - Equatable + +extension ITunesSubCategory: Equatable { + + public static func ==(lhs: ITunesSubCategory, rhs: ITunesSubCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension ITunesSubCategory.Attributes: Equatable { + + public static func ==(lhs: ITunesSubCategory.Attributes, rhs: ITunesSubCategory.Attributes) -> Bool { + return lhs.text == rhs.text + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RDFPath.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RDFPath.swift new file mode 100644 index 0000000..863ed2f --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RDFPath.swift @@ -0,0 +1,89 @@ +// +// RDFPath.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes the individual path for each XML DOM element of an RDF feed +/// +/// See http://www.rssboard.org/rss-0-9-0 +enum RDFPath: String { + + case rdf = "/rdf:RDF" + case rdfChannel = "/rdf:RDF/channel" + case rdfChannelTitle = "/rdf:RDF/channel/title" + case rdfChannelLink = "/rdf:RDF/channel/link" + case rdfChannelDescription = "/rdf:RDF/channel/description" + case rdfChannelImage = "/rdf:RDF/channel/image" + case rdfChannelItems = "/rdf:RDF/channel/items" + case rdfChannelItemsRdfSeq = "/rdf:RDF/channel/items/rdf:Seq" + case rdfChannelItemsRdfSeqRdfLi = "/rdf:RDF/channel/items/rdf:Seq/rdf:li" + case rdfImage = "/rdf:RDF/image" + case rdfImageTitle = "/rdf:RDF/image/title" + case rdfImageURL = "/rdf:RDF/image/url" + case rdfImageLink = "/rdf:RDF/image/link" + case rdfItem = "/rdf:RDF/item" + case rdfItemTitle = "/rdf:RDF/item/title" + case rdfItemLink = "/rdf:RDF/item/link" + case rdfItemDescription = "/rdf:RDF/item/description" + + // Syndication + + case rdfChannelSyndicationUpdatePeriod = "/rdf:RDF/channel/sy:updatePeriod" + case rdfChannelSyndicationUpdateFrequency = "/rdf:RDF/channel/sy:updateFrequency" + case rdfChannelSyndicationUpdateBase = "/rdf:RDF/channel/sy:updateBase" + + // Dublin Core + + case rdfChannelDublinCoreTitle = "/rdf:RDF/channel/dc:title" + case rdfChannelDublinCoreCreator = "/rdf:RDF/channel/dc:creator" + case rdfChannelDublinCoreSubject = "/rdf:RDF/channel/dc:subject" + case rdfChannelDublinCoreDescription = "/rdf:RDF/channel/dc:description" + case rdfChannelDublinCorePublisher = "/rdf:RDF/channel/dc:publisher" + case rdfChannelDublinCoreContributor = "/rdf:RDF/channel/dc:contributor" + case rdfChannelDublinCoreDate = "/rdf:RDF/channel/dc:date" + case rdfChannelDublinCoreType = "/rdf:RDF/channel/dc:type" + case rdfChannelDublinCoreFormat = "/rdf:RDF/channel/dc:format" + case rdfChannelDublinCoreIdentifier = "/rdf:RDF/channel/dc:identifier" + case rdfChannelDublinCoreSource = "/rdf:RDF/channel/dc:source" + case rdfChannelDublinCoreLanguage = "/rdf:RDF/channel/dc:language" + case rdfChannelDublinCoreRelation = "/rdf:RDF/channel/dc:relation" + case rdfChannelDublinCoreCoverage = "/rdf:RDF/channel/dc:coverage" + case rdfChannelDublinCoreRights = "/rdf:RDF/channel/dc:rights" + case rdfItemDublinCoreTitle = "/rdf:RDF/item/dc:title" + case rdfItemDublinCoreCreator = "/rdf:RDF/item/dc:creator" + case rdfItemDublinCoreSubject = "/rdf:RDF/item/dc:subject" + case rdfItemDublinCoreDescription = "/rdf:RDF/item/dc:description" + case rdfItemDublinCorePublisher = "/rdf:RDF/item/dc:publisher" + case rdfItemDublinCoreContributor = "/rdf:RDF/item/dc:contributor" + case rdfItemDublinCoreDate = "/rdf:RDF/item/dc:date" + case rdfItemDublinCoreType = "/rdf:RDF/item/dc:type" + case rdfItemDublinCoreFormat = "/rdf:RDF/item/dc:format" + case rdfItemDublinCoreIdentifier = "/rdf:RDF/item/dc:identifier" + case rdfItemDublinCoreSource = "/rdf:RDF/item/dc:source" + case rdfItemDublinCoreLanguage = "/rdf:RDF/item/dc:language" + case rdfItemDublinCoreRelation = "/rdf:RDF/item/dc:relation" + case rdfItemDublinCoreCoverage = "/rdf:RDF/item/dc:coverage" + case rdfItemDublinCoreRights = "/rdf:RDF/item/dc:rights" + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapAttributes.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapAttributes.swift new file mode 100644 index 0000000..256c60c --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapAttributes.swift @@ -0,0 +1,563 @@ +// +// RSSFeed + mapAttributes.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension RSSFeed { + + /// Maps the attributes of the specified dictionary for a given `RSSPath` + /// to the `RSSFeed` model, + /// + /// - Parameters: + /// - attributes: The attribute dictionary to map to the model. + /// - path: The path of feed's element. + func map(_ attributes: [String : String], for path: RSSPath) { + + switch path { + + case .rssChannelItem: + + if self.items == nil { + self.items = [] + } + + self.items?.append(RSSFeedItem()) + + case .rssChannelImage: + + if self.image == nil { + self.image = RSSFeedImage() + } + + case .rssChannelSkipDays: + + if self.skipDays == nil { + self.skipDays = [] + } + + case .rssChannelSkipHours: + + if self.skipHours == nil { + self.skipHours = [] + } + + case .rssChannelTextInput: + + if self.textInput == nil { + self.textInput = RSSFeedTextInput() + } + + case .rssChannelCategory: + + if self.categories == nil { + self.categories = [] + } + + self.categories?.append(RSSFeedCategory(attributes: attributes)) + + case .rssChannelCloud: + + if self.cloud == nil { + self.cloud = RSSFeedCloud(attributes: attributes) + } + + case .rssChannelItemCategory: + + if self.items?.last?.categories == nil { + self.items?.last?.categories = [] + } + + self.items?.last?.categories?.append(RSSFeedItemCategory(attributes: attributes)) + + case .rssChannelItemEnclosure: + + if self.items?.last?.enclosure == nil { + self.items?.last?.enclosure = RSSFeedItemEnclosure(attributes: attributes) + } + + case .rssChannelItemGUID: + + if self.items?.last?.guid == nil { + self.items?.last?.guid = RSSFeedItemGUID(attributes: attributes) + } + + case .rssChannelItemSource: + + if self.items?.last?.source == nil { + self.items?.last?.source = RSSFeedItemSource(attributes: attributes) + } + + case .rssChannelItemContentEncoded: + + if self.items?.last?.content == nil { + self.items?.last?.content = ContentNamespace() + } + + + case + .rssChannelSyndicationUpdateBase, + .rssChannelSyndicationUpdatePeriod, + .rssChannelSyndicationUpdateFrequency: + + if self.syndication == nil { + self.syndication = SyndicationNamespace() + } + + case + .rssChannelDublinCoreTitle, + .rssChannelDublinCoreCreator, + .rssChannelDublinCoreSubject, + .rssChannelDublinCoreDescription, + .rssChannelDublinCorePublisher, + .rssChannelDublinCoreContributor, + .rssChannelDublinCoreDate, + .rssChannelDublinCoreType, + .rssChannelDublinCoreFormat, + .rssChannelDublinCoreIdentifier, + .rssChannelDublinCoreSource, + .rssChannelDublinCoreLanguage, + .rssChannelDublinCoreRelation, + .rssChannelDublinCoreCoverage, + .rssChannelDublinCoreRights: + + if self.dublinCore == nil { + self.dublinCore = DublinCoreNamespace() + } + + case + .rssChannelItemDublinCoreTitle, + .rssChannelItemDublinCoreCreator, + .rssChannelItemDublinCoreSubject, + .rssChannelItemDublinCoreDescription, + .rssChannelItemDublinCorePublisher, + .rssChannelItemDublinCoreContributor, + .rssChannelItemDublinCoreDate, + .rssChannelItemDublinCoreType, + .rssChannelItemDublinCoreFormat, + .rssChannelItemDublinCoreIdentifier, + .rssChannelItemDublinCoreSource, + .rssChannelItemDublinCoreLanguage, + .rssChannelItemDublinCoreRelation, + .rssChannelItemDublinCoreCoverage, + .rssChannelItemDublinCoreRights: + + if self.items?.last?.dublinCore == nil { + self.items?.last?.dublinCore = DublinCoreNamespace() + } + + case + .rssChannelItunesAuthor, + .rssChannelItunesBlock, + .rssChannelItunesCategory, + .rssChannelItunesSubcategory, + .rssChannelItunesImage, + .rssChannelItunesExplicit, + .rssChannelItunesComplete, + .rssChannelItunesNewFeedURL, + .rssChannelItunesOwner, + .rssChannelItunesOwnerName, + .rssChannelItunesOwnerEmail, + .rssChannelItunesSubtitle, + .rssChannelItunesSummary, + .rssChannelItunesKeywords, + .rssChannelItunesType: + + if self.iTunes == nil { + self.iTunes = ITunesNamespace() + } + + switch path { + + case .rssChannelItunesCategory: + + if self.iTunes?.iTunesCategories == nil { + self.iTunes?.iTunesCategories = [] + } + + self.iTunes?.iTunesCategories?.append(ITunesCategory(attributes: attributes)) + + case .rssChannelItunesSubcategory: + + self.iTunes?.iTunesCategories?.last?.subcategory = ITunesSubCategory(attributes: attributes) + + case .rssChannelItunesImage: + + self.iTunes?.iTunesImage = ITunesImage(attributes: attributes) + + case .rssChannelItunesOwner: + + if self.iTunes?.iTunesOwner == nil { + self.iTunes?.iTunesOwner = ITunesOwner() + } + + default: break + + } + + case + .rssChannelItemItunesAuthor, + .rssChannelItemItunesBlock, + .rssChannelItemItunesDuration, + .rssChannelItemItunesImage, + .rssChannelItemItunesExplicit, + .rssChannelItemItunesIsClosedCaptioned, + .rssChannelItemItunesOrder, + .rssChannelItemItunesSubtitle, + .rssChannelItemItunesSummary, + .rssChannelItemItunesKeywords: + + if self.items?.last?.iTunes == nil { + self.items?.last?.iTunes = ITunesNamespace() + } + + switch path { + + case .rssChannelItemItunesImage: + + self.items?.last?.iTunes?.iTunesImage = ITunesImage(attributes: attributes) + + default: break + + } + + // MARK: Media + + case + .rssChannelItemMediaThumbnail, + .rssChannelItemMediaContent, + .rssChannelItemMediaContentTitle, + .rssChannelItemMediaContentDescription, + .rssChannelItemMediaContentPlayer, + .rssChannelItemMediaContentThumbnail, + .rssChannelItemMediaCommunity, + .rssChannelItemMediaCommunityMediaStarRating, + .rssChannelItemMediaCommunityMediaStatistics, + .rssChannelItemMediaCommunityMediaTags, + .rssChannelItemMediaComments, + .rssChannelItemMediaCommentsMediaComment, + .rssChannelItemMediaEmbed, + .rssChannelItemMediaEmbedMediaParam, + .rssChannelItemMediaResponses, + .rssChannelItemMediaResponsesMediaResponse, + .rssChannelItemMediaBackLinks, + .rssChannelItemMediaBackLinksBackLink, + .rssChannelItemMediaStatus, + .rssChannelItemMediaPrice, + .rssChannelItemMediaLicense, + .rssChannelItemMediaSubTitle, + .rssChannelItemMediaPeerLink, + .rssChannelItemMediaLocation, + .rssChannelItemMediaLocationPosition, + .rssChannelItemMediaRestriction, + .rssChannelItemMediaScenes, + .rssChannelItemMediaScenesMediaScene, + .rssChannelItemMediaGroup, + .rssChannelItemMediaGroupMediaCategory, + .rssChannelItemMediaGroupMediaCredit, + .rssChannelItemMediaGroupMediaRating, + .rssChannelItemMediaGroupMediaContent: + + if self.items?.last?.media == nil { + self.items?.last?.media = MediaNamespace() + } + + switch path { + + case .rssChannelItemMediaThumbnail: + + if self.items?.last?.media?.mediaThumbnails == nil { + self.items?.last?.media?.mediaThumbnails = [] + } + + self.items?.last?.media?.mediaThumbnails?.append(MediaThumbnail(attributes: attributes)) + + case .rssChannelItemMediaContent: + + if self.items?.last?.media?.mediaContents == nil { + self.items?.last?.media?.mediaContents = [] + } + + self.items?.last?.media?.mediaContents?.append(MediaContent(attributes: attributes)) + + case .rssChannelItemMediaContentTitle: + + if self.items?.last?.media?.mediaContents?.last?.mediaTitle == nil { + self.items?.last?.media?.mediaContents?.last?.mediaTitle = MediaTitle(attributes: attributes) + } + + case .rssChannelItemMediaContentDescription: + + if self.items?.last?.media?.mediaContents?.last?.mediaDescription == nil { + self.items?.last?.media?.mediaContents?.last?.mediaDescription = MediaDescription(attributes: attributes) + } + + case .rssChannelItemMediaContentPlayer: + + if self.items?.last?.media?.mediaContents?.last?.mediaPlayer == nil { + self.items?.last?.media?.mediaContents?.last?.mediaPlayer = MediaPlayer(attributes: attributes) + } + + case .rssChannelItemMediaContentThumbnail: + + if self.items?.last?.media?.mediaContents?.last?.mediaThumbnails == nil { + self.items?.last?.media?.mediaContents?.last?.mediaThumbnails = [] + } + + self.items?.last?.media?.mediaContents?.last?.mediaThumbnails?.append(MediaThumbnail(attributes: attributes)) + + case .rssChannelItemMediaCommunity: + + if self.items?.last?.media?.mediaCommunity == nil { + self.items?.last?.media?.mediaCommunity = MediaCommunity() + } + + case .rssChannelItemMediaCommunityMediaStarRating: + + if self.items?.last?.media?.mediaCommunity?.mediaStarRating == nil { + self.items?.last?.media?.mediaCommunity?.mediaStarRating = MediaStarRating(attributes: attributes) + } + + case .rssChannelItemMediaCommunityMediaStatistics: + + if self.items?.last?.media?.mediaCommunity?.mediaStatistics == nil { + self.items?.last?.media?.mediaCommunity?.mediaStatistics = MediaStatistics(attributes: attributes) + } + + case .rssChannelItemMediaCommunityMediaTags: + + if self.items?.last?.media?.mediaCommunity?.mediaTags == nil { + self.items?.last?.media?.mediaCommunity?.mediaTags = [] + } + + case .rssChannelItemMediaComments: + + if self.items?.last?.media?.mediaComments == nil { + self.items?.last?.media?.mediaComments = [] + } + + case .rssChannelItemMediaEmbed: + + if self.items?.last?.media?.mediaEmbed == nil { + self.items?.last?.media?.mediaEmbed = MediaEmbed(attributes: attributes) + } + + case .rssChannelItemMediaEmbedMediaParam: + + if self.items?.last?.media?.mediaEmbed?.mediaParams == nil { + self.items?.last?.media?.mediaEmbed?.mediaParams = [] + } + + self.items?.last?.media?.mediaEmbed?.mediaParams?.append(MediaParam(attributes: attributes)) + + case .rssChannelItemMediaResponses: + + if self.items?.last?.media?.mediaResponses == nil { + self.items?.last?.media?.mediaResponses = [] + } + + case .rssChannelItemMediaBackLinks: + + if self.items?.last?.media?.mediaBackLinks == nil { + self.items?.last?.media?.mediaBackLinks = [] + } + + case .rssChannelItemMediaStatus: + + if self.items?.last?.media?.mediaStatus == nil { + self.items?.last?.media?.mediaStatus = MediaStatus(attributes: attributes) + } + + case .rssChannelItemMediaPrice: + + if self.items?.last?.media?.mediaPrices == nil { + self.items?.last?.media?.mediaPrices = [] + } + + self.items?.last?.media?.mediaPrices?.append(MediaPrice(attributes: attributes)) + + case .rssChannelItemMediaLicense: + + if self.items?.last?.media?.mediaLicense == nil { + self.items?.last?.media?.mediaLicense = MediaLicence(attributes: attributes) + } + + case .rssChannelItemMediaSubTitle: + + if self.items?.last?.media?.mediaSubTitle == nil { + self.items?.last?.media?.mediaSubTitle = MediaSubTitle(attributes: attributes) + } + + case .rssChannelItemMediaPeerLink: + + if self.items?.last?.media?.mediaPeerLink == nil { + self.items?.last?.media?.mediaPeerLink = MediaPeerLink(attributes: attributes) + } + + case .rssChannelItemMediaLocation: + + if self.items?.last?.media?.mediaLocation == nil { + self.items?.last?.media?.mediaLocation = MediaLocation(attributes: attributes) + } + + case .rssChannelItemMediaRestriction: + + if self.items?.last?.media?.mediaRestriction == nil { + self.items?.last?.media?.mediaRestriction = MediaRestriction(attributes: attributes) + } + + case .rssChannelItemMediaScenes: + + if self.items?.last?.media?.mediaScenes == nil { + self.items?.last?.media?.mediaScenes = [] + } + + case .rssChannelItemMediaScenesMediaScene: + + if self.items?.last?.media?.mediaScenes == nil { + self.items?.last?.media?.mediaScenes = [] + } + + self.items?.last?.media?.mediaScenes?.append(MediaScene()) + + case .rssChannelItemMediaGroup: + + if self.items?.last?.media?.mediaGroup == nil { + self.items?.last?.media?.mediaGroup = MediaGroup() + } + + case .rssChannelItemMediaGroupMediaCategory: + + if self.items?.last?.media?.mediaGroup?.mediaCategory == nil { + self.items?.last?.media?.mediaGroup?.mediaCategory = MediaCategory(attributes: attributes) + } + + case .rssChannelItemMediaGroupMediaCredit: + + if self.items?.last?.media?.mediaGroup?.mediaCredits == nil { + self.items?.last?.media?.mediaGroup?.mediaCredits = [] + } + + self.items?.last?.media?.mediaGroup?.mediaCredits?.append(MediaCredit(attributes: attributes)) + + case .rssChannelItemMediaGroupMediaRating: + + if self.items?.last?.media?.mediaGroup?.mediaRating == nil { + self.items?.last?.media?.mediaGroup?.mediaRating = MediaRating(attributes: attributes) + } + + case .rssChannelItemMediaGroupMediaContent: + + if self.items?.last?.media?.mediaGroup?.mediaContents == nil { + self.items?.last?.media?.mediaGroup?.mediaContents = [] + } + + self.items?.last?.media?.mediaGroup?.mediaContents?.append(MediaContent(attributes: attributes)) + + default: break + + } + + + default: break + + + } + + + } + + /// Maps the attributes of the specified dictionary for a given `RSSPath` + /// to the `RSSFeed` model, + /// + /// - Parameters: + /// - attributes: The attribute dictionary to map to the model. + /// - path: The path of feed's element. + func map(_ attributes: [String : String], for path: RDFPath) { + + switch path { + + case .rdfItem: + if self.items == nil { + self.items = [] + } + + self.items?.append(RSSFeedItem()) + + case + .rdfChannelSyndicationUpdateBase, + .rdfChannelSyndicationUpdatePeriod, + .rdfChannelSyndicationUpdateFrequency: + + if self.syndication == nil { + self.syndication = SyndicationNamespace() + } + + case + .rdfChannelDublinCoreTitle, + .rdfChannelDublinCoreCreator, + .rdfChannelDublinCoreSubject, + .rdfChannelDublinCoreDescription, + .rdfChannelDublinCorePublisher, + .rdfChannelDublinCoreContributor, + .rdfChannelDublinCoreDate, + .rdfChannelDublinCoreType, + .rdfChannelDublinCoreFormat, + .rdfChannelDublinCoreIdentifier, + .rdfChannelDublinCoreSource, + .rdfChannelDublinCoreLanguage, + .rdfChannelDublinCoreRelation, + .rdfChannelDublinCoreCoverage, + .rdfChannelDublinCoreRights: + + if self.dublinCore == nil { + self.dublinCore = DublinCoreNamespace() + } + + case + .rdfItemDublinCoreTitle, + .rdfItemDublinCoreCreator, + .rdfItemDublinCoreSubject, + .rdfItemDublinCoreDescription, + .rdfItemDublinCorePublisher, + .rdfItemDublinCoreContributor, + .rdfItemDublinCoreDate, + .rdfItemDublinCoreType, + .rdfItemDublinCoreFormat, + .rdfItemDublinCoreIdentifier, + .rdfItemDublinCoreSource, + .rdfItemDublinCoreLanguage, + .rdfItemDublinCoreRelation, + .rdfItemDublinCoreCoverage, + .rdfItemDublinCoreRights: + + if self.items?.last?.dublinCore == nil { + self.items?.last?.dublinCore = DublinCoreNamespace() + } + + default: break + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapCharacters.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapCharacters.swift new file mode 100644 index 0000000..1685d88 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed + mapCharacters.swift @@ -0,0 +1,213 @@ +// +// RSSFeed + mapCharacters.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +extension RSSFeed { + + /// Maps the characters in the specified string to the `RSSFeed` model. + /// + /// - Parameters: + /// - string: The string to map to the model. + /// - path: The path of feed's element. + func map(_ string: String, for path: RSSPath) { + + switch path { + case .rssChannelTitle: self.title = self.title?.appending(string) ?? string + case .rssChannelLink: self.link = self.link?.appending(string) ?? string + case .rssChannelDescription: self.description = self.description?.appending(string) ?? string + case .rssChannelLanguage: self.language = self.language?.appending(string) ?? string + case .rssChannelCopyright: self.copyright = self.copyright?.appending(string) ?? string + case .rssChannelManagingEditor: self.managingEditor = self.managingEditor?.appending(string) ?? string + case .rssChannelWebMaster: self.webMaster = self.webMaster?.appending(string) ?? string + case .rssChannelPubDate: self.pubDate = string.toPermissiveDate() + case .rssChannelLastBuildDate: self.lastBuildDate = string.toPermissiveDate() + case .rssChannelCategory: self.categories?.last?.value = self.categories?.last?.value?.appending(string) ?? string + case .rssChannelGenerator: self.generator = self.generator?.appending(string) ?? string + case .rssChannelDocs: self.docs = self.docs?.appending(string) ?? string + case .rssChannelRating: self.rating = self.rating?.appending(string) ?? string + case .rssChannelTTL: self.ttl = Int(string) + case .rssChannelImageURL: self.image?.url = self.image?.url?.appending(string) ?? string + case .rssChannelImageTitle: self.image?.title = self.image?.title?.appending(string) ?? string + case .rssChannelImageLink: self.image?.link = self.image?.link?.appending(string) ?? string + case .rssChannelImageWidth: self.image?.width = Int(string) + case .rssChannelImageHeight: self.image?.height = Int(string) + case .rssChannelImageDescription: self.image?.description = self.image?.description?.appending(string) ?? string + case .rssChannelTextInputTitle: self.textInput?.title = self.textInput?.title?.appending(string) ?? string + case .rssChannelTextInputDescription: self.textInput?.description = self.textInput?.description?.appending(string) ?? string + case .rssChannelTextInputName: self.textInput?.name = self.textInput?.name?.appending(string) ?? string + case .rssChannelTextInputLink: self.textInput?.link = self.textInput?.link?.appending(string) ?? string + case .rssChannelSkipHoursHour: + if let hour = RSSFeedSkipHour(string), 0...23 ~= hour { + self.skipHours?.append(hour) + } + case .rssChannelSkipDaysDay: + if let day = RSSFeedSkipDay(rawValue: string) { + self.skipDays?.append(day) + } + case .rssChannelItemTitle: self.items?.last?.title = self.items?.last?.title?.appending(string) ?? string + case .rssChannelItemLink: self.items?.last?.link = self.items?.last?.link?.appending(string) ?? string + case .rssChannelItemDescription: self.items?.last?.description = self.items?.last?.description?.appending(string) ?? string + case .rssChannelItemAuthor: self.items?.last?.author = self.items?.last?.author?.appending(string) ?? string + case .rssChannelItemCategory: self.items?.last?.categories?.last?.value = self.items?.last?.categories?.last?.value?.appending(string) ?? string + case .rssChannelItemComments: self.items?.last?.comments = self.items?.last?.comments?.appending(string) ?? string + case .rssChannelItemGUID: self.items?.last?.guid?.value = self.items?.last?.guid?.value?.appending(string) ?? string + case .rssChannelItemPubDate: self.items?.last?.pubDate = string.toPermissiveDate() + case .rssChannelItemSource: self.items?.last?.source?.value = self.items?.last?.source?.value?.appending(string) ?? string + case .rssChannelItemContentEncoded: self.items?.last?.content?.contentEncoded = self.items?.last?.content?.contentEncoded?.appending(string) ?? string + case .rssChannelSyndicationUpdatePeriod: self.syndication?.syUpdatePeriod = SyndicationUpdatePeriod(rawValue: string) + case .rssChannelSyndicationUpdateFrequency: self.syndication?.syUpdateFrequency = Int(string) + case .rssChannelSyndicationUpdateBase: self.syndication?.syUpdateBase = string.toPermissiveDate() + case .rssChannelDublinCoreTitle: self.dublinCore?.dcTitle = self.dublinCore?.dcTitle?.appending(string) ?? string + case .rssChannelDublinCoreCreator: self.dublinCore?.dcCreator = self.dublinCore?.dcCreator?.appending(string) ?? string + case .rssChannelDublinCoreSubject: self.dublinCore?.dcSubject = self.dublinCore?.dcSubject?.appending(string) ?? string + case .rssChannelDublinCoreDescription: self.dublinCore?.dcDescription = self.dublinCore?.dcDescription?.appending(string) ?? string + case .rssChannelDublinCorePublisher: self.dublinCore?.dcPublisher = self.dublinCore?.dcPublisher?.appending(string) ?? string + case .rssChannelDublinCoreContributor: self.dublinCore?.dcContributor = self.dublinCore?.dcContributor?.appending(string) ?? string + case .rssChannelDublinCoreDate: self.dublinCore?.dcDate = string.toPermissiveDate() + case .rssChannelDublinCoreType: self.dublinCore?.dcType = self.dublinCore?.dcType?.appending(string) ?? string + case .rssChannelDublinCoreFormat: self.dublinCore?.dcFormat = self.dublinCore?.dcFormat?.appending(string) ?? string + case .rssChannelDublinCoreIdentifier: self.dublinCore?.dcIdentifier = self.dublinCore?.dcIdentifier?.appending(string) ?? string + case .rssChannelDublinCoreSource: self.dublinCore?.dcSource = self.dublinCore?.dcSource?.appending(string) ?? string + case .rssChannelDublinCoreLanguage: self.dublinCore?.dcLanguage = self.dublinCore?.dcLanguage?.appending(string) ?? string + case .rssChannelDublinCoreRelation: self.dublinCore?.dcRelation = self.dublinCore?.dcRelation?.appending(string) ?? string + case .rssChannelDublinCoreCoverage: self.dublinCore?.dcCoverage = self.dublinCore?.dcCoverage?.appending(string) ?? string + case .rssChannelDublinCoreRights: self.dublinCore?.dcRights = self.dublinCore?.dcRights?.appending(string) ?? string + case .rssChannelItemDublinCoreTitle: self.items?.last?.dublinCore?.dcTitle = self.items?.last?.dublinCore?.dcTitle?.appending(string) ?? string + case .rssChannelItemDublinCoreCreator: self.items?.last?.dublinCore?.dcCreator = self.items?.last?.dublinCore?.dcCreator?.appending(string) ?? string + case .rssChannelItemDublinCoreSubject: self.items?.last?.dublinCore?.dcSubject = self.items?.last?.dublinCore?.dcSubject?.appending(string) ?? string + case .rssChannelItemDublinCoreDescription: self.items?.last?.dublinCore?.dcDescription = self.items?.last?.dublinCore?.dcDescription?.appending(string) ?? string + case .rssChannelItemDublinCorePublisher: self.items?.last?.dublinCore?.dcPublisher = self.items?.last?.dublinCore?.dcPublisher?.appending(string) ?? string + case .rssChannelItemDublinCoreContributor: self.items?.last?.dublinCore?.dcContributor = self.items?.last?.dublinCore?.dcContributor?.appending(string) ?? string + case .rssChannelItemDublinCoreDate: self.items?.last?.dublinCore?.dcDate = string.toPermissiveDate() + case .rssChannelItemDublinCoreType: self.items?.last?.dublinCore?.dcType = self.items?.last?.dublinCore?.dcType?.appending(string) ?? string + case .rssChannelItemDublinCoreFormat: self.items?.last?.dublinCore?.dcFormat = self.items?.last?.dublinCore?.dcFormat?.appending(string) ?? string + case .rssChannelItemDublinCoreIdentifier: self.items?.last?.dublinCore?.dcIdentifier = self.items?.last?.dublinCore?.dcIdentifier?.appending(string) ?? string + case .rssChannelItemDublinCoreSource: self.items?.last?.dublinCore?.dcSource = self.items?.last?.dublinCore?.dcSource?.appending(string) ?? string + case .rssChannelItemDublinCoreLanguage: self.items?.last?.dublinCore?.dcLanguage = self.items?.last?.dublinCore?.dcLanguage?.appending(string) ?? string + case .rssChannelItemDublinCoreRelation: self.items?.last?.dublinCore?.dcRelation = self.items?.last?.dublinCore?.dcRelation?.appending(string) ?? string + case .rssChannelItemDublinCoreCoverage: self.items?.last?.dublinCore?.dcCoverage = self.items?.last?.dublinCore?.dcCoverage?.appending(string) ?? string + case .rssChannelItemDublinCoreRights: self.items?.last?.dublinCore?.dcRights = self.items?.last?.dublinCore?.dcRights?.appending(string) ?? string + case .rssChannelItunesAuthor: self.iTunes?.iTunesAuthor = self.iTunes?.iTunesAuthor?.appending(string) ?? string + case .rssChannelItunesBlock: self.iTunes?.iTunesBlock = self.iTunes?.iTunesBlock?.appending(string) ?? string + case .rssChannelItunesExplicit: self.iTunes?.iTunesExplicit = self.iTunes?.iTunesExplicit?.appending(string) ?? string + case .rssChannelItunesComplete: self.iTunes?.iTunesComplete = self.iTunes?.iTunesComplete?.appending(string) ?? string + case .rssChannelItunesNewFeedURL: self.iTunes?.iTunesNewFeedURL = self.iTunes?.iTunesNewFeedURL?.appending(string) ?? string + case .rssChannelItunesOwnerName: self.iTunes?.iTunesOwner?.name = self.iTunes?.iTunesOwner?.name?.appending(string) ?? string + case .rssChannelItunesOwnerEmail: self.iTunes?.iTunesOwner?.email = self.iTunes?.iTunesOwner?.email?.appending(string) ?? string + case .rssChannelItunesSubtitle: self.iTunes?.iTunesSubtitle = self.iTunes?.iTunesSubtitle?.appending(string) ?? string + case .rssChannelItunesSummary: self.iTunes?.iTunesSummary = self.iTunes?.iTunesSummary?.appending(string) ?? string + case .rssChannelItunesKeywords: self.iTunes?.iTunesKeywords = self.iTunes?.iTunesKeywords?.appending(string) ?? string + case .rssChannelItunesType: self.iTunes?.iTunesType = self.iTunes?.iTunesType?.appending(string) ?? string + case .rssChannelItemItunesAuthor: self.items?.last?.iTunes?.iTunesAuthor = self.items?.last?.iTunes?.iTunesAuthor?.appending(string) ?? string + case .rssChannelItemItunesBlock: self.items?.last?.iTunes?.iTunesBlock = self.items?.last?.iTunes?.iTunesBlock?.appending(string) ?? string + case .rssChannelItemItunesDuration: self.items?.last?.iTunes?.iTunesDuration = string.toDuration() + case .rssChannelItemItunesExplicit: self.items?.last?.iTunes?.iTunesExplicit = self.items?.last?.iTunes?.iTunesExplicit?.appending(string) ?? string + case .rssChannelItemItunesIsClosedCaptioned: self.items?.last?.iTunes?.isClosedCaptioned = self.items?.last?.iTunes?.isClosedCaptioned?.appending(string) ?? string + case .rssChannelItemItunesOrder: self.items?.last?.iTunes?.iTunesOrder = Int(string) + case .rssChannelItemItunesSubtitle: self.items?.last?.iTunes?.iTunesSubtitle = self.items?.last?.iTunes?.iTunesSubtitle?.appending(string) ?? string + case .rssChannelItemItunesSummary: self.items?.last?.iTunes?.iTunesSummary = self.items?.last?.iTunes?.iTunesSummary?.appending(string) ?? string + case .rssChannelItemItunesKeywords: self.items?.last?.iTunes?.iTunesKeywords = self.items?.last?.iTunes?.iTunesKeywords?.appending(string) ?? string + case .rssChannelItemItunesEpisodeType: self.items?.last?.iTunes?.iTunesEpisodeType = self.items?.last?.iTunes?.iTunesEpisodeType?.appending(string) ?? string + case .rssChannelItemItunesSeason: self.items?.last?.iTunes?.iTunesSeason = Int(string) + case .rssChannelItemItunesEpisode: self.items?.last?.iTunes?.iTunesEpisode = Int(string) + case .rssChannelItemMediaThumbnail: self.items?.last?.media?.mediaThumbnails?.last?.value = self.items?.last?.media?.mediaThumbnails?.last?.value?.appending(string) ?? string + case .rssChannelItemMediaLicense: self.items?.last?.media?.mediaLicense?.value = self.items?.last?.media?.mediaLicense?.value?.appending(string) ?? string + case .rssChannelItemMediaRestriction: self.items?.last?.media?.mediaRestriction?.value = self.items?.last?.media?.mediaRestriction?.value?.appending(string) ?? string + case .rssChannelItemMediaContentTitle: self.items?.last?.media?.mediaContents?.last?.mediaTitle?.value = self.items?.last?.media?.mediaContents?.last?.mediaTitle?.value?.appending(string) ?? string + case .rssChannelItemMediaContentDescription: self.items?.last?.media?.mediaContents?.last?.mediaDescription?.value = self.items?.last?.media?.mediaDescription?.value?.appending(string) ?? string + case .rssChannelItemMediaContentPlayer: self.items?.last?.media?.mediaContents?.last?.mediaPlayer?.value = self.items?.last?.media?.mediaContents?.last?.mediaPlayer?.value?.appending(string) ?? string + case .rssChannelItemMediaContentThumbnail: self.items?.last?.media?.mediaContents?.last?.mediaThumbnails?.last?.value = self.items?.last?.media?.mediaContents?.last?.mediaThumbnails?.last?.value?.appending(string) ?? string + case .rssChannelItemMediaCommunityMediaTags: self.items?.last?.media?.mediaCommunity?.mediaTags = MediaTag.tagsFrom(string: string) + case .rssChannelItemMediaCommentsMediaComment: self.items?.last?.media?.mediaComments?.append(string) + case .rssChannelItemMediaEmbedMediaParam: self.items?.last?.media?.mediaEmbed?.mediaParams?.last?.value = self.items?.last?.media?.mediaEmbed?.mediaParams?.last?.value?.appending(string) ?? string + case .rssChannelItemMediaGroupMediaCredit: self.items?.last?.media?.mediaGroup?.mediaCredits?.last?.value = self.items?.last?.media?.mediaGroup?.mediaCredits?.last?.value?.appending(string) ?? string + case .rssChannelItemMediaGroupMediaCategory: self.items?.last?.media?.mediaGroup?.mediaCategory?.value = self.items?.last?.media?.mediaGroup?.mediaCategory?.value?.appending(string) ?? string + case .rssChannelItemMediaGroupMediaRating: self.items?.last?.media?.mediaGroup?.mediaRating?.value = self.items?.last?.media?.mediaGroup?.mediaRating?.value?.appending(string) ?? string + case .rssChannelItemMediaResponsesMediaResponse: self.items?.last?.media?.mediaResponses?.append(string) + case .rssChannelItemMediaBackLinksBackLink: self.items?.last?.media?.mediaBackLinks?.append(string) + case .rssChannelItemMediaLocationPosition: self.items?.last?.media?.mediaLocation?.mapFrom(latLng: string) + case .rssChannelItemMediaScenesMediaSceneSceneTitle: self.items?.last?.media?.mediaScenes?.last?.sceneTitle = self.items?.last?.media?.mediaScenes?.last?.sceneTitle?.appending(string) ?? string + case .rssChannelItemMediaScenesMediaSceneSceneDescription: self.items?.last?.media?.mediaScenes?.last?.sceneDescription = self.items?.last?.media?.mediaScenes?.last?.sceneDescription?.appending(string) ?? string + case .rssChannelItemMediaScenesMediaSceneSceneStartTime: self.items?.last?.media?.mediaScenes?.last?.sceneStartTime = string.toDuration() + case .rssChannelItemMediaScenesMediaSceneSceneEndTime: self.items?.last?.media?.mediaScenes?.last?.sceneEndTime = string.toDuration() + default: break + } + + } + + /// Maps the characters in the specified string to the `RSSFeed` model. + /// + /// - Parameters: + /// - string: The string to map to the model. + /// - path: The path of feed's element. + func map(_ string: String, for path: RDFPath) { + + switch path { + case .rdfChannelTitle: self.title = self.title?.appending(string) ?? string + case .rdfChannelLink: self.link = self.link?.appending(string) ?? string + case .rdfChannelDescription: self.description = self.description?.appending(string) ?? string + case .rdfChannelImage: self.image?.url = self.image?.url?.appending(string) ?? string + case .rdfItemTitle: self.items?.last?.title = self.items?.last?.title?.appending(string) ?? string + case .rdfItemLink: self.items?.last?.link = self.items?.last?.link?.appending(string) ?? string + case .rdfItemDescription: self.items?.last?.description = self.items?.last?.description?.appending(string) ?? string + case .rdfChannelSyndicationUpdatePeriod: self.syndication?.syUpdatePeriod = SyndicationUpdatePeriod(rawValue: string) + case .rdfChannelSyndicationUpdateFrequency: self.syndication?.syUpdateFrequency = Int(string) + case .rdfChannelSyndicationUpdateBase: self.syndication?.syUpdateBase = string.toPermissiveDate() + case .rdfChannelDublinCoreTitle: self.dublinCore?.dcTitle = self.dublinCore?.dcTitle?.appending(string) ?? string + case .rdfChannelDublinCoreCreator: self.dublinCore?.dcCreator = self.dublinCore?.dcCreator?.appending(string) ?? string + case .rdfChannelDublinCoreSubject: self.dublinCore?.dcSubject = self.dublinCore?.dcSubject?.appending(string) ?? string + case .rdfChannelDublinCoreDescription: self.dublinCore?.dcDescription = self.dublinCore?.dcDescription?.appending(string) ?? string + case .rdfChannelDublinCorePublisher: self.dublinCore?.dcPublisher = self.dublinCore?.dcPublisher?.appending(string) ?? string + case .rdfChannelDublinCoreContributor: self.dublinCore?.dcContributor = self.dublinCore?.dcContributor?.appending(string) ?? string + case .rdfChannelDublinCoreDate: self.dublinCore?.dcDate = string.toPermissiveDate() + case .rdfChannelDublinCoreType: self.dublinCore?.dcType = self.dublinCore?.dcType?.appending(string) ?? string + case .rdfChannelDublinCoreFormat: self.dublinCore?.dcFormat = self.dublinCore?.dcFormat?.appending(string) ?? string + case .rdfChannelDublinCoreIdentifier: self.dublinCore?.dcIdentifier = self.dublinCore?.dcIdentifier?.appending(string) ?? string + case .rdfChannelDublinCoreSource: self.dublinCore?.dcSource = self.dublinCore?.dcSource?.appending(string) ?? string + case .rdfChannelDublinCoreLanguage: self.dublinCore?.dcLanguage = self.dublinCore?.dcLanguage?.appending(string) ?? string + case .rdfChannelDublinCoreRelation: self.dublinCore?.dcRelation = self.dublinCore?.dcRelation?.appending(string) ?? string + case .rdfChannelDublinCoreCoverage: self.dublinCore?.dcCoverage = self.dublinCore?.dcCoverage?.appending(string) ?? string + case .rdfChannelDublinCoreRights: self.dublinCore?.dcRights = self.dublinCore?.dcRights?.appending(string) ?? string + case .rdfItemDublinCoreTitle: self.items?.last?.dublinCore?.dcTitle = self.items?.last?.dublinCore?.dcTitle?.appending(string) ?? string + case .rdfItemDublinCoreCreator: self.items?.last?.dublinCore?.dcCreator = self.items?.last?.dublinCore?.dcCreator?.appending(string) ?? string + case .rdfItemDublinCoreSubject: self.items?.last?.dublinCore?.dcSubject = self.items?.last?.dublinCore?.dcSubject?.appending(string) ?? string + case .rdfItemDublinCoreDescription: self.items?.last?.dublinCore?.dcDescription = self.items?.last?.dublinCore?.dcDescription?.appending(string) ?? string + case .rdfItemDublinCorePublisher: self.items?.last?.dublinCore?.dcPublisher = self.items?.last?.dublinCore?.dcPublisher?.appending(string) ?? string + case .rdfItemDublinCoreContributor: self.items?.last?.dublinCore?.dcContributor = self.items?.last?.dublinCore?.dcContributor?.appending(string) ?? string + case .rdfItemDublinCoreDate: self.items?.last?.dublinCore?.dcDate = string.toPermissiveDate() + case .rdfItemDublinCoreType: self.items?.last?.dublinCore?.dcType = self.items?.last?.dublinCore?.dcType?.appending(string) ?? string + case .rdfItemDublinCoreFormat: self.items?.last?.dublinCore?.dcFormat = self.items?.last?.dublinCore?.dcFormat?.appending(string) ?? string + case .rdfItemDublinCoreIdentifier: self.items?.last?.dublinCore?.dcIdentifier = self.items?.last?.dublinCore?.dcIdentifier?.appending(string) ?? string + case .rdfItemDublinCoreSource: self.items?.last?.dublinCore?.dcSource = self.items?.last?.dublinCore?.dcSource?.appending(string) ?? string + case .rdfItemDublinCoreLanguage: self.items?.last?.dublinCore?.dcLanguage = self.items?.last?.dublinCore?.dcLanguage?.appending(string) ?? string + case .rdfItemDublinCoreRelation: self.items?.last?.dublinCore?.dcRelation = self.items?.last?.dublinCore?.dcRelation?.appending(string) ?? string + case .rdfItemDublinCoreCoverage: self.items?.last?.dublinCore?.dcCoverage = self.items?.last?.dublinCore?.dcCoverage?.appending(string) ?? string + case .rdfItemDublinCoreRights: self.items?.last?.dublinCore?.dcRights = self.items?.last?.dublinCore?.dcRights?.appending(string) ?? string + default: break + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed.swift new file mode 100644 index 0000000..6f7de1a --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeed.swift @@ -0,0 +1,287 @@ +// +// RSSFeed.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Data model for the XML DOM of the RSS 2.0 Specification +/// See http://cyber.law.harvard.edu/rss/rss.html +/// +/// At the top level, a RSS document is a element, with a mandatory +/// attribute called version, that specifies the version of RSS that the +/// document conforms to. If it conforms to this specification, the version +/// attribute must be 2.0. +/// +/// Subordinate to the element is a single element, which +/// contains information about the channel (metadata) and its contents. +public class RSSFeed { + + /// The name of the channel. It's how people refer to your service. If + /// you have an HTML website that contains the same information as your + /// RSS file, the title of your channel should be the same as the title + /// of your website. + /// + /// Example: GoUpstate.com News Headlines + public var title: String? + + /// The URL to the HTML website corresponding to the channel. + /// + /// Example: http://www.goupstate.com/ + public var link: String? + + /// Phrase or sentence describing the channel. + /// + /// Example: The latest news from GoUpstate.com, a Spartanburg Herald-Journal + /// Web site. + public var description: String? + + /// The language the channel is written in. This allows aggregators to group + /// all Italian language sites, for example, on a single page. A list of + /// allowable values for this element, as provided by Netscape, is here: + /// http://cyber.law.harvard.edu/rss/languages.html + /// + /// You may also use values defined by the W3C: + /// http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes + /// + /// Example: en-us + public var language: String? + + /// Copyright notice for content in the channel. + /// + /// Example: Copyright 2002, Spartanburg Herald-Journal + public var copyright: String? + + /// Email address for person responsible for editorial content. + /// + /// Example: geo@herald.com (George Matesky) + public var managingEditor: String? + + /// Email address for person responsible for technical issues relating to + /// channel. + /// + /// Example: betty@herald.com (Betty Guernsey) + public var webMaster: String? + + /// The publication date for the content in the channel. For example, the + /// New York Times publishes on a daily basis, the publication date flips + /// once every 24 hours. That's when the pubDate of the channel changes. + /// All date-times in RSS conform to the Date and Time Specification of + /// RFC 822, with the exception that the year may be expressed with two + /// characters or four characters (four preferred). + /// + /// Example: Sat, 07 Sep 2002 00:00:01 GMT + public var pubDate: Date? + + /// The last time the content of the channel changed. + /// + /// Example: Sat, 07 Sep 2002 09:42:31 GMT + public var lastBuildDate: Date? + + /// Specify one or more categories that the channel belongs to. Follows the + /// same rules as the -level category element. + /// + /// Example: Newspapers + public var categories: [RSSFeedCategory]? + + /// A string indicating the program used to generate the channel. + /// + /// Example: MightyInHouse Content System v2.3 + public var generator: String? + + /// A URL that points to the documentation for the format used in the RSS + /// file. It's probably a pointer to this page. It's for people who might + /// stumble across an RSS file on a Web server 25 years from now and wonder + /// what it is. + /// + /// Example: http://blogs.law.harvard.edu/tech/rss + public var docs: String? + + /// Allows processes to register with a cloud to be notified of updates to + /// the channel, implementing a lightweight publish-subscribe protocol for + /// RSS feeds. + /// + /// Example: + /// + /// is an optional sub-element of . + /// + /// It specifies a web service that supports the rssCloud interface which can + /// be implemented in HTTP-POST, XML-RPC or SOAP 1.1. + /// + /// Its purpose is to allow processes to register with a cloud to be notified + /// of updates to the channel, implementing a lightweight publish-subscribe + /// protocol for RSS feeds. + /// + /// + /// + /// In this example, to request notification on the channel it appears in, + /// you would send an XML-RPC message to rpc.sys.com on port 80, with a path + /// of /RPC2. The procedure to call is myCloud.rssPleaseNotify. + /// + /// A full explanation of this element and the rssCloud interface is here: + /// http://cyber.law.harvard.edu/rss/soapMeetsRss.html#rsscloudInterface + public var cloud: RSSFeedCloud? + + /// The PICS rating for the channel. + public var rating: String? + + /// ttl stands for time to live. It's a number of minutes that indicates how + /// long a channel can be cached before refreshing from the source. + /// + /// Example: 60 + /// + /// is an optional sub-element of . + /// + /// ttl stands for time to live. It's a number of minutes that indicates how + /// long a channel can be cached before refreshing from the source. This makes + /// it possible for RSS sources to be managed by a file-sharing network such + /// as Gnutella. + public var ttl: Int? + + /// Specifies a GIF, JPEG or PNG image that can be displayed with the channel. + /// + /// is an optional sub-element of , which contains three + /// required and three optional sub-elements. + /// + /// is the URL of a GIF, JPEG or PNG image that represents the channel. + /// + /// describes the image, it's used in the ALT attribute of the HTML + /// <img> tag when the channel is rendered in HTML. + /// + /// <link> is the URL of the site, when the channel is rendered, the image + /// is a link to the site. (Note, in practice the image <title> and <link> + /// should have the same value as the channel's <title> and <link>. + /// + /// Optional elements include <width> and <height>, numbers, indicating the + /// width and height of the image in pixels. <description> contains text + /// that is included in the TITLE attribute of the link formed around the + /// image in the HTML rendering. + /// + /// Maximum value for width is 144, default value is 88. + /// + /// Maximum value for height is 400, default value is 31. + public var image: RSSFeedImage? + + /// Specifies a text input box that can be displayed with the channel. + /// + /// A channel may optionally contain a <textInput> sub-element, which contains + /// four required sub-elements. + /// + /// <title> -- The label of the Submit button in the text input area. + /// + /// <description> -- Explains the text input area. + /// + /// <name> -- The name of the text object in the text input area. + /// + /// <link> -- The URL of the CGI script that processes text input requests. + /// + /// The purpose of the <textInput> element is something of a mystery. You can + /// use it to specify a search engine box. Or to allow a reader to provide + /// feedback. Most aggregators ignore it. + public var textInput: RSSFeedTextInput? + + /// A hint for aggregators telling them which hours they can skip. + /// + /// An XML element that contains up to 24 <hour> sub-elements whose value is a + /// number between 0 and 23, representing a time in GMT, when aggregators, if they + /// support the feature, may not read the channel on hours listed in the skipHours + /// element. + /// + /// The hour beginning at midnight is hour zero. + public var skipHours: [RSSFeedSkipHour]? + + /// A hint for aggregators telling them which days they can skip. + /// + /// An XML element that contains up to seven <day> sub-elements whose value + /// is Monday, Tuesday, Wednesday, Thursday, Friday, Saturday or Sunday. + /// Aggregators may not read the channel during days listed in the skipDays + /// element. + public var skipDays: [RSSFeedSkipDay]? + + /// A channel may contain any number of <item>s. An item may represent a + /// "story" -- much like a story in a newspaper or magazine; if so its + /// description is a synopsis of the story, and the link points to the full + /// story. An item may also be complete in itself, if so, the description + /// contains the text (entity-encoded HTML is allowed; see examples: + /// http://cyber.law.harvard.edu/rss/encodingDescriptions.html), and + /// the link and title may be omitted. All elements of an item are optional, + /// however at least one of title or description must be present. + public var items: [RSSFeedItem]? + + + // MARK: - Namespaces + + /// The Dublin Core Metadata Element Set is a standard for cross-domain + /// resource description. + /// + /// See https://tools.ietf.org/html/rfc5013 + public var dublinCore: DublinCoreNamespace? + + /// Provides syndication hints to aggregators and others picking up this RDF Site + /// Summary (RSS) feed regarding how often it is updated. For example, if you + /// updated your file twice an hour, updatePeriod would be "hourly" and + /// updateFrequency would be "2". The syndication module borrows from Ian Davis's + /// Open Content Syndication (OCS) directory format. It supercedes the RSS 0.91 + /// skipDay and skipHour elements. + /// + /// See http://web.resource.org/rss/1.0/modules/syndication/ + public var syndication: SyndicationNamespace? + + /// iTunes Podcasting Tags are de facto standard for podcast syndication. + /// See https://help.apple.com/itc/podcasts_connect/#/itcb54353390 + public var iTunes: ITunesNamespace? + + public init() { } + +} + +// MARK: - Equatable + +extension RSSFeed: Equatable { + + public static func ==(lhs: RSSFeed, rhs: RSSFeed) -> Bool { + return + lhs.categories == rhs.categories && + lhs.cloud == rhs.cloud && + lhs.copyright == rhs.copyright && + lhs.description == rhs.description && + lhs.docs == rhs.docs && + lhs.dublinCore == rhs.dublinCore && + lhs.generator == rhs.generator && + lhs.items == rhs.items && + lhs.iTunes == rhs.iTunes && + lhs.language == rhs.language && + lhs.lastBuildDate == rhs.lastBuildDate && + lhs.link == rhs.link && + lhs.managingEditor == rhs.managingEditor && + lhs.pubDate == rhs.pubDate && + lhs.rating == rhs.rating && + lhs.skipDays == rhs.skipDays && + lhs.skipHours == rhs.skipHours && + lhs.syndication == rhs.syndication && + lhs.textInput == rhs.textInput && + lhs.title == rhs.title && + lhs.ttl == rhs.ttl && + lhs.webMaster == rhs.webMaster + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCategory.swift new file mode 100644 index 0000000..1f115f5 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCategory.swift @@ -0,0 +1,95 @@ +// +// RSSFeedCategory.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The category of `<channel>`. Identifies a category or tag to which the feed +/// belongs. +public class RSSFeedCategory { + + /// The element's attributes. + public class Attributes { + + /// A string that identifies a categorization taxonomy. It's an optional + /// attribute of `<category>`. e.g. "http://www.fool.com/cusips" + public var domain: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension RSSFeedCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedCategory.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.domain = attributeDict["domain"] + + } + +} + +// MARK: - Equatable + +extension RSSFeedCategory: Equatable { + + public static func ==(lhs: RSSFeedCategory, rhs: RSSFeedCategory) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedCategory.Attributes: Equatable { + + public static func ==(lhs: RSSFeedCategory.Attributes, rhs: RSSFeedCategory.Attributes) -> Bool { + return lhs.domain == rhs.domain + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCloud.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCloud.swift new file mode 100644 index 0000000..998d76e --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedCloud.swift @@ -0,0 +1,134 @@ +// +// RSSFeedCloud.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Allows processes to register with a cloud to be notified of updates to +/// the channel, implementing a lightweight publish-subscribe protocol for +/// RSS feeds. +/// +/// Example: <cloud domain="rpc.sys.com" port="80" path="/RPC2" registerProcedure="pingMe" protocol="soap"/> +/// +/// <cloud> is an optional sub-element of <channel>. +/// +/// It specifies a web service that supports the rssCloud interface which can +/// be implemented in HTTP-POST, XML-RPC or SOAP 1.1. +/// +/// Its purpose is to allow processes to register with a cloud to be notified +/// of updates to the channel, implementing a lightweight publish-subscribe +/// protocol for RSS feeds. +/// +/// <cloud domain="rpc.sys.com" port="80" path="/RPC2" registerProcedure="myCloud.rssPleaseNotify" protocol="xml-rpc" /> +/// +/// In this example, to request notification on the channel it appears in, +/// you would send an XML-RPC message to rpc.sys.com on port 80, with a path +/// of /RPC2. The procedure to call is myCloud.rssPleaseNotify. +/// +/// A full explanation of this element and the rssCloud interface is here: +/// http://cyber.law.harvard.edu/rss/soapMeetsRss.html#rsscloudInterface +public class RSSFeedCloud { + + /// The attributes of the `<channel>`'s `<cloud>` element. + public class Attributes { + + /// The domain to register notification to. + public var domain: String? + + /// The port to connect to. + public var port: Int? + + /// The path to the RPC service. e.g. "/RPC2". + public var path: String? + + /// The procedure to call. e.g. "myCloud.rssPleaseNotify" . + public var registerProcedure: String? + + /// The `protocol` specification. Can be HTTP-POST, XML-RPC or SOAP 1.1 - + /// Note: "protocol" is a reserved keyword, so `protocolSpecification` + /// is used instead and refers to the `protocol` attribute of the `cloud` + /// element. + public var protocolSpecification: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension RSSFeedCloud { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedCloud.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedCloud.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.domain = attributeDict["domain"] + self.port = Int(attributeDict["port"] ?? "") + self.path = attributeDict["path"] + self.registerProcedure = attributeDict["registerProcedure"] + self.protocolSpecification = attributeDict["protocol"] + + } + +} + +// MARK: - Equatable + +extension RSSFeedCloud: Equatable { + + public static func ==(lhs: RSSFeedCloud, rhs: RSSFeedCloud) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedCloud.Attributes: Equatable { + + public static func ==(lhs: RSSFeedCloud.Attributes, rhs: RSSFeedCloud.Attributes) -> Bool { + return + lhs.domain == rhs.domain && + lhs.port == rhs.port && + lhs.path == rhs.path && + lhs.registerProcedure == rhs.registerProcedure && + lhs.protocolSpecification == rhs.protocolSpecification + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedImage.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedImage.swift new file mode 100644 index 0000000..9e12f44 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedImage.swift @@ -0,0 +1,93 @@ +// +// RSSFeedImage.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Specifies a GIF, JPEG or PNG image that can be displayed with the channel. +/// +/// <image> is an optional sub-element of <channel>, which contains three +/// required and three optional sub-elements. +/// +/// <url> is the URL of a GIF, JPEG or PNG image that represents the channel. +/// +/// <title> describes the image, it's used in the ALT attribute of the HTML +/// <img> tag when the channel is rendered in HTML. +/// +/// <link> is the URL of the site, when the channel is rendered, the image +/// is a link to the site. (Note, in practice the image <title> and <link> +/// should have the same value as the channel's <title> and <link>. +/// +/// Optional elements include <width> and <height>, numbers, indicating the +/// width and height of the image in pixels. <description> contains text +/// that is included in the TITLE attribute of the link formed around the +/// image in the HTML rendering. +/// +/// Maximum value for width is 144, default value is 88. +/// +/// Maximum value for height is 400, default value is 31. +public class RSSFeedImage { + + /// The URL of a GIF, JPEG or PNG image that represents the channel. + public var url: String? + + /// Describes the image, it's used in the ALT attribute of the HTML `<img>` + /// tag when the channel is rendered in HTML. + public var title: String? + + /// The URL of the site, when the channel is rendered, the image is a link + /// to the site. (Note, in practice the image `<title>` and `<link>` should + /// have the same value as the channel's `<title>` and `<link>`. + public var link: String? + + /// Optional element `<width>` indicating the width of the image in pixels. + /// Maximum value for width is 144, default value is 88. + public var width: Int? + + /// Optional element `<height>` indicating the height of the image in pixels. + /// Maximum value for height is 400, default value is 31. + public var height: Int? + + /// Contains text that is included in the TITLE attribute of the link formed + /// around the image in the HTML rendering. + public var description: String? + + public init() { } + +} + +// MARK: - Equatable + +extension RSSFeedImage: Equatable { + + public static func ==(lhs: RSSFeedImage, rhs: RSSFeedImage) -> Bool { + return + lhs.url == rhs.url && + lhs.title == rhs.title && + lhs.link == rhs.link && + lhs.width == rhs.width && + lhs.height == rhs.height && + lhs.description == rhs.description + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItem.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItem.swift new file mode 100644 index 0000000..813338a --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItem.swift @@ -0,0 +1,221 @@ +// +// RSSFeedItem.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A channel may contain any number of <item>s. An item may represent a +/// "story" -- much like a story in a newspaper or magazine; if so its +/// description is a synopsis of the story, and the link points to the full +/// story. An item may also be complete in itself, if so, the description +/// contains the text (entity-encoded HTML is allowed; see examples: +/// http://cyber.law.harvard.edu/rss/encodingDescriptions.html), and +/// the link and title may be omitted. All elements of an item are optional, +/// however at least one of title or description must be present. +public class RSSFeedItem { + + /// The title of the item. + /// + /// Example: Venice Film Festival Tries to Quit Sinking + public var title: String? + + /// The URL of the item. + /// + /// Example: http://nytimes.com/2004/12/07FEST.html + public var link: String? + + /// The item synopsis. + /// + /// Example: Some of the most heated chatter at the Venice Film Festival this + /// week was about the way that the arrival of the stars at the Palazzo del + /// Cinema was being staged. + public var description: String? + + /// Email address of the author of the item. + /// + /// Example: oprah\@oxygen.net + /// + /// <author> is an optional sub-element of <item>. + /// + /// It's the email address of the author of the item. For newspapers and + /// magazines syndicating via RSS, the author is the person who wrote the + /// article that the <item> describes. For collaborative weblogs, the author + /// of the item might be different from the managing editor or webmaster. + /// For a weblog authored by a single individual it would make sense to omit + /// the <author> element. + /// + /// <author>lawyer@boyer.net (Lawyer Boyer)</author> + public var author: String? + + /// Includes the item in one or more categories. + /// + /// <category> is an optional sub-element of <item>. + /// + /// It has one optional attribute, domain, a string that identifies a + /// categorization taxonomy. + /// + /// The value of the element is a forward-slash-separated string that + /// identifies a hierarchic location in the indicated taxonomy. Processors + /// may establish conventions for the interpretation of categories. + /// + /// Two examples are provided below: + /// + /// <category>Grateful Dead</category> + /// <category domain="http://www.fool.com/cusips">MSFT</category> + /// + /// You may include as many category elements as you need to, for different + /// domains, and to have an item cross-referenced in different parts of the + /// same domain. + public var categories: [RSSFeedItemCategory]? + + /// URL of a page for comments relating to the item. + /// + /// Example: http://www.myblog.org/cgi-local/mt/mt-comments.cgi?entry_id=290 + /// + /// <comments> is an optional sub-element of <item>. + /// + /// If present, it is the url of the comments page for the item. + /// + /// <comments>http://ekzemplo.com/entry/4403/comments</comments> + /// + /// More about comments here: + /// http://cyber.law.harvard.edu/rss/weblogComments.html + public var comments: String? + + /// Describes a media object that is attached to the item. + /// + /// <enclosure> is an optional sub-element of <item>. + /// + /// It has three required attributes. url says where the enclosure is located, + /// length says how big it is in bytes, and type says what its type is, a + /// standard MIME type. + /// + /// The url must be an http url. + /// + /// <enclosure url="http://www.scripting.com/mp3s/weatherReportSuite.mp3" + /// length="12216320" type="audio/mpeg" /> + public var enclosure: RSSFeedItemEnclosure? + + /// A string that uniquely identifies the item. + /// + /// Example: http://inessential.com/2002/09/01.php#a2 + /// + /// <guid> is an optional sub-element of <item>. + /// + /// guid stands for globally unique identifier. It's a string that uniquely + /// identifies the item. When present, an aggregator may choose to use this + /// string to determine if an item is new. + /// + /// <guid>http://some.server.com/weblogItem3207</guid> + /// + /// There are no rules for the syntax of a guid. Aggregators must view them + /// as a string. It's up to the source of the feed to establish the + /// uniqueness of the string. + /// + /// If the guid element has an attribute named "isPermaLink" with a value of + /// true, the reader may assume that it is a permalink to the item, that is, + /// a url that can be opened in a Web browser, that points to the full item + /// described by the <item> element. An example: + /// + /// <guid isPermaLink="true">http://inessential.com/2002/09/01.php#a2</guid> + /// + /// isPermaLink is optional, its default value is true. If its value is false, + /// the guid may not be assumed to be a url, or a url to anything in + /// particular. + public var guid: RSSFeedItemGUID? + + /// Indicates when the item was published. + /// + /// Example: Sun, 19 May 2002 15:21:36 GMT + /// + /// <pubDate> is an optional sub-element of <item>. + /// + /// Its value is a date, indicating when the item was published. If it's a + /// date in the future, aggregators may choose to not display the item until + /// that date. + public var pubDate: Date? + + /// The RSS channel that the item came from. + /// + /// <source> is an optional sub-element of <item>. + /// + /// Its value is the name of the RSS channel that the item came from, derived + /// from its <title>. It has one required attribute, url, which links to the + /// XMLization of the source. + /// + /// <source url="http://www.tomalak.org/links2.xml">Tomalak's Realm</source> + /// + /// The purpose of this element is to propagate credit for links, to + /// publicize the sources of news items. It can be used in the Post command + /// of an aggregator. It should be generated automatically when forwarding + /// an item from an aggregator to a weblog authoring tool. + public var source: RSSFeedItemSource? + + + // MARK: - Namespaces + + /// The Dublin Core Metadata Element Set is a standard for cross-domain + /// resource description. + /// + /// See https://tools.ietf.org/html/rfc5013 + public var dublinCore: DublinCoreNamespace? + + /// A module for the actual content of websites, in multiple formats. + /// + /// See http://web.resource.org/rss/1.0/modules/content/ + public var content: ContentNamespace? + + /// iTunes Podcasting Tags are de facto standard for podcast syndication. + /// see https://help.apple.com/itc/podcasts_connect/#/itcb54353390 + public var iTunes: ITunesNamespace? + + /// Media RSS is a new RSS module that supplements the <enclosure> + /// capabilities of RSS 2.0. + public var media: MediaNamespace? + + public init() { } + +} + +// MARK: - Equatable + +extension RSSFeedItem: Equatable { + + public static func ==(lhs: RSSFeedItem, rhs: RSSFeedItem) -> Bool { + return + lhs.author == rhs.author && + lhs.categories == rhs.categories && + lhs.comments == rhs.comments && + lhs.content == rhs.content && + lhs.description == rhs.description && + lhs.dublinCore == rhs.dublinCore && + lhs.enclosure == rhs.enclosure && + lhs.guid == rhs.guid && + lhs.iTunes == rhs.iTunes && + lhs.media == rhs.media && + lhs.pubDate == rhs.pubDate && + lhs.source == rhs.source && + lhs.title == rhs.title + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemCategory.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemCategory.swift new file mode 100644 index 0000000..f6794e5 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemCategory.swift @@ -0,0 +1,113 @@ +// +// RSSFeedItemCategory.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Includes the item in one or more categories. +/// +/// <category> is an optional sub-element of <item>. +/// +/// It has one optional attribute, domain, a string that identifies a +/// categorization taxonomy. +/// +/// The value of the element is a forward-slash-separated string that +/// identifies a hierarchic location in the indicated taxonomy. Processors +/// may establish conventions for the interpretation of categories. +/// +/// Two examples are provided below: +/// +/// <category>Grateful Dead</category> +/// <category domain="http://www.fool.com/cusips">MSFT</category> +/// +/// You may include as many category elements as you need to, for different +/// domains, and to have an item cross-referenced in different parts of the +/// same domain. +public class RSSFeedItemCategory { + + /// The element's attributes. + public class Attributes { + + /// A string that identifies a categorization taxonomy. It's an optional + /// attribute of `<category>`. + /// + /// Example: http://www.fool.com/cusips + public var domain: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension RSSFeedItemCategory { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedItemCategory.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedItemCategory.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.domain = attributeDict["domain"] + + } + +} + +// MARK: - Equatable + +extension RSSFeedItemCategory: Equatable { + + public static func ==(lhs: RSSFeedItemCategory, rhs: RSSFeedItemCategory) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedItemCategory.Attributes: Equatable { + + public static func ==(lhs: RSSFeedItemCategory.Attributes, rhs: RSSFeedItemCategory.Attributes) -> Bool { + return lhs.domain == rhs.domain + } + +} + diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemEnclosure.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemEnclosure.swift new file mode 100644 index 0000000..d74a389 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemEnclosure.swift @@ -0,0 +1,116 @@ +// +// RSSFeedItemEnclosure.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes a media object that is attached to the item. +/// +/// <enclosure> is an optional sub-element of <item>. +/// +/// It has three required attributes. url says where the enclosure is located, +/// length says how big it is in bytes, and type says what its type is, a +/// standard MIME type. +/// +/// The url must be an http url. +/// +/// <enclosure url="http://www.scripting.com/mp3s/weatherReportSuite.mp3" +/// length="12216320" type="audio/mpeg" /> +public class RSSFeedItemEnclosure { + + /// The element's attributes. + public class Attributes { + + /// Where the enclosure is located. + /// + /// Example: http://www.scripting.com/mp3s/weatherReportSuite.mp3 + public var url: String? + + /// How big the media object is in bytes. + /// + /// Example: 12216320 + public var length: Int64? + + /// Standard MIME type. + /// + /// Example: audio/mpeg + public var type: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + public init() { } + +} + +// MARK: - Initializers + +extension RSSFeedItemEnclosure { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedItemEnclosure.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedItemEnclosure.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + self.type = attributeDict["type"] + self.length = Int64(attributeDict["length"] ?? "") + + } + +} + +// MARK: - Equatable + +extension RSSFeedItemEnclosure: Equatable { + + public static func ==(lhs: RSSFeedItemEnclosure, rhs: RSSFeedItemEnclosure) -> Bool { + return lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedItemEnclosure.Attributes: Equatable { + + public static func ==(lhs: RSSFeedItemEnclosure.Attributes, rhs: RSSFeedItemEnclosure.Attributes) -> Bool { + return + lhs.url == rhs.url && + lhs.type == rhs.type && + lhs.length == rhs.length + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemGUID.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemGUID.swift new file mode 100644 index 0000000..d1398cf --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemGUID.swift @@ -0,0 +1,127 @@ +// +// RSSFeedItemGUID.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A string that uniquely identifies the item. +/// +/// Example: http://inessential.com/2002/09/01.php#a2 +/// +/// <guid> is an optional sub-element of <item>. +/// +/// guid stands for globally unique identifier. It's a string that uniquely +/// identifies the item. When present, an aggregator may choose to use this +/// string to determine if an item is new. +/// +/// <guid>http://some.server.com/weblogItem3207</guid> +/// +/// There are no rules for the syntax of a guid. Aggregators must view them +/// as a string. It's up to the source of the feed to establish the +/// uniqueness of the string. +/// +/// If the guid element has an attribute named "isPermaLink" with a value of +/// true, the reader may assume that it is a permalink to the item, that is, +/// a url that can be opened in a Web browser, that points to the full item +/// described by the <item> element. An example: +/// +/// <guid isPermaLink="true">http://inessential.com/2002/09/01.php#a2</guid> +/// +/// isPermaLink is optional, its default value is true. If its value is false, +/// the guid may not be assumed to be a url, or a url to anything in +/// particular. +public class RSSFeedItemGUID { + + /// The element's attributes. + public class Attributes { + + /// If the guid element has an attribute named "isPermaLink" with a value of + /// true, the reader may assume that it is a permalink to the item, that is, + /// a url that can be opened in a Web browser, that points to the full item + /// described by the <item> element. An example: + /// + /// <guid isPermaLink="true">http://inessential.com/2002/09/01.php#a2</guid> + /// + /// isPermaLink is optional, its default value is true. If its value is false, + /// the guid may not be assumed to be a url, or a url to anything in + /// particular. + public var isPermaLink: Bool? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension RSSFeedItemGUID { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedItemGUID.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedItemGUID.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.isPermaLink = attributeDict["isPermaLink"]?.toBool() + + } + +} + +// MARK: - Equatable + +extension RSSFeedItemGUID: Equatable { + + public static func ==(lhs: RSSFeedItemGUID, rhs: RSSFeedItemGUID) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedItemGUID.Attributes: Equatable { + + public static func ==(lhs: RSSFeedItemGUID.Attributes, rhs: RSSFeedItemGUID.Attributes) -> Bool { + return lhs.isPermaLink == rhs.isPermaLink + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemSource.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemSource.swift new file mode 100644 index 0000000..731b364 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedItemSource.swift @@ -0,0 +1,107 @@ +// +// RSSFeedItemSource.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The RSS channel that the item came from. +/// +/// <source> is an optional sub-element of <item>. +/// +/// Its value is the name of the RSS channel that the item came from, derived +/// from its <title>. It has one required attribute, url, which links to the +/// XMLization of the source. +/// +/// <source url="http://www.tomalak.org/links2.xml">Tomalak's Realm</source> +/// +/// The purpose of this element is to propagate credit for links, to +/// publicize the sources of news items. It can be used in the Post command +/// of an aggregator. It should be generated automatically when forwarding +/// an item from an aggregator to a weblog authoring tool. +public class RSSFeedItemSource { + + /// The element's attributes. + public class Attributes { + + /// Required attribute of the `Source` element, which links to the + /// XMLization of the source. e.g. "http://www.tomalak.org/links2.xml" + public var url: String? + + } + + /// The element's attributes. + public var attributes: Attributes? + + /// The element's value. + public var value: String? + + public init() { } + +} + +// MARK: - Initializers + +extension RSSFeedItemSource { + + convenience init(attributes attributeDict: [String : String]) { + self.init() + self.attributes = RSSFeedItemSource.Attributes(attributes: attributeDict) + } + +} + +extension RSSFeedItemSource.Attributes { + + convenience init?(attributes attributeDict: [String : String]) { + + if attributeDict.isEmpty { + return nil + } + + self.init() + + self.url = attributeDict["url"] + + } + +} + +// MARK: - Equatable + +extension RSSFeedItemSource: Equatable { + + public static func ==(lhs: RSSFeedItemSource, rhs: RSSFeedItemSource) -> Bool { + return + lhs.value == rhs.value && + lhs.attributes == rhs.attributes + } + +} + +extension RSSFeedItemSource.Attributes: Equatable { + + public static func ==(lhs: RSSFeedItemSource.Attributes, rhs: RSSFeedItemSource.Attributes) -> Bool { + return lhs.url == rhs.url + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipDay.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipDay.swift new file mode 100644 index 0000000..f226d87 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipDay.swift @@ -0,0 +1,70 @@ +// +// RSSFeedSkipDay.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A hint for aggregators telling them which days they can skip. +/// +/// An XML element that contains up to seven <day> sub-elements whose value +/// is Monday, Tuesday, Wednesday, Thursday, Friday, Saturday or Sunday. +/// Aggregators may not read the channel during days listed in the skipDays +/// element. +/// +/// - monday: Aggregator hint to skip parsing on `Monday`. +/// - tuesday: Aggregator hint to skip parsing on `Tuesday`. +/// - wednesday: Aggregator hint to skip parsing on `Wednesday`. +/// - thursday: Aggregator hint to skip parsing on `Thursday`. +/// - friday: Aggregator hint to skip parsing on `Friday`. +/// - saturday: Aggregator hint to skip parsing on `Saturday`. +/// - sunday: Aggregator hint to skip parsing on `Sunday`. +public enum RSSFeedSkipDay: String { + case monday = "monday" + case tuesday = "tuesday" + case wednesday = "wednesday" + case thursday = "thursday" + case friday = "friday" + case saturday = "saturday" + case sunday = "sunday" +} + +extension RSSFeedSkipDay { + + /// Lowercase the incoming `rawValue` string to try and match the + /// `RSSFeedSkipDay`'s `rawValue` + /// + /// - Parameter rawValue: The raw value + public init?(rawValue: String) { + switch rawValue.lowercased() { + case "monday": self = .monday + case "tuesday": self = .tuesday + case "wednesday": self = .wednesday + case "thursday": self = .thursday + case "friday": self = .friday + case "saturday": self = .saturday + case "sunday": self = .sunday + default: return nil + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipHour.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipHour.swift new file mode 100644 index 0000000..ca47eb0 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedSkipHour.swift @@ -0,0 +1,35 @@ +// +// RSSFeedSkipHour.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// A hint for aggregators telling them which hours they can skip. +/// +/// An XML element that contains up to 24 <hour> sub-elements whose value is a +/// number between 0 and 23, representing a time in GMT, when aggregators, if they +/// support the feature, may not read the channel on hours listed in the skipHours +/// element. +/// +/// The hour beginning at midnight is hour zero. +public typealias RSSFeedSkipHour = Int diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedTextInput.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedTextInput.swift new file mode 100644 index 0000000..2a37879 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSFeedTextInput.swift @@ -0,0 +1,73 @@ +// +// RSSFeedTextInput.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Specifies a text input box that can be displayed with the channel. +/// +/// A channel may optionally contain a <textInput> sub-element, which contains +/// four required sub-elements. +/// +/// <title> -- The label of the Submit button in the text input area. +/// +/// <description> -- Explains the text input area. +/// +/// <name> -- The name of the text object in the text input area. +/// +/// <link> -- The URL of the CGI script that processes text input requests. +/// +/// The purpose of the <textInput> element is something of a mystery. You can +/// use it to specify a search engine box. Or to allow a reader to provide +/// feedback. Most aggregators ignore it. +public class RSSFeedTextInput { + + /// The label of the Submit button in the text input area. + public var title: String? + + /// Explains the text input area. + public var description: String? + + /// The name of the text object in the text input area. + public var name: String? + + /// The URL of the CGI script that processes text input requests. + public var link: String? + + public init() { } + +} + +// MARK: - Equatable + +extension RSSFeedTextInput: Equatable { + + public static func ==(lhs: RSSFeedTextInput, rhs: RSSFeedTextInput) -> Bool { + return + lhs.title == rhs.title && + lhs.description == rhs.description && + lhs.name == rhs.name && + lhs.link == lhs.link + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSPath.swift b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSPath.swift new file mode 100644 index 0000000..f749c11 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Models/RSS/RSSPath.swift @@ -0,0 +1,192 @@ +// +// RSSPath.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Describes the individual path for each XML DOM element of an RSS feed +/// +/// See http://web.resource.org/rss/1.0/modules/content/ +enum RSSPath: String { + + case rss = "/rss" + case rssChannel = "/rss/channel" + case rssChannelTitle = "/rss/channel/title" + case rssChannelLink = "/rss/channel/link" + case rssChannelDescription = "/rss/channel/description" + case rssChannelLanguage = "/rss/channel/language" + case rssChannelCopyright = "/rss/channel/copyright" + case rssChannelManagingEditor = "/rss/channel/managingEditor" + case rssChannelWebMaster = "/rss/channel/webMaster" + case rssChannelPubDate = "/rss/channel/pubDate" + case rssChannelLastBuildDate = "/rss/channel/lastBuildDate" + case rssChannelCategory = "/rss/channel/category" + case rssChannelGenerator = "/rss/channel/generator" + case rssChannelDocs = "/rss/channel/docs" + case rssChannelCloud = "/rss/channel/cloud" + case rssChannelRating = "/rss/channel/rating" + case rssChannelTTL = "/rss/channel/ttl" + case rssChannelImage = "/rss/channel/image" + case rssChannelImageURL = "/rss/channel/image/url" + case rssChannelImageTitle = "/rss/channel/image/title" + case rssChannelImageLink = "/rss/channel/image/link" + case rssChannelImageWidth = "/rss/channel/image/width" + case rssChannelImageHeight = "/rss/channel/image/height" + case rssChannelImageDescription = "/rss/channel/image/description" + case rssChannelTextInput = "/rss/channel/textInput" + case rssChannelTextInputTitle = "/rss/channel/textInput/title" + case rssChannelTextInputDescription = "/rss/channel/textInput/description" + case rssChannelTextInputName = "/rss/channel/textInput/name" + case rssChannelTextInputLink = "/rss/channel/textInput/link" + case rssChannelSkipHours = "/rss/channel/skipHours" + case rssChannelSkipHoursHour = "/rss/channel/skipHours/hour" + case rssChannelSkipDays = "/rss/channel/skipDays" + case rssChannelSkipDaysDay = "/rss/channel/skipDays/day" + case rssChannelItem = "/rss/channel/item" + case rssChannelItemTitle = "/rss/channel/item/title" + case rssChannelItemLink = "/rss/channel/item/link" + case rssChannelItemDescription = "/rss/channel/item/description" + case rssChannelItemAuthor = "/rss/channel/item/author" + case rssChannelItemCategory = "/rss/channel/item/category" + case rssChannelItemComments = "/rss/channel/item/comments" + case rssChannelItemEnclosure = "/rss/channel/item/enclosure" + case rssChannelItemGUID = "/rss/channel/item/guid" + case rssChannelItemPubDate = "/rss/channel/item/pubDate" + case rssChannelItemSource = "/rss/channel/item/source" + + // Content + + case rssChannelItemContentEncoded = "/rss/channel/item/content:encoded" + + // Syndication + + case rssChannelSyndicationUpdatePeriod = "/rss/channel/sy:updatePeriod" + case rssChannelSyndicationUpdateFrequency = "/rss/channel/sy:updateFrequency" + case rssChannelSyndicationUpdateBase = "/rss/channel/sy:updateBase" + + // Dublin Core + + case rssChannelDublinCoreTitle = "/rss/channel/dc:title" + case rssChannelDublinCoreCreator = "/rss/channel/dc:creator" + case rssChannelDublinCoreSubject = "/rss/channel/dc:subject" + case rssChannelDublinCoreDescription = "/rss/channel/dc:description" + case rssChannelDublinCorePublisher = "/rss/channel/dc:publisher" + case rssChannelDublinCoreContributor = "/rss/channel/dc:contributor" + case rssChannelDublinCoreDate = "/rss/channel/dc:date" + case rssChannelDublinCoreType = "/rss/channel/dc:type" + case rssChannelDublinCoreFormat = "/rss/channel/dc:format" + case rssChannelDublinCoreIdentifier = "/rss/channel/dc:identifier" + case rssChannelDublinCoreSource = "/rss/channel/dc:source" + case rssChannelDublinCoreLanguage = "/rss/channel/dc:language" + case rssChannelDublinCoreRelation = "/rss/channel/dc:relation" + case rssChannelDublinCoreCoverage = "/rss/channel/dc:coverage" + case rssChannelDublinCoreRights = "/rss/channel/dc:rights" + case rssChannelItemDublinCoreTitle = "/rss/channel/item/dc:title" + case rssChannelItemDublinCoreCreator = "/rss/channel/item/dc:creator" + case rssChannelItemDublinCoreSubject = "/rss/channel/item/dc:subject" + case rssChannelItemDublinCoreDescription = "/rss/channel/item/dc:description" + case rssChannelItemDublinCorePublisher = "/rss/channel/item/dc:publisher" + case rssChannelItemDublinCoreContributor = "/rss/channel/item/dc:contributor" + case rssChannelItemDublinCoreDate = "/rss/channel/item/dc:date" + case rssChannelItemDublinCoreType = "/rss/channel/item/dc:type" + case rssChannelItemDublinCoreFormat = "/rss/channel/item/dc:format" + case rssChannelItemDublinCoreIdentifier = "/rss/channel/item/dc:identifier" + case rssChannelItemDublinCoreSource = "/rss/channel/item/dc:source" + case rssChannelItemDublinCoreLanguage = "/rss/channel/item/dc:language" + case rssChannelItemDublinCoreRelation = "/rss/channel/item/dc:relation" + case rssChannelItemDublinCoreCoverage = "/rss/channel/item/dc:coverage" + case rssChannelItemDublinCoreRights = "/rss/channel/item/dc:rights" + + // iTunes Podcasting Tags + + case rssChannelItunesAuthor = "/rss/channel/itunes:author" + case rssChannelItunesBlock = "/rss/channel/itunes:block" + case rssChannelItunesCategory = "/rss/channel/itunes:category" + case rssChannelItunesSubcategory = "/rss/channel/itunes:category/itunes:category" + case rssChannelItunesImage = "/rss/channel/itunes:image" + case rssChannelItunesExplicit = "/rss/channel/itunes:explicit" + case rssChannelItunesComplete = "/rss/channel/itunes:complete" + case rssChannelItunesNewFeedURL = "/rss/channel/itunes:new-feed-url" + case rssChannelItunesOwner = "/rss/channel/itunes:owner" + case rssChannelItunesOwnerEmail = "/rss/channel/itunes:owner/itunes:email" + case rssChannelItunesOwnerName = "/rss/channel/itunes:owner/itunes:name" + case rssChannelItunesSubtitle = "/rss/channel/itunes:subtitle" + case rssChannelItunesSummary = "/rss/channel/itunes:summary" + case rssChannelItunesKeywords = "/rss/channel/itunes:keywords" + case rssChannelItunesType = "/rss/channel/itunes:type" + + case rssChannelItemItunesAuthor = "/rss/channel/item/itunes:author" + case rssChannelItemItunesBlock = "/rss/channel/item/itunes:block" + case rssChannelItemItunesImage = "/rss/channel/item/itunes:image" + case rssChannelItemItunesDuration = "/rss/channel/item/itunes:duration" + case rssChannelItemItunesExplicit = "/rss/channel/item/itunes:explicit" + case rssChannelItemItunesIsClosedCaptioned = "/rss/channel/item/itunes:isClosedCaptioned" + case rssChannelItemItunesOrder = "/rss/channel/item/itunes:order" + case rssChannelItemItunesSubtitle = "/rss/channel/item/itunes:subtitle" + case rssChannelItemItunesSummary = "/rss/channel/item/itunes:summary" + case rssChannelItemItunesKeywords = "/rss/channel/item/itunes:keywords" + case rssChannelItemItunesEpisodeType = "/rss/channel/item/itunes:episodeType" + case rssChannelItemItunesSeason = "/rss/channel/item/itunes:season" + case rssChannelItemItunesEpisode = "/rss/channel/item/itunes:episode" + + // MARK: Media + + case rssChannelItemMediaThumbnail = "/rss/channel/item/media:thumbnail" + case rssChannelItemMediaContent = "/rss/channel/item/media:content" + case rssChannelItemMediaContentTitle = "/rss/channel/item/media:content/media:title" + case rssChannelItemMediaContentDescription = "/rss/channel/item/media:content/media:description" + case rssChannelItemMediaContentPlayer = "/rss/channel/item/media:content/media:player" + case rssChannelItemMediaContentThumbnail = "/rss/channel/item/media:content/media:thumbnail" + case rssChannelItemMediaCommunity = "/rss/channel/item/media:community" + case rssChannelItemMediaCommunityMediaStarRating = "/rss/channel/item/media:community/media:starRating" + case rssChannelItemMediaCommunityMediaStatistics = "/rss/channel/item/media:community/media:statistics" + case rssChannelItemMediaCommunityMediaTags = "/rss/channel/item/media:community/media:tags" + case rssChannelItemMediaComments = "/rss/channel/item/media:comments" + case rssChannelItemMediaCommentsMediaComment = "/rss/channel/item/media:comments/media:comment" + case rssChannelItemMediaEmbed = "/rss/channel/item/media:embed" + case rssChannelItemMediaEmbedMediaParam = "/rss/channel/item/media:embed/media:param" + case rssChannelItemMediaResponses = "/rss/channel/item/media:responses" + case rssChannelItemMediaResponsesMediaResponse = "/rss/channel/item/media:responses/media:response" + case rssChannelItemMediaBackLinks = "/rss/channel/item/media:backLinks" + case rssChannelItemMediaBackLinksBackLink = "/rss/channel/item/media:backLinks/media:backLink" + case rssChannelItemMediaStatus = "/rss/channel/item/media:status" + case rssChannelItemMediaPrice = "/rss/channel/item/media:price" + case rssChannelItemMediaLicense = "/rss/channel/item/media:license" + case rssChannelItemMediaSubTitle = "/rss/channel/item/media:subTitle" + case rssChannelItemMediaPeerLink = "/rss/channel/item/media:peerLink" + case rssChannelItemMediaLocation = "/rss/channel/item/media:location" + case rssChannelItemMediaLocationPosition = "/rss/channel/item/media:location/georss:where/gml:Point/gml:pos" + case rssChannelItemMediaRestriction = "/rss/channel/item/media:restriction" + case rssChannelItemMediaScenes = "/rss/channel/item/media:scenes" + case rssChannelItemMediaScenesMediaScene = "/rss/channel/item/media:scenes/media:scene" + case rssChannelItemMediaScenesMediaSceneSceneTitle = "/rss/channel/item/media:scenes/media:scene/sceneTitle" + case rssChannelItemMediaScenesMediaSceneSceneDescription = "/rss/channel/item/media:scenes/media:scene/sceneDescription" + case rssChannelItemMediaScenesMediaSceneSceneStartTime = "/rss/channel/item/media:scenes/media:scene/sceneStartTime" + case rssChannelItemMediaScenesMediaSceneSceneEndTime = "/rss/channel/item/media:scenes/media:scene/sceneEndTime" + case rssChannelItemMediaGroup = "/rss/channel/item/media:group" + case rssChannelItemMediaGroupMediaCredit = "/rss/channel/item/media:group/media:credit" + case rssChannelItemMediaGroupMediaCategory = "/rss/channel/item/media:group/media:category" + case rssChannelItemMediaGroupMediaRating = "/rss/channel/item/media:group/media:rating" + case rssChannelItemMediaGroupMediaContent = "/rss/channel/item/media:group/media:content" + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/FeedDataType.swift b/Pods/FeedKit/Sources/FeedKit/Parser/FeedDataType.swift new file mode 100644 index 0000000..5cdcf18 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/FeedDataType.swift @@ -0,0 +1,71 @@ +// +// FeedDataType.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Types of data to determine how to parse a feed. +/// +/// - xml: XML Data Type. +/// - json: JSON Data Type. +enum FeedDataType: String { + case xml + case json +} + +fileprivate let inspectionPrefixLength = 200 + +extension FeedDataType { + + /// A `FeedDataType` from the specified `Data` object + /// + /// - Parameter data: The `Data` object. + init?(data: Data) { + // As a practical matter, the dispositive characters will be found near + // the start of the buffer. It's expensive to convert the entire buffer to + // a string because the conversion is not lazy. So inspect only a prefix + // of the buffer. + let string = String(decoding: data.prefix(inspectionPrefixLength), as: UTF8.self) + let dispositiveCharacters = CharacterSet.alphanumerics + .union(CharacterSet.punctuationCharacters) + .union(CharacterSet.symbols) + for scalar in string.unicodeScalars { + if !dispositiveCharacters.contains(scalar) { // Skip whitespace, BOM marker if present + continue + } + let char = Character(scalar) + switch char { + case "<": + self = .xml + return + case "{": + self = .json + return + default: + return nil + } + } + return nil + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/FeedParser.swift b/Pods/FeedKit/Sources/FeedKit/Parser/FeedParser.swift new file mode 100644 index 0000000..1563887 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/FeedParser.swift @@ -0,0 +1,128 @@ +// +// FeedParser.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation +import Dispatch + +/// An RSS and Atom feed parser. `FeedParser` uses `Foundation`'s `XMLParser`. +public class FeedParser { + + private var data: Data? + private var url: URL? + private var xmlStream: InputStream? + + /// A FeedParser handler provider. + var parser: FeedParserProtocol? + + /// Initializes the parser with the JSON or XML content referenced by the given URL. + /// + /// - Parameter URL: URL whose contents are read to produce the feed data + public init(URL: URL) { + self.url = URL + } + + /// Initializes the parser with the xml or json contents encapsulated in a + /// given data object. + /// + /// - Parameter data: XML or JSON data + public init(data: Data) { + self.data = data + } + + /// Initializes the parser with the XML contents encapsulated in a + /// given InputStream. + /// + /// - Parameter xmlStream: An InputStream that yields XML data. + public init(xmlStream: InputStream) { + self.xmlStream = xmlStream + } + + /// Starts parsing the feed. + /// + /// - Returns: The parsed `Result`. + public func parse() -> Result { + + if let url = url { + // The `Data(contentsOf:)` initializer doesn't handle the `feed` URI scheme. As such, + // it's sanitized first, in case it's in fact a `feed` scheme. + guard let sanitizedSchemeUrl = url.replacing(scheme: "feed", with: "http") else { + return Result.failure(ParserError.internalError(reason: "Failed url sanitizing.").value) + } + + do { + data = try Data(contentsOf: sanitizedSchemeUrl) + } catch { + return Result.failure(error as NSError) + } + } + + if let data = data { + guard let feedDataType = FeedDataType(data: data) else { + return Result.failure(ParserError.feedNotFound.value) + } + switch feedDataType { + case .json: parser = JSONFeedParser(data: data) + case .xml: parser = XMLFeedParser(data: data) + } + return parser!.parse() + } + + if let xmlStream = xmlStream { + parser = XMLFeedParser(stream: xmlStream) + return parser!.parse() + } + + return Result.failure(ParserError.internalError(reason: "Fatal error. Unable to parse from the initialized state.").value) + + } + + /// Starts parsing the feed asynchronously. Parsing runs by default on the + /// global queue. You are responsible to manually bring the result closure + /// to whichever queue is apropriate, if any. + /// + /// Usually to the Main queue if UI Updates are needed. + /// + /// DispatchQueue.main.async { + /// // UI Updates + /// } + /// + /// - Parameters: + /// - queue: The queue on which the completion handler is dispatched. + /// - result: The parsed `Result`. + public func parseAsync( + queue: DispatchQueue = DispatchQueue.global(), + result: @escaping (Result) -> Void) + { + queue.async { + result(self.parse()) + } + } + + /// Stops parsing XML feeds. + public func abortParsing() { + guard let xmlFeedParser = parser as? XMLFeedParser else { return } + xmlFeedParser.xmlParser.abortParsing() + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/FeedParserProtocol.swift b/Pods/FeedKit/Sources/FeedKit/Parser/FeedParserProtocol.swift new file mode 100644 index 0000000..53f32a9 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/FeedParserProtocol.swift @@ -0,0 +1,31 @@ +// +// FeedParserProtocol.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The protocol for Parsing handlers. +protocol FeedParserProtocol { + init(data: Data) + func parse() -> Result +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/JSONFeedParser.swift b/Pods/FeedKit/Sources/FeedKit/Parser/JSONFeedParser.swift new file mode 100644 index 0000000..5827f9a --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/JSONFeedParser.swift @@ -0,0 +1,51 @@ +// +// JSONFeedParser.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The actual engine behind the `FeedKit` framework. `JSONFeedParser` handles +/// the parsing of JSON Feeds. +/// +/// See: https://jsonfeed.org/version/1 +class JSONFeedParser: FeedParserProtocol { + + let data: Data + + required public init(data: Data) { + self.data = data + } + + func parse() -> Result { + do { + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = Date.decodingStrategy + let decoded = try decoder.decode(JSONFeed.self, from: data) + return Result.json(decoded) + } catch { + return Result.failure(NSError(domain: error.localizedDescription, code: -1)) + } + + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/ParserError.swift b/Pods/FeedKit/Sources/FeedKit/Parser/ParserError.swift new file mode 100644 index 0000000..87f7513 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/ParserError.swift @@ -0,0 +1,82 @@ +// +// ParserError.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + + +/// Error types with `NSError` codes and user info providers +/// +/// - feedNotFound: Couldn't parse any known feed. +/// - feedCDATABlockEncodingError: Unable to convert the bytes in `CDATABlock` +/// to Unicode characters using the UTF-8 encoding. +/// - internalError: An internal error from which the user cannot recover. +public enum ParserError { + + case feedNotFound + case feedCDATABlockEncodingError(path: String) + case internalError(reason: String) + + /// An error's code for the specified case. + var code: Int { + switch self { + case .feedNotFound: return -1000 + case .feedCDATABlockEncodingError: return -10001 + case .internalError(_): return -90000 + } + } + + /// The error's userInfo dictionary for the specified case. + var userInfo: [String: String] { + switch self { + case .feedNotFound: + return [ + NSLocalizedDescriptionKey: "Feed not found", + NSLocalizedFailureReasonErrorKey: "Couldn't parse any known feed", + NSLocalizedRecoverySuggestionErrorKey: "Provide a valid Atom/RSS/JSON feed " + ] + + case .feedCDATABlockEncodingError(let path): + return [ + NSLocalizedDescriptionKey: "`CDATAblock` encoding error", + NSLocalizedFailureReasonErrorKey: "Unable to convert the bytes in `CDATABlock` to Unicode characters using the UTF-8 encoding at current path: \(path)", + NSLocalizedRecoverySuggestionErrorKey: "Make sure the encoding provided in a `CDATABlock` is encoded as UTF-8" + ] + + case .internalError(let reason): + return [ + NSLocalizedDescriptionKey: "Internal unresolved error: \(reason)", + NSLocalizedFailureReasonErrorKey: "Unable to recover from an internal unresolved error: \(reason)", + NSLocalizedRecoverySuggestionErrorKey: "If you're seeing this error you probably should open an issue on github" + ] + + } + + } + + /// The `NSError` from the specified case. + var value: NSError { + return NSError(domain:"com.feedkit.error", code: self.code, userInfo: self.userInfo) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/Result.swift b/Pods/FeedKit/Sources/FeedKit/Parser/Result.swift new file mode 100644 index 0000000..6555ab0 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/Result.swift @@ -0,0 +1,92 @@ +// +// Result.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Used to provide the result of a parsed feed, whether the parsing was +/// successfull or encountered an error. +/// +/// - atom: The parsed `AtomFeed` model. +/// - rss: The parsed `RSSFeed` model. +/// - json: The parsed `JSONFeed` model. +/// - failure: The failure `NSError` generated from parsing errors. +public enum Result { + + case atom(AtomFeed) + case rss(RSSFeed) + case json(JSONFeed) + case failure(NSError) + + /// Returns `true` if the result is a success, `false` otherwise. + public var isSuccess: Bool { + switch self { + case .atom: return true + case .rss: return true + case .json: return true + case .failure: return false + } + } + + /// Returns `true` if the result is a failure, `false` otherwise. + public var isFailure: Bool { + return !isSuccess + } + + /// Returns the parsed rss feed value if the result is a success, `nil` + /// otherwise. + public var rssFeed: RSSFeed? { + switch self { + case .rss(let value): return value + default: return nil + } + } + + /// Returns the parsed atom feed if the result is a success, `nil` + /// otherwise. + public var atomFeed: AtomFeed? { + switch self { + case .atom(let value): return value + default: return nil + } + } + + /// Returns the parsed json feed if the result is a success, `nil` + /// otherwise. + public var jsonFeed: JSONFeed? { + switch self { + case .json(let value): return value + default: return nil + } + } + + /// Returns the associated error value if the result is a failure, `nil` + /// otherwise. + public var error: NSError? { + switch self { + case .failure(let error): return error + default: return nil + } + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedParser.swift b/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedParser.swift new file mode 100644 index 0000000..33b48fc --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedParser.swift @@ -0,0 +1,209 @@ +// +// XMLFeedParser.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// The actual engine behind the `FeedKit` framework. `XMLFeedParser` handles +/// the parsing of RSS and Atom feeds. It is an `XMLParserDelegate` of +/// itself. +class XMLFeedParser: NSObject, XMLParserDelegate, FeedParserProtocol { + + /// The Feed Type currently being parsed. The Initial value of this variable + /// is unknown until a recognizable element that matches a feed type is + /// found. + var feedType: XMLFeedType? + + /// The RSS feed model. + var rssFeed: RSSFeed? + + /// The Atom feed model. + var atomFeed: AtomFeed? + + /// The XML Parser. + let xmlParser: XMLParser + + /// An XML Feed Parser, for rss and atom feeds. + /// + /// - Parameter data: A `Data` object containing an XML feed. + required init(data: Data) { + self.xmlParser = XMLParser(data: data) + super.init() + self.xmlParser.delegate = self + } + + /// An XML Feed Parser, for rss and atom feeds. + /// + /// - Parameter stream: An `InputStream` object containing an XML feed. + init(stream: InputStream) { + self.xmlParser = XMLParser(stream: stream) + super.init() + self.xmlParser.delegate = self + } + + /// The current path along the XML's DOM elements. Path components are + /// updated to reflect the current XML element being parsed. + /// e.g. "/rss/channel/title" means it's currently parsing the channels + /// `<title>` element. + fileprivate var currentXMLDOMPath: URL = URL(string: "/")! + + /// A parsing error, if any. + var parsingError: NSError? + var parseComplete = false + + /// Starts parsing the feed. + func parse() -> Result { + let _ = self.xmlParser.parse() + + if let error = parsingError { + return Result.failure(error) + } + + guard let feedType = self.feedType else { + return Result.failure(ParserError.feedNotFound.value) + } + + switch feedType { + case .atom: return Result.atom(self.atomFeed!) + case .rdf, .rss: return Result.rss(self.rssFeed!) + } + + } + + /// Redirects characters found between XML elements to their proper model + /// mappers based on the `currentXMLDOMPath`. + /// + /// - Parameter string: The characters to map. + fileprivate func map(_ string: String) { + guard let feedType = self.feedType else { return } + + switch feedType { + case .atom: + if let path = AtomPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.atomFeed?.map(string, for: path) + } + + case .rdf: + if let path = RDFPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.rssFeed?.map(string, for: path) + } + + case .rss: + if let path = RSSPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.rssFeed?.map(string, for: path) + } + + } + + } + +} + +// MARK: - XMLParser delegate + +extension XMLFeedParser { + + func parser( + _ parser: XMLParser, + didStartElement elementName: String, + namespaceURI: String?, + qualifiedName qName: String?, + attributes attributeDict: [String : String]) + { + + // Update the current path along the XML's DOM elements by appending the new component with `elementName`. + self.currentXMLDOMPath = self.currentXMLDOMPath.appendingPathComponent(elementName) + + // Get the feed type from the element, if it hasn't been done yet. + guard let feedType = self.feedType else { + self.feedType = XMLFeedType(rawValue: elementName) + return + } + + switch feedType { + case .atom: + if self.atomFeed == nil { + self.atomFeed = AtomFeed() + } + if let path = AtomPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.atomFeed?.map(attributeDict, for: path) + } + + case .rdf: + if self.rssFeed == nil { + self.rssFeed = RSSFeed() + } + if let path = RDFPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.rssFeed?.map(attributeDict, for: path) + } + + case .rss: + if self.rssFeed == nil { + self.rssFeed = RSSFeed() + } + if let path = RSSPath(rawValue: self.currentXMLDOMPath.absoluteString) { + self.rssFeed?.map(attributeDict, for: path) + } + + } + + } + + func parser( + _ parser: XMLParser, + didEndElement elementName: String, + namespaceURI: String?, + qualifiedName qName: String?) + { + // Update the current path along the XML's DOM elements by deleting last component. + self.currentXMLDOMPath = self.currentXMLDOMPath.deletingLastPathComponent() + if currentXMLDOMPath.absoluteString == "/" { + parseComplete = true + xmlParser.abortParsing() + } + } + + func parser(_ parser: XMLParser, foundCDATA CDATABlock: Data) + { + guard let string = String(data: CDATABlock, encoding: .utf8) else { + self.xmlParser.abortParsing() + self.parsingError = ParserError.feedCDATABlockEncodingError(path: self.currentXMLDOMPath.absoluteString).value + return + } + self.map(string) + } + + func parser(_ parser: XMLParser, foundCharacters string: String) { + self.map(string) + } + + func parser(_ parser: XMLParser, parseErrorOccurred parseError: Error) { + // Ignore errors that occur after a feed is successfully parsed. Some + // real-world feeds contain junk such as "[]" after the XML segment; + // just ignore this stuff. + guard !parseComplete, parsingError == nil else { return } + self.parsingError = NSError(domain: parseError.localizedDescription, code: -1, + userInfo: ["CurrentPath": currentXMLDOMPath.absoluteString]) + } + +} diff --git a/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedType.swift b/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedType.swift new file mode 100644 index 0000000..9eb0bc0 --- /dev/null +++ b/Pods/FeedKit/Sources/FeedKit/Parser/XMLFeedType.swift @@ -0,0 +1,36 @@ +// +// XMLFeedType.swift +// +// Copyright (c) 2016 - 2018 Nuno Manuel Dias +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +import Foundation + +/// Types of feed. The `rawValue` matches the top-level XML element of a feed. +/// +/// - atom: The `Atom Syndication Format feed type. +/// - rdf: The Really Simple Syndication feed type version 0.90. +/// - rss: The Really Simple Syndication feed type version 2.0. +enum XMLFeedType: String { + case atom = "feed" + case rdf = "rdf:RDF" + case rss = "rss" +} diff --git a/Pods/Kingfisher/LICENSE b/Pods/Kingfisher/LICENSE new file mode 100644 index 0000000..5023261 --- /dev/null +++ b/Pods/Kingfisher/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2018 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Pods/Kingfisher/README.md b/Pods/Kingfisher/README.md new file mode 100644 index 0000000..33c71b5 --- /dev/null +++ b/Pods/Kingfisher/README.md @@ -0,0 +1,157 @@ +<p align="center"> +<img src="https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png" alt="Kingfisher" title="Kingfisher" width="557"/> +</p> + +<p align="center"> +<a href="https://travis-ci.org/onevcat/Kingfisher"><img src="https://img.shields.io/travis/onevcat/Kingfisher/master.svg"></a> +<a href="https://github.com/Carthage/Carthage/"><img src="https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat"></a> +<a href="https://github.com/JamitLabs/Accio"><img src="https://img.shields.io/badge/Accio-supported-0A7CF5.svg?style=flat"></a> +<a href="http://onevcat.github.io/Kingfisher/"><img src="https://img.shields.io/cocoapods/v/Kingfisher.svg?style=flat"></a> +<a href="https://raw.githubusercontent.com/onevcat/Kingfisher/master/LICENSE"><img src="https://img.shields.io/cocoapods/l/Kingfisher.svg?style=flat"></a> +<a href="http://onevcat.github.io/Kingfisher/"><img src="https://img.shields.io/cocoapods/p/Kingfisher.svg?style=flat"></a> +<a href="https://codebeat.co/projects/github-com-onevcat-kingfisher"><img alt="codebeat badge" src="https://codebeat.co/assets/svg/badges/A-398b39-669406e9e1b136187b91af587d4092b0160370f271f66a651f444b990c2730e9.svg" /></a> +<br /> +<a href="#backers" alt="sponsors on Open Collective"><img src="https://opencollective.com/Kingfisher/backers/badge.svg" /></a> +<a href="#sponsors" alt="Sponsors on Open Collective"><img src="https://opencollective.com/Kingfisher/sponsors/badge.svg" /></a> +</p> + +Kingfisher is a powerful, pure-Swift library for downloading and caching images from the web. It provides you a chance to use a pure-Swift way to work with remote images in your next app. + +## Features + +- [x] Asynchronous image downloading and caching. +- [x] Loading image from either `URLSession`-based networking or local provided data. +- [x] Useful image processors and filters provided. +- [x] Multiple-layer hybrid cache for both memory and disk. +- [x] Fine control on cache behavior. Customizable expiration date and size limit. +- [x] Cancelable downloading and auto-reusing previous downloaded content to improve performance. +- [x] Independent components. Use the downloader, caching system and image processors separately as you need. +- [x] Prefetching images and showing them from cache to boost your app. +- [x] View extensions for `UIImageView`, `NSImageView`, `NSButton` and `UIButton` to directly set an image from a URL. +- [x] Built-in transition animation when setting images. +- [x] Customizable placeholder and indicator while loading images. +- [x] Extensible image processing and image format easily. + +### Kingfisher 101 + +The simplest use-case is setting an image to an image view with the `UIImageView` extension: + +```swift +let url = URL(string: "https://example.com/image.png") +imageView.kf.setImage(with: url) +``` + +Kingfisher will download the image from `url`, send it to both memory cache and disk cache, and display it in `imageView`. When you set with the same URL later, the image will be retrieved from cache and shown immediately. + +### A More Advanced Example + +With the powerful options, you can do hard tasks with Kingfisher in a simple way. For example, the code below: + +1. Downloads a high-resolution image. +2. Downsamples it to match the image view size. +3. Makes it round cornered with a given radius. +4. Shows a system indicator and a placeholder image while downloading. +5. When prepared, it animates the small thumbnail image with a "fade in" effect. +6. The original large image is also cached to disk for later use, to get rid of downloading it again in a detail view. +7. A console log is printed when the task finishes, either for success or failure. + +```swift +let url = URL(string: "https://example.com/high_resolution_image.png") +let processor = DownsamplingImageProcessor(size: imageView.size) + >> RoundCornerImageProcessor(cornerRadius: 20) +imageView.kf.indicatorType = .activity +imageView.kf.setImage( + with: url, + placeholder: UIImage(named: "placeholderImage"), + options: [ + .processor(processor), + .scaleFactor(UIScreen.main.scale), + .transition(.fade(1)), + .cacheOriginalImage + ]) +{ + result in + switch result { + case .success(let value): + print("Task done for: \(value.source.url?.absoluteString ?? "")") + case .failure(let error): + print("Job failed: \(error.localizedDescription)") + } +} +``` + +It is really a very common situation I can meet in my daily work. Think about how many lines you need to write without Kingfisher. You will fall in love with it if you give it a try! + +### Learn More + +To learn the using of Kingfisher by more examples, take a look at the [Cheat Sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet). There we summarized most common tasks in Kingfisher, you can get a better idea on what this framework can do. There are also some tips for performance in the same page, remember to check them too. + +## Requirements + +- iOS 10.0+ / macOS 10.12+ / tvOS 10.0+ / watchOS 3.0+ +- Swift 4.0+ + +[Kingfisher 5.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-5.0-Migration-Guide) - Kingfisher 5.x is NOT fully compatible with version 4.x. However, the migration is not difficult. Depending on your use cases, it may take no effect or several minutes to modify your existing code for the new version. Please follow the [migration guide](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-5.0-Migration-Guide) when you prepare to upgrade Kingfisher in your project. + +If you are using an even earlier version, see the guides below to know the steps for migrating. + +> - Kingfisher 4.0 Migration - Kingfisher 3.x should be source compatible to Kingfisher 4. The reason for a major update is that we need to specify the Swift version explicitly for Xcode. All deprecated methods in Kingfisher 3 has been removed, so please ensure you have no warning left before you migrate from Kingfisher 3 to Kingfisher 4. If you have any trouble in migrating, please open an issue to discuss. +> - [Kingfisher 3.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-3.0-Migration-Guide) - If you are upgrading to Kingfisher 3.x from an earlier version, please read this for more information. + +## Next Steps + +We prepared a [wiki page](https://github.com/onevcat/Kingfisher/wiki). You can find tons of useful things there. + +* [Installation Guide](https://github.com/onevcat/Kingfisher/wiki/Installation-Guide) - Follow it to integrate Kingfisher into your project. +* [Cheat Sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet)- Curious about what Kingfisher could do and how would it look like when used in your project? See this page for useful code snippets. If you are already familiar with Kingfisher, you could also learn new tricks to improve the way you use Kingfisher! +* [API Reference](http://onevcat.github.io/Kingfisher/) - Lastly, please remember to read the full whenever you may need a more detailed reference. + +## Other + +### Future of Kingfisher + +I want to keep Kingfisher lightweight. This framework will focus on providing a simple solution for downloading and caching images. This doesn’t mean the framework can’t be improved. Kingfisher is far from perfect, so necessary and useful updates will be made to make it better. + +### Developments and Tests + +Any contributing and pull requests are warmly welcome. However, before you plan to implement some features or try to fix an uncertain issue, it is recommended to open a discussion first. It would be appreciated if your pull requests could build and with all tests green. :) + +### About the logo + +The logo of Kingfisher is inspired by [Tangram (七巧板)](http://en.wikipedia.org/wiki/Tangram), a dissection puzzle consisting of seven flat shapes from China. I believe she's a kingfisher bird instead of a swift, but someone insists that she is a pigeon. I guess I should give her a name. Hi, guys, do you have any suggestions? + +### Contact + +Follow and contact me on [Twitter](http://twitter.com/onevcat) or [Sina Weibo](http://weibo.com/onevcat). If you find an issue, just [open a ticket](https://github.com/onevcat/Kingfisher/issues/new). Pull requests are warmly welcome as well. + +## Contributors + +This project exists thanks to all the people who contribute. [[Contribute]](https://github.com/onevcat/Kingfisher/blob/master/CONTRIBUTING.md). +<a href="https://opencollective.com/kingfisher#backer"><img src="https://opencollective.com/kingfisher/contributors.svg?width=890" /></a> + + +## Backers + +Thank you to all our backers! Your support is really important for the project and encourages us to continue. 🙏 [[Become a backer](https://opencollective.com/kingfisher#backer)] + +<a href="https://opencollective.com/kingfisher#backers" target="_blank"><img src="https://opencollective.com/kingfisher/backers.svg?width=890"></a> + + +## Sponsors + +Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/kingfisher#sponsor)] + +<a href="https://opencollective.com/kingfisher/sponsor/0/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/0/avatar.svg"></a> +<a href="https://opencollective.com/kingfisher/sponsor/1/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/1/avatar.svg"></a> +<a href="https://opencollective.com/kingfisher/sponsor/2/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/2/avatar.svg"></a> +<a href="https://opencollective.com/kingfisher/sponsor/3/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/3/avatar.svg"></a> +<a href="https://opencollective.com/kingfisher/sponsor/4/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/4/avatar.svg"></a> +<a href="https://opencollective.com/kingfisher/sponsor/5/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/5/avatar.svg"></a> +<a href="https://opencollective.com/kingfisher/sponsor/6/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/6/avatar.svg"></a> +<a href="https://opencollective.com/kingfisher/sponsor/7/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/7/avatar.svg"></a> +<a href="https://opencollective.com/kingfisher/sponsor/8/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/8/avatar.svg"></a> +<a href="https://opencollective.com/kingfisher/sponsor/9/website" target="_blank"><img src="https://opencollective.com/kingfisher/sponsor/9/avatar.svg"></a> + +### License + +Kingfisher is released under the MIT license. See LICENSE for details. diff --git a/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift b/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift new file mode 100644 index 0000000..ea72c72 --- /dev/null +++ b/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift @@ -0,0 +1,113 @@ +// +// CacheSerializer.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/02. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// An `CacheSerializer` is used to convert some data to an image object after +/// retrieving it from disk storage, and vice versa, to convert an image to data object +/// for storing to the disk storage. +public protocol CacheSerializer { + + /// Gets the serialized data from a provided image + /// and optional original data for caching to disk. + /// + /// - Parameters: + /// - image: The image needed to be serialized. + /// - original: The original data which is just downloaded. + /// If the image is retrieved from cache instead of + /// downloaded, it will be `nil`. + /// - Returns: The data object for storing to disk, or `nil` when no valid + /// data could be serialized. + func data(with image: Image, original: Data?) -> Data? + + /// Gets an image from provided serialized data. + /// + /// - Parameters: + /// - data: The data from which an image should be deserialized. + /// - options: The parsed options for deserialization. + /// - Returns: An image deserialized or `nil` when no valid image + /// could be deserialized. + func image(with data: Data, options: KingfisherParsedOptionsInfo) -> Image? + + /// Gets an image deserialized from provided data. + /// + /// - Parameters: + /// - data: The data from which an image should be deserialized. + /// - options: Options for deserialization. + /// - Returns: An image deserialized or `nil` when no valid image + /// could be deserialized. + /// - Note: + /// This method is deprecated. Please implement the version with + /// `KingfisherParsedOptionsInfo` as parameter instead. + @available(*, deprecated, + message: "Deprecated. Implement the method with same name but with `KingfisherParsedOptionsInfo` instead.") + func image(with data: Data, options: KingfisherOptionsInfo?) -> Image? +} + +extension CacheSerializer { + public func image(with data: Data, options: KingfisherOptionsInfo?) -> Image? { + return image(with: data, options: KingfisherParsedOptionsInfo(options)) + } +} + +/// Represents a basic and default `CacheSerializer` used in Kingfisher disk cache system. +/// It could serialize and deserialize images in PNG, JPEG and GIF format. For +/// image other than these formats, a normalized `pngRepresentation` will be used. +public struct DefaultCacheSerializer: CacheSerializer { + + /// The default general cache serializer used across Kingfisher's cache. + public static let `default` = DefaultCacheSerializer() + private init() {} + + /// - Parameters: + /// - image: The image needed to be serialized. + /// - original: The original data which is just downloaded. + /// If the image is retrieved from cache instead of + /// downloaded, it will be `nil`. + /// - Returns: The data object for storing to disk, or `nil` when no valid + /// data could be serialized. + /// + /// - Note: + /// Only when `original` contains valid PNG, JPEG and GIF format data, the `image` will be + /// converted to the corresponding data type. Otherwise, if the `original` is provided but it is not + /// a valid format, the `original` data will be used for cache. + /// + /// If `original` is `nil`, the input `image` will be encoded as PNG data. + public func data(with image: Image, original: Data?) -> Data? { + return image.kf.data(format: original?.kf.imageFormat ?? .unknown) + } + + /// Gets an image deserialized from provided data. + /// + /// - Parameters: + /// - data: The data from which an image should be deserialized. + /// - options: Options for deserialization. + /// - Returns: An image deserialized or `nil` when no valid image + /// could be deserialized. + public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> Image? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/Pods/Kingfisher/Sources/Cache/DiskStorage.swift b/Pods/Kingfisher/Sources/Cache/DiskStorage.swift new file mode 100644 index 0000000..d292d87 --- /dev/null +++ b/Pods/Kingfisher/Sources/Cache/DiskStorage.swift @@ -0,0 +1,425 @@ +// +// DiskStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + + +/// Represents a set of conception related to storage which stores a certain type of value in disk. +/// This is a namespace for the disk storage types. A `Backend` with a certain `Config` will be used to describe the +/// storage. See these composed types for more information. +public enum DiskStorage { + + /// Represents a storage back-end for the `DiskStorage`. The value is serialized to data + /// and stored as file in the file system under a specified location. + /// + /// You can config a `DiskStorage.Backend` in its initializer by passing a `DiskStorage.Config` value. + /// or modifying the `config` property after it being created. `DiskStorage` will use file's attributes to keep + /// track of a file for its expiration or size limitation. + public class Backend<T: DataTransformable> { + /// The config used for this disk storage. + public var config: Config + + // The final storage URL on disk, with `name` and `cachePathBlock` considered. + public let directoryURL: URL + + let metaChangingQueue: DispatchQueue + + /// Creates a disk storage with the given `DiskStorage.Config`. + /// + /// - Parameter config: The config used for this disk storage. + /// - Throws: An error if the folder for storage cannot be got or created. + public init(config: Config) throws { + + self.config = config + + let url: URL + if let directory = config.directory { + url = directory + } else { + url = try config.fileManager.url( + for: .cachesDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: true) + } + + let cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)" + directoryURL = config.cachePathBlock(url, cacheName) + + metaChangingQueue = DispatchQueue(label: cacheName) + + try prepareDirectory() + } + + // Creates the storage folder. + func prepareDirectory() throws { + let fileManager = config.fileManager + let path = directoryURL.path + + guard !fileManager.fileExists(atPath: path) else { return } + + do { + try fileManager.createDirectory( + atPath: path, + withIntermediateDirectories: true, + attributes: nil) + } catch { + throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error)) + } + } + + func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) throws + { + let expiration = expiration ?? config.expiration + // The expiration indicates that already expired, no need to store. + guard !expiration.isExpired else { return } + + let data: Data + do { + data = try value.toData() + } catch { + throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error)) + } + + let fileURL = cacheFileURL(forKey: key) + + let now = Date() + let attributes: [FileAttributeKey : Any] = [ + // The last access date. + .creationDate: now.fileAttributeDate, + // The estimated expiration date. + .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate + ] + config.fileManager.createFile(atPath: fileURL.path, contents: data, attributes: attributes) + } + + func value(forKey key: String) throws -> T? { + return try value(forKey: key, referenceDate: Date(), actuallyLoad: true) + } + + func value(forKey key: String, referenceDate: Date, actuallyLoad: Bool) throws -> T? { + let fileManager = config.fileManager + let fileURL = cacheFileURL(forKey: key) + let filePath = fileURL.path + guard fileManager.fileExists(atPath: filePath) else { + return nil + } + + let meta: FileMeta + do { + let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey] + meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys) + } catch { + throw KingfisherError.cacheError( + reason: .invalidURLResource(error: error, key: key, url: fileURL)) + } + + if meta.expired(referenceDate: referenceDate) { + return nil + } + if !actuallyLoad { return T.empty } + + do { + let data = try Data(contentsOf: fileURL) + let obj = try T.fromData(data) + metaChangingQueue.async { meta.extendExpiration(with: fileManager) } + return obj + } catch { + throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error)) + } + } + + func isCached(forKey key: String) -> Bool { + return isCached(forKey: key, referenceDate: Date()) + } + + func isCached(forKey key: String, referenceDate: Date) -> Bool { + do { + guard let _ = try value(forKey: key, referenceDate: referenceDate, actuallyLoad: false) else { + return false + } + return true + } catch { + return false + } + } + + func remove(forKey key: String) throws { + let fileURL = cacheFileURL(forKey: key) + try removeFile(at: fileURL) + } + + func removeFile(at url: URL) throws { + try config.fileManager.removeItem(at: url) + } + + func removeAll() throws { + try removeAll(skipCreatingDirectory: false) + } + + func removeAll(skipCreatingDirectory: Bool) throws { + try config.fileManager.removeItem(at: directoryURL) + if !skipCreatingDirectory { + try prepareDirectory() + } + } + + /// The URL of the cached file with a given computed `key`. + /// + /// - Note: + /// This method does not guarantee there is an image already cached in the returned URL. It just gives your + /// the URL that the image should be if it exists in disk storage, with the give key. + /// + /// - Parameter key: The final computed key used when caching the image. Please note that usually this is not + /// the `cacheKey` of an image `Source`. It is the computed key with processor identifier considered. + public func cacheFileURL(forKey key: String) -> URL { + let fileName = cacheFileName(forKey: key) + return directoryURL.appendingPathComponent(fileName) + } + + func cacheFileName(forKey key: String) -> String { + if config.usesHashedFileName { + let hashedKey = key.kf.md5 + if let ext = config.pathExtension { + return "\(hashedKey).\(ext)" + } + return hashedKey + } else { + if let ext = config.pathExtension { + return "\(key).\(ext)" + } + return key + } + } + + func allFileURLs(for propertyKeys: [URLResourceKey]) throws -> [URL] { + let fileManager = config.fileManager + + guard let directoryEnumerator = fileManager.enumerator( + at: directoryURL, includingPropertiesForKeys: propertyKeys, options: .skipsHiddenFiles) else + { + throw KingfisherError.cacheError(reason: .fileEnumeratorCreationFailed(url: directoryURL)) + } + + guard let urls = directoryEnumerator.allObjects as? [URL] else { + throw KingfisherError.cacheError(reason: .invalidFileEnumeratorContent(url: directoryURL)) + } + return urls + } + + func removeExpiredValues(referenceDate: Date = Date()) throws -> [URL] { + let propertyKeys: [URLResourceKey] = [ + .isDirectoryKey, + .contentModificationDateKey + ] + + let urls = try allFileURLs(for: propertyKeys) + let keys = Set(propertyKeys) + let expiredFiles = urls.filter { fileURL in + do { + let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) + if meta.isDirectory { + return false + } + return meta.expired(referenceDate: referenceDate) + } catch { + return true + } + } + try expiredFiles.forEach { url in + try removeFile(at: url) + } + return expiredFiles + } + + func removeSizeExceededValues() throws -> [URL] { + + if config.sizeLimit == 0 { return [] } // Back compatible. 0 means no limit. + + var size = try totalSize() + if size < config.sizeLimit { return [] } + + let propertyKeys: [URLResourceKey] = [ + .isDirectoryKey, + .creationDateKey, + .fileSizeKey + ] + let keys = Set(propertyKeys) + + let urls = try allFileURLs(for: propertyKeys) + var pendings: [FileMeta] = urls.compactMap { fileURL in + guard let meta = try? FileMeta(fileURL: fileURL, resourceKeys: keys) else { + return nil + } + return meta + } + // Sort by last access date. Most recent file first. + pendings.sort(by: FileMeta.lastAccessDate) + + var removed: [URL] = [] + let target = config.sizeLimit / 2 + while size > target, let meta = pendings.popLast() { + size -= UInt(meta.fileSize) + try removeFile(at: meta.url) + removed.append(meta.url) + } + return removed + } + + /// Get the total file size of the folder in bytes. + func totalSize() throws -> UInt { + let propertyKeys: [URLResourceKey] = [.fileSizeKey] + let urls = try allFileURLs(for: propertyKeys) + let keys = Set(propertyKeys) + let totalSize: UInt = urls.reduce(0) { size, fileURL in + do { + let meta = try FileMeta(fileURL: fileURL, resourceKeys: keys) + return size + UInt(meta.fileSize) + } catch { + return size + } + } + return totalSize + } + } +} + +extension DiskStorage { + /// Represents the config used in a `DiskStorage`. + public struct Config { + + /// The file size limit on disk of the storage in bytes. 0 means no limit. + public var sizeLimit: UInt + + /// The `StorageExpiration` used in this disk storage. Default is `.days(7)`, + /// means that the disk cache would expire in one week. + public var expiration: StorageExpiration = .days(7) + + /// The preferred extension of cache item. It will be appended to the file name as its extension. + /// Default is `nil`, means that the cache file does not contain a file extension. + public var pathExtension: String? = nil + + /// Default is `true`, means that the cache file name will be hashed before storing. + public var usesHashedFileName = true + + let name: String + let fileManager: FileManager + let directory: URL? + + var cachePathBlock: ((_ directory: URL, _ cacheName: String) -> URL)! = { + (directory, cacheName) in + return directory.appendingPathComponent(cacheName, isDirectory: true) + } + + /// Creates a config value based on given parameters. + /// + /// - Parameters: + /// - name: The name of cache. It is used as a part of storage folder. It is used to identify the disk + /// storage. Two storages with the same `name` would share the same folder in disk, and it should + /// be prevented. + /// - sizeLimit: The size limit in bytes for all existing files in the disk storage. + /// - fileManager: The `FileManager` used to manipulate files on disk. Default is `FileManager.default`. + /// - directory: The URL where the disk storage should live. The storage will use this as the root folder, + /// and append a path which is constructed by input `name`. Default is `nil`, indicates that + /// the cache directory under user domain mask will be used. + public init( + name: String, + sizeLimit: UInt, + fileManager: FileManager = .default, + directory: URL? = nil) + { + self.name = name + self.fileManager = fileManager + self.directory = directory + self.sizeLimit = sizeLimit + } + } +} + +extension DiskStorage { + struct FileMeta { + + let url: URL + + let lastAccessDate: Date? + let estimatedExpirationDate: Date? + let isDirectory: Bool + let fileSize: Int + + static func lastAccessDate(lhs: FileMeta, rhs: FileMeta) -> Bool { + return lhs.lastAccessDate ?? .distantPast > rhs.lastAccessDate ?? .distantPast + } + + init(fileURL: URL, resourceKeys: Set<URLResourceKey>) throws { + let meta = try fileURL.resourceValues(forKeys: resourceKeys) + self.init( + fileURL: fileURL, + lastAccessDate: meta.creationDate, + estimatedExpirationDate: meta.contentModificationDate, + isDirectory: meta.isDirectory ?? false, + fileSize: meta.fileSize ?? 0) + } + + init( + fileURL: URL, + lastAccessDate: Date?, + estimatedExpirationDate: Date?, + isDirectory: Bool, + fileSize: Int) + { + self.url = fileURL + self.lastAccessDate = lastAccessDate + self.estimatedExpirationDate = estimatedExpirationDate + self.isDirectory = isDirectory + self.fileSize = fileSize + } + + func expired(referenceDate: Date) -> Bool { + return estimatedExpirationDate?.isPast(referenceDate: referenceDate) ?? true + } + + func extendExpiration(with fileManager: FileManager) { + guard let lastAccessDate = lastAccessDate, + let lastEstimatedExpiration = estimatedExpirationDate else + { + return + } + + let originalExpiration: StorageExpiration = + .seconds(lastEstimatedExpiration.timeIntervalSince(lastAccessDate)) + let attributes: [FileAttributeKey : Any] = [ + .creationDate: Date().fileAttributeDate, + .modificationDate: originalExpiration.estimatedExpirationSinceNow.fileAttributeDate + ] + + try? fileManager.setAttributes(attributes, ofItemAtPath: url.path) + } + } +} + diff --git a/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift b/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift new file mode 100644 index 0000000..442b82c --- /dev/null +++ b/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift @@ -0,0 +1,102 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Junyu Kuang on 5/28/17. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// `FormatIndicatedCacheSerializer` lets you indicate an image format for serialized caches. +/// +/// It could serialize and deserialize PNG, JPEG and GIF images. For +/// image other than these formats, a normalized `pngRepresentation` will be used. +/// +/// Example: +/// ```` +/// let profileImageSize = CGSize(width: 44, height: 44) +/// +/// // A round corner image. +/// let imageProcessor = RoundCornerImageProcessor( +/// cornerRadius: profileImageSize.width / 2, targetSize: profileImageSize) +/// +/// let optionsInfo: KingfisherOptionsInfo = [ +/// .cacheSerializer(FormatIndicatedCacheSerializer.png), +/// .processor(imageProcessor)] +/// +/// A URL pointing to a JPEG image. +/// let url = URL(string: "https://example.com/image.jpg")! +/// +/// // Image will be always cached as PNG format to preserve alpha channel for round rectangle. +/// // So when you load it from cache again later, it will be still round cornered. +/// // Otherwise, the corner part would be filled by white color (since JPEG does not contain an alpha channel). +/// imageView.kf.setImage(with: url, options: optionsInfo) +/// ```` +public struct FormatIndicatedCacheSerializer: CacheSerializer { + + /// A `FormatIndicatedCacheSerializer` which converts image from and to PNG format. If the image cannot be + /// represented by PNG format, it will fallback to its real format which is determined by `original` data. + public static let png = FormatIndicatedCacheSerializer(imageFormat: .PNG) + + /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format. If the image cannot be + /// represented by JPEG format, it will fallback to its real format which is determined by `original` data. + public static let jpeg = FormatIndicatedCacheSerializer(imageFormat: .JPEG) + + /// A `FormatIndicatedCacheSerializer` which converts image from and to GIF format. If the image cannot be + /// represented by GIF format, it will fallback to its real format which is determined by `original` data. + public static let gif = FormatIndicatedCacheSerializer(imageFormat: .GIF) + + /// The indicated image format. + private let imageFormat: ImageFormat + + /// Creates data which represents the given `image` under a format. + public func data(with image: Image, original: Data?) -> Data? { + + func imageData(withFormat imageFormat: ImageFormat) -> Data? { + switch imageFormat { + case .PNG: return image.kf.pngRepresentation() + case .JPEG: return image.kf.jpegRepresentation(compressionQuality: 1.0) + case .GIF: return image.kf.gifRepresentation() + case .unknown: return nil + } + } + + // generate data with indicated image format + if let data = imageData(withFormat: imageFormat) { + return data + } + + let originalFormat = original?.kf.imageFormat ?? .unknown + + // generate data with original image's format + if originalFormat != imageFormat, let data = imageData(withFormat: originalFormat) { + return data + } + + return original ?? image.kf.normalized.kf.pngRepresentation() + } + + /// Same implementation as `DefaultCacheSerializer`. + public func image(with data: Data, options: KingfisherParsedOptionsInfo) -> Image? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/Pods/Kingfisher/Sources/Cache/ImageCache.swift b/Pods/Kingfisher/Sources/Cache/ImageCache.swift new file mode 100644 index 0000000..66a1874 --- /dev/null +++ b/Pods/Kingfisher/Sources/Cache/ImageCache.swift @@ -0,0 +1,839 @@ +// +// ImageCache.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension Notification.Name { + /// This notification will be sent when the disk cache got cleaned either there are cached files expired or the + /// total size exceeding the max allowed size. The manually invoking of `clearDiskCache` method will not trigger + /// this notification. + /// + /// The `object` of this notification is the `ImageCache` object which sends the notification. + /// A list of removed hashes (files) could be retrieved by accessing the array under + /// `KingfisherDiskCacheCleanedHashKey` key in `userInfo` of the notification object you received. + /// By checking the array, you could know the hash codes of files are removed. + public static let KingfisherDidCleanDiskCache = + Notification.Name("com.onevcat.Kingfisher.KingfisherDidCleanDiskCache") +} + +/// Key for array of cleaned hashes in `userInfo` of `KingfisherDidCleanDiskCacheNotification`. +public let KingfisherDiskCacheCleanedHashKey = "com.onevcat.Kingfisher.cleanedHash" + +/// Cache type of a cached image. +/// - none: The image is not cached yet when retrieving it. +/// - memory: The image is cached in memory. +/// - disk: The image is cached in disk. +public enum CacheType { + /// The image is not cached yet when retrieving it. + case none + /// The image is cached in memory. + case memory + /// The image is cached in disk. + case disk + + /// Whether the cache type represents the image is already cached or not. + public var cached: Bool { + switch self { + case .memory, .disk: return true + case .none: return false + } + } +} + +/// Represents the caching operation result. +public struct CacheStoreResult { + + /// The cache result for memory cache. Caching an image to memory will never fail. + public let memoryCacheResult: Result<(), Never> + + /// The cache result for disk cache. If an error happens during caching operation, + /// you can get it from `.failure` case of this `diskCacheResult`. + public let diskCacheResult: Result<(), KingfisherError> +} + +extension Image: CacheCostCalculable { + /// Cost of an image + public var cacheCost: Int { return kf.cost } +} + +extension Data: DataTransformable { + public func toData() throws -> Data { + return self + } + + public static func fromData(_ data: Data) throws -> Data { + return data + } + + public static let empty = Data() +} + + +/// Represents the getting image operation from the cache. +/// +/// - disk: The image can be retrieved from disk cache. +/// - memory: The image can be retrieved memory cache. +/// - none: The image does not exist in the cache. +public enum ImageCacheResult { + + /// The image can be retrieved from disk cache. + case disk(Image) + + /// The image can be retrieved memory cache. + case memory(Image) + + /// The image does not exist in the cache. + case none + + /// Extracts the image from cache result. It returns the associated `Image` value for + /// `.disk` and `.memory` case. For `.none` case, `nil` is returned. + public var image: Image? { + switch self { + case .disk(let image): return image + case .memory(let image): return image + case .none: return nil + } + } + + /// Returns the corresponding `CacheType` value based on the result type of `self`. + public var cacheType: CacheType { + switch self { + case .disk: return .disk + case .memory: return .memory + case .none: return .none + } + } +} + +/// Represents a hybrid caching system which is composed by a `MemoryStorage.Backend` and a `DiskStorage.Backend`. +/// `ImageCache` is a high level abstract for storing an image as well as its data to disk memory and disk, and +/// retrieving them back. +/// +/// While a default image cache object will be used if you prefer the extension methods of Kingfisher, you can create +/// your own cache object and configure its storages as your need. This class also provide an interface for you to set +/// the memory and disk storage config. +open class ImageCache { + + // MARK: Singleton + /// The default `ImageCache` object. Kingfisher will use this cache for its related methods if there is no + /// other cache specified. The `name` of this default cache is "default", and you should not use this name + /// for any of your customize cache. + public static let `default` = ImageCache(name: "default") + + // MARK: Public Properties + /// The `MemoryStorage.Backend` object used in this cache. This storage holds loaded images in memory with a + /// reasonable expire duration and a maximum memory usage. To modify the configuration of a storage, just set + /// the storage `config` and its properties. + public let memoryStorage: MemoryStorage.Backend<Image> + + /// The `DiskStorage.Backend` object used in this cache. This storage stores loaded images in disk with a + /// reasonable expire duration and a maximum disk usage. To modify the configuration of a storage, just set + /// the storage `config` and its properties. + public let diskStorage: DiskStorage.Backend<Data> + + private let ioQueue: DispatchQueue + + /// Closure that defines the disk cache path from a given path and cacheName. + public typealias DiskCachePathClosure = (URL, String) -> URL + + // MARK: Initializers + + /// Creates an `ImageCache` from a customized `MemoryStorage` and `DiskStorage`. + /// + /// - Parameters: + /// - memoryStorage: The `MemoryStorage.Backend` object to use in the image cache. + /// - diskStorage: The `DiskStorage.Backend` object to use in the image cache. + public init( + memoryStorage: MemoryStorage.Backend<Image>, + diskStorage: DiskStorage.Backend<Data>) + { + self.memoryStorage = memoryStorage + self.diskStorage = diskStorage + let ioQueueName = "com.onevcat.Kingfisher.ImageCache.ioQueue.\(UUID().uuidString)" + ioQueue = DispatchQueue(label: ioQueueName) + + let notifications: [(Notification.Name, Selector)] + #if !os(macOS) && !os(watchOS) + #if swift(>=4.2) + notifications = [ + (UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)), + (UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)), + (UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache)) + ] + #else + notifications = [ + (NSNotification.Name.UIApplicationDidReceiveMemoryWarning, #selector(clearMemoryCache)), + (NSNotification.Name.UIApplicationWillTerminate, #selector(cleanExpiredDiskCache)), + (NSNotification.Name.UIApplicationDidEnterBackground, #selector(backgroundCleanExpiredDiskCache)) + ] + #endif + #elseif os(macOS) + notifications = [ + (NSApplication.willResignActiveNotification, #selector(cleanExpiredDiskCache)), + ] + #else + notifications = [] + #endif + notifications.forEach { + NotificationCenter.default.addObserver(self, selector: $0.1, name: $0.0, object: nil) + } + } + + /// Creates an `ImageCache` with a given `name`. Both `MemoryStorage` and `DiskStorage` will be created + /// with a default config based on the `name`. + /// + /// - Parameter name: The name of cache object. It is used to setup disk cache directories and IO queue. + /// You should not use the same `name` for different caches, otherwise, the disk storage would + /// be conflicting to each other. The `name` should not be an empty string. + public convenience init(name: String) { + try! self.init(name: name, cacheDirectoryURL: nil, diskCachePathClosure: nil) + } + + /// Creates an `ImageCache` with a given `name`, cache directory `path` + /// and a closure to modify the cache directory. + /// + /// - Parameters: + /// - name: The name of cache object. It is used to setup disk cache directories and IO queue. + /// You should not use the same `name` for different caches, otherwise, the disk storage would + /// be conflicting to each other. + /// - cacheDirectoryURL: Location of cache directory URL on disk. It will be internally pass to the + /// initializer of `DiskStorage` as the disk cache directory. If `nil`, the cache + /// directory under user domain mask will be used. + /// - diskCachePathClosure: Closure that takes in an optional initial path string and generates + /// the final disk cache path. You could use it to fully customize your cache path. + /// - Throws: An error that happens during image cache creating, such as unable to create a directory at the given + /// path. + public convenience init( + name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? = nil) throws + { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") + } + + let totalMemory = ProcessInfo.processInfo.physicalMemory + let costLimit = totalMemory / 4 + let memoryStorage = MemoryStorage.Backend<Image>(config: + .init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit))) + + var diskConfig = DiskStorage.Config( + name: name, + sizeLimit: 0, + directory: cacheDirectoryURL + ) + if let closure = diskCachePathClosure { + diskConfig.cachePathBlock = closure + } + let diskStorage = try DiskStorage.Backend<Data>(config: diskConfig) + diskConfig.cachePathBlock = nil + + self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Storing Images + + open func store(_ image: Image, + original: Data? = nil, + forKey key: String, + options: KingfisherParsedOptionsInfo, + toDisk: Bool = true, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + let identifier = options.processor.identifier + let callbackQueue = options.callbackQueue + + let computedKey = key.computedKey(with: identifier) + // Memory storage should not throw. + memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration) + + guard toDisk else { + if let completionHandler = completionHandler { + let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) + callbackQueue.execute { completionHandler(result) } + } + return + } + + ioQueue.async { + let serializer = options.cacheSerializer + if let data = serializer.data(with: image, original: original) { + self.syncStoreToDisk( + data, + forKey: key, + processorIdentifier: identifier, + callbackQueue: callbackQueue, + expiration: options.diskCacheExpiration, + completionHandler: completionHandler) + } else { + guard let completionHandler = completionHandler else { return } + + let diskError = KingfisherError.cacheError( + reason: .cannotSerializeImage(image: image, original: original, serializer: serializer)) + let result = CacheStoreResult( + memoryCacheResult: .success(()), + diskCacheResult: .failure(diskError)) + callbackQueue.execute { completionHandler(result) } + } + } + } + + /// Stores an image to the cache. + /// + /// - Parameters: + /// - image: The image to be stored. + /// - original: The original data of the image. This value will be forwarded to the provided `serializer` for + /// further use. By default, Kingfisher uses a `DefaultCacheSerializer` to serialize the image to + /// data for caching in disk, it checks the image format based on `original` data to determine in + /// which image format should be used. For other types of `serializer`, it depends on their + /// implementation detail on how to use this original data. + /// - key: The key used for caching the image. + /// - identifier: The identifier of processor being used for caching. If you are using a processor for the + /// image, pass the identifier of processor to this parameter. + /// - serializer: The `CacheSerializer` + /// - toDisk: Whether this image should be cached to disk or not. If `false`, the image is only cached in memory. + /// Otherwise, it is cached in both memory storage and disk storage. Default is `true`. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. For case + /// that `toDisk` is `false`, a `.untouch` queue means `callbackQueue` will be invoked from the + /// caller queue of this method. If `toDisk` is `true`, the `completionHandler` will be called + /// from an internal file IO queue. To change this behavior, specify another `CallbackQueue` + /// value. + /// - completionHandler: A closure which is invoked when the cache operation finishes. + open func store(_ image: Image, + original: Data? = nil, + forKey key: String, + processorIdentifier identifier: String = "", + cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default, + toDisk: Bool = true, + callbackQueue: CallbackQueue = .untouch, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + struct TempProcessor: ImageProcessor { + let identifier: String + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + return nil + } + } + + let options = KingfisherParsedOptionsInfo([ + .processor(TempProcessor(identifier: identifier)), + .cacheSerializer(serializer), + .callbackQueue(callbackQueue) + ]) + store(image, original: original, forKey: key, options: options, + toDisk: toDisk, completionHandler: completionHandler) + } + + open func storeToDisk( + _ data: Data, + forKey key: String, + processorIdentifier identifier: String = "", + expiration: StorageExpiration? = nil, + callbackQueue: CallbackQueue = .untouch, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + ioQueue.async { + self.syncStoreToDisk( + data, + forKey: key, + processorIdentifier: identifier, + callbackQueue: callbackQueue, + expiration: expiration, + completionHandler: completionHandler) + } + } + + private func syncStoreToDisk( + _ data: Data, + forKey key: String, + processorIdentifier identifier: String = "", + callbackQueue: CallbackQueue = .untouch, + expiration: StorageExpiration? = nil, + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + let computedKey = key.computedKey(with: identifier) + let result: CacheStoreResult + do { + try self.diskStorage.store(value: data, forKey: computedKey, expiration: expiration) + result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(())) + } catch { + let diskError: KingfisherError + if let error = error as? KingfisherError { + diskError = error + } else { + diskError = .cacheError(reason: .cannotConvertToData(object: data, error: error)) + } + + result = CacheStoreResult( + memoryCacheResult: .success(()), + diskCacheResult: .failure(diskError) + ) + } + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler(result) } + } + } + + // MARK: Removing Images + + /// Removes the image for the given key from the cache. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: The identifier of processor being used for caching. If you are using a processor for the + /// image, pass the identifier of processor to this parameter. + /// - fromMemory: Whether this image should be removed from memory storage or not. + /// If `false`, the image won't be removed from the memory storage. Default is `true`. + /// - fromDisk: Whether this image should be removed from disk storage or not. + /// If `false`, the image won't be removed from the disk storage. Default is `true`. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. + /// - completionHandler: A closure which is invoked when the cache removing operation finishes. + open func removeImage(forKey key: String, + processorIdentifier identifier: String = "", + fromMemory: Bool = true, + fromDisk: Bool = true, + callbackQueue: CallbackQueue = .untouch, + completionHandler: (() -> Void)? = nil) + { + let computedKey = key.computedKey(with: identifier) + + if fromMemory { + try? memoryStorage.remove(forKey: computedKey) + } + + if fromDisk { + ioQueue.async{ + try? self.diskStorage.remove(forKey: computedKey) + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler() } + } + } + } else { + if let completionHandler = completionHandler { + callbackQueue.execute { completionHandler() } + } + } + } + + func retrieveImage(forKey key: String, + options: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .untouch, + completionHandler: ((Result<ImageCacheResult, KingfisherError>) -> Void)?) + { + // No completion handler. No need to start working and early return. + guard let completionHandler = completionHandler else { return } + + // Try to check the image from memory cache first. + if let image = retrieveImageInMemoryCache(forKey: key, options: options) { + let image = options.imageModifier?.modify(image) ?? image + callbackQueue.execute { completionHandler(.success(.memory(image))) } + } else if options.fromMemoryCacheOrRefresh { + callbackQueue.execute { completionHandler(.success(.none)) } + } else { + // Begin to disk search. + self.retrieveImageInDiskCache(forKey: key, options: options, callbackQueue: callbackQueue) { + result in + // The callback queue is already correct in this closure. + switch result { + case .success(let image): + + guard let image = image else { + // No image found in disk storage. + completionHandler(.success(.none)) + return + } + + let finalImage = options.imageModifier?.modify(image) ?? image + // Cache the disk image to memory. + // We are passing `false` to `toDisk`, the memory cache does not change + // callback queue, we can call `completionHandler` without another dispatch. + var cacheOptions = options + cacheOptions.callbackQueue = .untouch + self.store( + finalImage, + forKey: key, + options: cacheOptions, + toDisk: false) + { + _ in + completionHandler(.success(.disk(finalImage))) + } + case .failure(let error): + completionHandler(.failure(error)) + } + } + } + } + + // MARK: Getting Images + + /// Gets an image for a given key from the cache, either from memory storage or disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. + /// - completionHandler: A closure which is invoked when the image getting operation finishes. If the + /// image retrieving operation finishes without problem, an `ImageCacheResult` value + /// will be sent to this closure as result. Otherwise, a `KingfisherError` result + /// with detail failing reason will be sent. + open func retrieveImage(forKey key: String, + options: KingfisherOptionsInfo? = nil, + callbackQueue: CallbackQueue = .untouch, + completionHandler: ((Result<ImageCacheResult, KingfisherError>) -> Void)?) + { + retrieveImage( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + func retrieveImageInMemoryCache( + forKey key: String, + options: KingfisherParsedOptionsInfo) -> Image? + { + let computedKey = key.computedKey(with: options.processor.identifier) + return memoryStorage.value(forKey: computedKey, extendingExpiration: options.memoryCacheAccessExtendingExpiration) + } + + /// Gets an image for a given key from the memory storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image. + /// - Returns: The image stored in memory cache, if exists and valid. Otherwise, if the image does not exist or + /// has already expired, `nil` is returned. + open func retrieveImageInMemoryCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil) -> Image? + { + return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options)) + } + + func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .untouch, + completionHandler: @escaping (Result<Image?, KingfisherError>) -> Void) + { + let computedKey = key.computedKey(with: options.processor.identifier) + let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue) + loadingQueue.execute { + do { + var image: Image? = nil + if let data = try self.diskStorage.value(forKey: computedKey) { + image = options.cacheSerializer.image(with: data, options: options) + } + callbackQueue.execute { completionHandler(.success(image)) } + } catch { + if let error = error as? KingfisherError { + callbackQueue.execute { completionHandler(.failure(error)) } + } else { + assertionFailure("The internal thrown error should be a `KingfisherError`.") + } + } + } + } + + /// Gets an image for a given key from the disk storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherOptionsInfo` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.untouch`. + /// - completionHandler: A closure which is invoked when the operation finishes. + open func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil, + callbackQueue: CallbackQueue = .untouch, + completionHandler: @escaping (Result<Image?, KingfisherError>) -> Void) + { + retrieveImageInDiskCache( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + // MARK: Cleaning + /// Clears the memory storage of this cache. + @objc public func clearMemoryCache() { + try? memoryStorage.removeAll() + } + + /// Clears the disk storage of this cache. This is an async operation. + /// + /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + open func clearDiskCache(completion handler: (()->())? = nil) { + ioQueue.async { + do { + try self.diskStorage.removeAll() + } catch _ { } + if let handler = handler { + DispatchQueue.main.async { handler() } + } + } + } + + /// Clears the expired images from disk storage. This is an async operation. + open func cleanExpiredMemoryCache() { + memoryStorage.removeExpired() + } + + /// Clears the expired images from disk storage. This is an async operation. + @objc func cleanExpiredDiskCache() { + cleanExpiredDiskCache(completion: nil) + } + + /// Clears the expired images from disk storage. This is an async operation. + /// + /// - Parameter handler: A closure which is invoked when the cache clearing operation finishes. + /// This `handler` will be called from the main queue. + open func cleanExpiredDiskCache(completion handler: (() -> Void)? = nil) { + ioQueue.async { + do { + var removed: [URL] = [] + let removedExpired = try self.diskStorage.removeExpiredValues() + removed.append(contentsOf: removedExpired) + + let removedSizeExceeded = try self.diskStorage.removeSizeExceededValues() + removed.append(contentsOf: removedSizeExceeded) + + if !removed.isEmpty { + DispatchQueue.main.async { + let cleanedHashes = removed.map { $0.lastPathComponent } + NotificationCenter.default.post( + name: .KingfisherDidCleanDiskCache, + object: self, + userInfo: [KingfisherDiskCacheCleanedHashKey: cleanedHashes]) + } + } + + if let handler = handler { + DispatchQueue.main.async { handler() } + } + } catch {} + } + } + +#if !os(macOS) && !os(watchOS) + /// Clears the expired images from disk storage when app is in background. This is an async operation. + /// In most cases, you should not call this method explicitly. + /// It will be called automatically when `UIApplicationDidEnterBackgroundNotification` received. + @objc public func backgroundCleanExpiredDiskCache() { + // if 'sharedApplication()' is unavailable, then return + guard let sharedApplication = KingfisherWrapper<UIApplication>.shared else { return } + + func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) { + sharedApplication.endBackgroundTask(task) + #if swift(>=4.2) + task = UIBackgroundTaskIdentifier.invalid + #else + task = UIBackgroundTaskInvalid + #endif + } + + var backgroundTask: UIBackgroundTaskIdentifier! + backgroundTask = sharedApplication.beginBackgroundTask { + endBackgroundTask(&backgroundTask!) + } + + cleanExpiredDiskCache { + endBackgroundTask(&backgroundTask!) + } + } +#endif + + // MARK: Image Cache State + + /// Returns the cache type for a given `key` and `identifier` combination. + /// This method is used for checking whether an image is cached in current cache. + /// It also provides information on which kind of cache can it be found in the return value. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: A `CacheType` instance which indicates the cache status. + /// `.none` means the image is not in cache or it is already expired. + open func imageCachedType( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> CacheType + { + let computedKey = key.computedKey(with: identifier) + if memoryStorage.isCached(forKey: computedKey) { return .memory } + if diskStorage.isCached(forKey: computedKey) { return .disk } + return .none + } + + /// Returns whether the file exists in cache for a given `key` and `identifier` combination. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: A `Bool` which indicates whether a cache could match the given `key` and `identifier` combination. + /// + /// - Note: + /// The return value does not contain information about from which kind of storage the cache matches. + /// To get the information about cache type according `CacheType`, + /// use `imageCachedType(forKey:processorIdentifier:)` instead. + public func isCached( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> Bool + { + return imageCachedType(forKey: key, processorIdentifier: identifier).cached + } + + /// Gets the hash used as cache file name for the key. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: The hash which is used as the cache file name. + /// + /// - Note: + /// By default, for a given combination of `key` and `identifier`, `ImageCache` will use the value + /// returned by this method as the cache file name. You can use this value to check and match cache file + /// if you need. + open func hash( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String + { + let computedKey = key.computedKey(with: identifier) + return diskStorage.cacheFileName(forKey: computedKey) + } + + /// Calculates the size taken by the disk storage. + /// It is the total file size of all cached files in the `diskStorage` on disk in bytes. + /// + /// - Parameter handler: Called with the size calculating finishes. This closure is invoked from the main queue. + open func calculateDiskStorageSize(completion handler: @escaping ((Result<UInt, KingfisherError>) -> Void)) { + ioQueue.async { + do { + let size = try self.diskStorage.totalSize() + DispatchQueue.main.async { handler(.success(size)) } + } catch { + if let error = error as? KingfisherError { + DispatchQueue.main.async { handler(.failure(error)) } + } else { + assertionFailure("The internal thrown error should be a `KingfisherError`.") + } + + } + } + } + + /// Gets the cache path for the key. + /// It is useful for projects with web view or anyone that needs access to the local file path. + /// + /// i.e. Replacing the `<img src='path_for_key'>` tag in your HTML. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - identifier: Processor identifier which used for this image. Default is the `identifier` of + /// `DefaultImageProcessor.default`. + /// - Returns: The disk path of cached image under the given `key` and `identifier`. + /// + /// - Note: + /// This method does not guarantee there is an image already cached in the returned path. It just gives your + /// the path that the image should be, if it exists in disk storage. + /// + /// You could use `isCached(forKey:)` method to check whether the image is cached under that key in disk. + open func cachePath( + forKey key: String, + processorIdentifier identifier: String = DefaultImageProcessor.default.identifier) -> String + { + let computedKey = key.computedKey(with: identifier) + return diskStorage.cacheFileURL(forKey: computedKey).path + } +} + +extension Dictionary { + func keysSortedByValue(_ isOrderedBefore: (Value, Value) -> Bool) -> [Key] { + return Array(self).sorted{ isOrderedBefore($0.1, $1.1) }.map{ $0.0 } + } +} + +#if !os(macOS) && !os(watchOS) +// MARK: - For App Extensions +extension UIApplication: KingfisherCompatible { } +extension KingfisherWrapper where Base: UIApplication { + public static var shared: UIApplication? { + let selector = NSSelectorFromString("sharedApplication") + guard Base.responds(to: selector) else { return nil } + return Base.perform(selector).takeUnretainedValue() as? UIApplication + } +} +#endif + +extension String { + func computedKey(with identifier: String) -> String { + if identifier.isEmpty { + return self + } else { + return appending("@\(identifier)") + } + } +} + +extension ImageCache { + + /// Creates an `ImageCache` with a given `name`, cache directory `path` + /// and a closure to modify the cache directory. + /// + /// - Parameters: + /// - name: The name of cache object. It is used to setup disk cache directories and IO queue. + /// You should not use the same `name` for different caches, otherwise, the disk storage would + /// be conflicting to each other. + /// - path: Location of cache URL on disk. It will be internally pass to the initializer of `DiskStorage` as the + /// disk cache directory. + /// - diskCachePathClosure: Closure that takes in an optional initial path string and generates + /// the final disk cache path. You could use it to fully customize your cache path. + /// - Throws: An error that happens during image cache creating, such as unable to create a directory at the given + /// path. + @available(*, deprecated, message: "Use `init(name:cacheDirectoryURL:diskCachePathClosure:)` instead", + renamed: "init(name:cacheDirectoryURL:diskCachePathClosure:)") + public convenience init( + name: String, + path: String?, + diskCachePathClosure: DiskCachePathClosure? = nil) throws + { + let directoryURL = path.flatMap { URL(string: $0) } + try self.init(name: name, cacheDirectoryURL: directoryURL, diskCachePathClosure: diskCachePathClosure) + } +} diff --git a/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift b/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift new file mode 100644 index 0000000..214df3b --- /dev/null +++ b/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift @@ -0,0 +1,242 @@ +// +// MemoryStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a set of conception related to storage which stores a certain type of value in memory. +/// This is a namespace for the memory storage types. A `Backend` with a certain `Config` will be used to describe the +/// storage. See these composed types for more information. +public enum MemoryStorage { + + /// Represents a storage which stores a certain type of value in memory. It provides fast access, + /// but limited storing size. The stored value type needs to conform to `CacheCostCalculable`, + /// and its `cacheCost` will be used to determine the cost of size for the cache item. + /// + /// You can config a `MemoryStorage.Backend` in its initializer by passing a `MemoryStorage.Config` value. + /// or modifying the `config` property after it being created. The backend of `MemoryStorage` has + /// upper limitation on cost size in memory and item count. All items in the storage has an expiration + /// date. When retrieved, if the target item is already expired, it will be recognized as it does not + /// exist in the storage. The `MemoryStorage` also contains a scheduled self clean task, to evict expired + /// items from memory. + public class Backend<T: CacheCostCalculable> { + let storage = NSCache<NSString, StorageObject<T>>() + var keys = Set<String>() + + var cleanTimer: Timer? = nil + let lock = NSLock() + + let cacheDelegate = CacheDelegate<StorageObject<T>>() + + /// The config used in this storage. It is a value you can set and + /// use to config the storage in air. + public var config: Config { + didSet { + storage.totalCostLimit = config.totalCostLimit + storage.countLimit = config.countLimit + } + } + + /// Creates a `MemoryStorage` with a given `config`. + /// + /// - Parameter config: The config used to create the storage. It determines the max size limitation, + /// default expiration setting and more. + public init(config: Config) { + self.config = config + storage.totalCostLimit = config.totalCostLimit + storage.countLimit = config.countLimit + storage.delegate = cacheDelegate + cacheDelegate.onObjectRemoved.delegate(on: self) { (self, obj) in + self.keys.remove(obj.key) + } + + cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.removeExpired() + } + } + + func removeExpired() { + lock.lock() + defer { lock.unlock() } + for key in keys { + let nsKey = key as NSString + guard let object = storage.object(forKey: nsKey) else { + keys.remove(key) + continue + } + if object.estimatedExpiration.isPast { + storage.removeObject(forKey: nsKey) + keys.remove(key) + } + } + } + + // Storing in memory will not throw. It is just for meeting protocol requirement and + // forwarding to no throwing method. + func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) throws + { + storeNoThrow(value: value, forKey: key, expiration: expiration) + } + + // The no throw version for storing value in cache. Kingfisher knows the detail so it + // could use this version to make syntax simpler internally. + func storeNoThrow( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) + { + lock.lock() + defer { lock.unlock() } + let expiration = expiration ?? config.expiration + // The expiration indicates that already expired, no need to store. + guard !expiration.isExpired else { return } + + let object = StorageObject(value, key: key, expiration: expiration) + storage.setObject(object, forKey: key as NSString, cost: value.cacheCost) + keys.insert(key) + } + + /// Use this when you actually access the memory cached item. + /// By default, this will extend the expired data for the accessed item. + /// + /// - Parameters: + /// - key: Cache Key + /// - extendingExpiration: expiration value to extend item expiration time: + /// * .none: The item expires after the original time, without extending after access. + /// * .cacheTime: The item expiration extends by the original cache time after each access. + /// * .expirationTime: The item expiration extends by the provided time after each access. + /// - Returns: cached object or nil + func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? { + guard let object = storage.object(forKey: key as NSString) else { + return nil + } + if object.expired { + return nil + } + object.extendExpiration(extendingExpiration) + return object.value + } + + func isCached(forKey key: String) -> Bool { + guard let _ = value(forKey: key, extendingExpiration: .none) else { + return false + } + return true + } + + func remove(forKey key: String) throws { + lock.lock() + defer { lock.unlock() } + storage.removeObject(forKey: key as NSString) + keys.remove(key) + } + + func removeAll() throws { + lock.lock() + defer { lock.unlock() } + storage.removeAllObjects() + keys.removeAll() + } + + class CacheDelegate<T>: NSObject, NSCacheDelegate { + let onObjectRemoved = Delegate<T, Void>() + func cache(_ cache: NSCache<AnyObject, AnyObject>, willEvictObject obj: Any) { + if let obj = obj as? T { + onObjectRemoved.call(obj) + } + } + } + } +} + +extension MemoryStorage { + /// Represents the config used in a `MemoryStorage`. + public struct Config { + + /// Total cost limit of the storage in bytes. + public var totalCostLimit: Int + + /// The item count limit of the memory storage. + public var countLimit: Int = .max + + /// The `StorageExpiration` used in this memory storage. Default is `.seconds(300)`, + /// means that the memory cache would expire in 5 minutes. + public var expiration: StorageExpiration = .seconds(300) + + /// The time interval between the storage do clean work for swiping expired items. + public let cleanInterval: TimeInterval + + /// Creates a config from a given `totalCostLimit` value. + /// + /// - Parameters: + /// - totalCostLimit: Total cost limit of the storage in bytes. + /// - cleanInterval: The time interval between the storage do clean work for swiping expired items. + /// Default is 120, means the auto eviction happens once per two minutes. + /// + /// - Note: + /// Other members of `MemoryStorage.Config` will use their default values when created. + public init(totalCostLimit: Int, cleanInterval: TimeInterval = 120) { + self.totalCostLimit = totalCostLimit + self.cleanInterval = cleanInterval + } + } +} + +extension MemoryStorage { + class StorageObject<T> { + let value: T + let expiration: StorageExpiration + let key: String + + private(set) var estimatedExpiration: Date + + init(_ value: T, key: String, expiration: StorageExpiration) { + self.value = value + self.key = key + self.expiration = expiration + + self.estimatedExpiration = expiration.estimatedExpirationSinceNow + } + + func extendExpiration(_ extendingExpiration: ExpirationExtending = .cacheTime) { + switch extendingExpiration { + case .none: + return + case .cacheTime: + self.estimatedExpiration = expiration.estimatedExpirationSinceNow + case .expirationTime(let expirationTime): + self.estimatedExpiration = expirationTime.estimatedExpirationSinceNow + } + } + + var expired: Bool { + return estimatedExpiration.isPast + } + } +} diff --git a/Pods/Kingfisher/Sources/Cache/Storage.swift b/Pods/Kingfisher/Sources/Cache/Storage.swift new file mode 100644 index 0000000..acd7f06 --- /dev/null +++ b/Pods/Kingfisher/Sources/Cache/Storage.swift @@ -0,0 +1,108 @@ +// +// Storage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Constants for some time intervals +struct TimeConstants { + static let secondsInOneMinute = 60 + static let minutesInOneHour = 60 + static let hoursInOneDay = 24 + static let secondsInOneDay = secondsInOneMinute * minutesInOneHour * hoursInOneDay +} + +/// Represents the expiration strategy used in storage. +/// +/// - never: The item never expires. +/// - seconds: The item expires after a time duration of given seconds from now. +/// - days: The item expires after a time duration of given days from now. +/// - date: The item expires after a given date. +public enum StorageExpiration { + /// The item never expires. + case never + /// The item expires after a time duration of given seconds from now. + case seconds(TimeInterval) + /// The item expires after a time duration of given days from now. + case days(Int) + /// The item expires after a given date. + case date(Date) + /// Indicates the item is already expired. Use this to skip cache. + case expired + + func estimatedExpirationSince(_ date: Date) -> Date { + switch self { + case .never: return .distantFuture + case .seconds(let seconds): return date.addingTimeInterval(seconds) + case .days(let days): return date.addingTimeInterval(TimeInterval(TimeConstants.secondsInOneDay * days)) + case .date(let ref): return ref + case .expired: return .distantPast + } + } + + var estimatedExpirationSinceNow: Date { + return estimatedExpirationSince(Date()) + } + + var isExpired: Bool { + return timeInterval <= 0 + } + + var timeInterval: TimeInterval { + switch self { + case .never: return .infinity + case .seconds(let seconds): return seconds + case .days(let days): return TimeInterval(TimeConstants.secondsInOneDay * days) + case .date(let ref): return ref.timeIntervalSinceNow + case .expired: return -(.infinity) + } + } +} + +/// Represents the expiration extending strategy used in storage to after access. +/// +/// - none: The item expires after the original time, without extending after access. +/// - cacheTime: The item expiration extends by the original cache time after each access. +/// - expirationTime: The item expiration extends by the provided time after each access. +public enum ExpirationExtending { + /// The item expires after the original time, without extending after access. + case none + /// The item expiration extends by the original cache time after each access. + case cacheTime + /// The item expiration extends by the provided time after each access. + case expirationTime(_ expiration: StorageExpiration) +} + +/// Represents types which cost in memory can be calculated. +public protocol CacheCostCalculable { + var cacheCost: Int { get } +} + +/// Represents types which can be converted to and from data. +public protocol DataTransformable { + func toData() throws -> Data + static func fromData(_ data: Data) throws -> Self + static var empty: Self { get } +} diff --git a/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift b/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift new file mode 100644 index 0000000..85838f8 --- /dev/null +++ b/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift @@ -0,0 +1,404 @@ +// +// ImageView+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension KingfisherWrapper where Base: ImageView { + + // MARK: Setting Image + + /// Sets an image to the image view with a `Source`. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from a source. Since all parameters + /// have a default value except the `source`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// // Set image from a network source. + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: .network(url)) + /// + /// // Or set image from a data provider. + /// let provider = LocalFileImageDataProvider(fileURL: fileURL) + /// imageView.kf.setImage(with: .provider(provider)) + /// ``` + /// + /// For both `.network` and `.provider` source, there are corresponding view extension methods. So the code + /// above is equivalent to: + /// + /// ``` + /// imageView.kf.setImage(with: url) + /// imageView.kf.setImage(with: provider) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the source. + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + mutatingSelf.placeholder = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + let noImageOrPlaceholderSet = base.image == nil && self.placeholder == nil + if !options.keepCurrentImageWhileLoading || noImageOrPlaceholderSet { + // Always set placeholder while there is no image/placeholder yet. + mutatingSelf.placeholder = placeholder + } + + let maybeIndicator = indicator + maybeIndicator?.startAnimatingView() + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if base.shouldPreloadAllAnimation() { + options.preloadAllAnimationData = true + } + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.image = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + maybeIndicator?.stopAnimatingView() + guard issuedIdentifier == self.taskIdentifier else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.taskIdentifier = nil + + switch result { + case .success(let value): + guard self.needsTransition(options: options, cacheType: value.cacheType) else { + mutatingSelf.placeholder = nil + self.base.image = value.image + completionHandler?(result) + return + } + + self.makeTransition(image: value.image, transition: options.transition) { + completionHandler?(result) + } + + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + mutatingSelf.imageTask = task + return task + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// This is the easiest way to use Kingfisher to boost the image setting process from network. Since all parameters + /// have a default value except the `resource`, you can set an image from a certain URL to an image view like this: + /// + /// ``` + /// let url = URL(string: "https://example.com/image.png")! + /// imageView.kf.setImage(with: url) + /// ``` + /// + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource.map { .network($0) }, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Sets an image to the image view with a data provider. + /// + /// - Parameters: + /// - provider: The `ImageDataProvider` object contains information about the data. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// Internally, this method will use `KingfisherManager` to get the image data, from either cache + /// or the data provider. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with provider: ImageDataProvider?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: provider.map { .provider($0) }, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } + + private func needsTransition(options: KingfisherParsedOptionsInfo, cacheType: CacheType) -> Bool { + switch options.transition { + case .none: + return false + #if !os(macOS) + default: + if options.forceTransition { return true } + if cacheType == .none { return true } + return false + #endif + } + } + + private func makeTransition(image: Image, transition: ImageTransition, done: @escaping () -> Void) { + #if !os(macOS) + // Force hiding the indicator without transition first. + UIView.transition( + with: self.base, + duration: 0.0, + options: [], + animations: { self.indicator?.stopAnimatingView() }, + completion: { _ in + var mutatingSelf = self + mutatingSelf.placeholder = nil + UIView.transition( + with: self.base, + duration: transition.duration, + options: [transition.animationOptions, .allowUserInteraction], + animations: { transition.animations?(self.base, image) }, + completion: { finished in + transition.completion?(finished) + done() + } + ) + } + ) + #else + done() + #endif + } +} + +// MARK: - Associated Object +private var taskIdentifierKey: Void? +private var indicatorKey: Void? +private var indicatorTypeKey: Void? +private var placeholderKey: Void? +private var imageTaskKey: Void? + +extension KingfisherWrapper where Base: ImageView { + + // MARK: Properties + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box<Source.Identifier.Value>? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + /// Holds which indicator type is going to be used. + /// Default is `.none`, means no indicator will be shown while downloading. + public var indicatorType: IndicatorType { + get { + return getAssociatedObject(base, &indicatorTypeKey) ?? .none + } + + set { + switch newValue { + case .none: indicator = nil + case .activity: indicator = ActivityIndicator() + case .image(let data): indicator = ImageIndicator(imageData: data) + case .custom(let anIndicator): indicator = anIndicator + } + + setRetainedAssociatedObject(base, &indicatorTypeKey, newValue) + } + } + + /// Holds any type that conforms to the protocol `Indicator`. + /// The protocol `Indicator` has a `view` property that will be shown when loading an image. + /// It will be `nil` if `indicatorType` is `.none`. + public private(set) var indicator: Indicator? { + get { + let box: Box<Indicator>? = getAssociatedObject(base, &indicatorKey) + return box?.value + } + + set { + // Remove previous + if let previousIndicator = indicator { + previousIndicator.view.removeFromSuperview() + } + + // Add new + if let newIndicator = newValue { + // Set default indicator layout + let view = newIndicator.view + + base.addSubview(view) + view.translatesAutoresizingMaskIntoConstraints = false + view.centerXAnchor.constraint( + equalTo: base.centerXAnchor, constant: newIndicator.centerOffset.x).isActive = true + view.centerYAnchor.constraint( + equalTo: base.centerYAnchor, constant: newIndicator.centerOffset.y).isActive = true + + newIndicator.view.isHidden = true + } + + // Save in associated object + // Wrap newValue with Box to workaround an issue that Swift does not recognize + // and casting protocol for associate object correctly. https://github.com/onevcat/Kingfisher/issues/872 + setRetainedAssociatedObject(base, &indicatorKey, newValue.map(Box.init)) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + /// Represents the `Placeholder` used for this image view. A `Placeholder` will be shown in the view while + /// it is downloading an image. + public private(set) var placeholder: Placeholder? { + get { return getAssociatedObject(base, &placeholderKey) } + set { + if let previousPlaceholder = placeholder { + previousPlaceholder.remove(from: base) + } + + if let newPlaceholder = newValue { + newPlaceholder.add(to: base) + } else { + base.image = nil + } + setRetainedAssociatedObject(base, &placeholderKey, newValue) + } + } +} + + +@objc extension ImageView { + func shouldPreloadAllAnimation() -> Bool { return true } +} + +extension KingfisherWrapper where Base: ImageView { + /// Gets the image URL bound to this image view. + @available(*, deprecated, message: "Use `taskIdentifier` instead to identify a setting task.") + public private(set) var webURL: URL? { + get { return nil } + set { } + } +} diff --git a/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift b/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift new file mode 100644 index 0000000..64ddc92 --- /dev/null +++ b/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift @@ -0,0 +1,390 @@ +// +// UIButton+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/13. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import UIKit + +extension KingfisherWrapper where Base: UIButton { + + // MARK: Setting Image + /// Sets an image to the button for a specified state with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? + { + guard let source = source else { + base.setImage(placeholder, for: state) + setTaskIdentifier(nil, for: state) + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + if !options.keepCurrentImageWhileLoading { + base.setImage(placeholder, for: state) + } + + var mutatingSelf = self + let issuedIdentifier = Source.Identifier.next() + setTaskIdentifier(issuedIdentifier, for: state) + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setImage(image, for: state) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.taskIdentifier(for: state) } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.taskIdentifier(for: state) else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.imageTask = nil + mutatingSelf.setTaskIdentifier(nil, for: state) + + switch result { + case .success(let value): + self.base.setImage(value.image, for: state) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.setImage(image, for: state) + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + /// Sets an image to the button for a specified state with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource.map { Source.network($0) }, + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + // MARK: Cancelling Downloading Task + + /// Cancels the image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelImageDownloadTask() { + imageTask?.cancel() + } + + // MARK: Setting Background Image + + /// Sets a background image to the button for a specified state with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested source + /// Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setBackgroundImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? + { + guard let source = source else { + base.setBackgroundImage(placeholder, for: state) + setBackgroundTaskIdentifier(nil, for: state) + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + if !options.keepCurrentImageWhileLoading { + base.setBackgroundImage(placeholder, for: state) + } + + var mutatingSelf = self + let issuedIdentifier = Source.Identifier.next() + setBackgroundTaskIdentifier(issuedIdentifier, for: state) + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.setBackgroundImage(image, for: state) + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.backgroundTaskIdentifier(for: state) } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.backgroundTaskIdentifier(for: state) else { + let reason: KingfisherError.ImageSettingErrorReason + do { + let value = try result.get() + reason = .notCurrentSourceTask(result: value, error: nil, source: source) + } catch { + reason = .notCurrentSourceTask(result: nil, error: error, source: source) + } + let error = KingfisherError.imageSettingError(reason: reason) + completionHandler?(.failure(error)) + return + } + + mutatingSelf.backgroundImageTask = nil + mutatingSelf.setBackgroundTaskIdentifier(nil, for: state) + + switch result { + case .success(let value): + self.base.setBackgroundImage(value.image, for: state) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.setBackgroundImage(image, for: state) + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.backgroundImageTask = task + return task + } + + /// Sets a background image to the button for a specified state with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - state: The button state to which the image should be set. + /// - placeholder: A placeholder to show while retrieving the image from the given `resource`. + /// - options: An options set to define image setting behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. + /// - completionHandler: Called when the image retrieved and set finished. + /// - Returns: A task represents the image downloading. + /// + /// - Note: + /// Internally, this method will use `KingfisherManager` to get the requested resource, from either cache + /// or network. Since this method will perform UI changes, you must call it from the main thread. + /// Both `progressBlock` and `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setBackgroundImage( + with resource: Resource?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? + { + return setBackgroundImage( + with: resource.map { .network($0) }, + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + // MARK: Cancelling Background Downloading Task + + /// Cancels the background image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelBackgroundImageDownloadTask() { + backgroundImageTask?.cancel() + } +} + +// MARK: - Associated Object +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: UIButton { + + private typealias TaskIdentifier = Box<[UInt: Source.Identifier.Value]> + + public func taskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { + return taskIdentifierInfo.value[state.rawValue] + } + + private func setTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { + taskIdentifierInfo.value[state.rawValue] = identifier + } + + private var taskIdentifierInfo: TaskIdentifier { + return getAssociatedObject(base, &taskIdentifierKey) ?? { + setRetainedAssociatedObject(base, &taskIdentifierKey, $0) + return $0 + } (TaskIdentifier([:])) + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + + +private var backgroundTaskIdentifierKey: Void? +private var backgroundImageTaskKey: Void? + +// MARK: Background Properties +extension KingfisherWrapper where Base: UIButton { + + public func backgroundTaskIdentifier(for state: UIControl.State) -> Source.Identifier.Value? { + return backgroundTaskIdentifierInfo.value[state.rawValue] + } + + private func setBackgroundTaskIdentifier(_ identifier: Source.Identifier.Value?, for state: UIControl.State) { + backgroundTaskIdentifierInfo.value[state.rawValue] = identifier + } + + private var backgroundTaskIdentifierInfo: TaskIdentifier { + return getAssociatedObject(base, &backgroundTaskIdentifierKey) ?? { + setRetainedAssociatedObject(base, &backgroundTaskIdentifierKey, $0) + return $0 + } (TaskIdentifier([:])) + } + + private var backgroundImageTask: DownloadTask? { + get { return getAssociatedObject(base, &backgroundImageTaskKey) } + mutating set { setRetainedAssociatedObject(base, &backgroundImageTaskKey, newValue) } + } +} + +extension KingfisherWrapper where Base: UIButton { + + /// Gets the image URL of this button for a specified state. + /// + /// - Parameter state: The state that uses the specified image. + /// - Returns: Current URL for image. + @available(*, deprecated, message: "Use `taskIdentifier` instead to identify a setting task.") + public func webURL(for state: UIControl.State) -> URL? { + return nil + } + + /// Gets the background image URL of this button for a specified state. + /// + /// - Parameter state: The state that uses the specified background image. + /// - Returns: Current URL for image. + @available(*, deprecated, message: "Use `backgroundTaskIdentifier` instead to identify a setting task.") + public func backgroundWebURL(for state: UIControl.State) -> URL? { + return nil + } +} diff --git a/Pods/Kingfisher/Sources/General/Deprecated.swift b/Pods/Kingfisher/Sources/General/Deprecated.swift new file mode 100644 index 0000000..fa47a33 --- /dev/null +++ b/Pods/Kingfisher/Sources/General/Deprecated.swift @@ -0,0 +1,654 @@ +// +// Deprecated.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(AppKit) +import AppKit +#elseif canImport(UIKit) +import UIKit +#endif + +// MARK: - Deprecated +extension KingfisherWrapper where Base: Image { + @available(*, deprecated, message: + "Will be removed soon. Pass parameters with `ImageCreatingOptions`, use `image(with:options:)` instead.") + public static func image( + data: Data, + scale: CGFloat, + preloadAllAnimationData: Bool, + onlyFirstFrame: Bool) -> Image? + { + let options = ImageCreatingOptions( + scale: scale, + duration: 0.0, + preloadAll: preloadAllAnimationData, + onlyFirstFrame: onlyFirstFrame) + return KingfisherWrapper.image(data: data, options: options) + } + + @available(*, deprecated, message: + "Will be removed soon. Pass parameters with `ImageCreatingOptions`, use `animatedImage(with:options:)` instead.") + public static func animated( + with data: Data, + scale: CGFloat = 1.0, + duration: TimeInterval = 0.0, + preloadAll: Bool, + onlyFirstFrame: Bool = false) -> Image? + { + let options = ImageCreatingOptions( + scale: scale, duration: duration, preloadAll: preloadAll, onlyFirstFrame: onlyFirstFrame) + return animatedImage(data: data, options: options) + } +} + +@available(*, deprecated, message: "Will be removed soon. Use `Result<RetrieveImageResult>` based callback instead") +public typealias CompletionHandler = + ((_ image: Image?, _ error: NSError?, _ cacheType: CacheType, _ imageURL: URL?) -> Void) + +@available(*, deprecated, message: "Will be removed soon. Use `Result<ImageLoadingResult>` based callback instead") +public typealias ImageDownloaderCompletionHandler = + ((_ image: Image?, _ error: NSError?, _ url: URL?, _ originalData: Data?) -> Void) + +// MARK: - Deprecated +@available(*, deprecated, message: "Will be removed soon. Use `DownloadTask` to cancel a task.") +extension RetrieveImageTask { + @available(*, deprecated, message: "RetrieveImageTask.empty will be removed soon. Use `nil` to represent a no task.") + public static let empty = RetrieveImageTask() +} + +// MARK: - Deprecated +extension KingfisherManager { + /// Get an image with resource. + /// If `.empty` is used as `options`, Kingfisher will seek the image in memory and disk first. + /// If not found, it will download the image at `resource.downloadURL` and cache it with `resource.cacheKey`. + /// These default behaviors could be adjusted by passing different options. See `KingfisherOptions` for more. + /// + /// - Parameters: + /// - resource: Resource object contains information such as `cacheKey` and `downloadURL`. + /// - options: A dictionary could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time downloaded data changed. This could be used as a progress UI. + /// - completionHandler: Called when the whole retrieving process finished. + /// - Returns: A `RetrieveImageTask` task object. You can use this object to cancel the task. + @available(*, deprecated, message: "Use `Result` based callback instead.") + @discardableResult + public func retrieveImage(with resource: Resource, + options: KingfisherOptionsInfo?, + progressBlock: DownloadProgressBlock?, + completionHandler: CompletionHandler?) -> DownloadTask? + { + return retrieveImage(with: resource, options: options, progressBlock: progressBlock) { + result in + switch result { + case .success(let value): completionHandler?(value.image, nil, value.cacheType, value.source.url) + case .failure(let error): completionHandler?(nil, error as NSError, .none, resource.downloadURL) + } + } + } +} + +// MARK: - Deprecated +extension ImageDownloader { + @available(*, deprecated, message: "Use `Result` based callback instead.") + @discardableResult + open func downloadImage(with url: URL, + retrieveImageTask: RetrieveImageTask? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: ImageDownloaderProgressBlock? = nil, + completionHandler: ImageDownloaderCompletionHandler?) -> DownloadTask? + { + return downloadImage(with: url, options: options, progressBlock: progressBlock) { + result in + switch result { + case .success(let value): completionHandler?(value.image, nil, value.url, value.originalData) + case .failure(let error): completionHandler?(nil, error as NSError, nil, nil) + } + } + } +} + +@available(*, deprecated, message: "RetrieveImageDownloadTask is removed. Use `DownloadTask` to cancel a task.") +public struct RetrieveImageDownloadTask { +} + +@available(*, deprecated, message: "RetrieveImageTask is removed. Use `DownloadTask` to cancel a task.") +public final class RetrieveImageTask { +} + +@available(*, deprecated, message: "Use `DownloadProgressBlock` instead.", renamed: "DownloadProgressBlock") +public typealias ImageDownloaderProgressBlock = DownloadProgressBlock + +#if !os(watchOS) +// MARK: - Deprecated +extension KingfisherWrapper where Base: ImageView { + @available(*, deprecated, message: "Use `Result` based callback instead.") + @discardableResult + public func setImage(with resource: Resource?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler?) -> DownloadTask? + { + return setImage(with: resource, placeholder: placeholder, options: options, progressBlock: progressBlock) { + result in + switch result { + case .success(let value): + completionHandler?(value.image, nil, value.cacheType, value.source.url) + case .failure(let error): + completionHandler?(nil, error as NSError, .none, nil) + } + } + } +} +#endif + +#if canImport(UIKit) && !os(watchOS) +// MARK: - Deprecated +extension KingfisherWrapper where Base: UIButton { + @available(*, deprecated, message: "Use `Result` based callback instead.") + @discardableResult + public func setImage( + with resource: Resource?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler?) -> DownloadTask? + { + return setImage( + with: resource, + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock) + { + result in + switch result { + case .success(let value): + completionHandler?(value.image, nil, value.cacheType, value.source.url) + case .failure(let error): + completionHandler?(nil, error as NSError, .none, nil) + } + } + } + + @available(*, deprecated, message: "Use `Result` based callback instead.") + @discardableResult + public func setBackgroundImage( + with resource: Resource?, + for state: UIControl.State, + placeholder: UIImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler?) -> DownloadTask? + { + return setBackgroundImage( + with: resource, + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock) + { + result in + switch result { + case .success(let value): + completionHandler?(value.image, nil, value.cacheType, value.source.url) + case .failure(let error): + completionHandler?(nil, error as NSError, .none, nil) + } + } + } +} +#endif + +#if os(watchOS) +import WatchKit +// MARK: - Deprecated +extension KingfisherWrapper where Base: WKInterfaceImage { + @available(*, deprecated, message: "Use `Result` based callback instead.") + @discardableResult + public func setImage(_ resource: Resource?, + placeholder: Image? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler?) -> DownloadTask? + { + return setImage( + with: resource, + placeholder: placeholder, + options: options, + progressBlock: progressBlock) + { + result in + switch result { + case .success(let value): + completionHandler?(value.image, nil, value.cacheType, value.source.url) + case .failure(let error): + completionHandler?(nil, error as NSError, .none, nil) + } + } + } +} +#endif + +#if os(macOS) +// MARK: - Deprecated +extension KingfisherWrapper where Base: NSButton { + @discardableResult + @available(*, deprecated, message: "Use `Result` based callback instead.") + public func setImage(with resource: Resource?, + placeholder: Image? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler?) -> DownloadTask? + { + return setImage( + with: resource, + placeholder: placeholder, + options: options, + progressBlock: progressBlock) + { + result in + switch result { + case .success(let value): + completionHandler?(value.image, nil, value.cacheType, value.source.url) + case .failure(let error): + completionHandler?(nil, error as NSError, .none, nil) + } + } + } + + @discardableResult + @available(*, deprecated, message: "Use `Result` based callback instead.") + public func setAlternateImage(with resource: Resource?, + placeholder: Image? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: CompletionHandler?) -> DownloadTask? + { + return setAlternateImage( + with: resource, + placeholder: placeholder, + options: options, + progressBlock: progressBlock) + { + result in + switch result { + case .success(let value): + completionHandler?(value.image, nil, value.cacheType, value.source.url) + case .failure(let error): + completionHandler?(nil, error as NSError, .none, nil) + } + } + } +} +#endif + +// MARK: - Deprecated +extension ImageCache { + /// The largest cache cost of memory cache. The total cost is pixel count of + /// all cached images in memory. + /// Default is unlimited. Memory cache will be purged automatically when a + /// memory warning notification is received. + @available(*, deprecated, message: "Use `memoryStorage.config.totalCostLimit` instead.", + renamed: "memoryStorage.config.totalCostLimit") + open var maxMemoryCost: Int { + get { return memoryStorage.config.totalCostLimit } + set { memoryStorage.config.totalCostLimit = newValue } + } + + /// The default DiskCachePathClosure + @available(*, deprecated, message: "Not needed anymore.") + public final class func defaultDiskCachePathClosure(path: String?, cacheName: String) -> String { + let dstPath = path ?? NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true).first! + return (dstPath as NSString).appendingPathComponent(cacheName) + } + + /// The default file extension appended to cached files. + @available(*, deprecated, message: "Use `diskStorage.config.pathExtension` instead.", + renamed: "diskStorage.config.pathExtension") + open var pathExtension: String? { + get { return diskStorage.config.pathExtension } + set { diskStorage.config.pathExtension = newValue } + } + + ///The disk cache location. + @available(*, deprecated, message: "Use `diskStorage.directoryURL.absoluteString` instead.", + renamed: "diskStorage.directoryURL.absoluteString") + public var diskCachePath: String { + return diskStorage.directoryURL.absoluteString + } + + /// The largest disk size can be taken for the cache. It is the total + /// allocated size of cached files in bytes. + /// Default is no limit. + @available(*, deprecated, message: "Use `diskStorage.config.sizeLimit` instead.", + renamed: "diskStorage.config.sizeLimit") + open var maxDiskCacheSize: UInt { + get { return UInt(diskStorage.config.sizeLimit) } + set { diskStorage.config.sizeLimit = newValue } + } + + @available(*, deprecated, message: "Use `diskStorage.cacheFileURL(forKey:).path` instead.", + renamed: "diskStorage.cacheFileURL(forKey:)") + open func cachePath(forComputedKey key: String) -> String { + return diskStorage.cacheFileURL(forKey: key).path + } + + /** + Get an image for a key from disk. + + - parameter key: Key for the image. + - parameter options: Options of retrieving image. If you need to retrieve an image which was + stored with a specified `ImageProcessor`, pass the processor in the option too. + + - returns: The image object if it is cached, or `nil` if there is no such key in the cache. + */ + @available(*, deprecated, + message: "Use `Result` based `retrieveImageInDiskCache(forKey:options:callbackQueue:completionHandler:)` instead.", + renamed: "retrieveImageInDiskCache(forKey:options:callbackQueue:completionHandler:)") + open func retrieveImageInDiskCache(forKey key: String, options: KingfisherOptionsInfo? = nil) -> Image? { + let options = options ?? .empty + let computedKey = key.computedKey(with: options.processor.identifier) + do { + if let data = try diskStorage.value(forKey: computedKey) { + return options.cacheSerializer.image(with: data, options: options) + } + } catch {} + return nil + } + + @available(*, deprecated, + message: "Use `Result` based `retrieveImage(forKey:options:callbackQueue:completionHandler:)` instead.", + renamed: "retrieveImage(forKey:options:callbackQueue:completionHandler:)") + open func retrieveImage(forKey key: String, + options: KingfisherOptionsInfo?, + completionHandler: ((Image?, CacheType) -> Void)?) + { + retrieveImage( + forKey: key, + options: options, + callbackQueue: .dispatch((options ?? .empty).callbackDispatchQueue)) + { + result in + do { + let value = try result.get() + completionHandler?(value.image, value.cacheType) + } catch { + completionHandler?(nil, .none) + } + } + } + + /// The longest time duration in second of the cache being stored in disk. + /// Default is 1 week (60 * 60 * 24 * 7 seconds). + /// Setting this to a negative value will make the disk cache never expiring. + @available(*, deprecated, message: "Deprecated. Use `diskStorage.config.expiration` instead") + open var maxCachePeriodInSecond: TimeInterval { + get { return diskStorage.config.expiration.timeInterval } + set { diskStorage.config.expiration = newValue < 0 ? .never : .seconds(newValue) } + } + + @available(*, deprecated, message: "Use `Result` based callback instead.") + open func store(_ image: Image, + original: Data? = nil, + forKey key: String, + processorIdentifier identifier: String = "", + cacheSerializer serializer: CacheSerializer = DefaultCacheSerializer.default, + toDisk: Bool = true, + completionHandler: (() -> Void)?) + { + store( + image, + original: original, + forKey: key, + processorIdentifier: identifier, + cacheSerializer: serializer, + toDisk: toDisk) + { + _ in + completionHandler?() + } + } + + @available(*, deprecated, message: "Use the `Result`-based `calculateDiskStorageSize` instead.") + open func calculateDiskCacheSize(completion handler: @escaping ((_ size: UInt) -> Void)) { + calculateDiskStorageSize { result in + let size: UInt? = try? result.get() + handler(size ?? 0) + } + } +} + +// MARK: - Deprecated +extension Collection where Iterator.Element == KingfisherOptionsInfoItem { + /// The queue of callbacks should happen from Kingfisher. + @available(*, deprecated, message: "Use `callbackQueue` instead.", renamed: "callbackQueue") + public var callbackDispatchQueue: DispatchQueue { + return KingfisherParsedOptionsInfo(Array(self)).callbackQueue.queue + } +} + +/// Error domain of Kingfisher +@available(*, deprecated, message: "Use `KingfisherError.domain` instead.", renamed: "KingfisherError.domain") +public let KingfisherErrorDomain = "com.onevcat.Kingfisher.Error" + +/// Key will be used in the `userInfo` of `.invalidStatusCode` +@available(*, unavailable, +message: "Use `.invalidHTTPStatusCode` or `isInvalidResponseStatusCode` of `KingfisherError` instead for the status code.") +public let KingfisherErrorStatusCodeKey = "statusCode" + +// MARK: - Deprecated +extension Collection where Iterator.Element == KingfisherOptionsInfoItem { + /// The target `ImageCache` which is used. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `targetCache` instead.") + public var targetCache: ImageCache? { + return KingfisherParsedOptionsInfo(Array(self)).targetCache + } + + /// The original `ImageCache` which is used. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `originalCache` instead.") + public var originalCache: ImageCache? { + return KingfisherParsedOptionsInfo(Array(self)).originalCache + } + + /// The `ImageDownloader` which is specified. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `downloader` instead.") + public var downloader: ImageDownloader? { + return KingfisherParsedOptionsInfo(Array(self)).downloader + } + + /// Member for animation transition when using UIImageView. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `transition` instead.") + public var transition: ImageTransition { + return KingfisherParsedOptionsInfo(Array(self)).transition + } + + /// A `Float` value set as the priority of image download task. The value for it should be + /// between 0.0~1.0. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `downloadPriority` instead.") + public var downloadPriority: Float { + return KingfisherParsedOptionsInfo(Array(self)).downloadPriority + } + + /// Whether an image will be always downloaded again or not. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `forceRefresh` instead.") + public var forceRefresh: Bool { + return KingfisherParsedOptionsInfo(Array(self)).forceRefresh + } + + /// Whether an image should be got only from memory cache or download. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `fromMemoryCacheOrRefresh` instead.") + public var fromMemoryCacheOrRefresh: Bool { + return KingfisherParsedOptionsInfo(Array(self)).fromMemoryCacheOrRefresh + } + + /// Whether the transition should always happen or not. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `forceTransition` instead.") + public var forceTransition: Bool { + return KingfisherParsedOptionsInfo(Array(self)).forceTransition + } + + /// Whether cache the image only in memory or not. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `cacheMemoryOnly` instead.") + public var cacheMemoryOnly: Bool { + return KingfisherParsedOptionsInfo(Array(self)).cacheMemoryOnly + } + + /// Whether the caching operation will be waited or not. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `waitForCache` instead.") + public var waitForCache: Bool { + return KingfisherParsedOptionsInfo(Array(self)).waitForCache + } + + /// Whether only load the images from cache or not. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `onlyFromCache` instead.") + public var onlyFromCache: Bool { + return KingfisherParsedOptionsInfo(Array(self)).onlyFromCache + } + + /// Whether the image should be decoded in background or not. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `backgroundDecode` instead.") + public var backgroundDecode: Bool { + return KingfisherParsedOptionsInfo(Array(self)).backgroundDecode + } + + /// Whether the image data should be all loaded at once if it is an animated image. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `preloadAllAnimationData` instead.") + public var preloadAllAnimationData: Bool { + return KingfisherParsedOptionsInfo(Array(self)).preloadAllAnimationData + } + + /// The `CallbackQueue` on which completion handler should be invoked. + /// If not set in the options, `.mainCurrentOrAsync` will be used. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `callbackQueue` instead.") + public var callbackQueue: CallbackQueue { + return KingfisherParsedOptionsInfo(Array(self)).callbackQueue + } + + /// The scale factor which should be used for the image. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `scaleFactor` instead.") + public var scaleFactor: CGFloat { + return KingfisherParsedOptionsInfo(Array(self)).scaleFactor + } + + /// The `ImageDownloadRequestModifier` will be used before sending a download request. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `requestModifier` instead.") + public var modifier: ImageDownloadRequestModifier? { + return KingfisherParsedOptionsInfo(Array(self)).requestModifier + } + + /// `ImageProcessor` for processing when the downloading finishes. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `processor` instead.") + public var processor: ImageProcessor { + return KingfisherParsedOptionsInfo(Array(self)).processor + } + + /// `ImageModifier` for modifying right before the image is displayed. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `imageModifier` instead.") + public var imageModifier: ImageModifier? { + return KingfisherParsedOptionsInfo(Array(self)).imageModifier + } + + /// `CacheSerializer` to convert image to data for storing in cache. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `cacheSerializer` instead.") + public var cacheSerializer: CacheSerializer { + return KingfisherParsedOptionsInfo(Array(self)).cacheSerializer + } + + /// Keep the existing image while setting another image to an image view. + /// Or the placeholder will be used while downloading. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `keepCurrentImageWhileLoading` instead.") + public var keepCurrentImageWhileLoading: Bool { + return KingfisherParsedOptionsInfo(Array(self)).keepCurrentImageWhileLoading + } + + /// Whether the options contains `.onlyLoadFirstFrame`. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `onlyLoadFirstFrame` instead.") + public var onlyLoadFirstFrame: Bool { + return KingfisherParsedOptionsInfo(Array(self)).onlyLoadFirstFrame + } + + /// Whether the options contains `.cacheOriginalImage`. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `cacheOriginalImage` instead.") + public var cacheOriginalImage: Bool { + return KingfisherParsedOptionsInfo(Array(self)).cacheOriginalImage + } + + /// The image which should be used when download image request fails. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `onFailureImage` instead.") + public var onFailureImage: Optional<Image?> { + return KingfisherParsedOptionsInfo(Array(self)).onFailureImage + } + + /// Whether the `ImagePrefetcher` should load images to memory in an aggressive way or not. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `alsoPrefetchToMemory` instead.") + public var alsoPrefetchToMemory: Bool { + return KingfisherParsedOptionsInfo(Array(self)).alsoPrefetchToMemory + } + + /// Whether the disk storage file loading should happen in a synchronous behavior or not. + @available(*, deprecated, + message: "Create a `KingfisherParsedOptionsInfo` from `KingfisherOptionsInfo` and use `loadDiskFileSynchronously` instead.") + public var loadDiskFileSynchronously: Bool { + return KingfisherParsedOptionsInfo(Array(self)).loadDiskFileSynchronously + } +} + +/// The default modifier. +/// It does nothing and returns the image as is. +@available(*, deprecated, message: "Use `nil` in KingfisherOptionsInfo to indicate no modifier.") +public struct DefaultImageModifier: ImageModifier { + + /// A default `DefaultImageModifier` which can be used everywhere. + public static let `default` = DefaultImageModifier() + private init() {} + + /// Modifies an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: Image) -> Image { return image } +} diff --git a/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift b/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift new file mode 100644 index 0000000..c387409 --- /dev/null +++ b/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift @@ -0,0 +1,145 @@ +// +// ImageDataProvider.swift +// Kingfisher +// +// Created by onevcat on 2018/11/13. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a data provider to provide image data to Kingfisher when setting with +/// `Source.provider` source. Compared to `Source.network` member, it gives a chance +/// to load some image data in your own way, as long as you can provide the data +/// representation for the image. +public protocol ImageDataProvider { + + /// The key used in cache. + var cacheKey: String { get } + + /// Provides the data which represents image. Kingfisher uses the data you pass in the + /// handler to process images and caches it for later use. + /// + /// - Parameter handler: The handler you should call when you prepared your data. + /// If the data is loaded successfully, call the handler with + /// a `.success` with the data associated. Otherwise, call it + /// with a `.failure` and pass the error. + /// + /// - Note: + /// If the `handler` is called with a `.failure` with error, a `dataProviderError` of + /// `ImageSettingErrorReason` will be finally thrown out to you as the `KingfisherError` + /// from the framework. + func data(handler: @escaping (Result<Data, Error>) -> Void) +} + +/// Represents an image data provider for loading from a local file URL on disk. +/// Uses this type for adding a disk image to Kingfisher. Compared to loading it +/// directly, you can get benefit of using Kingfisher's extension methods, as well +/// as applying `ImageProcessor`s and storing the image to `ImageCache` of Kingfisher. +public struct LocalFileImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + + /// The file URL from which the image be loaded. + public let fileURL: URL + + // MARK: Initializers + + /// Creates an image data provider by supplying the target local file URL. + /// + /// - Parameters: + /// - fileURL: The file URL from which the image be loaded. + /// - cacheKey: The key is used for caching the image data. By default, + /// the `absoluteString` of `fileURL` is used. + public init(fileURL: URL, cacheKey: String? = nil) { + self.fileURL = fileURL + self.cacheKey = cacheKey ?? fileURL.absoluteString + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: (Result<Data, Error>) -> Void) { + handler(Result(catching: { try Data(contentsOf: fileURL) })) + } +} + +/// Represents an image data provider for loading image from a given Base64 encoded string. +public struct Base64ImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + /// The encoded Base64 string for the image. + public let base64String: String + + // MARK: Initializers + + /// Creates an image data provider by supplying the Base64 encoded string. + /// + /// - Parameters: + /// - base64String: The Base64 encoded string for an image. + /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. + public init(base64String: String, cacheKey: String) { + self.base64String = base64String + self.cacheKey = cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: (Result<Data, Error>) -> Void) { + let data = Data(base64Encoded: base64String)! + handler(.success(data)) + } +} + +/// Represents an image data provider for a raw data object. +public struct RawImageDataProvider: ImageDataProvider { + + // MARK: Public Properties + + /// The raw data object to provide to Kingfisher image loader. + public let data: Data + + // MARK: Initializers + + /// Creates an image data provider by the given raw `data` value and a `cacheKey` be used in Kingfisher cache. + /// + /// - Parameters: + /// - data: The raw data reprensents an image. + /// - cacheKey: The key is used for caching the image data. You need a different key for any different image. + public init(data: Data, cacheKey: String) { + self.data = data + self.cacheKey = cacheKey + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler: @escaping (Result<Data, Error>) -> Void) { + handler(.success(data)) + } +} diff --git a/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift b/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift new file mode 100644 index 0000000..f12d8a9 --- /dev/null +++ b/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift @@ -0,0 +1,74 @@ +// +// Resource.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents an image resource at a certain url and a given cache key. +/// Kingfisher will use a `Resource` to download a resource from network and cache it with the cache key when +/// using `Source.network` as its image setting source. +public protocol Resource { + + /// The key used in cache. + var cacheKey: String { get } + + /// The target image URL. + var downloadURL: URL { get } +} + +/// ImageResource is a simple combination of `downloadURL` and `cacheKey`. +/// When passed to image view set methods, Kingfisher will try to download the target +/// image from the `downloadURL`, and then store it with the `cacheKey` as the key in cache. +public struct ImageResource: Resource { + + // MARK: - Initializers + + /// Creates an image resource. + /// + /// - Parameters: + /// - downloadURL: The target image URL from where the image can be downloaded. + /// - cacheKey: The cache key. If `nil`, Kingfisher will use the `absoluteString` of `downloadURL` as the key. + /// Default is `nil`. + public init(downloadURL: URL, cacheKey: String? = nil) { + self.downloadURL = downloadURL + self.cacheKey = cacheKey ?? downloadURL.absoluteString + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public let cacheKey: String + + /// The target image URL. + public let downloadURL: URL +} + +/// URL conforms to `Resource` in Kingfisher. +/// The `absoluteString` of this URL is used as `cacheKey`. And the URL itself will be used as `downloadURL`. +/// If you need customize the url and/or cache key, use `ImageResource` instead. +extension URL: Resource { + public var cacheKey: String { return absoluteString } + public var downloadURL: URL { return self } +} diff --git a/Pods/Kingfisher/Sources/General/ImageSource/Source.swift b/Pods/Kingfisher/Sources/General/ImageSource/Source.swift new file mode 100644 index 0000000..dd922a2 --- /dev/null +++ b/Pods/Kingfisher/Sources/General/ImageSource/Source.swift @@ -0,0 +1,98 @@ +// +// Source.swift +// Kingfisher +// +// Created by onevcat on 2018/11/17. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents an image setting source for Kingfisher methods. +/// +/// A `Source` value indicates the way how the target image can be retrieved and cached. +/// +/// - network: The target image should be got from network remotely. The associated `Resource` +/// value defines detail information like image URL and cache key. +/// - provider: The target image should be provided in a data format. Normally, it can be an image +/// from local storage or in any other encoding format (like Base64). +public enum Source { + + /// Represents the source task identifier when setting an image to a view with extension methods. + public enum Identifier { + + /// The underlying value type of source identifier. + public typealias Value = UInt + static var current: Value = 0 + static func next() -> Value { + current += 1 + return current + } + } + + // MARK: Member Cases + + /// The target image should be got from network remotely. The associated `Resource` + /// value defines detail information like image URL and cache key. + case network(Resource) + + /// The target image should be provided in a data format. Normally, it can be an image + /// from local storage or in any other encoding format (like Base64). + case provider(ImageDataProvider) + + // MARK: Getting Properties + + /// The cache key defined for this source value. + public var cacheKey: String { + switch self { + case .network(let resource): return resource.cacheKey + case .provider(let provider): return provider.cacheKey + } + } + + /// The URL defined for this source value. + /// + /// For a `.network` source, it is the `downloadURL` of associated `Resource` instance. + /// For a `.provider` value, it is always `nil`. + public var url: URL? { + switch self { + case .network(let resource): return resource.downloadURL + // `ImageDataProvider` does not provide a URL. All it cares is how to get the data back. + case .provider(_): return nil + } + } +} + +extension Source { + var asResource: Resource? { + guard case .network(let resource) = self else { + return nil + } + return resource + } + + var asProvider: ImageDataProvider? { + guard case .provider(let provider) = self else { + return nil + } + return provider + } +} diff --git a/Pods/Kingfisher/Sources/General/Kingfisher.swift b/Pods/Kingfisher/Sources/General/Kingfisher.swift new file mode 100644 index 0000000..4098401 --- /dev/null +++ b/Pods/Kingfisher/Sources/General/Kingfisher.swift @@ -0,0 +1,89 @@ +// +// Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 16/9/14. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import ImageIO + +#if os(macOS) +import AppKit +public typealias Image = NSImage +public typealias View = NSView +public typealias Color = NSColor +public typealias ImageView = NSImageView +public typealias Button = NSButton +#else +import UIKit +public typealias Image = UIImage +public typealias Color = UIColor +#if !os(watchOS) +public typealias ImageView = UIImageView +public typealias View = UIView +public typealias Button = UIButton +#else +import WatchKit +#endif +#endif + +/// Wrapper for Kingfisher compatible types. This type provides an extension point for +/// connivence methods in Kingfisher. +public struct KingfisherWrapper<Base> { + public let base: Base + public init(_ base: Base) { + self.base = base + } +} + +/// Represents an object type that is compatible with Kingfisher. You can use `kf` property to get a +/// value in the namespace of Kingfisher. +public protocol KingfisherCompatible: AnyObject { } + +/// Represents a value type that is compatible with Kingfisher. You can use `kf` property to get a +/// value in the namespace of Kingfisher. +public protocol KingfisherCompatibleValue {} + +extension KingfisherCompatible { + /// Gets a namespace holder for Kingfisher compatible types. + public var kf: KingfisherWrapper<Self> { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension KingfisherCompatibleValue { + /// Gets a namespace holder for Kingfisher compatible types. + public var kf: KingfisherWrapper<Self> { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension Image: KingfisherCompatible { } +#if !os(watchOS) +extension ImageView: KingfisherCompatible { } +extension Button: KingfisherCompatible { } +#else +extension WKInterfaceImage: KingfisherCompatible { } +#endif diff --git a/Pods/Kingfisher/Sources/General/KingfisherError.swift b/Pods/Kingfisher/Sources/General/KingfisherError.swift new file mode 100644 index 0000000..2add785 --- /dev/null +++ b/Pods/Kingfisher/Sources/General/KingfisherError.swift @@ -0,0 +1,400 @@ +// +// KingfisherError.swift +// Kingfisher +// +// Created by onevcat on 2018/09/26. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension Never {} + +/// Represents all the errors which can happen in Kingfisher framework. +/// Kingfisher related methods always throw a `KingfisherError` or invoke the callback with `KingfisherError` +/// as its error type. To handle errors from Kingfisher, you switch over the error to get a reason catalog, +/// then switch over the reason to know error detail. +public enum KingfisherError: Error { + + // MARK: Error Reason Types + + /// Represents the error reason during networking request phase. + /// + /// - emptyRequest: The request is empty. Code 1001. + /// - invalidURL: The URL of request is invalid. Code 1002. + /// - taskCancelled: The downloading task is cancelled by user. Code 1003. + public enum RequestErrorReason { + + /// The request is empty. Code 1001. + case emptyRequest + + /// The URL of request is invalid. Code 1002. + /// - request: The request is tend to be sent but its URL is invalid. + case invalidURL(request: URLRequest) + + /// The downloading task is cancelled by user. Code 1003. + /// - task: The session data task which is cancelled. + /// - token: The cancel token which is used for cancelling the task. + case taskCancelled(task: SessionDataTask, token: SessionDataTask.CancelToken) + } + + /// Represents the error reason during networking response phase. + /// + /// - invalidURLResponse: The response is not a valid URL response. Code 2001. + /// - invalidHTTPStatusCode: The response contains an invalid HTTP status code. Code 2002. + /// - URLSessionError: An error happens in the system URL session. Code 2003. + /// - dataModifyingFailed: Data modifying fails on returning a valid data. Code 2004. + /// - noURLResponse: The task is done but no URL response found. Code 2005. + public enum ResponseErrorReason { + + /// The response is not a valid URL response. Code 2001. + /// - response: The received invalid URL response. + /// The response is expected to be an HTTP response, but it is not. + case invalidURLResponse(response: URLResponse) + + /// The response contains an invalid HTTP status code. Code 2002. + /// - Note: + /// By default, status code 200..<400 is recognized as valid. You can override + /// this behavior by conforming to the `ImageDownloaderDelegate`. + /// - response: The received response. + case invalidHTTPStatusCode(response: HTTPURLResponse) + + /// An error happens in the system URL session. Code 2003. + /// - error: The underlying URLSession error object. + case URLSessionError(error: Error) + + /// Data modifying fails on returning a valid data. Code 2004. + /// - task: The failed task. + case dataModifyingFailed(task: SessionDataTask) + + /// The task is done but no URL response found. Code 2005. + /// - task: The failed task. + case noURLResponse(task: SessionDataTask) + } + + /// Represents the error reason during Kingfisher caching system. + /// + /// - fileEnumeratorCreationFailed: Cannot create a file enumerator for a certain disk URL. Code 3001. + /// - invalidFileEnumeratorContent: Cannot get correct file contents from a file enumerator. Code 3002. + /// - invalidURLResource: The file at target URL exists, but its URL resource is unavailable. Code 3003. + /// - cannotLoadDataFromDisk: The file at target URL exists, but the data cannot be loaded from it. Code 3004. + /// - cannotCreateDirectory: Cannot create a folder at a given path. Code 3005. + /// - imageNotExisting: The requested image does not exist in cache. Code 3006. + /// - cannotConvertToData: Cannot convert an object to data for storing. Code 3007. + /// - cannotSerializeImage: Cannot serialize an image to data for storing. Code 3008. + public enum CacheErrorReason { + + /// Cannot create a file enumerator for a certain disk URL. Code 3001. + /// - url: The target disk URL from which the file enumerator should be created. + case fileEnumeratorCreationFailed(url: URL) + + /// Cannot get correct file contents from a file enumerator. Code 3002. + /// - url: The target disk URL from which the content of a file enumerator should be got. + case invalidFileEnumeratorContent(url: URL) + + /// The file at target URL exists, but its URL resource is unavailable. Code 3003. + /// - error: The underlying error thrown by file manager. + /// - key: The key used to getting the resource from cache. + /// - url: The disk URL where the target cached file exists. + case invalidURLResource(error: Error, key: String, url: URL) + + /// The file at target URL exists, but the data cannot be loaded from it. Code 3004. + /// - url: The disk URL where the target cached file exists. + /// - error: The underlying error which describes why this error happens. + case cannotLoadDataFromDisk(url: URL, error: Error) + + /// Cannot create a folder at a given path. Code 3005. + /// - path: The disk path where the directory creating operation fails. + /// - error: The underlying error which describes why this error happens. + case cannotCreateDirectory(path: String, error: Error) + + /// The requested image does not exist in cache. Code 3006. + /// - key: Key of the requested image in cache. + case imageNotExisting(key: String) + + /// Cannot convert an object to data for storing. Code 3007. + /// - object: The object which needs be convert to data. + case cannotConvertToData(object: Any, error: Error) + + /// Cannot serialize an image to data for storing. Code 3008. + /// - image: The input image needs to be serialized to cache. + /// - original: The original image data, if exists. + /// - serializer: The `CacheSerializer` used for the image serializing. + case cannotSerializeImage(image: Image?, original: Data?, serializer: CacheSerializer) + } + + + /// Represents the error reason during image processing phase. + /// + /// - processingFailed: Image processing fails. There is no valid output image from the processor. Code 4001. + public enum ProcessorErrorReason { + /// Image processing fails. There is no valid output image from the processor. Code 4001. + /// - processor: The `ImageProcessor` used to process the image or its data in `item`. + /// - item: The image or its data content. + case processingFailed(processor: ImageProcessor, item: ImageProcessItem) + } + + /// Represents the error reason during image setting in a view related class. + /// + /// - emptySource: The input resource is empty or `nil`. Code 5001. + /// - notCurrentSourceTask: The source task is finished, but it is not the one expected now. Code 5002. + /// - dataProviderError: An error happens during getting data from an `ImageDataProvider`. Code 5003. + public enum ImageSettingErrorReason { + + /// The input resource is empty or `nil`. Code 5001. + case emptySource + + /// The resource task is finished, but it is not the one expected now. This usually happens when you set another + /// resource on the view without cancelling the current on-going one. The previous setting task will fail with + /// this `.notCurrentSourceTask` error when a result got, regardless of it being successful or not for that task. + /// The result of this original task is contained in the associated value. + /// Code 5002. + /// - result: The `RetrieveImageResult` if the source task is finished without problem. `nil` if an error + /// happens. + /// - error: The `Error` if an issue happens during image setting task. `nil` if the task finishes without + /// problem. + /// - source: The original source value of the taks. + case notCurrentSourceTask(result: RetrieveImageResult?, error: Error?, source: Source) + + /// An error happens during getting data from an `ImageDataProvider`. Code 5003. + case dataProviderError(provider: ImageDataProvider, error: Error) + } + + // MARK: Member Cases + + /// Represents the error reason during networking request phase. + case requestError(reason: RequestErrorReason) + /// Represents the error reason during networking response phase. + case responseError(reason: ResponseErrorReason) + /// Represents the error reason during Kingfisher caching system. + case cacheError(reason: CacheErrorReason) + /// Represents the error reason during image processing phase. + case processorError(reason: ProcessorErrorReason) + /// Represents the error reason during image setting in a view related class. + case imageSettingError(reason: ImageSettingErrorReason) + + // MARK: Helper Properties & Methods + + /// Helper property to check whether this error is a `RequestErrorReason.taskCancelled` or not. + public var isTaskCancelled: Bool { + if case .requestError(reason: .taskCancelled) = self { + return true + } + return false + } + + /// Helper method to check whether this error is a `ResponseErrorReason.invalidHTTPStatusCode` and the + /// associated value is a given status code. + /// + /// - Parameter code: The given status code. + /// - Returns: If `self` is a `ResponseErrorReason.invalidHTTPStatusCode` error + /// and its status code equals to `code`, `true` is returned. Otherwise, `false`. + public func isInvalidResponseStatusCode(_ code: Int) -> Bool { + if case .responseError(reason: .invalidHTTPStatusCode(let response)) = self { + return response.statusCode == code + } + return false + } + + public var isInvalidResponseStatusCode: Bool { + if case .responseError(reason: .invalidHTTPStatusCode) = self { + return true + } + return false + } + + /// Helper property to check whether this error is a `ImageSettingErrorReason.notCurrentSourceTask` or not. + /// When a new image setting task starts while the old one is still running, the new task identifier will be + /// set and the old one is overwritten. A `.notCurrentSourceTask` error will be raised when the old task finishes + /// to let you know the setting process finishes with a certain result, but the image view or button is not set. + public var isNotCurrentTask: Bool { + if case .imageSettingError(reason: .notCurrentSourceTask(_, _, _)) = self { + return true + } + return false + } +} + +// MARK: - LocalizedError Conforming +extension KingfisherError: LocalizedError { + + /// A localized message describing what error occurred. + public var errorDescription: String? { + switch self { + case .requestError(let reason): return reason.errorDescription + case .responseError(let reason): return reason.errorDescription + case .cacheError(let reason): return reason.errorDescription + case .processorError(let reason): return reason.errorDescription + case .imageSettingError(let reason): return reason.errorDescription + } + } +} + + +// MARK: - CustomNSError Conforming +extension KingfisherError: CustomNSError { + + /// The error domain of `KingfisherError`. All errors from Kingfisher is under this domain. + public static let domain = "com.onevcat.Kingfisher.Error" + + /// The error code within the given domain. + public var errorCode: Int { + switch self { + case .requestError(let reason): return reason.errorCode + case .responseError(let reason): return reason.errorCode + case .cacheError(let reason): return reason.errorCode + case .processorError(let reason): return reason.errorCode + case .imageSettingError(let reason): return reason.errorCode + } + } +} + +extension KingfisherError.RequestErrorReason { + var errorDescription: String? { + switch self { + case .emptyRequest: + return "The request is empty or `nil`." + case .invalidURL(let request): + return "The request contains an invalid or empty URL. Request: \(request)." + case .taskCancelled(let task, let token): + return "The session task was cancelled. Task: \(task), cancel token: \(token)." + } + } + + var errorCode: Int { + switch self { + case .emptyRequest: return 1001 + case .invalidURL: return 1002 + case .taskCancelled: return 1003 + } + } +} + +extension KingfisherError.ResponseErrorReason { + var errorDescription: String? { + switch self { + case .invalidURLResponse(let response): + return "The URL response is invalid: \(response)" + case .invalidHTTPStatusCode(let response): + return "The HTTP status code in response is invalid. Code: \(response.statusCode), response: \(response)." + case .URLSessionError(let error): + return "A URL session error happened. The underlying error: \(error)" + case .dataModifyingFailed(let task): + return "The data modifying delegate returned `nil` for the downloaded data. Task: \(task)." + case .noURLResponse(let task): + return "No URL response received. Task: \(task)," + } + } + + var errorCode: Int { + switch self { + case .invalidURLResponse: return 2001 + case .invalidHTTPStatusCode: return 2002 + case .URLSessionError: return 2003 + case .dataModifyingFailed: return 2004 + case .noURLResponse: return 2005 + } + } +} + +extension KingfisherError.CacheErrorReason { + var errorDescription: String? { + switch self { + case .fileEnumeratorCreationFailed(let url): + return "Cannot create file enumerator for URL: \(url)." + case .invalidFileEnumeratorContent(let url): + return "Cannot get contents from the file enumerator at URL: \(url)." + case .invalidURLResource(let error, let key, let url): + return "Cannot get URL resource values or data for the given URL: \(url). " + + "Cache key: \(key). Underlying error: \(error)" + case .cannotLoadDataFromDisk(let url, let error): + return "Cannot load data from disk at URL: \(url). Underlying error: \(error)" + case .cannotCreateDirectory(let path, let error): + return "Cannot create directory at given path: Path: \(path). Underlying error: \(error)" + case .imageNotExisting(let key): + return "The image is not in cache, but you requires it should only be " + + "from cache by enabling the `.onlyFromCache` option. Key: \(key)." + case .cannotConvertToData(let object, let error): + return "Cannot convert the input object to a `Data` object when storing it to disk cache. " + + "Object: \(object). Underlying error: \(error)" + case .cannotSerializeImage(let image, let originalData, let serializer): + return "Cannot serialize an image due to the cache serializer returning `nil`. " + + "Image: \(String(describing:image)), original data: \(String(describing: originalData)), serializer: \(serializer)." + } + } + + var errorCode: Int { + switch self { + case .fileEnumeratorCreationFailed: return 3001 + case .invalidFileEnumeratorContent: return 3002 + case .invalidURLResource: return 3003 + case .cannotLoadDataFromDisk: return 3004 + case .cannotCreateDirectory: return 3005 + case .imageNotExisting: return 3006 + case .cannotConvertToData: return 3007 + case .cannotSerializeImage: return 3008 + } + } +} + +extension KingfisherError.ProcessorErrorReason { + var errorDescription: String? { + switch self { + case .processingFailed(let processor, let item): + return "Processing image failed. Processor: \(processor). Processing item: \(item)." + } + } + + var errorCode: Int { + switch self { + case .processingFailed: return 4001 + } + } +} + +extension KingfisherError.ImageSettingErrorReason { + var errorDescription: String? { + switch self { + case .emptySource: + return "The input resource is empty." + case .notCurrentSourceTask(let result, let error, let resource): + if let result = result { + return "Retrieving resource succeeded, but this source is " + + "not the one currently expected. Result: \(result). Resource: \(resource)." + } else if let error = error { + return "Retrieving resource failed, and this resource is " + + "not the one currently expected. Error: \(error). Resource: \(resource)." + } else { + return nil + } + case .dataProviderError(let provider, let error): + return "Image data provider fails to provide data. Provider: \(provider), error: \(error)" + } + } + + var errorCode: Int { + switch self { + case .emptySource: return 5001 + case .notCurrentSourceTask: return 5002 + case .dataProviderError: return 5003 + } + } +} diff --git a/Pods/Kingfisher/Sources/General/KingfisherManager.swift b/Pods/Kingfisher/Sources/General/KingfisherManager.swift new file mode 100644 index 0000000..69473bb --- /dev/null +++ b/Pods/Kingfisher/Sources/General/KingfisherManager.swift @@ -0,0 +1,430 @@ +// +// KingfisherManager.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +import Foundation + +/// The downloading progress block type. +/// The parameter value is the `receivedSize` of current response. +/// The second parameter is the total expected data length from response's "Content-Length" header. +/// If the expected length is not available, this block will not be called. +public typealias DownloadProgressBlock = ((_ receivedSize: Int64, _ totalSize: Int64) -> Void) + +/// Represents the result of a Kingfisher retrieving image task. +public struct RetrieveImageResult { + + /// Gets the image object of this result. + public let image: Image + + /// Gets the cache source of the image. It indicates from which layer of cache this image is retrieved. + /// If the image is just downloaded from network, `.none` will be returned. + public let cacheType: CacheType + + /// The `Source` from which the retrieve task begins. + public let source: Source +} + +/// Main manager class of Kingfisher. It connects Kingfisher downloader and cache, +/// to provide a set of convenience methods to use Kingfisher for tasks. +/// You can use this class to retrieve an image via a specified URL from web or cache. +public class KingfisherManager { + + /// Represents a shared manager used across Kingfisher. + /// Use this instance for getting or storing images with Kingfisher. + public static let shared = KingfisherManager() + + // Mark: Public Properties + /// The `ImageCache` used by this manager. It is `ImageCache.default` by default. + /// If a cache is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be + /// used instead. + public var cache: ImageCache + + /// The `ImageDownloader` used by this manager. It is `ImageDownloader.default` by default. + /// If a downloader is specified in `KingfisherManager.defaultOptions`, the value in `defaultOptions` will be + /// used instead. + public var downloader: ImageDownloader + + /// Default options used by the manager. This option will be used in + /// Kingfisher manager related methods, as well as all view extension methods. + /// You can also passing other options for each image task by sending an `options` parameter + /// to Kingfisher's APIs. The per image options will overwrite the default ones, + /// if the option exists in both. + public var defaultOptions = KingfisherOptionsInfo.empty + + // Use `defaultOptions` to overwrite the `downloader` and `cache`. + private var currentDefaultOptions: KingfisherOptionsInfo { + return [.downloader(downloader), .targetCache(cache)] + defaultOptions + } + + private let processingQueue: CallbackQueue + + private convenience init() { + self.init(downloader: .default, cache: .default) + } + + /// Creates an image setting manager with specified downloader and cache. + /// + /// - Parameters: + /// - downloader: The image downloader used to download images. + /// - cache: The image cache which stores memory and disk images. + public init(downloader: ImageDownloader, cache: ImageCache) { + self.downloader = downloader + self.cache = cache + + let processQueueName = "com.onevcat.Kingfisher.KingfisherManager.processQueue.\(UUID().uuidString)" + processingQueue = .dispatch(DispatchQueue(label: processQueueName)) + } + + // Mark: Getting Images + + /// Gets an image from a given resource. + /// + /// - Parameters: + /// - resource: The `Resource` object defines data information like key or URL. + /// - options: Options to use when creating the animated image. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. `progressBlock` is always called in + /// main queue. + /// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked + /// from the `options.callbackQueue`. If not specified, the main queue will be used. + /// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource, + /// the started `DownloadTask` is returned. Otherwise, `nil` is returned. + /// + /// - Note: + /// This method will first check whether the requested `resource` is already in cache or not. If cached, + /// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it + /// will download the `resource`, store it in cache, then call `completionHandler`. + /// + @discardableResult + public func retrieveImage( + with resource: Resource, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask? + { + let source = Source.network(resource) + return retrieveImage( + with: source, options: options, progressBlock: progressBlock, completionHandler: completionHandler + ) + } + + /// Gets an image from a given resource. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - options: Options to use when creating the animated image. + /// - progressBlock: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. `progressBlock` is always called in + /// main queue. + /// - completionHandler: Called when the image retrieved and set finished. This completion handler will be invoked + /// from the `options.callbackQueue`. If not specified, the main queue will be used. + /// - Returns: A task represents the image downloading. If there is a download task starts for `.network` resource, + /// the started `DownloadTask` is returned. Otherwise, `nil` is returned. + /// + /// - Note: + /// This method will first check whether the requested `source` is already in cache or not. If cached, + /// it returns `nil` and invoke the `completionHandler` after the cached image retrieved. Otherwise, it + /// will try to load the `source`, store it in cache, then call `completionHandler`. + /// + public func retrieveImage( + with source: Source, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask? + { + let options = currentDefaultOptions + (options ?? .empty) + var info = KingfisherParsedOptionsInfo(options) + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return retrieveImage( + with: source, + options: info, + completionHandler: completionHandler) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask? + { + if options.forceRefresh { + return loadAndCacheImage( + source: source, + options: options, + completionHandler: completionHandler)?.value + + } else { + let loadedFromCache = retrieveImageFromCache( + source: source, + options: options, + completionHandler: completionHandler) + + if loadedFromCache { + return nil + } + + if options.onlyFromCache { + let error = KingfisherError.cacheError(reason: .imageNotExisting(key: source.cacheKey)) + completionHandler?(.failure(error)) + return nil + } + + return loadAndCacheImage( + source: source, + options: options, + completionHandler: completionHandler)?.value + } + } + + func provideImage( + provider: ImageDataProvider, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)?) + { + guard let completionHandler = completionHandler else { return } + provider.data { result in + switch result { + case .success(let data): + (options.processingQueue ?? self.processingQueue).execute { + let processor = options.processor + let processingItem = ImageProcessItem.data(data) + guard let image = processor.process(item: processingItem, options: options) else { + options.callbackQueue.execute { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: processingItem)) + completionHandler(.failure(error)) + } + return + } + + options.callbackQueue.execute { + let result = ImageLoadingResult(image: image, url: nil, originalData: data) + completionHandler(.success(result)) + } + } + case .failure(let error): + options.callbackQueue.execute { + let error = KingfisherError.imageSettingError( + reason: .dataProviderError(provider: provider, error: error)) + completionHandler(.failure(error)) + } + + } + } + } + + @discardableResult + func loadAndCacheImage( + source: Source, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> DownloadTask.WrappedTask? + { + func cacheImage(_ result: Result<ImageLoadingResult, KingfisherError>) + { + switch result { + case .success(let value): + // Add image to cache. + let targetCache = options.targetCache ?? self.cache + targetCache.store( + value.image, + original: value.originalData, + forKey: source.cacheKey, + options: options, + toDisk: !options.cacheMemoryOnly) + { + _ in + if options.waitForCache { + let result = RetrieveImageResult(image: value.image, cacheType: .none, source: source) + completionHandler?(.success(result)) + } + } + + // Add original image to cache if necessary. + let needToCacheOriginalImage = options.cacheOriginalImage && + options.processor != DefaultImageProcessor.default + if needToCacheOriginalImage { + let originalCache = options.originalCache ?? targetCache + originalCache.storeToDisk( + value.originalData, + forKey: source.cacheKey, + processorIdentifier: DefaultImageProcessor.default.identifier, + expiration: options.diskCacheExpiration) + } + + if !options.waitForCache { + let result = RetrieveImageResult(image: value.image, cacheType: .none, source: source) + completionHandler?(.success(result)) + } + + case .failure(let error): + completionHandler?(.failure(error)) + } + } + + switch source { + case .network(let resource): + let downloader = options.downloader ?? self.downloader + guard let task = downloader.downloadImage( + with: resource.downloadURL, + options: options, + completionHandler: cacheImage) else { + return nil + } + return .download(task) + + case .provider(let provider): + provideImage(provider: provider, options: options, completionHandler: cacheImage) + return .dataProviding + } + } + + /// Retrieves image from memory or disk cache. + /// + /// - Parameters: + /// - source: The target source from which to get image. + /// - key: The key to use when caching the image. + /// - url: Image request URL. This is not used when retrieving image from cache. It is just used for + /// `RetrieveImageResult` callback compatibility. + /// - options: Options on how to get the image from image cache. + /// - completionHandler: Called when the image retrieving finishes, either with succeeded + /// `RetrieveImageResult` or an error. + /// - Returns: `true` if the requested image or the original image before being processed is existing in cache. + /// Otherwise, this method returns `false`. + /// + /// - Note: + /// The image retrieving could happen in either memory cache or disk cache. The `.processor` option in + /// `options` will be considered when searching in the cache. If no processed image is found, Kingfisher + /// will try to check whether an original version of that image is existing or not. If there is already an + /// original, Kingfisher retrieves it from cache and processes it. Then, the processed image will be store + /// back to cache for later use. + func retrieveImageFromCache( + source: Source, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)?) -> Bool + { + // 1. Check whether the image was already in target cache. If so, just get it. + let targetCache = options.targetCache ?? cache + let key = source.cacheKey + let targetImageCached = targetCache.imageCachedType( + forKey: key, processorIdentifier: options.processor.identifier) + + let validCache = targetImageCached.cached && + (options.fromMemoryCacheOrRefresh == false || targetImageCached == .memory) + if validCache { + targetCache.retrieveImage(forKey: key, options: options) { result in + guard let completionHandler = completionHandler else { return } + options.callbackQueue.execute { + result.match( + onSuccess: { cacheResult in + let value: Result<RetrieveImageResult, KingfisherError> + if let image = cacheResult.image { + value = result.map { + RetrieveImageResult(image: image, cacheType: $0.cacheType, source: source) + } + } else { + value = .failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key))) + } + completionHandler(value) + }, + onFailure: { _ in + completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))) + } + ) + } + } + return true + } + + // 2. Check whether the original image exists. If so, get it, process it, save to storage and return. + let originalCache = options.originalCache ?? targetCache + // No need to store the same file in the same cache again. + if originalCache === targetCache && options.processor == DefaultImageProcessor.default { + return false + } + + // Check whether the unprocessed image existing or not. + let originalImageCached = originalCache.imageCachedType( + forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier).cached + if originalImageCached { + // Now we are ready to get found the original image from cache. We need the unprocessed image, so remove + // any processor from options first. + var optionsWithoutProcessor = options + optionsWithoutProcessor.processor = DefaultImageProcessor.default + originalCache.retrieveImage(forKey: key, options: optionsWithoutProcessor) { result in + + result.match( + onSuccess: { cacheResult in + guard let image = cacheResult.image else { + return + } + + let processor = options.processor + (options.processingQueue ?? self.processingQueue).execute { + let item = ImageProcessItem.image(image) + guard let processedImage = processor.process(item: item, options: options) else { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: item)) + options.callbackQueue.execute { completionHandler?(.failure(error)) } + return + } + + var cacheOptions = options + cacheOptions.callbackQueue = .untouch + targetCache.store( + processedImage, + forKey: key, + options: cacheOptions, + toDisk: !options.cacheMemoryOnly) + { + _ in + if options.waitForCache { + let value = RetrieveImageResult(image: processedImage, cacheType: .none, source: source) + options.callbackQueue.execute { completionHandler?(.success(value)) } + } + } + + if !options.waitForCache { + let value = RetrieveImageResult(image: processedImage, cacheType: .none, source: source) + options.callbackQueue.execute { completionHandler?(.success(value)) } + } + } + }, + onFailure: { _ in + // This should not happen actually, since we already confirmed `originalImageCached` is `true`. + // Just in case... + options.callbackQueue.execute { + completionHandler?(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))) + } + } + ) + } + return true + } + + return false + } +} diff --git a/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift b/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift new file mode 100644 index 0000000..f9efb27 --- /dev/null +++ b/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift @@ -0,0 +1,350 @@ +// +// KingfisherOptionsInfo.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/23. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + + +/// KingfisherOptionsInfo is a typealias for [KingfisherOptionsInfoItem]. +/// You can use the enum of option item with value to control some behaviors of Kingfisher. +public typealias KingfisherOptionsInfo = [KingfisherOptionsInfoItem] + +extension Array where Element == KingfisherOptionsInfoItem { + static let empty: KingfisherOptionsInfo = [] +} + +/// Represents the available option items could be used in `KingfisherOptionsInfo`. +public enum KingfisherOptionsInfoItem { + + /// Kingfisher will use the associated `ImageCache` object when handling related operations, + /// including trying to retrieve the cached images and store the downloaded image to it. + case targetCache(ImageCache) + + /// The `ImageCache` for storing and retrieving original images. If `originalCache` is + /// contained in the options, it will be preferred for storing and retrieving original images. + /// If there is no `.originalCache` in the options, `.targetCache` will be used to store original images. + /// + /// When using KingfisherManager to download and store an image, if `cacheOriginalImage` is + /// applied in the option, the original image will be stored to this `originalCache`. At the + /// same time, if a requested final image (with processor applied) cannot be found in `targetCache`, + /// Kingfisher will try to search the original image to check whether it is already there. If found, + /// it will be used and applied with the given processor. It is an optimization for not downloading + /// the same image for multiple times. + case originalCache(ImageCache) + + /// Kingfisher will use the associated `ImageDownloader` object to download the requested images. + case downloader(ImageDownloader) + + /// Member for animation transition when using `UIImageView`. Kingfisher will use the `ImageTransition` of + /// this enum to animate the image in if it is downloaded from web. The transition will not happen when the + /// image is retrieved from either memory or disk cache by default. If you need to do the transition even when + /// the image being retrieved from cache, set `.forceRefresh` as well. + case transition(ImageTransition) + + /// Associated `Float` value will be set as the priority of image download task. The value for it should be + /// between 0.0~1.0. If this option not set, the default value (`URLSessionTask.defaultPriority`) will be used. + case downloadPriority(Float) + + /// If set, Kingfisher will ignore the cache and try to fire a download task for the resource. + case forceRefresh + + /// If set, Kingfisher will try to retrieve the image from memory cache first. If the image is not in memory + /// cache, then it will ignore the disk cache but download the image again from network. This is useful when + /// you want to display a changeable image behind the same url at the same app session, while avoiding download + /// it for multiple times. + case fromMemoryCacheOrRefresh + + /// If set, setting the image to an image view will happen with transition even when retrieved from cache. + /// See `.transition` option for more. + case forceTransition + + /// If set, Kingfisher will only cache the value in memory but not in disk. + case cacheMemoryOnly + + /// If set, Kingfisher will wait for caching operation to be completed before calling the completion block. + case waitForCache + + /// If set, Kingfisher will only try to retrieve the image from cache, but not from network. If the image is + /// not in cache, the image retrieving will fail with an error. + case onlyFromCache + + /// Decode the image in background thread before using. It will decode the downloaded image data and do a off-screen + /// rendering to extract pixel information in background. This can speed up display, but will cost more time to + /// prepare the image for using. + case backgroundDecode + + /// The associated value of this member will be used as the target queue of dispatch callbacks when + /// retrieving images from cache. If not set, Kingfisher will use main queue for callbacks. + @available(*, deprecated, message: "Use `.callbackQueue(CallbackQueue)` instead.") + case callbackDispatchQueue(DispatchQueue?) + + /// The associated value will be used as the target queue of dispatch callbacks when retrieving images from + /// cache. If not set, Kingfisher will use `.mainCurrentOrAsync` for callbacks. + /// + /// - Note: + /// This option does not affect the callbacks for UI related extension methods. You will always get the + /// callbacks called from main queue. + case callbackQueue(CallbackQueue) + + /// The associated value will be used as the scale factor when converting retrieved data to an image. + /// Specify the image scale, instead of your screen scale. You may need to set the correct scale when you dealing + /// with 2x or 3x retina images. Otherwise, Kingfisher will convert the data to image object at `scale` 1.0. + case scaleFactor(CGFloat) + + /// Whether all the animated image data should be preloaded. Default is `false`, which means only following frames + /// will be loaded on need. If `true`, all the animated image data will be loaded and decoded into memory. + /// + /// This option is mainly used for back compatibility internally. You should not set it directly. Instead, + /// you should choose the image view class to control the GIF data loading. There are two classes in Kingfisher + /// support to display a GIF image. `AnimatedImageView` does not preload all data, it takes much less memory, but + /// uses more CPU when display. While a normal image view (`UIImageView` or `NSImageView`) loads all data at once, + /// which uses more memory but only decode image frames once. + case preloadAllAnimationData + + /// The `ImageDownloadRequestModifier` contained will be used to change the request before it being sent. + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// The original request will be sent without any modification by default. + case requestModifier(ImageDownloadRequestModifier) + + /// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection. + /// This is the posibility you can modify the image download request during redirect. You can modify the request for + /// some customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url + /// mapping. + /// The original redirection request will be sent without any modification by default. + case redirectHandler(ImageDownloadRedirectHandler) + + /// Processor for processing when the downloading finishes, a processor will convert the downloaded data to an image + /// and/or apply some filter on it. If a cache is connected to the downloader (it happens when you are using + /// KingfisherManager or any of the view extension methods), the converted image will also be sent to cache as well. + /// If not set, the `DefaultImageProcessor.default` will be used. + case processor(ImageProcessor) + + /// Supplies a `CacheSerializer` to convert some data to an image object for + /// retrieving from disk cache or vice versa for storing to disk cache. + /// If not set, the `DefaultCacheSerializer.default` will be used. + case cacheSerializer(CacheSerializer) + + /// An `ImageModifier` is for modifying an image as needed right before it is used. If the image was fetched + /// directly from the downloader, the modifier will run directly after the `ImageProcessor`. If the image is being + /// fetched from a cache, the modifier will run after the `CacheSerializer`. + /// + /// Use `ImageModifier` when you need to set properties that do not persist when caching the image on a concrete + /// type of `Image`, such as the `renderingMode` or the `alignmentInsets` of `UIImage`. + case imageModifier(ImageModifier) + + /// Keep the existing image of image view while setting another image to it. + /// By setting this option, the placeholder image parameter of image view extension method + /// will be ignored and the current image will be kept while loading or downloading the new image. + case keepCurrentImageWhileLoading + + /// If set, Kingfisher will only load the first frame from an animated image file as a single image. + /// Loading an animated images may take too much memory. It will be useful when you want to display a + /// static preview of the first frame from a animated image. + /// + /// This option will be ignored if the target image is not animated image data. + case onlyLoadFirstFrame + + /// If set and an `ImageProcessor` is used, Kingfisher will try to cache both the final result and original + /// image. Kingfisher will have a chance to use the original image when another processor is applied to the same + /// resource, instead of downloading it again. You can use `.originalCache` to specify a cache or the original + /// images if necessary. + /// + /// The original image will be only cached to disk storage. + case cacheOriginalImage + + /// If set and a downloading error occurred Kingfisher will set provided image (or empty) + /// in place of requested one. It's useful when you don't want to show placeholder + /// during loading time but wants to use some default image when requests will be failed. + case onFailureImage(Image?) + + /// If set and used in `ImagePrefetcher`, the prefetching operation will load the images into memory storage + /// aggressively. By default this is not contained in the options, that means if the requested image is already + /// in disk cache, Kingfisher will not try to load it to memory. + case alsoPrefetchToMemory + + /// If set, the disk storage loading will happen in the same calling queue. By default, disk storage file loading + /// happens in its own queue with an asynchronous dispatch behavior. Although it provides better non-blocking disk + /// loading performance, it also causes a flickering when you reload an image from disk, if the image view already + /// has an image set. + /// + /// Set this options will stop that flickering by keeping all loading in the same queue (typically the UI queue + /// if you are using Kingfisher's extension methods to set an image), with a tradeoff of loading performance. + case loadDiskFileSynchronously + + /// The expiration setting for memory cache. By default, the underlying `MemoryStorage.Backend` uses the + /// expiration in its config for all items. If set, the `MemoryStorage.Backend` will use this associated + /// value to overwrite the config setting for this caching item. + case memoryCacheExpiration(StorageExpiration) + + /// The expiration extending setting for memory cache. The item expiration time will be incremented by this value after access. + /// By default, the underlying `MemoryStorage.Backend` uses the initial cache expiration as extending value: .cacheTime. + /// To disable extending option at all add memoryCacheAccessExtendingExpiration(.none) to options. + case memoryCacheAccessExtendingExpiration(ExpirationExtending) + + /// The expiration setting for memory cache. By default, the underlying `DiskStorage.Backend` uses the + /// expiration in its config for all items. If set, the `DiskStorage.Backend` will use this associated + /// value to overwrite the config setting for this caching item. + case diskCacheExpiration(StorageExpiration) + + /// Decides on which queue the image processing should happen. By default, Kingfisher uses a pre-defined serial + /// queue to process images. Use this option to change this behavior. For example, specify a `.mainCurrentOrAsync` + /// to let the image be processed in main queue to prevent a possible flickering (but with a possibility of + /// blocking the UI, especially if the processor needs a lot of time to run). + case processingQueue(CallbackQueue) + + /// Enable progressive image loading, Kingfisher will use the `ImageProgressive` of + case progressiveJPEG(ImageProgressive) +} + +// Improve performance by parsing the input `KingfisherOptionsInfo` (self) first. +// So we can prevent the iterating over the options array again and again. +/// The parsed options info used across Kingfisher methods. Each property in this type corresponds a case member +/// in `KingfisherOptionsInfoItem`. When a `KingfisherOptionsInfo` sent to Kingfisher related methods, it will be +/// parsed and converted to a `KingfisherParsedOptionsInfo` first, and pass through the internal methods. +public struct KingfisherParsedOptionsInfo { + + public var targetCache: ImageCache? = nil + public var originalCache: ImageCache? = nil + public var downloader: ImageDownloader? = nil + public var transition: ImageTransition = .none + public var downloadPriority: Float = URLSessionTask.defaultPriority + public var forceRefresh = false + public var fromMemoryCacheOrRefresh = false + public var forceTransition = false + public var cacheMemoryOnly = false + public var waitForCache = false + public var onlyFromCache = false + public var backgroundDecode = false + public var preloadAllAnimationData = false + public var callbackQueue: CallbackQueue = .mainCurrentOrAsync + public var scaleFactor: CGFloat = 1.0 + public var requestModifier: ImageDownloadRequestModifier? = nil + public var redirectHandler: ImageDownloadRedirectHandler? = nil + public var processor: ImageProcessor = DefaultImageProcessor.default + public var imageModifier: ImageModifier? = nil + public var cacheSerializer: CacheSerializer = DefaultCacheSerializer.default + public var keepCurrentImageWhileLoading = false + public var onlyLoadFirstFrame = false + public var cacheOriginalImage = false + public var onFailureImage: Optional<Image?> = .none + public var alsoPrefetchToMemory = false + public var loadDiskFileSynchronously = false + public var memoryCacheExpiration: StorageExpiration? = nil + public var memoryCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime + public var diskCacheExpiration: StorageExpiration? = nil + public var processingQueue: CallbackQueue? = nil + public var progressiveJPEG: ImageProgressive? = nil + + var onDataReceived: [DataReceivingSideEffect]? = nil + + public init(_ info: KingfisherOptionsInfo?) { + guard let info = info else { return } + for option in info { + switch option { + case .targetCache(let value): targetCache = value + case .originalCache(let value): originalCache = value + case .downloader(let value): downloader = value + case .transition(let value): transition = value + case .downloadPriority(let value): downloadPriority = value + case .forceRefresh: forceRefresh = true + case .fromMemoryCacheOrRefresh: fromMemoryCacheOrRefresh = true + case .forceTransition: forceTransition = true + case .cacheMemoryOnly: cacheMemoryOnly = true + case .waitForCache: waitForCache = true + case .onlyFromCache: onlyFromCache = true + case .backgroundDecode: backgroundDecode = true + case .preloadAllAnimationData: preloadAllAnimationData = true + case .callbackQueue(let value): callbackQueue = value + case .scaleFactor(let value): scaleFactor = value + case .requestModifier(let value): requestModifier = value + case .redirectHandler(let value): redirectHandler = value + case .processor(let value): processor = value + case .imageModifier(let value): imageModifier = value + case .cacheSerializer(let value): cacheSerializer = value + case .keepCurrentImageWhileLoading: keepCurrentImageWhileLoading = true + case .onlyLoadFirstFrame: onlyLoadFirstFrame = true + case .cacheOriginalImage: cacheOriginalImage = true + case .onFailureImage(let value): onFailureImage = .some(value) + case .alsoPrefetchToMemory: alsoPrefetchToMemory = true + case .loadDiskFileSynchronously: loadDiskFileSynchronously = true + case .callbackDispatchQueue(let value): callbackQueue = value.map { .dispatch($0) } ?? .mainCurrentOrAsync + case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration + case .memoryCacheAccessExtendingExpiration(let expirationExtending): memoryCacheAccessExtendingExpiration = expirationExtending + case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration + case .processingQueue(let queue): processingQueue = queue + case .progressiveJPEG(let value): progressiveJPEG = value + } + } + + if originalCache == nil { + originalCache = targetCache + } + } +} + +extension KingfisherParsedOptionsInfo { + var imageCreatingOptions: ImageCreatingOptions { + return ImageCreatingOptions( + scale: scaleFactor, + duration: 0.0, + preloadAll: preloadAllAnimationData, + onlyFirstFrame: onlyLoadFirstFrame) + } +} + +protocol DataReceivingSideEffect: AnyObject { + var onShouldApply: () -> Bool { get set } + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) +} + +class ImageLoadingProgressSideEffect: DataReceivingSideEffect { + + var onShouldApply: () -> Bool = { return true } + + let block: DownloadProgressBlock + + init(_ block: @escaping DownloadProgressBlock) { + self.block = block + } + + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { + guard onShouldApply() else { return } + guard + let expectedContentLength = task.task.response?.expectedContentLength, + expectedContentLength != -1 else { + return + } + + let dataLength = Int64(task.mutableData.count) + DispatchQueue.main.async { + self.block(dataLength, expectedContentLength) + } + } +} diff --git a/Pods/Kingfisher/Sources/Image/Filter.swift b/Pods/Kingfisher/Sources/Image/Filter.swift new file mode 100644 index 0000000..e93bc8b --- /dev/null +++ b/Pods/Kingfisher/Sources/Image/Filter.swift @@ -0,0 +1,142 @@ +// +// Filter.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/31. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import CoreImage + +// Reuse the same CI Context for all CI drawing. +private let ciContext = CIContext(options: nil) + +/// Represents the type of transformer method, which will be used in to provide a `Filter`. +public typealias Transformer = (CIImage) -> CIImage? + +/// Represents a processor based on a `CIImage` `Filter`. +/// It requires a filter to create an `ImageProcessor`. +public protocol CIImageProcessor: ImageProcessor { + var filter: Filter { get } +} + +extension CIImageProcessor { + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + return image.kf.apply(filter) + case .data: + return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} + +/// A wrapper struct for a `Transformer` of CIImage filters. A `Filter` +/// value could be used to create a `CIImage` processor. +public struct Filter { + + let transform: Transformer + + public init(transform: @escaping Transformer) { + self.transform = transform + } + + /// Tint filter which will apply a tint color to images. + public static var tint: (Color) -> Filter = { + color in + Filter { + input in + + let colorFilter = CIFilter(name: "CIConstantColorGenerator")! + colorFilter.setValue(CIColor(color: color), forKey: kCIInputColorKey) + + let filter = CIFilter(name: "CISourceOverCompositing")! + + let colorImage = colorFilter.outputImage + filter.setValue(colorImage, forKey: kCIInputImageKey) + filter.setValue(input, forKey: kCIInputBackgroundImageKey) + + return filter.outputImage?.cropped(to: input.extent) + } + } + + /// Represents color control elements. It is a tuple of + /// `(brightness, contrast, saturation, inputEV)` + public typealias ColorElement = (CGFloat, CGFloat, CGFloat, CGFloat) + + /// Color control filter which will apply color control change to images. + public static var colorControl: (ColorElement) -> Filter = { arg -> Filter in + let (brightness, contrast, saturation, inputEV) = arg + return Filter { input in + let paramsColor = [kCIInputBrightnessKey: brightness, + kCIInputContrastKey: contrast, + kCIInputSaturationKey: saturation] + let blackAndWhite = input.applyingFilter("CIColorControls", parameters: paramsColor) + let paramsExposure = [kCIInputEVKey: inputEV] + return blackAndWhite.applyingFilter("CIExposureAdjust", parameters: paramsExposure) + } + } +} + +extension KingfisherWrapper where Base: Image { + + /// Applies a `Filter` containing `CIImage` transformer to `self`. + /// + /// - Parameter filter: The filter used to transform `self`. + /// - Returns: A transformed image by input `Filter`. + /// + /// - Note: + /// Only CG-based images are supported. If any error happens + /// during transforming, `self` will be returned. + public func apply(_ filter: Filter) -> Image { + + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Tint image only works for CG-based image.") + return base + } + + let inputImage = CIImage(cgImage: cgImage) + guard let outputImage = filter.transform(inputImage) else { + return base + } + + guard let result = ciContext.createCGImage(outputImage, from: outputImage.extent) else { + assertionFailure("[Kingfisher] Can not make an tint image within context.") + return base + } + + #if os(macOS) + return fixedForRetinaPixel(cgImage: result, to: size) + #else + return Image(cgImage: result, scale: base.scale, orientation: base.imageOrientation) + #endif + } + +} diff --git a/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift b/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift new file mode 100644 index 0000000..fd2f443 --- /dev/null +++ b/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift @@ -0,0 +1,121 @@ +// +// AnimatedImage.swift +// Kingfisher +// +// Created by onevcat on 2018/09/26. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import ImageIO + +/// Represents a set of image creating options used in Kingfisher. +public struct ImageCreatingOptions { + + /// The target scale of image needs to be created. + public let scale: CGFloat + + /// The expected animation duration if an animated image being created. + public let duration: TimeInterval + + /// For an animated image, whether or not all frames should be loaded before displaying. + public let preloadAll: Bool + + /// For an animated image, whether or not only the first image should be + /// loaded as a static image. It is useful for preview purpose of an animated image. + public let onlyFirstFrame: Bool + + /// Creates an `ImageCreatingOptions` object. + /// + /// - Parameters: + /// - scale: The target scale of image needs to be created. Default is `1.0`. + /// - duration: The expected animation duration if an animated image being created. + /// A value less or equal to `0.0` means the animated image duration will + /// be determined by the frame data. Default is `0.0`. + /// - preloadAll: For an animated image, whether or not all frames should be loaded before displaying. + /// Default is `false`. + /// - onlyFirstFrame: For an animated image, whether or not only the first image should be + /// loaded as a static image. It is useful for preview purpose of an animated image. + /// Default is `false`. + public init( + scale: CGFloat = 1.0, + duration: TimeInterval = 0.0, + preloadAll: Bool = false, + onlyFirstFrame: Bool = false) + { + self.scale = scale + self.duration = duration + self.preloadAll = preloadAll + self.onlyFirstFrame = onlyFirstFrame + } +} + +// Represents the decoding for a GIF image. This class extracts frames from an `imageSource`, then +// hold the images for later use. +class GIFAnimatedImage { + let images: [Image] + let duration: TimeInterval + + init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) { + let frameCount = CGImageSourceGetCount(imageSource) + var images = [Image]() + var gifDuration = 0.0 + + for i in 0 ..< frameCount { + guard let imageRef = CGImageSourceCreateImageAtIndex(imageSource, i, info as CFDictionary) else { + return nil + } + + if frameCount == 1 { + gifDuration = .infinity + } else { + // Get current animated GIF frame duration + gifDuration += GIFAnimatedImage.getFrameDuration(from: imageSource, at: i) + } + images.append(KingfisherWrapper.image(cgImage: imageRef, scale: options.scale, refImage: nil)) + if options.onlyFirstFrame { break } + } + self.images = images + self.duration = gifDuration + } + + // Calculates frame duration for a gif frame out of the kCGImagePropertyGIFDictionary dictionary. + static func getFrameDuration(from gifInfo: [String: Any]?) -> TimeInterval { + let defaultFrameDuration = 0.1 + guard let gifInfo = gifInfo else { return defaultFrameDuration } + + let unclampedDelayTime = gifInfo[kCGImagePropertyGIFUnclampedDelayTime as String] as? NSNumber + let delayTime = gifInfo[kCGImagePropertyGIFDelayTime as String] as? NSNumber + let duration = unclampedDelayTime ?? delayTime + + guard let frameDuration = duration else { return defaultFrameDuration } + return frameDuration.doubleValue > 0.011 ? frameDuration.doubleValue : defaultFrameDuration + } + + // Calculates frame duration at a specific index for a gif from an `imageSource`. + static func getFrameDuration(from imageSource: CGImageSource, at index: Int) -> TimeInterval { + guard let properties = CGImageSourceCopyPropertiesAtIndex(imageSource, index, nil) + as? [String: Any] else { return 0.0 } + + let gifInfo = properties[kCGImagePropertyGIFDictionary as String] as? [String: Any] + return getFrameDuration(from: gifInfo) + } +} diff --git a/Pods/Kingfisher/Sources/Image/Image.swift b/Pods/Kingfisher/Sources/Image/Image.swift new file mode 100644 index 0000000..d1ae4f7 --- /dev/null +++ b/Pods/Kingfisher/Sources/Image/Image.swift @@ -0,0 +1,367 @@ +// +// Image.swift +// Kingfisher +// +// Created by Wei Wang on 16/1/6. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#if os(macOS) +import AppKit +private var imagesKey: Void? +private var durationKey: Void? +#else +import UIKit +import MobileCoreServices +private var imageSourceKey: Void? +#endif + +#if !os(watchOS) +import CoreImage +#endif + +import CoreGraphics +import ImageIO + +private var animatedImageDataKey: Void? + +// MARK: - Image Properties +extension KingfisherWrapper where Base: Image { + private(set) var animatedImageData: Data? { + get { return getAssociatedObject(base, &animatedImageDataKey) } + set { setRetainedAssociatedObject(base, &animatedImageDataKey, newValue) } + } + + #if os(macOS) + var cgImage: CGImage? { + return base.cgImage(forProposedRect: nil, context: nil, hints: nil) + } + + var scale: CGFloat { + return 1.0 + } + + private(set) var images: [Image]? { + get { return getAssociatedObject(base, &imagesKey) } + set { setRetainedAssociatedObject(base, &imagesKey, newValue) } + } + + private(set) var duration: TimeInterval { + get { return getAssociatedObject(base, &durationKey) ?? 0.0 } + set { setRetainedAssociatedObject(base, &durationKey, newValue) } + } + + var size: CGSize { + return base.representations.reduce(.zero) { size, rep in + let width = max(size.width, CGFloat(rep.pixelsWide)) + let height = max(size.height, CGFloat(rep.pixelsHigh)) + return CGSize(width: width, height: height) + } + } + #else + var cgImage: CGImage? { return base.cgImage } + var scale: CGFloat { return base.scale } + var images: [Image]? { return base.images } + var duration: TimeInterval { return base.duration } + var size: CGSize { return base.size } + + private(set) var imageSource: CGImageSource? { + get { return getAssociatedObject(base, &imageSourceKey) } + set { setRetainedAssociatedObject(base, &imageSourceKey, newValue) } + } + #endif + + // Bitmap memory cost with bytes. + var cost: Int { + let pixel = Int(size.width * size.height * scale * scale) + guard let cgImage = cgImage else { + return pixel * 4 + } + return pixel * cgImage.bitsPerPixel / 8 + } +} + +// MARK: - Image Conversion +extension KingfisherWrapper where Base: Image { + #if os(macOS) + static func image(cgImage: CGImage, scale: CGFloat, refImage: Image?) -> Image { + return Image(cgImage: cgImage, size: .zero) + } + + /// Normalize the image. This getter does nothing on macOS but return the image itself. + public var normalized: Image { return base } + + #else + /// Creating an image from a give `CGImage` at scale and orientation for refImage. The method signature is for + /// compatibility of macOS version. + static func image(cgImage: CGImage, scale: CGFloat, refImage: Image?) -> Image { + return Image(cgImage: cgImage, scale: scale, orientation: refImage?.imageOrientation ?? .up) + } + + /// Returns normalized image for current `base` image. + /// This method will try to redraw an image with orientation and scale considered. + public var normalized: Image { + // prevent animated image (GIF) lose it's images + guard images == nil else { return base.copy() as! Image } + // No need to do anything if already up + guard base.imageOrientation != .up else { return base.copy() as! Image } + + return draw(to: size, inverting: true, refImage: Image()) { + fixOrientation(in: $0) + } + } + + func fixOrientation(in context: CGContext) { + + var transform = CGAffineTransform.identity + + let orientation = base.imageOrientation + + switch orientation { + case .down, .downMirrored: + transform = transform.translatedBy(x: size.width, y: size.height) + transform = transform.rotated(by: .pi) + case .left, .leftMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.rotated(by: .pi / 2.0) + case .right, .rightMirrored: + transform = transform.translatedBy(x: 0, y: size.height) + transform = transform.rotated(by: .pi / -2.0) + case .up, .upMirrored: + break + #if compiler(>=5) + @unknown default: + break + #endif + } + + //Flip image one more time if needed to, this is to prevent flipped image + switch orientation { + case .upMirrored, .downMirrored: + transform = transform.translatedBy(x: size.width, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .leftMirrored, .rightMirrored: + transform = transform.translatedBy(x: size.height, y: 0) + transform = transform.scaledBy(x: -1, y: 1) + case .up, .down, .left, .right: + break + #if compiler(>=5) + @unknown default: + break + #endif + } + + context.concatenate(transform) + switch orientation { + case .left, .leftMirrored, .right, .rightMirrored: + context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.height, height: size.width)) + default: + context.draw(cgImage!, in: CGRect(x: 0, y: 0, width: size.width, height: size.height)) + } + } + #endif +} + +// MARK: - Image Representation +extension KingfisherWrapper where Base: Image { + /// Returns PNG representation of `base` image. + /// + /// - Returns: PNG data of image. + public func pngRepresentation() -> Data? { + #if os(macOS) + guard let cgImage = cgImage else { + return nil + } + let rep = NSBitmapImageRep(cgImage: cgImage) + return rep.representation(using: .png, properties: [:]) + #else + #if swift(>=4.2) + return base.pngData() + #else + return UIImagePNGRepresentation(base) + #endif + #endif + } + + /// Returns JPEG representation of `base` image. + /// + /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. + /// - Returns: JPEG data of image. + public func jpegRepresentation(compressionQuality: CGFloat) -> Data? { + #if os(macOS) + guard let cgImage = cgImage else { + return nil + } + let rep = NSBitmapImageRep(cgImage: cgImage) + return rep.representation(using:.jpeg, properties: [.compressionFactor: compressionQuality]) + #else + #if swift(>=4.2) + return base.jpegData(compressionQuality: compressionQuality) + #else + return UIImageJPEGRepresentation(base, compressionQuality) + #endif + #endif + } + + /// Returns GIF representation of `base` image. + /// + /// - Returns: Original GIF data of image. + public func gifRepresentation() -> Data? { + return animatedImageData + } + + /// Returns a data representation for `base` image, with the `format` as the format indicator. + /// + /// - Parameter format: The format in which the output data should be. If `unknown`, the `base` image will be + /// converted in the PNG representation. + /// - Returns: The output data representing. + public func data(format: ImageFormat) -> Data? { + let data: Data? + switch format { + case .PNG: data = pngRepresentation() + case .JPEG: data = jpegRepresentation(compressionQuality: 1.0) + case .GIF: data = gifRepresentation() + case .unknown: data = normalized.kf.pngRepresentation() + } + + return data + } +} + +// MARK: - Creating Images +extension KingfisherWrapper where Base: Image { + + /// Creates an animated image from a given data and options. Currently only GIF data is supported. + /// + /// - Parameters: + /// - data: The animated image data. + /// - options: Options to use when creating the animated image. + /// - Returns: An `Image` object represents the animated image. It is in form of an array of image frames with a + /// certain duration. `nil` if anything wrong when creating animated image. + public static func animatedImage(data: Data, options: ImageCreatingOptions) -> Image? { + let info: [String: Any] = [ + kCGImageSourceShouldCache as String: true, + kCGImageSourceTypeIdentifierHint as String: kUTTypeGIF + ] + + guard let imageSource = CGImageSourceCreateWithData(data as CFData, info as CFDictionary) else { + return nil + } + + #if os(macOS) + guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else { + return nil + } + var image: Image? + if options.onlyFirstFrame { + image = animatedImage.images.first + } else { + image = Image(data: data) + var kf = image?.kf + kf?.images = animatedImage.images + kf?.duration = animatedImage.duration + } + image?.kf.animatedImageData = data + return image + #else + + var image: Image? + if options.preloadAll || options.onlyFirstFrame { + // Use `images` image if you want to preload all animated data + guard let animatedImage = GIFAnimatedImage(from: imageSource, for: info, options: options) else { + return nil + } + if options.onlyFirstFrame { + image = animatedImage.images.first + } else { + let duration = options.duration <= 0.0 ? animatedImage.duration : options.duration + image = .animatedImage(with: animatedImage.images, duration: duration) + } + image?.kf.animatedImageData = data + } else { + image = Image(data: data, scale: options.scale) + var kf = image?.kf + kf?.imageSource = imageSource + kf?.animatedImageData = data + } + + return image + #endif + } + + /// Creates an image from a given data and options. `.JPEG`, `.PNG` or `.GIF` is supported. For other + /// image format, image initializer from system will be used. If no image object could be created from + /// the given `data`, `nil` will be returned. + /// + /// - Parameters: + /// - data: The image data representation. + /// - options: Options to use when creating the image. + /// - Returns: An `Image` object represents the image if created. If the `data` is invalid or not supported, `nil` + /// will be returned. + public static func image(data: Data, options: ImageCreatingOptions) -> Image? { + var image: Image? + switch data.kf.imageFormat { + case .JPEG: + image = Image(data: data, scale: options.scale) + case .PNG: + image = Image(data: data, scale: options.scale) + case .GIF: + image = KingfisherWrapper.animatedImage(data: data, options: options) + case .unknown: + image = Image(data: data, scale: options.scale) + } + return image + } + + /// Creates a downsampled image from given data to a certain size and scale. + /// + /// - Parameters: + /// - data: The image data contains a JPEG or PNG image. + /// - pointSize: The target size in point to which the image should be downsampled. + /// - scale: The scale of result image. + /// - Returns: A downsampled `Image` object following the input conditions. + /// + /// - Note: + /// Different from image `resize` methods, downsampling will not render the original + /// input image in pixel format. It does downsampling from the image data, so it is much + /// more memory efficient and friendly. Choose to use downsampling as possible as you can. + /// + /// The input size should be smaller than the size of input image. If it is larger than the + /// original image size, the result image will be the same size of input without downsampling. + public static func downsampledImage(data: Data, to pointSize: CGSize, scale: CGFloat) -> Image? { + let imageSourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary + guard let imageSource = CGImageSourceCreateWithData(data as CFData, imageSourceOptions) else { + return nil + } + + let maxDimensionInPixels = max(pointSize.width, pointSize.height) * scale + let downsampleOptions = [ + kCGImageSourceCreateThumbnailFromImageAlways: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceThumbnailMaxPixelSize: maxDimensionInPixels] as CFDictionary + guard let downsampledImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsampleOptions) else { + return nil + } + return KingfisherWrapper.image(cgImage: downsampledImage, scale: scale, refImage: nil) + } +} diff --git a/Pods/Kingfisher/Sources/Image/ImageDrawing.swift b/Pods/Kingfisher/Sources/Image/ImageDrawing.swift new file mode 100644 index 0000000..29daf20 --- /dev/null +++ b/Pods/Kingfisher/Sources/Image/ImageDrawing.swift @@ -0,0 +1,529 @@ +// +// ImageDrawing.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Accelerate + +#if canImport(AppKit) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +// MARK: - Image Transforming +extension KingfisherWrapper where Base: Image { + // MARK: Blend Mode + /// Create image from `base` image and apply blend mode. + /// + /// - parameter blendMode: The blend mode of creating image. + /// - parameter alpha: The alpha should be used for image. + /// - parameter backgroundColor: The background color for the output image. + /// + /// - returns: An image with blend mode applied. + /// + /// - Note: This method only works for CG-based image. + #if !os(macOS) + public func image(withBlendMode blendMode: CGBlendMode, + alpha: CGFloat = 1.0, + backgroundColor: Color? = nil) -> Image + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Blend mode image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + UIRectFill(rect) + } + + base.draw(in: rect, blendMode: blendMode, alpha: alpha) + } + } + #endif + + #if os(macOS) + // MARK: Compositing + /// Creates image from `base` image and apply compositing operation. + /// + /// - Parameters: + /// - compositingOperation: The compositing operation of creating image. + /// - alpha: The alpha should be used for image. + /// - backgroundColor: The background color for the output image. + /// - Returns: An image with compositing operation applied. + /// + /// - Note: This method only works for CG-based image. For any non-CG-based image, `base` itself is returned. + public func image(withCompositingOperation compositingOperation: NSCompositingOperation, + alpha: CGFloat = 1.0, + backgroundColor: Color? = nil) -> Image + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Compositing Operation image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: .zero, size: size) + return draw(to: rect.size) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + rect.fill() + } + base.draw(in: rect, from: .zero, operation: compositingOperation, fraction: alpha) + } + } + #endif + + // MARK: Round Corner + /// Creates a round corner image from on `base` image. + /// + /// - Parameters: + /// - radius: The round corner radius of creating image. + /// - size: The target size of creating image. + /// - corners: The target corners which will be applied rounding. + /// - backgroundColor: The background color for the output image + /// - Returns: An image with round corner of `self`. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func image(withRoundRadius radius: CGFloat, + fit size: CGSize, + roundingCorners corners: RectCorner = .all, + backgroundColor: Color? = nil) -> Image + { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Round corner image only works for CG-based image.") + return base + } + + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + return draw(to: size) { _ in + #if os(macOS) + if let backgroundColor = backgroundColor { + let rectPath = NSBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: radius) + #if swift(>=4.2) + path.windingRule = .evenOdd + #else + path.windingRule = .evenOddWindingRule + #endif + path.addClip() + base.draw(in: rect) + #else + guard let context = UIGraphicsGetCurrentContext() else { + assertionFailure("[Kingfisher] Failed to create CG context for image.") + return + } + + if let backgroundColor = backgroundColor { + let rectPath = UIBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners.uiRectCorner, + cornerRadii: CGSize(width: radius, height: radius) + ) + context.addPath(path.cgPath) + context.clip() + base.draw(in: rect) + #endif + } + } + + #if os(iOS) || os(tvOS) + func resize(to size: CGSize, for contentMode: UIView.ContentMode) -> Image { + switch contentMode { + case .scaleAspectFit: + return resize(to: size, for: .aspectFit) + case .scaleAspectFill: + return resize(to: size, for: .aspectFill) + default: + return resize(to: size) + } + } + #endif + + // MARK: Resizing + /// Resizes `base` image to an image with new size. + /// + /// - Parameter size: The target size in point. + /// - Returns: An image with new size. + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func resize(to size: CGSize) -> Image { + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Resize only works for CG-based image.") + return base + } + + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + return draw(to: size) { _ in + #if os(macOS) + base.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + #else + base.draw(in: rect) + #endif + } + } + + /// Resizes `base` image to an image of new size, respecting the given content mode. + /// + /// - Parameters: + /// - targetSize: The target size in point. + /// - contentMode: Content mode of output image should be. + /// - Returns: An image with new size. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func resize(to targetSize: CGSize, for contentMode: ContentMode) -> Image { + let newSize = size.kf.resize(to: targetSize, for: contentMode) + return resize(to: newSize) + } + + // MARK: Cropping + /// Crops `base` image to a new size with a given anchor. + /// + /// - Parameters: + /// - size: The target size. + /// - anchor: The anchor point from which the size should be calculated. + /// - Returns: An image with new size. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func crop(to size: CGSize, anchorOn anchor: CGPoint) -> Image { + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Crop only works for CG-based image.") + return base + } + + let rect = self.size.kf.constrainedRect(for: size, anchor: anchor) + guard let image = cgImage.cropping(to: rect.scaled(scale)) else { + assertionFailure("[Kingfisher] Cropping image failed.") + return base + } + + return KingfisherWrapper.image(cgImage: image, scale: scale, refImage: base) + } + + // MARK: Blur + /// Creates an image with blur effect based on `base` image. + /// + /// - Parameter radius: The blur radius should be used when creating blur effect. + /// - Returns: An image with blur effect applied. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func blurred(withRadius radius: CGFloat) -> Image { + + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Blur only works for CG-based image.") + return base + } + + // http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement + // let d = floor(s * 3*sqrt(2*pi)/4 + 0.5) + // if d is odd, use three box-blurs of size 'd', centered on the output pixel. + let s = Float(max(radius, 2.0)) + // We will do blur on a resized image (*0.5), so the blur radius could be half as well. + + // Fix the slow compiling time for Swift 3. + // See https://github.com/onevcat/Kingfisher/issues/611 + let pi2 = 2 * Float.pi + let sqrtPi2 = sqrt(pi2) + var targetRadius = floor(s * 3.0 * sqrtPi2 / 4.0 + 0.5) + + if targetRadius.isEven { targetRadius += 1 } + + // Determine necessary iteration count by blur radius. + let iterations: Int + if radius < 0.5 { + iterations = 1 + } else if radius < 1.5 { + iterations = 2 + } else { + iterations = 3 + } + + let w = Int(size.width) + let h = Int(size.height) + let rowBytes = Int(CGFloat(cgImage.bytesPerRow)) + + func createEffectBuffer(_ context: CGContext) -> vImage_Buffer { + let data = context.data + let width = vImagePixelCount(context.width) + let height = vImagePixelCount(context.height) + let rowBytes = context.bytesPerRow + + return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes) + } + + guard let context = beginContext(size: size, scale: scale, inverting: true) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: w, height: h)) + endContext() + + var inBuffer = createEffectBuffer(context) + + guard let outContext = beginContext(size: size, scale: scale, inverting: true) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + defer { endContext() } + var outBuffer = createEffectBuffer(outContext) + + for _ in 0 ..< iterations { + let flag = vImage_Flags(kvImageEdgeExtend) + vImageBoxConvolve_ARGB8888( + &inBuffer, &outBuffer, nil, 0, 0, UInt32(targetRadius), UInt32(targetRadius), nil, flag) + // Next inBuffer should be the outButter of current iteration + (inBuffer, outBuffer) = (outBuffer, inBuffer) + } + + #if os(macOS) + let result = outContext.makeImage().flatMap { + fixedForRetinaPixel(cgImage: $0, to: size) + } + #else + let result = outContext.makeImage().flatMap { + Image(cgImage: $0, scale: base.scale, orientation: base.imageOrientation) + } + #endif + guard let blurredImage = result else { + assertionFailure("[Kingfisher] Can not make an blurred image within this context.") + return base + } + + return blurredImage + } + + // MARK: Overlay + /// Creates an image from `base` image with a color overlay layer. + /// + /// - Parameters: + /// - color: The color should be use to overlay. + /// - fraction: Fraction of input color. From 0.0 to 1.0. 0.0 means solid color, + /// 1.0 means transparent overlay. + /// - Returns: An image with a color overlay applied. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image, `base` itself is returned. + public func overlaying(with color: Color, fraction: CGFloat) -> Image { + + guard let _ = cgImage else { + assertionFailure("[Kingfisher] Overlaying only works for CG-based image.") + return base + } + + let rect = CGRect(x: 0, y: 0, width: size.width, height: size.height) + return draw(to: rect.size) { context in + #if os(macOS) + base.draw(in: rect) + if fraction > 0 { + color.withAlphaComponent(1 - fraction).set() + rect.fill(using: .sourceAtop) + } + #else + color.set() + UIRectFill(rect) + base.draw(in: rect, blendMode: .destinationIn, alpha: 1.0) + + if fraction > 0 { + base.draw(in: rect, blendMode: .sourceAtop, alpha: fraction) + } + #endif + } + } + + // MARK: Tint + /// Creates an image from `base` image with a color tint. + /// + /// - Parameter color: The color should be used to tint `base` + /// - Returns: An image with a color tint applied. + public func tinted(with color: Color) -> Image { + #if os(watchOS) + return base + #else + return apply(.tint(color)) + #endif + } + + // MARK: Color Control + + /// Create an image from `self` with color control. + /// + /// - Parameters: + /// - brightness: Brightness changing to image. + /// - contrast: Contrast changing to image. + /// - saturation: Saturation changing to image. + /// - inputEV: InputEV changing to image. + /// - Returns: An image with color control applied. + public func adjusted(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) -> Image { + #if os(watchOS) + return base + #else + return apply(.colorControl((brightness, contrast, saturation, inputEV))) + #endif + } + + /// Return an image with given scale. + /// + /// - Parameter scale: Target scale factor the new image should have. + /// - Returns: The image with target scale. If the base image is already in the scale, `base` will be returned. + public func scaled(to scale: CGFloat) -> Image { + guard scale != self.scale else { + return base + } + guard let cgImage = cgImage else { + assertionFailure("[Kingfisher] Scaling only works for CG-based image.") + return base + } + return KingfisherWrapper.image(cgImage: cgImage, scale: scale, refImage: base) + } +} + +// MARK: - Decoding Image +extension KingfisherWrapper where Base: Image { + + /// Returns the decoded image of the `base` image. It will draw the image in a plain context and return the data + /// from it. This could improve the drawing performance when an image is just created from data but not yet + /// displayed for the first time. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image or animated image, `base` itself is returned. + public var decoded: Image { return decoded(scale: scale) } + + /// Returns decoded image of the `base` image at a given scale. It will draw the image in a plain context and + /// return the data from it. This could improve the drawing performance when an image is just created from + /// data but not yet displayed for the first time. + /// + /// - Parameter scale: The given scale of target image should be. + /// - Returns: The decoded image ready to be displayed. + /// + /// - Note: This method only works for CG-based image. The current image scale is kept. + /// For any non-CG-based image or animated image, `base` itself is returned. + public func decoded(scale: CGFloat) -> Image { + // Prevent animated image (GIF) losing it's images + #if os(iOS) + if imageSource != nil { return base } + #else + if images != nil { return base } + #endif + + guard let imageRef = cgImage else { + assertionFailure("[Kingfisher] Decoding only works for CG-based image.") + return base + } + + let size = CGSize(width: CGFloat(imageRef.width) / scale, height: CGFloat(imageRef.height) / scale) + return draw(to: size, inverting: true, scale: scale) { context in + context.draw(imageRef, in: CGRect(origin: .zero, size: size)) + } + } +} + +extension KingfisherWrapper where Base: Image { + + func beginContext(size: CGSize, scale: CGFloat, inverting: Bool = false) -> CGContext? { + #if os(macOS) + guard let rep = NSBitmapImageRep( + bitmapDataPlanes: nil, + pixelsWide: Int(size.width), + pixelsHigh: Int(size.height), + bitsPerSample: cgImage?.bitsPerComponent ?? 8, + samplesPerPixel: 4, + hasAlpha: true, + isPlanar: false, + colorSpaceName: .calibratedRGB, + bytesPerRow: 0, + bitsPerPixel: 0) else + { + assertionFailure("[Kingfisher] Image representation cannot be created.") + return nil + } + rep.size = size + NSGraphicsContext.saveGraphicsState() + guard let context = NSGraphicsContext(bitmapImageRep: rep) else { + assertionFailure("[Kingfisher] Image context cannot be created.") + return nil + } + + NSGraphicsContext.current = context + return context.cgContext + #else + UIGraphicsBeginImageContextWithOptions(size, false, scale) + guard let context = UIGraphicsGetCurrentContext() else { return nil } + if inverting { // If drawing a CGImage, we need to make context flipped. + context.scaleBy(x: 1.0, y: -1.0) + context.translateBy(x: 0, y: -size.height) + } + return context + #endif + } + + func endContext() { + #if os(macOS) + NSGraphicsContext.restoreGraphicsState() + #else + UIGraphicsEndImageContext() + #endif + } + + func draw(to size: CGSize, inverting: Bool = false, scale: CGFloat? = nil, refImage: Image? = nil, draw: (CGContext) -> Void) -> Image { + let targetScale = scale ?? self.scale + guard let context = beginContext(size: size, scale: targetScale, inverting: inverting) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + defer { endContext() } + draw(context) + guard let cgImage = context.makeImage() else { + return base + } + return KingfisherWrapper.image(cgImage: cgImage, scale: targetScale, refImage: refImage ?? base) + } + + #if os(macOS) + func fixedForRetinaPixel(cgImage: CGImage, to size: CGSize) -> Image { + + let image = Image(cgImage: cgImage, size: base.size) + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + + return draw(to: self.size) { context in + image.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + } + } + #endif +} diff --git a/Pods/Kingfisher/Sources/Image/ImageFormat.swift b/Pods/Kingfisher/Sources/Image/ImageFormat.swift new file mode 100644 index 0000000..14e3c7d --- /dev/null +++ b/Pods/Kingfisher/Sources/Image/ImageFormat.swift @@ -0,0 +1,131 @@ +// +// ImageFormat.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents image format. +/// +/// - unknown: The format cannot be recognized or not supported yet. +/// - PNG: PNG image format. +/// - JPEG: JPEG image format. +/// - GIF: GIF image format. +public enum ImageFormat { + /// The format cannot be recognized or not supported yet. + case unknown + /// PNG image format. + case PNG + /// JPEG image format. + case JPEG + /// GIF image format. + case GIF + + struct HeaderData { + static var PNG: [UInt8] = [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A] + static var JPEG_SOI: [UInt8] = [0xFF, 0xD8] + static var JPEG_IF: [UInt8] = [0xFF] + static var GIF: [UInt8] = [0x47, 0x49, 0x46] + } + + /// https://en.wikipedia.org/wiki/JPEG + public enum JPEGMarker { + case SOF0 //baseline + case SOF2 //progressive + case DHT //Huffman Table + case DQT //Quantization Table + case DRI //Restart Interval + case SOS //Start Of Scan + case RSTn(UInt8) //Restart + case APPn //Application-specific + case COM //Comment + case EOI //End Of Image + + var bytes: [UInt8] { + switch self { + case .SOF0: return [0xFF, 0xC0] + case .SOF2: return [0xFF, 0xC2] + case .DHT: return [0xFF, 0xC4] + case .DQT: return [0xFF, 0xDB] + case .DRI: return [0xFF, 0xDD] + case .SOS: return [0xFF, 0xDA] + case .RSTn(let n): return [0xFF, 0xD0 + n] + case .APPn: return [0xFF, 0xE0] + case .COM: return [0xFF, 0xFE] + case .EOI: return [0xFF, 0xD9] + } + } + } +} + + +extension Data: KingfisherCompatibleValue {} + +// MARK: - Misc Helpers +extension KingfisherWrapper where Base == Data { + /// Gets the image format corresponding to the data. + public var imageFormat: ImageFormat { + guard base.count > 8 else { return .unknown } + + var buffer = [UInt8](repeating: 0, count: 8) + base.copyBytes(to: &buffer, count: 8) + + if buffer == ImageFormat.HeaderData.PNG { + return .PNG + + } else if buffer[0] == ImageFormat.HeaderData.JPEG_SOI[0], + buffer[1] == ImageFormat.HeaderData.JPEG_SOI[1], + buffer[2] == ImageFormat.HeaderData.JPEG_IF[0] + { + return .JPEG + + } else if buffer[0] == ImageFormat.HeaderData.GIF[0], + buffer[1] == ImageFormat.HeaderData.GIF[1], + buffer[2] == ImageFormat.HeaderData.GIF[2] + { + return .GIF + } + + return .unknown + } + + public func contains(jpeg marker: ImageFormat.JPEGMarker) -> Bool { + guard imageFormat == .JPEG else { + return false + } + + var buffer = [UInt8](repeating: 0, count: base.count) + base.copyBytes(to: &buffer, count: base.count) + for (index, item) in buffer.enumerated() { + guard + item == marker.bytes.first, + buffer.count > index + 1, + buffer[index + 1] == marker.bytes[1] else { + continue + } + return true + } + return false + } +} diff --git a/Pods/Kingfisher/Sources/Image/ImageProcessor.swift b/Pods/Kingfisher/Sources/Image/ImageProcessor.swift new file mode 100644 index 0000000..d21d137 --- /dev/null +++ b/Pods/Kingfisher/Sources/Image/ImageProcessor.swift @@ -0,0 +1,837 @@ +// +// ImageProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/26. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +#if canImport(AppKit) +import AppKit +#endif + +/// Represents an item which could be processed by an `ImageProcessor`. +/// +/// - image: Input image. The processor should provide a way to apply +/// processing on this `image` and return the result image. +/// - data: Input data. The processor should provide a way to apply +/// processing on this `image` and return the result image. +public enum ImageProcessItem { + + /// Input image. The processor should provide a way to apply + /// processing on this `image` and return the result image. + case image(Image) + + /// Input data. The processor should provide a way to apply + /// processing on this `image` and return the result image. + case data(Data) +} + +/// An `ImageProcessor` would be used to convert some downloaded data to an image. +public protocol ImageProcessor { + /// Identifier of the processor. It will be used to identify the processor when + /// caching and retrieving an image. You might want to make sure that processors with + /// same properties/functionality have the same identifiers, so correct processed images + /// could be retrieved with proper key. + /// + /// - Note: Do not supply an empty string for a customized processor, which is already reserved by + /// the `DefaultImageProcessor`. It is recommended to use a reverse domain name notation string of + /// your own for the identifier. + var identifier: String { get } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: The return value should be `nil` if processing failed while converting an input item to image. + /// If `nil` received by the processing caller, an error will be reported and the process flow stops. + /// If the processing flow is not critical for your flow, then when the input item is already an image + /// (`.image` case) and there is any errors in the processing, you could return the input image itself + /// to keep the processing pipeline continuing. + /// - Note: Most processor only supports CG-based images. watchOS is not supported for processors containing + /// a filter, the input image will be returned directly on watchOS. + /// - Note: + /// This method is deprecated. Please implement the version with + /// `KingfisherParsedOptionsInfo` as parameter instead. + @available(*, deprecated, + message: "Deprecated. Implement the method with same name but with `KingfisherParsedOptionsInfo` instead.") + func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: The parsed options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: The return value should be `nil` if processing failed while converting an input item to image. + /// If `nil` received by the processing caller, an error will be reported and the process flow stops. + /// If the processing flow is not critical for your flow, then when the input item is already an image + /// (`.image` case) and there is any errors in the processing, you could return the input image itself + /// to keep the processing pipeline continuing. + /// - Note: Most processor only supports CG-based images. watchOS is not supported for processors containing + /// a filter, the input image will be returned directly on watchOS. + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? +} + +extension ImageProcessor { + public func process(item: ImageProcessItem, options: KingfisherOptionsInfo) -> Image? { + return process(item: item, options: KingfisherParsedOptionsInfo(options)) + } +} + +extension ImageProcessor { + + /// Appends an `ImageProcessor` to another. The identifier of the new `ImageProcessor` + /// will be "\(self.identifier)|>\(another.identifier)". + /// + /// - Parameter another: An `ImageProcessor` you want to append to `self`. + /// - Returns: The new `ImageProcessor` will process the image in the order + /// of the two processors concatenated. + public func append(another: ImageProcessor) -> ImageProcessor { + let newIdentifier = identifier.appending("|>\(another.identifier)") + return GeneralProcessor(identifier: newIdentifier) { + item, options in + if let image = self.process(item: item, options: options) { + return another.process(item: .image(image), options: options) + } else { + return nil + } + } + } +} + +func ==(left: ImageProcessor, right: ImageProcessor) -> Bool { + return left.identifier == right.identifier +} + +func !=(left: ImageProcessor, right: ImageProcessor) -> Bool { + return !(left == right) +} + +typealias ProcessorImp = ((ImageProcessItem, KingfisherParsedOptionsInfo) -> Image?) +struct GeneralProcessor: ImageProcessor { + let identifier: String + let p: ProcessorImp + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + return p(item, options) + } +} + +/// The default processor. It converts the input data to a valid image. +/// Images of .PNG, .JPEG and .GIF format are supported. +/// If an image item is given as `.image` case, `DefaultImageProcessor` will +/// do nothing on it and return the associated image. +public struct DefaultImageProcessor: ImageProcessor { + + /// A default `DefaultImageProcessor` could be used across. + public static let `default` = DefaultImageProcessor() + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier = "" + + /// Creates a `DefaultImageProcessor`. Use `DefaultImageProcessor.default` to get an instance, + /// if you do not have a good reason to create your own `DefaultImageProcessor`. + public init() {} + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + case .data(let data): + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } + } +} + +/// Represents the rect corner setting when processing a round corner image. +public struct RectCorner: OptionSet { + + /// Raw value of the rect corner. + public let rawValue: Int + + /// Represents the top left corner. + public static let topLeft = RectCorner(rawValue: 1 << 0) + + /// Represents the top right corner. + public static let topRight = RectCorner(rawValue: 1 << 1) + + /// Represents the bottom left corner. + public static let bottomLeft = RectCorner(rawValue: 1 << 2) + + /// Represents the bottom right corner. + public static let bottomRight = RectCorner(rawValue: 1 << 3) + + /// Represents all corners. + public static let all: RectCorner = [.topLeft, .topRight, .bottomLeft, .bottomRight] + + /// Creates a `RectCorner` option set with a given value. + /// + /// - Parameter rawValue: The value represents a certain corner option. + public init(rawValue: Int) { + self.rawValue = rawValue + } + + var cornerIdentifier: String { + if self == .all { + return "" + } + return "_corner(\(rawValue))" + } +} + +#if !os(macOS) +/// Processor for adding an blend mode to images. Only CG-based images are supported. +public struct BlendImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Blend Mode will be used to blend the input image. + public let blendMode: CGBlendMode + + /// Alpha will be used when blend image. + public let alpha: CGFloat + + /// Background color of the output image. If `nil`, it will stay transparent. + public let backgroundColor: Color? + + /// Creates a `BlendImageProcessor`. + /// + /// - Parameters: + /// - blendMode: Blend Mode will be used to blend the input image. + /// - alpha: Alpha will be used when blend image. From 0.0 to 1.0. 1.0 means solid image, + /// 0.0 means transparent image (not visible at all). Default is 1.0. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + public init(blendMode: CGBlendMode, alpha: CGFloat = 1.0, backgroundColor: Color? = nil) { + self.blendMode = blendMode + self.alpha = alpha + self.backgroundColor = backgroundColor + var identifier = "com.onevcat.Kingfisher.BlendImageProcessor(\(blendMode.rawValue),\(alpha))" + if let color = backgroundColor { + identifier.append("_\(color.hex)") + } + self.identifier = identifier + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.image(withBlendMode: blendMode, alpha: alpha, backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} +#endif + +#if os(macOS) +/// Processor for adding an compositing operation to images. Only CG-based images are supported in macOS. +public struct CompositingImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Compositing operation will be used to the input image. + public let compositingOperation: NSCompositingOperation + + /// Alpha will be used when compositing image. + public let alpha: CGFloat + + /// Background color of the output image. If `nil`, it will stay transparent. + public let backgroundColor: Color? + + /// Creates a `CompositingImageProcessor` + /// + /// - Parameters: + /// - compositingOperation: Compositing operation will be used to the input image. + /// - alpha: Alpha will be used when compositing image. + /// From 0.0 to 1.0. 1.0 means solid image, 0.0 means transparent image. + /// Default is 1.0. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + public init(compositingOperation: NSCompositingOperation, + alpha: CGFloat = 1.0, + backgroundColor: Color? = nil) + { + self.compositingOperation = compositingOperation + self.alpha = alpha + self.backgroundColor = backgroundColor + var identifier = "com.onevcat.Kingfisher.CompositingImageProcessor(\(compositingOperation.rawValue),\(alpha))" + if let color = backgroundColor { + identifier.append("_\(color.hex)") + } + self.identifier = identifier + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.image( + withCompositingOperation: compositingOperation, + alpha: alpha, + backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} +#endif + +/// Processor for making round corner images. Only CG-based images are supported in macOS, +/// if a non-CG image passed in, the processor will do nothing. +/// +/// Note: The input image will be rendered with round corner pixels removed. If the image itself does not contain +/// alpha channel (for example, a JPEG image), the processed image will contain an alpha channel in memory in order +/// to show correctly. However, when cached into disk, the image format will be respected and the alpha channel will +/// be removed. That means when you load the processed image from cache again, you will lose transparent corner. +/// You could use `FormatIndicatedCacheSerializer.png` to force Kingfisher to serialize the image to PNG format in this +/// case. +public struct RoundCornerImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Corner radius will be applied in processing. + public let cornerRadius: CGFloat + + /// The target corners which will be applied rounding. + public let roundingCorners: RectCorner + + /// Target size of output image should be. If `nil`, the image will keep its original size after processing. + public let targetSize: CGSize? + + /// Background color of the output image. If `nil`, it will use a transparent background. + public let backgroundColor: Color? + + /// Creates a `RoundCornerImageProcessor`. + /// + /// - Parameters: + /// - cornerRadius: Corner radius will be applied in processing. + /// - targetSize: Target size of output image should be. If `nil`, + /// the image will keep its original size after processing. + /// Default is `nil`. + /// - corners: The target corners which will be applied rounding. Default is `.all`. + /// - backgroundColor: Background color to apply for the output image. Default is `nil`. + public init( + cornerRadius: CGFloat, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: Color? = nil) + { + self.cornerRadius = cornerRadius + self.targetSize = targetSize + self.roundingCorners = corners + self.backgroundColor = backgroundColor + + self.identifier = { + var identifier = "" + + if let size = targetSize { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(cornerRadius)_\(size)\(corners.cornerIdentifier))" + } else { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(cornerRadius)\(corners.cornerIdentifier))" + } + if let backgroundColor = backgroundColor { + identifier += "_\(backgroundColor)" + } + + return identifier + }() + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + let size = targetSize ?? image.kf.size + return image.kf.scaled(to: options.scaleFactor) + .kf.image( + withRoundRadius: cornerRadius, + fit: size, + roundingCorners: roundingCorners, + backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} + + +/// Represents how a size adjusts itself to fit a target size. +/// +/// - none: Not scale the content. +/// - aspectFit: Scales the content to fit the size of the view by maintaining the aspect ratio. +/// - aspectFill: Scales the content to fill the size of the view. +public enum ContentMode { + /// Not scale the content. + case none + /// Scales the content to fit the size of the view by maintaining the aspect ratio. + case aspectFit + /// Scales the content to fill the size of the view. + case aspectFill +} + +/// Processor for resizing images. +/// If you need to resize a data represented image to a smaller size, use `DownsamplingImageProcessor` +/// instead, which is more efficient and takes less memory. +public struct ResizingImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// The reference size for resizing operation in point. + public let referenceSize: CGSize + + /// Target content mode of output image should be. + /// Default is `.none`. + public let targetContentMode: ContentMode + + /// Creates a `ResizingImageProcessor`. + /// + /// - Parameters: + /// - referenceSize: The reference size for resizing operation in point. + /// - mode: Target content mode of output image should be. + /// + /// - Note: + /// The instance of `ResizingImageProcessor` will follow its `mode` property + /// and try to resizing the input images to fit or fill the `referenceSize`. + /// That means if you are using a `mode` besides of `.none`, you may get an + /// image with its size not be the same as the `referenceSize`. + /// + /// **Example**: With input image size: {100, 200}, + /// `referenceSize`: {100, 100}, `mode`: `.aspectFit`, + /// you will get an output image with size of {50, 100}, which "fit"s + /// the `referenceSize`. + /// + /// If you need an output image exactly to be a specified size, append or use + /// a `CroppingImageProcessor`. + public init(referenceSize: CGSize, mode: ContentMode = .none) { + self.referenceSize = referenceSize + self.targetContentMode = mode + + if mode == .none { + self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize))" + } else { + self.identifier = "com.onevcat.Kingfisher.ResizingImageProcessor(\(referenceSize), \(mode))" + } + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.resize(to: referenceSize, for: targetContentMode) + case .data: + return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} + +/// Processor for adding blur effect to images. `Accelerate.framework` is used underhood for +/// a better performance. A simulated Gaussian blur with specified blur radius will be applied. +public struct BlurImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Blur radius for the simulated Gaussian blur. + public let blurRadius: CGFloat + + /// Creates a `BlurImageProcessor` + /// + /// - parameter blurRadius: Blur radius for the simulated Gaussian blur. + public init(blurRadius: CGFloat) { + self.blurRadius = blurRadius + self.identifier = "com.onevcat.Kingfisher.BlurImageProcessor(\(blurRadius))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + let radius = blurRadius * options.scaleFactor + return image.kf.scaled(to: options.scaleFactor) + .kf.blurred(withRadius: radius) + case .data: + return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} + +/// Processor for adding an overlay to images. Only CG-based images are supported in macOS. +public struct OverlayImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Overlay color will be used to overlay the input image. + public let overlay: Color + + /// Fraction will be used when overlay the color to image. + public let fraction: CGFloat + + /// Creates an `OverlayImageProcessor` + /// + /// - parameter overlay: Overlay color will be used to overlay the input image. + /// - parameter fraction: Fraction will be used when overlay the color to image. + /// From 0.0 to 1.0. 0.0 means solid color, 1.0 means transparent overlay. + public init(overlay: Color, fraction: CGFloat = 0.5) { + self.overlay = overlay + self.fraction = fraction + self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.hex)_\(fraction))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.overlaying(with: overlay, fraction: fraction) + case .data: + return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} + +/// Processor for tint images with color. Only CG-based images are supported. +public struct TintImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Tint color will be used to tint the input image. + public let tint: Color + + /// Creates a `TintImageProcessor` + /// + /// - parameter tint: Tint color will be used to tint the input image. + public init(tint: Color) { + self.tint = tint + self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.hex))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.tinted(with: tint) + case .data: + return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} + +/// Processor for applying some color control to images. Only CG-based images are supported. +/// watchOS is not supported. +public struct ColorControlsProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Brightness changing to image. + public let brightness: CGFloat + + /// Contrast changing to image. + public let contrast: CGFloat + + /// Saturation changing to image. + public let saturation: CGFloat + + /// InputEV changing to image. + public let inputEV: CGFloat + + /// Creates a `ColorControlsProcessor` + /// + /// - Parameters: + /// - brightness: Brightness changing to image. + /// - contrast: Contrast changing to image. + /// - saturation: Saturation changing to image. + /// - inputEV: InputEV changing to image. + public init(brightness: CGFloat, contrast: CGFloat, saturation: CGFloat, inputEV: CGFloat) { + self.brightness = brightness + self.contrast = contrast + self.saturation = saturation + self.inputEV = inputEV + self.identifier = "com.onevcat.Kingfisher.ColorControlsProcessor(\(brightness)_\(contrast)_\(saturation)_\(inputEV))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.adjusted(brightness: brightness, contrast: contrast, saturation: saturation, inputEV: inputEV) + case .data: + return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} + +/// Processor for applying black and white effect to images. Only CG-based images are supported. +/// watchOS is not supported. +public struct BlackWhiteProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier = "com.onevcat.Kingfisher.BlackWhiteProcessor" + + /// Creates a `BlackWhiteProcessor` + public init() {} + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + return ColorControlsProcessor(brightness: 0.0, contrast: 1.0, saturation: 0.0, inputEV: 0.7) + .process(item: item, options: options) + } +} + +/// Processor for cropping an image. Only CG-based images are supported. +/// watchOS is not supported. +public struct CroppingImageProcessor: ImageProcessor { + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Target size of output image should be. + public let size: CGSize + + /// Anchor point from which the output size should be calculate. + /// The anchor point is consisted by two values between 0.0 and 1.0. + /// It indicates a related point in current image. + /// See `CroppingImageProcessor.init(size:anchor:)` for more. + public let anchor: CGPoint + + /// Creates a `CroppingImageProcessor`. + /// + /// - Parameters: + /// - size: Target size of output image should be. + /// - anchor: The anchor point from which the size should be calculated. + /// Default is `CGPoint(x: 0.5, y: 0.5)`, which means the center of input image. + /// - Note: + /// The anchor point is consisted by two values between 0.0 and 1.0. + /// It indicates a related point in current image, eg: (0.0, 0.0) for top-left + /// corner, (0.5, 0.5) for center and (1.0, 1.0) for bottom-right corner. + /// The `size` property of `CroppingImageProcessor` will be used along with + /// `anchor` to calculate a target rectangle in the size of image. + /// + /// The target size will be automatically calculated with a reasonable behavior. + /// For example, when you have an image size of `CGSize(width: 100, height: 100)`, + /// and a target size of `CGSize(width: 20, height: 20)`: + /// - with a (0.0, 0.0) anchor (top-left), the crop rect will be `{0, 0, 20, 20}`; + /// - with a (0.5, 0.5) anchor (center), it will be `{40, 40, 20, 20}` + /// - while with a (1.0, 1.0) anchor (bottom-right), it will be `{80, 80, 20, 20}` + public init(size: CGSize, anchor: CGPoint = CGPoint(x: 0.5, y: 0.5)) { + self.size = size + self.anchor = anchor + self.identifier = "com.onevcat.Kingfisher.CroppingImageProcessor(\(size)_\(anchor))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + return image.kf.scaled(to: options.scaleFactor) + .kf.crop(to: size, anchorOn: anchor) + case .data: return (DefaultImageProcessor.default >> self).process(item: item, options: options) + } + } +} + +/// Processor for downsampling an image. Compared to `ResizingImageProcessor`, this processor +/// does not render the images to resize. Instead, it downsample the input data directly to an +/// image. It is a more efficient than `ResizingImageProcessor`. +/// +/// Only CG-based images are supported. Animated images (like GIF) is not supported. +public struct DownsamplingImageProcessor: ImageProcessor { + + /// Target size of output image should be. It should be smaller than the size of + /// input image. If it is larger, the result image will be the same size of input + /// data without downsampling. + public let size: CGSize + + /// Identifier of the processor. + /// - Note: See documentation of `ImageProcessor` protocol for more. + public let identifier: String + + /// Creates a `DownsamplingImageProcessor`. + /// + /// - Parameter size: The target size of the downsample operation. + public init(size: CGSize) { + self.size = size + self.identifier = "com.onevcat.Kingfisher.DownsamplingImageProcessor(\(size))" + } + + /// Processes the input `ImageProcessItem` with this processor. + /// + /// - Parameters: + /// - item: Input item which will be processed by `self`. + /// - options: Options when processing the item. + /// - Returns: The processed image. + /// + /// - Note: See documentation of `ImageProcessor` protocol for more. + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> Image? { + switch item { + case .image(let image): + guard let data = image.kf.data(format: .unknown) else { + return nil + } + return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) + case .data(let data): + return KingfisherWrapper.downsampledImage(data: data, to: size, scale: options.scaleFactor) + } + } +} + +/// Concatenates two `ImageProcessor`s. `ImageProcessor.append(another:)` is used internally. +/// +/// - Parameters: +/// - left: The first processor. +/// - right: The second processor. +/// - Returns: The concatenated processor. +public func >>(left: ImageProcessor, right: ImageProcessor) -> ImageProcessor { + return left.append(another: right) +} + +extension Color { + var hex: String { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + #if os(macOS) + (usingColorSpace(.sRGB) ?? self).getRed(&r, green: &g, blue: &b, alpha: &a) + #else + getRed(&r, green: &g, blue: &b, alpha: &a) + #endif + + let rInt = Int(r * 255) << 24 + let gInt = Int(g * 255) << 16 + let bInt = Int(b * 255) << 8 + let aInt = Int(a * 255) + + let rgba = rInt | gInt | bInt | aInt + + return String(format:"#%08x", rgba) + } +} diff --git a/Pods/Kingfisher/Sources/Image/ImageProgressive.swift b/Pods/Kingfisher/Sources/Image/ImageProgressive.swift new file mode 100644 index 0000000..2331a50 --- /dev/null +++ b/Pods/Kingfisher/Sources/Image/ImageProgressive.swift @@ -0,0 +1,309 @@ +// +// ImageProgressive.swift +// Kingfisher +// +// Created by lixiang on 2019/5/10. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CoreGraphics + +private let sharedProcessingQueue: CallbackQueue = + .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) + +public struct ImageProgressive { + + /// A default `ImageProgressive` could be used across. + public static let `default` = ImageProgressive( + isBlur: true, + isFastestScan: true, + scanInterval: 0 + ) + + /// Whether to enable blur effect processing + let isBlur: Bool + /// Whether to enable the fastest scan + let isFastestScan: Bool + /// Minimum time interval for each scan + let scanInterval: TimeInterval + + public init(isBlur: Bool, + isFastestScan: Bool, + scanInterval: TimeInterval) { + self.isBlur = isBlur + self.isFastestScan = isFastestScan + self.scanInterval = scanInterval + } +} + +protocol ImageSettable: AnyObject { + var image: Image? { get set } +} + +final class ImageProgressiveProvider: DataReceivingSideEffect { + + var onShouldApply: () -> Bool = { return true } + + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { + update(data: task.mutableData, with: task.callbacks) + } + + private let option: ImageProgressive + private let refresh: (Image) -> Void + + private let decoder: ImageProgressiveDecoder + private let queue = ImageProgressiveSerialQueue() + + init?(_ options: KingfisherParsedOptionsInfo, + refresh: @escaping (Image) -> Void) { + guard let option = options.progressiveJPEG else { return nil } + + self.option = option + self.refresh = refresh + self.decoder = ImageProgressiveDecoder( + option, + processingQueue: options.processingQueue ?? sharedProcessingQueue, + creatingOptions: options.imageCreatingOptions + ) + } + + func update(data: Data, with callbacks: [SessionDataTask.TaskCallback]) { + guard !data.isEmpty else { return } + + queue.add(minimum: option.scanInterval) { completion in + guard self.onShouldApply() else { + self.queue.clean() + completion() + return + } + + func decode(_ data: Data) { + self.decoder.decode(data, with: callbacks) { image in + defer { completion() } + guard self.onShouldApply() else { return } + guard let image = image else { return } + self.refresh(image) + } + } + + if self.option.isFastestScan { + decode(self.decoder.scanning(data) ?? Data()) + + } else { + self.decoder.scanning(data).forEach { decode($0) } + } + } + } +} + +private final class ImageProgressiveDecoder { + + private let option: ImageProgressive + private let processingQueue: CallbackQueue + private let creatingOptions: ImageCreatingOptions + private(set) var scannedCount = 0 + private(set) var scannedIndex = -1 + + init(_ option: ImageProgressive, + processingQueue: CallbackQueue, + creatingOptions: ImageCreatingOptions) { + self.option = option + self.processingQueue = processingQueue + self.creatingOptions = creatingOptions + } + + func scanning(_ data: Data) -> [Data] { + guard data.kf.contains(jpeg: .SOF2) else { + return [] + } + guard scannedIndex + 1 < data.count else { + return [] + } + + var datas: [Data] = [] + var index = scannedIndex + 1 + var count = scannedCount + + while index < data.count - 1 { + scannedIndex = index + // 0xFF, 0xDA - Start Of Scan + let SOS = ImageFormat.JPEGMarker.SOS.bytes + if data[index] == SOS[0], data[index + 1] == SOS[1] { + if count > 0 { + datas.append(data[0 ..< index]) + } + count += 1 + } + index += 1 + } + + // Found more scans this the previous time + guard count > scannedCount else { return [] } + scannedCount = count + + // `> 1` checks that we've received a first scan (SOS) and then received + // and also received a second scan (SOS). This way we know that we have + // at least one full scan available. + guard count > 1 else { return [] } + return datas + } + + func scanning(_ data: Data) -> Data? { + guard data.kf.contains(jpeg: .SOF2) else { + return nil + } + guard scannedIndex + 1 < data.count else { + return nil + } + + var index = scannedIndex + 1 + var count = scannedCount + var lastSOSIndex = 0 + + while index < data.count - 1 { + scannedIndex = index + // 0xFF, 0xDA - Start Of Scan + let SOS = ImageFormat.JPEGMarker.SOS.bytes + if data[index] == SOS[0], data[index + 1] == SOS[1] { + lastSOSIndex = index + count += 1 + } + index += 1 + } + + // Found more scans this the previous time + guard count > scannedCount else { return nil } + scannedCount = count + + // `> 1` checks that we've received a first scan (SOS) and then received + // and also received a second scan (SOS). This way we know that we have + // at least one full scan available. + guard count > 1 && lastSOSIndex > 0 else { return nil } + return data[0 ..< lastSOSIndex] + } + + func decode(_ data: Data, + with callbacks: [SessionDataTask.TaskCallback], + completion: @escaping (Image?) -> Void) { + guard data.kf.contains(jpeg: .SOF2) else { + CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } + return + } + + func processing(_ data: Data) { + let processor = ImageDataProcessor( + data: data, + callbacks: callbacks, + processingQueue: processingQueue + ) + processor.onImageProcessed.delegate(on: self) { (self, result) in + guard let image = try? result.0.get() else { + CallbackQueue.mainCurrentOrAsync.execute { completion(nil) } + return + } + + CallbackQueue.mainCurrentOrAsync.execute { completion(image) } + } + processor.process() + } + + // Blur partial images. + let count = scannedCount + + if option.isBlur, count < 6 { + processingQueue.execute { + // Progressively reduce blur as we load more scans. + let image = KingfisherWrapper<Image>.image( + data: data, + options: self.creatingOptions + ) + let radius = max(2, 14 - count * 4) + let temp = image?.kf.blurred(withRadius: CGFloat(radius)) + processing(temp?.kf.data(format: .JPEG) ?? data) + } + + } else { + processing(data) + } + } +} + +private final class ImageProgressiveSerialQueue { + typealias ClosureCallback = ((@escaping () -> Void)) -> Void + + private let queue: DispatchQueue = .init(label: "com.onevcat.Kingfisher.ImageProgressive.SerialQueue") + private var items: [DispatchWorkItem] = [] + private var notify: (() -> Void)? + private var lastTime: TimeInterval? + var count: Int { return items.count } + + func add(minimum interval: TimeInterval, closure: @escaping ClosureCallback) { + let completion = { [weak self] in + guard let self = self else { return } + + self.queue.async { [weak self] in + guard let self = self else { return } + guard !self.items.isEmpty else { return } + + self.items.removeFirst() + + if let next = self.items.first { + self.queue.asyncAfter( + deadline: .now() + interval, + execute: next + ) + + } else { + self.lastTime = Date().timeIntervalSince1970 + self.notify?() + self.notify = nil + } + } + } + + queue.async { [weak self] in + guard let self = self else { return } + + let item = DispatchWorkItem { + closure(completion) + } + if self.items.isEmpty { + let difference = Date().timeIntervalSince1970 - (self.lastTime ?? 0) + let delay = difference < interval ? interval - difference : 0 + self.queue.asyncAfter(deadline: .now() + delay, execute: item) + } + self.items.append(item) + } + } + + func notify(_ closure: @escaping () -> Void) { + self.notify = closure + } + + func clean() { + queue.async { [weak self] in + guard let self = self else { return } + self.items.forEach { $0.cancel() } + self.items.removeAll() + } + } +} diff --git a/Pods/Kingfisher/Sources/Image/ImageTransition.swift b/Pods/Kingfisher/Sources/Image/ImageTransition.swift new file mode 100644 index 0000000..c13a9d2 --- /dev/null +++ b/Pods/Kingfisher/Sources/Image/ImageTransition.swift @@ -0,0 +1,115 @@ +// +// ImageTransition.swift +// Kingfisher +// +// Created by Wei Wang on 15/9/18. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(iOS) || os(tvOS) +import UIKit + +/// Transition effect which will be used when an image downloaded and set by `UIImageView` +/// extension API in Kingfisher. You can assign an enum value with transition duration as +/// an item in `KingfisherOptionsInfo` to enable the animation transition. +/// +/// Apple's UIViewAnimationOptions is used under the hood. +/// For custom transition, you should specified your own transition options, animations and +/// completion handler as well. +/// +/// - none: No animation transition. +/// - fade: Fade in the loaded image in a given duration. +/// - flipFromLeft: Flip from left transition. +/// - flipFromRight: Flip from right transition. +/// - flipFromTop: Flip from top transition. +/// - flipFromBottom: Flip from bottom transition. +/// - custom: Custom transition. +public enum ImageTransition { + /// No animation transition. + case none + /// Fade in the loaded image in a given duration. + case fade(TimeInterval) + /// Flip from left transition. + case flipFromLeft(TimeInterval) + /// Flip from right transition. + case flipFromRight(TimeInterval) + /// Flip from top transition. + case flipFromTop(TimeInterval) + /// Flip from bottom transition. + case flipFromBottom(TimeInterval) + /// Custom transition defined by a general animation block. + /// - duration: The time duration of this custom transition. + /// - options: `UIView.AnimationOptions` should be used in the transition. + /// - animations: The animation block will be applied when setting image. + /// - completion: A block called when the transition animation finishes. + case custom(duration: TimeInterval, + options: UIView.AnimationOptions, + animations: ((UIImageView, UIImage) -> Void)?, + completion: ((Bool) -> Void)?) + + var duration: TimeInterval { + switch self { + case .none: return 0 + case .fade(let duration): return duration + + case .flipFromLeft(let duration): return duration + case .flipFromRight(let duration): return duration + case .flipFromTop(let duration): return duration + case .flipFromBottom(let duration): return duration + + case .custom(let duration, _, _, _): return duration + } + } + + var animationOptions: UIView.AnimationOptions { + switch self { + case .none: return [] + case .fade: return .transitionCrossDissolve + + case .flipFromLeft: return .transitionFlipFromLeft + case .flipFromRight: return .transitionFlipFromRight + case .flipFromTop: return .transitionFlipFromTop + case .flipFromBottom: return .transitionFlipFromBottom + + case .custom(_, let options, _, _): return options + } + } + + var animations: ((UIImageView, UIImage) -> Void)? { + switch self { + case .custom(_, _, let animations, _): return animations + default: return { $0.image = $1 } + } + } + + var completion: ((Bool) -> Void)? { + switch self { + case .custom(_, _, _, let completion): return completion + default: return nil + } + } +} +#else +// Just a placeholder for compiling on macOS. +public enum ImageTransition { + case none +} +#endif diff --git a/Pods/Kingfisher/Sources/Image/Placeholder.swift b/Pods/Kingfisher/Sources/Image/Placeholder.swift new file mode 100644 index 0000000..78b0aef --- /dev/null +++ b/Pods/Kingfisher/Sources/Image/Placeholder.swift @@ -0,0 +1,76 @@ +// +// Placeholder.swift +// Kingfisher +// +// Created by Tieme van Veen on 28/08/2017. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Represents a placeholder type which could be set while loading as well as +/// loading finished without getting an image. +public protocol Placeholder { + + /// How the placeholder should be added to a given image view. + func add(to imageView: ImageView) + + /// How the placeholder should be removed from a given image view. + func remove(from imageView: ImageView) +} + +/// Default implementation of an image placeholder. The image will be set or +/// reset directly for `image` property of the image view. +extension Image: Placeholder { + /// How the placeholder should be added to a given image view. + public func add(to imageView: ImageView) { imageView.image = self } + + /// How the placeholder should be removed from a given image view. + public func remove(from imageView: ImageView) { imageView.image = nil } +} + +/// Default implementation of an arbitrary view as placeholder. The view will be +/// added as a subview when adding and be removed from its super view when removing. +/// +/// To use your customize View type as placeholder, simply let it conforming to +/// `Placeholder` by `extension MyView: Placeholder {}`. +extension Placeholder where Self: View { + + /// How the placeholder should be added to a given image view. + public func add(to imageView: ImageView) { + imageView.addSubview(self) + translatesAutoresizingMaskIntoConstraints = false + + centerXAnchor.constraint(equalTo: imageView.centerXAnchor).isActive = true + centerYAnchor.constraint(equalTo: imageView.centerYAnchor).isActive = true + heightAnchor.constraint(equalTo: imageView.heightAnchor).isActive = true + widthAnchor.constraint(equalTo: imageView.widthAnchor).isActive = true + } + + /// How the placeholder should be removed from a given image view. + public func remove(from imageView: ImageView) { + removeFromSuperview() + } +} diff --git a/Pods/Kingfisher/Sources/Kingfisher.h b/Pods/Kingfisher/Sources/Kingfisher.h new file mode 100644 index 0000000..356adde --- /dev/null +++ b/Pods/Kingfisher/Sources/Kingfisher.h @@ -0,0 +1,37 @@ +// +// Kingfisher.h +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#import <Foundation/Foundation.h> + +//! Project version number for Kingfisher. +FOUNDATION_EXPORT double KingfisherVersionNumber; + +//! Project version string for Kingfisher. +FOUNDATION_EXPORT const unsigned char KingfisherVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import <Kingfisher/PublicHeader.h> + + diff --git a/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift b/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift new file mode 100644 index 0000000..5f6fc57 --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift @@ -0,0 +1,91 @@ +// +// AuthenticationChallengeResponsable.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Protocol indicates that an authentication challenge could be handled. +public protocol AuthenticationChallengeResponsable: AnyObject { + + /// Called when a session level authentication challenge is received. + /// This method provide a chance to handle and response to the authentication + /// challenge before downloading could start. + /// + /// - Parameters: + /// - downloader: The downloader which receives this challenge. + /// - challenge: An object that contains the request for authentication. + /// - completionHandler: A handler that your delegate method must call. + /// + /// - Note: This method is a forward from `URLSessionDelegate.urlSession(:didReceiveChallenge:completionHandler:)`. + /// Please refer to the document of it in `URLSessionDelegate`. + func downloader( + _ downloader: ImageDownloader, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + + /// Called when a task level authentication challenge is received. + /// This method provide a chance to handle and response to the authentication + /// challenge before downloading could start. + /// + /// - Parameters: + /// - downloader: The downloader which receives this challenge. + /// - task: The task whose request requires authentication. + /// - challenge: An object that contains the request for authentication. + /// - completionHandler: A handler that your delegate method must call. + func downloader( + _ downloader: ImageDownloader, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) +} + +extension AuthenticationChallengeResponsable { + + public func downloader( + _ downloader: ImageDownloader, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust { + if let trustedHosts = downloader.trustedHosts, trustedHosts.contains(challenge.protectionSpace.host) { + let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!) + completionHandler(.useCredential, credential) + return + } + } + + completionHandler(.performDefaultHandling, nil) + } + + public func downloader( + _ downloader: ImageDownloader, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + completionHandler(.performDefaultHandling, nil) + } + +} diff --git a/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift b/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift new file mode 100644 index 0000000..057334f --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift @@ -0,0 +1,80 @@ +// +// ImageDataProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +private let sharedProcessingQueue: CallbackQueue = + .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) + +// Handles image processing work on an own process queue. +class ImageDataProcessor { + let data: Data + let callbacks: [SessionDataTask.TaskCallback] + let queue: CallbackQueue + + // Note: We have an optimization choice there, to reduce queue dispatch by checking callback + // queue settings in each option... + let onImageProcessed = Delegate<(Result<Image, KingfisherError>, SessionDataTask.TaskCallback), Void>() + + init(data: Data, callbacks: [SessionDataTask.TaskCallback], processingQueue: CallbackQueue?) { + self.data = data + self.callbacks = callbacks + self.queue = processingQueue ?? sharedProcessingQueue + } + + func process() { + queue.execute(doProcess) + } + + private func doProcess() { + var processedImages = [String: Image]() + for callback in callbacks { + let processor = callback.options.processor + var image = processedImages[processor.identifier] + if image == nil { + image = processor.process(item: .data(data), options: callback.options) + processedImages[processor.identifier] = image + } + + let result: Result<Image, KingfisherError> + if let image = image { + var finalImage = image + if let imageModifier = callback.options.imageModifier { + finalImage = imageModifier.modify(image) + } + if callback.options.backgroundDecode { + finalImage = finalImage.kf.decoded + } + result = .success(finalImage) + } else { + let error = KingfisherError.processorError( + reason: .processingFailed(processor: processor, item: .data(data))) + result = .failure(error) + } + onImageProcessed.call((result, callback)) + } + } +} diff --git a/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift b/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift new file mode 100644 index 0000000..33fb496 --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift @@ -0,0 +1,369 @@ +// +// ImageDownloader.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Represents a success result of an image downloading progress. +public struct ImageLoadingResult { + + /// The downloaded image. + public let image: Image + + /// Original URL of the image request. + public let url: URL? + + /// The raw data received from downloader. + public let originalData: Data +} + +/// Represents a task of an image downloading process. +public struct DownloadTask { + + /// The `SessionDataTask` object bounded to this download task. Multiple `DownloadTask`s could refer + /// to a same `sessionTask`. This is an optimization in Kingfisher to prevent multiple downloading task + /// for the same URL resource at the same time. + /// + /// When you `cancel` a `DownloadTask`, this `SessionDataTask` and its cancel token will be pass through. + /// You can use them to identify the cancelled task. + public let sessionTask: SessionDataTask + + /// The cancel token which is used to cancel the task. This is only for identify the task when it is cancelled. + /// To cancel a `DownloadTask`, use `cancel` instead. + public let cancelToken: SessionDataTask.CancelToken + + /// Cancel this task if it is running. It will do nothing if this task is not running. + /// + /// - Note: + /// In Kingfisher, there is an optimization to prevent starting another download task if the target URL is being + /// downloading. However, even when internally no new session task created, a `DownloadTask` will be still created + /// and returned when you call related methods, but it will share the session downloading task with a previous task. + /// In this case, if multiple `DownloadTask`s share a single session download task, cancelling a `DownloadTask` + /// does not affect other `DownloadTask`s. + /// + /// If you need to cancel all `DownloadTask`s of a url, use `ImageDownloader.cancel(url:)`. If you need to cancel + /// all downloading tasks of an `ImageDownloader`, use `ImageDownloader.cancelAll()`. + public func cancel() { + sessionTask.cancel(token: cancelToken) + } +} + +extension DownloadTask { + enum WrappedTask { + case download(DownloadTask) + case dataProviding + + func cancel() { + switch self { + case .download(let task): task.cancel() + case .dataProviding: break + } + } + + var value: DownloadTask? { + switch self { + case .download(let task): return task + case .dataProviding: return nil + } + } + } +} + +/// Represents a downloading manager for requesting the image with a URL from server. +open class ImageDownloader { + + // MARK: Singleton + /// The default downloader. + public static let `default` = ImageDownloader(name: "default") + + // MARK: Public Properties + /// The duration before the downloading is timeout. Default is 15 seconds. + open var downloadTimeout: TimeInterval = 15.0 + + /// A set of trusted hosts when receiving server trust challenges. A challenge with host name contained in this + /// set will be ignored. You can use this set to specify the self-signed site. It only will be used if you don't + /// specify the `authenticationChallengeResponder`. + /// + /// If `authenticationChallengeResponder` is set, this property will be ignored and the implementation of + /// `authenticationChallengeResponder` will be used instead. + open var trustedHosts: Set<String>? + + /// Use this to set supply a configuration for the downloader. By default, + /// NSURLSessionConfiguration.ephemeralSessionConfiguration() will be used. + /// + /// You could change the configuration before a downloading task starts. + /// A configuration without persistent storage for caches is requested for downloader working correctly. + open var sessionConfiguration = URLSessionConfiguration.ephemeral { + didSet { + session.invalidateAndCancel() + session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + } + } + + /// Whether the download requests should use pipeline or not. Default is false. + open var requestsUsePipelining = false + + /// Delegate of this `ImageDownloader` object. See `ImageDownloaderDelegate` protocol for more. + open weak var delegate: ImageDownloaderDelegate? + + /// A responder for authentication challenge. + /// Downloader will forward the received authentication challenge for the downloading session to this responder. + open weak var authenticationChallengeResponder: AuthenticationChallengeResponsable? + + private let name: String + private let sessionDelegate: SessionDelegate + private var session: URLSession + + // MARK: Initializers + + /// Creates a downloader with name. + /// + /// - Parameter name: The name for the downloader. It should not be empty. + public init(name: String) { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the downloader. " + + "A downloader with empty name is not permitted.") + } + + self.name = name + + sessionDelegate = SessionDelegate() + session = URLSession( + configuration: sessionConfiguration, + delegate: sessionDelegate, + delegateQueue: nil) + + authenticationChallengeResponder = self + setupSessionHandler() + } + + deinit { session.invalidateAndCancel() } + + private func setupSessionHandler() { + sessionDelegate.onReceiveSessionChallenge.delegate(on: self) { (self, invoke) in + self.authenticationChallengeResponder?.downloader(self, didReceive: invoke.1, completionHandler: invoke.2) + } + sessionDelegate.onReceiveSessionTaskChallenge.delegate(on: self) { (self, invoke) in + self.authenticationChallengeResponder?.downloader( + self, task: invoke.1, didReceive: invoke.2, completionHandler: invoke.3) + } + sessionDelegate.onValidStatusCode.delegate(on: self) { (self, code) in + return (self.delegate ?? self).isValidStatusCode(code, for: self) + } + sessionDelegate.onDownloadingFinished.delegate(on: self) { (self, value) in + let (url, result) = value + do { + let value = try result.get() + self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: value, error: nil) + } catch { + self.delegate?.imageDownloader(self, didFinishDownloadingImageForURL: url, with: nil, error: error) + } + } + sessionDelegate.onDidDownloadData.delegate(on: self) { (self, task) in + guard let url = task.task.originalRequest?.url else { + return task.mutableData + } + return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, for: url) + } + } + + @discardableResult + func downloadImage( + with url: URL, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? + { + // Creates default request. + var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout) + request.httpShouldUsePipelining = requestsUsePipelining + + if let requestModifier = options.requestModifier { + // Modifies request before sending. + guard let r = requestModifier.modified(for: request) else { + options.callbackQueue.execute { + completionHandler?(.failure(KingfisherError.requestError(reason: .emptyRequest))) + } + return nil + } + request = r + } + + // There is a possibility that request modifier changed the url to `nil` or empty. + // In this case, throw an error. + guard let url = request.url, !url.absoluteString.isEmpty else { + options.callbackQueue.execute { + completionHandler?(.failure(KingfisherError.requestError(reason: .invalidURL(request: request)))) + } + return nil + } + + // Wraps `completionHandler` to `onCompleted` respectively. + + let onCompleted = completionHandler.map { + block -> Delegate<Result<ImageLoadingResult, KingfisherError>, Void> in + let delegate = Delegate<Result<ImageLoadingResult, KingfisherError>, Void>() + delegate.delegate(on: self) { (_, callback) in + block(callback) + } + return delegate + } + + // SessionDataTask.TaskCallback is a wrapper for `onCompleted` and `options` (for processor info) + let callback = SessionDataTask.TaskCallback( + onCompleted: onCompleted, + options: options + ) + + // Ready to start download. Add it to session task manager (`sessionHandler`) + + let downloadTask: DownloadTask + if let existingTask = sessionDelegate.task(for: url) { + downloadTask = sessionDelegate.append(existingTask, url: url, callback: callback) + } else { + let sessionDataTask = session.dataTask(with: request) + sessionDataTask.priority = options.downloadPriority + downloadTask = sessionDelegate.add(sessionDataTask, url: url, callback: callback) + } + + let sessionTask = downloadTask.sessionTask + + // Start the session task if not started yet. + if !sessionTask.started { + sessionTask.onTaskDone.delegate(on: self) { (self, done) in + // Underlying downloading finishes. + // result: Result<(Data, URLResponse?)>, callbacks: [TaskCallback] + let (result, callbacks) = done + + // Before processing the downloaded data. + do { + let value = try result.get() + self.delegate?.imageDownloader( + self, + didFinishDownloadingImageForURL: url, + with: value.1, + error: nil + ) + } catch { + self.delegate?.imageDownloader( + self, + didFinishDownloadingImageForURL: url, + with: nil, + error: error + ) + } + + switch result { + // Download finished. Now process the data to an image. + case .success(let (data, response)): + let processor = ImageDataProcessor( + data: data, callbacks: callbacks, processingQueue: options.processingQueue) + processor.onImageProcessed.delegate(on: self) { (self, result) in + // `onImageProcessed` will be called for `callbacks.count` times, with each + // `SessionDataTask.TaskCallback` as the input parameter. + // result: Result<Image>, callback: SessionDataTask.TaskCallback + let (result, callback) = result + + if let image = try? result.get() { + self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response) + } + + let imageResult = result.map { ImageLoadingResult(image: $0, url: url, originalData: data) } + let queue = callback.options.callbackQueue + queue.execute { callback.onCompleted?.call(imageResult) } + } + processor.process() + + case .failure(let error): + callbacks.forEach { callback in + let queue = callback.options.callbackQueue + queue.execute { callback.onCompleted?.call(.failure(error)) } + } + } + } + delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request) + sessionTask.resume() + } + return downloadTask + } + + // MARK: Dowloading Task + /// Downloads an image with a URL and option. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. + /// - progressBlock: Called when the download progress updated. This block will be always be called in main queue. + /// - completionHandler: Called when the download progress finishes. This block will be called in the queue + /// defined in `.callbackQueue` in `options` parameter. + /// - Returns: A downloading task. You could call `cancel` on it to stop the download task. + @discardableResult + open func downloadImage( + with url: URL, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result<ImageLoadingResult, KingfisherError>) -> Void)? = nil) -> DownloadTask? + { + var info = KingfisherParsedOptionsInfo(options) + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return downloadImage( + with: url, + options: info, + completionHandler: completionHandler) + } +} + +// MARK: Cancelling Task +extension ImageDownloader { + + /// Cancel all downloading tasks for this `ImageDownloader`. It will trigger the completion handlers + /// for all not-yet-finished downloading tasks. + /// + /// If you need to only cancel a certain task, call `cancel()` on the `DownloadTask` + /// returned by the downloading methods. If you need to cancel all `DownloadTask`s of a certain url, + /// use `ImageDownloader.cancel(url:)`. + public func cancelAll() { + sessionDelegate.cancelAll() + } + + /// Cancel all downloading tasks for a given URL. It will trigger the completion handlers for + /// all not-yet-finished downloading tasks for the URL. + /// + /// - Parameter url: The URL which you want to cancel downloading. + public func cancel(url: URL) { + sessionDelegate.cancel(url: url) + } +} + +// Use the default implementation from extension of `AuthenticationChallengeResponsable`. +extension ImageDownloader: AuthenticationChallengeResponsable {} + +// Use the default implementation from extension of `ImageDownloaderDelegate`. +extension ImageDownloader: ImageDownloaderDelegate {} diff --git a/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift b/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift new file mode 100644 index 0000000..8efc824 --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift @@ -0,0 +1,127 @@ +// +// ImageDownloaderDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Protocol of `ImageDownloader`. This protocol provides a set of methods which are related to image downloader +/// working stages and rules. +public protocol ImageDownloaderDelegate: AnyObject { + + /// Called when the `ImageDownloader` object will start downloading an image from a specified URL. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - url: URL of the starting request. + /// - request: The request object for the download process. + /// + func imageDownloader(_ downloader: ImageDownloader, willDownloadImageForURL url: URL, with request: URLRequest?) + + /// Called when the `ImageDownloader` completes a downloading request with success or failure. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - url: URL of the original request URL. + /// - response: The response object of the downloading process. + /// - error: The error in case of failure. + /// + func imageDownloader( + _ downloader: ImageDownloader, + didFinishDownloadingImageForURL url: URL, + with response: URLResponse?, + error: Error?) + + /// Called when the `ImageDownloader` object successfully downloaded image data from specified URL. This is + /// your last chance to verify or modify the downloaded data before Kingfisher tries to perform addition + /// processing on the image data. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - data: The original downloaded data. + /// - url: The URL of the original request URL. + /// - Returns: The data from which Kingfisher should use to create an image. You need to provide valid data + /// which content is one of the supported image file format. Kingfisher will perform process on this + /// data and try to convert it to an image object. + /// - Note: + /// This can be used to pre-process raw image data before creation of `Image` instance (i.e. + /// decrypting or verification). If `nil` returned, the processing is interrupted and a `KingfisherError` with + /// `ResponseErrorReason.dataModifyingFailed` will be raised. You could use this fact to stop the image + /// processing flow if you find the data is corrupted or malformed. + func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? + + /// Called when the `ImageDownloader` object successfully downloads and processes an image from specified URL. + /// + /// - Parameters: + /// - downloader: The `ImageDownloader` object which is used for the downloading operation. + /// - image: The downloaded and processed image. + /// - url: URL of the original request URL. + /// - response: The original response object of the downloading process. + /// + func imageDownloader( + _ downloader: ImageDownloader, + didDownload image: Image, + for url: URL, + with response: URLResponse?) + + /// Checks if a received HTTP status code is valid or not. + /// By default, a status code in range 200..<400 is considered as valid. + /// If an invalid code is received, the downloader will raise an `KingfisherError` with + /// `ResponseErrorReason.invalidHTTPStatusCode` as its reason. + /// + /// - Parameters: + /// - code: The received HTTP status code. + /// - downloader: The `ImageDownloader` object asks for validate status code. + /// - Returns: Returns a value to indicate whether this HTTP status code is valid or not. + /// - Note: If the default 200 to 400 valid code does not suit your need, + /// you can implement this method to change that behavior. + func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool +} + +// Default implementation for `ImageDownloaderDelegate`. +extension ImageDownloaderDelegate { + public func imageDownloader( + _ downloader: ImageDownloader, + willDownloadImageForURL url: URL, + with request: URLRequest?) {} + + public func imageDownloader( + _ downloader: ImageDownloader, + didFinishDownloadingImageForURL url: URL, + with response: URLResponse?, + error: Error?) {} + + public func imageDownloader( + _ downloader: ImageDownloader, + didDownload image: Image, + for url: URL, + with response: URLResponse?) {} + + public func isValidStatusCode(_ code: Int, for downloader: ImageDownloader) -> Bool { + return (200..<400).contains(code) + } + public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? { + return data + } +} diff --git a/Pods/Kingfisher/Sources/Networking/ImageModifier.swift b/Pods/Kingfisher/Sources/Networking/ImageModifier.swift new file mode 100644 index 0000000..6953bf7 --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/ImageModifier.swift @@ -0,0 +1,116 @@ +// +// ImageModifier.swift +// Kingfisher +// +// Created by Ethan Gill on 2017/11/28. +// +// Copyright (c) 2019 Ethan Gill <ethan.gill@me.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// An `ImageModifier` can be used to change properties on an image in between +/// cache serialization and use of the image. The modified returned image will be +/// only used for current rendering purpose, the serialization data will not contain +/// the changes applied by the `ImageModifier`. +public protocol ImageModifier { + /// Modify an input `Image`. + /// + /// - parameter image: Image which will be modified by `self` + /// + /// - returns: The modified image. + /// + /// - Note: The return value will be unmodified if modifying is not possible on + /// the current platform. + /// - Note: Most modifiers support UIImage or NSImage, but not CGImage. + func modify(_ image: Image) -> Image +} + +/// A wrapper for creating an `ImageModifier` easier. +/// This type conforms to `ImageModifier` and wraps an image modify block. +/// If the `block` throws an error, the original image will be used. +public struct AnyImageModifier: ImageModifier { + + /// A block which modifies images, or returns the original image + /// if modification cannot be performed with an error. + let block: (Image) throws -> Image + + /// Creates an `AnyImageModifier` with a given `modify` block. + public init(modify: @escaping (Image) throws -> Image) { + block = modify + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: Image) -> Image { + return (try? block(image)) ?? image + } +} + +#if os(iOS) || os(tvOS) || os(watchOS) +import UIKit + +/// Modifier for setting the rendering mode of images. +public struct RenderingModeImageModifier: ImageModifier { + + /// The rendering mode to apply to the image. + public let renderingMode: UIImage.RenderingMode + + /// Creates a `RenderingModeImageModifier`. + /// + /// - Parameter renderingMode: The rendering mode to apply to the image. Default is `.automatic`. + public init(renderingMode: UIImage.RenderingMode = .automatic) { + self.renderingMode = renderingMode + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: Image) -> Image { + return image.withRenderingMode(renderingMode) + } +} + +/// Modifier for setting the `flipsForRightToLeftLayoutDirection` property of images. +public struct FlipsForRightToLeftLayoutDirectionImageModifier: ImageModifier { + + /// Creates a `FlipsForRightToLeftLayoutDirectionImageModifier`. + public init() {} + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: Image) -> Image { + return image.imageFlippedForRightToLeftLayoutDirection() + } +} + +/// Modifier for setting the `alignmentRectInsets` property of images. +public struct AlignmentRectInsetsImageModifier: ImageModifier { + + /// The alignment insets to apply to the image + public let alignmentInsets: UIEdgeInsets + + /// Creates an `AlignmentRectInsetsImageModifier`. + public init(alignmentInsets: UIEdgeInsets) { + self.alignmentInsets = alignmentInsets + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: Image) -> Image { + return image.withAlignmentRectInsets(alignmentInsets) + } +} +#endif diff --git a/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift b/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift new file mode 100644 index 0000000..f83b352 --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift @@ -0,0 +1,368 @@ +// +// ImagePrefetcher.swift +// Kingfisher +// +// Created by Claire Knight <claire.knight@moggytech.co.uk> on 24/02/2016 +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +/// Progress update block of prefetcher when initialized with a list of resources. +/// +/// - `skippedResources`: An array of resources that are already cached before the prefetching starting. +/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while +/// downloading, encountered an error when downloading or the download not being started at all. +/// - `completedResources`: An array of resources that are downloaded and cached successfully. +public typealias PrefetcherProgressBlock = + ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void) + +/// Progress update block of prefetcher when initialized with a list of resources. +/// +/// - `skippedSources`: An array of sources that are already cached before the prefetching starting. +/// - `failedSources`: An array of sources that fail to be fetched. +/// - `completedResources`: An array of sources that are fetched and cached successfully. +public typealias PrefetcherSourceProgressBlock = + ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void) + +/// Completion block of prefetcher when initialized with a list of sources. +/// +/// - `skippedResources`: An array of resources that are already cached before the prefetching starting. +/// - `failedResources`: An array of resources that fail to be downloaded. It could because of being cancelled while +/// downloading, encountered an error when downloading or the download not being started at all. +/// - `completedResources`: An array of resources that are downloaded and cached successfully. +public typealias PrefetcherCompletionHandler = + ((_ skippedResources: [Resource], _ failedResources: [Resource], _ completedResources: [Resource]) -> Void) + +/// Completion block of prefetcher when initialized with a list of sources. +/// +/// - `skippedSources`: An array of sources that are already cached before the prefetching starting. +/// - `failedSources`: An array of sources that fail to be fetched. +/// - `completedSources`: An array of sources that are fetched and cached successfully. +public typealias PrefetcherSourceCompletionHandler = + ((_ skippedSources: [Source], _ failedSources: [Source], _ completedSources: [Source]) -> Void) + +/// `ImagePrefetcher` represents a downloading manager for requesting many images via URLs, then caching them. +/// This is useful when you know a list of image resources and want to download them before showing. It also works with +/// some Cocoa prefetching mechanism like table view or collection view `prefetchDataSource`, to start image downloading +/// and caching before they display on screen. +public class ImagePrefetcher: CustomStringConvertible { + + public var description: String { + return "\(Unmanaged.passUnretained(self).toOpaque())" + } + + /// The maximum concurrent downloads to use when prefetching images. Default is 5. + public var maxConcurrentDownloads = 5 + + private let prefetchSources: [Source] + private let optionsInfo: KingfisherParsedOptionsInfo + + private var progressBlock: PrefetcherProgressBlock? + private var completionHandler: PrefetcherCompletionHandler? + + private var progressSourceBlock: PrefetcherSourceProgressBlock? + private var completionSourceHandler: PrefetcherSourceCompletionHandler? + + private var tasks = [String: DownloadTask.WrappedTask]() + + private var pendingSources: ArraySlice<Source> + private var skippedSources = [Source]() + private var completedSources = [Source]() + private var failedSources = [Source]() + + private var stopped = false + + // A manager used for prefetching. We will use the helper methods in manager. + private let manager: KingfisherManager + + private let pretchQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.pretchQueue") + private static let requestingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.ImagePrefetcher.requestingQueue") + + private var finished: Bool { + let totalFinished: Int = failedSources.count + skippedSources.count + completedSources.count + return totalFinished == prefetchSources.count && tasks.isEmpty + } + + /// Creates an image prefetcher with an array of URLs. + /// + /// The prefetcher should be initiated with a list of prefetching targets. The URLs list is immutable. + /// After you get a valid `ImagePrefetcher` object, you call `start()` on it to begin the prefetching process. + /// The images which are already cached will be skipped without downloading again. + /// + /// - Parameters: + /// - urls: The URLs which should be prefetched. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time an resource is downloaded, skipped or cancelled. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + urls: [URL], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherProgressBlock? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + let resources: [Resource] = urls.map { $0 } + self.init( + resources: resources, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// Creates an image prefetcher with an array of resources. + /// + /// - Parameters: + /// - resources: The resources which should be prefetched. See `Resource` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time an resource is downloaded, skipped or cancelled. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init( + resources: [Resource], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherProgressBlock? = nil, + completionHandler: PrefetcherCompletionHandler? = nil) + { + self.init(sources: resources.map { .network($0) }, options: options) + self.progressBlock = progressBlock + self.completionHandler = completionHandler + } + + /// Creates an image prefetcher with an array of sources. + /// + /// - Parameters: + /// - sources: The sources which should be prefetched. See `Source` type for more. + /// - options: Options could control some behaviors. See `KingfisherOptionsInfo` for more. + /// - progressBlock: Called every time an source fetching successes, fails, is skipped. + /// - completionHandler: Called when the whole prefetching process finished. + /// + /// - Note: + /// By default, the `ImageDownloader.defaultDownloader` and `ImageCache.defaultCache` will be used as + /// the downloader and cache target respectively. You can specify another downloader or cache by using + /// a customized `KingfisherOptionsInfo`. Both the progress and completion block will be invoked in + /// main thread. The `.callbackQueue` value in `optionsInfo` will be ignored in this method. + public convenience init(sources: [Source], + options: KingfisherOptionsInfo? = nil, + progressBlock: PrefetcherSourceProgressBlock? = nil, + completionHandler: PrefetcherSourceCompletionHandler? = nil) + { + self.init(sources: sources, options: options) + self.progressSourceBlock = progressBlock + self.completionSourceHandler = completionHandler + } + + init(sources: [Source], options: KingfisherOptionsInfo?) { + var options = KingfisherParsedOptionsInfo(options) + prefetchSources = sources + pendingSources = ArraySlice(sources) + + // We want all callbacks from our prefetch queue, so we should ignore the callback queue in options. + // Add our own callback dispatch queue to make sure all internal callbacks are + // coming back in our expected queue. + options.callbackQueue = .dispatch(pretchQueue) + optionsInfo = options + + let cache = optionsInfo.targetCache ?? .default + let downloader = optionsInfo.downloader ?? .default + manager = KingfisherManager(downloader: downloader, cache: cache) + } + + /// Starts to download the resources and cache them. This can be useful for background downloading + /// of assets that are required for later use in an app. This code will not try and update any UI + /// with the results of the process. + public func start() { + pretchQueue.async { + guard !self.stopped else { + assertionFailure("You can not restart the same prefetcher. Try to create a new prefetcher.") + self.handleComplete() + return + } + + guard self.maxConcurrentDownloads > 0 else { + assertionFailure("There should be concurrent downloads value should be at least 1.") + self.handleComplete() + return + } + + // Empty case. + guard self.prefetchSources.count > 0 else { + self.handleComplete() + return + } + + let initialConcurrentDownloads = min(self.prefetchSources.count, self.maxConcurrentDownloads) + for _ in 0 ..< initialConcurrentDownloads { + if let resource = self.pendingSources.popFirst() { + self.startPrefetching(resource) + } + } + } + } + + /// Stops current downloading progress, and cancel any future prefetching activity that might be occuring. + public func stop() { + pretchQueue.async { + if self.finished { return } + self.stopped = true + self.tasks.values.forEach { $0.cancel() } + } + } + + private func downloadAndCache(_ source: Source) { + + let downloadTaskCompletionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void) = { result in + self.tasks.removeValue(forKey: source.cacheKey) + do { + let _ = try result.get() + self.completedSources.append(source) + } catch { + self.failedSources.append(source) + } + + self.reportProgress() + if self.stopped { + if self.tasks.isEmpty { + self.failedSources.append(contentsOf: self.pendingSources) + self.handleComplete() + } + } else { + self.reportCompletionOrStartNext() + } + } + + var downloadTask: DownloadTask.WrappedTask? + ImagePrefetcher.requestingQueue.sync { + downloadTask = manager.loadAndCacheImage( + source: source, + options: optionsInfo, + completionHandler: downloadTaskCompletionHandler) + } + + if let downloadTask = downloadTask { + tasks[source.cacheKey] = downloadTask + } + } + + private func append(cached source: Source) { + skippedSources.append(source) + + reportProgress() + reportCompletionOrStartNext() + } + + private func startPrefetching(_ source: Source) + { + if optionsInfo.forceRefresh { + downloadAndCache(source) + return + } + + let cacheType = manager.cache.imageCachedType( + forKey: source.cacheKey, + processorIdentifier: optionsInfo.processor.identifier) + switch cacheType { + case .memory: + append(cached: source) + case .disk: + if optionsInfo.alsoPrefetchToMemory { + _ = manager.retrieveImageFromCache( + source: source, + options: optionsInfo) + { + _ in + self.append(cached: source) + } + } else { + append(cached: source) + } + case .none: + downloadAndCache(source) + } + } + + private func reportProgress() { + + if progressBlock == nil && progressSourceBlock == nil { + return + } + + let skipped = self.skippedSources + let failed = self.failedSources + let completed = self.completedSources + CallbackQueue.mainCurrentOrAsync.execute { + self.progressSourceBlock?(skipped, failed, completed) + self.progressBlock?( + skipped.compactMap { $0.asResource }, + failed.compactMap { $0.asResource }, + completed.compactMap { $0.asResource } + ) + } + } + + private func reportCompletionOrStartNext() { + if let resource = self.pendingSources.popFirst() { + // Loose call stack for huge ammount of sources. + pretchQueue.async { self.startPrefetching(resource) } + } else { + guard allFinished else { return } + self.handleComplete() + } + } + + var allFinished: Bool { + return skippedSources.count + failedSources.count + completedSources.count == prefetchSources.count + } + + private func handleComplete() { + + if completionHandler == nil && completionSourceHandler == nil { + return + } + + // The completion handler should be called on the main thread + CallbackQueue.mainCurrentOrAsync.execute { + self.completionSourceHandler?(self.skippedSources, self.failedSources, self.completedSources) + self.completionHandler?( + self.skippedSources.compactMap { $0.asResource }, + self.failedSources.compactMap { $0.asResource }, + self.completedSources.compactMap { $0.asResource } + ) + self.completionHandler = nil + self.progressBlock = nil + } + } +} diff --git a/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift b/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift new file mode 100644 index 0000000..c5ca276 --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift @@ -0,0 +1,76 @@ +// +// RedirectHandler.swift +// Kingfisher +// +// Created by Roman Maidanovych on 2018/12/10. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents and wraps a method for modifying request during an image download request redirection. +public protocol ImageDownloadRedirectHandler { + + /// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection. + /// This is the posibility you can modify the image download request during redirection. You can modify the + /// request for some customizing purpose, such as adding auth token to the header, do basic HTTP auth or + /// something like url mapping. + /// + /// Usually, you pass an `ImageDownloadRedirectHandler` as the associated value of + /// `KingfisherOptionsInfoItem.redirectHandler` and use it as the `options` parameter in related methods. + /// + /// If you do nothing with the input `request` and return it as is, a downloading process will redirect with it. + /// + /// - Parameters: + /// - task: The current `SessionDataTask` which triggers this redirect. + /// - response: The response received during redirection. + /// - newRequest: The request for redirection which can be modified. + /// - completionHandler: A closure for being called with modified request. + func handleHTTPRedirection( + for task: SessionDataTask, + response: HTTPURLResponse, + newRequest: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) +} + +/// A wrapper for creating an `ImageDownloadRedirectHandler` easier. +/// This type conforms to `ImageDownloadRedirectHandler` and wraps an redirect request modify block. +public struct AnyRedirectHandler: ImageDownloadRedirectHandler { + + let block: (SessionDataTask, HTTPURLResponse, URLRequest, (URLRequest?) -> Void) -> Void + + public func handleHTTPRedirection( + for task: SessionDataTask, + response: HTTPURLResponse, + newRequest: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) + { + block(task, response, newRequest, completionHandler) + } + + /// Creates a value of `ImageDownloadRedirectHandler` which runs `modify` block. + /// + /// - Parameter modify: The request modifying block runs when a request modifying task comes. + /// + public init(handle: @escaping (SessionDataTask, HTTPURLResponse, URLRequest, (URLRequest?) -> Void) -> Void) { + block = handle + } +} diff --git a/Pods/Kingfisher/Sources/Networking/RequestModifier.swift b/Pods/Kingfisher/Sources/Networking/RequestModifier.swift new file mode 100644 index 0000000..06b062a --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/RequestModifier.swift @@ -0,0 +1,69 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/05. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents and wraps a method for modifying request before an image download request starts. +public protocol ImageDownloadRequestModifier { + + /// A method will be called just before the `request` being sent. + /// This is the last chance you can modify the image download request. You can modify the request for some + /// customizing purpose, such as adding auth token to the header, do basic HTTP auth or something like url mapping. + /// + /// Usually, you pass an `ImageDownloadRequestModifier` as the associated value of + /// `KingfisherOptionsInfoItem.requestModifier` and use it as the `options` parameter in related methods. + /// + /// If you do nothing with the input `request` and return it as is, a downloading process will start with it. + /// + /// - Parameter request: The input request contains necessary information like `url`. This request is generated + /// according to your resource url as a GET request. + /// - Returns: A modified version of request, which you wish to use for downloading an image. If `nil` returned, + /// a `KingfisherError.requestError` with `.emptyRequest` as its reason will occur. + /// + func modified(for request: URLRequest) -> URLRequest? +} + +/// A wrapper for creating an `ImageDownloadRequestModifier` easier. +/// This type conforms to `ImageDownloadRequestModifier` and wraps an image modify block. +public struct AnyModifier: ImageDownloadRequestModifier { + + let block: (URLRequest) -> URLRequest? + + /// For `ImageDownloadRequestModifier` conformation. + public func modified(for request: URLRequest) -> URLRequest? { + return block(request) + } + + /// Creates a value of `ImageDownloadRequestModifier` which runs `modify` block. + /// + /// - Parameter modify: The request modifying block runs when a request modifying task comes. + /// The return `URLRequest?` value of this block will be used as the image download request. + /// If `nil` returned, a `KingfisherError.requestError` with `.emptyRequest` as its + /// reason will occur. + public init(modify: @escaping (URLRequest) -> URLRequest?) { + block = modify + } +} diff --git a/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift b/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift new file mode 100644 index 0000000..2fcfbf0 --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift @@ -0,0 +1,119 @@ +// +// SessionDataTask.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents a session data task in `ImageDownloader`. It consists of an underlying `URLSessionDataTask` and +/// an array of `TaskCallback`. Multiple `TaskCallback`s could be added for a single downloading data task. +public class SessionDataTask { + + /// Represents the type of token which used for cancelling a task. + public typealias CancelToken = Int + + struct TaskCallback { + let onCompleted: Delegate<Result<ImageLoadingResult, KingfisherError>, Void>? + let options: KingfisherParsedOptionsInfo + } + + /// Downloaded raw data of current task. + public private(set) var mutableData: Data + + /// The underlying download task. It is only for debugging purpose when you encountered an error. You should not + /// modify the content of this task or start it yourself. + public let task: URLSessionDataTask + private var callbacksStore = [CancelToken: TaskCallback]() + + var callbacks: [SessionDataTask.TaskCallback] { + lock.lock() + defer { lock.unlock() } + return Array(callbacksStore.values) + } + + private var currentToken = 0 + private let lock = NSLock() + + let onTaskDone = Delegate<(Result<(Data, URLResponse?), KingfisherError>, [TaskCallback]), Void>() + let onCallbackCancelled = Delegate<(CancelToken, TaskCallback), Void>() + + var started = false + var containsCallbacks: Bool { + // We should be able to use `task.state != .running` to check it. + // However, in some rare cases, cancelling the task does not change + // task state to `.cancelling` immediately, but still in `.running`. + // So we need to check callbacks count to for sure that it is safe to remove the + // task in delegate. + return !callbacks.isEmpty + } + + init(task: URLSessionDataTask) { + self.task = task + mutableData = Data() + } + + func addCallback(_ callback: TaskCallback) -> CancelToken { + lock.lock() + defer { lock.unlock() } + callbacksStore[currentToken] = callback + defer { currentToken += 1 } + return currentToken + } + + func removeCallback(_ token: CancelToken) -> TaskCallback? { + lock.lock() + defer { lock.unlock() } + if let callback = callbacksStore[token] { + callbacksStore[token] = nil + return callback + } + return nil + } + + func resume() { + guard !started else { return } + started = true + task.resume() + } + + func cancel(token: CancelToken) { + guard let callback = removeCallback(token) else { + return + } + if callbacksStore.count == 0 { + task.cancel() + } + onCallbackCancelled.call((token, callback)) + } + + func forceCancel() { + for token in callbacksStore.keys { + cancel(token: token) + } + } + + func didReceiveData(_ data: Data) { + mutableData.append(data) + } +} diff --git a/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift b/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift new file mode 100644 index 0000000..dde757b --- /dev/null +++ b/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift @@ -0,0 +1,251 @@ +// +// SessionDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +// Represents the delegate object of downloader session. It also behave like a task manager for downloading. +class SessionDelegate: NSObject { + + typealias SessionChallengeFunc = ( + URLSession, + URLAuthenticationChallenge, + (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) + + typealias SessionTaskChallengeFunc = ( + URLSession, + URLSessionTask, + URLAuthenticationChallenge, + (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) + + private var tasks: [URL: SessionDataTask] = [:] + private let lock = NSLock() + + let onValidStatusCode = Delegate<Int, Bool>() + let onDownloadingFinished = Delegate<(URL, Result<URLResponse, KingfisherError>), Void>() + let onDidDownloadData = Delegate<SessionDataTask, Data?>() + + let onReceiveSessionChallenge = Delegate<SessionChallengeFunc, Void>() + let onReceiveSessionTaskChallenge = Delegate<SessionTaskChallengeFunc, Void>() + + func add( + _ dataTask: URLSessionDataTask, + url: URL, + callback: SessionDataTask.TaskCallback) -> DownloadTask + { + lock.lock() + defer { lock.unlock() } + + // Create a new task if necessary. + let task = SessionDataTask(task: dataTask) + task.onCallbackCancelled.delegate(on: self) { [unowned task] (self, value) in + let (token, callback) = value + + let error = KingfisherError.requestError(reason: .taskCancelled(task: task, token: token)) + task.onTaskDone.call((.failure(error), [callback])) + // No other callbacks waiting, we can clear the task now. + if !task.containsCallbacks { + let dataTask = task.task + self.remove(dataTask) + } + } + let token = task.addCallback(callback) + tasks[url] = task + return DownloadTask(sessionTask: task, cancelToken: token) + } + + func append( + _ task: SessionDataTask, + url: URL, + callback: SessionDataTask.TaskCallback) -> DownloadTask + { + let token = task.addCallback(callback) + return DownloadTask(sessionTask: task, cancelToken: token) + } + + private func remove(_ task: URLSessionTask) { + guard let url = task.originalRequest?.url else { + return + } + lock.lock() + defer {lock.unlock()} + tasks[url] = nil + } + + private func task(for task: URLSessionTask) -> SessionDataTask? { + + guard let url = task.originalRequest?.url else { + return nil + } + + lock.lock() + defer { lock.unlock() } + guard let sessionTask = tasks[url] else { + return nil + } + guard sessionTask.task.taskIdentifier == task.taskIdentifier else { + return nil + } + return sessionTask + } + + func task(for url: URL) -> SessionDataTask? { + lock.lock() + defer { lock.unlock() } + return tasks[url] + } + + func cancelAll() { + lock.lock() + let taskValues = tasks.values + lock.unlock() + for task in taskValues { + task.forceCancel() + } + } + + func cancel(url: URL) { + lock.lock() + let task = tasks[url] + lock.unlock() + task?.forceCancel() + } +} + +extension SessionDelegate: URLSessionDataDelegate { + + func urlSession( + _ session: URLSession, + dataTask: URLSessionDataTask, + didReceive response: URLResponse, + completionHandler: @escaping (URLSession.ResponseDisposition) -> Void) + { + guard let httpResponse = response as? HTTPURLResponse else { + let error = KingfisherError.responseError(reason: .invalidURLResponse(response: response)) + onCompleted(task: dataTask, result: .failure(error)) + completionHandler(.cancel) + return + } + + let httpStatusCode = httpResponse.statusCode + guard onValidStatusCode.call(httpStatusCode) == true else { + let error = KingfisherError.responseError(reason: .invalidHTTPStatusCode(response: httpResponse)) + onCompleted(task: dataTask, result: .failure(error)) + completionHandler(.cancel) + return + } + completionHandler(.allow) + } + + func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + guard let task = self.task(for: dataTask) else { + return + } + + task.didReceiveData(data) + + task.callbacks.forEach { callback in + callback.options.onDataReceived?.forEach { sideEffect in + sideEffect.onDataReceived(session, task: task, data: data) + } + } + } + + func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let sessionTask = self.task(for: task) else { return } + + if let url = task.originalRequest?.url { + let result: Result<URLResponse, KingfisherError> + if let error = error { + result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) + } else if let response = task.response { + result = .success(response) + } else { + result = .failure(KingfisherError.responseError(reason: .noURLResponse(task: sessionTask))) + } + onDownloadingFinished.call((url, result)) + } + + let result: Result<(Data, URLResponse?), KingfisherError> + if let error = error { + result = .failure(KingfisherError.responseError(reason: .URLSessionError(error: error))) + } else { + if let data = onDidDownloadData.call(sessionTask), let finalData = data { + result = .success((finalData, task.response)) + } else { + result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask))) + } + } + onCompleted(task: task, result: result) + } + + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + onReceiveSessionChallenge.call((session, challenge, completionHandler)) + } + + func urlSession( + _ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler)) + } + + func urlSession( + _ session: URLSession, + task: URLSessionTask, + willPerformHTTPRedirection response: HTTPURLResponse, + newRequest request: URLRequest, + completionHandler: @escaping (URLRequest?) -> Void) + { + guard let sessionDataTask = self.task(for: task), + let redirectHandler = Array(sessionDataTask.callbacks).last?.options.redirectHandler else + { + completionHandler(request) + return + } + + redirectHandler.handleHTTPRedirection( + for: sessionDataTask, + response: response, + newRequest: request, + completionHandler: completionHandler) + } + + private func onCompleted(task: URLSessionTask, result: Result<(Data, URLResponse?), KingfisherError>) { + guard let sessionTask = self.task(for: task) else { + return + } + remove(task) + sessionTask.onTaskDone.call((result, sessionTask.callbacks)) + } +} diff --git a/Pods/Kingfisher/Sources/Utility/Box.swift b/Pods/Kingfisher/Sources/Utility/Box.swift new file mode 100644 index 0000000..0303a6e --- /dev/null +++ b/Pods/Kingfisher/Sources/Utility/Box.swift @@ -0,0 +1,34 @@ +// +// Box.swift +// Kingfisher +// +// Created by Wei Wang on 2018/3/17. +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +class Box<T> { + var value: T + + init(_ value: T) { + self.value = value + } +} diff --git a/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift b/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift new file mode 100644 index 0000000..fa67f14 --- /dev/null +++ b/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift @@ -0,0 +1,81 @@ +// +// CallbackQueue.swift +// Kingfisher +// +// Created by onevcat on 2018/10/15. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// Represents callback queue behaviors when an calling of closure be dispatched. +/// +/// - asyncMain: Dispatch the calling to `DispatchQueue.main` with an `async` behavior. +/// - currentMainOrAsync: Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not +/// `.main`. Otherwise, call the closure immediately in current main queue. +/// - untouch: Do not change the calling queue for closure. +/// - dispatch: Dispatches to a specified `DispatchQueue`. +public enum CallbackQueue { + /// Dispatch the calling to `DispatchQueue.main` with an `async` behavior. + case mainAsync + /// Dispatch the calling to `DispatchQueue.main` with an `async` behavior if current queue is not + /// `.main`. Otherwise, call the closure immediately in current main queue. + case mainCurrentOrAsync + /// Do not change the calling queue for closure. + case untouch + /// Dispatches to a specified `DispatchQueue`. + case dispatch(DispatchQueue) + + public func execute(_ block: @escaping () -> Void) { + switch self { + case .mainAsync: + DispatchQueue.main.async { block() } + case .mainCurrentOrAsync: + DispatchQueue.main.safeAsync { block() } + case .untouch: + block() + case .dispatch(let queue): + queue.async { block() } + } + } + + var queue: DispatchQueue { + switch self { + case .mainAsync: return .main + case .mainCurrentOrAsync: return .main + case .untouch: return OperationQueue.current?.underlyingQueue ?? .main + case .dispatch(let queue): return queue + } + } +} + +extension DispatchQueue { + // This method will dispatch the `block` to self. + // If `self` is the main queue, and current thread is main thread, the block + // will be invoked immediately instead of being dispatched. + func safeAsync(_ block: @escaping ()->()) { + if self === DispatchQueue.main && Thread.isMainThread { + block() + } else { + async { block() } + } + } +} diff --git a/Pods/Kingfisher/Sources/Utility/Delegate.swift b/Pods/Kingfisher/Sources/Utility/Delegate.swift new file mode 100644 index 0000000..15915c9 --- /dev/null +++ b/Pods/Kingfisher/Sources/Utility/Delegate.swift @@ -0,0 +1,53 @@ +// +// Delegate.swift +// Kingfisher +// +// Created by onevcat on 2018/10/10. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +/// A delegate helper type to "shadow" weak `self`, to prevent creating an unexpected retain cycle. +class Delegate<Input, Output> { + init() {} + + private var block: ((Input) -> Output?)? + + func delegate<T: AnyObject>(on target: T, block: ((T, Input) -> Output)?) { + // The `target` is weak inside block, so you do not need to worry about it in the caller side. + self.block = { [weak target] input in + guard let target = target else { return nil } + return block?(target, input) + } + } + + func call(_ input: Input) -> Output? { + return block?(input) + } +} + +extension Delegate where Input == Void { + // To make syntax better for `Void` input. + func call() -> Output? { + return call(()) + } +} diff --git a/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift b/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift new file mode 100644 index 0000000..147ce01 --- /dev/null +++ b/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift @@ -0,0 +1,125 @@ +// +// ExtensionHelpers.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +extension Float { + var isEven: Bool { + return truncatingRemainder(dividingBy: 2.0) == 0 + } +} + +#if canImport(AppKit) +import AppKit +extension NSBezierPath { + convenience init(roundedRect rect: NSRect, topLeftRadius: CGFloat, topRightRadius: CGFloat, + bottomLeftRadius: CGFloat, bottomRightRadius: CGFloat) + { + self.init() + + let maxCorner = min(rect.width, rect.height) / 2 + + let radiusTopLeft = min(maxCorner, max(0, topLeftRadius)) + let radiusTopRight = min(maxCorner, max(0, topRightRadius)) + let radiusBottomLeft = min(maxCorner, max(0, bottomLeftRadius)) + let radiusBottomRight = min(maxCorner, max(0, bottomRightRadius)) + + guard !rect.isEmpty else { + return + } + + let topLeft = NSPoint(x: rect.minX, y: rect.maxY) + let topRight = NSPoint(x: rect.maxX, y: rect.maxY) + let bottomRight = NSPoint(x: rect.maxX, y: rect.minY) + + move(to: NSPoint(x: rect.midX, y: rect.maxY)) + appendArc(from: topLeft, to: rect.origin, radius: radiusTopLeft) + appendArc(from: rect.origin, to: bottomRight, radius: radiusBottomLeft) + appendArc(from: bottomRight, to: topRight, radius: radiusBottomRight) + appendArc(from: topRight, to: topLeft, radius: radiusTopRight) + close() + } + + convenience init(roundedRect rect: NSRect, byRoundingCorners corners: RectCorner, radius: CGFloat) { + let radiusTopLeft = corners.contains(.topLeft) ? radius : 0 + let radiusTopRight = corners.contains(.topRight) ? radius : 0 + let radiusBottomLeft = corners.contains(.bottomLeft) ? radius : 0 + let radiusBottomRight = corners.contains(.bottomRight) ? radius : 0 + + self.init(roundedRect: rect, topLeftRadius: radiusTopLeft, topRightRadius: radiusTopRight, + bottomLeftRadius: radiusBottomLeft, bottomRightRadius: radiusBottomRight) + } +} + +extension Image { + // macOS does not support scale. This is just for code compatibility across platforms. + convenience init?(data: Data, scale: CGFloat) { + self.init(data: data) + } +} +#endif + +#if canImport(UIKit) +import UIKit +extension RectCorner { + var uiRectCorner: UIRectCorner { + + var result: UIRectCorner = [] + + if contains(.topLeft) { result.insert(.topLeft) } + if contains(.topRight) { result.insert(.topRight) } + if contains(.bottomLeft) { result.insert(.bottomLeft) } + if contains(.bottomRight) { result.insert(.bottomRight) } + + return result + } +} +#endif + +extension Date { + var isPast: Bool { + return isPast(referenceDate: Date()) + } + + var isFuture: Bool { + return !isPast + } + + func isPast(referenceDate: Date) -> Bool { + return timeIntervalSince(referenceDate) <= 0 + } + + func isFuture(referenceDate: Date) -> Bool { + return !isPast(referenceDate: referenceDate) + } + + // `Date` in memory is a wrap for `TimeInterval`. But in file attribute it can only accept `Int` number. + // By default the system will `round` it. But it is not friendly for testing purpose. + // So we always `ceil` the value when used for file attributes. + var fileAttributeDate: Date { + return Date(timeIntervalSince1970: ceil(timeIntervalSince1970)) + } +} diff --git a/Pods/Kingfisher/Sources/Utility/Result.swift b/Pods/Kingfisher/Sources/Utility/Result.swift new file mode 100644 index 0000000..8b9c5fd --- /dev/null +++ b/Pods/Kingfisher/Sources/Utility/Result.swift @@ -0,0 +1,239 @@ +// +// Result.swift +// Kingfisher +// +// Created by onevcat on 2018/09/22. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +#if swift(>=4.3) +/// Result type already built-in +#else +/// A value that represents either a success or failure, capturing associated +/// values in both cases. +public enum Result<Success, Failure> { + /// A success, storing a `Value`. + case success(Success) + + /// A failure, storing an `Error`. + case failure(Failure) + + /// Evaluates the given transform closure when this `Result` instance is + /// `.success`, passing the value as a parameter. + /// + /// Use the `map` method with a closure that returns a non-`Result` value. + /// + /// - Parameter transform: A closure that takes the successful value of the + /// instance. + /// - Returns: A new `Result` instance with the result of the transform, if + /// it was applied. + public func map<NewSuccess>( + _ transform: (Success) -> NewSuccess + ) -> Result<NewSuccess, Failure> { + switch self { + case let .success(success): + return .success(transform(success)) + case let .failure(failure): + return .failure(failure) + } + } + + /// Evaluates the given transform closure when this `Result` instance is + /// `.failure`, passing the error as a parameter. + /// + /// Use the `mapError` method with a closure that returns a non-`Result` + /// value. + /// + /// - Parameter transform: A closure that takes the failure value of the + /// instance. + /// - Returns: A new `Result` instance with the result of the transform, if + /// it was applied. + public func mapError<NewFailure>( + _ transform: (Failure) -> NewFailure + ) -> Result<Success, NewFailure> { + switch self { + case let .success(success): + return .success(success) + case let .failure(failure): + return .failure(transform(failure)) + } + } + + /// Evaluates the given transform closure when this `Result` instance is + /// `.success`, passing the value as a parameter and flattening the result. + /// + /// - Parameter transform: A closure that takes the successful value of the + /// instance. + /// - Returns: A new `Result` instance, either from the transform or from + /// the previous error value. + public func flatMap<NewSuccess>( + _ transform: (Success) -> Result<NewSuccess, Failure> + ) -> Result<NewSuccess, Failure> { + switch self { + case let .success(success): + return transform(success) + case let .failure(failure): + return .failure(failure) + } + } + + /// Evaluates the given transform closure when this `Result` instance is + /// `.failure`, passing the error as a parameter and flattening the result. + /// + /// - Parameter transform: A closure that takes the error value of the + /// instance. + /// - Returns: A new `Result` instance, either from the transform or from + /// the previous success value. + public func flatMapError<NewFailure>( + _ transform: (Failure) -> Result<Success, NewFailure> + ) -> Result<Success, NewFailure> { + switch self { + case let .success(success): + return .success(success) + case let .failure(failure): + return transform(failure) + } + } +} + +extension Result where Failure: Error { + /// Returns the success value as a throwing expression. + /// + /// Use this method to retrieve the value of this result if it represents a + /// success, or to catch the value if it represents a failure. + /// + /// let integerResult: Result<Int, Error> = .success(5) + /// do { + /// let value = try integerResult.get() + /// print("The value is \(value).") + /// } catch error { + /// print("Error retrieving the value: \(error)") + /// } + /// // Prints "The value is 5." + /// + /// - Returns: The success value, if the instance represents a success. + /// - Throws: The failure value, if the instance represents a failure. + public func get() throws -> Success { + switch self { + case let .success(success): + return success + case let .failure(failure): + throw failure + } + } + + /// Unwraps the `Result` into a throwing expression. + /// + /// - Returns: The success value, if the instance is a success. + /// - Throws: The error value, if the instance is a failure. + @available(*, deprecated, message: "This method will be removed soon. Use `get() throws -> Success` instead.") + public func unwrapped() throws -> Success { + switch self { + case let .success(value): + return value + case let .failure(error): + throw error + } + } +} + +extension Result where Failure == Swift.Error { + /// Creates a new result by evaluating a throwing closure, capturing the + /// returned value as a success, or any thrown error as a failure. + /// + /// - Parameter body: A throwing closure to evaluate. + @_transparent + public init(catching body: () throws -> Success) { + do { + self = .success(try body()) + } catch { + self = .failure(error) + } + } +} + +extension Result : Equatable where Success : Equatable, Failure: Equatable { } + +extension Result : Hashable where Success : Hashable, Failure : Hashable { } + +extension Result : CustomDebugStringConvertible { + public var debugDescription: String { + var output = "Result." + switch self { + case let .success(value): + output += "success(" + debugPrint(value, terminator: "", to: &output) + case let .failure(error): + output += "failure(" + debugPrint(error, terminator: "", to: &output) + } + output += ")" + + return output + } +} +#endif + +// These helper methods are not public since we do not want them to be exposed or cause any conflicting. +// However, they are just wrapper of `ResultUtil` static methods. +extension Result where Failure: Error { + + /// Evaluates the given transform closures to create a single output value. + /// + /// - Parameters: + /// - onSuccess: A closure that transforms the success value. + /// - onFailure: A closure that transforms the error value. + /// - Returns: A single `Output` value. + func match<Output>( + onSuccess: (Success) -> Output, + onFailure: (Failure) -> Output) -> Output + { + switch self { + case let .success(value): + return onSuccess(value) + case let .failure(error): + return onFailure(error) + } + } + + func matchSuccess<Output>(with folder: (Success?) -> Output) -> Output { + return match( + onSuccess: { value in return folder(value) }, + onFailure: { _ in return folder(nil) } + ) + } + + func matchFailure<Output>(with folder: (Error?) -> Output) -> Output { + return match( + onSuccess: { _ in return folder(nil) }, + onFailure: { error in return folder(error) } + ) + } + + func match<Output>(with folder: (Success?, Error?) -> Output) -> Output { + return match( + onSuccess: { return folder($0, nil) }, + onFailure: { return folder(nil, $0) } + ) + } +} diff --git a/Pods/Kingfisher/Sources/Utility/Runtime.swift b/Pods/Kingfisher/Sources/Utility/Runtime.swift new file mode 100644 index 0000000..d5818e2 --- /dev/null +++ b/Pods/Kingfisher/Sources/Utility/Runtime.swift @@ -0,0 +1,35 @@ +// +// Runtime.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/12. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation + +func getAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer) -> T? { + return objc_getAssociatedObject(object, key) as? T +} + +func setRetainedAssociatedObject<T>(_ object: Any, _ key: UnsafeRawPointer, _ value: T) { + objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +} diff --git a/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift b/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift new file mode 100644 index 0000000..19d05d6 --- /dev/null +++ b/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift @@ -0,0 +1,110 @@ +// +// SizeExtensions.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import CoreGraphics + +extension CGSize: KingfisherCompatibleValue {} +extension KingfisherWrapper where Base == CGSize { + + /// Returns a size by resizing the `base` size to a target size under a given content mode. + /// + /// - Parameters: + /// - size: The target size to resize to. + /// - contentMode: Content mode of the target size should be when resizing. + /// - Returns: The resized size under the given `ContentMode`. + public func resize(to size: CGSize, for contentMode: ContentMode) -> CGSize { + switch contentMode { + case .aspectFit: + return constrained(size) + case .aspectFill: + return filling(size) + case .none: + return size + } + } + + /// Returns a size by resizing the `base` size by making it aspect fitting the given `size`. + /// + /// - Parameter size: The size in which the `base` should fit in. + /// - Returns: The size fitted in by the input `size`, while keeps `base` aspect. + public func constrained(_ size: CGSize) -> CGSize { + let aspectWidth = round(aspectRatio * size.height) + let aspectHeight = round(size.width / aspectRatio) + + return aspectWidth > size.width ? + CGSize(width: size.width, height: aspectHeight) : + CGSize(width: aspectWidth, height: size.height) + } + + /// Returns a size by resizing the `base` size by making it aspect filling the given `size`. + /// + /// - Parameter size: The size in which the `base` should fill. + /// - Returns: The size be filled by the input `size`, while keeps `base` aspect. + public func filling(_ size: CGSize) -> CGSize { + let aspectWidth = round(aspectRatio * size.height) + let aspectHeight = round(size.width / aspectRatio) + + return aspectWidth < size.width ? + CGSize(width: size.width, height: aspectHeight) : + CGSize(width: aspectWidth, height: size.height) + } + + /// Returns a `CGRect` for which the `base` size is constrained to an input `size` at a given `anchor` point. + /// + /// - Parameters: + /// - size: The size in which the `base` should be constrained to. + /// - anchor: An anchor point in which the size constraint should happen. + /// - Returns: The result `CGRect` for the constraint operation. + public func constrainedRect(for size: CGSize, anchor: CGPoint) -> CGRect { + + let unifiedAnchor = CGPoint(x: anchor.x.clamped(to: 0.0...1.0), + y: anchor.y.clamped(to: 0.0...1.0)) + + let x = unifiedAnchor.x * base.width - unifiedAnchor.x * size.width + let y = unifiedAnchor.y * base.height - unifiedAnchor.y * size.height + let r = CGRect(x: x, y: y, width: size.width, height: size.height) + + let ori = CGRect(origin: .zero, size: base) + return ori.intersection(r) + } + + private var aspectRatio: CGFloat { + return base.height == 0.0 ? 1.0 : base.width / base.height + } +} + +extension CGRect { + func scaled(_ scale: CGFloat) -> CGRect { + return CGRect(x: origin.x * scale, y: origin.y * scale, + width: size.width * scale, height: size.height * scale) + } +} + +extension Comparable { + func clamped(to limits: ClosedRange<Self>) -> Self { + return min(max(self, limits.lowerBound), limits.upperBound) + } +} diff --git a/Pods/Kingfisher/Sources/Utility/String+MD5.swift b/Pods/Kingfisher/Sources/Utility/String+MD5.swift new file mode 100644 index 0000000..e390e99 --- /dev/null +++ b/Pods/Kingfisher/Sources/Utility/String+MD5.swift @@ -0,0 +1,49 @@ +// +// String+MD5.swift +// Kingfisher +// +// Created by Wei Wang on 18/09/25. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import CommonCrypto + +extension String: KingfisherCompatibleValue { } +extension KingfisherWrapper where Base == String { + var md5: String { + guard let data = base.data(using: .utf8) else { + return base + } + var digest = [UInt8](repeating: 0, count: Int(CC_MD5_DIGEST_LENGTH)) + #if swift(>=5.0) + _ = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in + return CC_MD5(bytes.baseAddress, CC_LONG(data.count), &digest) + } + #else + _ = data.withUnsafeBytes { bytes in + return CC_MD5(bytes, CC_LONG(data.count), &digest) + } + #endif + + return digest.reduce(into: "") { $0 += String(format: "%02x", $1) } + } +} diff --git a/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift b/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift new file mode 100644 index 0000000..2dfc9ce --- /dev/null +++ b/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift @@ -0,0 +1,570 @@ +// +// AnimatableImageView.swift +// Kingfisher +// +// Created by bl4ckra1sond3tre on 4/22/16. +// +// The AnimatableImageView, AnimatedFrame and Animator is a modified version of +// some classes from kaishin's Gifu project (https://github.com/kaishin/Gifu) +// +// The MIT License (MIT) +// +// Copyright (c) 2019 Reda Lemeden. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// +// The name and characters used in the demo of this software are property of their +// respective owners. + +import UIKit +import ImageIO + +/// Protocol of `AnimatedImageView`. +public protocol AnimatedImageViewDelegate: AnyObject { + + /// Called after the animatedImageView has finished each animation loop. + /// + /// - Parameters: + /// - imageView: The `AnimatedImageView` that is being animated. + /// - count: The looped count. + func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) + + /// Called after the `AnimatedImageView` has reached the max repeat count. + /// + /// - Parameter imageView: The `AnimatedImageView` that is being animated. + func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) +} + +extension AnimatedImageViewDelegate { + public func animatedImageView(_ imageView: AnimatedImageView, didPlayAnimationLoops count: UInt) {} + public func animatedImageViewDidFinishAnimating(_ imageView: AnimatedImageView) {} +} + +#if swift(>=4.2) +let KFRunLoopModeCommon = RunLoop.Mode.common +#else +let KFRunLoopModeCommon = RunLoopMode.commonModes +#endif + +/// Represents a subclass of `UIImageView` for displaying animated image. +/// Different from showing animated image in a normal `UIImageView` (which load all frames at one time), +/// `AnimatedImageView` only tries to load several frames (defined by `framePreloadCount`) to reduce memory usage. +/// It provides a tradeoff between memory usage and CPU time. If you have a memory issue when using a normal image +/// view to load GIF data, you could give this class a try. +/// +/// Kingfisher supports setting GIF animated data to either `UIImageView` and `AnimatedImageView` out of box. So +/// it would be fairly easy to switch between them. +open class AnimatedImageView: UIImageView { + + /// Proxy object for preventing a reference cycle between the `CADDisplayLink` and `AnimatedImageView`. + class TargetProxy { + private weak var target: AnimatedImageView? + + init(target: AnimatedImageView) { + self.target = target + } + + @objc func onScreenUpdate() { + target?.updateFrameIfNeeded() + } + } + + /// Enumeration that specifies repeat count of GIF + public enum RepeatCount: Equatable { + case once + case finite(count: UInt) + case infinite + + public static func ==(lhs: RepeatCount, rhs: RepeatCount) -> Bool { + switch (lhs, rhs) { + case let (.finite(l), .finite(r)): + return l == r + case (.once, .once), + (.infinite, .infinite): + return true + case (.once, .finite(let count)), + (.finite(let count), .once): + return count == 1 + case (.once, _), + (.infinite, _), + (.finite, _): + return false + } + } + } + + // MARK: - Public property + /// Whether automatically play the animation when the view become visible. Default is `true`. + public var autoPlayAnimatedImage = true + + /// The count of the frames should be preloaded before shown. + public var framePreloadCount = 10 + + /// Specifies whether the GIF frames should be pre-scaled to the image view's size or not. + /// If the downloaded image is larger than the image view's size, it will help to reduce some memory use. + /// Default is `true`. + public var needsPrescaling = true + + /// Decode the GIF frames in background thread before using. It will decode frames data and do a off-screen + /// rendering to extract pixel information in background. This can reduce the main thread CPU usage. + public var backgroundDecode = true + + /// The animation timer's run loop mode. Default is `RunLoop.Mode.common`. + /// Set this property to `RunLoop.Mode.default` will make the animation pause during UIScrollView scrolling. + public var runLoopMode = KFRunLoopModeCommon { + willSet { + guard runLoopMode == newValue else { return } + stopAnimating() + displayLink.remove(from: .main, forMode: runLoopMode) + displayLink.add(to: .main, forMode: newValue) + startAnimating() + } + } + + /// The repeat count. The animated image will keep animate until it the loop count reaches this value. + /// Setting this value to another one will reset current animation. + /// + /// Default is `.infinite`, which means the animation will last forever. + public var repeatCount = RepeatCount.infinite { + didSet { + if oldValue != repeatCount { + reset() + setNeedsDisplay() + layer.setNeedsDisplay() + } + } + } + + /// Delegate of this `AnimatedImageView` object. See `AnimatedImageViewDelegate` protocol for more. + public weak var delegate: AnimatedImageViewDelegate? + + // MARK: - Private property + /// `Animator` instance that holds the frames of a specific image in memory. + private var animator: Animator? + + // Dispatch queue used for preloading images. + private lazy var preloadQueue: DispatchQueue = { + return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") + }() + + // A flag to avoid invalidating the displayLink on deinit if it was never created, because displayLink is so lazy. + private var isDisplayLinkInitialized: Bool = false + + // A display link that keeps calling the `updateFrame` method on every screen refresh. + private lazy var displayLink: CADisplayLink = { + isDisplayLinkInitialized = true + let displayLink = CADisplayLink( + target: TargetProxy(target: self), selector: #selector(TargetProxy.onScreenUpdate)) + displayLink.add(to: .main, forMode: runLoopMode) + displayLink.isPaused = true + return displayLink + }() + + // MARK: - Override + override open var image: Image? { + didSet { + if image != oldValue { + reset() + } + setNeedsDisplay() + layer.setNeedsDisplay() + } + } + + deinit { + if isDisplayLinkInitialized { + displayLink.invalidate() + } + } + + override open var isAnimating: Bool { + if isDisplayLinkInitialized { + return !displayLink.isPaused + } else { + return super.isAnimating + } + } + + /// Starts the animation. + override open func startAnimating() { + guard !isAnimating else { return } + if animator?.isReachMaxRepeatCount ?? false { + return + } + + displayLink.isPaused = false + } + + /// Stops the animation. + override open func stopAnimating() { + super.stopAnimating() + if isDisplayLinkInitialized { + displayLink.isPaused = true + } + } + + override open func display(_ layer: CALayer) { + if let currentFrame = animator?.currentFrameImage { + layer.contents = currentFrame.cgImage + } else { + layer.contents = image?.cgImage + } + } + + override open func didMoveToWindow() { + super.didMoveToWindow() + didMove() + } + + override open func didMoveToSuperview() { + super.didMoveToSuperview() + didMove() + } + + // This is for back compatibility that using regular `UIImageView` to show animated image. + override func shouldPreloadAllAnimation() -> Bool { + return false + } + + // Reset the animator. + private func reset() { + animator = nil + if let imageSource = image?.kf.imageSource { + let targetSize = bounds.scaled(UIScreen.main.scale).size + let animator = Animator( + imageSource: imageSource, + contentMode: contentMode, + size: targetSize, + framePreloadCount: framePreloadCount, + repeatCount: repeatCount, + preloadQueue: preloadQueue) + animator.delegate = self + animator.needsPrescaling = needsPrescaling + animator.backgroundDecode = backgroundDecode + animator.prepareFramesAsynchronously() + self.animator = animator + } + didMove() + } + + private func didMove() { + if autoPlayAnimatedImage && animator != nil { + if let _ = superview, let _ = window { + startAnimating() + } else { + stopAnimating() + } + } + } + + /// Update the current frame with the displayLink duration. + private func updateFrameIfNeeded() { + guard let animator = animator else { + return + } + + guard !animator.isFinished else { + stopAnimating() + delegate?.animatedImageViewDidFinishAnimating(self) + return + } + + let duration: CFTimeInterval + + // CA based display link is opt-out from ProMotion by default. + // So the duration and its FPS might not match. + // See [#718](https://github.com/onevcat/Kingfisher/issues/718) + // By setting CADisableMinimumFrameDuration to YES in Info.plist may + // cause the preferredFramesPerSecond being 0 + if displayLink.preferredFramesPerSecond == 0 { + duration = displayLink.duration + } else { + // Some devices (like iPad Pro 10.5) will have a different FPS. + duration = 1.0 / Double(displayLink.preferredFramesPerSecond) + } + + animator.shouldChangeFrame(with: duration) { [weak self] hasNewFrame in + if hasNewFrame { + self?.layer.setNeedsDisplay() + } + } + } +} + +protocol AnimatorDelegate: AnyObject { + func animator(_ animator: AnimatedImageView.Animator, didPlayAnimationLoops count: UInt) +} + +extension AnimatedImageView: AnimatorDelegate { + func animator(_ animator: Animator, didPlayAnimationLoops count: UInt) { + delegate?.animatedImageView(self, didPlayAnimationLoops: count) + } +} + +extension AnimatedImageView { + + // Represents a single frame in a GIF. + struct AnimatedFrame { + + // The image to display for this frame. Its value is nil when the frame is removed from the buffer. + let image: UIImage? + + // The duration that this frame should remain active. + let duration: TimeInterval + + // A placeholder frame with no image assigned. + // Used to replace frames that are no longer needed in the animation. + var placeholderFrame: AnimatedFrame { + return AnimatedFrame(image: nil, duration: duration) + } + + // Whether this frame instance contains an image or not. + var isPlaceholder: Bool { + return image == nil + } + + // Returns a new instance from an optional image. + // + // - parameter image: An optional `UIImage` instance to be assigned to the new frame. + // - returns: An `AnimatedFrame` instance. + func makeAnimatedFrame(image: UIImage?) -> AnimatedFrame { + return AnimatedFrame(image: image, duration: duration) + } + } +} + +extension AnimatedImageView { + + // MARK: - Animator + class Animator { + private let size: CGSize + private let maxFrameCount: Int + private let imageSource: CGImageSource + private let maxRepeatCount: RepeatCount + + private let maxTimeStep: TimeInterval = 1.0 + private var animatedFrames = [AnimatedFrame]() + private var frameCount = 0 + private var timeSinceLastFrameChange: TimeInterval = 0.0 + private var currentRepeatCount: UInt = 0 + + var isFinished: Bool = false + + var needsPrescaling = true + + var backgroundDecode = true + + weak var delegate: AnimatorDelegate? + + // Total duration of one animation loop + var loopDuration: TimeInterval = 0 + + // Current active frame image + var currentFrameImage: UIImage? { + return frame(at: currentFrameIndex) + } + + // Current active frame duration + var currentFrameDuration: TimeInterval { + return duration(at: currentFrameIndex) + } + + // The index of the current GIF frame. + var currentFrameIndex = 0 { + didSet { + previousFrameIndex = oldValue + } + } + + var previousFrameIndex = 0 { + didSet { + preloadQueue.async { + self.updatePreloadedFrames() + } + } + } + + var isReachMaxRepeatCount: Bool { + switch maxRepeatCount { + case .once: + return currentRepeatCount >= 1 + case .finite(let maxCount): + return currentRepeatCount >= maxCount + case .infinite: + return false + } + } + + var isLastFrame: Bool { + return currentFrameIndex == frameCount - 1 + } + + var preloadingIsNeeded: Bool { + return maxFrameCount < frameCount - 1 + } + + var contentMode = UIView.ContentMode.scaleToFill + + private lazy var preloadQueue: DispatchQueue = { + return DispatchQueue(label: "com.onevcat.Kingfisher.Animator.preloadQueue") + }() + + /// Creates an animator with image source reference. + /// + /// - Parameters: + /// - source: The reference of animated image. + /// - mode: Content mode of the `AnimatedImageView`. + /// - size: Size of the `AnimatedImageView`. + /// - count: Count of frames needed to be preloaded. + /// - repeatCount: The repeat count should this animator uses. + init(imageSource source: CGImageSource, + contentMode mode: UIView.ContentMode, + size: CGSize, + framePreloadCount count: Int, + repeatCount: RepeatCount, + preloadQueue: DispatchQueue) { + self.imageSource = source + self.contentMode = mode + self.size = size + self.maxFrameCount = count + self.maxRepeatCount = repeatCount + self.preloadQueue = preloadQueue + } + + func frame(at index: Int) -> Image? { + return animatedFrames[safe: index]?.image + } + + func duration(at index: Int) -> TimeInterval { + return animatedFrames[safe: index]?.duration ?? .infinity + } + + func prepareFramesAsynchronously() { + frameCount = Int(CGImageSourceGetCount(imageSource)) + animatedFrames.reserveCapacity(frameCount) + preloadQueue.async { [weak self] in + self?.setupAnimatedFrames() + } + } + + func shouldChangeFrame(with duration: CFTimeInterval, handler: (Bool) -> Void) { + incrementTimeSinceLastFrameChange(with: duration) + + if currentFrameDuration > timeSinceLastFrameChange { + handler(false) + } else { + resetTimeSinceLastFrameChange() + incrementCurrentFrameIndex() + handler(true) + } + } + + private func setupAnimatedFrames() { + resetAnimatedFrames() + + var duration: TimeInterval = 0 + + (0..<frameCount).forEach { index in + let frameDuration = GIFAnimatedImage.getFrameDuration(from: imageSource, at: index) + duration += min(frameDuration, maxTimeStep) + animatedFrames += [AnimatedFrame(image: nil, duration: frameDuration)] + + if index > maxFrameCount { return } + animatedFrames[index] = animatedFrames[index].makeAnimatedFrame(image: loadFrame(at: index)) + } + + self.loopDuration = duration + } + + private func resetAnimatedFrames() { + animatedFrames = [] + } + + private func loadFrame(at index: Int) -> UIImage? { + let options: [CFString: Any] = [ + kCGImageSourceCreateThumbnailFromImageIfAbsent: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height) + ] + + let resize = needsPrescaling && size != .zero + guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, + index, + resize ? options as CFDictionary : nil) else { + return nil + } + + let image = Image(cgImage: cgImage) + return backgroundDecode ? image.kf.decoded : image + } + + private func updatePreloadedFrames() { + guard preloadingIsNeeded else { + return + } + + animatedFrames[previousFrameIndex] = animatedFrames[previousFrameIndex].placeholderFrame + + preloadIndexes(start: currentFrameIndex).forEach { index in + let currentAnimatedFrame = animatedFrames[index] + if !currentAnimatedFrame.isPlaceholder { return } + animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index)) + } + } + + private func incrementCurrentFrameIndex() { + currentFrameIndex = increment(frameIndex: currentFrameIndex) + if isReachMaxRepeatCount && isLastFrame { + isFinished = true + } else if currentFrameIndex == 0 { + currentRepeatCount += 1 + delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) + } + } + + private func incrementTimeSinceLastFrameChange(with duration: TimeInterval) { + timeSinceLastFrameChange += min(maxTimeStep, duration) + } + + private func resetTimeSinceLastFrameChange() { + timeSinceLastFrameChange -= currentFrameDuration + } + + private func increment(frameIndex: Int, by value: Int = 1) -> Int { + return (frameIndex + value) % frameCount + } + + private func preloadIndexes(start index: Int) -> [Int] { + let nextIndex = increment(frameIndex: index) + let lastIndex = increment(frameIndex: index, by: maxFrameCount) + + if lastIndex >= nextIndex { + return [Int](nextIndex...lastIndex) + } else { + return [Int](nextIndex..<frameCount) + [Int](0...lastIndex) + } + } + } +} + +extension Array { + subscript(safe index: Int) -> Element? { + return indices ~= index ? self[index] : nil + } +} diff --git a/Pods/Kingfisher/Sources/Views/Indicator.swift b/Pods/Kingfisher/Sources/Views/Indicator.swift new file mode 100644 index 0000000..fa72a75 --- /dev/null +++ b/Pods/Kingfisher/Sources/Views/Indicator.swift @@ -0,0 +1,188 @@ +// +// Indicator.swift +// Kingfisher +// +// Created by João D. Moreira on 30/08/16. +// +// Copyright (c) 2019 Wei Wang <onevcat@gmail.com> +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +#if canImport(AppKit) +import AppKit +public typealias IndicatorView = NSView +#else +import UIKit +public typealias IndicatorView = UIView +#endif + +/// Represents the activity indicator type which should be added to +/// an image view when an image is being downloaded. +/// +/// - none: No indicator. +/// - activity: Uses the system activity indicator. +/// - image: Uses an image as indicator. GIF is supported. +/// - custom: Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol. +public enum IndicatorType { + /// No indicator. + case none + /// Uses the system activity indicator. + case activity + /// Uses an image as indicator. GIF is supported. + case image(imageData: Data) + /// Uses a custom indicator. The type of associated value should conform to the `Indicator` protocol. + case custom(indicator: Indicator) +} + +/// An indicator type which can be used to show the download task is in progress. +public protocol Indicator { + + /// Called when the indicator should start animating. + func startAnimatingView() + + /// Called when the indicator should stop animating. + func stopAnimatingView() + + /// Center offset of the indicator. Kingfisher will use this value to determine the position of + /// indicator in the super view. + var centerOffset: CGPoint { get } + + /// The indicator view which would be added to the super view. + var view: IndicatorView { get } +} + +extension Indicator { + + /// Default implementation of `centerOffset` of `Indicator`. The default value is `.zero`, means that there is + /// no offset for the indicator view. + public var centerOffset: CGPoint { return .zero } +} + +// Displays a NSProgressIndicator / UIActivityIndicatorView +final class ActivityIndicator: Indicator { + + #if os(macOS) + private let activityIndicatorView: NSProgressIndicator + #else + private let activityIndicatorView: UIActivityIndicatorView + #endif + private var animatingCount = 0 + + var view: IndicatorView { + return activityIndicatorView + } + + func startAnimatingView() { + if animatingCount == 0 { + #if os(macOS) + activityIndicatorView.startAnimation(nil) + #else + activityIndicatorView.startAnimating() + #endif + activityIndicatorView.isHidden = false + } + animatingCount += 1 + } + + func stopAnimatingView() { + animatingCount = max(animatingCount - 1, 0) + if animatingCount == 0 { + #if os(macOS) + activityIndicatorView.stopAnimation(nil) + #else + activityIndicatorView.stopAnimating() + #endif + activityIndicatorView.isHidden = true + } + } + + init() { + #if os(macOS) + activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) + activityIndicatorView.controlSize = .small + activityIndicatorView.style = .spinning + #else + #if os(tvOS) + let indicatorStyle = UIActivityIndicatorView.Style.white + #else + let indicatorStyle = UIActivityIndicatorView.Style.gray + #endif + #if swift(>=4.2) + activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle) + #else + activityIndicatorView = UIActivityIndicatorView(activityIndicatorStyle: indicatorStyle) + #endif + #endif + } +} + +// MARK: - ImageIndicator +// Displays an ImageView. Supports gif +final class ImageIndicator: Indicator { + private let animatedImageIndicatorView: ImageView + + var view: IndicatorView { + return animatedImageIndicatorView + } + + init?( + imageData data: Data, + processor: ImageProcessor = DefaultImageProcessor.default, + options: KingfisherParsedOptionsInfo? = nil) + { + var options = options ?? KingfisherParsedOptionsInfo(nil) + // Use normal image view to show animations, so we need to preload all animation data. + if !options.preloadAllAnimationData { + options.preloadAllAnimationData = true + } + + guard let image = processor.process(item: .data(data), options: options) else { + return nil + } + + animatedImageIndicatorView = ImageView() + animatedImageIndicatorView.image = image + + #if os(macOS) + // Need for gif to animate on macOS + animatedImageIndicatorView.imageScaling = .scaleNone + animatedImageIndicatorView.canDrawSubviewsIntoLayer = true + #else + animatedImageIndicatorView.contentMode = .center + #endif + } + + func startAnimatingView() { + #if os(macOS) + animatedImageIndicatorView.animates = true + #else + animatedImageIndicatorView.startAnimating() + #endif + animatedImageIndicatorView.isHidden = false + } + + func stopAnimatingView() { + #if os(macOS) + animatedImageIndicatorView.animates = false + #else + animatedImageIndicatorView.stopAnimating() + #endif + animatedImageIndicatorView.isHidden = true + } +} diff --git a/Pods/Manifest.lock b/Pods/Manifest.lock new file mode 100644 index 0000000..ddc934c --- /dev/null +++ b/Pods/Manifest.lock @@ -0,0 +1,24 @@ +PODS: + - FeedKit (8.1.1) + - Kingfisher (5.7.0) + - SVProgressHUD (2.2.5) + +DEPENDENCIES: + - FeedKit (~> 8.1) + - Kingfisher (~> 5.7) + - SVProgressHUD (~> 2.2) + +SPEC REPOS: + https://github.com/cocoapods/specs.git: + - FeedKit + - Kingfisher + - SVProgressHUD + +SPEC CHECKSUMS: + FeedKit: 3418eed25f0b493b205b4de1b8511ac21d413fa9 + Kingfisher: c7d211b54f1f30d8060aadab177d52b4349c825b + SVProgressHUD: 1428aafac632c1f86f62aa4243ec12008d7a51d6 + +PODFILE CHECKSUM: 3412147b32c5fcffbdb5018d5e3a38ea8cc2b8d4 + +COCOAPODS: 1.7.2 diff --git a/Pods/Pods.xcodeproj/project.pbxproj b/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000..344c4ab --- /dev/null +++ b/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,1575 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 00F3A9C8CA21565B4B7AD1F24916E73B /* MediaSubTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92428E51776332F76D13B0B410BF93B6 /* MediaSubTitle.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 033945D564AEB6ECD53E4D574700DC27 /* RSSFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16614D7ADA5196DAD4B56CAB05556EFC /* RSSFeed.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 038D25ACE83BC39A81C2489555BB69D7 /* Pods-rss-reader-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 732D3FDE937FE3DED879CF34FADD3AA6 /* Pods-rss-reader-dummy.m */; }; + 055E7E01B1CD7E508F9DF3ECCF90D57D /* JSONFeedParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2D1DE6DC7CAF8BF4BC8DC34BBD6CD559 /* JSONFeedParser.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 073B9D7521458F8915742E4D4FC605C0 /* MediaContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40A6B4EC9527311C961032DAAC088138 /* MediaContent.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 073EBC91634A2B318E36A798D0EB200E /* AtomFeed + mapCharacters.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2BECD29B14E75CB2F77DB2C6080C870 /* AtomFeed + mapCharacters.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 0812AA51AEDD629B2C34B4297E6E0274 /* iTunesCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2709B3CD2BFBBC25A29EDE7B272D9D98 /* iTunesCategory.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 08FF12ED51AB815DA30478115AD69A84 /* RSSPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8806A61FD9846EC0B2C5A987BE9B5C22 /* RSSPath.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 0C8DA927DCA34A34191DD5BE88F7FE7E /* RSSFeed + mapCharacters.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD5D4382DA8BB442887B50B9AAABEF4E /* RSSFeed + mapCharacters.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 0CA80ED7E59E6BCE64CEEE649A3C6D2F /* SVProgressHUD.h in Headers */ = {isa = PBXBuildFile; fileRef = FBB5954ABD5F62A35C5A55EB72DB66B7 /* SVProgressHUD.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0DB652314FBEE783580DFBA1327739AC /* JSONFeedAuthor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40024CBBE39B27E7087FF09697F6AF8F /* JSONFeedAuthor.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 0E7CEAE2A49A10164728A1CC1E657A36 /* MediaText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63037E47C69963786FDC94A801EFC1C9 /* MediaText.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 0EB010641B5BB3B4F9F3F8C48B8F43D2 /* AtomFeedEntryAuthor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53BCD7D5157AE3707182C9039200AA28 /* AtomFeedEntryAuthor.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 10F1569D025E3737E99F2C8238471D27 /* MediaParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCADB4A6524885FB2211BCD934AB3089 /* MediaParam.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 110E982216772861B919ED18D2F2D07E /* AtomFeedLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 651384ED4AEE4550A759201DD39912C1 /* AtomFeedLink.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 13A494F795FD220FC54FF06A8E9FBA87 /* String+MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = A6CFC82AB159D55EC8632E8F4FE4F860 /* String+MD5.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 142640D3547A3760893CC57B1C533F9D /* MediaPeerLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16668BD526836AC845AFFA68E4821CD0 /* MediaPeerLink.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 1467DCC83C265C5E079BCBCE8C930527 /* RSSFeed + mapAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFA500E5179F1C9F702A5E5A85408F5 /* RSSFeed + mapAttributes.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 154CF2AD328B1D874021654776969DB9 /* MediaScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = E21F48C149AC7BE4DEDC1FC2DC88BC48 /* MediaScene.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 1719C634E234F7F04B5BC41A9EECE3DF /* ParserError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FFD32A86B7B35FFA615DA2B70A06674 /* ParserError.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 189AF6B9611D028F4D0F553420D123F4 /* iTunesSubCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 541B97121181512AF32CDA8299F661FD /* iTunesSubCategory.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 18EC7CFA2A1F9D494FFA712EB4A00BED /* SyndicationUpdatePeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD41C6BBAF64669FA894679F798E0944 /* SyndicationUpdatePeriod.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 18EDB325C46E6985C30463F48F68928C /* AtomFeedCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE3F92D56CBB1544BE441AB5F5933A52 /* AtomFeedCategory.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 1A9291D46FAB68A67AF5801C60091520 /* RSSFeedSkipHour.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBFA82C0CF3C9741780E91F24C8217D9 /* RSSFeedSkipHour.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 1C816AA1100B07979C954FF4887D2512 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ACF8955D8CDC111802C06F42BA42DA7 /* ImageFormat.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 1E37499305E26A478A559CA658AEAB6C /* JSONFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = AECA125A27B9400F30D492BC6AD00A3C /* JSONFeed.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 203F98456AEFAAA6B288FADB0975CFCE /* Kingfisher-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = D9ECE6BA45D6EE56BEDCAB33E0962DCC /* Kingfisher-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 227AFB8E66DF3543661168E4FE669F8A /* FeedDataType.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7579CE4E143236193EA287B21927614 /* FeedDataType.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 23F070DD81AB8CB83CA08BA572F16826 /* RSSFeedItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D7D79FBDEB1A0671571B63608F5F5B82 /* RSSFeedItemSource.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 242706DC099EEFB54A5FE8E5FB689CE5 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E00DB9D78B75E2A2930841FE4AFCBF2 /* Source.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 244C5805DC1654F022D8072763E1ADB9 /* RSSFeedItemGUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = A63B2C150405A5A878FB5902045E3170 /* RSSFeedItemGUID.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 248843CDBED004C50ABA8E59C5713419 /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FEC38A9FFD814395AA2F91655CD53834 /* ImagePrefetcher.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 264E1A478BDFA31DA6842749C72EA535 /* SVProgressHUD.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 54994F2207A861CC21DB3D9BAF60CBB7 /* SVProgressHUD.bundle */; }; + 2739EE37237A7D0926C2B81AB2A67896 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBAFD5ECEE4620CCFE2454114AD50772 /* CacheSerializer.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 28D990407E4192188B6F701FD02800AC /* JSONFeedAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B0F4E84CEF07AD6C9B735D955FA94AD /* JSONFeedAttachment.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 2BED157C050F421E3820D7A59430F3E2 /* MediaCredit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 583FE1E1149C6E9FE3AE24152C09846B /* MediaCredit.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 2C8D6BE7CD7BB661925F0C6C6619B2E3 /* MediaCopyright.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FF8662A73745FCCBAC4709E0289106C /* MediaCopyright.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 2D2801427B88163A3FE72A41B896508F /* SVProgressHUD.m in Sources */ = {isa = PBXBuildFile; fileRef = 6C557C33404E811077EFCBF326E97300 /* SVProgressHUD.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 2FA41B134B6113F26902B5C53295DFB9 /* FeedParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4DE7E9990ADC97DBF60731DF7A8C7A /* FeedParser.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 2FABED4580092268CC87F9403D2A8A4A /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = EF6DD4E0CBC5D7D9AA1D3A161C2010D8 /* RedirectHandler.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 2FEFDD8774FEDC3E1E57498E58322DE8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 443A41F4868E2A22FF5E9590C4DFC9F9 /* Foundation.framework */; }; + 305E27FCC7492EB8903D794D8A96EF99 /* String + toDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70F53F4D6B65D8E0DA5A6507B30C5147 /* String + toDate.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 31528FA91AB63958E8909A08826B4BE3 /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 93EF214AB500180623133D713A88F583 /* DiskStorage.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 3173641F16B0577B73D506D06E78CBAE /* MediaLocation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19C23BA34041C032A4CC0157E2C24ED3 /* MediaLocation.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 336A41C351628C1E5B0B4EB48A906DD7 /* RSSFeedCloud.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF301F3886EACFBEE0CDDABA55E2AD97 /* RSSFeedCloud.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 3682672E63B3B2F8714D49F4E526FE08 /* URL + replacingScheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86EBB1C1B89196F9FC75E90492200B52 /* URL + replacingScheme.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 369455C05E84142AB77CDA2F4CB624B9 /* AtomFeedSubtitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97F58968C852A2C4C9F6698BC9874EFD /* AtomFeedSubtitle.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 36B028172F7163B49D931630F497F6E0 /* AtomFeedGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = C60BEFFC34C4F703FA0C19C3E4C9C8D5 /* AtomFeedGenerator.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 3B43D899F709090CC09C751F7E2598E5 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6A00470DD5C3F8B5219AC3A535470E1 /* ImageCache.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 3BA0E7BB5C61807BBD53F1343AD8AF17 /* AtomFeedEntryCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B78DEA3C9913B578EC5C7F1581DDCB7 /* AtomFeedEntryCategory.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 3C2EB58B4DD52CF60E88D6290325F99F /* iTunesOwner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 16F684574F8BDB5474F326ED6D92C607 /* iTunesOwner.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 3DFF7C6E6BE9AD90CA4DC334BC7C7217 /* MediaRestriction.swift in Sources */ = {isa = PBXBuildFile; fileRef = C03B8AD8A6AA30865F6B4E31C77346FD /* MediaRestriction.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 42B572E80566E8851C81777954E68EBF /* JSONFeedHub.swift in Sources */ = {isa = PBXBuildFile; fileRef = E958E7AE64834D7BF3595CA4D9BA62DC /* JSONFeedHub.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 45941825BC934119884B8E06016C8BB7 /* ImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7052ABCD4CC2B0E50BE8BFB157BD6E44 /* ImageDataProvider.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 4744296660B21C03A3D2412D13EE7F0B /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 324E675D213C3049EFAC157D647D898B /* Result.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 4804C796B0288D0412E7F095317999D0 /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 504091429B677E8FB7CA49B4C26F7868 /* ImageTransition.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 485E8C15B4C2E986BB0F4E5E11562AA1 /* XMLFeedParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D412FD9DA895FF13762572FF113566B /* XMLFeedParser.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 4A125BC3F7DB1C72C99261F06E4B0DDF /* MediaThumbnail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E8A5532883C5E07B2B8BBA4340CBE84 /* MediaThumbnail.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 4A9E953B6844847D60D4A72A4AC8104F /* MediaStatistics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F69FEEEDC68DE0F38F88B8F3DBF30FE /* MediaStatistics.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 4C22C3C0D8C162A61B03898A178C1A69 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6129971BFA2CDEBDE94EE6354B48AEE6 /* Placeholder.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 4F583F7ACCF07298F9E99740AF83AA92 /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76D1F29EAB3985C1E7BB823578DA422D /* CallbackQueue.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 5017F297AAA171A30A29BC2F85D30D1B /* SVIndefiniteAnimatedView.m in Sources */ = {isa = PBXBuildFile; fileRef = C275A08017080970F47C305FA7EAD303 /* SVIndefiniteAnimatedView.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 54673701214B48CEF8136269D791B482 /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17F321D9C70F4C90E5FEBA75C14B5572 /* MemoryStorage.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 5578DBFDA0183FEFE14052D02795D5E0 /* AtomFeedContributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75725CFB719AC4FE06D1F6C0C7C2C5DE /* AtomFeedContributor.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 5629D646E14624BE2CB59D6592AA9E75 /* MediaStarRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50A119B2D52752517068E094C36318D2 /* MediaStarRating.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 567DBB92174116AED62947169642C400 /* AtomFeedEntryLink.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD8C980C56E88282C19E6F62A77FBF5F /* AtomFeedEntryLink.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 56BAD22FE6CEF39D48EDBC1401070B84 /* RFC3339DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D61270DB9206085FA776593CA97E4AD /* RFC3339DateFormatter.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 5762628817E5F2058ABE3BA80E3CFA4A /* MediaHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8F3E2C62138F15C0AB5B2BBAA78E9E57 /* MediaHash.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 5970A55B036B38488F0E41E1F8495E4E /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34F04EB55291C530F492BB0131B22DB3 /* ImageProgressive.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 5BAD3B67D901438E569930C898DC8E6C /* Pods-rss-reader-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 6644152013A89EF84F8B5A0D82454382 /* Pods-rss-reader-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5D4759FF7D34320BC1BFE3171BD83250 /* Kingfisher.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A60DABDD337558FACDB5EC539E0ADC2 /* Kingfisher.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5FE02B9DFC0737DB60435091546ACDA8 /* RSSFeedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509EADEBE44C157920A347C15321FBFF /* RSSFeedImage.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 6009C5769E0038B20692E7328E59CD9A /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = B346C9C88A30476600F3846CC3E8E58C /* Storage.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 60A5B1A89CF28CC9E0FB642AD346FF69 /* Array + Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2166F72D19D1AFA02AE8FDE50D87AFD8 /* Array + Equatable.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 61D552992D3522BCF5DFFA2B716FFA67 /* SVProgressAnimatedView.m in Sources */ = {isa = PBXBuildFile; fileRef = EB3EAD8084ABFD8F4E3B8C07E60D4B4A /* SVProgressAnimatedView.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 633A123F6AA4CDD4C1F7B7AE87E92CC8 /* RSSFeedItemCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = C54C6012604BF887ED3CBF71B3138E7B /* RSSFeedItemCategory.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 6424D3B7E24310353026B9B60A255C9C /* iTunesImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD568E39CD0A28C5DA53EB4AC4ADFD22 /* iTunesImage.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 6456FA04CF508F55B3B54A5C504DBEF7 /* ImageView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = D77DBB3D7B16431C2E8485ECD4928014 /* ImageView+Kingfisher.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 67776A8ED6E3B931649AD03090E61BBC /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 794656D58BCB267487E25BF1004E4584 /* Accelerate.framework */; }; + 69B7A7EF76EA0E030D834AB508C33271 /* RSSFeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = D18CBA10D13D64E9BE27F5F05E2C92B5 /* RSSFeedItem.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 6B3C32A5B5296C6B58EEADA1330717EB /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EEDDF880825F67E1E975AE70682DECF6 /* CFNetwork.framework */; }; + 6D79BECCC5B6689C3529EA7DEABA5655 /* AtomFeedAuthor.swift in Sources */ = {isa = PBXBuildFile; fileRef = A540002F1A251610EF19914E5C64C837 /* AtomFeedAuthor.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 702CC18C73D99CE5FBD89C07F73A3728 /* FeedKit-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C4AF6D73894070DD070CFAD0184F2D /* FeedKit-dummy.m */; }; + 725F9F4A21430F094ACB74E28A192507 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A65F50DD58299EC33AA2DA09EB4BB15 /* SessionDataTask.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 735AC52A9D1D5B6508D65328E3AD9E8B /* MediaNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B2F15846ACD42C0B7824A6959AF9DAE /* MediaNamespace.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 7465C633F25B81D4872FD8B34FB7A04E /* MediaDescription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DB688F82F59B6848979D3EEB0DEDCAB /* MediaDescription.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 74936F026B909235BD1134B85ED39A67 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8CDCB9B6D610DB97B879EA1FF4394227 /* Box.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 77DCE19243521302D7C8DECFB5306050 /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71713BE51E82A2610A9B9700D0113F36 /* Indicator.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 7B67479EBAB1C7FB8016086FB2A6C36F /* MediaEmbed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E722A19CE147433309D0E60986525E4 /* MediaEmbed.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 7B97C87E6B21F3C9A39F3FAC8D543E37 /* AtomFeedEntryContributor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5320685CDE87E856EBBD5CA6E45CB410 /* AtomFeedEntryContributor.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 7EE55A9563025F727B3927FE304A8F69 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 543B972EC7077844CCCF91693EE8B8AD /* Resource.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 7F9C37EFF94BD1D269179AC4E2BAED8D /* MediaTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48919EFB65ED62EBEC5DD3A70D9A6D80 /* MediaTag.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 80ADA274F8C90BD1D2047FC7C0550C91 /* AtomFeed + mapAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8772019334A779595507E54CF4638844 /* AtomFeed + mapAttributes.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 82AAAB7D00602E4421E64496ADFA18CA /* KingfisherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B69A442482F6550C4925B8CBFAFD580 /* KingfisherError.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 82B7E5099B7FD6A199E9492FC76A5B8A /* Deprecated.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0AC1583D82E255CC775A8F9142102F /* Deprecated.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 877F08DB7B5D28F403662CF36FEEE820 /* RDFPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165C52E8454597B7916E9C051A69B06F /* RDFPath.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 87A7B91615CAF5AD17EA2FA9241CA357 /* KingfisherOptionsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1AE804EA3DE7381E6071BC2A1F4B259E /* KingfisherOptionsInfo.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 87CF6D3613627429C651988DA4FE1ACD /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99248F1A349B54D8C816EFB6BFD72F3D /* SessionDelegate.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 8C033DAA94EC978A1A9934E45CEDB2DC /* FeedKit-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 87676A7E8E4B90B49C136A48EA452055 /* FeedKit-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8C237220DEC7332BA4EF352E98A5381F /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E29C7196BE81BB59CBC46537AF9C40A /* AnimatedImageView.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 8C351C88FD63A54AF59B79CB66BAEA3D /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = A36DBC80309568D8C36F5CDDEBF43456 /* RequestModifier.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 90CC11986A2DF24E9E9BABFAA33EAD19 /* iTunesNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFA5D4A65D5F6198862AD5125F0A1F6 /* iTunesNamespace.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 96EADE99073DBA8FCE7904627E2D18EE /* SVIndefiniteAnimatedView.h in Headers */ = {isa = PBXBuildFile; fileRef = DFBB354B076400AE5066045D21B09AAD /* SVIndefiniteAnimatedView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 99456829035D2883017E8EBCDB82C231 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FA5AF49B1CA933991F633BBA2102CF33 /* QuartzCore.framework */; }; + 9ADF639A256B1FEBF985539A6E86A62D /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83A51626365D30D6EE7BF3A037EE515C /* ImageDownloaderDelegate.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 9B483AABE10368563FD5A687B28B9BC5 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED121190AB2708ADD56BE77731CE7297 /* ImageDataProcessor.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 9B9D036796D371A3A9E1C581F1D1106F /* AtomFeedEntrySource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2DADC5341CACB6B3269CAC721CBB2034 /* AtomFeedEntrySource.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 9BCF1F2949BA37B8FB61A7D529D2314C /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3639E2EC69C9FB2739632F887DAFD0 /* ImageDownloader.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 9BF0BA499FF3E37A8B9CEC704EC00BA9 /* String + toDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC693C3B81D9486A36EF6318F5B9F5C8 /* String + toDuration.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + 9EE0C393C016BF8D51A8762181DC7F27 /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE1CA9DECFF54250A9827AC454F43814 /* KingfisherManager.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + A1AC959CC5F9E2837BA9B8BB1DEA0ACB /* MediaTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D255D956932AA5F68990F4E1D5F6B2C /* MediaTitle.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + A2C29BC3E4187AC14B5EA4775729BA89 /* String + toBool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CCDA59331AA268A3476F6AAE39BA607 /* String + toBool.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + A3810D9AEA6C4D8B5AB7A1A45397F331 /* XMLFeedType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C47B6B014A9B3D98175CA0F0857B78D /* XMLFeedType.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + A3EF264FE0D4780D5D04F4DF5943A555 /* AtomFeedEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EEB378F530376775079C1741BCC8D3A /* AtomFeedEntry.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + A760D87AB7011C7ECDB714AFAA054590 /* RSSFeedTextInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = DFC50DAC18C1698B4D9FA6D04F6BE333 /* RSSFeedTextInput.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + AB48F74A791D71FE6A1A12E5D0AB787B /* JSONFeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3C42F443F10754A18CD3F7A3428B707B /* JSONFeedItem.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + AC0EED138B01B6A427C73C1D7D169183 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E71A6ACE602CD43F29E85F5DD7DD877 /* Filter.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + B1778FF3264766BF9E59AA21E8CD2897 /* MediaRights.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5F7330D2BFBBDFA5568FB0EF70149F /* MediaRights.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + B63B7E7E743B189124AD4F3CEC542C6B /* SyndicationNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 625A1DF46F6C55CB1F40843E95E89D63 /* SyndicationNamespace.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + B650BEABA29BF753EFACF1779B5E7C0D /* Kingfisher-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 656EF9B4CA373CF0B71CBAD0B284AC38 /* Kingfisher-dummy.m */; }; + B6810F38B400C8C3F09E206001233D62 /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = F86D924C65E6221835308B1C96B50003 /* Kingfisher.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + B7E6A02AE234F17885948C03F8E86732 /* UIButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8628FA0B8A98C0D0E4DB0A73305C0921 /* UIButton+Kingfisher.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + B9F01224843ED00E8198B73A0871CA91 /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6A6F3BB2C3AB4A1D609182D814BE032E /* ImageProcessor.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + BA5F5446C689D273471CED79A8826237 /* MediaCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = A58FD4E6088116886843E812E4E1EED3 /* MediaCategory.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + BB777C6CD71A788DB36E972CDD36C8E5 /* AtomFeedEntrySummary.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1FD70607A9BA8F197300BCC3F30FCD58 /* AtomFeedEntrySummary.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + BB92D94DA24C7EBD147D10901E181B5F /* RFC822DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2871EE69A16EA3E6A8233CFBD02007DD /* RFC822DateFormatter.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + BE7771F000A75785BD4737778FD7DE4C /* SVProgressHUD-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = F46308DB03F9943DECEDA43F019A4F1A /* SVProgressHUD-dummy.m */; }; + BFFAF10DB9969D44312C7F5A318A48D0 /* MediaPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D3F42F1BDCB9B98C796CFD5EE71EBAE /* MediaPlayer.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + C0C32D91FC3E821256E5E39C48DA29A3 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83441447DF020810DCAA0CE159CD4F52 /* Delegate.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + C2E9CA406FA22142181739ED45C12D6F /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14E949082AB7C8A8E09198AE3A8DE273 /* AuthenticationChallengeResponsable.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + C3CA920F320825CB4C9B4041C257E3E1 /* GIFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CB9382DF97097374B02DE969548FE47 /* GIFAnimatedImage.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + C5A7629109A5C715E58DDD422778785D /* MediaLicence.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44DC20D483098B86B38C2FF7CF4DE93 /* MediaLicence.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + C5BFA31A2D0C0B893CE605CD5AF44400 /* MediaGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3AFB4DCCCF6F2ABDC42DCE081F3FF5E7 /* MediaGroup.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + C5DA83446CA44CDB6F9F0E08802A7BF0 /* DublinCoreNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC5B72C88D115DD470FEBA574A100D7F /* DublinCoreNamespace.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + C9B2585312927033FE5D3CB9511C71E8 /* RSSFeedCategory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1088DA43224CC281BA4329787983EE30 /* RSSFeedCategory.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + CAAB9B7375BFBEDAA48CE4AF0D33CF5B /* Date + codingStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE064AC47BD833563801A0B2B0055DC9 /* Date + codingStrategy.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + CAC07C0D19114DB366430C72EB91C2F2 /* AtomFeedEntryContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 63EFD00DF940AD7B61B32836F698AF6C /* AtomFeedEntryContent.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + CB2C4195422C4DBB0498E56384F002A9 /* SizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4E94ADCAADD684CABFEDF90C2EB5481 /* SizeExtensions.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + CE09ED6A621D2A33CD07586E0F999AA1 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 443A41F4868E2A22FF5E9590C4DFC9F9 /* Foundation.framework */; }; + D169FB8F82E88E646EBAFA2AD581F345 /* ImageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC83A87595BAAA9F2364DA4A9A7C5AC /* ImageModifier.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + D1CEB905D848609DF9E7B720AB2D2B68 /* RSSFeedSkipDay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20AE9422BF116D6275189CF9A6B5889B /* RSSFeedSkipDay.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + D4914107FF5A197B9D0BC6D1C089E33D /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 443A41F4868E2A22FF5E9590C4DFC9F9 /* Foundation.framework */; }; + D7EA47C26320F33ADAF57632F7D13896 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 443A41F4868E2A22FF5E9590C4DFC9F9 /* Foundation.framework */; }; + D87B2D5ED73534ACF97CE0A52FA18AE4 /* SVRadialGradientLayer.h in Headers */ = {isa = PBXBuildFile; fileRef = E3D03B6D9C3ADE679C59CC3CB19AA938 /* SVRadialGradientLayer.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DB19804BA6B117B4B5CBB959C9C3AFFB /* SVRadialGradientLayer.m in Sources */ = {isa = PBXBuildFile; fileRef = 772A536060D21EF4EAFC4B074DA99157 /* SVRadialGradientLayer.m */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + DB295F5C89B9118DE89BAEB49ACCCE1B /* MediaCommunity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 555C48B8D8CA7BB74DB490F2FAE4F307 /* MediaCommunity.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + DC5E3722AEC8AAA22522F74F7A2F8DDB /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE1DD8A7EC65FEB33977CD4A3DAD94DF /* Runtime.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + DC7BDA3522A381DB3391B725910830F4 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69C4D09353AF812A98619BAC8667583C /* Result.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + DDA449EC2E3881A3BCE542BEE9A65FFB /* DateSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 137BD81FFC8CD495086672AAF63809C5 /* DateSpec.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + DE12057AB8895931A0E9499619C5CFC8 /* SVProgressHUD-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 74A97F1462EFFCE3C86A7E552A583851 /* SVProgressHUD-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DEAD74F1DF0D2A15077DF8E4A7691DCC /* ExtensionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B7664EB364606F03335718CB0A513E /* ExtensionHelpers.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + E0186A63060F0EB3676F95CD0ACE0E18 /* SVProgressAnimatedView.h in Headers */ = {isa = PBXBuildFile; fileRef = 83A9A5C594C71235536FC962544032E9 /* SVProgressAnimatedView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E5427D943DE6A10F0BE8AAE01F8FC0B4 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1CA7B876F0213261CCA695BD922EC6F5 /* FormatIndicatedCacheSerializer.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + E958799A1B06F5EEA032166E8DFA9F7E /* RSSFeedItemEnclosure.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DA18158BBA03C01440B16464BC82202 /* RSSFeedItemEnclosure.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + EA2DF0C7981254111ED5227D89B5810C /* AtomPath.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEA69B64AFDFD241BF07C3B55E0BE92D /* AtomPath.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + EBBD11156E33F00FA156B6F0D1B4EC28 /* MediaStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54E8CE224B938A3BB61D084D6BD675EF /* MediaStatus.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + ECA20B8B8575BC5D4CA524D97013629E /* ImageDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1BBBBC3E2C89C0571FAFD881789F472 /* ImageDrawing.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + F09CEDFE1D6C126FEAA474EE2C4B7511 /* MediaRating.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4972FDAB83ACEF46497DFFFFC16516F6 /* MediaRating.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + F3B50ED57CF2AC153F25EC71D5710C36 /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6065ACC800466B7633BF7FC7D757DB95 /* Image.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + F9507F35C737F7D33CDB800055902927 /* AtomFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C2ECD6C191B0A19CEF79A2C6426038 /* AtomFeed.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + F97F54ADB03FC0313000CF758ECB726B /* ISO8601DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1446399C9EA6CC5BA86B74D225AE755 /* ISO8601DateFormatter.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + FD6D183C7A0B05A39DC6EC612B1C468A /* FeedParserProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BE008FD48D4F6E35FBA3FED7146877A /* FeedParserProtocol.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + FE59AD70E40482440E43666AB68E8185 /* MediaPrice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0B7EDC96D234509D1F409722E364843D /* MediaPrice.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; + FF73F255FCBC657C9AC4C4B2892F9A49 /* ContentNamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FC0A57318F55C550BEC24DA6E7BEAE /* ContentNamespace.swift */; settings = {COMPILER_FLAGS = "-w -Xanalyzer -analyzer-disable-all-checks"; }; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 14F21D00F7ED5AF6DFB232FA8FDD6F4F /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 1C8D67D8B72D6BA42CCEDB648537A340; + remoteInfo = SVProgressHUD; + }; + 7C0A6267D1F8885C20AD0A32B0114CCC /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 36B772207A736867F8DE882B98BC804D; + remoteInfo = FeedKit; + }; + E660666349D1C11D4C6818E494B64D40 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E8022D22FAA6690B5E1C379C1BCE1491; + remoteInfo = Kingfisher; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 09279B507E41AFA4AF04526590091315 /* Pods-rss-reader-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-rss-reader-Info.plist"; sourceTree = "<group>"; }; + 0B7EDC96D234509D1F409722E364843D /* MediaPrice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaPrice.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaPrice.swift; sourceTree = "<group>"; }; + 0C47B6B014A9B3D98175CA0F0857B78D /* XMLFeedType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XMLFeedType.swift; path = Sources/FeedKit/Parser/XMLFeedType.swift; sourceTree = "<group>"; }; + 0E8A5532883C5E07B2B8BBA4340CBE84 /* MediaThumbnail.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaThumbnail.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaThumbnail.swift; sourceTree = "<group>"; }; + 1088DA43224CC281BA4329787983EE30 /* RSSFeedCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedCategory.swift; path = Sources/FeedKit/Models/RSS/RSSFeedCategory.swift; sourceTree = "<group>"; }; + 137BD81FFC8CD495086672AAF63809C5 /* DateSpec.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DateSpec.swift; path = Sources/FeedKit/Dates/DateSpec.swift; sourceTree = "<group>"; }; + 14E949082AB7C8A8E09198AE3A8DE273 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationChallengeResponsable.swift; path = Sources/Networking/AuthenticationChallengeResponsable.swift; sourceTree = "<group>"; }; + 15B5400ED744016D47908B20C6B2FFEE /* Kingfisher.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.xcconfig; sourceTree = "<group>"; }; + 165C52E8454597B7916E9C051A69B06F /* RDFPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RDFPath.swift; path = Sources/FeedKit/Models/RSS/RDFPath.swift; sourceTree = "<group>"; }; + 16614D7ADA5196DAD4B56CAB05556EFC /* RSSFeed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeed.swift; path = Sources/FeedKit/Models/RSS/RSSFeed.swift; sourceTree = "<group>"; }; + 16668BD526836AC845AFFA68E4821CD0 /* MediaPeerLink.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaPeerLink.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaPeerLink.swift; sourceTree = "<group>"; }; + 16F684574F8BDB5474F326ED6D92C607 /* iTunesOwner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesOwner.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesOwner.swift; sourceTree = "<group>"; }; + 17F321D9C70F4C90E5FEBA75C14B5572 /* MemoryStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MemoryStorage.swift; path = Sources/Cache/MemoryStorage.swift; sourceTree = "<group>"; }; + 19C23BA34041C032A4CC0157E2C24ED3 /* MediaLocation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaLocation.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaLocation.swift; sourceTree = "<group>"; }; + 1AE804EA3DE7381E6071BC2A1F4B259E /* KingfisherOptionsInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherOptionsInfo.swift; path = Sources/General/KingfisherOptionsInfo.swift; sourceTree = "<group>"; }; + 1CA7B876F0213261CCA695BD922EC6F5 /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FormatIndicatedCacheSerializer.swift; path = Sources/Cache/FormatIndicatedCacheSerializer.swift; sourceTree = "<group>"; }; + 1CB9382DF97097374B02DE969548FE47 /* GIFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GIFAnimatedImage.swift; path = Sources/Image/GIFAnimatedImage.swift; sourceTree = "<group>"; }; + 1DA18158BBA03C01440B16464BC82202 /* RSSFeedItemEnclosure.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItemEnclosure.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItemEnclosure.swift; sourceTree = "<group>"; }; + 1FD70607A9BA8F197300BCC3F30FCD58 /* AtomFeedEntrySummary.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntrySummary.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntrySummary.swift; sourceTree = "<group>"; }; + 20AE9422BF116D6275189CF9A6B5889B /* RSSFeedSkipDay.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedSkipDay.swift; path = Sources/FeedKit/Models/RSS/RSSFeedSkipDay.swift; sourceTree = "<group>"; }; + 2166F72D19D1AFA02AE8FDE50D87AFD8 /* Array + Equatable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Array + Equatable.swift"; path = "Sources/FeedKit/Extensions/Array + Equatable.swift"; sourceTree = "<group>"; }; + 24DD8983EF39A2BC204FBBB8787B405B /* Kingfisher-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Kingfisher-Info.plist"; sourceTree = "<group>"; }; + 25DAB53DA28D19E601C22308841BFFDB /* FeedKit.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = FeedKit.xcconfig; sourceTree = "<group>"; }; + 2709B3CD2BFBBC25A29EDE7B272D9D98 /* iTunesCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesCategory.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesCategory.swift; sourceTree = "<group>"; }; + 2871EE69A16EA3E6A8233CFBD02007DD /* RFC822DateFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RFC822DateFormatter.swift; path = Sources/FeedKit/Dates/RFC822DateFormatter.swift; sourceTree = "<group>"; }; + 2AEC8A5C033FBDAF26C348606A20D5E9 /* Pods-rss-reader-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-rss-reader-acknowledgements.markdown"; sourceTree = "<group>"; }; + 2CCDA59331AA268A3476F6AAE39BA607 /* String + toBool.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String + toBool.swift"; path = "Sources/FeedKit/Extensions/String + toBool.swift"; sourceTree = "<group>"; }; + 2D1DE6DC7CAF8BF4BC8DC34BBD6CD559 /* JSONFeedParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedParser.swift; path = Sources/FeedKit/Parser/JSONFeedParser.swift; sourceTree = "<group>"; }; + 2DADC5341CACB6B3269CAC721CBB2034 /* AtomFeedEntrySource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntrySource.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntrySource.swift; sourceTree = "<group>"; }; + 2F69FEEEDC68DE0F38F88B8F3DBF30FE /* MediaStatistics.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaStatistics.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaStatistics.swift; sourceTree = "<group>"; }; + 2FF8662A73745FCCBAC4709E0289106C /* MediaCopyright.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaCopyright.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaCopyright.swift; sourceTree = "<group>"; }; + 324E675D213C3049EFAC157D647D898B /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Sources/Utility/Result.swift; sourceTree = "<group>"; }; + 338684D55B61478F757D050C28A9D840 /* Kingfisher.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Kingfisher.modulemap; sourceTree = "<group>"; }; + 34F04EB55291C530F492BB0131B22DB3 /* ImageProgressive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProgressive.swift; path = Sources/Image/ImageProgressive.swift; sourceTree = "<group>"; }; + 37FC0A57318F55C550BEC24DA6E7BEAE /* ContentNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ContentNamespace.swift; path = Sources/FeedKit/Models/Namespaces/Content/ContentNamespace.swift; sourceTree = "<group>"; }; + 3AFB4DCCCF6F2ABDC42DCE081F3FF5E7 /* MediaGroup.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaGroup.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaGroup.swift; sourceTree = "<group>"; }; + 3B0F4E84CEF07AD6C9B735D955FA94AD /* JSONFeedAttachment.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedAttachment.swift; path = Sources/FeedKit/Models/JSON/JSONFeedAttachment.swift; sourceTree = "<group>"; }; + 3B3639E2EC69C9FB2739632F887DAFD0 /* ImageDownloader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloader.swift; path = Sources/Networking/ImageDownloader.swift; sourceTree = "<group>"; }; + 3BE008FD48D4F6E35FBA3FED7146877A /* FeedParserProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedParserProtocol.swift; path = Sources/FeedKit/Parser/FeedParserProtocol.swift; sourceTree = "<group>"; }; + 3C42F443F10754A18CD3F7A3428B707B /* JSONFeedItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedItem.swift; path = Sources/FeedKit/Models/JSON/JSONFeedItem.swift; sourceTree = "<group>"; }; + 40024CBBE39B27E7087FF09697F6AF8F /* JSONFeedAuthor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedAuthor.swift; path = Sources/FeedKit/Models/JSON/JSONFeedAuthor.swift; sourceTree = "<group>"; }; + 40A6B4EC9527311C961032DAAC088138 /* MediaContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaContent.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaContent.swift; sourceTree = "<group>"; }; + 443A41F4868E2A22FF5E9590C4DFC9F9 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 473ACF809D1259FBE60761A8967D7CCF /* Pods_rss_reader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Pods_rss_reader.framework; path = "Pods-rss-reader.framework"; sourceTree = BUILT_PRODUCTS_DIR; }; + 48919EFB65ED62EBEC5DD3A70D9A6D80 /* MediaTag.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaTag.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaTag.swift; sourceTree = "<group>"; }; + 4972FDAB83ACEF46497DFFFFC16516F6 /* MediaRating.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaRating.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaRating.swift; sourceTree = "<group>"; }; + 4BD98FAA796D88DDBAADD135E4BA7E74 /* FeedKit-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "FeedKit-Info.plist"; sourceTree = "<group>"; }; + 4E00DB9D78B75E2A2930841FE4AFCBF2 /* Source.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Source.swift; path = Sources/General/ImageSource/Source.swift; sourceTree = "<group>"; }; + 504091429B677E8FB7CA49B4C26F7868 /* ImageTransition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageTransition.swift; path = Sources/Image/ImageTransition.swift; sourceTree = "<group>"; }; + 509EADEBE44C157920A347C15321FBFF /* RSSFeedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedImage.swift; path = Sources/FeedKit/Models/RSS/RSSFeedImage.swift; sourceTree = "<group>"; }; + 50A119B2D52752517068E094C36318D2 /* MediaStarRating.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaStarRating.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaStarRating.swift; sourceTree = "<group>"; }; + 50C444083B852533D2ECB5739685258D /* FeedKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = FeedKit.framework; path = FeedKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5320685CDE87E856EBBD5CA6E45CB410 /* AtomFeedEntryContributor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryContributor.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryContributor.swift; sourceTree = "<group>"; }; + 539B0E6130E9C3F2FAF9C62D7FB858B0 /* SVProgressHUD-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SVProgressHUD-prefix.pch"; sourceTree = "<group>"; }; + 53BCD7D5157AE3707182C9039200AA28 /* AtomFeedEntryAuthor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryAuthor.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryAuthor.swift; sourceTree = "<group>"; }; + 541B97121181512AF32CDA8299F661FD /* iTunesSubCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesSubCategory.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesSubCategory.swift; sourceTree = "<group>"; }; + 543B972EC7077844CCCF91693EE8B8AD /* Resource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Resource.swift; path = Sources/General/ImageSource/Resource.swift; sourceTree = "<group>"; }; + 5465124EF36B55F03A0E2DCA7E34F883 /* FeedKit-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FeedKit-prefix.pch"; sourceTree = "<group>"; }; + 54994F2207A861CC21DB3D9BAF60CBB7 /* SVProgressHUD.bundle */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = "wrapper.plug-in"; name = SVProgressHUD.bundle; path = SVProgressHUD/SVProgressHUD.bundle; sourceTree = "<group>"; }; + 549F0513E63CBA097AE4956D265CD6CF /* SVProgressHUD-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SVProgressHUD-Info.plist"; sourceTree = "<group>"; }; + 54E8CE224B938A3BB61D084D6BD675EF /* MediaStatus.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaStatus.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaStatus.swift; sourceTree = "<group>"; }; + 555C48B8D8CA7BB74DB490F2FAE4F307 /* MediaCommunity.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaCommunity.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaCommunity.swift; sourceTree = "<group>"; }; + 583FE1E1149C6E9FE3AE24152C09846B /* MediaCredit.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaCredit.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaCredit.swift; sourceTree = "<group>"; }; + 5D3F42F1BDCB9B98C796CFD5EE71EBAE /* MediaPlayer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaPlayer.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaPlayer.swift; sourceTree = "<group>"; }; + 5D61270DB9206085FA776593CA97E4AD /* RFC3339DateFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RFC3339DateFormatter.swift; path = Sources/FeedKit/Dates/RFC3339DateFormatter.swift; sourceTree = "<group>"; }; + 5E722A19CE147433309D0E60986525E4 /* MediaEmbed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaEmbed.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaEmbed.swift; sourceTree = "<group>"; }; + 6065ACC800466B7633BF7FC7D757DB95 /* Image.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.swift; path = Sources/Image/Image.swift; sourceTree = "<group>"; }; + 6129971BFA2CDEBDE94EE6354B48AEE6 /* Placeholder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Placeholder.swift; path = Sources/Image/Placeholder.swift; sourceTree = "<group>"; }; + 625A1DF46F6C55CB1F40843E95E89D63 /* SyndicationNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SyndicationNamespace.swift; path = Sources/FeedKit/Models/Namespaces/Syndication/SyndicationNamespace.swift; sourceTree = "<group>"; }; + 63037E47C69963786FDC94A801EFC1C9 /* MediaText.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaText.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaText.swift; sourceTree = "<group>"; }; + 63EFD00DF940AD7B61B32836F698AF6C /* AtomFeedEntryContent.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryContent.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryContent.swift; sourceTree = "<group>"; }; + 651384ED4AEE4550A759201DD39912C1 /* AtomFeedLink.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedLink.swift; path = Sources/FeedKit/Models/Atom/AtomFeedLink.swift; sourceTree = "<group>"; }; + 656EF9B4CA373CF0B71CBAD0B284AC38 /* Kingfisher-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Kingfisher-dummy.m"; sourceTree = "<group>"; }; + 6644152013A89EF84F8B5A0D82454382 /* Pods-rss-reader-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-rss-reader-umbrella.h"; sourceTree = "<group>"; }; + 69C4D09353AF812A98619BAC8667583C /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Sources/FeedKit/Parser/Result.swift; sourceTree = "<group>"; }; + 6A65F50DD58299EC33AA2DA09EB4BB15 /* SessionDataTask.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDataTask.swift; path = Sources/Networking/SessionDataTask.swift; sourceTree = "<group>"; }; + 6A6F3BB2C3AB4A1D609182D814BE032E /* ImageProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProcessor.swift; path = Sources/Image/ImageProcessor.swift; sourceTree = "<group>"; }; + 6B69A442482F6550C4925B8CBFAFD580 /* KingfisherError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherError.swift; path = Sources/General/KingfisherError.swift; sourceTree = "<group>"; }; + 6C557C33404E811077EFCBF326E97300 /* SVProgressHUD.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SVProgressHUD.m; path = SVProgressHUD/SVProgressHUD.m; sourceTree = "<group>"; }; + 6E71A6ACE602CD43F29E85F5DD7DD877 /* Filter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Filter.swift; path = Sources/Image/Filter.swift; sourceTree = "<group>"; }; + 7052ABCD4CC2B0E50BE8BFB157BD6E44 /* ImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProvider.swift; path = Sources/General/ImageSource/ImageDataProvider.swift; sourceTree = "<group>"; }; + 70F53F4D6B65D8E0DA5A6507B30C5147 /* String + toDate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String + toDate.swift"; path = "Sources/FeedKit/Extensions/String + toDate.swift"; sourceTree = "<group>"; }; + 71713BE51E82A2610A9B9700D0113F36 /* Indicator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Indicator.swift; path = Sources/Views/Indicator.swift; sourceTree = "<group>"; }; + 732D3FDE937FE3DED879CF34FADD3AA6 /* Pods-rss-reader-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-rss-reader-dummy.m"; sourceTree = "<group>"; }; + 74A97F1462EFFCE3C86A7E552A583851 /* SVProgressHUD-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SVProgressHUD-umbrella.h"; sourceTree = "<group>"; }; + 75725CFB719AC4FE06D1F6C0C7C2C5DE /* AtomFeedContributor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedContributor.swift; path = Sources/FeedKit/Models/Atom/AtomFeedContributor.swift; sourceTree = "<group>"; }; + 76D1F29EAB3985C1E7BB823578DA422D /* CallbackQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CallbackQueue.swift; path = Sources/Utility/CallbackQueue.swift; sourceTree = "<group>"; }; + 772A536060D21EF4EAFC4B074DA99157 /* SVRadialGradientLayer.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SVRadialGradientLayer.m; path = SVProgressHUD/SVRadialGradientLayer.m; sourceTree = "<group>"; }; + 794656D58BCB267487E25BF1004E4584 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/Accelerate.framework; sourceTree = DEVELOPER_DIR; }; + 7A60DABDD337558FACDB5EC539E0ADC2 /* Kingfisher.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = Kingfisher.h; path = Sources/Kingfisher.h; sourceTree = "<group>"; }; + 7AC83A87595BAAA9F2364DA4A9A7C5AC /* ImageModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageModifier.swift; path = Sources/Networking/ImageModifier.swift; sourceTree = "<group>"; }; + 7ACF8955D8CDC111802C06F42BA42DA7 /* ImageFormat.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageFormat.swift; path = Sources/Image/ImageFormat.swift; sourceTree = "<group>"; }; + 7B4DE7E9990ADC97DBF60731DF7A8C7A /* FeedParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedParser.swift; path = Sources/FeedKit/Parser/FeedParser.swift; sourceTree = "<group>"; }; + 7BDB2E021C2D9E1393C2982560383C50 /* Pods-rss-reader-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-rss-reader-frameworks.sh"; sourceTree = "<group>"; }; + 7DB688F82F59B6848979D3EEB0DEDCAB /* MediaDescription.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaDescription.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaDescription.swift; sourceTree = "<group>"; }; + 7EEB378F530376775079C1741BCC8D3A /* AtomFeedEntry.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntry.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntry.swift; sourceTree = "<group>"; }; + 83441447DF020810DCAA0CE159CD4F52 /* Delegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Delegate.swift; path = Sources/Utility/Delegate.swift; sourceTree = "<group>"; }; + 83A51626365D30D6EE7BF3A037EE515C /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloaderDelegate.swift; path = Sources/Networking/ImageDownloaderDelegate.swift; sourceTree = "<group>"; }; + 83A9A5C594C71235536FC962544032E9 /* SVProgressAnimatedView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SVProgressAnimatedView.h; path = SVProgressHUD/SVProgressAnimatedView.h; sourceTree = "<group>"; }; + 84C4AF6D73894070DD070CFAD0184F2D /* FeedKit-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "FeedKit-dummy.m"; sourceTree = "<group>"; }; + 8628FA0B8A98C0D0E4DB0A73305C0921 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIButton+Kingfisher.swift"; path = "Sources/Extensions/UIButton+Kingfisher.swift"; sourceTree = "<group>"; }; + 86EBB1C1B89196F9FC75E90492200B52 /* URL + replacingScheme.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "URL + replacingScheme.swift"; path = "Sources/FeedKit/Extensions/URL + replacingScheme.swift"; sourceTree = "<group>"; }; + 87676A7E8E4B90B49C136A48EA452055 /* FeedKit-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "FeedKit-umbrella.h"; sourceTree = "<group>"; }; + 8772019334A779595507E54CF4638844 /* AtomFeed + mapAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "AtomFeed + mapAttributes.swift"; path = "Sources/FeedKit/Models/Atom/AtomFeed + mapAttributes.swift"; sourceTree = "<group>"; }; + 879CA8B700D8ECD761CE717A3274FF80 /* Pods-rss-reader.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-rss-reader.debug.xcconfig"; sourceTree = "<group>"; }; + 8806A61FD9846EC0B2C5A987BE9B5C22 /* RSSPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSPath.swift; path = Sources/FeedKit/Models/RSS/RSSPath.swift; sourceTree = "<group>"; }; + 8AF9112064738C4385903F106E768CF7 /* Pods-rss-reader-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-rss-reader-acknowledgements.plist"; sourceTree = "<group>"; }; + 8B2F15846ACD42C0B7824A6959AF9DAE /* MediaNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaNamespace.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaNamespace.swift; sourceTree = "<group>"; }; + 8CDCB9B6D610DB97B879EA1FF4394227 /* Box.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Box.swift; path = Sources/Utility/Box.swift; sourceTree = "<group>"; }; + 8D255D956932AA5F68990F4E1D5F6B2C /* MediaTitle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaTitle.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaTitle.swift; sourceTree = "<group>"; }; + 8F3E2C62138F15C0AB5B2BBAA78E9E57 /* MediaHash.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaHash.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaHash.swift; sourceTree = "<group>"; }; + 92428E51776332F76D13B0B410BF93B6 /* MediaSubTitle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaSubTitle.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaSubTitle.swift; sourceTree = "<group>"; }; + 92553CFE920FDC70CB5FC41E817A75DA /* SVProgressHUD.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SVProgressHUD.modulemap; sourceTree = "<group>"; }; + 93EF214AB500180623133D713A88F583 /* DiskStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DiskStorage.swift; path = Sources/Cache/DiskStorage.swift; sourceTree = "<group>"; }; + 95B7664EB364606F03335718CB0A513E /* ExtensionHelpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExtensionHelpers.swift; path = Sources/Utility/ExtensionHelpers.swift; sourceTree = "<group>"; }; + 97F58968C852A2C4C9F6698BC9874EFD /* AtomFeedSubtitle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedSubtitle.swift; path = Sources/FeedKit/Models/Atom/AtomFeedSubtitle.swift; sourceTree = "<group>"; }; + 99248F1A349B54D8C816EFB6BFD72F3D /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Sources/Networking/SessionDelegate.swift; sourceTree = "<group>"; }; + 9B78DEA3C9913B578EC5C7F1581DDCB7 /* AtomFeedEntryCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryCategory.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryCategory.swift; sourceTree = "<group>"; }; + 9D412FD9DA895FF13762572FF113566B /* XMLFeedParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = XMLFeedParser.swift; path = Sources/FeedKit/Parser/XMLFeedParser.swift; sourceTree = "<group>"; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; lastKnownFileType = text; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 9E29C7196BE81BB59CBC46537AF9C40A /* AnimatedImageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedImageView.swift; path = Sources/Views/AnimatedImageView.swift; sourceTree = "<group>"; }; + 9FFD32A86B7B35FFA615DA2B70A06674 /* ParserError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ParserError.swift; path = Sources/FeedKit/Parser/ParserError.swift; sourceTree = "<group>"; }; + A06FDFF795586A6D1ADB680C2F6FC1CE /* FeedKit.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = FeedKit.modulemap; sourceTree = "<group>"; }; + A2BECD29B14E75CB2F77DB2C6080C870 /* AtomFeed + mapCharacters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "AtomFeed + mapCharacters.swift"; path = "Sources/FeedKit/Models/Atom/AtomFeed + mapCharacters.swift"; sourceTree = "<group>"; }; + A36DBC80309568D8C36F5CDDEBF43456 /* RequestModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestModifier.swift; path = Sources/Networking/RequestModifier.swift; sourceTree = "<group>"; }; + A4E94ADCAADD684CABFEDF90C2EB5481 /* SizeExtensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SizeExtensions.swift; path = Sources/Utility/SizeExtensions.swift; sourceTree = "<group>"; }; + A540002F1A251610EF19914E5C64C837 /* AtomFeedAuthor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedAuthor.swift; path = Sources/FeedKit/Models/Atom/AtomFeedAuthor.swift; sourceTree = "<group>"; }; + A58FD4E6088116886843E812E4E1EED3 /* MediaCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaCategory.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaCategory.swift; sourceTree = "<group>"; }; + A63B2C150405A5A878FB5902045E3170 /* RSSFeedItemGUID.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItemGUID.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItemGUID.swift; sourceTree = "<group>"; }; + A6CFC82AB159D55EC8632E8F4FE4F860 /* String+MD5.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String+MD5.swift"; path = "Sources/Utility/String+MD5.swift"; sourceTree = "<group>"; }; + AA0AC1583D82E255CC775A8F9142102F /* Deprecated.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Deprecated.swift; path = Sources/General/Deprecated.swift; sourceTree = "<group>"; }; + AD8C980C56E88282C19E6F62A77FBF5F /* AtomFeedEntryLink.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedEntryLink.swift; path = Sources/FeedKit/Models/Atom/AtomFeedEntryLink.swift; sourceTree = "<group>"; }; + AE064AC47BD833563801A0B2B0055DC9 /* Date + codingStrategy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "Date + codingStrategy.swift"; path = "Sources/FeedKit/Extensions/Date + codingStrategy.swift"; sourceTree = "<group>"; }; + AE3F92D56CBB1544BE441AB5F5933A52 /* AtomFeedCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedCategory.swift; path = Sources/FeedKit/Models/Atom/AtomFeedCategory.swift; sourceTree = "<group>"; }; + AEA69B64AFDFD241BF07C3B55E0BE92D /* AtomPath.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomPath.swift; path = Sources/FeedKit/Models/Atom/AtomPath.swift; sourceTree = "<group>"; }; + AECA125A27B9400F30D492BC6AD00A3C /* JSONFeed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeed.swift; path = Sources/FeedKit/Models/JSON/JSONFeed.swift; sourceTree = "<group>"; }; + B346C9C88A30476600F3846CC3E8E58C /* Storage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Storage.swift; path = Sources/Cache/Storage.swift; sourceTree = "<group>"; }; + BBAFD5ECEE4620CCFE2454114AD50772 /* CacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CacheSerializer.swift; path = Sources/Cache/CacheSerializer.swift; sourceTree = "<group>"; }; + BBFA500E5179F1C9F702A5E5A85408F5 /* RSSFeed + mapAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "RSSFeed + mapAttributes.swift"; path = "Sources/FeedKit/Models/RSS/RSSFeed + mapAttributes.swift"; sourceTree = "<group>"; }; + BBFA82C0CF3C9741780E91F24C8217D9 /* RSSFeedSkipHour.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedSkipHour.swift; path = Sources/FeedKit/Models/RSS/RSSFeedSkipHour.swift; sourceTree = "<group>"; }; + C03B8AD8A6AA30865F6B4E31C77346FD /* MediaRestriction.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaRestriction.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaRestriction.swift; sourceTree = "<group>"; }; + C1BBBBC3E2C89C0571FAFD881789F472 /* ImageDrawing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDrawing.swift; path = Sources/Image/ImageDrawing.swift; sourceTree = "<group>"; }; + C275A08017080970F47C305FA7EAD303 /* SVIndefiniteAnimatedView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SVIndefiniteAnimatedView.m; path = SVProgressHUD/SVIndefiniteAnimatedView.m; sourceTree = "<group>"; }; + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Kingfisher.framework; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C54C6012604BF887ED3CBF71B3138E7B /* RSSFeedItemCategory.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItemCategory.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItemCategory.swift; sourceTree = "<group>"; }; + C60BEFFC34C4F703FA0C19C3E4C9C8D5 /* AtomFeedGenerator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeedGenerator.swift; path = Sources/FeedKit/Models/Atom/AtomFeedGenerator.swift; sourceTree = "<group>"; }; + C7579CE4E143236193EA287B21927614 /* FeedDataType.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FeedDataType.swift; path = Sources/FeedKit/Parser/FeedDataType.swift; sourceTree = "<group>"; }; + C8CEA895EF932140041F71FFB1CE2B39 /* SVProgressHUD.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SVProgressHUD.xcconfig; sourceTree = "<group>"; }; + CC4D6ED92D036652805EBFF9079B0AC4 /* Kingfisher-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-prefix.pch"; sourceTree = "<group>"; }; + CD01E3FBFE3B9F258995EC51925E1C45 /* Pods-rss-reader.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-rss-reader.release.xcconfig"; sourceTree = "<group>"; }; + CD568E39CD0A28C5DA53EB4AC4ADFD22 /* iTunesImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesImage.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesImage.swift; sourceTree = "<group>"; }; + CD5D4382DA8BB442887B50B9AAABEF4E /* RSSFeed + mapCharacters.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "RSSFeed + mapCharacters.swift"; path = "Sources/FeedKit/Models/RSS/RSSFeed + mapCharacters.swift"; sourceTree = "<group>"; }; + CF301F3886EACFBEE0CDDABA55E2AD97 /* RSSFeedCloud.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedCloud.swift; path = Sources/FeedKit/Models/RSS/RSSFeedCloud.swift; sourceTree = "<group>"; }; + D18CBA10D13D64E9BE27F5F05E2C92B5 /* RSSFeedItem.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItem.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItem.swift; sourceTree = "<group>"; }; + D44DC20D483098B86B38C2FF7CF4DE93 /* MediaLicence.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaLicence.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaLicence.swift; sourceTree = "<group>"; }; + D6A00470DD5C3F8B5219AC3A535470E1 /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/Cache/ImageCache.swift; sourceTree = "<group>"; }; + D77DBB3D7B16431C2E8485ECD4928014 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ImageView+Kingfisher.swift"; path = "Sources/Extensions/ImageView+Kingfisher.swift"; sourceTree = "<group>"; }; + D7D79FBDEB1A0671571B63608F5F5B82 /* RSSFeedItemSource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedItemSource.swift; path = Sources/FeedKit/Models/RSS/RSSFeedItemSource.swift; sourceTree = "<group>"; }; + D9ECE6BA45D6EE56BEDCAB33E0962DCC /* Kingfisher-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-umbrella.h"; sourceTree = "<group>"; }; + DC5B72C88D115DD470FEBA574A100D7F /* DublinCoreNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DublinCoreNamespace.swift; path = "Sources/FeedKit/Models/Namespaces/Dublin Core/DublinCoreNamespace.swift"; sourceTree = "<group>"; }; + DC5F7330D2BFBBDFA5568FB0EF70149F /* MediaRights.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaRights.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaRights.swift; sourceTree = "<group>"; }; + DCADB4A6524885FB2211BCD934AB3089 /* MediaParam.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaParam.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaParam.swift; sourceTree = "<group>"; }; + DCFA5D4A65D5F6198862AD5125F0A1F6 /* iTunesNamespace.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = iTunesNamespace.swift; path = Sources/FeedKit/Models/Namespaces/iTunes/iTunesNamespace.swift; sourceTree = "<group>"; }; + DE1DD8A7EC65FEB33977CD4A3DAD94DF /* Runtime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Runtime.swift; path = Sources/Utility/Runtime.swift; sourceTree = "<group>"; }; + DFBB354B076400AE5066045D21B09AAD /* SVIndefiniteAnimatedView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SVIndefiniteAnimatedView.h; path = SVProgressHUD/SVIndefiniteAnimatedView.h; sourceTree = "<group>"; }; + DFC50DAC18C1698B4D9FA6D04F6BE333 /* RSSFeedTextInput.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RSSFeedTextInput.swift; path = Sources/FeedKit/Models/RSS/RSSFeedTextInput.swift; sourceTree = "<group>"; }; + E21F48C149AC7BE4DEDC1FC2DC88BC48 /* MediaScene.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MediaScene.swift; path = Sources/FeedKit/Models/Namespaces/Media/MediaScene.swift; sourceTree = "<group>"; }; + E2C2ECD6C191B0A19CEF79A2C6426038 /* AtomFeed.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AtomFeed.swift; path = Sources/FeedKit/Models/Atom/AtomFeed.swift; sourceTree = "<group>"; }; + E3D03B6D9C3ADE679C59CC3CB19AA938 /* SVRadialGradientLayer.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SVRadialGradientLayer.h; path = SVProgressHUD/SVRadialGradientLayer.h; sourceTree = "<group>"; }; + E7E7946458BA34E79D2D7610D1E8E74D /* Pods-rss-reader.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-rss-reader.modulemap"; sourceTree = "<group>"; }; + E958E7AE64834D7BF3595CA4D9BA62DC /* JSONFeedHub.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = JSONFeedHub.swift; path = Sources/FeedKit/Models/JSON/JSONFeedHub.swift; sourceTree = "<group>"; }; + E97D43C46A45EE515A4DA3AF94398441 /* SVProgressHUD.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SVProgressHUD.framework; path = SVProgressHUD.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + EB3EAD8084ABFD8F4E3B8C07E60D4B4A /* SVProgressAnimatedView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = SVProgressAnimatedView.m; path = SVProgressHUD/SVProgressAnimatedView.m; sourceTree = "<group>"; }; + EC693C3B81D9486A36EF6318F5B9F5C8 /* String + toDuration.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String + toDuration.swift"; path = "Sources/FeedKit/Extensions/String + toDuration.swift"; sourceTree = "<group>"; }; + ED121190AB2708ADD56BE77731CE7297 /* ImageDataProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProcessor.swift; path = Sources/Networking/ImageDataProcessor.swift; sourceTree = "<group>"; }; + EEDDF880825F67E1E975AE70682DECF6 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; }; + EF6DD4E0CBC5D7D9AA1D3A161C2010D8 /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Sources/Networking/RedirectHandler.swift; sourceTree = "<group>"; }; + F1446399C9EA6CC5BA86B74D225AE755 /* ISO8601DateFormatter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ISO8601DateFormatter.swift; path = Sources/FeedKit/Dates/ISO8601DateFormatter.swift; sourceTree = "<group>"; }; + F46308DB03F9943DECEDA43F019A4F1A /* SVProgressHUD-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SVProgressHUD-dummy.m"; sourceTree = "<group>"; }; + F86D924C65E6221835308B1C96B50003 /* Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Kingfisher.swift; path = Sources/General/Kingfisher.swift; sourceTree = "<group>"; }; + FA5AF49B1CA933991F633BBA2102CF33 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/QuartzCore.framework; sourceTree = DEVELOPER_DIR; }; + FBB5954ABD5F62A35C5A55EB72DB66B7 /* SVProgressHUD.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = SVProgressHUD.h; path = SVProgressHUD/SVProgressHUD.h; sourceTree = "<group>"; }; + FD41C6BBAF64669FA894679F798E0944 /* SyndicationUpdatePeriod.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SyndicationUpdatePeriod.swift; path = Sources/FeedKit/Models/Namespaces/Syndication/SyndicationUpdatePeriod.swift; sourceTree = "<group>"; }; + FE1CA9DECFF54250A9827AC454F43814 /* KingfisherManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherManager.swift; path = Sources/General/KingfisherManager.swift; sourceTree = "<group>"; }; + FEC38A9FFD814395AA2F91655CD53834 /* ImagePrefetcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImagePrefetcher.swift; path = Sources/Networking/ImagePrefetcher.swift; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 292196AFFFBA78379751F2E4FBB32298 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D7EA47C26320F33ADAF57632F7D13896 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + B3D1D1191F24E31CEEA04BDB3FB4C0F2 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D4914107FF5A197B9D0BC6D1C089E33D /* Foundation.framework in Frameworks */, + 99456829035D2883017E8EBCDB82C231 /* QuartzCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F07B3306CA3FE3F776588D27A5FF3D09 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 67776A8ED6E3B931649AD03090E61BBC /* Accelerate.framework in Frameworks */, + 6B3C32A5B5296C6B58EEADA1330717EB /* CFNetwork.framework in Frameworks */, + CE09ED6A621D2A33CD07586E0F999AA1 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FD74D8EFC263F0D55FD6FCAA0F1337A8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 2FEFDD8774FEDC3E1E57498E58322DE8 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 025A7E43AD7674DAF6620397DC0D40B5 /* Support Files */ = { + isa = PBXGroup; + children = ( + 338684D55B61478F757D050C28A9D840 /* Kingfisher.modulemap */, + 15B5400ED744016D47908B20C6B2FFEE /* Kingfisher.xcconfig */, + 656EF9B4CA373CF0B71CBAD0B284AC38 /* Kingfisher-dummy.m */, + 24DD8983EF39A2BC204FBBB8787B405B /* Kingfisher-Info.plist */, + CC4D6ED92D036652805EBFF9079B0AC4 /* Kingfisher-prefix.pch */, + D9ECE6BA45D6EE56BEDCAB33E0962DCC /* Kingfisher-umbrella.h */, + ); + name = "Support Files"; + path = "../Target Support Files/Kingfisher"; + sourceTree = "<group>"; + }; + 4B4119361F235A8947A3EC0FF1D26C35 /* Resources */ = { + isa = PBXGroup; + children = ( + 54994F2207A861CC21DB3D9BAF60CBB7 /* SVProgressHUD.bundle */, + ); + name = Resources; + sourceTree = "<group>"; + }; + 8522466A179C67070AB870F7D3659E89 /* Support Files */ = { + isa = PBXGroup; + children = ( + 92553CFE920FDC70CB5FC41E817A75DA /* SVProgressHUD.modulemap */, + C8CEA895EF932140041F71FFB1CE2B39 /* SVProgressHUD.xcconfig */, + F46308DB03F9943DECEDA43F019A4F1A /* SVProgressHUD-dummy.m */, + 549F0513E63CBA097AE4956D265CD6CF /* SVProgressHUD-Info.plist */, + 539B0E6130E9C3F2FAF9C62D7FB858B0 /* SVProgressHUD-prefix.pch */, + 74A97F1462EFFCE3C86A7E552A583851 /* SVProgressHUD-umbrella.h */, + ); + name = "Support Files"; + path = "../Target Support Files/SVProgressHUD"; + sourceTree = "<group>"; + }; + 8803558965EDB2ADA2D8513BCC64BE57 /* Kingfisher */ = { + isa = PBXGroup; + children = ( + 9E29C7196BE81BB59CBC46537AF9C40A /* AnimatedImageView.swift */, + 14E949082AB7C8A8E09198AE3A8DE273 /* AuthenticationChallengeResponsable.swift */, + 8CDCB9B6D610DB97B879EA1FF4394227 /* Box.swift */, + BBAFD5ECEE4620CCFE2454114AD50772 /* CacheSerializer.swift */, + 76D1F29EAB3985C1E7BB823578DA422D /* CallbackQueue.swift */, + 83441447DF020810DCAA0CE159CD4F52 /* Delegate.swift */, + AA0AC1583D82E255CC775A8F9142102F /* Deprecated.swift */, + 93EF214AB500180623133D713A88F583 /* DiskStorage.swift */, + 95B7664EB364606F03335718CB0A513E /* ExtensionHelpers.swift */, + 6E71A6ACE602CD43F29E85F5DD7DD877 /* Filter.swift */, + 1CA7B876F0213261CCA695BD922EC6F5 /* FormatIndicatedCacheSerializer.swift */, + 1CB9382DF97097374B02DE969548FE47 /* GIFAnimatedImage.swift */, + 6065ACC800466B7633BF7FC7D757DB95 /* Image.swift */, + D6A00470DD5C3F8B5219AC3A535470E1 /* ImageCache.swift */, + ED121190AB2708ADD56BE77731CE7297 /* ImageDataProcessor.swift */, + 7052ABCD4CC2B0E50BE8BFB157BD6E44 /* ImageDataProvider.swift */, + 3B3639E2EC69C9FB2739632F887DAFD0 /* ImageDownloader.swift */, + 83A51626365D30D6EE7BF3A037EE515C /* ImageDownloaderDelegate.swift */, + C1BBBBC3E2C89C0571FAFD881789F472 /* ImageDrawing.swift */, + 7ACF8955D8CDC111802C06F42BA42DA7 /* ImageFormat.swift */, + 7AC83A87595BAAA9F2364DA4A9A7C5AC /* ImageModifier.swift */, + FEC38A9FFD814395AA2F91655CD53834 /* ImagePrefetcher.swift */, + 6A6F3BB2C3AB4A1D609182D814BE032E /* ImageProcessor.swift */, + 34F04EB55291C530F492BB0131B22DB3 /* ImageProgressive.swift */, + 504091429B677E8FB7CA49B4C26F7868 /* ImageTransition.swift */, + D77DBB3D7B16431C2E8485ECD4928014 /* ImageView+Kingfisher.swift */, + 71713BE51E82A2610A9B9700D0113F36 /* Indicator.swift */, + 7A60DABDD337558FACDB5EC539E0ADC2 /* Kingfisher.h */, + F86D924C65E6221835308B1C96B50003 /* Kingfisher.swift */, + 6B69A442482F6550C4925B8CBFAFD580 /* KingfisherError.swift */, + FE1CA9DECFF54250A9827AC454F43814 /* KingfisherManager.swift */, + 1AE804EA3DE7381E6071BC2A1F4B259E /* KingfisherOptionsInfo.swift */, + 17F321D9C70F4C90E5FEBA75C14B5572 /* MemoryStorage.swift */, + 6129971BFA2CDEBDE94EE6354B48AEE6 /* Placeholder.swift */, + EF6DD4E0CBC5D7D9AA1D3A161C2010D8 /* RedirectHandler.swift */, + A36DBC80309568D8C36F5CDDEBF43456 /* RequestModifier.swift */, + 543B972EC7077844CCCF91693EE8B8AD /* Resource.swift */, + 324E675D213C3049EFAC157D647D898B /* Result.swift */, + DE1DD8A7EC65FEB33977CD4A3DAD94DF /* Runtime.swift */, + 6A65F50DD58299EC33AA2DA09EB4BB15 /* SessionDataTask.swift */, + 99248F1A349B54D8C816EFB6BFD72F3D /* SessionDelegate.swift */, + A4E94ADCAADD684CABFEDF90C2EB5481 /* SizeExtensions.swift */, + 4E00DB9D78B75E2A2930841FE4AFCBF2 /* Source.swift */, + B346C9C88A30476600F3846CC3E8E58C /* Storage.swift */, + A6CFC82AB159D55EC8632E8F4FE4F860 /* String+MD5.swift */, + 8628FA0B8A98C0D0E4DB0A73305C0921 /* UIButton+Kingfisher.swift */, + 025A7E43AD7674DAF6620397DC0D40B5 /* Support Files */, + ); + name = Kingfisher; + path = Kingfisher; + sourceTree = "<group>"; + }; + 8C950CDF52F1F187BC1DD04C4A198465 /* Pods */ = { + isa = PBXGroup; + children = ( + C9D1E846816B52568F79565215EFB717 /* FeedKit */, + 8803558965EDB2ADA2D8513BCC64BE57 /* Kingfisher */, + ED1551C937F4ED0130DD2CD665E4DE69 /* SVProgressHUD */, + ); + name = Pods; + sourceTree = "<group>"; + }; + 8E300B8511A75C1DE6CBDE499D0FEA03 /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + D498D432C2693071C20C6B1950881E05 /* Pods-rss-reader */, + ); + name = "Targets Support Files"; + sourceTree = "<group>"; + }; + 9D96ED884CA61191A8316BB9BF87C3D6 /* iOS */ = { + isa = PBXGroup; + children = ( + 794656D58BCB267487E25BF1004E4584 /* Accelerate.framework */, + EEDDF880825F67E1E975AE70682DECF6 /* CFNetwork.framework */, + 443A41F4868E2A22FF5E9590C4DFC9F9 /* Foundation.framework */, + FA5AF49B1CA933991F633BBA2102CF33 /* QuartzCore.framework */, + ); + name = iOS; + sourceTree = "<group>"; + }; + BA4F31F07263C99FC76E66D632A59F09 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 9D96ED884CA61191A8316BB9BF87C3D6 /* iOS */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + C9D1E846816B52568F79565215EFB717 /* FeedKit */ = { + isa = PBXGroup; + children = ( + 2166F72D19D1AFA02AE8FDE50D87AFD8 /* Array + Equatable.swift */, + E2C2ECD6C191B0A19CEF79A2C6426038 /* AtomFeed.swift */, + 8772019334A779595507E54CF4638844 /* AtomFeed + mapAttributes.swift */, + A2BECD29B14E75CB2F77DB2C6080C870 /* AtomFeed + mapCharacters.swift */, + A540002F1A251610EF19914E5C64C837 /* AtomFeedAuthor.swift */, + AE3F92D56CBB1544BE441AB5F5933A52 /* AtomFeedCategory.swift */, + 75725CFB719AC4FE06D1F6C0C7C2C5DE /* AtomFeedContributor.swift */, + 7EEB378F530376775079C1741BCC8D3A /* AtomFeedEntry.swift */, + 53BCD7D5157AE3707182C9039200AA28 /* AtomFeedEntryAuthor.swift */, + 9B78DEA3C9913B578EC5C7F1581DDCB7 /* AtomFeedEntryCategory.swift */, + 63EFD00DF940AD7B61B32836F698AF6C /* AtomFeedEntryContent.swift */, + 5320685CDE87E856EBBD5CA6E45CB410 /* AtomFeedEntryContributor.swift */, + AD8C980C56E88282C19E6F62A77FBF5F /* AtomFeedEntryLink.swift */, + 2DADC5341CACB6B3269CAC721CBB2034 /* AtomFeedEntrySource.swift */, + 1FD70607A9BA8F197300BCC3F30FCD58 /* AtomFeedEntrySummary.swift */, + C60BEFFC34C4F703FA0C19C3E4C9C8D5 /* AtomFeedGenerator.swift */, + 651384ED4AEE4550A759201DD39912C1 /* AtomFeedLink.swift */, + 97F58968C852A2C4C9F6698BC9874EFD /* AtomFeedSubtitle.swift */, + AEA69B64AFDFD241BF07C3B55E0BE92D /* AtomPath.swift */, + 37FC0A57318F55C550BEC24DA6E7BEAE /* ContentNamespace.swift */, + AE064AC47BD833563801A0B2B0055DC9 /* Date + codingStrategy.swift */, + 137BD81FFC8CD495086672AAF63809C5 /* DateSpec.swift */, + DC5B72C88D115DD470FEBA574A100D7F /* DublinCoreNamespace.swift */, + C7579CE4E143236193EA287B21927614 /* FeedDataType.swift */, + 7B4DE7E9990ADC97DBF60731DF7A8C7A /* FeedParser.swift */, + 3BE008FD48D4F6E35FBA3FED7146877A /* FeedParserProtocol.swift */, + F1446399C9EA6CC5BA86B74D225AE755 /* ISO8601DateFormatter.swift */, + 2709B3CD2BFBBC25A29EDE7B272D9D98 /* iTunesCategory.swift */, + CD568E39CD0A28C5DA53EB4AC4ADFD22 /* iTunesImage.swift */, + DCFA5D4A65D5F6198862AD5125F0A1F6 /* iTunesNamespace.swift */, + 16F684574F8BDB5474F326ED6D92C607 /* iTunesOwner.swift */, + 541B97121181512AF32CDA8299F661FD /* iTunesSubCategory.swift */, + AECA125A27B9400F30D492BC6AD00A3C /* JSONFeed.swift */, + 3B0F4E84CEF07AD6C9B735D955FA94AD /* JSONFeedAttachment.swift */, + 40024CBBE39B27E7087FF09697F6AF8F /* JSONFeedAuthor.swift */, + E958E7AE64834D7BF3595CA4D9BA62DC /* JSONFeedHub.swift */, + 3C42F443F10754A18CD3F7A3428B707B /* JSONFeedItem.swift */, + 2D1DE6DC7CAF8BF4BC8DC34BBD6CD559 /* JSONFeedParser.swift */, + A58FD4E6088116886843E812E4E1EED3 /* MediaCategory.swift */, + 555C48B8D8CA7BB74DB490F2FAE4F307 /* MediaCommunity.swift */, + 40A6B4EC9527311C961032DAAC088138 /* MediaContent.swift */, + 2FF8662A73745FCCBAC4709E0289106C /* MediaCopyright.swift */, + 583FE1E1149C6E9FE3AE24152C09846B /* MediaCredit.swift */, + 7DB688F82F59B6848979D3EEB0DEDCAB /* MediaDescription.swift */, + 5E722A19CE147433309D0E60986525E4 /* MediaEmbed.swift */, + 3AFB4DCCCF6F2ABDC42DCE081F3FF5E7 /* MediaGroup.swift */, + 8F3E2C62138F15C0AB5B2BBAA78E9E57 /* MediaHash.swift */, + D44DC20D483098B86B38C2FF7CF4DE93 /* MediaLicence.swift */, + 19C23BA34041C032A4CC0157E2C24ED3 /* MediaLocation.swift */, + 8B2F15846ACD42C0B7824A6959AF9DAE /* MediaNamespace.swift */, + DCADB4A6524885FB2211BCD934AB3089 /* MediaParam.swift */, + 16668BD526836AC845AFFA68E4821CD0 /* MediaPeerLink.swift */, + 5D3F42F1BDCB9B98C796CFD5EE71EBAE /* MediaPlayer.swift */, + 0B7EDC96D234509D1F409722E364843D /* MediaPrice.swift */, + 4972FDAB83ACEF46497DFFFFC16516F6 /* MediaRating.swift */, + C03B8AD8A6AA30865F6B4E31C77346FD /* MediaRestriction.swift */, + DC5F7330D2BFBBDFA5568FB0EF70149F /* MediaRights.swift */, + E21F48C149AC7BE4DEDC1FC2DC88BC48 /* MediaScene.swift */, + 50A119B2D52752517068E094C36318D2 /* MediaStarRating.swift */, + 2F69FEEEDC68DE0F38F88B8F3DBF30FE /* MediaStatistics.swift */, + 54E8CE224B938A3BB61D084D6BD675EF /* MediaStatus.swift */, + 92428E51776332F76D13B0B410BF93B6 /* MediaSubTitle.swift */, + 48919EFB65ED62EBEC5DD3A70D9A6D80 /* MediaTag.swift */, + 63037E47C69963786FDC94A801EFC1C9 /* MediaText.swift */, + 0E8A5532883C5E07B2B8BBA4340CBE84 /* MediaThumbnail.swift */, + 8D255D956932AA5F68990F4E1D5F6B2C /* MediaTitle.swift */, + 9FFD32A86B7B35FFA615DA2B70A06674 /* ParserError.swift */, + 165C52E8454597B7916E9C051A69B06F /* RDFPath.swift */, + 69C4D09353AF812A98619BAC8667583C /* Result.swift */, + 5D61270DB9206085FA776593CA97E4AD /* RFC3339DateFormatter.swift */, + 2871EE69A16EA3E6A8233CFBD02007DD /* RFC822DateFormatter.swift */, + 16614D7ADA5196DAD4B56CAB05556EFC /* RSSFeed.swift */, + BBFA500E5179F1C9F702A5E5A85408F5 /* RSSFeed + mapAttributes.swift */, + CD5D4382DA8BB442887B50B9AAABEF4E /* RSSFeed + mapCharacters.swift */, + 1088DA43224CC281BA4329787983EE30 /* RSSFeedCategory.swift */, + CF301F3886EACFBEE0CDDABA55E2AD97 /* RSSFeedCloud.swift */, + 509EADEBE44C157920A347C15321FBFF /* RSSFeedImage.swift */, + D18CBA10D13D64E9BE27F5F05E2C92B5 /* RSSFeedItem.swift */, + C54C6012604BF887ED3CBF71B3138E7B /* RSSFeedItemCategory.swift */, + 1DA18158BBA03C01440B16464BC82202 /* RSSFeedItemEnclosure.swift */, + A63B2C150405A5A878FB5902045E3170 /* RSSFeedItemGUID.swift */, + D7D79FBDEB1A0671571B63608F5F5B82 /* RSSFeedItemSource.swift */, + 20AE9422BF116D6275189CF9A6B5889B /* RSSFeedSkipDay.swift */, + BBFA82C0CF3C9741780E91F24C8217D9 /* RSSFeedSkipHour.swift */, + DFC50DAC18C1698B4D9FA6D04F6BE333 /* RSSFeedTextInput.swift */, + 8806A61FD9846EC0B2C5A987BE9B5C22 /* RSSPath.swift */, + 2CCDA59331AA268A3476F6AAE39BA607 /* String + toBool.swift */, + 70F53F4D6B65D8E0DA5A6507B30C5147 /* String + toDate.swift */, + EC693C3B81D9486A36EF6318F5B9F5C8 /* String + toDuration.swift */, + 625A1DF46F6C55CB1F40843E95E89D63 /* SyndicationNamespace.swift */, + FD41C6BBAF64669FA894679F798E0944 /* SyndicationUpdatePeriod.swift */, + 86EBB1C1B89196F9FC75E90492200B52 /* URL + replacingScheme.swift */, + 9D412FD9DA895FF13762572FF113566B /* XMLFeedParser.swift */, + 0C47B6B014A9B3D98175CA0F0857B78D /* XMLFeedType.swift */, + D31B22AE7BC700FA289C7FACE66F6E8A /* Support Files */, + ); + name = FeedKit; + path = FeedKit; + sourceTree = "<group>"; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + BA4F31F07263C99FC76E66D632A59F09 /* Frameworks */, + 8C950CDF52F1F187BC1DD04C4A198465 /* Pods */, + DC6DAFC24F657FFDE33D0D702BB43D67 /* Products */, + 8E300B8511A75C1DE6CBDE499D0FEA03 /* Targets Support Files */, + ); + sourceTree = "<group>"; + }; + D31B22AE7BC700FA289C7FACE66F6E8A /* Support Files */ = { + isa = PBXGroup; + children = ( + A06FDFF795586A6D1ADB680C2F6FC1CE /* FeedKit.modulemap */, + 25DAB53DA28D19E601C22308841BFFDB /* FeedKit.xcconfig */, + 84C4AF6D73894070DD070CFAD0184F2D /* FeedKit-dummy.m */, + 4BD98FAA796D88DDBAADD135E4BA7E74 /* FeedKit-Info.plist */, + 5465124EF36B55F03A0E2DCA7E34F883 /* FeedKit-prefix.pch */, + 87676A7E8E4B90B49C136A48EA452055 /* FeedKit-umbrella.h */, + ); + name = "Support Files"; + path = "../Target Support Files/FeedKit"; + sourceTree = "<group>"; + }; + D498D432C2693071C20C6B1950881E05 /* Pods-rss-reader */ = { + isa = PBXGroup; + children = ( + E7E7946458BA34E79D2D7610D1E8E74D /* Pods-rss-reader.modulemap */, + 2AEC8A5C033FBDAF26C348606A20D5E9 /* Pods-rss-reader-acknowledgements.markdown */, + 8AF9112064738C4385903F106E768CF7 /* Pods-rss-reader-acknowledgements.plist */, + 732D3FDE937FE3DED879CF34FADD3AA6 /* Pods-rss-reader-dummy.m */, + 7BDB2E021C2D9E1393C2982560383C50 /* Pods-rss-reader-frameworks.sh */, + 09279B507E41AFA4AF04526590091315 /* Pods-rss-reader-Info.plist */, + 6644152013A89EF84F8B5A0D82454382 /* Pods-rss-reader-umbrella.h */, + 879CA8B700D8ECD761CE717A3274FF80 /* Pods-rss-reader.debug.xcconfig */, + CD01E3FBFE3B9F258995EC51925E1C45 /* Pods-rss-reader.release.xcconfig */, + ); + name = "Pods-rss-reader"; + path = "Target Support Files/Pods-rss-reader"; + sourceTree = "<group>"; + }; + DC6DAFC24F657FFDE33D0D702BB43D67 /* Products */ = { + isa = PBXGroup; + children = ( + 50C444083B852533D2ECB5739685258D /* FeedKit.framework */, + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher.framework */, + 473ACF809D1259FBE60761A8967D7CCF /* Pods_rss_reader.framework */, + E97D43C46A45EE515A4DA3AF94398441 /* SVProgressHUD.framework */, + ); + name = Products; + sourceTree = "<group>"; + }; + ED1551C937F4ED0130DD2CD665E4DE69 /* SVProgressHUD */ = { + isa = PBXGroup; + children = ( + DFBB354B076400AE5066045D21B09AAD /* SVIndefiniteAnimatedView.h */, + C275A08017080970F47C305FA7EAD303 /* SVIndefiniteAnimatedView.m */, + 83A9A5C594C71235536FC962544032E9 /* SVProgressAnimatedView.h */, + EB3EAD8084ABFD8F4E3B8C07E60D4B4A /* SVProgressAnimatedView.m */, + FBB5954ABD5F62A35C5A55EB72DB66B7 /* SVProgressHUD.h */, + 6C557C33404E811077EFCBF326E97300 /* SVProgressHUD.m */, + E3D03B6D9C3ADE679C59CC3CB19AA938 /* SVRadialGradientLayer.h */, + 772A536060D21EF4EAFC4B074DA99157 /* SVRadialGradientLayer.m */, + 4B4119361F235A8947A3EC0FF1D26C35 /* Resources */, + 8522466A179C67070AB870F7D3659E89 /* Support Files */, + ); + name = SVProgressHUD; + path = SVProgressHUD; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 11D4493BA7FF77F097C1F019904A4C3A /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 5BAD3B67D901438E569930C898DC8E6C /* Pods-rss-reader-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6735BCF705F56E514257A1D52B661F6C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 8C033DAA94EC978A1A9934E45CEDB2DC /* FeedKit-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7C044C67F11489EFA3459B8399E7061C /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 96EADE99073DBA8FCE7904627E2D18EE /* SVIndefiniteAnimatedView.h in Headers */, + E0186A63060F0EB3676F95CD0ACE0E18 /* SVProgressAnimatedView.h in Headers */, + DE12057AB8895931A0E9499619C5CFC8 /* SVProgressHUD-umbrella.h in Headers */, + 0CA80ED7E59E6BCE64CEEE649A3C6D2F /* SVProgressHUD.h in Headers */, + D87B2D5ED73534ACF97CE0A52FA18AE4 /* SVRadialGradientLayer.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AE1843D8294075737CB8B327FC154AEE /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 203F98456AEFAAA6B288FADB0975CFCE /* Kingfisher-umbrella.h in Headers */, + 5D4759FF7D34320BC1BFE3171BD83250 /* Kingfisher.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 1C8D67D8B72D6BA42CCEDB648537A340 /* SVProgressHUD */ = { + isa = PBXNativeTarget; + buildConfigurationList = E76C288BA44757915CF3700D8A326EB7 /* Build configuration list for PBXNativeTarget "SVProgressHUD" */; + buildPhases = ( + 7C044C67F11489EFA3459B8399E7061C /* Headers */, + 54545EA62932CE6B8B101D93D78DE32C /* Sources */, + B3D1D1191F24E31CEEA04BDB3FB4C0F2 /* Frameworks */, + 7BB2E885B360CCC1AEF0C191542BA06E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SVProgressHUD; + productName = SVProgressHUD; + productReference = E97D43C46A45EE515A4DA3AF94398441 /* SVProgressHUD.framework */; + productType = "com.apple.product-type.framework"; + }; + 36B772207A736867F8DE882B98BC804D /* FeedKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = A07B948C57300D7CDB444D661FE1D01B /* Build configuration list for PBXNativeTarget "FeedKit" */; + buildPhases = ( + 6735BCF705F56E514257A1D52B661F6C /* Headers */, + FE51F8997489F53D8EF8BEB2B0C0A8A1 /* Sources */, + 292196AFFFBA78379751F2E4FBB32298 /* Frameworks */, + CA973EB6FF1A12CADF25A15DE9B035E4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = FeedKit; + productName = FeedKit; + productReference = 50C444083B852533D2ECB5739685258D /* FeedKit.framework */; + productType = "com.apple.product-type.framework"; + }; + 8095ED035A345C53B5D8D9AD64DD77EF /* Pods-rss-reader */ = { + isa = PBXNativeTarget; + buildConfigurationList = 181E25616FB73FA29A3213E2F9CDF468 /* Build configuration list for PBXNativeTarget "Pods-rss-reader" */; + buildPhases = ( + 11D4493BA7FF77F097C1F019904A4C3A /* Headers */, + 3A3820760E12D1142DB3300F6ED9A973 /* Sources */, + FD74D8EFC263F0D55FD6FCAA0F1337A8 /* Frameworks */, + 9488E589A84E8ECD11E6387AD55CCE69 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CE4EA61FDDF7FFDBD0FB2B20A63D259A /* PBXTargetDependency */, + F7E5F59B9E3EF56E72BEBBD15A1D9B99 /* PBXTargetDependency */, + AE169D9C61ECF1EA8D16B31DA7F3E9E6 /* PBXTargetDependency */, + ); + name = "Pods-rss-reader"; + productName = "Pods-rss-reader"; + productReference = 473ACF809D1259FBE60761A8967D7CCF /* Pods_rss_reader.framework */; + productType = "com.apple.product-type.framework"; + }; + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */ = { + isa = PBXNativeTarget; + buildConfigurationList = B0848743DBC84525A838C80EB5BF2A20 /* Build configuration list for PBXNativeTarget "Kingfisher" */; + buildPhases = ( + AE1843D8294075737CB8B327FC154AEE /* Headers */, + 4F2585F3B1D5EE3B57BF7DF67D1C0804 /* Sources */, + F07B3306CA3FE3F776588D27A5FF3D09 /* Frameworks */, + 7ADA03050A53BAC090314ACDC30F2BAA /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Kingfisher; + productName = Kingfisher; + productReference = C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher.framework */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1100; + LastUpgradeCheck = 1100; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 10.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + productRefGroup = DC6DAFC24F657FFDE33D0D702BB43D67 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 36B772207A736867F8DE882B98BC804D /* FeedKit */, + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */, + 8095ED035A345C53B5D8D9AD64DD77EF /* Pods-rss-reader */, + 1C8D67D8B72D6BA42CCEDB648537A340 /* SVProgressHUD */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7ADA03050A53BAC090314ACDC30F2BAA /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7BB2E885B360CCC1AEF0C191542BA06E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 264E1A478BDFA31DA6842749C72EA535 /* SVProgressHUD.bundle in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9488E589A84E8ECD11E6387AD55CCE69 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA973EB6FF1A12CADF25A15DE9B035E4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 3A3820760E12D1142DB3300F6ED9A973 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 038D25ACE83BC39A81C2489555BB69D7 /* Pods-rss-reader-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 4F2585F3B1D5EE3B57BF7DF67D1C0804 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8C237220DEC7332BA4EF352E98A5381F /* AnimatedImageView.swift in Sources */, + C2E9CA406FA22142181739ED45C12D6F /* AuthenticationChallengeResponsable.swift in Sources */, + 74936F026B909235BD1134B85ED39A67 /* Box.swift in Sources */, + 2739EE37237A7D0926C2B81AB2A67896 /* CacheSerializer.swift in Sources */, + 4F583F7ACCF07298F9E99740AF83AA92 /* CallbackQueue.swift in Sources */, + C0C32D91FC3E821256E5E39C48DA29A3 /* Delegate.swift in Sources */, + 82B7E5099B7FD6A199E9492FC76A5B8A /* Deprecated.swift in Sources */, + 31528FA91AB63958E8909A08826B4BE3 /* DiskStorage.swift in Sources */, + DEAD74F1DF0D2A15077DF8E4A7691DCC /* ExtensionHelpers.swift in Sources */, + AC0EED138B01B6A427C73C1D7D169183 /* Filter.swift in Sources */, + E5427D943DE6A10F0BE8AAE01F8FC0B4 /* FormatIndicatedCacheSerializer.swift in Sources */, + C3CA920F320825CB4C9B4041C257E3E1 /* GIFAnimatedImage.swift in Sources */, + F3B50ED57CF2AC153F25EC71D5710C36 /* Image.swift in Sources */, + 3B43D899F709090CC09C751F7E2598E5 /* ImageCache.swift in Sources */, + 9B483AABE10368563FD5A687B28B9BC5 /* ImageDataProcessor.swift in Sources */, + 45941825BC934119884B8E06016C8BB7 /* ImageDataProvider.swift in Sources */, + 9BCF1F2949BA37B8FB61A7D529D2314C /* ImageDownloader.swift in Sources */, + 9ADF639A256B1FEBF985539A6E86A62D /* ImageDownloaderDelegate.swift in Sources */, + ECA20B8B8575BC5D4CA524D97013629E /* ImageDrawing.swift in Sources */, + 1C816AA1100B07979C954FF4887D2512 /* ImageFormat.swift in Sources */, + D169FB8F82E88E646EBAFA2AD581F345 /* ImageModifier.swift in Sources */, + 248843CDBED004C50ABA8E59C5713419 /* ImagePrefetcher.swift in Sources */, + B9F01224843ED00E8198B73A0871CA91 /* ImageProcessor.swift in Sources */, + 5970A55B036B38488F0E41E1F8495E4E /* ImageProgressive.swift in Sources */, + 4804C796B0288D0412E7F095317999D0 /* ImageTransition.swift in Sources */, + 6456FA04CF508F55B3B54A5C504DBEF7 /* ImageView+Kingfisher.swift in Sources */, + 77DCE19243521302D7C8DECFB5306050 /* Indicator.swift in Sources */, + B650BEABA29BF753EFACF1779B5E7C0D /* Kingfisher-dummy.m in Sources */, + B6810F38B400C8C3F09E206001233D62 /* Kingfisher.swift in Sources */, + 82AAAB7D00602E4421E64496ADFA18CA /* KingfisherError.swift in Sources */, + 9EE0C393C016BF8D51A8762181DC7F27 /* KingfisherManager.swift in Sources */, + 87A7B91615CAF5AD17EA2FA9241CA357 /* KingfisherOptionsInfo.swift in Sources */, + 54673701214B48CEF8136269D791B482 /* MemoryStorage.swift in Sources */, + 4C22C3C0D8C162A61B03898A178C1A69 /* Placeholder.swift in Sources */, + 2FABED4580092268CC87F9403D2A8A4A /* RedirectHandler.swift in Sources */, + 8C351C88FD63A54AF59B79CB66BAEA3D /* RequestModifier.swift in Sources */, + 7EE55A9563025F727B3927FE304A8F69 /* Resource.swift in Sources */, + 4744296660B21C03A3D2412D13EE7F0B /* Result.swift in Sources */, + DC5E3722AEC8AAA22522F74F7A2F8DDB /* Runtime.swift in Sources */, + 725F9F4A21430F094ACB74E28A192507 /* SessionDataTask.swift in Sources */, + 87CF6D3613627429C651988DA4FE1ACD /* SessionDelegate.swift in Sources */, + CB2C4195422C4DBB0498E56384F002A9 /* SizeExtensions.swift in Sources */, + 242706DC099EEFB54A5FE8E5FB689CE5 /* Source.swift in Sources */, + 6009C5769E0038B20692E7328E59CD9A /* Storage.swift in Sources */, + 13A494F795FD220FC54FF06A8E9FBA87 /* String+MD5.swift in Sources */, + B7E6A02AE234F17885948C03F8E86732 /* UIButton+Kingfisher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 54545EA62932CE6B8B101D93D78DE32C /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5017F297AAA171A30A29BC2F85D30D1B /* SVIndefiniteAnimatedView.m in Sources */, + 61D552992D3522BCF5DFFA2B716FFA67 /* SVProgressAnimatedView.m in Sources */, + BE7771F000A75785BD4737778FD7DE4C /* SVProgressHUD-dummy.m in Sources */, + 2D2801427B88163A3FE72A41B896508F /* SVProgressHUD.m in Sources */, + DB19804BA6B117B4B5CBB959C9C3AFFB /* SVRadialGradientLayer.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FE51F8997489F53D8EF8BEB2B0C0A8A1 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 60A5B1A89CF28CC9E0FB642AD346FF69 /* Array + Equatable.swift in Sources */, + 80ADA274F8C90BD1D2047FC7C0550C91 /* AtomFeed + mapAttributes.swift in Sources */, + 073EBC91634A2B318E36A798D0EB200E /* AtomFeed + mapCharacters.swift in Sources */, + F9507F35C737F7D33CDB800055902927 /* AtomFeed.swift in Sources */, + 6D79BECCC5B6689C3529EA7DEABA5655 /* AtomFeedAuthor.swift in Sources */, + 18EDB325C46E6985C30463F48F68928C /* AtomFeedCategory.swift in Sources */, + 5578DBFDA0183FEFE14052D02795D5E0 /* AtomFeedContributor.swift in Sources */, + A3EF264FE0D4780D5D04F4DF5943A555 /* AtomFeedEntry.swift in Sources */, + 0EB010641B5BB3B4F9F3F8C48B8F43D2 /* AtomFeedEntryAuthor.swift in Sources */, + 3BA0E7BB5C61807BBD53F1343AD8AF17 /* AtomFeedEntryCategory.swift in Sources */, + CAC07C0D19114DB366430C72EB91C2F2 /* AtomFeedEntryContent.swift in Sources */, + 7B97C87E6B21F3C9A39F3FAC8D543E37 /* AtomFeedEntryContributor.swift in Sources */, + 567DBB92174116AED62947169642C400 /* AtomFeedEntryLink.swift in Sources */, + 9B9D036796D371A3A9E1C581F1D1106F /* AtomFeedEntrySource.swift in Sources */, + BB777C6CD71A788DB36E972CDD36C8E5 /* AtomFeedEntrySummary.swift in Sources */, + 36B028172F7163B49D931630F497F6E0 /* AtomFeedGenerator.swift in Sources */, + 110E982216772861B919ED18D2F2D07E /* AtomFeedLink.swift in Sources */, + 369455C05E84142AB77CDA2F4CB624B9 /* AtomFeedSubtitle.swift in Sources */, + EA2DF0C7981254111ED5227D89B5810C /* AtomPath.swift in Sources */, + FF73F255FCBC657C9AC4C4B2892F9A49 /* ContentNamespace.swift in Sources */, + CAAB9B7375BFBEDAA48CE4AF0D33CF5B /* Date + codingStrategy.swift in Sources */, + DDA449EC2E3881A3BCE542BEE9A65FFB /* DateSpec.swift in Sources */, + C5DA83446CA44CDB6F9F0E08802A7BF0 /* DublinCoreNamespace.swift in Sources */, + 227AFB8E66DF3543661168E4FE669F8A /* FeedDataType.swift in Sources */, + 702CC18C73D99CE5FBD89C07F73A3728 /* FeedKit-dummy.m in Sources */, + 2FA41B134B6113F26902B5C53295DFB9 /* FeedParser.swift in Sources */, + FD6D183C7A0B05A39DC6EC612B1C468A /* FeedParserProtocol.swift in Sources */, + F97F54ADB03FC0313000CF758ECB726B /* ISO8601DateFormatter.swift in Sources */, + 0812AA51AEDD629B2C34B4297E6E0274 /* iTunesCategory.swift in Sources */, + 6424D3B7E24310353026B9B60A255C9C /* iTunesImage.swift in Sources */, + 90CC11986A2DF24E9E9BABFAA33EAD19 /* iTunesNamespace.swift in Sources */, + 3C2EB58B4DD52CF60E88D6290325F99F /* iTunesOwner.swift in Sources */, + 189AF6B9611D028F4D0F553420D123F4 /* iTunesSubCategory.swift in Sources */, + 1E37499305E26A478A559CA658AEAB6C /* JSONFeed.swift in Sources */, + 28D990407E4192188B6F701FD02800AC /* JSONFeedAttachment.swift in Sources */, + 0DB652314FBEE783580DFBA1327739AC /* JSONFeedAuthor.swift in Sources */, + 42B572E80566E8851C81777954E68EBF /* JSONFeedHub.swift in Sources */, + AB48F74A791D71FE6A1A12E5D0AB787B /* JSONFeedItem.swift in Sources */, + 055E7E01B1CD7E508F9DF3ECCF90D57D /* JSONFeedParser.swift in Sources */, + BA5F5446C689D273471CED79A8826237 /* MediaCategory.swift in Sources */, + DB295F5C89B9118DE89BAEB49ACCCE1B /* MediaCommunity.swift in Sources */, + 073B9D7521458F8915742E4D4FC605C0 /* MediaContent.swift in Sources */, + 2C8D6BE7CD7BB661925F0C6C6619B2E3 /* MediaCopyright.swift in Sources */, + 2BED157C050F421E3820D7A59430F3E2 /* MediaCredit.swift in Sources */, + 7465C633F25B81D4872FD8B34FB7A04E /* MediaDescription.swift in Sources */, + 7B67479EBAB1C7FB8016086FB2A6C36F /* MediaEmbed.swift in Sources */, + C5BFA31A2D0C0B893CE605CD5AF44400 /* MediaGroup.swift in Sources */, + 5762628817E5F2058ABE3BA80E3CFA4A /* MediaHash.swift in Sources */, + C5A7629109A5C715E58DDD422778785D /* MediaLicence.swift in Sources */, + 3173641F16B0577B73D506D06E78CBAE /* MediaLocation.swift in Sources */, + 735AC52A9D1D5B6508D65328E3AD9E8B /* MediaNamespace.swift in Sources */, + 10F1569D025E3737E99F2C8238471D27 /* MediaParam.swift in Sources */, + 142640D3547A3760893CC57B1C533F9D /* MediaPeerLink.swift in Sources */, + BFFAF10DB9969D44312C7F5A318A48D0 /* MediaPlayer.swift in Sources */, + FE59AD70E40482440E43666AB68E8185 /* MediaPrice.swift in Sources */, + F09CEDFE1D6C126FEAA474EE2C4B7511 /* MediaRating.swift in Sources */, + 3DFF7C6E6BE9AD90CA4DC334BC7C7217 /* MediaRestriction.swift in Sources */, + B1778FF3264766BF9E59AA21E8CD2897 /* MediaRights.swift in Sources */, + 154CF2AD328B1D874021654776969DB9 /* MediaScene.swift in Sources */, + 5629D646E14624BE2CB59D6592AA9E75 /* MediaStarRating.swift in Sources */, + 4A9E953B6844847D60D4A72A4AC8104F /* MediaStatistics.swift in Sources */, + EBBD11156E33F00FA156B6F0D1B4EC28 /* MediaStatus.swift in Sources */, + 00F3A9C8CA21565B4B7AD1F24916E73B /* MediaSubTitle.swift in Sources */, + 7F9C37EFF94BD1D269179AC4E2BAED8D /* MediaTag.swift in Sources */, + 0E7CEAE2A49A10164728A1CC1E657A36 /* MediaText.swift in Sources */, + 4A125BC3F7DB1C72C99261F06E4B0DDF /* MediaThumbnail.swift in Sources */, + A1AC959CC5F9E2837BA9B8BB1DEA0ACB /* MediaTitle.swift in Sources */, + 1719C634E234F7F04B5BC41A9EECE3DF /* ParserError.swift in Sources */, + 877F08DB7B5D28F403662CF36FEEE820 /* RDFPath.swift in Sources */, + DC7BDA3522A381DB3391B725910830F4 /* Result.swift in Sources */, + 56BAD22FE6CEF39D48EDBC1401070B84 /* RFC3339DateFormatter.swift in Sources */, + BB92D94DA24C7EBD147D10901E181B5F /* RFC822DateFormatter.swift in Sources */, + 1467DCC83C265C5E079BCBCE8C930527 /* RSSFeed + mapAttributes.swift in Sources */, + 0C8DA927DCA34A34191DD5BE88F7FE7E /* RSSFeed + mapCharacters.swift in Sources */, + 033945D564AEB6ECD53E4D574700DC27 /* RSSFeed.swift in Sources */, + C9B2585312927033FE5D3CB9511C71E8 /* RSSFeedCategory.swift in Sources */, + 336A41C351628C1E5B0B4EB48A906DD7 /* RSSFeedCloud.swift in Sources */, + 5FE02B9DFC0737DB60435091546ACDA8 /* RSSFeedImage.swift in Sources */, + 69B7A7EF76EA0E030D834AB508C33271 /* RSSFeedItem.swift in Sources */, + 633A123F6AA4CDD4C1F7B7AE87E92CC8 /* RSSFeedItemCategory.swift in Sources */, + E958799A1B06F5EEA032166E8DFA9F7E /* RSSFeedItemEnclosure.swift in Sources */, + 244C5805DC1654F022D8072763E1ADB9 /* RSSFeedItemGUID.swift in Sources */, + 23F070DD81AB8CB83CA08BA572F16826 /* RSSFeedItemSource.swift in Sources */, + D1CEB905D848609DF9E7B720AB2D2B68 /* RSSFeedSkipDay.swift in Sources */, + 1A9291D46FAB68A67AF5801C60091520 /* RSSFeedSkipHour.swift in Sources */, + A760D87AB7011C7ECDB714AFAA054590 /* RSSFeedTextInput.swift in Sources */, + 08FF12ED51AB815DA30478115AD69A84 /* RSSPath.swift in Sources */, + A2C29BC3E4187AC14B5EA4775729BA89 /* String + toBool.swift in Sources */, + 305E27FCC7492EB8903D794D8A96EF99 /* String + toDate.swift in Sources */, + 9BF0BA499FF3E37A8B9CEC704EC00BA9 /* String + toDuration.swift in Sources */, + B63B7E7E743B189124AD4F3CEC542C6B /* SyndicationNamespace.swift in Sources */, + 18EC7CFA2A1F9D494FFA712EB4A00BED /* SyndicationUpdatePeriod.swift in Sources */, + 3682672E63B3B2F8714D49F4E526FE08 /* URL + replacingScheme.swift in Sources */, + 485E8C15B4C2E986BB0F4E5E11562AA1 /* XMLFeedParser.swift in Sources */, + A3810D9AEA6C4D8B5AB7A1A45397F331 /* XMLFeedType.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + AE169D9C61ECF1EA8D16B31DA7F3E9E6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = SVProgressHUD; + target = 1C8D67D8B72D6BA42CCEDB648537A340 /* SVProgressHUD */; + targetProxy = 14F21D00F7ED5AF6DFB232FA8FDD6F4F /* PBXContainerItemProxy */; + }; + CE4EA61FDDF7FFDBD0FB2B20A63D259A /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = FeedKit; + target = 36B772207A736867F8DE882B98BC804D /* FeedKit */; + targetProxy = 7C0A6267D1F8885C20AD0A32B0114CCC /* PBXContainerItemProxy */; + }; + F7E5F59B9E3EF56E72BEBBD15A1D9B99 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Kingfisher; + target = E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */; + targetProxy = E660666349D1C11D4C6818E494B64D40 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 131356BE54884448CA49C07BEDF4BB2A /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_DEBUG=1", + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Debug; + }; + 79A8465211B0D0081BE3549ADCE2AA08 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 25DAB53DA28D19E601C22308841BFFDB /* FeedKit.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/FeedKit/FeedKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/FeedKit/FeedKit-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/FeedKit/FeedKit.modulemap"; + PRODUCT_MODULE_NAME = FeedKit; + PRODUCT_NAME = FeedKit; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 8199B4DC91858DC34CCA904A4438F4F7 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 879CA8B700D8ECD761CE717A3274FF80 /* Pods-rss-reader.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-rss-reader/Pods-rss-reader-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-rss-reader/Pods-rss-reader.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 88B75A679D7A2D7B9A808B996E49F3FC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 15B5400ED744016D47908B20C6B2FFEE /* Kingfisher.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; + PRODUCT_MODULE_NAME = Kingfisher; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 996001DAD20CED9D9EFFCC0503E4E0DF /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C8CEA895EF932140041F71FFB1CE2B39 /* SVProgressHUD.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/SVProgressHUD/SVProgressHUD-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/SVProgressHUD/SVProgressHUD-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/SVProgressHUD/SVProgressHUD.modulemap"; + PRODUCT_MODULE_NAME = SVProgressHUD; + PRODUCT_NAME = SVProgressHUD; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + AB556174290E39DE9C082CA03D93CCB1 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = C8CEA895EF932140041F71FFB1CE2B39 /* SVProgressHUD.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/SVProgressHUD/SVProgressHUD-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/SVProgressHUD/SVProgressHUD-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/SVProgressHUD/SVProgressHUD.modulemap"; + PRODUCT_MODULE_NAME = SVProgressHUD; + PRODUCT_NAME = SVProgressHUD; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + D2456F9201345BD913EA10AABCA9D5EA /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = CD01E3FBFE3B9F258995EC51925E1C45 /* Pods-rss-reader.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = "Target Support Files/Pods-rss-reader/Pods-rss-reader-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MACH_O_TYPE = staticlib; + MODULEMAP_FILE = "Target Support Files/Pods-rss-reader/Pods-rss-reader.modulemap"; + OTHER_LDFLAGS = ""; + OTHER_LIBTOOLFLAGS = ""; + PODS_ROOT = "$(SRCROOT)"; + PRODUCT_BUNDLE_IDENTIFIER = "org.cocoapods.${PRODUCT_NAME:rfc1034identifier}"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + D3C85F45DB51A8C745560E614E4E0F4D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 15B5400ED744016D47908B20C6B2FFEE /* Kingfisher.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/Kingfisher/Kingfisher-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/Kingfisher/Kingfisher-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 10.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/Kingfisher/Kingfisher.modulemap"; + PRODUCT_MODULE_NAME = Kingfisher; + PRODUCT_NAME = Kingfisher; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + F090CD07A80273D5A73C8EA19224ADDB /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = ( + "POD_CONFIGURATION_RELEASE=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRIP_INSTALLED_PRODUCT = NO; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_VERSION = 5.0; + SYMROOT = "${SRCROOT}/../build"; + }; + name = Release; + }; + F1CB0E0238963A7B802E7BCBE3960740 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 25DAB53DA28D19E601C22308841BFFDB /* FeedKit.xcconfig */; + buildSettings = { + CODE_SIGN_IDENTITY = ""; + "CODE_SIGN_IDENTITY[sdk=appletvos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = ""; + "CODE_SIGN_IDENTITY[sdk=watchos*]" = ""; + CURRENT_PROJECT_VERSION = 1; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_PREFIX_HEADER = "Target Support Files/FeedKit/FeedKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/FeedKit/FeedKit-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 8.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/FeedKit/FeedKit.modulemap"; + PRODUCT_MODULE_NAME = FeedKit; + PRODUCT_NAME = FeedKit; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) "; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 181E25616FB73FA29A3213E2F9CDF468 /* Build configuration list for PBXNativeTarget "Pods-rss-reader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8199B4DC91858DC34CCA904A4438F4F7 /* Debug */, + D2456F9201345BD913EA10AABCA9D5EA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 131356BE54884448CA49C07BEDF4BB2A /* Debug */, + F090CD07A80273D5A73C8EA19224ADDB /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A07B948C57300D7CDB444D661FE1D01B /* Build configuration list for PBXNativeTarget "FeedKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 79A8465211B0D0081BE3549ADCE2AA08 /* Debug */, + F1CB0E0238963A7B802E7BCBE3960740 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + B0848743DBC84525A838C80EB5BF2A20 /* Build configuration list for PBXNativeTarget "Kingfisher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D3C85F45DB51A8C745560E614E4E0F4D /* Debug */, + 88B75A679D7A2D7B9A808B996E49F3FC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E76C288BA44757915CF3700D8A326EB7 /* Build configuration list for PBXNativeTarget "SVProgressHUD" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AB556174290E39DE9C082CA03D93CCB1 /* Debug */, + 996001DAD20CED9D9EFFCC0503E4E0DF /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/Pods/SVProgressHUD/LICENSE b/Pods/SVProgressHUD/LICENSE new file mode 100644 index 0000000..f8c911b --- /dev/null +++ b/Pods/SVProgressHUD/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2011-2018 Sam Vermette, Tobias Tiemerding and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Pods/SVProgressHUD/README.md b/Pods/SVProgressHUD/README.md new file mode 100644 index 0000000..a9ce9e0 --- /dev/null +++ b/Pods/SVProgressHUD/README.md @@ -0,0 +1,218 @@ +# SVProgressHUD + +![Pod Version](https://img.shields.io/cocoapods/v/SVProgressHUD.svg?style=flat) +![Pod Platform](https://img.shields.io/cocoapods/p/SVProgressHUD.svg?style=flat) +![Pod License](https://img.shields.io/cocoapods/l/SVProgressHUD.svg?style=flat) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-green.svg?style=flat)](https://github.com/Carthage/Carthage) +[![CocoaPods compatible](https://img.shields.io/badge/CocoaPods-compatible-green.svg?style=flat)](https://cocoapods.org) + +`SVProgressHUD` is a clean and easy-to-use HUD meant to display the progress of an ongoing task on iOS and tvOS. + +![SVProgressHUD](http://f.cl.ly/items/2G1F1Z0M0k0h2U3V1p39/SVProgressHUD.gif) + +## Demo + +Try `SVProgressHUD` on [Appetize.io](https://appetize.io/app/p8r2cvy8kq74x7q7tjqf5gyatr). + +## Installation + +### From CocoaPods + +[CocoaPods](http://cocoapods.org) is a dependency manager for Objective-C, which automates and simplifies the process of using 3rd-party libraries like `SVProgressHUD` in your projects. First, add the following line to your [Podfile](http://guides.cocoapods.org/using/using-cocoapods.html): + +```ruby +pod 'SVProgressHUD' +``` + +If you want to use the latest features of `SVProgressHUD` use normal external source dependencies. + +```ruby +pod 'SVProgressHUD', :git => 'https://github.com/SVProgressHUD/SVProgressHUD.git' +``` + +This pulls from the `master` branch directly. + +Second, install `SVProgressHUD` into your project: + +```ruby +pod install +``` + +### Carthage + +[Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks. To integrate `SVProgressHUD` into your Xcode project using Carthage, specify it in your `Cartfile`: + +```ogdl +github "SVProgressHUD/SVProgressHUD" +``` + +Run `carthage bootstrap` to build the framework in your repository's Carthage directory. You can then include it in your target's `carthage copy-frameworks` build phase. For more information on this, please see [Carthage's documentation](https://github.com/carthage/carthage#if-youre-building-for-ios-tvos-or-watchos). + +### Manually + +* Drag the `SVProgressHUD/SVProgressHUD` folder into your project. +* Take care that `SVProgressHUD.bundle` is added to `Targets->Build Phases->Copy Bundle Resources`. +* Add the **QuartzCore** framework to your project. + +## Swift + +Even though `SVProgressHUD` is written in Objective-C, it can be used in Swift with no hassle. If you use [CocoaPods](http://cocoapods.org) add the following line to your [Podfile](http://guides.cocoapods.org/using/using-cocoapods.html): + +```ruby +use_frameworks! +``` + +If you added `SVProgressHUD` manually, just add a [bridging header](https://developer.apple.com/library/content/documentation/Swift/Conceptual/BuildingCocoaApps/MixandMatch.html) file to your project with the `SVProgressHUD` header included. + +## Usage + +(see sample Xcode project in `/Demo`) + +`SVProgressHUD` is created as a singleton (i.e. it doesn't need to be explicitly allocated and instantiated; you directly call `[SVProgressHUD method]`). + +**Use `SVProgressHUD` wisely! Only use it if you absolutely need to perform a task before taking the user forward. Bad use case examples: pull to refresh, infinite scrolling, sending message.** + +Using `SVProgressHUD` in your app will usually look as simple as this (using Grand Central Dispatch): + +```objective-c +[SVProgressHUD show]; +dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // time-consuming task + dispatch_async(dispatch_get_main_queue(), ^{ + [SVProgressHUD dismiss]; + }); +}); +``` + +### Showing the HUD + +You can show the status of indeterminate tasks using one of the following: + +```objective-c ++ (void)show; ++ (void)showWithStatus:(NSString*)string; +``` + +If you'd like the HUD to reflect the progress of a task, use one of these: + +```objective-c ++ (void)showProgress:(CGFloat)progress; ++ (void)showProgress:(CGFloat)progress status:(NSString*)status; +``` + +### Dismissing the HUD + +The HUD can be dismissed using: + +```objective-c ++ (void)dismiss; ++ (void)dismissWithDelay:(NSTimeInterval)delay; +``` + +If you'd like to stack HUDs, you can balance out every show call using: + +``` ++ (void)popActivity; +``` + +The HUD will get dismissed once the popActivity calls will match the number of show calls. + +Or show a confirmation glyph before before getting dismissed a little bit later. The display time depends on `minimumDismissTimeInterval` and the length of the given string. + +```objective-c ++ (void)showInfoWithStatus:(NSString*)string; ++ (void)showSuccessWithStatus:(NSString*)string; ++ (void)showErrorWithStatus:(NSString*)string; ++ (void)showImage:(UIImage*)image status:(NSString*)string; +``` + +## Customization + +`SVProgressHUD` can be customized via the following methods: + +```objective-c ++ (void)setDefaultStyle:(SVProgressHUDStyle)style; // default is SVProgressHUDStyleLight ++ (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType; // default is SVProgressHUDMaskTypeNone ++ (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type; // default is SVProgressHUDAnimationTypeFlat ++ (void)setContainerView:(UIView*)containerView; // default is window level ++ (void)setMinimumSize:(CGSize)minimumSize; // default is CGSizeZero, can be used to avoid resizing ++ (void)setRingThickness:(CGFloat)width; // default is 2 pt ++ (void)setRingRadius:(CGFloat)radius; // default is 18 pt ++ (void)setRingNoTextRadius:(CGFloat)radius; // default is 24 pt ++ (void)setCornerRadius:(CGFloat)cornerRadius; // default is 14 pt ++ (void)setBorderColor:(nonnull UIColor*)color; // default is nil ++ (void)setBorderWidth:(CGFloat)width; // default is 0 ++ (void)setFont:(UIFont*)font; // default is [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline] ++ (void)setForegroundColor:(UIColor*)color; // default is [UIColor blackColor], only used for SVProgressHUDStyleCustom ++ (void)setBackgroundColor:(UIColor*)color; // default is [UIColor whiteColor], only used for SVProgressHUDStyleCustom ++ (void)setBackgroundLayerColor:(UIColor*)color; // default is [UIColor colorWithWhite:0 alpha:0.4], only used for SVProgressHUDMaskTypeCustom ++ (void)setImageViewSize:(CGSize)size; // default is 28x28 pt ++ (void)setShouldTintImages:(BOOL)shouldTintImages; // default is YES ++ (void)setInfoImage:(UIImage*)image; // default is the bundled info image provided by Freepik ++ (void)setSuccessImage:(UIImage*)image; // default is bundled success image from Freepik ++ (void)setErrorImage:(UIImage*)image; // default is bundled error image from Freepik ++ (void)setViewForExtension:(UIView*)view; // default is nil, only used if #define SV_APP_EXTENSIONS is set ++ (void)setGraceTimeInterval:(NSTimeInterval)interval; // default is 0 seconds ++ (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval; // default is 5.0 seconds ++ (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval; // default is CGFLOAT_MAX ++ (void)setFadeInAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds ++ (void)setFadeOutAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds ++ (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel; // default is UIWindowLevelNormal ++ (void)setHapticsEnabled:(BOOL)hapticsEnabled; // default is NO +``` + +Additionally `SVProgressHUD` supports the `UIAppearance` protocol for most of the above methods. + +### Hint + +As standard `SVProgressHUD` offers two preconfigured styles: + +* `SVProgressHUDStyleLight`: White background with black spinner and text +* `SVProgressHUDStyleDark`: Black background with white spinner and text + +If you want to use custom colors use `setForegroundColor` and `setBackgroundColor:`. These implicitly set the HUD's style to `SVProgressHUDStyleCustom`. + +## Haptic Feedback + +For users with newer devices (starting with the iPhone 7), `SVProgressHUD` can automatically trigger haptic feedback depending on which HUD is being displayed. The feedback maps as follows: + +* `showSuccessWithStatus:` <-> `UINotificationFeedbackTypeSuccess` +* `showInfoWithStatus:` <-> `UINotificationFeedbackTypeWarning` +* `showErrorWithStatus:` <-> `UINotificationFeedbackTypeError` + +To enable this functionality, use `setHapticsEnabled:`. + +Users with devices prior to iPhone 7 will have no change in functionality. + +## Notifications + +`SVProgressHUD` posts four notifications via `NSNotificationCenter` in response to being shown/dismissed: +* `SVProgressHUDWillAppearNotification` when the show animation starts +* `SVProgressHUDDidAppearNotification` when the show animation completes +* `SVProgressHUDWillDisappearNotification` when the dismiss animation starts +* `SVProgressHUDDidDisappearNotification` when the dismiss animation completes + +Each notification passes a `userInfo` dictionary holding the HUD's status string (if any), retrievable via `SVProgressHUDStatusUserInfoKey`. + +`SVProgressHUD` also posts `SVProgressHUDDidReceiveTouchEventNotification` when users touch on the overall screen or `SVProgressHUDDidTouchDownInsideNotification` when a user touches on the HUD directly. For this notifications `userInfo` is not passed but the object parameter contains the `UIEvent` that related to the touch. + +## App Extensions + +When using `SVProgressHUD` in an App Extension, `#define SV_APP_EXTENSIONS` to avoid using unavailable APIs. Additionally call `setViewForExtension:` from your extensions view controller with `self.view`. + +## Contributing to this project + +If you have feature requests or bug reports, feel free to help out by sending pull requests or by [creating new issues](https://github.com/SVProgressHUD/SVProgressHUD/issues/new). Please take a moment to +review the guidelines written by [Nicolas Gallagher](https://github.com/necolas): + +* [Bug reports](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md#bugs) +* [Feature requests](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md#features) +* [Pull requests](https://github.com/necolas/issue-guidelines/blob/master/CONTRIBUTING.md#pull-requests) + +## License + +`SVProgressHUD` is distributed under the terms and conditions of the [MIT license](https://github.com/SVProgressHUD/SVProgressHUD/blob/master/LICENSE.txt). The success, error and info icons are made by [Freepik](http://www.freepik.com) from [Flaticon](http://www.flaticon.com) and are licensed under [Creative Commons BY 3.0](http://creativecommons.org/licenses/by/3.0/). + +## Credits + +`SVProgressHUD` is brought to you by [Sam Vermette](http://samvermette.com), [Tobias Tiemerding](http://tiemerding.com) and [contributors to the project](https://github.com/SVProgressHUD/SVProgressHUD/contributors). If you're using `SVProgressHUD` in your project, attribution would be very appreciated. diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.h b/Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.h new file mode 100644 index 0000000..b624dd0 --- /dev/null +++ b/Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.h @@ -0,0 +1,17 @@ +// +// SVIndefiniteAnimatedView.h +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Copyright (c) 2014-2018 Guillaume Campagna. All rights reserved. +// + +#import <UIKit/UIKit.h> + +@interface SVIndefiniteAnimatedView : UIView + +@property (nonatomic, assign) CGFloat strokeThickness; +@property (nonatomic, assign) CGFloat radius; +@property (nonatomic, strong) UIColor *strokeColor; + +@end + diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.m b/Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.m new file mode 100644 index 0000000..09a38d0 --- /dev/null +++ b/Pods/SVProgressHUD/SVProgressHUD/SVIndefiniteAnimatedView.m @@ -0,0 +1,137 @@ +// +// SVIndefiniteAnimatedView.m +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Copyright (c) 2014-2018 Guillaume Campagna. All rights reserved. +// + +#import "SVIndefiniteAnimatedView.h" +#import "SVProgressHUD.h" + +@interface SVIndefiniteAnimatedView () + +@property (nonatomic, strong) CAShapeLayer *indefiniteAnimatedLayer; + +@end + +@implementation SVIndefiniteAnimatedView + +- (void)willMoveToSuperview:(UIView*)newSuperview { + if (newSuperview) { + [self layoutAnimatedLayer]; + } else { + [_indefiniteAnimatedLayer removeFromSuperlayer]; + _indefiniteAnimatedLayer = nil; + } +} + +- (void)layoutAnimatedLayer { + CALayer *layer = self.indefiniteAnimatedLayer; + [self.layer addSublayer:layer]; + + CGFloat widthDiff = CGRectGetWidth(self.bounds) - CGRectGetWidth(layer.bounds); + CGFloat heightDiff = CGRectGetHeight(self.bounds) - CGRectGetHeight(layer.bounds); + layer.position = CGPointMake(CGRectGetWidth(self.bounds) - CGRectGetWidth(layer.bounds) / 2 - widthDiff / 2, CGRectGetHeight(self.bounds) - CGRectGetHeight(layer.bounds) / 2 - heightDiff / 2); +} + +- (CAShapeLayer*)indefiniteAnimatedLayer { + if(!_indefiniteAnimatedLayer) { + CGPoint arcCenter = CGPointMake(self.radius+self.strokeThickness/2+5, self.radius+self.strokeThickness/2+5); + UIBezierPath* smoothedPath = [UIBezierPath bezierPathWithArcCenter:arcCenter radius:self.radius startAngle:(CGFloat) (M_PI*3/2) endAngle:(CGFloat) (M_PI/2+M_PI*5) clockwise:YES]; + + _indefiniteAnimatedLayer = [CAShapeLayer layer]; + _indefiniteAnimatedLayer.contentsScale = [[UIScreen mainScreen] scale]; + _indefiniteAnimatedLayer.frame = CGRectMake(0.0f, 0.0f, arcCenter.x*2, arcCenter.y*2); + _indefiniteAnimatedLayer.fillColor = [UIColor clearColor].CGColor; + _indefiniteAnimatedLayer.strokeColor = self.strokeColor.CGColor; + _indefiniteAnimatedLayer.lineWidth = self.strokeThickness; + _indefiniteAnimatedLayer.lineCap = kCALineCapRound; + _indefiniteAnimatedLayer.lineJoin = kCALineJoinBevel; + _indefiniteAnimatedLayer.path = smoothedPath.CGPath; + + CALayer *maskLayer = [CALayer layer]; + + NSBundle *bundle = [NSBundle bundleForClass:[SVProgressHUD class]]; + NSURL *url = [bundle URLForResource:@"SVProgressHUD" withExtension:@"bundle"]; + NSBundle *imageBundle = [NSBundle bundleWithURL:url]; + + NSString *path = [imageBundle pathForResource:@"angle-mask" ofType:@"png"]; + + maskLayer.contents = (__bridge id)[[UIImage imageWithContentsOfFile:path] CGImage]; + maskLayer.frame = _indefiniteAnimatedLayer.bounds; + _indefiniteAnimatedLayer.mask = maskLayer; + + NSTimeInterval animationDuration = 1; + CAMediaTimingFunction *linearCurve = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; + + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"]; + animation.fromValue = (id) 0; + animation.toValue = @(M_PI*2); + animation.duration = animationDuration; + animation.timingFunction = linearCurve; + animation.removedOnCompletion = NO; + animation.repeatCount = INFINITY; + animation.fillMode = kCAFillModeForwards; + animation.autoreverses = NO; + [_indefiniteAnimatedLayer.mask addAnimation:animation forKey:@"rotate"]; + + CAAnimationGroup *animationGroup = [CAAnimationGroup animation]; + animationGroup.duration = animationDuration; + animationGroup.repeatCount = INFINITY; + animationGroup.removedOnCompletion = NO; + animationGroup.timingFunction = linearCurve; + + CABasicAnimation *strokeStartAnimation = [CABasicAnimation animationWithKeyPath:@"strokeStart"]; + strokeStartAnimation.fromValue = @0.015; + strokeStartAnimation.toValue = @0.515; + + CABasicAnimation *strokeEndAnimation = [CABasicAnimation animationWithKeyPath:@"strokeEnd"]; + strokeEndAnimation.fromValue = @0.485; + strokeEndAnimation.toValue = @0.985; + + animationGroup.animations = @[strokeStartAnimation, strokeEndAnimation]; + [_indefiniteAnimatedLayer addAnimation:animationGroup forKey:@"progress"]; + + } + return _indefiniteAnimatedLayer; +} + +- (void)setFrame:(CGRect)frame { + if(!CGRectEqualToRect(frame, super.frame)) { + [super setFrame:frame]; + + if(self.superview) { + [self layoutAnimatedLayer]; + } + } + +} + +- (void)setRadius:(CGFloat)radius { + if(radius != _radius) { + _radius = radius; + + [_indefiniteAnimatedLayer removeFromSuperlayer]; + _indefiniteAnimatedLayer = nil; + + if(self.superview) { + [self layoutAnimatedLayer]; + } + } +} + +- (void)setStrokeColor:(UIColor*)strokeColor { + _strokeColor = strokeColor; + _indefiniteAnimatedLayer.strokeColor = strokeColor.CGColor; +} + +- (void)setStrokeThickness:(CGFloat)strokeThickness { + _strokeThickness = strokeThickness; + _indefiniteAnimatedLayer.lineWidth = _strokeThickness; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return CGSizeMake((self.radius+self.strokeThickness/2+5)*2, (self.radius+self.strokeThickness/2+5)*2); +} + +@end diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.h b/Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.h new file mode 100644 index 0000000..6de23b4 --- /dev/null +++ b/Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.h @@ -0,0 +1,17 @@ +// +// SVProgressAnimatedView.h +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Copyright (c) 2017-2018 Tobias Tiemerding. All rights reserved. +// + +#import <UIKit/UIKit.h> + +@interface SVProgressAnimatedView : UIView + +@property (nonatomic, assign) CGFloat radius; +@property (nonatomic, assign) CGFloat strokeThickness; +@property (nonatomic, strong) UIColor *strokeColor; +@property (nonatomic, assign) CGFloat strokeEnd; + +@end diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.m b/Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.m new file mode 100644 index 0000000..a347c85 --- /dev/null +++ b/Pods/SVProgressHUD/SVProgressHUD/SVProgressAnimatedView.m @@ -0,0 +1,96 @@ +// +// SVProgressAnimatedView.m +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Copyright (c) 2017-2018 Tobias Tiemerding. All rights reserved. +// + +#import "SVProgressAnimatedView.h" + +@interface SVProgressAnimatedView () + +@property (nonatomic, strong) CAShapeLayer *ringAnimatedLayer; + +@end + +@implementation SVProgressAnimatedView + +- (void)willMoveToSuperview:(UIView*)newSuperview { + if (newSuperview) { + [self layoutAnimatedLayer]; + } else { + [_ringAnimatedLayer removeFromSuperlayer]; + _ringAnimatedLayer = nil; + } +} + +- (void)layoutAnimatedLayer { + CALayer *layer = self.ringAnimatedLayer; + [self.layer addSublayer:layer]; + + CGFloat widthDiff = CGRectGetWidth(self.bounds) - CGRectGetWidth(layer.bounds); + CGFloat heightDiff = CGRectGetHeight(self.bounds) - CGRectGetHeight(layer.bounds); + layer.position = CGPointMake(CGRectGetWidth(self.bounds) - CGRectGetWidth(layer.bounds) / 2 - widthDiff / 2, CGRectGetHeight(self.bounds) - CGRectGetHeight(layer.bounds) / 2 - heightDiff / 2); +} + +- (CAShapeLayer*)ringAnimatedLayer { + if(!_ringAnimatedLayer) { + CGPoint arcCenter = CGPointMake(self.radius+self.strokeThickness/2+5, self.radius+self.strokeThickness/2+5); + UIBezierPath* smoothedPath = [UIBezierPath bezierPathWithArcCenter:arcCenter radius:self.radius startAngle:(CGFloat)-M_PI_2 endAngle:(CGFloat) (M_PI + M_PI_2) clockwise:YES]; + + _ringAnimatedLayer = [CAShapeLayer layer]; + _ringAnimatedLayer.contentsScale = [[UIScreen mainScreen] scale]; + _ringAnimatedLayer.frame = CGRectMake(0.0f, 0.0f, arcCenter.x*2, arcCenter.y*2); + _ringAnimatedLayer.fillColor = [UIColor clearColor].CGColor; + _ringAnimatedLayer.strokeColor = self.strokeColor.CGColor; + _ringAnimatedLayer.lineWidth = self.strokeThickness; + _ringAnimatedLayer.lineCap = kCALineCapRound; + _ringAnimatedLayer.lineJoin = kCALineJoinBevel; + _ringAnimatedLayer.path = smoothedPath.CGPath; + } + return _ringAnimatedLayer; +} + +- (void)setFrame:(CGRect)frame { + if(!CGRectEqualToRect(frame, super.frame)) { + [super setFrame:frame]; + + if(self.superview) { + [self layoutAnimatedLayer]; + } + } +} + +- (void)setRadius:(CGFloat)radius { + if(radius != _radius) { + _radius = radius; + + [_ringAnimatedLayer removeFromSuperlayer]; + _ringAnimatedLayer = nil; + + if(self.superview) { + [self layoutAnimatedLayer]; + } + } +} + +- (void)setStrokeColor:(UIColor*)strokeColor { + _strokeColor = strokeColor; + _ringAnimatedLayer.strokeColor = strokeColor.CGColor; +} + +- (void)setStrokeThickness:(CGFloat)strokeThickness { + _strokeThickness = strokeThickness; + _ringAnimatedLayer.lineWidth = _strokeThickness; +} + +- (void)setStrokeEnd:(CGFloat)strokeEnd { + _strokeEnd = strokeEnd; + _ringAnimatedLayer.strokeEnd = _strokeEnd; +} + +- (CGSize)sizeThatFits:(CGSize)size { + return CGSizeMake((self.radius+self.strokeThickness/2+5)*2, (self.radius+self.strokeThickness/2+5)*2); +} + +@end diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask.png new file mode 100644 index 0000000..0150a03 Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@2x.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@2x.png new file mode 100644 index 0000000..9a302b6 Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@2x.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@3x.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@3x.png new file mode 100644 index 0000000..d07f3ce Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/angle-mask@3x.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error.png new file mode 100644 index 0000000..a57c8e4 Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@2x.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@2x.png new file mode 100644 index 0000000..aaf6798 Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@2x.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@3x.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@3x.png new file mode 100644 index 0000000..c92518f Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/error@3x.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info.png new file mode 100644 index 0000000..a3a1f75 Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@2x.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@2x.png new file mode 100644 index 0000000..1b333e7 Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@2x.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@3x.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@3x.png new file mode 100644 index 0000000..d56aa0c Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/info@3x.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success.png new file mode 100644 index 0000000..44769d0 Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@2x.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@2x.png new file mode 100644 index 0000000..a9d1653 Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@2x.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@3x.png b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@3x.png new file mode 100644 index 0000000..42bad9b Binary files /dev/null and b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.bundle/success@3x.png differ diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.h b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.h new file mode 100644 index 0000000..6aa935c --- /dev/null +++ b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.h @@ -0,0 +1,147 @@ +// +// SVProgressHUD.h +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Copyright (c) 2011-2018 Sam Vermette and contributors. All rights reserved. +// + +#import <UIKit/UIKit.h> +#import <AvailabilityMacros.h> + +extern NSString * _Nonnull const SVProgressHUDDidReceiveTouchEventNotification; +extern NSString * _Nonnull const SVProgressHUDDidTouchDownInsideNotification; +extern NSString * _Nonnull const SVProgressHUDWillDisappearNotification; +extern NSString * _Nonnull const SVProgressHUDDidDisappearNotification; +extern NSString * _Nonnull const SVProgressHUDWillAppearNotification; +extern NSString * _Nonnull const SVProgressHUDDidAppearNotification; + +extern NSString * _Nonnull const SVProgressHUDStatusUserInfoKey; + +typedef NS_ENUM(NSInteger, SVProgressHUDStyle) { + SVProgressHUDStyleLight, // default style, white HUD with black text, HUD background will be blurred + SVProgressHUDStyleDark, // black HUD and white text, HUD background will be blurred + SVProgressHUDStyleCustom // uses the fore- and background color properties +}; + +typedef NS_ENUM(NSUInteger, SVProgressHUDMaskType) { + SVProgressHUDMaskTypeNone = 1, // default mask type, allow user interactions while HUD is displayed + SVProgressHUDMaskTypeClear, // don't allow user interactions with background objects + SVProgressHUDMaskTypeBlack, // don't allow user interactions with background objects and dim the UI in the back of the HUD (as seen in iOS 7 and above) + SVProgressHUDMaskTypeGradient, // don't allow user interactions with background objects and dim the UI with a a-la UIAlertView background gradient (as seen in iOS 6) + SVProgressHUDMaskTypeCustom // don't allow user interactions with background objects and dim the UI in the back of the HUD with a custom color +}; + +typedef NS_ENUM(NSUInteger, SVProgressHUDAnimationType) { + SVProgressHUDAnimationTypeFlat, // default animation type, custom flat animation (indefinite animated ring) + SVProgressHUDAnimationTypeNative // iOS native UIActivityIndicatorView +}; + +typedef void (^SVProgressHUDShowCompletion)(void); +typedef void (^SVProgressHUDDismissCompletion)(void); + +@interface SVProgressHUD : UIView + +#pragma mark - Customization + +@property (assign, nonatomic) SVProgressHUDStyle defaultStyle UI_APPEARANCE_SELECTOR; // default is SVProgressHUDStyleLight +@property (assign, nonatomic) SVProgressHUDMaskType defaultMaskType UI_APPEARANCE_SELECTOR; // default is SVProgressHUDMaskTypeNone +@property (assign, nonatomic) SVProgressHUDAnimationType defaultAnimationType UI_APPEARANCE_SELECTOR; // default is SVProgressHUDAnimationTypeFlat +@property (strong, nonatomic, nullable) UIView *containerView; // if nil then use default window level +@property (assign, nonatomic) CGSize minimumSize UI_APPEARANCE_SELECTOR; // default is CGSizeZero, can be used to avoid resizing for a larger message +@property (assign, nonatomic) CGFloat ringThickness UI_APPEARANCE_SELECTOR; // default is 2 pt +@property (assign, nonatomic) CGFloat ringRadius UI_APPEARANCE_SELECTOR; // default is 18 pt +@property (assign, nonatomic) CGFloat ringNoTextRadius UI_APPEARANCE_SELECTOR; // default is 24 pt +@property (assign, nonatomic) CGFloat cornerRadius UI_APPEARANCE_SELECTOR; // default is 14 pt +@property (strong, nonatomic, nonnull) UIFont *font UI_APPEARANCE_SELECTOR; // default is [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline] +@property (strong, nonatomic, nonnull) UIColor *backgroundColor UI_APPEARANCE_SELECTOR; // default is [UIColor whiteColor] +@property (strong, nonatomic, nonnull) UIColor *foregroundColor UI_APPEARANCE_SELECTOR; // default is [UIColor blackColor] +@property (strong, nonatomic, nonnull) UIColor *backgroundLayerColor UI_APPEARANCE_SELECTOR;// default is [UIColor colorWithWhite:0 alpha:0.4] +@property (assign, nonatomic) CGSize imageViewSize UI_APPEARANCE_SELECTOR; // default is 28x28 pt +@property (assign, nonatomic) BOOL shouldTintImages UI_APPEARANCE_SELECTOR; // default is YES +@property (strong, nonatomic, nonnull) UIImage *infoImage UI_APPEARANCE_SELECTOR; // default is the bundled info image provided by Freepik +@property (strong, nonatomic, nonnull) UIImage *successImage UI_APPEARANCE_SELECTOR; // default is the bundled success image provided by Freepik +@property (strong, nonatomic, nonnull) UIImage *errorImage UI_APPEARANCE_SELECTOR; // default is the bundled error image provided by Freepik +@property (strong, nonatomic, nonnull) UIView *viewForExtension UI_APPEARANCE_SELECTOR; // default is nil, only used if #define SV_APP_EXTENSIONS is set +@property (assign, nonatomic) NSTimeInterval graceTimeInterval; // default is 0 seconds +@property (assign, nonatomic) NSTimeInterval minimumDismissTimeInterval; // default is 5.0 seconds +@property (assign, nonatomic) NSTimeInterval maximumDismissTimeInterval; // default is CGFLOAT_MAX + +@property (assign, nonatomic) UIOffset offsetFromCenter UI_APPEARANCE_SELECTOR; // default is 0, 0 + +@property (assign, nonatomic) NSTimeInterval fadeInAnimationDuration UI_APPEARANCE_SELECTOR; // default is 0.15 +@property (assign, nonatomic) NSTimeInterval fadeOutAnimationDuration UI_APPEARANCE_SELECTOR; // default is 0.15 + +@property (assign, nonatomic) UIWindowLevel maxSupportedWindowLevel; // default is UIWindowLevelNormal + +@property (assign, nonatomic) BOOL hapticsEnabled; // default is NO + ++ (void)setDefaultStyle:(SVProgressHUDStyle)style; // default is SVProgressHUDStyleLight ++ (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType; // default is SVProgressHUDMaskTypeNone ++ (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type; // default is SVProgressHUDAnimationTypeFlat ++ (void)setContainerView:(nullable UIView*)containerView; // default is window level ++ (void)setMinimumSize:(CGSize)minimumSize; // default is CGSizeZero, can be used to avoid resizing for a larger message ++ (void)setRingThickness:(CGFloat)ringThickness; // default is 2 pt ++ (void)setRingRadius:(CGFloat)radius; // default is 18 pt ++ (void)setRingNoTextRadius:(CGFloat)radius; // default is 24 pt ++ (void)setCornerRadius:(CGFloat)cornerRadius; // default is 14 pt ++ (void)setBorderColor:(nonnull UIColor*)color; // default is nil ++ (void)setBorderWidth:(CGFloat)width; // default is 0 ++ (void)setFont:(nonnull UIFont*)font; // default is [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline] ++ (void)setForegroundColor:(nonnull UIColor*)color; // default is [UIColor blackColor], only used for SVProgressHUDStyleCustom ++ (void)setBackgroundColor:(nonnull UIColor*)color; // default is [UIColor whiteColor], only used for SVProgressHUDStyleCustom ++ (void)setBackgroundLayerColor:(nonnull UIColor*)color; // default is [UIColor colorWithWhite:0 alpha:0.5], only used for SVProgressHUDMaskTypeCustom ++ (void)setImageViewSize:(CGSize)size; // default is 28x28 pt ++ (void)setShouldTintImages:(BOOL)shouldTintImages; // default is YES ++ (void)setInfoImage:(nonnull UIImage*)image; // default is the bundled info image provided by Freepik ++ (void)setSuccessImage:(nonnull UIImage*)image; // default is the bundled success image provided by Freepik ++ (void)setErrorImage:(nonnull UIImage*)image; // default is the bundled error image provided by Freepik ++ (void)setViewForExtension:(nonnull UIView*)view; // default is nil, only used if #define SV_APP_EXTENSIONS is set ++ (void)setGraceTimeInterval:(NSTimeInterval)interval; // default is 0 seconds ++ (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval; // default is 5.0 seconds ++ (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval; // default is infinite ++ (void)setFadeInAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds ++ (void)setFadeOutAnimationDuration:(NSTimeInterval)duration; // default is 0.15 seconds ++ (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel; // default is UIWindowLevelNormal ++ (void)setHapticsEnabled:(BOOL)hapticsEnabled; // default is NO + +#pragma mark - Show Methods + ++ (void)show; ++ (void)showWithMaskType:(SVProgressHUDMaskType)maskType __attribute__((deprecated("Use show and setDefaultMaskType: instead."))); ++ (void)showWithStatus:(nullable NSString*)status; ++ (void)showWithStatus:(nullable NSString*)status maskType:(SVProgressHUDMaskType)maskType __attribute__((deprecated("Use showWithStatus: and setDefaultMaskType: instead."))); + ++ (void)showProgress:(float)progress; ++ (void)showProgress:(float)progress maskType:(SVProgressHUDMaskType)maskType __attribute__((deprecated("Use showProgress: and setDefaultMaskType: instead."))); ++ (void)showProgress:(float)progress status:(nullable NSString*)status; ++ (void)showProgress:(float)progress status:(nullable NSString*)status maskType:(SVProgressHUDMaskType)maskType __attribute__((deprecated("Use showProgress:status: and setDefaultMaskType: instead."))); + ++ (void)setStatus:(nullable NSString*)status; // change the HUD loading status while it's showing + +// stops the activity indicator, shows a glyph + status, and dismisses the HUD a little bit later ++ (void)showInfoWithStatus:(nullable NSString*)status; ++ (void)showInfoWithStatus:(nullable NSString*)status maskType:(SVProgressHUDMaskType)maskType __attribute__((deprecated("Use showInfoWithStatus: and setDefaultMaskType: instead."))); ++ (void)showSuccessWithStatus:(nullable NSString*)status; ++ (void)showSuccessWithStatus:(nullable NSString*)status maskType:(SVProgressHUDMaskType)maskType __attribute__((deprecated("Use showSuccessWithStatus: and setDefaultMaskType: instead."))); ++ (void)showErrorWithStatus:(nullable NSString*)status; ++ (void)showErrorWithStatus:(nullable NSString*)status maskType:(SVProgressHUDMaskType)maskType __attribute__((deprecated("Use showErrorWithStatus: and setDefaultMaskType: instead."))); + +// shows a image + status, use white PNGs with the imageViewSize (default is 28x28 pt) ++ (void)showImage:(nonnull UIImage*)image status:(nullable NSString*)status; ++ (void)showImage:(nonnull UIImage*)image status:(nullable NSString*)status maskType:(SVProgressHUDMaskType)maskType __attribute__((deprecated("Use showImage:status: and setDefaultMaskType: instead."))); + ++ (void)setOffsetFromCenter:(UIOffset)offset; ++ (void)resetOffsetFromCenter; + ++ (void)popActivity; // decrease activity count, if activity count == 0 the HUD is dismissed ++ (void)dismiss; ++ (void)dismissWithCompletion:(nullable SVProgressHUDDismissCompletion)completion; ++ (void)dismissWithDelay:(NSTimeInterval)delay; ++ (void)dismissWithDelay:(NSTimeInterval)delay completion:(nullable SVProgressHUDDismissCompletion)completion; + ++ (BOOL)isVisible; + ++ (NSTimeInterval)displayDurationForString:(nullable NSString*)string; + +@end + diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.m b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.m new file mode 100644 index 0000000..2b66992 --- /dev/null +++ b/Pods/SVProgressHUD/SVProgressHUD/SVProgressHUD.m @@ -0,0 +1,1509 @@ +// +// SVProgressHUD.h +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Copyright (c) 2011-2018 Sam Vermette and contributors. All rights reserved. +// + +#if !__has_feature(objc_arc) +#error SVProgressHUD is ARC only. Either turn on ARC for the project or use -fobjc-arc flag +#endif + +#import "SVProgressHUD.h" +#import "SVIndefiniteAnimatedView.h" +#import "SVProgressAnimatedView.h" +#import "SVRadialGradientLayer.h" + +NSString * const SVProgressHUDDidReceiveTouchEventNotification = @"SVProgressHUDDidReceiveTouchEventNotification"; +NSString * const SVProgressHUDDidTouchDownInsideNotification = @"SVProgressHUDDidTouchDownInsideNotification"; +NSString * const SVProgressHUDWillDisappearNotification = @"SVProgressHUDWillDisappearNotification"; +NSString * const SVProgressHUDDidDisappearNotification = @"SVProgressHUDDidDisappearNotification"; +NSString * const SVProgressHUDWillAppearNotification = @"SVProgressHUDWillAppearNotification"; +NSString * const SVProgressHUDDidAppearNotification = @"SVProgressHUDDidAppearNotification"; + +NSString * const SVProgressHUDStatusUserInfoKey = @"SVProgressHUDStatusUserInfoKey"; + +static const CGFloat SVProgressHUDParallaxDepthPoints = 10.0f; +static const CGFloat SVProgressHUDUndefinedProgress = -1; +static const CGFloat SVProgressHUDDefaultAnimationDuration = 0.15f; +static const CGFloat SVProgressHUDVerticalSpacing = 12.0f; +static const CGFloat SVProgressHUDHorizontalSpacing = 12.0f; +static const CGFloat SVProgressHUDLabelSpacing = 8.0f; + + +@interface SVProgressHUD () + +@property (nonatomic, strong) NSTimer *graceTimer; +@property (nonatomic, strong) NSTimer *fadeOutTimer; + +@property (nonatomic, strong) UIControl *controlView; +@property (nonatomic, strong) UIView *backgroundView; +@property (nonatomic, strong) SVRadialGradientLayer *backgroundRadialGradientLayer; +@property (nonatomic, strong) UIVisualEffectView *hudView; +@property (nonatomic, strong) UILabel *statusLabel; +@property (nonatomic, strong) UIImageView *imageView; + +@property (nonatomic, strong) UIView *indefiniteAnimatedView; +@property (nonatomic, strong) SVProgressAnimatedView *ringView; +@property (nonatomic, strong) SVProgressAnimatedView *backgroundRingView; + +@property (nonatomic, readwrite) CGFloat progress; +@property (nonatomic, readwrite) NSUInteger activityCount; + +@property (nonatomic, readonly) CGFloat visibleKeyboardHeight; +@property (nonatomic, readonly) UIWindow *frontWindow; + +#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 +@property (nonatomic, strong) UINotificationFeedbackGenerator *hapticGenerator NS_AVAILABLE_IOS(10_0); +#endif + +@end + +@implementation SVProgressHUD { + BOOL _isInitializing; +} + ++ (SVProgressHUD*)sharedView { + static dispatch_once_t once; + + static SVProgressHUD *sharedView; +#if !defined(SV_APP_EXTENSIONS) + dispatch_once(&once, ^{ sharedView = [[self alloc] initWithFrame:[[[UIApplication sharedApplication] delegate] window].bounds]; }); +#else + dispatch_once(&once, ^{ sharedView = [[self alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; }); +#endif + return sharedView; +} + + +#pragma mark - Setters + ++ (void)setStatus:(NSString*)status { + [[self sharedView] setStatus:status]; +} + ++ (void)setDefaultStyle:(SVProgressHUDStyle)style { + [self sharedView].defaultStyle = style; +} + ++ (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType { + [self sharedView].defaultMaskType = maskType; +} + ++ (void)setDefaultAnimationType:(SVProgressHUDAnimationType)type { + [self sharedView].defaultAnimationType = type; +} + ++ (void)setContainerView:(nullable UIView*)containerView { + [self sharedView].containerView = containerView; +} + ++ (void)setMinimumSize:(CGSize)minimumSize { + [self sharedView].minimumSize = minimumSize; +} + ++ (void)setRingThickness:(CGFloat)ringThickness { + [self sharedView].ringThickness = ringThickness; +} + ++ (void)setRingRadius:(CGFloat)radius { + [self sharedView].ringRadius = radius; +} + ++ (void)setRingNoTextRadius:(CGFloat)radius { + [self sharedView].ringNoTextRadius = radius; +} + ++ (void)setCornerRadius:(CGFloat)cornerRadius { + [self sharedView].cornerRadius = cornerRadius; +} + ++ (void)setBorderColor:(nonnull UIColor*)color { + [self sharedView].hudView.layer.borderColor = color.CGColor; +} + ++ (void)setBorderWidth:(CGFloat)width { + [self sharedView].hudView.layer.borderWidth = width; +} + ++ (void)setFont:(UIFont*)font { + [self sharedView].font = font; +} + ++ (void)setForegroundColor:(UIColor*)color { + [self sharedView].foregroundColor = color; + [self setDefaultStyle:SVProgressHUDStyleCustom]; +} + ++ (void)setBackgroundColor:(UIColor*)color { + [self sharedView].backgroundColor = color; + [self setDefaultStyle:SVProgressHUDStyleCustom]; +} + ++ (void)setBackgroundLayerColor:(UIColor*)color { + [self sharedView].backgroundLayerColor = color; +} + ++ (void)setImageViewSize:(CGSize)size { + [self sharedView].imageViewSize = size; +} + ++ (void)setShouldTintImages:(BOOL)shouldTintImages { + [self sharedView].shouldTintImages = shouldTintImages; +} + ++ (void)setInfoImage:(UIImage*)image { + [self sharedView].infoImage = image; +} + ++ (void)setSuccessImage:(UIImage*)image { + [self sharedView].successImage = image; +} + ++ (void)setErrorImage:(UIImage*)image { + [self sharedView].errorImage = image; +} + ++ (void)setViewForExtension:(UIView*)view { + [self sharedView].viewForExtension = view; +} + ++ (void)setGraceTimeInterval:(NSTimeInterval)interval { + [self sharedView].graceTimeInterval = interval; +} + ++ (void)setMinimumDismissTimeInterval:(NSTimeInterval)interval { + [self sharedView].minimumDismissTimeInterval = interval; +} + ++ (void)setMaximumDismissTimeInterval:(NSTimeInterval)interval { + [self sharedView].maximumDismissTimeInterval = interval; +} + ++ (void)setFadeInAnimationDuration:(NSTimeInterval)duration { + [self sharedView].fadeInAnimationDuration = duration; +} + ++ (void)setFadeOutAnimationDuration:(NSTimeInterval)duration { + [self sharedView].fadeOutAnimationDuration = duration; +} + ++ (void)setMaxSupportedWindowLevel:(UIWindowLevel)windowLevel { + [self sharedView].maxSupportedWindowLevel = windowLevel; +} + ++ (void)setHapticsEnabled:(BOOL)hapticsEnabled { + [self sharedView].hapticsEnabled = hapticsEnabled; +} + +#pragma mark - Show Methods + ++ (void)show { + [self showWithStatus:nil]; +} + ++ (void)showWithMaskType:(SVProgressHUDMaskType)maskType { + SVProgressHUDMaskType existingMaskType = [self sharedView].defaultMaskType; + [self setDefaultMaskType:maskType]; + [self show]; + [self setDefaultMaskType:existingMaskType]; +} + ++ (void)showWithStatus:(NSString*)status { + [self showProgress:SVProgressHUDUndefinedProgress status:status]; +} + ++ (void)showWithStatus:(NSString*)status maskType:(SVProgressHUDMaskType)maskType { + SVProgressHUDMaskType existingMaskType = [self sharedView].defaultMaskType; + [self setDefaultMaskType:maskType]; + [self showWithStatus:status]; + [self setDefaultMaskType:existingMaskType]; +} + ++ (void)showProgress:(float)progress { + [self showProgress:progress status:nil]; +} + ++ (void)showProgress:(float)progress maskType:(SVProgressHUDMaskType)maskType { + SVProgressHUDMaskType existingMaskType = [self sharedView].defaultMaskType; + [self setDefaultMaskType:maskType]; + [self showProgress:progress]; + [self setDefaultMaskType:existingMaskType]; +} + ++ (void)showProgress:(float)progress status:(NSString*)status { + [[self sharedView] showProgress:progress status:status]; +} + ++ (void)showProgress:(float)progress status:(NSString*)status maskType:(SVProgressHUDMaskType)maskType { + SVProgressHUDMaskType existingMaskType = [self sharedView].defaultMaskType; + [self setDefaultMaskType:maskType]; + [self showProgress:progress status:status]; + [self setDefaultMaskType:existingMaskType]; +} + + +#pragma mark - Show, then automatically dismiss methods + ++ (void)showInfoWithStatus:(NSString*)status { + [self showImage:[self sharedView].infoImage status:status]; + +#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 + if (@available(iOS 10.0, *)) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[self sharedView].hapticGenerator notificationOccurred:UINotificationFeedbackTypeWarning]; + }); + } +#endif +} + ++ (void)showInfoWithStatus:(NSString*)status maskType:(SVProgressHUDMaskType)maskType { + SVProgressHUDMaskType existingMaskType = [self sharedView].defaultMaskType; + [self setDefaultMaskType:maskType]; + [self showInfoWithStatus:status]; + [self setDefaultMaskType:existingMaskType]; +} + ++ (void)showSuccessWithStatus:(NSString*)status { + [self showImage:[self sharedView].successImage status:status]; + +#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 + if (@available(iOS 10, *)) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[self sharedView].hapticGenerator notificationOccurred:UINotificationFeedbackTypeSuccess]; + }); + } +#endif +} + ++ (void)showSuccessWithStatus:(NSString*)status maskType:(SVProgressHUDMaskType)maskType { + SVProgressHUDMaskType existingMaskType = [self sharedView].defaultMaskType; + [self setDefaultMaskType:maskType]; + [self showSuccessWithStatus:status]; + [self setDefaultMaskType:existingMaskType]; + +#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 + if (@available(iOS 10.0, *)) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[self sharedView].hapticGenerator notificationOccurred:UINotificationFeedbackTypeSuccess]; + }); + } +#endif +} + ++ (void)showErrorWithStatus:(NSString*)status { + [self showImage:[self sharedView].errorImage status:status]; + +#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 + if (@available(iOS 10.0, *)) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[self sharedView].hapticGenerator notificationOccurred:UINotificationFeedbackTypeError]; + }); + } +#endif +} + ++ (void)showErrorWithStatus:(NSString*)status maskType:(SVProgressHUDMaskType)maskType { + SVProgressHUDMaskType existingMaskType = [self sharedView].defaultMaskType; + [self setDefaultMaskType:maskType]; + [self showErrorWithStatus:status]; + [self setDefaultMaskType:existingMaskType]; + +#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 + if (@available(iOS 10.0, *)) { + dispatch_async(dispatch_get_main_queue(), ^{ + [[self sharedView].hapticGenerator notificationOccurred:UINotificationFeedbackTypeError]; + }); + } +#endif +} + ++ (void)showImage:(UIImage*)image status:(NSString*)status { + NSTimeInterval displayInterval = [self displayDurationForString:status]; + [[self sharedView] showImage:image status:status duration:displayInterval]; +} + ++ (void)showImage:(UIImage*)image status:(NSString*)status maskType:(SVProgressHUDMaskType)maskType { + SVProgressHUDMaskType existingMaskType = [self sharedView].defaultMaskType; + [self setDefaultMaskType:maskType]; + [self showImage:image status:status]; + [self setDefaultMaskType:existingMaskType]; +} + + +#pragma mark - Dismiss Methods + ++ (void)popActivity { + if([self sharedView].activityCount > 0) { + [self sharedView].activityCount--; + } + if([self sharedView].activityCount == 0) { + [[self sharedView] dismiss]; + } +} + ++ (void)dismiss { + [self dismissWithDelay:0.0 completion:nil]; +} + ++ (void)dismissWithCompletion:(SVProgressHUDDismissCompletion)completion { + [self dismissWithDelay:0.0 completion:completion]; +} + ++ (void)dismissWithDelay:(NSTimeInterval)delay { + [self dismissWithDelay:delay completion:nil]; +} + ++ (void)dismissWithDelay:(NSTimeInterval)delay completion:(SVProgressHUDDismissCompletion)completion { + [[self sharedView] dismissWithDelay:delay completion:completion]; +} + + +#pragma mark - Offset + ++ (void)setOffsetFromCenter:(UIOffset)offset { + [self sharedView].offsetFromCenter = offset; +} + ++ (void)resetOffsetFromCenter { + [self setOffsetFromCenter:UIOffsetZero]; +} + + +#pragma mark - Instance Methods + +- (instancetype)initWithFrame:(CGRect)frame { + if((self = [super initWithFrame:frame])) { + _isInitializing = YES; + + self.userInteractionEnabled = NO; + self.activityCount = 0; + + self.backgroundView.alpha = 0.0f; + self.imageView.alpha = 0.0f; + self.statusLabel.alpha = 0.0f; + self.indefiniteAnimatedView.alpha = 0.0f; + self.ringView.alpha = self.backgroundRingView.alpha = 0.0f; + + + _backgroundColor = [UIColor whiteColor]; + _foregroundColor = [UIColor blackColor]; + _backgroundLayerColor = [UIColor colorWithWhite:0 alpha:0.4]; + + // Set default values + _defaultMaskType = SVProgressHUDMaskTypeNone; + _defaultStyle = SVProgressHUDStyleLight; + _defaultAnimationType = SVProgressHUDAnimationTypeFlat; + _minimumSize = CGSizeZero; + _font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline]; + + _imageViewSize = CGSizeMake(28.0f, 28.0f); + _shouldTintImages = YES; + + NSBundle *bundle = [NSBundle bundleForClass:[SVProgressHUD class]]; + NSURL *url = [bundle URLForResource:@"SVProgressHUD" withExtension:@"bundle"]; + NSBundle *imageBundle = [NSBundle bundleWithURL:url]; + + _infoImage = [UIImage imageWithContentsOfFile:[imageBundle pathForResource:@"info" ofType:@"png"]]; + _successImage = [UIImage imageWithContentsOfFile:[imageBundle pathForResource:@"success" ofType:@"png"]]; + _errorImage = [UIImage imageWithContentsOfFile:[imageBundle pathForResource:@"error" ofType:@"png"]]; + + _ringThickness = 2.0f; + _ringRadius = 18.0f; + _ringNoTextRadius = 24.0f; + + _cornerRadius = 14.0f; + + _graceTimeInterval = 0.0f; + _minimumDismissTimeInterval = 5.0; + _maximumDismissTimeInterval = CGFLOAT_MAX; + + _fadeInAnimationDuration = SVProgressHUDDefaultAnimationDuration; + _fadeOutAnimationDuration = SVProgressHUDDefaultAnimationDuration; + + _maxSupportedWindowLevel = UIWindowLevelNormal; + + _hapticsEnabled = NO; + + // Accessibility support + self.accessibilityIdentifier = @"SVProgressHUD"; + self.isAccessibilityElement = YES; + + _isInitializing = NO; + } + return self; +} + +- (void)updateHUDFrame { + // Check if an image or progress ring is displayed + BOOL imageUsed = (self.imageView.image) && !(self.imageView.hidden); + BOOL progressUsed = self.imageView.hidden; + + // Calculate size of string + CGRect labelRect = CGRectZero; + CGFloat labelHeight = 0.0f; + CGFloat labelWidth = 0.0f; + + if(self.statusLabel.text) { + CGSize constraintSize = CGSizeMake(200.0f, 300.0f); + labelRect = [self.statusLabel.text boundingRectWithSize:constraintSize + options:(NSStringDrawingOptions)(NSStringDrawingUsesFontLeading | NSStringDrawingTruncatesLastVisibleLine | NSStringDrawingUsesLineFragmentOrigin) + attributes:@{NSFontAttributeName: self.statusLabel.font} + context:NULL]; + labelHeight = ceilf(CGRectGetHeight(labelRect)); + labelWidth = ceilf(CGRectGetWidth(labelRect)); + } + + // Calculate hud size based on content + // For the beginning use default values, these + // might get update if string is too large etc. + CGFloat hudWidth; + CGFloat hudHeight; + + CGFloat contentWidth = 0.0f; + CGFloat contentHeight = 0.0f; + + if(imageUsed || progressUsed) { + contentWidth = CGRectGetWidth(imageUsed ? self.imageView.frame : self.indefiniteAnimatedView.frame); + contentHeight = CGRectGetHeight(imageUsed ? self.imageView.frame : self.indefiniteAnimatedView.frame); + } + + // |-spacing-content-spacing-| + hudWidth = SVProgressHUDHorizontalSpacing + MAX(labelWidth, contentWidth) + SVProgressHUDHorizontalSpacing; + + // |-spacing-content-(labelSpacing-label-)spacing-| + hudHeight = SVProgressHUDVerticalSpacing + labelHeight + contentHeight + SVProgressHUDVerticalSpacing; + if(self.statusLabel.text && (imageUsed || progressUsed)){ + // Add spacing if both content and label are used + hudHeight += SVProgressHUDLabelSpacing; + } + + // Update values on subviews + self.hudView.bounds = CGRectMake(0.0f, 0.0f, MAX(self.minimumSize.width, hudWidth), MAX(self.minimumSize.height, hudHeight)); + + // Animate value update + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + + // Spinner and image view + CGFloat centerY; + if(self.statusLabel.text) { + CGFloat yOffset = MAX(SVProgressHUDVerticalSpacing, (self.minimumSize.height - contentHeight - SVProgressHUDLabelSpacing - labelHeight) / 2.0f); + centerY = yOffset + contentHeight / 2.0f; + } else { + centerY = CGRectGetMidY(self.hudView.bounds); + } + self.indefiniteAnimatedView.center = CGPointMake(CGRectGetMidX(self.hudView.bounds), centerY); + if(self.progress != SVProgressHUDUndefinedProgress) { + self.backgroundRingView.center = self.ringView.center = CGPointMake(CGRectGetMidX(self.hudView.bounds), centerY); + } + self.imageView.center = CGPointMake(CGRectGetMidX(self.hudView.bounds), centerY); + + // Label + if(imageUsed || progressUsed) { + centerY = CGRectGetMaxY(imageUsed ? self.imageView.frame : self.indefiniteAnimatedView.frame) + SVProgressHUDLabelSpacing + labelHeight / 2.0f; + } else { + centerY = CGRectGetMidY(self.hudView.bounds); + } + self.statusLabel.frame = labelRect; + self.statusLabel.center = CGPointMake(CGRectGetMidX(self.hudView.bounds), centerY); + + [CATransaction commit]; +} + +#if TARGET_OS_IOS +- (void)updateMotionEffectForOrientation:(UIInterfaceOrientation)orientation { + UIInterpolatingMotionEffectType xMotionEffectType = UIInterfaceOrientationIsPortrait(orientation) ? UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis : UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis; + UIInterpolatingMotionEffectType yMotionEffectType = UIInterfaceOrientationIsPortrait(orientation) ? UIInterpolatingMotionEffectTypeTiltAlongVerticalAxis : UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis; + [self updateMotionEffectForXMotionEffectType:xMotionEffectType yMotionEffectType:yMotionEffectType]; +} +#endif + +- (void)updateMotionEffectForXMotionEffectType:(UIInterpolatingMotionEffectType)xMotionEffectType yMotionEffectType:(UIInterpolatingMotionEffectType)yMotionEffectType { + UIInterpolatingMotionEffect *effectX = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.x" type:xMotionEffectType]; + effectX.minimumRelativeValue = @(-SVProgressHUDParallaxDepthPoints); + effectX.maximumRelativeValue = @(SVProgressHUDParallaxDepthPoints); + + UIInterpolatingMotionEffect *effectY = [[UIInterpolatingMotionEffect alloc] initWithKeyPath:@"center.y" type:yMotionEffectType]; + effectY.minimumRelativeValue = @(-SVProgressHUDParallaxDepthPoints); + effectY.maximumRelativeValue = @(SVProgressHUDParallaxDepthPoints); + + UIMotionEffectGroup *effectGroup = [UIMotionEffectGroup new]; + effectGroup.motionEffects = @[effectX, effectY]; + + // Clear old motion effect, then add new motion effects + self.hudView.motionEffects = @[]; + [self.hudView addMotionEffect:effectGroup]; +} + +- (void)updateViewHierarchy { + // Add the overlay to the application window if necessary + if(!self.controlView.superview) { + if(self.containerView){ + [self.containerView addSubview:self.controlView]; + } else { +#if !defined(SV_APP_EXTENSIONS) + [self.frontWindow addSubview:self.controlView]; +#else + // If SVProgressHUD is used inside an app extension add it to the given view + if(self.viewForExtension) { + [self.viewForExtension addSubview:self.controlView]; + } +#endif + } + } else { + // The HUD is already on screen, but maybe not in front. Therefore + // ensure that overlay will be on top of rootViewController (which may + // be changed during runtime). + [self.controlView.superview bringSubviewToFront:self.controlView]; + } + + // Add self to the overlay view + if(!self.superview) { + [self.controlView addSubview:self]; + } +} + +- (void)setStatus:(NSString*)status { + self.statusLabel.text = status; + self.statusLabel.hidden = status.length == 0; + [self updateHUDFrame]; +} + +- (void)setGraceTimer:(NSTimer*)timer { + if(_graceTimer) { + [_graceTimer invalidate]; + _graceTimer = nil; + } + if(timer) { + _graceTimer = timer; + } +} + +- (void)setFadeOutTimer:(NSTimer*)timer { + if(_fadeOutTimer) { + [_fadeOutTimer invalidate]; + _fadeOutTimer = nil; + } + if(timer) { + _fadeOutTimer = timer; + } +} + + +#pragma mark - Notifications and their handling + +- (void)registerNotifications { +#if TARGET_OS_IOS + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIApplicationDidChangeStatusBarOrientationNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIKeyboardWillHideNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIKeyboardDidHideNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIKeyboardWillShowNotification + object:nil]; + + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIKeyboardDidShowNotification + object:nil]; +#endif + [[NSNotificationCenter defaultCenter] addObserver:self + selector:@selector(positionHUD:) + name:UIApplicationDidBecomeActiveNotification + object:nil]; +} + +- (NSDictionary*)notificationUserInfo { + return (self.statusLabel.text ? @{SVProgressHUDStatusUserInfoKey : self.statusLabel.text} : nil); +} + +- (void)positionHUD:(NSNotification*)notification { + CGFloat keyboardHeight = 0.0f; + double animationDuration = 0.0; + +#if !defined(SV_APP_EXTENSIONS) && TARGET_OS_IOS + self.frame = [[[UIApplication sharedApplication] delegate] window].bounds; + UIInterfaceOrientation orientation = UIApplication.sharedApplication.statusBarOrientation; +#elif !defined(SV_APP_EXTENSIONS) && !TARGET_OS_IOS + self.frame= [UIApplication sharedApplication].keyWindow.bounds; +#else + if (self.viewForExtension) { + self.frame = self.viewForExtension.frame; + } else { + self.frame = UIScreen.mainScreen.bounds; + } +#if TARGET_OS_IOS + UIInterfaceOrientation orientation = CGRectGetWidth(self.frame) > CGRectGetHeight(self.frame) ? UIInterfaceOrientationLandscapeLeft : UIInterfaceOrientationPortrait; +#endif +#endif + +#if TARGET_OS_IOS + // Get keyboardHeight in regard to current state + if(notification) { + NSDictionary* keyboardInfo = [notification userInfo]; + CGRect keyboardFrame = [keyboardInfo[UIKeyboardFrameBeginUserInfoKey] CGRectValue]; + animationDuration = [keyboardInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue]; + + if(notification.name == UIKeyboardWillShowNotification || notification.name == UIKeyboardDidShowNotification) { + keyboardHeight = CGRectGetWidth(keyboardFrame); + + if(UIInterfaceOrientationIsPortrait(orientation)) { + keyboardHeight = CGRectGetHeight(keyboardFrame); + } + } + } else { + keyboardHeight = self.visibleKeyboardHeight; + } +#endif + + // Get the currently active frame of the display (depends on orientation) + CGRect orientationFrame = self.bounds; + +#if !defined(SV_APP_EXTENSIONS) && TARGET_OS_IOS + CGRect statusBarFrame = UIApplication.sharedApplication.statusBarFrame; +#else + CGRect statusBarFrame = CGRectZero; +#endif + +#if TARGET_OS_IOS + // Update the motion effects in regard to orientation + [self updateMotionEffectForOrientation:orientation]; +#else + [self updateMotionEffectForXMotionEffectType:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis yMotionEffectType:UIInterpolatingMotionEffectTypeTiltAlongHorizontalAxis]; +#endif + + // Calculate available height for display + CGFloat activeHeight = CGRectGetHeight(orientationFrame); + if(keyboardHeight > 0) { + activeHeight += CGRectGetHeight(statusBarFrame) * 2; + } + activeHeight -= keyboardHeight; + + CGFloat posX = CGRectGetMidX(orientationFrame); + CGFloat posY = floorf(activeHeight*0.45f); + + CGFloat rotateAngle = 0.0; + CGPoint newCenter = CGPointMake(posX, posY); + + if(notification) { + // Animate update if notification was present + [UIView animateWithDuration:animationDuration + delay:0 + options:(UIViewAnimationOptions) (UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState) + animations:^{ + [self moveToPoint:newCenter rotateAngle:rotateAngle]; + [self.hudView setNeedsDisplay]; + } completion:nil]; + } else { + [self moveToPoint:newCenter rotateAngle:rotateAngle]; + } +} + +- (void)moveToPoint:(CGPoint)newCenter rotateAngle:(CGFloat)angle { + self.hudView.transform = CGAffineTransformMakeRotation(angle); + if (self.containerView) { + self.hudView.center = CGPointMake(self.containerView.center.x + self.offsetFromCenter.horizontal, self.containerView.center.y + self.offsetFromCenter.vertical); + } else { + self.hudView.center = CGPointMake(newCenter.x + self.offsetFromCenter.horizontal, newCenter.y + self.offsetFromCenter.vertical); + } +} + + +#pragma mark - Event handling + +- (void)controlViewDidReceiveTouchEvent:(id)sender forEvent:(UIEvent*)event { + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidReceiveTouchEventNotification + object:self + userInfo:[self notificationUserInfo]]; + + UITouch *touch = event.allTouches.anyObject; + CGPoint touchLocation = [touch locationInView:self]; + + if(CGRectContainsPoint(self.hudView.frame, touchLocation)) { + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidTouchDownInsideNotification + object:self + userInfo:[self notificationUserInfo]]; + } +} + + +#pragma mark - Master show/dismiss methods + +- (void)showProgress:(float)progress status:(NSString*)status { + __weak SVProgressHUD *weakSelf = self; + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + __strong SVProgressHUD *strongSelf = weakSelf; + if(strongSelf){ + if(strongSelf.fadeOutTimer) { + strongSelf.activityCount = 0; + } + + // Stop timer + strongSelf.fadeOutTimer = nil; + strongSelf.graceTimer = nil; + + // Update / Check view hierarchy to ensure the HUD is visible + [strongSelf updateViewHierarchy]; + + // Reset imageView and fadeout timer if an image is currently displayed + strongSelf.imageView.hidden = YES; + strongSelf.imageView.image = nil; + + // Update text and set progress to the given value + strongSelf.statusLabel.hidden = status.length == 0; + strongSelf.statusLabel.text = status; + strongSelf.progress = progress; + + // Choose the "right" indicator depending on the progress + if(progress >= 0) { + // Cancel the indefiniteAnimatedView, then show the ringLayer + [strongSelf cancelIndefiniteAnimatedViewAnimation]; + + // Add ring to HUD + if(!strongSelf.ringView.superview){ + [strongSelf.hudView.contentView addSubview:strongSelf.ringView]; + } + if(!strongSelf.backgroundRingView.superview){ + [strongSelf.hudView.contentView addSubview:strongSelf.backgroundRingView]; + } + + // Set progress animated + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + strongSelf.ringView.strokeEnd = progress; + [CATransaction commit]; + + // Update the activity count + if(progress == 0) { + strongSelf.activityCount++; + } + } else { + // Cancel the ringLayer animation, then show the indefiniteAnimatedView + [strongSelf cancelRingLayerAnimation]; + + // Add indefiniteAnimatedView to HUD + [strongSelf.hudView.contentView addSubview:strongSelf.indefiniteAnimatedView]; + if([strongSelf.indefiniteAnimatedView respondsToSelector:@selector(startAnimating)]) { + [(id)strongSelf.indefiniteAnimatedView startAnimating]; + } + + // Update the activity count + strongSelf.activityCount++; + } + + // Fade in delayed if a grace time is set + if (self.graceTimeInterval > 0.0 && self.backgroundView.alpha == 0.0f) { + strongSelf.graceTimer = [NSTimer timerWithTimeInterval:self.graceTimeInterval target:strongSelf selector:@selector(fadeIn:) userInfo:nil repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:strongSelf.graceTimer forMode:NSRunLoopCommonModes]; + } else { + [strongSelf fadeIn:nil]; + } + + // Tell the Haptics Generator to prepare for feedback, which may come soon +#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 + if (@available(iOS 10.0, *)) { + [strongSelf.hapticGenerator prepare]; + } +#endif + } + }]; +} + +- (void)showImage:(UIImage*)image status:(NSString*)status duration:(NSTimeInterval)duration { + __weak SVProgressHUD *weakSelf = self; + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + __strong SVProgressHUD *strongSelf = weakSelf; + if(strongSelf){ + // Stop timer + strongSelf.fadeOutTimer = nil; + strongSelf.graceTimer = nil; + + // Update / Check view hierarchy to ensure the HUD is visible + [strongSelf updateViewHierarchy]; + + // Reset progress and cancel any running animation + strongSelf.progress = SVProgressHUDUndefinedProgress; + [strongSelf cancelRingLayerAnimation]; + [strongSelf cancelIndefiniteAnimatedViewAnimation]; + + // Update imageView + if (self.shouldTintImages) { + if (image.renderingMode != UIImageRenderingModeAlwaysTemplate) { + strongSelf.imageView.image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + } + strongSelf.imageView.tintColor = strongSelf.foregroundColorForStyle;; + } else { + strongSelf.imageView.image = image; + } + strongSelf.imageView.hidden = NO; + + // Update text + strongSelf.statusLabel.hidden = status.length == 0; + strongSelf.statusLabel.text = status; + + // Fade in delayed if a grace time is set + // An image will be dismissed automatically. Thus pass the duration as userInfo. + if (self.graceTimeInterval > 0.0 && self.backgroundView.alpha == 0.0f) { + strongSelf.graceTimer = [NSTimer timerWithTimeInterval:self.graceTimeInterval target:strongSelf selector:@selector(fadeIn:) userInfo:@(duration) repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:strongSelf.graceTimer forMode:NSRunLoopCommonModes]; + } else { + [strongSelf fadeIn:@(duration)]; + } + } + }]; +} + +- (void)fadeIn:(id)data { + // Update the HUDs frame to the new content and position HUD + [self updateHUDFrame]; + [self positionHUD:nil]; + + // Update accessibility as well as user interaction + if(self.defaultMaskType != SVProgressHUDMaskTypeNone) { + self.controlView.userInteractionEnabled = YES; + self.accessibilityLabel = self.statusLabel.text ?: NSLocalizedString(@"Loading", nil); + self.isAccessibilityElement = YES; + } else { + self.controlView.userInteractionEnabled = NO; + self.hudView.accessibilityLabel = self.statusLabel.text ?: NSLocalizedString(@"Loading", nil); + self.hudView.isAccessibilityElement = YES; + } + + // Get duration + id duration = [data isKindOfClass:[NSTimer class]] ? ((NSTimer *)data).userInfo : data; + + // Show if not already visible + if(self.backgroundView.alpha != 1.0f) { + // Post notification to inform user + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDWillAppearNotification + object:self + userInfo:[self notificationUserInfo]]; + + // Shrink HUD to to make a nice appear / pop up animation + self.hudView.transform = self.hudView.transform = CGAffineTransformScale(self.hudView.transform, 1/1.5f, 1/1.5f); + + __block void (^animationsBlock)(void) = ^{ + // Zoom HUD a little to make a nice appear / pop up animation + self.hudView.transform = CGAffineTransformIdentity; + + // Fade in all effects (colors, blur, etc.) + [self fadeInEffects]; + }; + + __block void (^completionBlock)(void) = ^{ + // Check if we really achieved to show the HUD (<=> alpha) + // and the change of these values has not been cancelled in between e.g. due to a dismissal + if(self.backgroundView.alpha == 1.0f){ + // Register observer <=> we now have to handle orientation changes etc. + [self registerNotifications]; + + // Post notification to inform user + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidAppearNotification + object:self + userInfo:[self notificationUserInfo]]; + + // Update accessibility + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.statusLabel.text); + + // Dismiss automatically if a duration was passed as userInfo. We start a timer + // which then will call dismiss after the predefined duration + if(duration){ + self.fadeOutTimer = [NSTimer timerWithTimeInterval:[(NSNumber *)duration doubleValue] target:self selector:@selector(dismiss) userInfo:nil repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:self.fadeOutTimer forMode:NSRunLoopCommonModes]; + } + } + }; + + // Animate appearance + if (self.fadeInAnimationDuration > 0) { + // Animate appearance + [UIView animateWithDuration:self.fadeInAnimationDuration + delay:0 + options:(UIViewAnimationOptions) (UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseIn | UIViewAnimationOptionBeginFromCurrentState) + animations:^{ + animationsBlock(); + } completion:^(BOOL finished) { + completionBlock(); + }]; + } else { + animationsBlock(); + completionBlock(); + } + + // Inform iOS to redraw the view hierarchy + [self setNeedsDisplay]; + } else { + // Update accessibility + UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, nil); + UIAccessibilityPostNotification(UIAccessibilityAnnouncementNotification, self.statusLabel.text); + + // Dismiss automatically if a duration was passed as userInfo. We start a timer + // which then will call dismiss after the predefined duration + if(duration){ + self.fadeOutTimer = [NSTimer timerWithTimeInterval:[(NSNumber *)duration doubleValue] target:self selector:@selector(dismiss) userInfo:nil repeats:NO]; + [[NSRunLoop mainRunLoop] addTimer:self.fadeOutTimer forMode:NSRunLoopCommonModes]; + } + } +} + +- (void)dismiss { + [self dismissWithDelay:0.0 completion:nil]; +} + +- (void)dismissWithDelay:(NSTimeInterval)delay completion:(SVProgressHUDDismissCompletion)completion { + __weak SVProgressHUD *weakSelf = self; + [[NSOperationQueue mainQueue] addOperationWithBlock:^{ + __strong SVProgressHUD *strongSelf = weakSelf; + if(strongSelf){ + // Stop timer + strongSelf.graceTimer = nil; + + // Post notification to inform user + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDWillDisappearNotification + object:nil + userInfo:[strongSelf notificationUserInfo]]; + + // Reset activity count + strongSelf.activityCount = 0; + + __block void (^animationsBlock)(void) = ^{ + // Shrink HUD a little to make a nice disappear animation + strongSelf.hudView.transform = CGAffineTransformScale(strongSelf.hudView.transform, 1/1.3f, 1/1.3f); + + // Fade out all effects (colors, blur, etc.) + [strongSelf fadeOutEffects]; + }; + + __block void (^completionBlock)(void) = ^{ + // Check if we really achieved to dismiss the HUD (<=> alpha values are applied) + // and the change of these values has not been cancelled in between e.g. due to a new show + if(self.backgroundView.alpha == 0.0f){ + // Clean up view hierarchy (overlays) + [strongSelf.controlView removeFromSuperview]; + [strongSelf.backgroundView removeFromSuperview]; + [strongSelf.hudView removeFromSuperview]; + [strongSelf removeFromSuperview]; + + // Reset progress and cancel any running animation + strongSelf.progress = SVProgressHUDUndefinedProgress; + [strongSelf cancelRingLayerAnimation]; + [strongSelf cancelIndefiniteAnimatedViewAnimation]; + + // Remove observer <=> we do not have to handle orientation changes etc. + [[NSNotificationCenter defaultCenter] removeObserver:strongSelf]; + + // Post notification to inform user + [[NSNotificationCenter defaultCenter] postNotificationName:SVProgressHUDDidDisappearNotification + object:strongSelf + userInfo:[strongSelf notificationUserInfo]]; + + // Tell the rootViewController to update the StatusBar appearance +#if !defined(SV_APP_EXTENSIONS) && TARGET_OS_IOS + UIViewController *rootController = [[UIApplication sharedApplication] keyWindow].rootViewController; + [rootController setNeedsStatusBarAppearanceUpdate]; +#endif + + // Run an (optional) completionHandler + if (completion) { + completion(); + } + } + }; + + // UIViewAnimationOptionBeginFromCurrentState AND a delay doesn't always work as expected + // When UIViewAnimationOptionBeginFromCurrentState is set, animateWithDuration: evaluates the current + // values to check if an animation is necessary. The evaluation happens at function call time and not + // after the delay => the animation is sometimes skipped. Therefore we delay using dispatch_after. + + dispatch_time_t dipatchTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)); + dispatch_after(dipatchTime, dispatch_get_main_queue(), ^{ + if (strongSelf.fadeOutAnimationDuration > 0) { + // Animate appearance + [UIView animateWithDuration:strongSelf.fadeOutAnimationDuration + delay:0 + options:(UIViewAnimationOptions) (UIViewAnimationOptionAllowUserInteraction | UIViewAnimationCurveEaseOut | UIViewAnimationOptionBeginFromCurrentState) + animations:^{ + animationsBlock(); + } completion:^(BOOL finished) { + completionBlock(); + }]; + } else { + animationsBlock(); + completionBlock(); + } + }); + + // Inform iOS to redraw the view hierarchy + [strongSelf setNeedsDisplay]; + } + }]; +} + + +#pragma mark - Ring progress animation + +- (UIView*)indefiniteAnimatedView { + // Get the correct spinner for defaultAnimationType + if(self.defaultAnimationType == SVProgressHUDAnimationTypeFlat){ + // Check if spinner exists and is an object of different class + if(_indefiniteAnimatedView && ![_indefiniteAnimatedView isKindOfClass:[SVIndefiniteAnimatedView class]]){ + [_indefiniteAnimatedView removeFromSuperview]; + _indefiniteAnimatedView = nil; + } + + if(!_indefiniteAnimatedView){ + _indefiniteAnimatedView = [[SVIndefiniteAnimatedView alloc] initWithFrame:CGRectZero]; + } + + // Update styling + SVIndefiniteAnimatedView *indefiniteAnimatedView = (SVIndefiniteAnimatedView*)_indefiniteAnimatedView; + indefiniteAnimatedView.strokeColor = self.foregroundColorForStyle; + indefiniteAnimatedView.strokeThickness = self.ringThickness; + indefiniteAnimatedView.radius = self.statusLabel.text ? self.ringRadius : self.ringNoTextRadius; + } else { + // Check if spinner exists and is an object of different class + if(_indefiniteAnimatedView && ![_indefiniteAnimatedView isKindOfClass:[UIActivityIndicatorView class]]){ + [_indefiniteAnimatedView removeFromSuperview]; + _indefiniteAnimatedView = nil; + } + + if(!_indefiniteAnimatedView){ + _indefiniteAnimatedView = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge]; + } + + // Update styling + UIActivityIndicatorView *activityIndicatorView = (UIActivityIndicatorView*)_indefiniteAnimatedView; + activityIndicatorView.color = self.foregroundColorForStyle; + } + [_indefiniteAnimatedView sizeToFit]; + + return _indefiniteAnimatedView; +} + +- (SVProgressAnimatedView*)ringView { + if(!_ringView) { + _ringView = [[SVProgressAnimatedView alloc] initWithFrame:CGRectZero]; + } + + // Update styling + _ringView.strokeColor = self.foregroundColorForStyle; + _ringView.strokeThickness = self.ringThickness; + _ringView.radius = self.statusLabel.text ? self.ringRadius : self.ringNoTextRadius; + + return _ringView; +} + +- (SVProgressAnimatedView*)backgroundRingView { + if(!_backgroundRingView) { + _backgroundRingView = [[SVProgressAnimatedView alloc] initWithFrame:CGRectZero]; + _backgroundRingView.strokeEnd = 1.0f; + } + + // Update styling + _backgroundRingView.strokeColor = [self.foregroundColorForStyle colorWithAlphaComponent:0.1f]; + _backgroundRingView.strokeThickness = self.ringThickness; + _backgroundRingView.radius = self.statusLabel.text ? self.ringRadius : self.ringNoTextRadius; + + return _backgroundRingView; +} + +- (void)cancelRingLayerAnimation { + // Animate value update, stop animation + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + + [self.hudView.layer removeAllAnimations]; + self.ringView.strokeEnd = 0.0f; + + [CATransaction commit]; + + // Remove from view + [self.ringView removeFromSuperview]; + [self.backgroundRingView removeFromSuperview]; +} + +- (void)cancelIndefiniteAnimatedViewAnimation { + // Stop animation + if([self.indefiniteAnimatedView respondsToSelector:@selector(stopAnimating)]) { + [(id)self.indefiniteAnimatedView stopAnimating]; + } + // Remove from view + [self.indefiniteAnimatedView removeFromSuperview]; +} + + +#pragma mark - Utilities + ++ (BOOL)isVisible { + // Checking one alpha value is sufficient as they are all the same + return [self sharedView].backgroundView.alpha > 0.0f; +} + + +#pragma mark - Getters + ++ (NSTimeInterval)displayDurationForString:(NSString*)string { + CGFloat minimum = MAX((CGFloat)string.length * 0.06 + 0.5, [self sharedView].minimumDismissTimeInterval); + return MIN(minimum, [self sharedView].maximumDismissTimeInterval); +} + +- (UIColor*)foregroundColorForStyle { + if(self.defaultStyle == SVProgressHUDStyleLight) { + return [UIColor blackColor]; + } else if(self.defaultStyle == SVProgressHUDStyleDark) { + return [UIColor whiteColor]; + } else { + return self.foregroundColor; + } +} + +- (UIColor*)backgroundColorForStyle { + if(self.defaultStyle == SVProgressHUDStyleLight) { + return [UIColor whiteColor]; + } else if(self.defaultStyle == SVProgressHUDStyleDark) { + return [UIColor blackColor]; + } else { + return self.backgroundColor; + } +} + +- (UIControl*)controlView { + if(!_controlView) { + _controlView = [UIControl new]; + _controlView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + _controlView.backgroundColor = [UIColor clearColor]; + _controlView.userInteractionEnabled = YES; + [_controlView addTarget:self action:@selector(controlViewDidReceiveTouchEvent:forEvent:) forControlEvents:UIControlEventTouchDown]; + } + + // Update frames +#if !defined(SV_APP_EXTENSIONS) + CGRect windowBounds = [[[UIApplication sharedApplication] delegate] window].bounds; + _controlView.frame = windowBounds; +#else + _controlView.frame = [UIScreen mainScreen].bounds; +#endif + + return _controlView; +} + +-(UIView *)backgroundView { + if(!_backgroundView){ + _backgroundView = [UIView new]; + _backgroundView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; + } + if(!_backgroundView.superview){ + [self insertSubview:_backgroundView belowSubview:self.hudView]; + } + + // Update styling + if(self.defaultMaskType == SVProgressHUDMaskTypeGradient){ + if(!_backgroundRadialGradientLayer){ + _backgroundRadialGradientLayer = [SVRadialGradientLayer layer]; + } + if(!_backgroundRadialGradientLayer.superlayer){ + [_backgroundView.layer insertSublayer:_backgroundRadialGradientLayer atIndex:0]; + } + _backgroundView.backgroundColor = [UIColor clearColor]; + } else { + if(_backgroundRadialGradientLayer && _backgroundRadialGradientLayer.superlayer){ + [_backgroundRadialGradientLayer removeFromSuperlayer]; + } + if(self.defaultMaskType == SVProgressHUDMaskTypeBlack){ + _backgroundView.backgroundColor = [UIColor colorWithWhite:0 alpha:0.4]; + } else if(self.defaultMaskType == SVProgressHUDMaskTypeCustom){ + _backgroundView.backgroundColor = self.backgroundLayerColor; + } else { + _backgroundView.backgroundColor = [UIColor clearColor]; + } + } + + // Update frame + if(_backgroundView){ + _backgroundView.frame = self.bounds; + } + if(_backgroundRadialGradientLayer){ + _backgroundRadialGradientLayer.frame = self.bounds; + + // Calculate the new center of the gradient, it may change if keyboard is visible + CGPoint gradientCenter = self.center; + gradientCenter.y = (self.bounds.size.height - self.visibleKeyboardHeight)/2; + _backgroundRadialGradientLayer.gradientCenter = gradientCenter; + [_backgroundRadialGradientLayer setNeedsDisplay]; + } + + return _backgroundView; +} +- (UIVisualEffectView*)hudView { + if(!_hudView) { + _hudView = [UIVisualEffectView new]; + _hudView.layer.masksToBounds = YES; + _hudView.autoresizingMask = UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleRightMargin | UIViewAutoresizingFlexibleLeftMargin; + } + if(!_hudView.superview) { + [self addSubview:_hudView]; + } + + // Update styling + _hudView.layer.cornerRadius = self.cornerRadius; + + return _hudView; +} + +- (UILabel*)statusLabel { + if(!_statusLabel) { + _statusLabel = [[UILabel alloc] initWithFrame:CGRectZero]; + _statusLabel.backgroundColor = [UIColor clearColor]; + _statusLabel.adjustsFontSizeToFitWidth = YES; + _statusLabel.textAlignment = NSTextAlignmentCenter; + _statusLabel.baselineAdjustment = UIBaselineAdjustmentAlignCenters; + _statusLabel.numberOfLines = 0; + } + if(!_statusLabel.superview) { + [self.hudView.contentView addSubview:_statusLabel]; + } + + // Update styling + _statusLabel.textColor = self.foregroundColorForStyle; + _statusLabel.font = self.font; + + return _statusLabel; +} + +- (UIImageView*)imageView { + if(_imageView && !CGSizeEqualToSize(_imageView.bounds.size, _imageViewSize)) { + [_imageView removeFromSuperview]; + _imageView = nil; + } + + if(!_imageView) { + _imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0.0f, 0.0f, _imageViewSize.width, _imageViewSize.height)]; + } + if(!_imageView.superview) { + [self.hudView.contentView addSubview:_imageView]; + } + + return _imageView; +} + + +#pragma mark - Helper + +- (CGFloat)visibleKeyboardHeight { +#if !defined(SV_APP_EXTENSIONS) + UIWindow *keyboardWindow = nil; + for (UIWindow *testWindow in UIApplication.sharedApplication.windows) { + if(![testWindow.class isEqual:UIWindow.class]) { + keyboardWindow = testWindow; + break; + } + } + + for (__strong UIView *possibleKeyboard in keyboardWindow.subviews) { + NSString *viewName = NSStringFromClass(possibleKeyboard.class); + if([viewName hasPrefix:@"UI"]){ + if([viewName hasSuffix:@"PeripheralHostView"] || [viewName hasSuffix:@"Keyboard"]){ + return CGRectGetHeight(possibleKeyboard.bounds); + } else if ([viewName hasSuffix:@"InputSetContainerView"]){ + for (__strong UIView *possibleKeyboardSubview in possibleKeyboard.subviews) { + viewName = NSStringFromClass(possibleKeyboardSubview.class); + if([viewName hasPrefix:@"UI"] && [viewName hasSuffix:@"InputSetHostView"]) { + CGRect convertedRect = [possibleKeyboard convertRect:possibleKeyboardSubview.frame toView:self]; + CGRect intersectedRect = CGRectIntersection(convertedRect, self.bounds); + if (!CGRectIsNull(intersectedRect)) { + return CGRectGetHeight(intersectedRect); + } + } + } + } + } + } +#endif + return 0; +} + +- (UIWindow *)frontWindow { +#if !defined(SV_APP_EXTENSIONS) + NSEnumerator *frontToBackWindows = [UIApplication.sharedApplication.windows reverseObjectEnumerator]; + for (UIWindow *window in frontToBackWindows) { + BOOL windowOnMainScreen = window.screen == UIScreen.mainScreen; + BOOL windowIsVisible = !window.hidden && window.alpha > 0; + BOOL windowLevelSupported = (window.windowLevel >= UIWindowLevelNormal && window.windowLevel <= self.maxSupportedWindowLevel); + BOOL windowKeyWindow = window.isKeyWindow; + + if(windowOnMainScreen && windowIsVisible && windowLevelSupported && windowKeyWindow) { + return window; + } + } +#endif + return nil; +} + +- (void)fadeInEffects { + if(self.defaultStyle != SVProgressHUDStyleCustom) { + // Add blur effect + UIBlurEffectStyle blurEffectStyle = self.defaultStyle == SVProgressHUDStyleDark ? UIBlurEffectStyleDark : UIBlurEffectStyleLight; + UIBlurEffect *blurEffect = [UIBlurEffect effectWithStyle:blurEffectStyle]; + self.hudView.effect = blurEffect; + + // We omit UIVibrancy effect and use a suitable background color as an alternative. + // This will make everything more readable. See the following for details: + // https://www.omnigroup.com/developer/how-to-make-text-in-a-uivisualeffectview-readable-on-any-background + + self.hudView.backgroundColor = [self.backgroundColorForStyle colorWithAlphaComponent:0.6f]; + } else { + self.hudView.backgroundColor = self.backgroundColorForStyle; + } + + // Fade in views + self.backgroundView.alpha = 1.0f; + + self.imageView.alpha = 1.0f; + self.statusLabel.alpha = 1.0f; + self.indefiniteAnimatedView.alpha = 1.0f; + self.ringView.alpha = self.backgroundRingView.alpha = 1.0f; +} + +- (void)fadeOutEffects +{ + if(self.defaultStyle != SVProgressHUDStyleCustom) { + // Remove blur effect + self.hudView.effect = nil; + } + + // Remove background color + self.hudView.backgroundColor = [UIColor clearColor]; + + // Fade out views + self.backgroundView.alpha = 0.0f; + + self.imageView.alpha = 0.0f; + self.statusLabel.alpha = 0.0f; + self.indefiniteAnimatedView.alpha = 0.0f; + self.ringView.alpha = self.backgroundRingView.alpha = 0.0f; +} + +#if TARGET_OS_IOS && __IPHONE_OS_VERSION_MAX_ALLOWED >= 100000 +- (UINotificationFeedbackGenerator *)hapticGenerator NS_AVAILABLE_IOS(10_0) { + // Only return if haptics are enabled + if(!self.hapticsEnabled) { + return nil; + } + + if(!_hapticGenerator) { + _hapticGenerator = [[UINotificationFeedbackGenerator alloc] init]; + } + return _hapticGenerator; +} +#endif + + +#pragma mark - UIAppearance Setters + +- (void)setDefaultStyle:(SVProgressHUDStyle)style { + if (!_isInitializing) _defaultStyle = style; +} + +- (void)setDefaultMaskType:(SVProgressHUDMaskType)maskType { + if (!_isInitializing) _defaultMaskType = maskType; +} + +- (void)setDefaultAnimationType:(SVProgressHUDAnimationType)animationType { + if (!_isInitializing) _defaultAnimationType = animationType; +} + +- (void)setContainerView:(UIView *)containerView { + if (!_isInitializing) _containerView = containerView; +} + +- (void)setMinimumSize:(CGSize)minimumSize { + if (!_isInitializing) _minimumSize = minimumSize; +} + +- (void)setRingThickness:(CGFloat)ringThickness { + if (!_isInitializing) _ringThickness = ringThickness; +} + +- (void)setRingRadius:(CGFloat)ringRadius { + if (!_isInitializing) _ringRadius = ringRadius; +} + +- (void)setRingNoTextRadius:(CGFloat)ringNoTextRadius { + if (!_isInitializing) _ringNoTextRadius = ringNoTextRadius; +} + +- (void)setCornerRadius:(CGFloat)cornerRadius { + if (!_isInitializing) _cornerRadius = cornerRadius; +} + +- (void)setFont:(UIFont*)font { + if (!_isInitializing) _font = font; +} + +- (void)setForegroundColor:(UIColor*)color { + if (!_isInitializing) _foregroundColor = color; +} + +- (void)setBackgroundColor:(UIColor*)color { + if (!_isInitializing) _backgroundColor = color; +} + +- (void)setBackgroundLayerColor:(UIColor*)color { + if (!_isInitializing) _backgroundLayerColor = color; +} + +- (void)setShouldTintImages:(BOOL)shouldTintImages { + if (!_isInitializing) _shouldTintImages = shouldTintImages; +} + +- (void)setInfoImage:(UIImage*)image { + if (!_isInitializing) _infoImage = image; +} + +- (void)setSuccessImage:(UIImage*)image { + if (!_isInitializing) _successImage = image; +} + +- (void)setErrorImage:(UIImage*)image { + if (!_isInitializing) _errorImage = image; +} + +- (void)setViewForExtension:(UIView*)view { + if (!_isInitializing) _viewForExtension = view; +} + +- (void)setOffsetFromCenter:(UIOffset)offset { + if (!_isInitializing) _offsetFromCenter = offset; +} + +- (void)setMinimumDismissTimeInterval:(NSTimeInterval)minimumDismissTimeInterval { + if (!_isInitializing) _minimumDismissTimeInterval = minimumDismissTimeInterval; +} + +- (void)setFadeInAnimationDuration:(NSTimeInterval)duration { + if (!_isInitializing) _fadeInAnimationDuration = duration; +} + +- (void)setFadeOutAnimationDuration:(NSTimeInterval)duration { + if (!_isInitializing) _fadeOutAnimationDuration = duration; +} + +- (void)setMaxSupportedWindowLevel:(UIWindowLevel)maxSupportedWindowLevel { + if (!_isInitializing) _maxSupportedWindowLevel = maxSupportedWindowLevel; +} + +@end diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.h b/Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.h new file mode 100644 index 0000000..68d452a --- /dev/null +++ b/Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.h @@ -0,0 +1,14 @@ +// +// SVRadialGradientLayer.h +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Copyright (c) 2014-2018 Tobias Tiemerding. All rights reserved. +// + +#import <QuartzCore/QuartzCore.h> + +@interface SVRadialGradientLayer : CALayer + +@property (nonatomic) CGPoint gradientCenter; + +@end diff --git a/Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.m b/Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.m new file mode 100644 index 0000000..c62e0f8 --- /dev/null +++ b/Pods/SVProgressHUD/SVProgressHUD/SVRadialGradientLayer.m @@ -0,0 +1,25 @@ +// +// SVRadialGradientLayer.m +// SVProgressHUD, https://github.com/SVProgressHUD/SVProgressHUD +// +// Copyright (c) 2014-2018 Tobias Tiemerding. All rights reserved. +// + +#import "SVRadialGradientLayer.h" + +@implementation SVRadialGradientLayer + +- (void)drawInContext:(CGContextRef)context { + size_t locationsCount = 2; + CGFloat locations[2] = {0.0f, 1.0f}; + CGFloat colors[8] = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.75f}; + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, colors, locations, locationsCount); + CGColorSpaceRelease(colorSpace); + + float radius = MIN(self.bounds.size.width , self.bounds.size.height); + CGContextDrawRadialGradient (context, gradient, self.gradientCenter, 0, self.gradientCenter, radius, kCGGradientDrawsAfterEndLocation); + CGGradientRelease(gradient); +} + +@end diff --git a/Pods/Target Support Files/FeedKit/FeedKit-Info.plist b/Pods/Target Support Files/FeedKit/FeedKit-Info.plist new file mode 100644 index 0000000..260005e --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit-Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>8.1.1</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/FeedKit/FeedKit-dummy.m b/Pods/Target Support Files/FeedKit/FeedKit-dummy.m new file mode 100644 index 0000000..7a788dd --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_FeedKit : NSObject +@end +@implementation PodsDummy_FeedKit +@end diff --git a/Pods/Target Support Files/FeedKit/FeedKit-prefix.pch b/Pods/Target Support Files/FeedKit/FeedKit-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/FeedKit/FeedKit-umbrella.h b/Pods/Target Support Files/FeedKit/FeedKit-umbrella.h new file mode 100644 index 0000000..a06f962 --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double FeedKitVersionNumber; +FOUNDATION_EXPORT const unsigned char FeedKitVersionString[]; + diff --git a/Pods/Target Support Files/FeedKit/FeedKit.modulemap b/Pods/Target Support Files/FeedKit/FeedKit.modulemap new file mode 100644 index 0000000..ecc6d00 --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit.modulemap @@ -0,0 +1,6 @@ +framework module FeedKit { + umbrella header "FeedKit-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/FeedKit/FeedKit.xcconfig b/Pods/Target Support Files/FeedKit/FeedKit.xcconfig new file mode 100644 index 0000000..41aa1db --- /dev/null +++ b/Pods/Target Support Files/FeedKit/FeedKit.xcconfig @@ -0,0 +1,9 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/FeedKit +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/FeedKit +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist b/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist new file mode 100644 index 0000000..0db9a90 --- /dev/null +++ b/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>5.7.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m b/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m new file mode 100644 index 0000000..1b89d0e --- /dev/null +++ b/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_Kingfisher : NSObject +@end +@implementation PodsDummy_Kingfisher +@end diff --git a/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch b/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h b/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h new file mode 100644 index 0000000..89b88ac --- /dev/null +++ b/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h @@ -0,0 +1,17 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "Kingfisher.h" + +FOUNDATION_EXPORT double KingfisherVersionNumber; +FOUNDATION_EXPORT const unsigned char KingfisherVersionString[]; + diff --git a/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap b/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap new file mode 100644 index 0000000..2a20d91 --- /dev/null +++ b/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap @@ -0,0 +1,6 @@ +framework module Kingfisher { + umbrella header "Kingfisher-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Kingfisher/Kingfisher.xcconfig b/Pods/Target Support Files/Kingfisher/Kingfisher.xcconfig new file mode 100644 index 0000000..6a03a1a --- /dev/null +++ b/Pods/Target Support Files/Kingfisher/Kingfisher.xcconfig @@ -0,0 +1,10 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS -suppress-warnings +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-Info.plist b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-Info.plist new file mode 100644 index 0000000..2243fe6 --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>1.0.0</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-acknowledgements.markdown b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-acknowledgements.markdown new file mode 100644 index 0000000..2e2faf0 --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-acknowledgements.markdown @@ -0,0 +1,79 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## FeedKit + +The MIT License (MIT) + +Copyright (c) 2016 - 2018 Nuno Manuel Dias + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + +## Kingfisher + +The MIT License (MIT) + +Copyright (c) 2018 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + + + +## SVProgressHUD + +MIT License + +Copyright (c) 2011-2018 Sam Vermette, Tobias Tiemerding and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +Generated by CocoaPods - https://cocoapods.org diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-acknowledgements.plist b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-acknowledgements.plist new file mode 100644 index 0000000..11bed54 --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-acknowledgements.plist @@ -0,0 +1,123 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>PreferenceSpecifiers</key> + <array> + <dict> + <key>FooterText</key> + <string>This application makes use of the following third party libraries:</string> + <key>Title</key> + <string>Acknowledgements</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>The MIT License (MIT) + +Copyright (c) 2016 - 2018 Nuno Manuel Dias + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</string> + <key>License</key> + <string>MIT</string> + <key>Title</key> + <string>FeedKit</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>The MIT License (MIT) + +Copyright (c) 2018 Wei Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + +</string> + <key>License</key> + <string>MIT</string> + <key>Title</key> + <string>Kingfisher</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>MIT License + +Copyright (c) 2011-2018 Sam Vermette, Tobias Tiemerding and contributors. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +</string> + <key>License</key> + <string>MIT</string> + <key>Title</key> + <string>SVProgressHUD</string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + <dict> + <key>FooterText</key> + <string>Generated by CocoaPods - https://cocoapods.org</string> + <key>Title</key> + <string></string> + <key>Type</key> + <string>PSGroupSpecifier</string> + </dict> + </array> + <key>StringsTable</key> + <string>Acknowledgements</string> + <key>Title</key> + <string>Acknowledgements</string> +</dict> +</plist> diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-dummy.m b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-dummy.m new file mode 100644 index 0000000..dc36291 --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_Pods_rss_reader : NSObject +@end +@implementation PodsDummy_Pods_rss_reader +@end diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Debug-input-files.xcfilelist b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 0000000..f7c7ddc --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,4 @@ +${PODS_ROOT}/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks.sh +${BUILT_PRODUCTS_DIR}/FeedKit/FeedKit.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework +${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Debug-output-files.xcfilelist b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 0000000..974e71e --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1,3 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FeedKit.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Release-input-files.xcfilelist b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Release-input-files.xcfilelist new file mode 100644 index 0000000..f7c7ddc --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,4 @@ +${PODS_ROOT}/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks.sh +${BUILT_PRODUCTS_DIR}/FeedKit/FeedKit.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework +${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Release-output-files.xcfilelist b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Release-output-files.xcfilelist new file mode 100644 index 0000000..974e71e --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-Release-output-files.xcfilelist @@ -0,0 +1,3 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FeedKit.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SVProgressHUD.framework \ No newline at end of file diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks.sh b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks.sh new file mode 100755 index 0000000..9bab378 --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks.sh @@ -0,0 +1,175 @@ +#!/bin/sh +set -e +set -u +set -o pipefail + +function on_error { + echo "$(realpath -mq "${0}"):$1: error: Unexpected failure" +} +trap 'on_error $LINENO' ERR + +if [ -z ${FRAMEWORKS_FOLDER_PATH+x} ]; then + # If FRAMEWORKS_FOLDER_PATH is not set, then there's nowhere for us to copy + # frameworks to, so exit 0 (signalling the script phase was successful). + exit 0 +fi + +echo "mkdir -p ${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" +mkdir -p "${CONFIGURATION_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + +COCOAPODS_PARALLEL_CODE_SIGN="${COCOAPODS_PARALLEL_CODE_SIGN:-false}" +SWIFT_STDLIB_PATH="${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# This protects against multiple targets copying the same framework dependency at the same time. The solution +# was originally proposed here: https://lists.samba.org/archive/rsync/2008-February/020158.html +RSYNC_PROTECT_TMP_FILES=(--filter "P .*.??????") + +# Copies and strips a vendored framework +install_framework() +{ + if [ -r "${BUILT_PRODUCTS_DIR}/$1" ]; then + local source="${BUILT_PRODUCTS_DIR}/$1" + elif [ -r "${BUILT_PRODUCTS_DIR}/$(basename "$1")" ]; then + local source="${BUILT_PRODUCTS_DIR}/$(basename "$1")" + elif [ -r "$1" ]; then + local source="$1" + fi + + local destination="${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}" + + if [ -L "${source}" ]; then + echo "Symlinked..." + source="$(readlink "${source}")" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${source}" "${destination}" + + local basename + basename="$(basename -s .framework "$1")" + binary="${destination}/${basename}.framework/${basename}" + + if ! [ -r "$binary" ]; then + binary="${destination}/${basename}" + elif [ -L "${binary}" ]; then + echo "Destination binary is symlinked..." + dirname="$(dirname "${binary}")" + binary="${dirname}/$(readlink "${binary}")" + fi + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"dynamically linked shared library"* ]]; then + strip_invalid_archs "$binary" + fi + + # Resign the code if required by the build settings to avoid unstable apps + code_sign_if_enabled "${destination}/$(basename "$1")" + + # Embed linked Swift runtime libraries. No longer necessary as of Xcode 7. + if [ "${XCODE_VERSION_MAJOR}" -lt 7 ]; then + local swift_runtime_libs + swift_runtime_libs=$(xcrun otool -LX "$binary" | grep --color=never @rpath/libswift | sed -E s/@rpath\\/\(.+dylib\).*/\\1/g | uniq -u) + for lib in $swift_runtime_libs; do + echo "rsync -auv \"${SWIFT_STDLIB_PATH}/${lib}\" \"${destination}\"" + rsync -auv "${SWIFT_STDLIB_PATH}/${lib}" "${destination}" + code_sign_if_enabled "${destination}/${lib}" + done + fi +} + +# Copies and strips a vendored dSYM +install_dsym() { + local source="$1" + if [ -r "$source" ]; then + # Copy the dSYM into a the targets temp dir. + 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}\"" + 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}" + + local basename + basename="$(basename -s .framework.dSYM "$source")" + binary="${DERIVED_FILES_DIR}/${basename}.framework.dSYM/Contents/Resources/DWARF/${basename}" + + # Strip invalid architectures so "fat" simulator / device frameworks work on device + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" + fi + + if [[ $STRIP_BINARY_RETVAL == 1 ]]; then + # Move the stripped file into its final destination. + 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}\"" + 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}" + else + # 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. + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.framework.dSYM" + fi + fi +} + +# Copies the bcsymbolmap files of a vendored framework +install_bcsymbolmap() { + local bcsymbolmap_path="$1" + local destination="${BUILT_PRODUCTS_DIR}" + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${bcsymbolmap_path}" "${destination}" +} + +# Signs a framework with the provided identity +code_sign_if_enabled() { + if [ -n "${EXPANDED_CODE_SIGN_IDENTITY:-}" -a "${CODE_SIGNING_REQUIRED:-}" != "NO" -a "${CODE_SIGNING_ALLOWED}" != "NO" ]; then + # Use the current code_sign_identity + echo "Code Signing $1 with Identity ${EXPANDED_CODE_SIGN_IDENTITY_NAME}" + local code_sign_cmd="/usr/bin/codesign --force --sign ${EXPANDED_CODE_SIGN_IDENTITY} ${OTHER_CODE_SIGN_FLAGS:-} --preserve-metadata=identifier,entitlements '$1'" + + if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + code_sign_cmd="$code_sign_cmd &" + fi + echo "$code_sign_cmd" + eval "$code_sign_cmd" + fi +} + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + # Get architectures for current target binary + binary_archs="$(lipo -info "$binary" | rev | cut -d ':' -f1 | awk '{$1=$1;print}' | rev)" + # Intersect them with the architectures we are building for + intersected_archs="$(echo ${ARCHS[@]} ${binary_archs[@]} | tr ' ' '\n' | sort | uniq -d)" + # If there are no archs supported by this binary then warn the user + if [[ -z "$intersected_archs" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + STRIP_BINARY_RETVAL=0 + return + fi + stripped="" + for arch in $binary_archs; do + if ! [[ "${ARCHS}" == *"$arch"* ]]; then + # Strip non-valid architectures in-place + lipo -remove "$arch" -output "$binary" "$binary" + stripped="$stripped $arch" + fi + done + if [[ "$stripped" ]]; then + echo "Stripped $binary of architectures:$stripped" + fi + STRIP_BINARY_RETVAL=1 +} + + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/FeedKit/FeedKit.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/FeedKit/FeedKit.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/SVProgressHUD/SVProgressHUD.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-umbrella.h b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-umbrella.h new file mode 100644 index 0000000..e14e9d1 --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_rss_readerVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_rss_readerVersionString[]; + diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader.debug.xcconfig b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader.debug.xcconfig new file mode 100644 index 0000000..2bb669a --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader.debug.xcconfig @@ -0,0 +1,12 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FeedKit" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FeedKit/FeedKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/FeedKit/FeedKit.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/FeedKit" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "FeedKit" -framework "Kingfisher" -framework "QuartzCore" -framework "SVProgressHUD" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader.modulemap b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader.modulemap new file mode 100644 index 0000000..73a5f3e --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader.modulemap @@ -0,0 +1,6 @@ +framework module Pods_rss_reader { + umbrella header "Pods-rss-reader-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader.release.xcconfig b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader.release.xcconfig new file mode 100644 index 0000000..2bb669a --- /dev/null +++ b/Pods/Target Support Files/Pods-rss-reader/Pods-rss-reader.release.xcconfig @@ -0,0 +1,12 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FeedKit" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/FeedKit/FeedKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) '@executable_path/Frameworks' '@loader_path/Frameworks' +OTHER_CFLAGS = $(inherited) -isystem "${PODS_CONFIGURATION_BUILD_DIR}/FeedKit/FeedKit.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" -isystem "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD/SVProgressHUD.framework/Headers" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/FeedKit" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" -iframework "${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD" +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -framework "FeedKit" -framework "Kingfisher" -framework "QuartzCore" -framework "SVProgressHUD" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_PODFILE_DIR_PATH = ${SRCROOT}/. +PODS_ROOT = ${SRCROOT}/Pods diff --git a/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-Info.plist b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-Info.plist new file mode 100644 index 0000000..ce4ba6f --- /dev/null +++ b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-Info.plist @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>en</string> + <key>CFBundleExecutable</key> + <string>${EXECUTABLE_NAME}</string> + <key>CFBundleIdentifier</key> + <string>${PRODUCT_BUNDLE_IDENTIFIER}</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>${PRODUCT_NAME}</string> + <key>CFBundlePackageType</key> + <string>FMWK</string> + <key>CFBundleShortVersionString</key> + <string>2.2.5</string> + <key>CFBundleSignature</key> + <string>????</string> + <key>CFBundleVersion</key> + <string>${CURRENT_PROJECT_VERSION}</string> + <key>NSPrincipalClass</key> + <string></string> +</dict> +</plist> diff --git a/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-dummy.m b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-dummy.m new file mode 100644 index 0000000..696032a --- /dev/null +++ b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-dummy.m @@ -0,0 +1,5 @@ +#import <Foundation/Foundation.h> +@interface PodsDummy_SVProgressHUD : NSObject +@end +@implementation PodsDummy_SVProgressHUD +@end diff --git a/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-prefix.pch b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-umbrella.h b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-umbrella.h new file mode 100644 index 0000000..bff1d78 --- /dev/null +++ b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD-umbrella.h @@ -0,0 +1,20 @@ +#ifdef __OBJC__ +#import <UIKit/UIKit.h> +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "SVIndefiniteAnimatedView.h" +#import "SVProgressAnimatedView.h" +#import "SVProgressHUD.h" +#import "SVRadialGradientLayer.h" + +FOUNDATION_EXPORT double SVProgressHUDVersionNumber; +FOUNDATION_EXPORT const unsigned char SVProgressHUDVersionString[]; + diff --git a/Pods/Target Support Files/SVProgressHUD/SVProgressHUD.modulemap b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD.modulemap new file mode 100644 index 0000000..2eaf140 --- /dev/null +++ b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD.modulemap @@ -0,0 +1,6 @@ +framework module SVProgressHUD { + umbrella header "SVProgressHUD-umbrella.h" + + export * + module * { export * } +} diff --git a/Pods/Target Support Files/SVProgressHUD/SVProgressHUD.xcconfig b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD.xcconfig new file mode 100644 index 0000000..38bb9b9 --- /dev/null +++ b/Pods/Target Support Files/SVProgressHUD/SVProgressHUD.xcconfig @@ -0,0 +1,9 @@ +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SVProgressHUD +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_LDFLAGS = $(inherited) -framework "QuartzCore" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/SVProgressHUD +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES diff --git a/rss-reader.xcodeproj/project.pbxproj b/rss-reader.xcodeproj/project.pbxproj new file mode 100644 index 0000000..120082a --- /dev/null +++ b/rss-reader.xcodeproj/project.pbxproj @@ -0,0 +1,627 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXBuildFile section */ + 77EA2A39EAA703E04968E379 /* Pods_rss_reader.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0CD6FB9A3EF45C6CB6A83473 /* Pods_rss_reader.framework */; }; + A23745312300137E00A5416B /* Feed.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23745302300137E00A5416B /* Feed.swift */; }; + A23745332300245600A5416B /* FeedDataObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A23745322300245600A5416B /* FeedDataObject.swift */; }; + A237453523002A4700A5416B /* ArticleDataObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A237453423002A4700A5416B /* ArticleDataObject.swift */; }; + A237453723003E5300A5416B /* ColorsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A237453623003E5300A5416B /* ColorsManager.swift */; }; + A237453923004FE300A5416B /* SourceEntity.swift in Sources */ = {isa = PBXBuildFile; fileRef = A237453823004FE300A5416B /* SourceEntity.swift */; }; + A27AAC6E22FED3DF001E069F /* DetailedSourceViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC6D22FED3DF001E069F /* DetailedSourceViewController.swift */; }; + A27AAC7022FED3F0001E069F /* AddSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC6F22FED3F0001E069F /* AddSourceViewModel.swift */; }; + A27AAC7222FED40B001E069F /* SourceListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC7122FED40B001E069F /* SourceListViewController.swift */; }; + A27AAC7422FED41E001E069F /* SourceListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC7322FED41E001E069F /* SourceListViewModel.swift */; }; + A27AAC7622FED42E001E069F /* ArticleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC7522FED42E001E069F /* ArticleViewController.swift */; }; + A27AAC7822FED43A001E069F /* ArticleViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC7722FED43A001E069F /* ArticleViewModel.swift */; }; + A27AAC7A22FED448001E069F /* FeedListViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC7922FED448001E069F /* FeedListViewController.swift */; }; + A27AAC7C22FED454001E069F /* FeedListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC7B22FED454001E069F /* FeedListViewModel.swift */; }; + A27AAC7E22FEE333001E069F /* UserDefaultsStorageManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC7D22FEE333001E069F /* UserDefaultsStorageManager.swift */; }; + A27AAC8022FEE451001E069F /* RSSNetworkManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC7F22FEE451001E069F /* RSSNetworkManager.swift */; }; + A27AAC8622FEF5F3001E069F /* SourceCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC8522FEF5F3001E069F /* SourceCell.swift */; }; + A27AAC8922FF0185001E069F /* ArticleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC8822FF0185001E069F /* ArticleCell.swift */; }; + A27AAC8B22FF065E001E069F /* ArcticleCellViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC8A22FF065E001E069F /* ArcticleCellViewModel.swift */; }; + A27AAC8E22FF0959001E069F /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC8D22FF0959001E069F /* Source.swift */; }; + A27AAC9022FF1C18001E069F /* SourceManagerProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC8F22FF1C18001E069F /* SourceManagerProtocols.swift */; }; + A27AAC9222FF1EEA001E069F /* ChangeSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC9122FF1EEA001E069F /* ChangeSourceViewModel.swift */; }; + A27AAC9422FF23B3001E069F /* DetailedSourceViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC9322FF23B3001E069F /* DetailedSourceViewModel.swift */; }; + A27AAC9722FF3191001E069F /* SourceDataObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC9622FF3191001E069F /* SourceDataObject.swift */; }; + A27AAC9922FFF0C4001E069F /* Alert.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC9822FFF0C4001E069F /* Alert.swift */; }; + A27AAC9C2300055B001E069F /* Article.swift in Sources */ = {isa = PBXBuildFile; fileRef = A27AAC9B2300055B001E069F /* Article.swift */; }; + A2A934EE22FEAE3D003B6A82 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A934ED22FEAE3D003B6A82 /* AppDelegate.swift */; }; + A2A934F522FEAE42003B6A82 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A2A934F422FEAE42003B6A82 /* Assets.xcassets */; }; + A2A934F822FEAE42003B6A82 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A2A934F622FEAE42003B6A82 /* LaunchScreen.storyboard */; }; + A2A9350022FEB18E003B6A82 /* AppCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A934FF22FEB18E003B6A82 /* AppCoordinator.swift */; }; + A2A9350222FEB745003B6A82 /* CoordinatorProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A9350122FEB745003B6A82 /* CoordinatorProtocols.swift */; }; + A2A9350722FEC35B003B6A82 /* FeedFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A9350622FEC35B003B6A82 /* FeedFlowCoordinator.swift */; }; + A2A9350922FEC396003B6A82 /* SourceListFlowCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A9350822FEC396003B6A82 /* SourceListFlowCoordinator.swift */; }; + A2A9350B22FECB84003B6A82 /* RSSSourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A2A9350A22FECB84003B6A82 /* RSSSourceManager.swift */; }; + A2A9350D22FECBE2003B6A82 /* FeedStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A2A9350C22FECBE2003B6A82 /* FeedStoryboard.storyboard */; }; + A2A9350F22FECBF7003B6A82 /* SourceStoryboard.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A2A9350E22FECBF7003B6A82 /* SourceStoryboard.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 0CD6FB9A3EF45C6CB6A83473 /* Pods_rss_reader.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_rss_reader.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 267D322FE1464305FB81EFE4 /* Pods-rss-reader.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rss-reader.release.xcconfig"; path = "Target Support Files/Pods-rss-reader/Pods-rss-reader.release.xcconfig"; sourceTree = "<group>"; }; + 86374D8D9304CB1683087721 /* Pods-rss-reader.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-rss-reader.debug.xcconfig"; path = "Target Support Files/Pods-rss-reader/Pods-rss-reader.debug.xcconfig"; sourceTree = "<group>"; }; + A23745302300137E00A5416B /* Feed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Feed.swift; sourceTree = "<group>"; }; + A23745322300245600A5416B /* FeedDataObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedDataObject.swift; sourceTree = "<group>"; }; + A237453423002A4700A5416B /* ArticleDataObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleDataObject.swift; sourceTree = "<group>"; }; + A237453623003E5300A5416B /* ColorsManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorsManager.swift; sourceTree = "<group>"; }; + A237453823004FE300A5416B /* SourceEntity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceEntity.swift; sourceTree = "<group>"; }; + A27AAC6D22FED3DF001E069F /* DetailedSourceViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailedSourceViewController.swift; sourceTree = "<group>"; }; + A27AAC6F22FED3F0001E069F /* AddSourceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddSourceViewModel.swift; sourceTree = "<group>"; }; + A27AAC7122FED40B001E069F /* SourceListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListViewController.swift; sourceTree = "<group>"; }; + A27AAC7322FED41E001E069F /* SourceListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListViewModel.swift; sourceTree = "<group>"; }; + A27AAC7522FED42E001E069F /* ArticleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewController.swift; sourceTree = "<group>"; }; + A27AAC7722FED43A001E069F /* ArticleViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewModel.swift; sourceTree = "<group>"; }; + A27AAC7922FED448001E069F /* FeedListViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewController.swift; sourceTree = "<group>"; }; + A27AAC7B22FED454001E069F /* FeedListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedListViewModel.swift; sourceTree = "<group>"; }; + A27AAC7D22FEE333001E069F /* UserDefaultsStorageManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserDefaultsStorageManager.swift; sourceTree = "<group>"; }; + A27AAC7F22FEE451001E069F /* RSSNetworkManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSNetworkManager.swift; sourceTree = "<group>"; }; + A27AAC8522FEF5F3001E069F /* SourceCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCell.swift; sourceTree = "<group>"; }; + A27AAC8822FF0185001E069F /* ArticleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleCell.swift; sourceTree = "<group>"; }; + A27AAC8A22FF065E001E069F /* ArcticleCellViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArcticleCellViewModel.swift; sourceTree = "<group>"; }; + A27AAC8D22FF0959001E069F /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = "<group>"; }; + A27AAC8F22FF1C18001E069F /* SourceManagerProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceManagerProtocols.swift; sourceTree = "<group>"; }; + A27AAC9122FF1EEA001E069F /* ChangeSourceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChangeSourceViewModel.swift; sourceTree = "<group>"; }; + A27AAC9322FF23B3001E069F /* DetailedSourceViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailedSourceViewModel.swift; sourceTree = "<group>"; }; + A27AAC9622FF3191001E069F /* SourceDataObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceDataObject.swift; sourceTree = "<group>"; }; + A27AAC9822FFF0C4001E069F /* Alert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Alert.swift; sourceTree = "<group>"; }; + A27AAC9B2300055B001E069F /* Article.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Article.swift; sourceTree = "<group>"; }; + A2A934EA22FEAE3D003B6A82 /* rss-reader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "rss-reader.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + A2A934ED22FEAE3D003B6A82 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; + A2A934F422FEAE42003B6A82 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; + A2A934F722FEAE42003B6A82 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; + A2A934F922FEAE42003B6A82 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; + A2A934FF22FEB18E003B6A82 /* AppCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppCoordinator.swift; sourceTree = "<group>"; }; + A2A9350122FEB745003B6A82 /* CoordinatorProtocols.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoordinatorProtocols.swift; sourceTree = "<group>"; }; + A2A9350622FEC35B003B6A82 /* FeedFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedFlowCoordinator.swift; sourceTree = "<group>"; }; + A2A9350822FEC396003B6A82 /* SourceListFlowCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListFlowCoordinator.swift; sourceTree = "<group>"; }; + A2A9350A22FECB84003B6A82 /* RSSSourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RSSSourceManager.swift; sourceTree = "<group>"; }; + A2A9350C22FECBE2003B6A82 /* FeedStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = FeedStoryboard.storyboard; sourceTree = "<group>"; }; + A2A9350E22FECBF7003B6A82 /* SourceStoryboard.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = SourceStoryboard.storyboard; sourceTree = "<group>"; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + A2A934E722FEAE3D003B6A82 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 77EA2A39EAA703E04968E379 /* Pods_rss_reader.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 47B9E298A64631E9BB7CF924 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 0CD6FB9A3EF45C6CB6A83473 /* Pods_rss_reader.framework */, + ); + name = Frameworks; + sourceTree = "<group>"; + }; + A27AAC8322FEE83F001E069F /* Services */ = { + isa = PBXGroup; + children = ( + A27AAC8F22FF1C18001E069F /* SourceManagerProtocols.swift */, + A2A9350A22FECB84003B6A82 /* RSSSourceManager.swift */, + A27AAC7D22FEE333001E069F /* UserDefaultsStorageManager.swift */, + A27AAC7F22FEE451001E069F /* RSSNetworkManager.swift */, + ); + path = Services; + sourceTree = "<group>"; + }; + A27AAC8422FEF5CF001E069F /* Cells */ = { + isa = PBXGroup; + children = ( + A27AAC8522FEF5F3001E069F /* SourceCell.swift */, + ); + path = Cells; + sourceTree = "<group>"; + }; + A27AAC8722FF0177001E069F /* Cells */ = { + isa = PBXGroup; + children = ( + A27AAC8822FF0185001E069F /* ArticleCell.swift */, + A27AAC8A22FF065E001E069F /* ArcticleCellViewModel.swift */, + ); + path = Cells; + sourceTree = "<group>"; + }; + A27AAC8C22FF088B001E069F /* Model objects */ = { + isa = PBXGroup; + children = ( + A27AAC8D22FF0959001E069F /* Source.swift */, + A23745302300137E00A5416B /* Feed.swift */, + A27AAC9B2300055B001E069F /* Article.swift */, + ); + path = "Model objects"; + sourceTree = "<group>"; + }; + A27AAC9522FF3177001E069F /* Data objects */ = { + isa = PBXGroup; + children = ( + A27AAC9622FF3191001E069F /* SourceDataObject.swift */, + A23745322300245600A5416B /* FeedDataObject.swift */, + A237453423002A4700A5416B /* ArticleDataObject.swift */, + A237453823004FE300A5416B /* SourceEntity.swift */, + ); + path = "Data objects"; + sourceTree = "<group>"; + }; + A27AAC9A22FFF0C8001E069F /* Utils */ = { + isa = PBXGroup; + children = ( + A27AAC9822FFF0C4001E069F /* Alert.swift */, + A237453623003E5300A5416B /* ColorsManager.swift */, + ); + path = Utils; + sourceTree = "<group>"; + }; + A2A934E122FEAE3D003B6A82 = { + isa = PBXGroup; + children = ( + A2A934EC22FEAE3D003B6A82 /* rss-reader */, + A2A934EB22FEAE3D003B6A82 /* Products */, + C6884FDB7C25277F3AADA4DA /* Pods */, + 47B9E298A64631E9BB7CF924 /* Frameworks */, + ); + sourceTree = "<group>"; + }; + A2A934EB22FEAE3D003B6A82 /* Products */ = { + isa = PBXGroup; + children = ( + A2A934EA22FEAE3D003B6A82 /* rss-reader.app */, + ); + name = Products; + sourceTree = "<group>"; + }; + A2A934EC22FEAE3D003B6A82 /* rss-reader */ = { + isa = PBXGroup; + children = ( + A27AAC9A22FFF0C8001E069F /* Utils */, + A27AAC9522FF3177001E069F /* Data objects */, + A27AAC8C22FF088B001E069F /* Model objects */, + A27AAC8322FEE83F001E069F /* Services */, + A2A9350322FEBE90003B6A82 /* Screens */, + A2A934ED22FEAE3D003B6A82 /* AppDelegate.swift */, + A2A934FF22FEB18E003B6A82 /* AppCoordinator.swift */, + A2A9350122FEB745003B6A82 /* CoordinatorProtocols.swift */, + A2A934F422FEAE42003B6A82 /* Assets.xcassets */, + A2A934F622FEAE42003B6A82 /* LaunchScreen.storyboard */, + A2A934F922FEAE42003B6A82 /* Info.plist */, + ); + path = "rss-reader"; + sourceTree = "<group>"; + }; + A2A9350322FEBE90003B6A82 /* Screens */ = { + isa = PBXGroup; + children = ( + A2A9350522FEC304003B6A82 /* Sources */, + A2A9350422FEBE96003B6A82 /* Feed */, + ); + path = Screens; + sourceTree = "<group>"; + }; + A2A9350422FEBE96003B6A82 /* Feed */ = { + isa = PBXGroup; + children = ( + A2A9351122FED0EB003B6A82 /* Article */, + A2A9351022FED0AE003B6A82 /* Feed list */, + A2A9350622FEC35B003B6A82 /* FeedFlowCoordinator.swift */, + A2A9350C22FECBE2003B6A82 /* FeedStoryboard.storyboard */, + ); + path = Feed; + sourceTree = "<group>"; + }; + A2A9350522FEC304003B6A82 /* Sources */ = { + isa = PBXGroup; + children = ( + A2A9351322FED0F8003B6A82 /* Detailed source */, + A2A9351222FED0F1003B6A82 /* Source list */, + A2A9350822FEC396003B6A82 /* SourceListFlowCoordinator.swift */, + A2A9350E22FECBF7003B6A82 /* SourceStoryboard.storyboard */, + ); + path = Sources; + sourceTree = "<group>"; + }; + A2A9351022FED0AE003B6A82 /* Feed list */ = { + isa = PBXGroup; + children = ( + A27AAC8722FF0177001E069F /* Cells */, + A27AAC7922FED448001E069F /* FeedListViewController.swift */, + A27AAC7B22FED454001E069F /* FeedListViewModel.swift */, + ); + path = "Feed list"; + sourceTree = "<group>"; + }; + A2A9351122FED0EB003B6A82 /* Article */ = { + isa = PBXGroup; + children = ( + A27AAC7522FED42E001E069F /* ArticleViewController.swift */, + A27AAC7722FED43A001E069F /* ArticleViewModel.swift */, + ); + path = Article; + sourceTree = "<group>"; + }; + A2A9351222FED0F1003B6A82 /* Source list */ = { + isa = PBXGroup; + children = ( + A27AAC8422FEF5CF001E069F /* Cells */, + A27AAC7122FED40B001E069F /* SourceListViewController.swift */, + A27AAC7322FED41E001E069F /* SourceListViewModel.swift */, + ); + path = "Source list"; + sourceTree = "<group>"; + }; + A2A9351322FED0F8003B6A82 /* Detailed source */ = { + isa = PBXGroup; + children = ( + A27AAC6D22FED3DF001E069F /* DetailedSourceViewController.swift */, + A27AAC9322FF23B3001E069F /* DetailedSourceViewModel.swift */, + A27AAC6F22FED3F0001E069F /* AddSourceViewModel.swift */, + A27AAC9122FF1EEA001E069F /* ChangeSourceViewModel.swift */, + ); + path = "Detailed source"; + sourceTree = "<group>"; + }; + C6884FDB7C25277F3AADA4DA /* Pods */ = { + isa = PBXGroup; + children = ( + 86374D8D9304CB1683087721 /* Pods-rss-reader.debug.xcconfig */, + 267D322FE1464305FB81EFE4 /* Pods-rss-reader.release.xcconfig */, + ); + path = Pods; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + A2A934E922FEAE3D003B6A82 /* rss-reader */ = { + isa = PBXNativeTarget; + buildConfigurationList = A2A934FC22FEAE42003B6A82 /* Build configuration list for PBXNativeTarget "rss-reader" */; + buildPhases = ( + FE2E09002541619D6D845DB1 /* [CP] Check Pods Manifest.lock */, + A2A934E622FEAE3D003B6A82 /* Sources */, + A2A934E722FEAE3D003B6A82 /* Frameworks */, + A2A934E822FEAE3D003B6A82 /* Resources */, + 0835C56550A965464D020C73 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "rss-reader"; + productName = "rss-reader"; + productReference = A2A934EA22FEAE3D003B6A82 /* rss-reader.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + A2A934E222FEAE3D003B6A82 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1020; + LastUpgradeCheck = 1020; + ORGANIZATIONNAME = kuleshov; + TargetAttributes = { + A2A934E922FEAE3D003B6A82 = { + CreatedOnToolsVersion = 10.2.1; + }; + }; + }; + buildConfigurationList = A2A934E522FEAE3D003B6A82 /* Build configuration list for PBXProject "rss-reader" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = A2A934E122FEAE3D003B6A82; + productRefGroup = A2A934EB22FEAE3D003B6A82 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + A2A934E922FEAE3D003B6A82 /* rss-reader */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + A2A934E822FEAE3D003B6A82 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A2A9350F22FECBF7003B6A82 /* SourceStoryboard.storyboard in Resources */, + A2A9350D22FECBE2003B6A82 /* FeedStoryboard.storyboard in Resources */, + A2A934F822FEAE42003B6A82 /* LaunchScreen.storyboard in Resources */, + A2A934F522FEAE42003B6A82 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 0835C56550A965464D020C73 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-rss-reader/Pods-rss-reader-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + FE2E09002541619D6D845DB1 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-rss-reader-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + A2A934E622FEAE3D003B6A82 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A237453923004FE300A5416B /* SourceEntity.swift in Sources */, + A27AAC9C2300055B001E069F /* Article.swift in Sources */, + A27AAC9022FF1C18001E069F /* SourceManagerProtocols.swift in Sources */, + A2A9350722FEC35B003B6A82 /* FeedFlowCoordinator.swift in Sources */, + A27AAC7422FED41E001E069F /* SourceListViewModel.swift in Sources */, + A27AAC9922FFF0C4001E069F /* Alert.swift in Sources */, + A27AAC9222FF1EEA001E069F /* ChangeSourceViewModel.swift in Sources */, + A27AAC7E22FEE333001E069F /* UserDefaultsStorageManager.swift in Sources */, + A27AAC7C22FED454001E069F /* FeedListViewModel.swift in Sources */, + A27AAC7022FED3F0001E069F /* AddSourceViewModel.swift in Sources */, + A27AAC7A22FED448001E069F /* FeedListViewController.swift in Sources */, + A2A9350022FEB18E003B6A82 /* AppCoordinator.swift in Sources */, + A2A9350922FEC396003B6A82 /* SourceListFlowCoordinator.swift in Sources */, + A27AAC8E22FF0959001E069F /* Source.swift in Sources */, + A237453723003E5300A5416B /* ColorsManager.swift in Sources */, + A27AAC7622FED42E001E069F /* ArticleViewController.swift in Sources */, + A27AAC8622FEF5F3001E069F /* SourceCell.swift in Sources */, + A2A9350222FEB745003B6A82 /* CoordinatorProtocols.swift in Sources */, + A237453523002A4700A5416B /* ArticleDataObject.swift in Sources */, + A27AAC7222FED40B001E069F /* SourceListViewController.swift in Sources */, + A27AAC9722FF3191001E069F /* SourceDataObject.swift in Sources */, + A2A9350B22FECB84003B6A82 /* RSSSourceManager.swift in Sources */, + A23745312300137E00A5416B /* Feed.swift in Sources */, + A27AAC8022FEE451001E069F /* RSSNetworkManager.swift in Sources */, + A27AAC8922FF0185001E069F /* ArticleCell.swift in Sources */, + A27AAC7822FED43A001E069F /* ArticleViewModel.swift in Sources */, + A27AAC9422FF23B3001E069F /* DetailedSourceViewModel.swift in Sources */, + A23745332300245600A5416B /* FeedDataObject.swift in Sources */, + A27AAC6E22FED3DF001E069F /* DetailedSourceViewController.swift in Sources */, + A2A934EE22FEAE3D003B6A82 /* AppDelegate.swift in Sources */, + A27AAC8B22FF065E001E069F /* ArcticleCellViewModel.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + A2A934F622FEAE42003B6A82 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + A2A934F722FEAE42003B6A82 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = "<group>"; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + A2A934FA22FEAE42003B6A82 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + A2A934FB22FEAE42003B6A82 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + A2A934FD22FEAE42003B6A82 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 86374D8D9304CB1683087721 /* Pods-rss-reader.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "rss-reader/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.kuleshov.rss-reader"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Debug; + }; + A2A934FE22FEAE42003B6A82 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 267D322FE1464305FB81EFE4 /* Pods-rss-reader.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = "rss-reader/Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "com.kuleshov.rss-reader"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = 1; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + A2A934E522FEAE3D003B6A82 /* Build configuration list for PBXProject "rss-reader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A2A934FA22FEAE42003B6A82 /* Debug */, + A2A934FB22FEAE42003B6A82 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + A2A934FC22FEAE42003B6A82 /* Build configuration list for PBXNativeTarget "rss-reader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + A2A934FD22FEAE42003B6A82 /* Debug */, + A2A934FE22FEAE42003B6A82 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = A2A934E222FEAE3D003B6A82 /* Project object */; +} diff --git a/rss-reader.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/rss-reader.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..fbfc58e --- /dev/null +++ b/rss-reader.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "self:rss-reader.xcodeproj"> + </FileRef> +</Workspace> diff --git a/rss-reader.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/rss-reader.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/rss-reader.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/rss-reader.xcworkspace/contents.xcworkspacedata b/rss-reader.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..ce5f94a --- /dev/null +++ b/rss-reader.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "group:rss-reader.xcodeproj"> + </FileRef> + <FileRef + location = "group:Pods/Pods.xcodeproj"> + </FileRef> +</Workspace> diff --git a/rss-reader.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/rss-reader.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/rss-reader.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>IDEDidComputeMac32BitWarning</key> + <true/> +</dict> +</plist> diff --git a/rss-reader/AppCoordinator.swift b/rss-reader/AppCoordinator.swift new file mode 100644 index 0000000..492d866 --- /dev/null +++ b/rss-reader/AppCoordinator.swift @@ -0,0 +1,45 @@ +// +// AppCoordinator.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +class AppCoordinator: Coordinator { + + private let tabBarController: UITabBarController + + private let sourceListCoordinator: SourceListFlowCoordinator + private let feedCoordinator: FeedFlowCoordinator + + private let sourceManager: SourceManager + + init() { + + //setup services + let storageManager = UserDefaultsStorageManager() + let networkManager = RSSNetworkManager() + + sourceManager = RSSSourceManager(storageManager: storageManager, networkManager: networkManager) + + + //setup flow + sourceListCoordinator = SourceListFlowCoordinator(sourceManager: sourceManager) + feedCoordinator = FeedFlowCoordinator(sourceManager: sourceManager) + + tabBarController = UITabBarController() + tabBarController.viewControllers = [feedCoordinator.rootViewController, + sourceListCoordinator.rootViewController] + } + + var rootViewController: UIViewController { + return tabBarController + } + + func finished() { + fatalError("App Coordinator can't finish root flows") + } +} diff --git a/rss-reader/AppDelegate.swift b/rss-reader/AppDelegate.swift new file mode 100644 index 0000000..a2ed29a --- /dev/null +++ b/rss-reader/AppDelegate.swift @@ -0,0 +1,55 @@ +// +// AppDelegate.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +@UIApplicationMain +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + private var appCoordinator: AppCoordinator? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + let screenSize = UIScreen.main.bounds.size + window = UIWindow(frame: .init(origin: .zero, size: screenSize)) + + appCoordinator = AppCoordinator() + window?.rootViewController = appCoordinator?.rootViewController + window?.makeKeyAndVisible() + + ColorsManager.setupColors() + + return true + } + + func applicationWillResignActive(_ application: UIApplication) { + // Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state. + // Use this method to pause ongoing tasks, disable timers, and invalidate graphics rendering callbacks. Games should use this method to pause the game. + } + + func applicationDidEnterBackground(_ application: UIApplication) { + // Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later. + // If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits. + } + + func applicationWillEnterForeground(_ application: UIApplication) { + // Called as part of the transition from the background to the active state; here you can undo many of the changes made on entering the background. + } + + func applicationDidBecomeActive(_ application: UIApplication) { + // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface. + } + + func applicationWillTerminate(_ application: UIApplication) { + // Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:. + } + + +} + diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/Contents.json b/rss-reader/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..1414c4e --- /dev/null +++ b/rss-reader/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,62 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "rss-icon-20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "rss-icon-20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "rss-icon-29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "rss-icon-29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "rss-icon-40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "rss-icon-40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "rss-icon-60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "rss-icon-60@3x.png", + "scale" : "3x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "rss-icon-1024.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-1024.png b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-1024.png new file mode 100644 index 0000000..102ad07 Binary files /dev/null and b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-1024.png differ diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-20@2x.png b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-20@2x.png new file mode 100644 index 0000000..8944892 Binary files /dev/null and b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-20@2x.png differ diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-20@3x.png b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-20@3x.png new file mode 100644 index 0000000..73d61b4 Binary files /dev/null and b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-20@3x.png differ diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-29@2x.png b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-29@2x.png new file mode 100644 index 0000000..044172a Binary files /dev/null and b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-29@2x.png differ diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-29@3x.png b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-29@3x.png new file mode 100644 index 0000000..063f9e1 Binary files /dev/null and b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-29@3x.png differ diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-40@2x.png b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-40@2x.png new file mode 100644 index 0000000..70cfa32 Binary files /dev/null and b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-40@2x.png differ diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-40@3x.png b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-40@3x.png new file mode 100644 index 0000000..619a3af Binary files /dev/null and b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-40@3x.png differ diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-60@2x.png b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-60@2x.png new file mode 100644 index 0000000..619a3af Binary files /dev/null and b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-60@2x.png differ diff --git a/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-60@3x.png b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-60@3x.png new file mode 100644 index 0000000..56f3460 Binary files /dev/null and b/rss-reader/Assets.xcassets/AppIcon.appiconset/rss-icon-60@3x.png differ diff --git a/rss-reader/Assets.xcassets/Contents.json b/rss-reader/Assets.xcassets/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/rss-reader/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/rss-reader/Assets.xcassets/launch screen/Contents.json b/rss-reader/Assets.xcassets/launch screen/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/rss-reader/Assets.xcassets/launch screen/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/rss-reader/Assets.xcassets/launch screen/launch_image.imageset/Contents.json b/rss-reader/Assets.xcassets/launch screen/launch_image.imageset/Contents.json new file mode 100644 index 0000000..291aac6 --- /dev/null +++ b/rss-reader/Assets.xcassets/launch screen/launch_image.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "rss-icon.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/rss-reader/Assets.xcassets/launch screen/launch_image.imageset/rss-icon.png b/rss-reader/Assets.xcassets/launch screen/launch_image.imageset/rss-icon.png new file mode 100644 index 0000000..e0e8640 Binary files /dev/null and b/rss-reader/Assets.xcassets/launch screen/launch_image.imageset/rss-icon.png differ diff --git a/rss-reader/Assets.xcassets/placeholders/Contents.json b/rss-reader/Assets.xcassets/placeholders/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/rss-reader/Assets.xcassets/placeholders/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/rss-reader/Assets.xcassets/placeholders/article_placeholder.imageset/Contents.json b/rss-reader/Assets.xcassets/placeholders/article_placeholder.imageset/Contents.json new file mode 100644 index 0000000..9fc8426 --- /dev/null +++ b/rss-reader/Assets.xcassets/placeholders/article_placeholder.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "placeholder.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/rss-reader/Assets.xcassets/placeholders/article_placeholder.imageset/placeholder.png b/rss-reader/Assets.xcassets/placeholders/article_placeholder.imageset/placeholder.png new file mode 100644 index 0000000..a274592 Binary files /dev/null and b/rss-reader/Assets.xcassets/placeholders/article_placeholder.imageset/placeholder.png differ diff --git a/rss-reader/Assets.xcassets/tab bar items/Contents.json b/rss-reader/Assets.xcassets/tab bar items/Contents.json new file mode 100644 index 0000000..da4a164 --- /dev/null +++ b/rss-reader/Assets.xcassets/tab bar items/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/rss-reader/Assets.xcassets/tab bar items/feed.imageset/Contents.json b/rss-reader/Assets.xcassets/tab bar items/feed.imageset/Contents.json new file mode 100644 index 0000000..5e9f975 --- /dev/null +++ b/rss-reader/Assets.xcassets/tab bar items/feed.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icons8-news-feed-25.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icons8-news-feed-50.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "icons8-news-feed-96.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/rss-reader/Assets.xcassets/tab bar items/feed.imageset/icons8-news-feed-25.png b/rss-reader/Assets.xcassets/tab bar items/feed.imageset/icons8-news-feed-25.png new file mode 100644 index 0000000..ec488b5 Binary files /dev/null and b/rss-reader/Assets.xcassets/tab bar items/feed.imageset/icons8-news-feed-25.png differ diff --git a/rss-reader/Assets.xcassets/tab bar items/feed.imageset/icons8-news-feed-50.png b/rss-reader/Assets.xcassets/tab bar items/feed.imageset/icons8-news-feed-50.png new file mode 100644 index 0000000..30f6856 Binary files /dev/null and b/rss-reader/Assets.xcassets/tab bar items/feed.imageset/icons8-news-feed-50.png differ diff --git a/rss-reader/Assets.xcassets/tab bar items/feed.imageset/icons8-news-feed-96.png b/rss-reader/Assets.xcassets/tab bar items/feed.imageset/icons8-news-feed-96.png new file mode 100644 index 0000000..12eca01 Binary files /dev/null and b/rss-reader/Assets.xcassets/tab bar items/feed.imageset/icons8-news-feed-96.png differ diff --git a/rss-reader/Assets.xcassets/tab bar items/sources.imageset/Contents.json b/rss-reader/Assets.xcassets/tab bar items/sources.imageset/Contents.json new file mode 100644 index 0000000..d18264f --- /dev/null +++ b/rss-reader/Assets.xcassets/tab bar items/sources.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "icons8-settings-25.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "icons8-settings-50.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "icons8-settings-150.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/rss-reader/Assets.xcassets/tab bar items/sources.imageset/icons8-settings-150.png b/rss-reader/Assets.xcassets/tab bar items/sources.imageset/icons8-settings-150.png new file mode 100644 index 0000000..bd525ab Binary files /dev/null and b/rss-reader/Assets.xcassets/tab bar items/sources.imageset/icons8-settings-150.png differ diff --git a/rss-reader/Assets.xcassets/tab bar items/sources.imageset/icons8-settings-25.png b/rss-reader/Assets.xcassets/tab bar items/sources.imageset/icons8-settings-25.png new file mode 100644 index 0000000..d12abc1 Binary files /dev/null and b/rss-reader/Assets.xcassets/tab bar items/sources.imageset/icons8-settings-25.png differ diff --git a/rss-reader/Assets.xcassets/tab bar items/sources.imageset/icons8-settings-50.png b/rss-reader/Assets.xcassets/tab bar items/sources.imageset/icons8-settings-50.png new file mode 100644 index 0000000..2444684 Binary files /dev/null and b/rss-reader/Assets.xcassets/tab bar items/sources.imageset/icons8-settings-50.png differ diff --git a/rss-reader/Base.lproj/LaunchScreen.storyboard b/rss-reader/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..94955f3 --- /dev/null +++ b/rss-reader/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM"> + <device id="retina6_1" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--View Controller--> + <scene sceneID="EHf-IW-A2E"> + <objects> + <viewController id="01J-lp-oVM" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3"> + <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="launch_image" translatesAutoresizingMaskIntoConstraints="NO" id="Dz9-fb-tSo"> + <rect key="frame" x="124" y="365" width="166" height="166"/> + <constraints> + <constraint firstAttribute="width" secondItem="Dz9-fb-tSo" secondAttribute="height" id="8hI-oF-fK9"/> + </constraints> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="RSS reader" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="llz-Od-trZ"> + <rect key="frame" x="148" y="551" width="118" height="30"/> + <fontDescription key="fontDescription" type="system" weight="light" pointSize="25"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + <constraints> + <constraint firstItem="llz-Od-trZ" firstAttribute="top" secondItem="Dz9-fb-tSo" secondAttribute="bottom" constant="20" id="3mZ-FZ-e33"/> + <constraint firstItem="Dz9-fb-tSo" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="Dd3-aj-AHe"/> + <constraint firstItem="llz-Od-trZ" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="JSQ-VQ-b6a"/> + <constraint firstItem="Dz9-fb-tSo" firstAttribute="width" secondItem="6Tk-OE-BBY" secondAttribute="width" multiplier="0.4" id="Kkr-c6-Xae"/> + <constraint firstItem="Dz9-fb-tSo" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="Xer-Oz-pDm"/> + </constraints> + <viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/> + </view> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="53" y="375"/> + </scene> + </scenes> + <resources> + <image name="launch_image" width="341.33334350585938" height="341.33334350585938"/> + </resources> +</document> diff --git a/rss-reader/CoordinatorProtocols.swift b/rss-reader/CoordinatorProtocols.swift new file mode 100644 index 0000000..95b51c7 --- /dev/null +++ b/rss-reader/CoordinatorProtocols.swift @@ -0,0 +1,17 @@ +// +// Coordinator protocols.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +protocol CoordinationDelegate: class { + func finished() +} + +protocol Coordinator: CoordinationDelegate { + var rootViewController: UIViewController { get } +} diff --git a/rss-reader/Data objects/ArticleDataObject.swift b/rss-reader/Data objects/ArticleDataObject.swift new file mode 100644 index 0000000..2db252a --- /dev/null +++ b/rss-reader/Data objects/ArticleDataObject.swift @@ -0,0 +1,21 @@ +// +// ArticleDataObject.swift +// rss-reader +// +// Created by Daniil Kuleshov on 11/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +class ArticleDataObject { + let title: String + let descriptionText: String + let imageURL: URL? + + init(article: Article) { + title = article.title ?? "Default title" + descriptionText = article.descriptionText?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "Default description" + imageURL = article.imageURL + } +} diff --git a/rss-reader/Data objects/FeedDataObject.swift b/rss-reader/Data objects/FeedDataObject.swift new file mode 100644 index 0000000..0e0361b --- /dev/null +++ b/rss-reader/Data objects/FeedDataObject.swift @@ -0,0 +1,24 @@ +// +// FeedDataObject.swift +// rss-reader +// +// Created by Daniil Kuleshov on 11/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +class FeedDataObject { + + private(set) var identifier: String? + let name: String + let imageURL: URL? + let articles: [ArticleDataObject] + + init(source: Source) { + identifier = source.identifier + self.name = source.name + self.imageURL = source.feed?.imageURL + self.articles = (source.feed?.articles ?? []).map { ArticleDataObject(article: $0) } + } +} diff --git a/rss-reader/Data objects/SourceDataObject.swift b/rss-reader/Data objects/SourceDataObject.swift new file mode 100644 index 0000000..835f9fb --- /dev/null +++ b/rss-reader/Data objects/SourceDataObject.swift @@ -0,0 +1,33 @@ +// +// SourceDataObject.swift +// rss-reader +// +// Created by Daniil Kuleshov on 11/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +class SourceDataObject { + + private(set) var identifier: String? + var name: String + var sourceURL: URL + + + init(source: Source) { + identifier = source.identifier + self.name = source.name + self.sourceURL = source.sourceURL + } + + init(name: String, sourceURL: URL) { + self.name = name + self.sourceURL = sourceURL + } + + func update(name: String, sourceURL: URL) { + self.name = name + self.sourceURL = sourceURL + } +} diff --git a/rss-reader/Data objects/SourceEntity.swift b/rss-reader/Data objects/SourceEntity.swift new file mode 100644 index 0000000..8bc2451 --- /dev/null +++ b/rss-reader/Data objects/SourceEntity.swift @@ -0,0 +1,22 @@ +// +// SourceEntity.swift +// rss-reader +// +// Created by Daniil Kuleshov on 11/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +struct SourceEntity: Codable { + + let identifier: String + let name: String + let sourceURL: URL + + init(source: Source) { + identifier = source.identifier + self.name = source.name + self.sourceURL = source.sourceURL + } +} diff --git a/rss-reader/Info.plist b/rss-reader/Info.plist new file mode 100644 index 0000000..b1e09c9 --- /dev/null +++ b/rss-reader/Info.plist @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CFBundleDevelopmentRegion</key> + <string>$(DEVELOPMENT_LANGUAGE)</string> + <key>CFBundleDisplayName</key> + <string>RSS Reader</string> + <key>CFBundleExecutable</key> + <string>$(EXECUTABLE_NAME)</string> + <key>CFBundleIdentifier</key> + <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> + <key>CFBundleInfoDictionaryVersion</key> + <string>6.0</string> + <key>CFBundleName</key> + <string>$(PRODUCT_NAME)</string> + <key>CFBundlePackageType</key> + <string>APPL</string> + <key>CFBundleShortVersionString</key> + <string>1.0</string> + <key>CFBundleVersion</key> + <string>1</string> + <key>LSRequiresIPhoneOS</key> + <true/> + <key>UILaunchStoryboardName</key> + <string>LaunchScreen</string> + <key>UIRequiredDeviceCapabilities</key> + <array> + <string>armv7</string> + </array> + <key>UISupportedInterfaceOrientations</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + </array> + <key>UISupportedInterfaceOrientations~ipad</key> + <array> + <string>UIInterfaceOrientationPortrait</string> + <string>UIInterfaceOrientationPortraitUpsideDown</string> + <string>UIInterfaceOrientationLandscapeLeft</string> + <string>UIInterfaceOrientationLandscapeRight</string> + </array> +</dict> +</plist> diff --git a/rss-reader/Model objects/Article.swift b/rss-reader/Model objects/Article.swift new file mode 100644 index 0000000..ac473cf --- /dev/null +++ b/rss-reader/Model objects/Article.swift @@ -0,0 +1,22 @@ +// +// Article.swift +// rss-reader +// +// Created by Daniil Kuleshov on 11/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation +import FeedKit + +class Article { + let title: String? + let descriptionText: String? + let imageURL: URL? = nil + + init(feedItem: RSSFeedItem) { + title = feedItem.title + descriptionText = feedItem.description + //TODO: Find way to get image from article + } +} diff --git a/rss-reader/Model objects/Feed.swift b/rss-reader/Model objects/Feed.swift new file mode 100644 index 0000000..aee6871 --- /dev/null +++ b/rss-reader/Model objects/Feed.swift @@ -0,0 +1,27 @@ +// +// Feed.swift +// rss-reader +// +// Created by Daniil Kuleshov on 11/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation +import FeedKit + +class Feed { + let name: String? + let imageURL: URL? + + let articles: [Article]? + + init(rssFeed: RSSFeed) { + name = rssFeed.title + if let urlString = rssFeed.image?.url { + imageURL = URL(string: urlString) + } else { + imageURL = nil + } + articles = rssFeed.items?.map { Article(feedItem: $0) } + } +} diff --git a/rss-reader/Model objects/Source.swift b/rss-reader/Model objects/Source.swift new file mode 100644 index 0000000..ac00a49 --- /dev/null +++ b/rss-reader/Model objects/Source.swift @@ -0,0 +1,40 @@ +// +// Source.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +class Source { + + let identifier: String + private(set) var name: String + private(set) var sourceURL: URL + private(set) var feed: Feed? + + init(dataObject: SourceDataObject) { + identifier = UUID().uuidString + self.name = dataObject.name + self.sourceURL = dataObject.sourceURL + } + + init(entity: SourceEntity) { + self.identifier = entity.identifier + self.name = entity.name + self.sourceURL = entity.sourceURL + } + + func update(dataObject: SourceDataObject) { + self.name = dataObject.name + self.sourceURL = dataObject.sourceURL + } + + func update(feed: Feed) { + self.feed = feed + } + + +} diff --git a/rss-reader/Screens/Feed/Article/ArticleViewController.swift b/rss-reader/Screens/Feed/Article/ArticleViewController.swift new file mode 100644 index 0000000..0ac386c --- /dev/null +++ b/rss-reader/Screens/Feed/Article/ArticleViewController.swift @@ -0,0 +1,28 @@ +// +// ArticleViewController.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +class ArticleViewController: UIViewController { + + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var pictureImageView: UIImageView! + @IBOutlet weak var descriptionTextView: UITextView! + + var viewModel: ArticleViewModel! + + override func viewDidLoad() { + super.viewDidLoad() + + titleLabel.text = viewModel.title() + descriptionTextView.text = viewModel.description() + pictureImageView.kf.setImage(with: viewModel.imageURL(), placeholder: UIImage(named: "article_placeholder")!) + } + + +} diff --git a/rss-reader/Screens/Feed/Article/ArticleViewModel.swift b/rss-reader/Screens/Feed/Article/ArticleViewModel.swift new file mode 100644 index 0000000..1538f55 --- /dev/null +++ b/rss-reader/Screens/Feed/Article/ArticleViewModel.swift @@ -0,0 +1,30 @@ +// +// ArticleViewModel.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +class ArticleViewModel { + + let articleDataObject: ArticleDataObject + + init(dataObject: ArticleDataObject) { + articleDataObject = dataObject + } + + func title() -> String { + return articleDataObject.title + } + + func description() -> String { + return articleDataObject.descriptionText + } + + func imageURL() -> URL? { + return articleDataObject.imageURL + } +} diff --git a/rss-reader/Screens/Feed/Feed list/Cells/ArcticleCellViewModel.swift b/rss-reader/Screens/Feed/Feed list/Cells/ArcticleCellViewModel.swift new file mode 100644 index 0000000..dd3cf9e --- /dev/null +++ b/rss-reader/Screens/Feed/Feed list/Cells/ArcticleCellViewModel.swift @@ -0,0 +1,14 @@ +// +// ArcticleCellViewModel.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +struct ArticleCellViewModel { + let title, descriptiontext: String + let imageURL: URL? +} diff --git a/rss-reader/Screens/Feed/Feed list/Cells/ArticleCell.swift b/rss-reader/Screens/Feed/Feed list/Cells/ArticleCell.swift new file mode 100644 index 0000000..bf0e75e --- /dev/null +++ b/rss-reader/Screens/Feed/Feed list/Cells/ArticleCell.swift @@ -0,0 +1,24 @@ +// +// ArticleCell.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit +import Kingfisher + +class ArticleCell: UITableViewCell { + + @IBOutlet private weak var articleImageView: UIImageView! + @IBOutlet private weak var titleLabel: UILabel! + @IBOutlet private weak var descriptionLabel: UILabel! + + func setAppearance(viewModel: ArticleCellViewModel) { + titleLabel.text = viewModel.title + descriptionLabel.text = viewModel.descriptiontext + articleImageView.kf.setImage(with: viewModel.imageURL, placeholder: UIImage(named: "article_placeholder")!) + } + +} diff --git a/rss-reader/Screens/Feed/Feed list/FeedListViewController.swift b/rss-reader/Screens/Feed/Feed list/FeedListViewController.swift new file mode 100644 index 0000000..4fcd208 --- /dev/null +++ b/rss-reader/Screens/Feed/Feed list/FeedListViewController.swift @@ -0,0 +1,90 @@ +// +// FeedListViewController.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +class FeedListViewController: UIViewController { + + private let articleCellHeight: CGFloat = 150.0 + + private let articleCellIdentifier = "articleCell" + private let refreshControl = UIRefreshControl() + + @IBOutlet private weak var tableView: UITableView! + + var viewModel: FeedListViewModel! + + override func viewDidLoad() { + super.viewDidLoad() + + tableView.tableFooterView = UIView() + refreshControl.addTarget(self, action: #selector(refreshAction), for: .valueChanged) + tableView.refreshControl = refreshControl + + refreshControl.beginRefreshing() + refreshAction() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + viewModel.updateFeedDataObjects() + tableView.reloadData() + } + + //MARK: actions + + @objc func refreshAction() { + viewModel.fetchFeed { (errorString) in + self.refreshControl.endRefreshing() + self.viewModel.updateFeedDataObjects() + self.tableView.reloadData() + + if let errorString = errorString { + Alert.showErrorAlert(on: self, message: errorString) + } + } + } + +} + +extension FeedListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + viewModel.didTapArticleAt(indexPath: indexPath) + } + + func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + return articleCellHeight + } +} + +extension FeedListViewController: UITableViewDataSource { + + func numberOfSections(in tableView: UITableView) -> Int { + return viewModel.numberOfSources() + } + + func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + return viewModel.feedTitleWith(index: section) + } + + func sectionIndexTitles(for tableView: UITableView) -> [String]? { + return (viewModel.numberOfSources() > 1) ? viewModel.indicatorTitles() : nil + } + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.numberOfArticlesInSourceAt(index: section) + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: articleCellIdentifier, for: indexPath) as! ArticleCell + cell.setAppearance(viewModel: viewModel.articleCellViewModelAt(indexPath: indexPath)) + return cell + } + +} diff --git a/rss-reader/Screens/Feed/Feed list/FeedListViewModel.swift b/rss-reader/Screens/Feed/Feed list/FeedListViewModel.swift new file mode 100644 index 0000000..d261e0b --- /dev/null +++ b/rss-reader/Screens/Feed/Feed list/FeedListViewModel.swift @@ -0,0 +1,65 @@ +// +// FeedListViewModel.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +protocol FeedListViewModelDelegate: CoordinationDelegate { + func openArticleWith(dataObject: ArticleDataObject) +} + +class FeedListViewModel { + + private let indicatorTitleLength = 3 + + private let sourceManager: FeedRetrieving & FeedUpdatable + private var feedDataObjects: [FeedDataObject] + + weak var coordinationDelegate: FeedListViewModelDelegate? + + init(sourceManager: FeedRetrieving & FeedUpdatable) { + self.sourceManager = sourceManager + feedDataObjects = sourceManager.getFeedDataObjects() + } + + func fetchFeed(completion: @escaping (String?) -> ()) { + sourceManager.updateFeed { (error) in + completion(error?.description()) + } + } + + func updateFeedDataObjects() { + feedDataObjects = sourceManager.getFeedDataObjects() + } + + func numberOfSources() -> Int { + return feedDataObjects.count + } + + func numberOfArticlesInSourceAt(index: Int) -> Int { + return feedDataObjects[index].articles.count + } + + func articleCellViewModelAt(indexPath: IndexPath) -> ArticleCellViewModel { + let article = feedDataObjects[indexPath.section].articles[indexPath.row] + return ArticleCellViewModel(title: article.title, descriptiontext: article.descriptionText, imageURL: article.imageURL) + } + + func feedTitleWith(index: Int) -> String { + return feedDataObjects[index].name + } + + func indicatorTitles() -> [String] { + return feedDataObjects.map { String($0.name.uppercased().prefix(indicatorTitleLength)) } + } + + func didTapArticleAt(indexPath: IndexPath) { + let article = feedDataObjects[indexPath.section].articles[indexPath.row] + coordinationDelegate?.openArticleWith(dataObject: article) + } + +} diff --git a/rss-reader/Screens/Feed/FeedFlowCoordinator.swift b/rss-reader/Screens/Feed/FeedFlowCoordinator.swift new file mode 100644 index 0000000..8cb9aca --- /dev/null +++ b/rss-reader/Screens/Feed/FeedFlowCoordinator.swift @@ -0,0 +1,51 @@ +// +// FeedFlowCoordinator.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +class FeedFlowCoordinator: Coordinator { + + private let feedNavigationController: UINavigationController + + private let feedStoryboardName = "FeedStoryboard" + private let feedListViewControllerIdentifier = "feedListViewController" + private let articleViewControllerIdentifier = "articleViewController" + + private let sourceManager: SourceManager + + init(sourceManager: SourceManager) { + self.sourceManager = sourceManager + + let feedListViewController = UIStoryboard(name: feedStoryboardName, bundle: nil).instantiateViewController(withIdentifier: feedListViewControllerIdentifier) as! FeedListViewController + feedNavigationController = UINavigationController(rootViewController: feedListViewController) + + let feedListViewModel = FeedListViewModel(sourceManager: sourceManager) + feedListViewModel.coordinationDelegate = self + feedListViewController.viewModel = feedListViewModel + + } + + var rootViewController: UIViewController { + return feedNavigationController + } + + func finished() { + feedNavigationController.popViewController(animated: true) + } +} + +extension FeedFlowCoordinator: FeedListViewModelDelegate { + func openArticleWith(dataObject: ArticleDataObject) { + let articleViewController = UIStoryboard(name: feedStoryboardName, bundle: nil).instantiateViewController(withIdentifier: articleViewControllerIdentifier) as! ArticleViewController + + let articleViewModel = ArticleViewModel(dataObject: dataObject) + articleViewController.viewModel = articleViewModel + + feedNavigationController.pushViewController(articleViewController, animated: true) + } +} diff --git a/rss-reader/Screens/Feed/FeedStoryboard.storyboard b/rss-reader/Screens/Feed/FeedStoryboard.storyboard new file mode 100644 index 0000000..4eef636 --- /dev/null +++ b/rss-reader/Screens/Feed/FeedStoryboard.storyboard @@ -0,0 +1,184 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> + <device id="retina6_1" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Feed--> + <scene sceneID="FT3-jq-gHn"> + <objects> + <viewController storyboardIdentifier="feedListViewController" title="Feed" id="h3g-zG-aWE" customClass="FeedListViewController" customModule="rss_reader" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="SMq-EC-EM4"> + <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="Bqv-BK-71n"> + <rect key="frame" x="0.0" y="44" width="414" height="769"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <color key="tintColor" white="0.33333333333333331" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <prototypes> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" accessoryType="disclosureIndicator" indentationWidth="10" reuseIdentifier="articleCell" rowHeight="122" id="oS2-9C-rVX" customClass="ArticleCell" customModule="rss_reader" customModuleProvider="target"> + <rect key="frame" x="0.0" y="28" width="414" height="122"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="oS2-9C-rVX" id="rSG-bd-jib"> + <rect key="frame" x="0.0" y="0.0" width="376" height="121.5"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="QNb-4B-vZu"> + <rect key="frame" x="10" y="20" width="81.5" height="81.5"/> + <constraints> + <constraint firstAttribute="width" secondItem="QNb-4B-vZu" secondAttribute="height" id="V9e-Vs-vkw"/> + </constraints> + </imageView> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="title" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="DpQ-zB-A3m"> + <rect key="frame" x="101.5" y="20" width="266.5" height="18"/> + <fontDescription key="fontDescription" type="system" weight="semibold" pointSize="15"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <label opaque="NO" userInteractionEnabled="NO" contentMode="TopLeft" horizontalHuggingPriority="251" verticalCompressionResistancePriority="748" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="nWe-yT-YJR"> + <rect key="frame" x="101.5" y="43" width="266.5" height="57.5"/> + <string key="text">descriptiondescriptiondescriptiondescriptiondescriptiondescriptiondescriptiondescriptiondescriptiondescriptiondescriptiondescriptiondescriptiondescription</string> + <fontDescription key="fontDescription" type="system" pointSize="12"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <constraints> + <constraint firstItem="QNb-4B-vZu" firstAttribute="top" secondItem="rSG-bd-jib" secondAttribute="top" constant="20" id="4vU-A7-1yt"/> + <constraint firstAttribute="trailingMargin" secondItem="nWe-yT-YJR" secondAttribute="trailing" id="U9V-ly-DQp"/> + <constraint firstItem="DpQ-zB-A3m" firstAttribute="leading" secondItem="QNb-4B-vZu" secondAttribute="trailing" constant="10" id="UI4-d0-ot7"/> + <constraint firstItem="DpQ-zB-A3m" firstAttribute="top" secondItem="QNb-4B-vZu" secondAttribute="top" id="aim-ni-Y2X"/> + <constraint firstAttribute="trailingMargin" secondItem="DpQ-zB-A3m" secondAttribute="trailing" id="bRu-3b-7ha"/> + <constraint firstItem="QNb-4B-vZu" firstAttribute="leading" secondItem="rSG-bd-jib" secondAttribute="leading" constant="10" id="ky4-9l-p2B"/> + <constraint firstItem="nWe-yT-YJR" firstAttribute="leading" secondItem="QNb-4B-vZu" secondAttribute="trailing" constant="10" id="lec-6i-K2Y"/> + <constraint firstItem="nWe-yT-YJR" firstAttribute="top" secondItem="DpQ-zB-A3m" secondAttribute="bottom" constant="5" id="pxv-ZK-2pl"/> + <constraint firstItem="nWe-yT-YJR" firstAttribute="bottom" relation="lessThanOrEqual" secondItem="QNb-4B-vZu" secondAttribute="bottom" id="veU-rv-jDJ"/> + <constraint firstItem="QNb-4B-vZu" firstAttribute="centerY" secondItem="rSG-bd-jib" secondAttribute="centerY" id="vou-LB-DNu"/> + </constraints> + </tableViewCellContentView> + <connections> + <outlet property="articleImageView" destination="QNb-4B-vZu" id="OpS-Fb-HGE"/> + <outlet property="descriptionLabel" destination="nWe-yT-YJR" id="1Tm-hv-Fzt"/> + <outlet property="titleLabel" destination="DpQ-zB-A3m" id="svg-B5-DZW"/> + </connections> + </tableViewCell> + </prototypes> + <connections> + <outlet property="dataSource" destination="h3g-zG-aWE" id="eEm-lj-TtH"/> + <outlet property="delegate" destination="h3g-zG-aWE" id="lq6-Lf-13S"/> + </connections> + </tableView> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstItem="Bqv-BK-71n" firstAttribute="top" secondItem="a0C-tx-apt" secondAttribute="top" id="Gpc-9Z-uCP"/> + <constraint firstItem="a0C-tx-apt" firstAttribute="bottom" secondItem="Bqv-BK-71n" secondAttribute="bottom" id="KJZ-Ue-4Pi"/> + <constraint firstItem="a0C-tx-apt" firstAttribute="trailing" secondItem="Bqv-BK-71n" secondAttribute="trailing" id="qUt-Fk-Sl3"/> + <constraint firstItem="Bqv-BK-71n" firstAttribute="leading" secondItem="a0C-tx-apt" secondAttribute="leading" id="zrd-oc-5Xk"/> + </constraints> + <viewLayoutGuide key="safeArea" id="a0C-tx-apt"/> + </view> + <tabBarItem key="tabBarItem" title="" image="feed" id="F6g-Px-gv7"/> + <nil key="simulatedTopBarMetrics"/> + <simulatedTabBarMetrics key="simulatedBottomBarMetrics"/> + <connections> + <outlet property="tableView" destination="Bqv-BK-71n" id="rHs-JE-cEF"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="fAb-V1-TNI" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-500.00000000000006" y="150.66964285714286"/> + </scene> + <!--Article View Controller--> + <scene sceneID="eAQ-SY-zDY"> + <objects> + <viewController storyboardIdentifier="articleViewController" hidesBottomBarWhenPushed="YES" id="kKV-Ns-SpO" customClass="ArticleViewController" customModule="rss_reader" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="UTA-Jx-whQ"> + <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="ntW-60-yMJ"> + <rect key="frame" x="0.0" y="44" width="414" height="818"/> + <subviews> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="cFv-UC-NAl" userLabel="content view"> + <rect key="frame" x="0.0" y="0.0" width="414" height="818"/> + <subviews> + <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" alignment="center" spacing="30" translatesAutoresizingMaskIntoConstraints="NO" id="9yc-IE-R6s"> + <rect key="frame" x="10" y="20" width="394" height="507.5"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="255" text="Title" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="3" baselineAdjustment="alignBaselines" minimumFontSize="15" translatesAutoresizingMaskIntoConstraints="NO" id="hYp-qx-7Nb"> + <rect key="frame" x="0.0" y="0.0" width="394" height="24"/> + <fontDescription key="fontDescription" type="boldSystem" pointSize="20"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="255" translatesAutoresizingMaskIntoConstraints="NO" id="u38-Gn-8nT"> + <rect key="frame" x="0.0" y="54" width="394" height="197"/> + <constraints> + <constraint firstAttribute="width" secondItem="u38-Gn-8nT" secondAttribute="height" multiplier="2:1" id="HJa-xy-NHt"/> + </constraints> + </imageView> + <textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" scrollEnabled="NO" showsHorizontalScrollIndicator="NO" showsVerticalScrollIndicator="NO" editable="NO" textAlignment="justified" translatesAutoresizingMaskIntoConstraints="NO" id="aLl-Fw-0fM"> + <rect key="frame" x="0.0" y="281" width="394" height="226.5"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string> + <fontDescription key="fontDescription" type="system" pointSize="16"/> + <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> + </textView> + </subviews> + <constraints> + <constraint firstItem="aLl-Fw-0fM" firstAttribute="width" secondItem="9yc-IE-R6s" secondAttribute="width" id="5Jg-Mg-9d3"/> + <constraint firstItem="u38-Gn-8nT" firstAttribute="width" secondItem="9yc-IE-R6s" secondAttribute="width" id="h5M-oh-hXG"/> + <constraint firstItem="hYp-qx-7Nb" firstAttribute="width" secondItem="9yc-IE-R6s" secondAttribute="width" id="jwb-MF-mVb"/> + </constraints> + </stackView> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstAttribute="bottom" relation="greaterThanOrEqual" secondItem="9yc-IE-R6s" secondAttribute="bottom" constant="20" id="P6m-bI-wPn"/> + <constraint firstItem="9yc-IE-R6s" firstAttribute="top" secondItem="cFv-UC-NAl" secondAttribute="top" constant="20" id="by0-W6-OQt"/> + <constraint firstItem="9yc-IE-R6s" firstAttribute="leading" secondItem="cFv-UC-NAl" secondAttribute="leading" constant="10" id="mdR-9i-v1F"/> + <constraint firstAttribute="trailing" secondItem="9yc-IE-R6s" secondAttribute="trailing" constant="10" id="t5x-IW-xZb"/> + </constraints> + </view> + </subviews> + <constraints> + <constraint firstAttribute="bottom" secondItem="cFv-UC-NAl" secondAttribute="bottom" id="SLF-pW-YHI"/> + <constraint firstItem="cFv-UC-NAl" firstAttribute="centerX" secondItem="ntW-60-yMJ" secondAttribute="centerX" id="Zt2-ei-o5A"/> + <constraint firstAttribute="trailing" secondItem="cFv-UC-NAl" secondAttribute="trailing" id="cSM-db-nRk"/> + <constraint firstItem="cFv-UC-NAl" firstAttribute="leading" secondItem="ntW-60-yMJ" secondAttribute="leading" id="haP-WE-tuc"/> + <constraint firstItem="cFv-UC-NAl" firstAttribute="height" secondItem="ntW-60-yMJ" secondAttribute="height" priority="749" id="o6o-YR-AEw"/> + <constraint firstItem="cFv-UC-NAl" firstAttribute="top" secondItem="ntW-60-yMJ" secondAttribute="top" id="pc3-DU-xu7"/> + </constraints> + </scrollView> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstItem="cQ3-Sr-zwr" firstAttribute="trailing" secondItem="ntW-60-yMJ" secondAttribute="trailing" id="TB1-Nu-pF8"/> + <constraint firstItem="ntW-60-yMJ" firstAttribute="leading" secondItem="cQ3-Sr-zwr" secondAttribute="leading" id="edd-7a-kQW"/> + <constraint firstItem="cQ3-Sr-zwr" firstAttribute="bottom" secondItem="ntW-60-yMJ" secondAttribute="bottom" id="lTw-6f-ybR"/> + <constraint firstItem="ntW-60-yMJ" firstAttribute="top" secondItem="cQ3-Sr-zwr" secondAttribute="top" id="sA3-C0-9Eb"/> + </constraints> + <viewLayoutGuide key="safeArea" id="cQ3-Sr-zwr"/> + </view> + <connections> + <outlet property="descriptionTextView" destination="aLl-Fw-0fM" id="TvL-wE-TdF"/> + <outlet property="pictureImageView" destination="u38-Gn-8nT" id="Ecn-Kq-4UI"/> + <outlet property="titleLabel" destination="hYp-qx-7Nb" id="49U-I9-y0m"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="DYE-6j-Egp" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="556.52173913043487" y="150.66964285714286"/> + </scene> + </scenes> + <resources> + <image name="feed" width="25" height="25"/> + </resources> +</document> diff --git a/rss-reader/Screens/Sources/Detailed source/AddSourceViewModel.swift b/rss-reader/Screens/Sources/Detailed source/AddSourceViewModel.swift new file mode 100644 index 0000000..a6cc04c --- /dev/null +++ b/rss-reader/Screens/Sources/Detailed source/AddSourceViewModel.swift @@ -0,0 +1,53 @@ +// +// DetailedSourceViewModel.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +class AddSourceViewModel { + + private let sourceManager: SourceAdding + + weak var coordinationDelegate: CoordinationDelegate? + + init(sourceManager: SourceAdding) { + self.sourceManager = sourceManager + } + +} + +extension AddSourceViewModel: DetailedSourceViewModel { + + func validate(name: String?, URLString: String?, completion: @escaping (String?) ->()) { + guard let name = name, !name.isEmpty else { + completion("Name shouldn't be empty!") + return + } + guard let URLString = URLString, let sourceURL = URL(string: URLString) else { + completion("Check URL!") + return + } + + let newDataObject = SourceDataObject(name: name, sourceURL: sourceURL) + + sourceManager.addSourceWith(dataObject: newDataObject) { (error) in + completion(error?.description()) + } + } + + func finish() { + coordinationDelegate?.finished() + } + + func getName() -> String? { + return nil + } + + func getURLString() -> String? { + return nil + } +} diff --git a/rss-reader/Screens/Sources/Detailed source/ChangeSourceViewModel.swift b/rss-reader/Screens/Sources/Detailed source/ChangeSourceViewModel.swift new file mode 100644 index 0000000..912dd9c --- /dev/null +++ b/rss-reader/Screens/Sources/Detailed source/ChangeSourceViewModel.swift @@ -0,0 +1,56 @@ +// +// ChangeSourceViewModel.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +class ChangeSourceViewModel { + + private let sourceManager: SourceUpdating + private let changeDataObject: SourceDataObject + + weak var coordinationDelegate: CoordinationDelegate? + + init(sourceManager: SourceUpdating, changeDataObject: SourceDataObject) { + self.sourceManager = sourceManager + self.changeDataObject = changeDataObject + } +} + +extension ChangeSourceViewModel: DetailedSourceViewModel { + + func validate(name: String?, URLString: String?, completion: @escaping (String?) ->()) { + guard let name = name, !name.isEmpty else { + completion("Name shouldn't be empty!") + return + } + guard let URLString = URLString, let sourceURL = URL(string: URLString) else { + completion("Check URL!") + return + } + + let shouldValidate = changeDataObject.sourceURL != sourceURL + + changeDataObject.update(name: name, sourceURL: sourceURL) + + sourceManager.updateSourceWith(dataObject: changeDataObject, shouldValidate: shouldValidate) { (error) in + completion(error?.description()) + } + } + + func finish() { + coordinationDelegate?.finished() + } + + func getName() -> String? { + return changeDataObject.name + } + + func getURLString() -> String? { + return changeDataObject.sourceURL.absoluteString + } +} diff --git a/rss-reader/Screens/Sources/Detailed source/DetailedSourceViewController.swift b/rss-reader/Screens/Sources/Detailed source/DetailedSourceViewController.swift new file mode 100644 index 0000000..f3655ac --- /dev/null +++ b/rss-reader/Screens/Sources/Detailed source/DetailedSourceViewController.swift @@ -0,0 +1,41 @@ +// +// DetailedSourceViewController.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit +import SVProgressHUD + +class DetailedSourceViewController: UIViewController { + + @IBOutlet private weak var nameTextField: UITextField! + @IBOutlet private weak var urlTextField: UITextField! + + var viewModel: DetailedSourceViewModel! + + override func viewDidLoad() { + super.viewDidLoad() + + nameTextField.text = viewModel.getName() + urlTextField.text = viewModel.getURLString() + } + + + //MARK: actions + + @IBAction func didTapDoneButton(_ sender: Any) { + SVProgressHUD.show() + viewModel.validate(name: nameTextField.text, URLString: urlTextField.text) { (errorString) in + if let errorString = errorString { + SVProgressHUD.showError(withStatus: errorString) + } else { + SVProgressHUD.showSuccess(withStatus: nil) + self.viewModel.finish() + } + } + } + +} diff --git a/rss-reader/Screens/Sources/Detailed source/DetailedSourceViewModel.swift b/rss-reader/Screens/Sources/Detailed source/DetailedSourceViewModel.swift new file mode 100644 index 0000000..c629fae --- /dev/null +++ b/rss-reader/Screens/Sources/Detailed source/DetailedSourceViewModel.swift @@ -0,0 +1,18 @@ +// +// DetailedSourceViewModel.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +protocol DetailedSourceViewModel { + func getName() -> String? + func getURLString() -> String? + func validate(name: String?, URLString: String?, completion: @escaping (String?) ->()) + func finish() +} + + diff --git a/rss-reader/Screens/Sources/Source list/Cells/SourceCell.swift b/rss-reader/Screens/Sources/Source list/Cells/SourceCell.swift new file mode 100644 index 0000000..444aa1a --- /dev/null +++ b/rss-reader/Screens/Sources/Source list/Cells/SourceCell.swift @@ -0,0 +1,19 @@ +// +// SourceCell.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +class SourceCell: UITableViewCell { + + @IBOutlet private weak var nameLabel: UILabel! + + func setAppearance(sourceName: String) { + nameLabel.text = sourceName + } + +} diff --git a/rss-reader/Screens/Sources/Source list/SourceListViewController.swift b/rss-reader/Screens/Sources/Source list/SourceListViewController.swift new file mode 100644 index 0000000..f5f1bb5 --- /dev/null +++ b/rss-reader/Screens/Sources/Source list/SourceListViewController.swift @@ -0,0 +1,70 @@ +// +// SourceListViewController.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +class SourceListViewController: UIViewController { + + private let sourceCellIdentifier = "sourceCell" + + @IBOutlet private weak var tableView: UITableView! + + var viewModel: SourceListViewModel! + + override func viewDidLoad() { + super.viewDidLoad() + tableView.tableFooterView = UIView() + } + + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + + viewModel.updateSourceDataObjects() + tableView.reloadData() + } + + + //MARK: actions + + @IBAction func didTapAddSourceButton(_ sender: Any) { + viewModel.didTapAddSourceButton() + } + + +} + +extension SourceListViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + viewModel.didTapChangeSourceAt(index: indexPath.row) + } + + func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { + return true + } + + func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath) { + if editingStyle == .delete { + viewModel.removeRowAt(index: indexPath.row) + tableView.deleteRows(at: [indexPath], with: .fade) + } + } +} + +extension SourceListViewController: UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return viewModel.numberOfSources() + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: sourceCellIdentifier, for: indexPath) as! SourceCell + cell.setAppearance(sourceName: viewModel.nameOfSourceAt(index: indexPath.row)) + return cell + } + +} diff --git a/rss-reader/Screens/Sources/Source list/SourceListViewModel.swift b/rss-reader/Screens/Sources/Source list/SourceListViewModel.swift new file mode 100644 index 0000000..fbc72d0 --- /dev/null +++ b/rss-reader/Screens/Sources/Source list/SourceListViewModel.swift @@ -0,0 +1,53 @@ +// +// SourceListViewModel.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +protocol SourceListViewModelDelegate: CoordinationDelegate { + func openAddSource() + func openChangeSourceWith(changeDataObject: SourceDataObject) +} + +class SourceListViewModel { + + private let sourceManager: SourceRetrieving & SourceRemoving + private var sourceDataObjects: [SourceDataObject] + + weak var coordinationDelegate: SourceListViewModelDelegate? + + init(sourceManager: SourceRetrieving & SourceRemoving) { + self.sourceManager = sourceManager + sourceDataObjects = sourceManager.getSourceDataObjects() + } + + func updateSourceDataObjects() { + sourceDataObjects = sourceManager.getSourceDataObjects() + } + + func numberOfSources() -> Int { + return sourceDataObjects.count + } + + func nameOfSourceAt(index: Int) -> String { + return sourceDataObjects[index].name + } + + func removeRowAt(index: Int) { + sourceManager.removeSourceWith(dataObject: sourceDataObjects[index]) + updateSourceDataObjects() + } + + func didTapAddSourceButton() { + coordinationDelegate?.openAddSource() + } + + func didTapChangeSourceAt(index: Int) { + coordinationDelegate?.openChangeSourceWith(changeDataObject: sourceDataObjects[index]) + } + +} diff --git a/rss-reader/Screens/Sources/SourceListFlowCoordinator.swift b/rss-reader/Screens/Sources/SourceListFlowCoordinator.swift new file mode 100644 index 0000000..db07741 --- /dev/null +++ b/rss-reader/Screens/Sources/SourceListFlowCoordinator.swift @@ -0,0 +1,61 @@ +// +// SourceListFlowCoordinator.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +class SourceListFlowCoordinator: Coordinator { + + private let sourceListNavigationController: UINavigationController + + private let sourceStoryboardName = "SourceStoryboard" + private let sourceListViewControllerIdentifier = "sourceListViewController" + private let detailedSourceViewControllerIdentifier = "detailedSourceViewController" + + private let sourceManager: SourceManager + + init(sourceManager: SourceManager) { + self.sourceManager = sourceManager + + let sourceListViewController = UIStoryboard(name: sourceStoryboardName, bundle: nil).instantiateViewController(withIdentifier: sourceListViewControllerIdentifier) as! SourceListViewController + sourceListNavigationController = UINavigationController(rootViewController: sourceListViewController) + + let sourceListViewModel = SourceListViewModel(sourceManager: sourceManager) + sourceListViewModel.coordinationDelegate = self + sourceListViewController.viewModel = sourceListViewModel + + } + + var rootViewController: UIViewController { + return sourceListNavigationController + } + + func finished() { + sourceListNavigationController.popViewController(animated: true) + } + +} + +extension SourceListFlowCoordinator: SourceListViewModelDelegate { + func openChangeSourceWith(changeDataObject: SourceDataObject) { + let detailedSourceViewController = UIStoryboard(name: sourceStoryboardName, bundle: nil).instantiateViewController(withIdentifier: detailedSourceViewControllerIdentifier) as! DetailedSourceViewController + let changeSourceViewModel = ChangeSourceViewModel(sourceManager: sourceManager, changeDataObject: changeDataObject) + changeSourceViewModel.coordinationDelegate = self + detailedSourceViewController.viewModel = changeSourceViewModel + + sourceListNavigationController.pushViewController(detailedSourceViewController, animated: true) + } + + func openAddSource() { + let detailedSourceViewController = UIStoryboard(name: sourceStoryboardName, bundle: nil).instantiateViewController(withIdentifier: detailedSourceViewControllerIdentifier) as! DetailedSourceViewController + let addSourceViewModel = AddSourceViewModel(sourceManager: sourceManager) + addSourceViewModel.coordinationDelegate = self + detailedSourceViewController.viewModel = addSourceViewModel + + sourceListNavigationController.pushViewController(detailedSourceViewController, animated: true) + } +} diff --git a/rss-reader/Screens/Sources/SourceStoryboard.storyboard b/rss-reader/Screens/Sources/SourceStoryboard.storyboard new file mode 100644 index 0000000..f5c43a1 --- /dev/null +++ b/rss-reader/Screens/Sources/SourceStoryboard.storyboard @@ -0,0 +1,155 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> + <device id="retina6_1" orientation="portrait"> + <adaptation id="fullscreen"/> + </device> + <dependencies> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <scenes> + <!--Source list--> + <scene sceneID="Fet-Fy-lcH"> + <objects> + <viewController storyboardIdentifier="sourceListViewController" title="Source list" id="oO8-6F-BOy" customClass="SourceListViewController" customModule="rss_reader" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="klR-YE-a2k"> + <rect key="frame" x="0.0" y="0.0" width="414" height="896"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" showsHorizontalScrollIndicator="NO" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="9UN-uG-4A3"> + <rect key="frame" x="0.0" y="88" width="414" height="725"/> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <prototypes> + <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" reuseIdentifier="sourceCell" id="5ZJ-qJ-w24" customClass="SourceCell" customModule="rss_reader" customModuleProvider="target"> + <rect key="frame" x="0.0" y="28" width="414" height="44"/> + <autoresizingMask key="autoresizingMask"/> + <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="5ZJ-qJ-w24" id="3tl-lg-phK"> + <rect key="frame" x="0.0" y="0.0" width="414" height="43.5"/> + <autoresizingMask key="autoresizingMask"/> + <subviews> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="source name" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="951-I6-a8n"> + <rect key="frame" x="20" y="11" width="374" height="22"/> + <fontDescription key="fontDescription" type="system" weight="medium" pointSize="18"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + </subviews> + <constraints> + <constraint firstAttribute="bottomMargin" secondItem="951-I6-a8n" secondAttribute="bottom" id="FVx-US-91h"/> + <constraint firstItem="951-I6-a8n" firstAttribute="leading" secondItem="3tl-lg-phK" secondAttribute="leadingMargin" id="cAg-BF-CEE"/> + <constraint firstItem="951-I6-a8n" firstAttribute="top" secondItem="3tl-lg-phK" secondAttribute="topMargin" id="jvE-ci-1h5"/> + <constraint firstAttribute="trailingMargin" secondItem="951-I6-a8n" secondAttribute="trailing" id="w1E-9l-8ms"/> + </constraints> + </tableViewCellContentView> + <connections> + <outlet property="nameLabel" destination="951-I6-a8n" id="1I0-Xo-2CJ"/> + </connections> + </tableViewCell> + </prototypes> + <connections> + <outlet property="dataSource" destination="oO8-6F-BOy" id="w76-e6-6RL"/> + <outlet property="delegate" destination="oO8-6F-BOy" id="ZM1-Lh-vJx"/> + </connections> + </tableView> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstItem="9UN-uG-4A3" firstAttribute="top" secondItem="N9l-kE-TIa" secondAttribute="top" id="7sG-yS-371"/> + <constraint firstItem="N9l-kE-TIa" firstAttribute="bottom" secondItem="9UN-uG-4A3" secondAttribute="bottom" id="LTu-2V-pSJ"/> + <constraint firstItem="N9l-kE-TIa" firstAttribute="trailing" secondItem="9UN-uG-4A3" secondAttribute="trailing" id="iSe-Vq-5mP"/> + <constraint firstItem="9UN-uG-4A3" firstAttribute="leading" secondItem="N9l-kE-TIa" secondAttribute="leading" id="vPY-N1-WiT"/> + </constraints> + <viewLayoutGuide key="safeArea" id="N9l-kE-TIa"/> + </view> + <tabBarItem key="tabBarItem" title="" image="sources" id="TkC-3a-h75"/> + <toolbarItems/> + <navigationItem key="navigationItem" title="Sources" id="BWZ-EY-ivf"> + <barButtonItem key="rightBarButtonItem" systemItem="add" id="VY4-6B-MTZ"> + <connections> + <action selector="didTapAddSourceButton:" destination="oO8-6F-BOy" id="vYf-8p-q2n"/> + </connections> + </barButtonItem> + </navigationItem> + <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" prompted="NO"/> + <simulatedToolbarMetrics key="simulatedBottomBarMetrics"/> + <connections> + <outlet property="tableView" destination="9UN-uG-4A3" id="YxF-UJ-SC9"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="Jfp-73-pjQ" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="-433" y="-152"/> + </scene> + <!--Detailed Source--> + <scene sceneID="54Y-0M-9St"> + <objects> + <viewController storyboardIdentifier="detailedSourceViewController" title="Detailed Source" hidesBottomBarWhenPushed="YES" id="FhJ-ku-lT5" customClass="DetailedSourceViewController" customModule="rss_reader" customModuleProvider="target" sceneMemberID="viewController"> + <view key="view" contentMode="scaleToFill" id="T2c-dN-1yp"> + <rect key="frame" x="0.0" y="0.0" width="414" height="808"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Source name" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="96H-49-PlD"> + <rect key="frame" x="20" y="70.5" width="374" height="30"/> + <nil key="textColor"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <textInputTraits key="textInputTraits" autocapitalizationType="sentences" textContentType="name"/> + </textField> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="URL:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xzx-zy-VPV"> + <rect key="frame" x="20" y="140.5" width="374" height="20.5"/> + <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Name:" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="4ad-f4-RrY"> + <rect key="frame" x="20" y="40" width="374" height="20.5"/> + <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> + <nil key="textColor"/> + <nil key="highlightedColor"/> + </label> + <textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" borderStyle="roundedRect" placeholder="Source URL" textAlignment="natural" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="d1w-QS-OIt"> + <rect key="frame" x="20" y="171" width="374" height="30"/> + <nil key="textColor"/> + <fontDescription key="fontDescription" type="system" pointSize="14"/> + <textInputTraits key="textInputTraits"/> + </textField> + </subviews> + <color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + <constraints> + <constraint firstItem="xzx-zy-VPV" firstAttribute="top" secondItem="96H-49-PlD" secondAttribute="bottom" constant="40" id="7Ev-sk-Nk8"/> + <constraint firstItem="Joo-s6-2EL" firstAttribute="trailing" secondItem="d1w-QS-OIt" secondAttribute="trailing" constant="20" id="9xv-y9-51l"/> + <constraint firstItem="d1w-QS-OIt" firstAttribute="top" secondItem="xzx-zy-VPV" secondAttribute="bottom" constant="10" id="IY4-F5-Qrs"/> + <constraint firstItem="96H-49-PlD" firstAttribute="leading" secondItem="Joo-s6-2EL" secondAttribute="leading" constant="20" id="NwP-qe-A6E"/> + <constraint firstItem="96H-49-PlD" firstAttribute="top" secondItem="4ad-f4-RrY" secondAttribute="bottom" constant="10" id="RNk-Ud-kPg"/> + <constraint firstItem="Joo-s6-2EL" firstAttribute="trailing" secondItem="xzx-zy-VPV" secondAttribute="trailing" constant="20" id="U4o-2p-MMu"/> + <constraint firstItem="4ad-f4-RrY" firstAttribute="top" secondItem="Joo-s6-2EL" secondAttribute="top" constant="40" id="caC-RL-Nxn"/> + <constraint firstItem="Joo-s6-2EL" firstAttribute="trailing" secondItem="96H-49-PlD" secondAttribute="trailing" constant="20" id="f9K-QG-o1X"/> + <constraint firstItem="Joo-s6-2EL" firstAttribute="trailing" secondItem="4ad-f4-RrY" secondAttribute="trailing" constant="20" id="fm0-Sx-6oS"/> + <constraint firstItem="xzx-zy-VPV" firstAttribute="leading" secondItem="Joo-s6-2EL" secondAttribute="leading" constant="20" id="t2n-tM-aAn"/> + <constraint firstItem="4ad-f4-RrY" firstAttribute="leading" secondItem="Joo-s6-2EL" secondAttribute="leading" constant="20" id="uBR-XJ-Zy3"/> + <constraint firstItem="d1w-QS-OIt" firstAttribute="leading" secondItem="Joo-s6-2EL" secondAttribute="leading" constant="20" id="ybE-m2-rco"/> + </constraints> + <viewLayoutGuide key="safeArea" id="Joo-s6-2EL"/> + </view> + <navigationItem key="navigationItem" title="Source details" id="Qz0-kP-Y19"> + <barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="6kQ-6F-XvJ"> + <connections> + <action selector="didTapDoneButton:" destination="FhJ-ku-lT5" id="Uv5-QU-Ubi"/> + </connections> + </barButtonItem> + </navigationItem> + <simulatedNavigationBarMetrics key="simulatedTopBarMetrics" translucent="NO" prompted="NO"/> + <connections> + <outlet property="nameTextField" destination="96H-49-PlD" id="yKr-hb-ena"/> + <outlet property="urlTextField" destination="d1w-QS-OIt" id="x4q-Ai-GWF"/> + </connections> + </viewController> + <placeholder placeholderIdentifier="IBFirstResponder" id="Mwx-eQ-aYv" userLabel="First Responder" sceneMemberID="firstResponder"/> + </objects> + <point key="canvasLocation" x="487" y="-152"/> + </scene> + </scenes> + <resources> + <image name="sources" width="25" height="25"/> + </resources> +</document> diff --git a/rss-reader/Services/RSSNetworkManager.swift b/rss-reader/Services/RSSNetworkManager.swift new file mode 100644 index 0000000..64851c6 --- /dev/null +++ b/rss-reader/Services/RSSNetworkManager.swift @@ -0,0 +1,33 @@ +// +// FeedNetworkManager.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation +import FeedKit + +enum NetworkError: String { + case fetchError = "Error while fetching feed" + + func description() -> String { + return self.rawValue + } +} + +class RSSNetworkManager: NetworkManager { + + func fetchFeedFrom(sourceURL: URL, completion: @escaping (Feed?, NetworkError?) -> ()) { + FeedParser(URL: sourceURL).parseAsync(queue: DispatchQueue.global(qos: .userInitiated)) { (result) in + DispatchQueue.main.async { + if let rssFeed = result.rssFeed { + completion(Feed(rssFeed: rssFeed), nil) + } else { + completion(nil, .fetchError) + } + } + } + } +} diff --git a/rss-reader/Services/RSSSourceManager.swift b/rss-reader/Services/RSSSourceManager.swift new file mode 100644 index 0000000..5d5bc41 --- /dev/null +++ b/rss-reader/Services/RSSSourceManager.swift @@ -0,0 +1,100 @@ +// +// RSSSourceManager.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +class RSSSourceManager: SourceManager { + + private let storageManager: StorageManager + private let networkManager: NetworkManager + + private var sources: [Source] + + init(storageManager: StorageManager, networkManager: NetworkManager) { + self.storageManager = storageManager + self.networkManager = networkManager + + sources = storageManager.getSources() + } + + func getSourceDataObjects() -> [SourceDataObject] { + return sources.map { SourceDataObject(source: $0) } + } + + func getFeedDataObjects() -> [FeedDataObject] { + return sources.map { FeedDataObject(source: $0) } + } + + func updateSourceWith(dataObject: SourceDataObject, shouldValidate: Bool, completion: @escaping (NetworkError?) -> ()) { + if let id = dataObject.identifier, let source = sourceWith(id: id) { + if !shouldValidate { + source.update(dataObject: dataObject) + completion(nil) + } else { + networkManager.fetchFeedFrom(sourceURL: dataObject.sourceURL) { (feed, error) in + if let error = error { + completion(error) + return + } else if let feed = feed { + source.update(feed: feed) + self.storageManager.update(source: source) + } + completion(nil) + } + } + } + } + + func updateFeed(completion: @escaping (NetworkError?) -> ()) { + DispatchQueue.global(qos: .userInitiated).async { + let group = DispatchGroup() + var finalError: NetworkError? = nil + for source in self.sources { + group.enter() + self.networkManager.fetchFeedFrom(sourceURL: source.sourceURL) { (feed, error) in + if let feed = feed { + source.update(feed: feed) + } else if let error = error { + finalError = error + } + group.leave() + } + } + group.notify(queue: .main) { + completion(finalError) + } + } + } + + private func sourceWith(id: String) -> Source? { + return sources.first(where: { $0.identifier == id }) + } + + func removeSourceWith(dataObject: SourceDataObject) { + if let dataObjectIndex = sources.firstIndex(where: { $0.identifier == dataObject.identifier }) { + storageManager.remove(source: sources[dataObjectIndex]) + sources.remove(at: dataObjectIndex) + } + } + + func addSourceWith(dataObject: SourceDataObject, completion: @escaping (NetworkError?) -> ()) { + let newSource = Source(dataObject: dataObject) + networkManager.fetchFeedFrom(sourceURL: newSource.sourceURL) { (feed, error) in + if let error = error { + completion(error) + return + } else if let feed = feed { + newSource.update(feed: feed) + self.storageManager.add(source: newSource) + self.sources.append(newSource) + } + completion(nil) + } + } +} + diff --git a/rss-reader/Services/SourceManagerProtocols.swift b/rss-reader/Services/SourceManagerProtocols.swift new file mode 100644 index 0000000..f0e7bb6 --- /dev/null +++ b/rss-reader/Services/SourceManagerProtocols.swift @@ -0,0 +1,57 @@ +// +// Source manager protocols.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +// basic manager protocols + +protocol SourceRemoving { + func removeSourceWith(dataObject: SourceDataObject) +} + +protocol SourceUpdating { + func updateSourceWith(dataObject: SourceDataObject, shouldValidate: Bool, completion: @escaping (NetworkError?) -> ()) +} + +protocol SourceAdding { + func addSourceWith(dataObject: SourceDataObject, completion: @escaping (NetworkError?) -> ()) +} + +protocol SourceRetrieving { + func getSourceDataObjects() -> [SourceDataObject] +} + +protocol FeedRetrieving { + func getFeedDataObjects() -> [FeedDataObject] +} + +protocol FeedUpdatable { + func updateFeed(completion: @escaping (NetworkError?) -> ()) +} + + +typealias SourceManager = SourceAdding & + SourceRemoving & + SourceUpdating & + SourceRetrieving & + FeedRetrieving & + FeedUpdatable + + +// internal managers + +protocol StorageManager { + func remove(source: Source) + func getSources() -> [Source] + func update(source: Source) + func add(source: Source) +} + +protocol NetworkManager { + func fetchFeedFrom(sourceURL: URL, completion: @escaping (Feed?, NetworkError?) -> ()) +} diff --git a/rss-reader/Services/UserDefaultsStorageManager.swift b/rss-reader/Services/UserDefaultsStorageManager.swift new file mode 100644 index 0000000..b5ffcec --- /dev/null +++ b/rss-reader/Services/UserDefaultsStorageManager.swift @@ -0,0 +1,81 @@ +// +// UserDefaultsStorageManager.swift +// rss-reader +// +// Created by Daniil Kuleshov on 10/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation + +/* + + I know that this class is so bad, but I didn't want to use Realm or CoreData for such a simple 'database', + but UserDefaults is so bad when it comes to work with custom objects. + + So I implement StoreManager database type methods with UserDefaults. :( + + + */ + +class UserDefaultsStorageManager: StorageManager { + + let sourcesKey = "sources" + + let userDefaults = UserDefaults.standard + + func remove(source: Source) { + var sources = retrieveSourceEntities() + if let index = sources.firstIndex(where: { $0.identifier == source.identifier }) { + sources.remove(at: index) + storeSourceEntities(sourceEntities: sources) + } + } + + func getSources() -> [Source] { + return retrieveSourceEntities().map { Source(entity: $0) } + } + + func update(source: Source) { + var sources = retrieveSourceEntities() + if let index = sources.firstIndex(where: { $0.identifier == source.identifier }) { + sources[index] = SourceEntity(source: source) + storeSourceEntities(sourceEntities: sources) + } + } + + func add(source: Source) { + var sources = retrieveSourceEntities() + sources.append(SourceEntity(source: source)) + storeSourceEntities(sourceEntities: sources) + + } + + private func retrieveSourceEntities() -> [SourceEntity] { + var sourceEntities = [SourceEntity]() + if let sourcesData = userDefaults.array(forKey: sourcesKey) as! [Data]? { + let decoder = JSONDecoder() + + for sourceData in sourcesData { + if let sourceEntity = try? decoder.decode(SourceEntity.self, from: sourceData) { + sourceEntities.append(sourceEntity) + } + } + } + return sourceEntities + } + + private func storeSourceEntities(sourceEntities: [SourceEntity]) { + let encoder = JSONEncoder() + var sourcesData = [Data]() + for sourceEntity in sourceEntities { + if let sourceData = try? encoder.encode(sourceEntity) { + sourcesData.append(sourceData) + } + } + userDefaults.set(sourcesData, forKey: sourcesKey) + userDefaults.synchronize() + } + + +} diff --git a/rss-reader/Utils/Alert.swift b/rss-reader/Utils/Alert.swift new file mode 100644 index 0000000..2f5090b --- /dev/null +++ b/rss-reader/Utils/Alert.swift @@ -0,0 +1,24 @@ +// +// Alert.swift +// rss-reader +// +// Created by Daniil Kuleshov on 11/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import UIKit + +class Alert { + + class func showInfoAlert(on viewController: UIViewController, title: String, message: String) { + let alertVC = UIAlertController(title: title, message: message, preferredStyle: .alert) + alertVC.addAction(UIAlertAction(title: "OK", style: .cancel)) + viewController.present(alertVC, animated: true) + } + + class func showErrorAlert(on viewController: UIViewController, message: String) { + let alertVC = UIAlertController(title: "Error", message: message, preferredStyle: .alert) + alertVC.addAction(UIAlertAction(title: "OK", style: .cancel)) + viewController.present(alertVC, animated: true) + } +} diff --git a/rss-reader/Utils/ColorsManager.swift b/rss-reader/Utils/ColorsManager.swift new file mode 100644 index 0000000..8008297 --- /dev/null +++ b/rss-reader/Utils/ColorsManager.swift @@ -0,0 +1,20 @@ +// +// ColorsManager.swift +// rss-reader +// +// Created by Daniil Kuleshov on 11/08/2019. +// Copyright © 2019 kuleshov. All rights reserved. +// + +import Foundation +import UIKit + +class ColorsManager { + + // In case of custom color scheme UIColor extension may be implemented + class func setupColors() { + UITabBar.appearance().tintColor = .darkGray + UINavigationBar.appearance().tintColor = .darkGray + UITableView.appearance().tintColor = .darkGray + } +}