From fbf8abba1fbed1da984c58c33581379b822fa86d Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 23 Sep 2022 12:46:17 +0530 Subject: [PATCH 01/12] initial project commit --- .gitignore | 2 + BulletinSDK/BulletinDataStore.swift | 58 +++ BulletinSDK/BulletinSDK.swift | 37 ++ BulletinSDK/Extension/String+Class.swift | 36 ++ BulletinSDK/Modal/ActionButton.swift | 40 ++ BulletinSDK/Modal/BulletPoint.swift | 108 +++++ BulletinSDK/Modal/BulletinItem.swift | 64 +++ BulletinSDK/Modal/Media.swift | 58 +++ BulletinSDK/Modal/Message.swift | 47 ++ BulletinSDK/Modal/PreTitle.swift | 32 ++ BulletinSDK/Modal/Title.swift | 33 ++ Demo/Bulletin.xcodeproj/project.pbxproj | 428 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + Demo/Bulletin/AppDelegate.swift | 36 ++ .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 13 + Demo/Bulletin/Assets.xcassets/Contents.json | 6 + .../Base.lproj/LaunchScreen.storyboard | 25 + Demo/Bulletin/Base.lproj/Main.storyboard | 24 + Demo/Bulletin/Info.plist | 25 + Demo/Bulletin/SceneDelegate.swift | 52 +++ Demo/Bulletin/ViewController.swift | 86 ++++ 23 files changed, 1236 insertions(+) create mode 100644 .gitignore create mode 100644 BulletinSDK/BulletinDataStore.swift create mode 100644 BulletinSDK/BulletinSDK.swift create mode 100644 BulletinSDK/Extension/String+Class.swift create mode 100644 BulletinSDK/Modal/ActionButton.swift create mode 100644 BulletinSDK/Modal/BulletPoint.swift create mode 100644 BulletinSDK/Modal/BulletinItem.swift create mode 100644 BulletinSDK/Modal/Media.swift create mode 100644 BulletinSDK/Modal/Message.swift create mode 100644 BulletinSDK/Modal/PreTitle.swift create mode 100644 BulletinSDK/Modal/Title.swift create mode 100644 Demo/Bulletin.xcodeproj/project.pbxproj create mode 100644 Demo/Bulletin.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Demo/Bulletin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Demo/Bulletin/AppDelegate.swift create mode 100644 Demo/Bulletin/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Demo/Bulletin/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Demo/Bulletin/Assets.xcassets/Contents.json create mode 100644 Demo/Bulletin/Base.lproj/LaunchScreen.storyboard create mode 100644 Demo/Bulletin/Base.lproj/Main.storyboard create mode 100644 Demo/Bulletin/Info.plist create mode 100644 Demo/Bulletin/SceneDelegate.swift create mode 100644 Demo/Bulletin/ViewController.swift diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..046729a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +Demo/Bulletin.xcodeproj/project.xcworkspace/xcuserdata/pranav.xcuserdatad/UserInterfaceState.xcuserstate +Demo/Bulletin.xcodeproj/xcuserdata/pranav.xcuserdatad/xcschemes/xcschememanagement.plist diff --git a/BulletinSDK/BulletinDataStore.swift b/BulletinSDK/BulletinDataStore.swift new file mode 100644 index 0000000..e744843 --- /dev/null +++ b/BulletinSDK/BulletinDataStore.swift @@ -0,0 +1,58 @@ +// +// BulletinDataStore.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import Foundation + +typealias Version = String + +class BulletinDataStore { + + + // MARK: - Variable + public private(set) var data = [Version: [BulletinItem]]() + + + // MARK: - Initialisation Methods + public init() {} + + public init?(attributes: [String : Any]) { + + } + + + // MARK: - Helper Methods + public func registerVersionInfo(version: Version, items: [[String: Any]]) { + + var bulletinItems = [BulletinItem]() + for itemAttributes in items { + guard let bulletinItem = BulletinItem.createBulletinItem(forAttributes: itemAttributes) else { continue } + bulletinItems.append(bulletinItem) + } + + registerVersionInfo(version: version, items: bulletinItems) + } + + public func registerVersionInfo(version: Version, items: [BulletinItem]) { + if items.isEmpty == false { + self.data[version] = items + } + } + + public func getData(fromVersion version: Version, limit: Int? = nil) { + +// for appVersion in self.data { +// if appVersion.key == version { +// let bulletinItem = self.data[version] +// for item in bulletinItem { +// print("✅\(item.text)") +// } +// } +// } + + } + +} diff --git a/BulletinSDK/BulletinSDK.swift b/BulletinSDK/BulletinSDK.swift new file mode 100644 index 0000000..d57a453 --- /dev/null +++ b/BulletinSDK/BulletinSDK.swift @@ -0,0 +1,37 @@ +// +// BulletinSDK.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import Foundation + +class BulletinSDK { + + + // MARK: - Variables + public let dataStore: BulletinDataStore + + + // MARK: Initialisation Method + public init(dataStore: BulletinDataStore) { + + // Set Data Store + self.dataStore = dataStore + } + + + // MARK: - Helper Methods + func showFullBulletin() { + + } + + func showBulletinFromLastVersion(limit: Int? = nil) { + + } + + func showBulletin(fromVersion version: String, limit: Int? = nil) { + dataStore.getData(fromVersion: version) + } +} diff --git a/BulletinSDK/Extension/String+Class.swift b/BulletinSDK/Extension/String+Class.swift new file mode 100644 index 0000000..e68be48 --- /dev/null +++ b/BulletinSDK/Extension/String+Class.swift @@ -0,0 +1,36 @@ +// +// String+Class.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import Foundation + +extension String { + + // Note : This method is capable of returning any Swift or Obj-C class with its name + public static func classFromString(_ className: String?) -> AnyClass? { + + if let className = className { + + // Get Class + let _class : AnyClass? = NSClassFromString(className) + if _class == nil { + + // Check for a swift class with the same name + let bundleName = Bundle.main.object(forInfoDictionaryKey: "CFBundleExecutable") as? String + if let bundleName = bundleName { + let swiftClassName = bundleName + "." + className + return NSClassFromString(swiftClassName) + } + } + return _class + } + return nil + } + + public func classFromString() -> AnyClass? { + return String.classFromString(self) + } +} diff --git a/BulletinSDK/Modal/ActionButton.swift b/BulletinSDK/Modal/ActionButton.swift new file mode 100644 index 0000000..60ca6e1 --- /dev/null +++ b/BulletinSDK/Modal/ActionButton.swift @@ -0,0 +1,40 @@ +// +// ActionButton.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + +class ActionButton: BulletinItem { + + + // MARK: Variables + var title: String? + var clickPayload: Any? + + + // MARK: - Initialisation Methods + override init () { + super.init() + + // Set Type + type = .actionButton + } + + + required init?(attributes: [AnyHashable: Any]) { + super.init(attributes: attributes) + + // Set Title + guard let title = attributes["title"] as? String else { + return nil + } + self.title = title + + // Set Click Payload + self.clickPayload = attributes["clickPayload"] + } +} + diff --git a/BulletinSDK/Modal/BulletPoint.swift b/BulletinSDK/Modal/BulletPoint.swift new file mode 100644 index 0000000..119c70c --- /dev/null +++ b/BulletinSDK/Modal/BulletPoint.swift @@ -0,0 +1,108 @@ +// +// BulletPoint.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + +class BulletPoint: BulletinItem { + + + // MARK: Variables + var bullet: Bullet? + var titleText: String? + var subTitleText: String? + + + // MARK: - Initialisation Methods + override init () { + super.init() + + // Set Type + type = .bulletPoint + } + + required init?(attributes: [AnyHashable: Any]) { + super.init(attributes: attributes) + + // Set Bullet + guard let bulletObj = attributes["bullet"] as? [String: Any], + let bullet = Bullet(attributes: bulletObj) else { + return nil + } + self.bullet = bullet + + // Set Title Text + self.titleText = attributes["titleText"] as? String + + // Set SubTitle Text + self.subTitleText = attributes["subTitleText"] as? String + + // Validation + if titleText == nil && subTitleText == nil { + return nil + } + } +} + + + +// MARK: - Bullet +class Bullet { + + + // MARK: - Enum + enum BulletType: String { + case unicode + case image + } + + + // MARK: - Variables + var bulletType: BulletType? + var unicode: String? + var imageUrl: URL? + + + // MARK: - Initialisation Methods + init() {} + + init?(attributes: [String: Any]?) { + + + // Validation + guard let attributes = attributes else { + return nil + } + + // Set Type + guard let bulletTypeString = attributes["bulletType"] as? String, + let bulletType = BulletType(rawValue: bulletTypeString) else { + return nil + } + self.bulletType = bulletType + + switch self.bulletType { + case .unicode: + // Set Unicode + guard let unicodeString = attributes["unicode"] as? String else { + return nil + } + self.unicode = unicodeString + + case .image: + // Set ImageUrl + guard let imageUrlString = attributes["imageUrl"] as? String, + let imageUrl = URL(string: imageUrlString) else { + return nil + } + self.imageUrl = imageUrl + + case .none: + break + } + } +} + diff --git a/BulletinSDK/Modal/BulletinItem.swift b/BulletinSDK/Modal/BulletinItem.swift new file mode 100644 index 0000000..e700998 --- /dev/null +++ b/BulletinSDK/Modal/BulletinItem.swift @@ -0,0 +1,64 @@ +// +// BulletinItem.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import Foundation + +class BulletinItem { + + + // MARK: - Declarations + public enum ItemType : String { + case undefined + case preTitle + case title + case message + case media + case bulletPoint + case actionButton + } + + + // MARK: - Variables + public var type: ItemType = .undefined + + + + // It Will Return Action Card Creatd With Proper Classes + public class func createBulletinItem(forAttributes attributes: [String: Any]?) -> BulletinItem? { + + // Validations + guard let attributes = attributes, + let typeString = attributes["type"] as? String, + let itemType = ItemType(rawValue: typeString), + itemType != .undefined else { + return nil + } + + // Get Bulletin Item + if let bulletinItem = itemType.rawValue.capitalized.classFromString() as? BulletinItem.Type { + return bulletinItem.init(attributes: attributes) + } + return nil + } + + + // MARK: - Initialisation Methods + init() {} + + required public init?(attributes: [AnyHashable: Any]) { + + // Get Item Type (create guard here) + if let itemTypeString = attributes["type"] as? String, + let type = ItemType(rawValue: itemTypeString) + { + self.type = type + } + else { + return nil + } + } +} diff --git a/BulletinSDK/Modal/Media.swift b/BulletinSDK/Modal/Media.swift new file mode 100644 index 0000000..e8a46cd --- /dev/null +++ b/BulletinSDK/Modal/Media.swift @@ -0,0 +1,58 @@ +// +// Media.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + + +class Media: BulletinItem { + + + // MARK: - Enum + enum MediaType: String { + case image + } + + + // MARK: - Variables + var mediaType: MediaType = .image + var url: URL? + var size: CGSize? + + + // MARK: - Initialisation Methods + override init () { + super.init() + + // Set Type + type = .media + } + + required init?(attributes: [AnyHashable: Any]) { + super.init(attributes: attributes) + + // Set Media Type + if let mediaTypeString = attributes["mediaType"] as? String, + let mediaType = MediaType(rawValue: mediaTypeString) { + self.mediaType = mediaType + } + + // Set Url + guard let urlString = attributes["url"] as? String, + let mediaUrl = URL(string: urlString) else { + return nil + } + url = mediaUrl + + // Set Size + if let width = attributes["width"] as? CGFloat, + let height = attributes["height"] as? CGFloat + { + size = CGSize(width: width, height: height) + } + } +} + diff --git a/BulletinSDK/Modal/Message.swift b/BulletinSDK/Modal/Message.swift new file mode 100644 index 0000000..22f02d6 --- /dev/null +++ b/BulletinSDK/Modal/Message.swift @@ -0,0 +1,47 @@ +// +// Message.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + + +class Message: BulletinItem { + + + // MARK: - Enum + enum MessageType: String { + case text + case html + } + + + // MARK: - Variables + var messageType: MessageType = .text + var text: String? + + + // MARK: - Initialisation Methods + override init () { + super.init() + + // Set Type + type = .message + } + + required init?(attributes: [AnyHashable: Any]) { + super.init(attributes: attributes) + + // Set Message Type + if let messasgeTypeString = attributes["messageType"] as? String, + let messageType = MessageType(rawValue: messasgeTypeString) { + self.messageType = messageType + } + + // Set Text + guard let text = attributes["text"] as? String else { return nil } + self.text = text + } +} diff --git a/BulletinSDK/Modal/PreTitle.swift b/BulletinSDK/Modal/PreTitle.swift new file mode 100644 index 0000000..db8495e --- /dev/null +++ b/BulletinSDK/Modal/PreTitle.swift @@ -0,0 +1,32 @@ +// +// PreTitle.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + +class PreTitle: BulletinItem { + + + // MARK: - Variable + var text: String? + + + // MARK: - Initialisation Methods + override init () { + super.init() + + // Set Type + type = .preTitle + } + + required init?(attributes: [AnyHashable: Any]) { + super.init(attributes: attributes) + + // Set PreTitle + guard let text = attributes["text"] as? String else { return nil } + self.text = text + } +} diff --git a/BulletinSDK/Modal/Title.swift b/BulletinSDK/Modal/Title.swift new file mode 100644 index 0000000..6469863 --- /dev/null +++ b/BulletinSDK/Modal/Title.swift @@ -0,0 +1,33 @@ +// +// Title.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + +class Title: BulletinItem { + + + // MARK: - Variable + var text: String? + + + // MARK: - Initialisation Methods + override init () { + super.init() + + // Set Type + type = .title + } + + required init?(attributes: [AnyHashable: Any]) { + super.init(attributes: attributes) + + // Set PreTitle + guard let text = attributes["text"] as? String else { return nil } + self.text = text + } +} + diff --git a/Demo/Bulletin.xcodeproj/project.pbxproj b/Demo/Bulletin.xcodeproj/project.pbxproj new file mode 100644 index 0000000..0e680ff --- /dev/null +++ b/Demo/Bulletin.xcodeproj/project.pbxproj @@ -0,0 +1,428 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 815E3D7928DC85510042CEA5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3D7828DC85510042CEA5 /* AppDelegate.swift */; }; + 815E3D7B28DC85510042CEA5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3D7A28DC85510042CEA5 /* SceneDelegate.swift */; }; + 815E3D7D28DC85510042CEA5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3D7C28DC85510042CEA5 /* ViewController.swift */; }; + 815E3D8028DC85510042CEA5 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 815E3D7E28DC85510042CEA5 /* Main.storyboard */; }; + 815E3D8228DC85510042CEA5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 815E3D8128DC85510042CEA5 /* Assets.xcassets */; }; + 815E3D8528DC85510042CEA5 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 815E3D8328DC85510042CEA5 /* LaunchScreen.storyboard */; }; + 815E3DB228DC884C0042CEA5 /* String+Class.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DA728DC884C0042CEA5 /* String+Class.swift */; }; + 815E3DB328DC884C0042CEA5 /* BulletinDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DA828DC884C0042CEA5 /* BulletinDataStore.swift */; }; + 815E3DB428DC884C0042CEA5 /* BulletinItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAA28DC884C0042CEA5 /* BulletinItem.swift */; }; + 815E3DB528DC884C0042CEA5 /* PreTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAB28DC884C0042CEA5 /* PreTitle.swift */; }; + 815E3DB628DC884C0042CEA5 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAC28DC884C0042CEA5 /* Message.swift */; }; + 815E3DB728DC884C0042CEA5 /* BulletPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAD28DC884C0042CEA5 /* BulletPoint.swift */; }; + 815E3DB828DC884C0042CEA5 /* Title.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAE28DC884C0042CEA5 /* Title.swift */; }; + 815E3DB928DC884C0042CEA5 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAF28DC884C0042CEA5 /* Media.swift */; }; + 815E3DBA28DC884C0042CEA5 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DB028DC884C0042CEA5 /* ActionButton.swift */; }; + 815E3DBB28DC884C0042CEA5 /* BulletinSDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DB128DC884C0042CEA5 /* BulletinSDK.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 815E3D7528DC85510042CEA5 /* Bulletin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bulletin.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 815E3D7828DC85510042CEA5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 815E3D7A28DC85510042CEA5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 815E3D7C28DC85510042CEA5 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 815E3D7F28DC85510042CEA5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 815E3D8128DC85510042CEA5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 815E3D8428DC85510042CEA5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 815E3D8628DC85510042CEA5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 815E3DA728DC884C0042CEA5 /* String+Class.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Class.swift"; sourceTree = ""; }; + 815E3DA828DC884C0042CEA5 /* BulletinDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BulletinDataStore.swift; sourceTree = ""; }; + 815E3DAA28DC884C0042CEA5 /* BulletinItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BulletinItem.swift; sourceTree = ""; }; + 815E3DAB28DC884C0042CEA5 /* PreTitle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreTitle.swift; sourceTree = ""; }; + 815E3DAC28DC884C0042CEA5 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; + 815E3DAD28DC884C0042CEA5 /* BulletPoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BulletPoint.swift; sourceTree = ""; }; + 815E3DAE28DC884C0042CEA5 /* Title.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Title.swift; sourceTree = ""; }; + 815E3DAF28DC884C0042CEA5 /* Media.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = ""; }; + 815E3DB028DC884C0042CEA5 /* ActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; + 815E3DB128DC884C0042CEA5 /* BulletinSDK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BulletinSDK.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 815E3D7228DC85510042CEA5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 815E3D6C28DC85510042CEA5 = { + isa = PBXGroup; + children = ( + 815E3DA528DC884C0042CEA5 /* BulletinSDK */, + 815E3D7728DC85510042CEA5 /* Bulletin */, + 815E3D7628DC85510042CEA5 /* Products */, + ); + sourceTree = ""; + }; + 815E3D7628DC85510042CEA5 /* Products */ = { + isa = PBXGroup; + children = ( + 815E3D7528DC85510042CEA5 /* Bulletin.app */, + ); + name = Products; + sourceTree = ""; + }; + 815E3D7728DC85510042CEA5 /* Bulletin */ = { + isa = PBXGroup; + children = ( + 815E3D7828DC85510042CEA5 /* AppDelegate.swift */, + 815E3D7A28DC85510042CEA5 /* SceneDelegate.swift */, + 815E3D7C28DC85510042CEA5 /* ViewController.swift */, + 815E3D7E28DC85510042CEA5 /* Main.storyboard */, + 815E3D8128DC85510042CEA5 /* Assets.xcassets */, + 815E3D8328DC85510042CEA5 /* LaunchScreen.storyboard */, + 815E3D8628DC85510042CEA5 /* Info.plist */, + ); + path = Bulletin; + sourceTree = ""; + }; + 815E3DA528DC884C0042CEA5 /* BulletinSDK */ = { + isa = PBXGroup; + children = ( + 815E3DA928DC884C0042CEA5 /* Modal */, + 815E3DA628DC884C0042CEA5 /* Extension */, + 815E3DA828DC884C0042CEA5 /* BulletinDataStore.swift */, + 815E3DB128DC884C0042CEA5 /* BulletinSDK.swift */, + ); + name = BulletinSDK; + path = ../BulletinSDK; + sourceTree = ""; + }; + 815E3DA628DC884C0042CEA5 /* Extension */ = { + isa = PBXGroup; + children = ( + 815E3DA728DC884C0042CEA5 /* String+Class.swift */, + ); + path = Extension; + sourceTree = ""; + }; + 815E3DA928DC884C0042CEA5 /* Modal */ = { + isa = PBXGroup; + children = ( + 815E3DAA28DC884C0042CEA5 /* BulletinItem.swift */, + 815E3DAB28DC884C0042CEA5 /* PreTitle.swift */, + 815E3DAC28DC884C0042CEA5 /* Message.swift */, + 815E3DAD28DC884C0042CEA5 /* BulletPoint.swift */, + 815E3DAE28DC884C0042CEA5 /* Title.swift */, + 815E3DAF28DC884C0042CEA5 /* Media.swift */, + 815E3DB028DC884C0042CEA5 /* ActionButton.swift */, + ); + path = Modal; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 815E3D7428DC85510042CEA5 /* Bulletin */ = { + isa = PBXNativeTarget; + buildConfigurationList = 815E3D8928DC85510042CEA5 /* Build configuration list for PBXNativeTarget "Bulletin" */; + buildPhases = ( + 815E3D7128DC85510042CEA5 /* Sources */, + 815E3D7228DC85510042CEA5 /* Frameworks */, + 815E3D7328DC85510042CEA5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Bulletin; + productName = Bulletin; + productReference = 815E3D7528DC85510042CEA5 /* Bulletin.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 815E3D6D28DC85510042CEA5 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1400; + LastUpgradeCheck = 1400; + TargetAttributes = { + 815E3D7428DC85510042CEA5 = { + CreatedOnToolsVersion = 14.0; + }; + }; + }; + buildConfigurationList = 815E3D7028DC85510042CEA5 /* Build configuration list for PBXProject "Bulletin" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 815E3D6C28DC85510042CEA5; + productRefGroup = 815E3D7628DC85510042CEA5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 815E3D7428DC85510042CEA5 /* Bulletin */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 815E3D7328DC85510042CEA5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 815E3D8528DC85510042CEA5 /* LaunchScreen.storyboard in Resources */, + 815E3D8228DC85510042CEA5 /* Assets.xcassets in Resources */, + 815E3D8028DC85510042CEA5 /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 815E3D7128DC85510042CEA5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 815E3DB928DC884C0042CEA5 /* Media.swift in Sources */, + 815E3DB828DC884C0042CEA5 /* Title.swift in Sources */, + 815E3D7D28DC85510042CEA5 /* ViewController.swift in Sources */, + 815E3DB428DC884C0042CEA5 /* BulletinItem.swift in Sources */, + 815E3DB328DC884C0042CEA5 /* BulletinDataStore.swift in Sources */, + 815E3DBB28DC884C0042CEA5 /* BulletinSDK.swift in Sources */, + 815E3DB228DC884C0042CEA5 /* String+Class.swift in Sources */, + 815E3DB528DC884C0042CEA5 /* PreTitle.swift in Sources */, + 815E3DB728DC884C0042CEA5 /* BulletPoint.swift in Sources */, + 815E3DB628DC884C0042CEA5 /* Message.swift in Sources */, + 815E3D7928DC85510042CEA5 /* AppDelegate.swift in Sources */, + 815E3D7B28DC85510042CEA5 /* SceneDelegate.swift in Sources */, + 815E3DBA28DC884C0042CEA5 /* ActionButton.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXVariantGroup section */ + 815E3D7E28DC85510042CEA5 /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 815E3D7F28DC85510042CEA5 /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 815E3D8328DC85510042CEA5 /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 815E3D8428DC85510042CEA5 /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 815E3D8728DC85510042CEA5 /* 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++20"; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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 = ( + "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 = 13.0; + 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; + }; + 815E3D8828DC85510042CEA5 /* 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++20"; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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_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 = 13.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 815E3D8A28DC85510042CEA5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CAQ757V6Q3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Bulletin/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.wrx.Bulletin.Bulletin; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 815E3D8B28DC85510042CEA5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = CAQ757V6Q3; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Bulletin/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = Main; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.wrx.Bulletin.Bulletin; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 815E3D7028DC85510042CEA5 /* Build configuration list for PBXProject "Bulletin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 815E3D8728DC85510042CEA5 /* Debug */, + 815E3D8828DC85510042CEA5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 815E3D8928DC85510042CEA5 /* Build configuration list for PBXNativeTarget "Bulletin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 815E3D8A28DC85510042CEA5 /* Debug */, + 815E3D8B28DC85510042CEA5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 815E3D6D28DC85510042CEA5 /* Project object */; +} diff --git a/Demo/Bulletin.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Demo/Bulletin.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/Demo/Bulletin.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Demo/Bulletin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/Bulletin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Demo/Bulletin.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Demo/Bulletin/AppDelegate.swift b/Demo/Bulletin/AppDelegate.swift new file mode 100644 index 0000000..eb56155 --- /dev/null +++ b/Demo/Bulletin/AppDelegate.swift @@ -0,0 +1,36 @@ +// +// AppDelegate.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Override point for customization after application launch. + return true + } + + // MARK: UISceneSession Lifecycle + + func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { + // Called when a new scene session is being created. + // Use this method to select a configuration to create the new scene with. + return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } + + func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { + // Called when the user discards a scene session. + // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. + // Use this method to release any resources that were specific to the discarded scenes, as they will not return. + } + + +} + diff --git a/Demo/Bulletin/Assets.xcassets/AccentColor.colorset/Contents.json b/Demo/Bulletin/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/Demo/Bulletin/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Bulletin/Assets.xcassets/AppIcon.appiconset/Contents.json b/Demo/Bulletin/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/Demo/Bulletin/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Bulletin/Assets.xcassets/Contents.json b/Demo/Bulletin/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/Demo/Bulletin/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Demo/Bulletin/Base.lproj/LaunchScreen.storyboard b/Demo/Bulletin/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..865e932 --- /dev/null +++ b/Demo/Bulletin/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Bulletin/Base.lproj/Main.storyboard b/Demo/Bulletin/Base.lproj/Main.storyboard new file mode 100644 index 0000000..25a7638 --- /dev/null +++ b/Demo/Bulletin/Base.lproj/Main.storyboard @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Bulletin/Info.plist b/Demo/Bulletin/Info.plist new file mode 100644 index 0000000..dd3c9af --- /dev/null +++ b/Demo/Bulletin/Info.plist @@ -0,0 +1,25 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + + diff --git a/Demo/Bulletin/SceneDelegate.swift b/Demo/Bulletin/SceneDelegate.swift new file mode 100644 index 0000000..230eb88 --- /dev/null +++ b/Demo/Bulletin/SceneDelegate.swift @@ -0,0 +1,52 @@ +// +// SceneDelegate.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + var window: UIWindow? + + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. + // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. + // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). + guard let _ = (scene as? UIWindowScene) else { return } + } + + func sceneDidDisconnect(_ scene: UIScene) { + // Called as the scene is being released by the system. + // This occurs shortly after the scene enters the background, or when its session is discarded. + // Release any resources associated with this scene that can be re-created the next time the scene connects. + // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). + } + + func sceneDidBecomeActive(_ scene: UIScene) { + // Called when the scene has moved from an inactive state to an active state. + // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. + } + + func sceneWillResignActive(_ scene: UIScene) { + // Called when the scene will move from an active state to an inactive state. + // This may occur due to temporary interruptions (ex. an incoming phone call). + } + + func sceneWillEnterForeground(_ scene: UIScene) { + // Called as the scene transitions from the background to the foreground. + // Use this method to undo the changes made on entering the background. + } + + func sceneDidEnterBackground(_ scene: UIScene) { + // Called as the scene transitions from the foreground to the background. + // Use this method to save data, release shared resources, and store enough scene-specific state information + // to restore the scene back to its current state. + } + + +} + diff --git a/Demo/Bulletin/ViewController.swift b/Demo/Bulletin/ViewController.swift new file mode 100644 index 0000000..360c4a8 --- /dev/null +++ b/Demo/Bulletin/ViewController.swift @@ -0,0 +1,86 @@ +// +// ViewController.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + +class ViewController: UIViewController { + + override func viewDidLoad() { + super.viewDidLoad() + // Do any additional setup after loading the view. + + let dataSource = BulletinDataStore() + + /// v 1 .13 ////// + let preTitle = PreTitle() + preTitle.text = "Version 1.13" + + let title = Title() + title.text = "Introducing Crypto Gifts" + + let message = Message() + message.messageType = .text + message.text = "Vestibulum id ligula porta felis euismod semper. Morbi leo risus, porta ac consectetur ac, vestibulum at eros." + + let media = Media() + media.url = URL(string: "https://media.wazirx.com/action_cards/coin_reports.jpg") + media.size = CGSize(width: 1200, height: 500) + + let bullet = Bullet() + bullet.bulletType = .unicode + bullet.unicode = "U+0031" + + let bulletPoint = BulletPoint() + bulletPoint.bullet = bullet + bulletPoint.titleText = "Vestibulum" + bulletPoint.subTitleText = "Etiam porta sem malesuada magna mollis euismod." + + let actionButton = ActionButton() + actionButton.title = "Take me to Crypto Gifts ->" + + let bulletItems = [preTitle, title, message, media, bulletPoint, actionButton] + + dataSource.registerVersionInfo(version: "1.13", items: bulletItems) + /// v 1 .13 ///// + + /// v 1 .13.1 ////// + let preTitle1 = PreTitle() + preTitle1.text = "Version 1.13.1" + + let title1 = Title() + title1.text = "In this update" + + let message1 = Message() + message1.messageType = .text + message1.text = "Vestibulum id ligula porta felis euismod semper." + + let bullet1 = Bullet() + bullet1.bulletType = .unicode + bullet1.unicode = "U+0031" + + let bulletPoint1 = BulletPoint() + bulletPoint1.bullet = bullet1 + bulletPoint1.titleText = "Vestibulum" + bulletPoint1.subTitleText = "Etiam porta sem malesuada magna mollis euismod." + + let bulletPoint2 = BulletPoint() + bulletPoint2.bullet = bullet1 + bulletPoint2.titleText = "Justo Condimentum" + bulletPoint2.subTitleText = "Sed posuere consectetur est at lobortis. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor." + + let bulletItems1 = [preTitle1, title1, message1, bulletPoint1, bulletPoint2] + + dataSource.registerVersionInfo(version: "1.13.1", items: bulletItems1) + /// v 1 .13 .1///// + + let sdk = BulletinSDK(dataStore: dataSource) + sdk.showBulletin(fromVersion: "1.13") + } + + +} + From f528010cd918ab8b91d7b977c00cf1af750806bf Mon Sep 17 00:00:00 2001 From: Pranav Date: Wed, 28 Sep 2022 11:24:29 +0530 Subject: [PATCH 02/12] Version sorting and comparison logic added --- BulletinSDK/BulletinDataStore.swift | 45 +++++++++---- BulletinSDK/BulletinSDK.swift | 37 ++++++++-- .../String+Class.swift | 0 BulletinSDK/Extensions/String+Version.swift | 41 ++++++++++++ .../Extensions/UserDefaults+Storage.swift | 37 ++++++++++ .../{Modal => Modals}/ActionButton.swift | 0 .../{Modal => Modals}/BulletPoint.swift | 0 BulletinSDK/Modals/BulletinInfo.swift | 29 ++++++++ .../{Modal => Modals}/BulletinItem.swift | 0 BulletinSDK/{Modal => Modals}/Media.swift | 0 BulletinSDK/{Modal => Modals}/Message.swift | 0 BulletinSDK/{Modal => Modals}/PreTitle.swift | 0 BulletinSDK/{Modal => Modals}/Title.swift | 0 BulletinSDK/Modals/Version.swift | 67 +++++++++++++++++++ Demo/Bulletin.xcodeproj/project.pbxproj | 32 +++++++-- 15 files changed, 262 insertions(+), 26 deletions(-) rename BulletinSDK/{Extension => Extensions}/String+Class.swift (100%) create mode 100644 BulletinSDK/Extensions/String+Version.swift create mode 100644 BulletinSDK/Extensions/UserDefaults+Storage.swift rename BulletinSDK/{Modal => Modals}/ActionButton.swift (100%) rename BulletinSDK/{Modal => Modals}/BulletPoint.swift (100%) create mode 100644 BulletinSDK/Modals/BulletinInfo.swift rename BulletinSDK/{Modal => Modals}/BulletinItem.swift (100%) rename BulletinSDK/{Modal => Modals}/Media.swift (100%) rename BulletinSDK/{Modal => Modals}/Message.swift (100%) rename BulletinSDK/{Modal => Modals}/PreTitle.swift (100%) rename BulletinSDK/{Modal => Modals}/Title.swift (100%) create mode 100644 BulletinSDK/Modals/Version.swift diff --git a/BulletinSDK/BulletinDataStore.swift b/BulletinSDK/BulletinDataStore.swift index e744843..e1e3ca9 100644 --- a/BulletinSDK/BulletinDataStore.swift +++ b/BulletinSDK/BulletinDataStore.swift @@ -7,13 +7,11 @@ import Foundation -typealias Version = String - class BulletinDataStore { // MARK: - Variable - public private(set) var data = [Version: [BulletinItem]]() + public private(set) var data = [BulletinInfo]() // MARK: - Initialisation Methods @@ -37,22 +35,41 @@ class BulletinDataStore { } public func registerVersionInfo(version: Version, items: [BulletinItem]) { - if items.isEmpty == false { - self.data[version] = items + + // Validation + guard items.isEmpty == false else { return } + + // Create Bulletin Info Object + let bulletinInfo = BulletinInfo(version: version, items: items) + + for (i,item) in data.enumerated() { + + // Check If Same Version Available In Data + if item.version == version { + + // Remove Value + data.remove(at: i) + + // Add New Value + data.append(bulletinInfo) + return + } } + + data.append(bulletinInfo) } - public func getData(fromVersion version: Version, limit: Int? = nil) { + public func getData(fromNewVersion newVersion: Version?, toOldVersion oldVersion: Version?, limit: Int? = nil) -> [BulletinInfo]? { + + // Sort Bulletin Info + let sorttedItems = data.sorted(by: { $0.version > $1.version }) -// for appVersion in self.data { -// if appVersion.key == version { -// let bulletinItem = self.data[version] -// for item in bulletinItem { -// print("✅\(item.text)") -// } -// } -// } + // Validation For Limit + guard let limit = limit else { + return sorttedItems + } + return nil } } diff --git a/BulletinSDK/BulletinSDK.swift b/BulletinSDK/BulletinSDK.swift index d57a453..3c61f39 100644 --- a/BulletinSDK/BulletinSDK.swift +++ b/BulletinSDK/BulletinSDK.swift @@ -23,15 +23,42 @@ class BulletinSDK { // MARK: - Helper Methods - func showFullBulletin() { + public func showFullBulletin() -> Bool { + // Get Bulletin Items + let items = dataStore.getData(fromNewVersion: nil, toOldVersion: nil) + + // Show Bulletin + return showBulletin(items: items) } - func showBulletinFromLastVersion(limit: Int? = nil) { - + public func showLastBulletins(limit: Int = 1) -> Bool { + + // Get Bulletin Items + let items = dataStore.getData(fromNewVersion: nil, toOldVersion: nil, limit: limit) + + // Show Bulletin + return showBulletin(items: items) } - func showBulletin(fromVersion version: String, limit: Int? = nil) { - dataStore.getData(fromVersion: version) + public func showUnseenBulletins(limit: Int? = nil) -> Bool { + + // Get Last Seen Version + guard let lastSeenVersion = UserDefaults.lastSeenVersion else { return false } + + // Get Bulletin Items + let items = dataStore.getData(fromNewVersion: lastSeenVersion, toOldVersion: nil, limit: limit) + + // Show Bulletin + return showBulletin(items: items) + } + + private func showBulletin(items: [BulletinInfo]?) -> Bool { + + // Validation + guard let items = items, + items.isEmpty == false else { return false } + + return false } } diff --git a/BulletinSDK/Extension/String+Class.swift b/BulletinSDK/Extensions/String+Class.swift similarity index 100% rename from BulletinSDK/Extension/String+Class.swift rename to BulletinSDK/Extensions/String+Class.swift diff --git a/BulletinSDK/Extensions/String+Version.swift b/BulletinSDK/Extensions/String+Version.swift new file mode 100644 index 0000000..8ab318e --- /dev/null +++ b/BulletinSDK/Extensions/String+Version.swift @@ -0,0 +1,41 @@ +// +// String+Version.swift +// Bulletin +// +// Created by Pranav Panchal on 28/09/22. +// Copyright © 2022 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import Foundation + + +extension String { + + var isNumeric: Bool { + guard self.count > 0 else { return false } + let nums: Set = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"] + return Set(self).isSubset(of: nums) + } + + func validateVersion() -> String? { + + // Convert String In to Array + let versionBlocks = self.components(separatedBy: ".") + + // Validation + guard versionBlocks.isEmpty == false else { return nil } + + // Validation For String as Int + for versionBlock in versionBlocks { + if versionBlock.isNumeric == false { + return nil + } + } + + return versionBlocks.joined(separator: ".") + } + + func versionCompare(_ otherVersion: String) -> ComparisonResult { + return self.compare(otherVersion, options: .numeric) + } +} diff --git a/BulletinSDK/Extensions/UserDefaults+Storage.swift b/BulletinSDK/Extensions/UserDefaults+Storage.swift new file mode 100644 index 0000000..c38ae5a --- /dev/null +++ b/BulletinSDK/Extensions/UserDefaults+Storage.swift @@ -0,0 +1,37 @@ +// +// UserDefaults+Storage.swift +// Bulletin +// +// Created by Pranav Panchal on 23/09/22. +// Copyright © 2022 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import Foundation + + +extension UserDefaults { + + class var bulletin: UserDefaults { + get { + return UserDefaults(suiteName: "Bulletin")! + } + set {} + } + + class var lastSeenVersion: Version? { + get { + + // Validation + guard let versionString = bulletin.string(forKey: "BLLastSeenVersion") else { return nil } + + return Version(versionString) + } + set { + if let newValue = newValue { + bulletin.set(newValue.version, forKey: "BLLastSeenVersion") + } else { + bulletin.removeObject(forKey: "BLLastSeenVersion") + } + } + } +} diff --git a/BulletinSDK/Modal/ActionButton.swift b/BulletinSDK/Modals/ActionButton.swift similarity index 100% rename from BulletinSDK/Modal/ActionButton.swift rename to BulletinSDK/Modals/ActionButton.swift diff --git a/BulletinSDK/Modal/BulletPoint.swift b/BulletinSDK/Modals/BulletPoint.swift similarity index 100% rename from BulletinSDK/Modal/BulletPoint.swift rename to BulletinSDK/Modals/BulletPoint.swift diff --git a/BulletinSDK/Modals/BulletinInfo.swift b/BulletinSDK/Modals/BulletinInfo.swift new file mode 100644 index 0000000..b93bbe5 --- /dev/null +++ b/BulletinSDK/Modals/BulletinInfo.swift @@ -0,0 +1,29 @@ +// +// BulletinInfo.swift +// Bulletin +// +// Created by Pranav Panchal on 27/09/22. +// Copyright © 2022 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import Foundation + + +class BulletinInfo { + + + // MARK: - Variables + public var version: Version + public var items: [BulletinItem] + + + // MARK: - Initialisation Method + init(version: Version, items: [BulletinItem]) { + + // Set Version + self.version = version + + // Set Items + self.items = items + } +} diff --git a/BulletinSDK/Modal/BulletinItem.swift b/BulletinSDK/Modals/BulletinItem.swift similarity index 100% rename from BulletinSDK/Modal/BulletinItem.swift rename to BulletinSDK/Modals/BulletinItem.swift diff --git a/BulletinSDK/Modal/Media.swift b/BulletinSDK/Modals/Media.swift similarity index 100% rename from BulletinSDK/Modal/Media.swift rename to BulletinSDK/Modals/Media.swift diff --git a/BulletinSDK/Modal/Message.swift b/BulletinSDK/Modals/Message.swift similarity index 100% rename from BulletinSDK/Modal/Message.swift rename to BulletinSDK/Modals/Message.swift diff --git a/BulletinSDK/Modal/PreTitle.swift b/BulletinSDK/Modals/PreTitle.swift similarity index 100% rename from BulletinSDK/Modal/PreTitle.swift rename to BulletinSDK/Modals/PreTitle.swift diff --git a/BulletinSDK/Modal/Title.swift b/BulletinSDK/Modals/Title.swift similarity index 100% rename from BulletinSDK/Modal/Title.swift rename to BulletinSDK/Modals/Title.swift diff --git a/BulletinSDK/Modals/Version.swift b/BulletinSDK/Modals/Version.swift new file mode 100644 index 0000000..12ec442 --- /dev/null +++ b/BulletinSDK/Modals/Version.swift @@ -0,0 +1,67 @@ +// +// Version.swift +// Bulletin +// +// Created by Pranav Panchal on 28/09/22. +// Copyright © 2022 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import Foundation + + +struct Version { + + + // MARK: - Variable + public let version: String + + + // MARK: - Initialisation Method + init?(_ version: String) { + + // Validation + guard let version = version.validateVersion() else { return nil } + + // Set Version + self.version = version + } + +} + +extension Version: Equatable { + static func == (lhs: Version, rhs: Version) -> Bool { + return lhs.version == rhs.version + } +} + +extension Version: Comparable { + static func < (lhs: Version, rhs: Version) -> Bool { + if lhs.version.versionCompare(rhs.version) == .orderedAscending { + return true + } + return false + } + + static func <= (lhs: Version, rhs: Version) -> Bool { + let result = lhs.version.versionCompare(rhs.version) + if result == .orderedAscending || result == .orderedSame { + return true + } + return false + } + + static func > (lhs: Version, rhs: Version) -> Bool { + if lhs.version.versionCompare(rhs.version) == .orderedDescending { + return true + } + return false + } + + static func >= (lhs: Version, rhs: Version) -> Bool { + let result = lhs.version.versionCompare(rhs.version) + if result == .orderedDescending || result == .orderedSame { + return true + } + return false + } +} diff --git a/Demo/Bulletin.xcodeproj/project.pbxproj b/Demo/Bulletin.xcodeproj/project.pbxproj index 0e680ff..4d6d953 100644 --- a/Demo/Bulletin.xcodeproj/project.pbxproj +++ b/Demo/Bulletin.xcodeproj/project.pbxproj @@ -23,6 +23,10 @@ 815E3DB928DC884C0042CEA5 /* Media.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAF28DC884C0042CEA5 /* Media.swift */; }; 815E3DBA28DC884C0042CEA5 /* ActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DB028DC884C0042CEA5 /* ActionButton.swift */; }; 815E3DBB28DC884C0042CEA5 /* BulletinSDK.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DB128DC884C0042CEA5 /* BulletinSDK.swift */; }; + 819772DC28E2CBEF00962913 /* BulletinInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819772DB28E2CBEF00962913 /* BulletinInfo.swift */; }; + 819772DE28E40B8100962913 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819772DD28E40B8100962913 /* Version.swift */; }; + 819772E028E40C4800962913 /* String+Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819772DF28E40C4800962913 /* String+Version.swift */; }; + 81E010E228DDA2D9003FFE79 /* UserDefaults+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81E010E128DDA2D9003FFE79 /* UserDefaults+Storage.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -44,6 +48,10 @@ 815E3DAF28DC884C0042CEA5 /* Media.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Media.swift; sourceTree = ""; }; 815E3DB028DC884C0042CEA5 /* ActionButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionButton.swift; sourceTree = ""; }; 815E3DB128DC884C0042CEA5 /* BulletinSDK.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BulletinSDK.swift; sourceTree = ""; }; + 819772DB28E2CBEF00962913 /* BulletinInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinInfo.swift; sourceTree = ""; }; + 819772DD28E40B8100962913 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; + 819772DF28E40C4800962913 /* String+Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Version.swift"; sourceTree = ""; }; + 81E010E128DDA2D9003FFE79 /* UserDefaults+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Storage.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -91,24 +99,26 @@ 815E3DA528DC884C0042CEA5 /* BulletinSDK */ = { isa = PBXGroup; children = ( - 815E3DA928DC884C0042CEA5 /* Modal */, - 815E3DA628DC884C0042CEA5 /* Extension */, - 815E3DA828DC884C0042CEA5 /* BulletinDataStore.swift */, + 815E3DA928DC884C0042CEA5 /* Modals */, + 815E3DA628DC884C0042CEA5 /* Extensions */, 815E3DB128DC884C0042CEA5 /* BulletinSDK.swift */, + 815E3DA828DC884C0042CEA5 /* BulletinDataStore.swift */, ); name = BulletinSDK; path = ../BulletinSDK; sourceTree = ""; }; - 815E3DA628DC884C0042CEA5 /* Extension */ = { + 815E3DA628DC884C0042CEA5 /* Extensions */ = { isa = PBXGroup; children = ( 815E3DA728DC884C0042CEA5 /* String+Class.swift */, + 81E010E128DDA2D9003FFE79 /* UserDefaults+Storage.swift */, + 819772DF28E40C4800962913 /* String+Version.swift */, ); - path = Extension; + path = Extensions; sourceTree = ""; }; - 815E3DA928DC884C0042CEA5 /* Modal */ = { + 815E3DA928DC884C0042CEA5 /* Modals */ = { isa = PBXGroup; children = ( 815E3DAA28DC884C0042CEA5 /* BulletinItem.swift */, @@ -118,8 +128,10 @@ 815E3DAE28DC884C0042CEA5 /* Title.swift */, 815E3DAF28DC884C0042CEA5 /* Media.swift */, 815E3DB028DC884C0042CEA5 /* ActionButton.swift */, + 819772DB28E2CBEF00962913 /* BulletinInfo.swift */, + 819772DD28E40B8100962913 /* Version.swift */, ); - path = Modal; + path = Modals; sourceTree = ""; }; /* End PBXGroup section */ @@ -149,8 +161,10 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; + CLASSPREFIX = ""; LastSwiftUpdateCheck = 1400; LastUpgradeCheck = 1400; + ORGANIZATIONNAME = "Copyright © 2022 Zanmai Labs Private Limited."; TargetAttributes = { 815E3D7428DC85510042CEA5 = { CreatedOnToolsVersion = 14.0; @@ -197,11 +211,15 @@ 815E3DB828DC884C0042CEA5 /* Title.swift in Sources */, 815E3D7D28DC85510042CEA5 /* ViewController.swift in Sources */, 815E3DB428DC884C0042CEA5 /* BulletinItem.swift in Sources */, + 81E010E228DDA2D9003FFE79 /* UserDefaults+Storage.swift in Sources */, 815E3DB328DC884C0042CEA5 /* BulletinDataStore.swift in Sources */, 815E3DBB28DC884C0042CEA5 /* BulletinSDK.swift in Sources */, + 819772DE28E40B8100962913 /* Version.swift in Sources */, 815E3DB228DC884C0042CEA5 /* String+Class.swift in Sources */, 815E3DB528DC884C0042CEA5 /* PreTitle.swift in Sources */, + 819772E028E40C4800962913 /* String+Version.swift in Sources */, 815E3DB728DC884C0042CEA5 /* BulletPoint.swift in Sources */, + 819772DC28E2CBEF00962913 /* BulletinInfo.swift in Sources */, 815E3DB628DC884C0042CEA5 /* Message.swift in Sources */, 815E3D7928DC85510042CEA5 /* AppDelegate.swift in Sources */, 815E3D7B28DC85510042CEA5 /* SceneDelegate.swift in Sources */, From 31d17d9b808bbce069d8a8cd3d48609b1cfff602 Mon Sep 17 00:00:00 2001 From: Pranav Date: Wed, 28 Sep 2022 11:44:29 +0530 Subject: [PATCH 03/12] restructuring of ui modals --- BulletinSDK/Modals/PreTitle.swift | 32 ------------- BulletinSDK/Modals/Title.swift | 33 -------------- .../Modals/{ => UI Modals}/ActionButton.swift | 0 .../Modals/{ => UI Modals}/BulletPoint.swift | 0 .../Modals/{ => UI Modals}/BulletinItem.swift | 1 - .../Modals/{ => UI Modals}/Media.swift | 0 .../Modals/{ => UI Modals}/Message.swift | 0 BulletinSDK/Modals/UI Modals/Title.swift | 45 +++++++++++++++++++ Demo/Bulletin.xcodeproj/project.pbxproj | 20 +++++---- 9 files changed, 57 insertions(+), 74 deletions(-) delete mode 100644 BulletinSDK/Modals/PreTitle.swift delete mode 100644 BulletinSDK/Modals/Title.swift rename BulletinSDK/Modals/{ => UI Modals}/ActionButton.swift (100%) rename BulletinSDK/Modals/{ => UI Modals}/BulletPoint.swift (100%) rename BulletinSDK/Modals/{ => UI Modals}/BulletinItem.swift (98%) rename BulletinSDK/Modals/{ => UI Modals}/Media.swift (100%) rename BulletinSDK/Modals/{ => UI Modals}/Message.swift (100%) create mode 100644 BulletinSDK/Modals/UI Modals/Title.swift diff --git a/BulletinSDK/Modals/PreTitle.swift b/BulletinSDK/Modals/PreTitle.swift deleted file mode 100644 index db8495e..0000000 --- a/BulletinSDK/Modals/PreTitle.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// PreTitle.swift -// Bulletin -// -// Created by Pranav Panchal on 22/09/22. -// - -import UIKit - -class PreTitle: BulletinItem { - - - // MARK: - Variable - var text: String? - - - // MARK: - Initialisation Methods - override init () { - super.init() - - // Set Type - type = .preTitle - } - - required init?(attributes: [AnyHashable: Any]) { - super.init(attributes: attributes) - - // Set PreTitle - guard let text = attributes["text"] as? String else { return nil } - self.text = text - } -} diff --git a/BulletinSDK/Modals/Title.swift b/BulletinSDK/Modals/Title.swift deleted file mode 100644 index 6469863..0000000 --- a/BulletinSDK/Modals/Title.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// Title.swift -// Bulletin -// -// Created by Pranav Panchal on 22/09/22. -// - -import UIKit - -class Title: BulletinItem { - - - // MARK: - Variable - var text: String? - - - // MARK: - Initialisation Methods - override init () { - super.init() - - // Set Type - type = .title - } - - required init?(attributes: [AnyHashable: Any]) { - super.init(attributes: attributes) - - // Set PreTitle - guard let text = attributes["text"] as? String else { return nil } - self.text = text - } -} - diff --git a/BulletinSDK/Modals/ActionButton.swift b/BulletinSDK/Modals/UI Modals/ActionButton.swift similarity index 100% rename from BulletinSDK/Modals/ActionButton.swift rename to BulletinSDK/Modals/UI Modals/ActionButton.swift diff --git a/BulletinSDK/Modals/BulletPoint.swift b/BulletinSDK/Modals/UI Modals/BulletPoint.swift similarity index 100% rename from BulletinSDK/Modals/BulletPoint.swift rename to BulletinSDK/Modals/UI Modals/BulletPoint.swift diff --git a/BulletinSDK/Modals/BulletinItem.swift b/BulletinSDK/Modals/UI Modals/BulletinItem.swift similarity index 98% rename from BulletinSDK/Modals/BulletinItem.swift rename to BulletinSDK/Modals/UI Modals/BulletinItem.swift index e700998..21559b2 100644 --- a/BulletinSDK/Modals/BulletinItem.swift +++ b/BulletinSDK/Modals/UI Modals/BulletinItem.swift @@ -13,7 +13,6 @@ class BulletinItem { // MARK: - Declarations public enum ItemType : String { case undefined - case preTitle case title case message case media diff --git a/BulletinSDK/Modals/Media.swift b/BulletinSDK/Modals/UI Modals/Media.swift similarity index 100% rename from BulletinSDK/Modals/Media.swift rename to BulletinSDK/Modals/UI Modals/Media.swift diff --git a/BulletinSDK/Modals/Message.swift b/BulletinSDK/Modals/UI Modals/Message.swift similarity index 100% rename from BulletinSDK/Modals/Message.swift rename to BulletinSDK/Modals/UI Modals/Message.swift diff --git a/BulletinSDK/Modals/UI Modals/Title.swift b/BulletinSDK/Modals/UI Modals/Title.swift new file mode 100644 index 0000000..b7bba97 --- /dev/null +++ b/BulletinSDK/Modals/UI Modals/Title.swift @@ -0,0 +1,45 @@ +// +// Title.swift +// Bulletin +// +// Created by Pranav Panchal on 22/09/22. +// + +import UIKit + +class Title: BulletinItem { + + + // MARK: - Variable + var preTitleText: String? + var titleText: String? + var subTitleText: String? + + + // MARK: - Initialisation Methods + override init () { + super.init() + + // Set Type + type = .title + } + + required init?(attributes: [AnyHashable: Any]) { + super.init(attributes: attributes) + + // Set Pre Title + preTitleText = attributes["preTitleText"] as? String + + // Set Title + titleText = attributes["titleText"] as? String + + // Set Sub Title + subTitleText = attributes["subTitleText"] as? String + + // Validations + if preTitleText == nil && titleText == nil && subTitleText == nil { + return nil + } + } +} + diff --git a/Demo/Bulletin.xcodeproj/project.pbxproj b/Demo/Bulletin.xcodeproj/project.pbxproj index 4d6d953..deffa5f 100644 --- a/Demo/Bulletin.xcodeproj/project.pbxproj +++ b/Demo/Bulletin.xcodeproj/project.pbxproj @@ -16,7 +16,6 @@ 815E3DB228DC884C0042CEA5 /* String+Class.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DA728DC884C0042CEA5 /* String+Class.swift */; }; 815E3DB328DC884C0042CEA5 /* BulletinDataStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DA828DC884C0042CEA5 /* BulletinDataStore.swift */; }; 815E3DB428DC884C0042CEA5 /* BulletinItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAA28DC884C0042CEA5 /* BulletinItem.swift */; }; - 815E3DB528DC884C0042CEA5 /* PreTitle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAB28DC884C0042CEA5 /* PreTitle.swift */; }; 815E3DB628DC884C0042CEA5 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAC28DC884C0042CEA5 /* Message.swift */; }; 815E3DB728DC884C0042CEA5 /* BulletPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAD28DC884C0042CEA5 /* BulletPoint.swift */; }; 815E3DB828DC884C0042CEA5 /* Title.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3DAE28DC884C0042CEA5 /* Title.swift */; }; @@ -41,7 +40,6 @@ 815E3DA728DC884C0042CEA5 /* String+Class.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Class.swift"; sourceTree = ""; }; 815E3DA828DC884C0042CEA5 /* BulletinDataStore.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BulletinDataStore.swift; sourceTree = ""; }; 815E3DAA28DC884C0042CEA5 /* BulletinItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BulletinItem.swift; sourceTree = ""; }; - 815E3DAB28DC884C0042CEA5 /* PreTitle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreTitle.swift; sourceTree = ""; }; 815E3DAC28DC884C0042CEA5 /* Message.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = ""; }; 815E3DAD28DC884C0042CEA5 /* BulletPoint.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BulletPoint.swift; sourceTree = ""; }; 815E3DAE28DC884C0042CEA5 /* Title.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Title.swift; sourceTree = ""; }; @@ -119,19 +117,26 @@ sourceTree = ""; }; 815E3DA928DC884C0042CEA5 /* Modals */ = { + isa = PBXGroup; + children = ( + 819772E128E41D8200962913 /* UI Modals */, + 819772DB28E2CBEF00962913 /* BulletinInfo.swift */, + 819772DD28E40B8100962913 /* Version.swift */, + ); + path = Modals; + sourceTree = ""; + }; + 819772E128E41D8200962913 /* UI Modals */ = { isa = PBXGroup; children = ( 815E3DAA28DC884C0042CEA5 /* BulletinItem.swift */, - 815E3DAB28DC884C0042CEA5 /* PreTitle.swift */, + 815E3DAE28DC884C0042CEA5 /* Title.swift */, 815E3DAC28DC884C0042CEA5 /* Message.swift */, 815E3DAD28DC884C0042CEA5 /* BulletPoint.swift */, - 815E3DAE28DC884C0042CEA5 /* Title.swift */, 815E3DAF28DC884C0042CEA5 /* Media.swift */, 815E3DB028DC884C0042CEA5 /* ActionButton.swift */, - 819772DB28E2CBEF00962913 /* BulletinInfo.swift */, - 819772DD28E40B8100962913 /* Version.swift */, ); - path = Modals; + path = "UI Modals"; sourceTree = ""; }; /* End PBXGroup section */ @@ -216,7 +221,6 @@ 815E3DBB28DC884C0042CEA5 /* BulletinSDK.swift in Sources */, 819772DE28E40B8100962913 /* Version.swift in Sources */, 815E3DB228DC884C0042CEA5 /* String+Class.swift in Sources */, - 815E3DB528DC884C0042CEA5 /* PreTitle.swift in Sources */, 819772E028E40C4800962913 /* String+Version.swift in Sources */, 815E3DB728DC884C0042CEA5 /* BulletPoint.swift in Sources */, 819772DC28E2CBEF00962913 /* BulletinInfo.swift in Sources */, From 3bc3daae65a9f2e60e23f245dac0160d3bf43008 Mon Sep 17 00:00:00 2001 From: Pranav Date: Tue, 14 Feb 2023 12:03:01 +0530 Subject: [PATCH 04/12] datastore file --- BulletinSDK/BulletinDataStore.swift | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/BulletinSDK/BulletinDataStore.swift b/BulletinSDK/BulletinDataStore.swift index e1e3ca9..d4535b6 100644 --- a/BulletinSDK/BulletinDataStore.swift +++ b/BulletinSDK/BulletinDataStore.swift @@ -62,14 +62,26 @@ class BulletinDataStore { public func getData(fromNewVersion newVersion: Version?, toOldVersion oldVersion: Version?, limit: Int? = nil) -> [BulletinInfo]? { // Sort Bulletin Info - let sorttedItems = data.sorted(by: { $0.version > $1.version }) + let sortedInfos = data.sorted(by: { $0.version > $1.version }) - // Validation For Limit - guard let limit = limit else { - return sorttedItems + var filteredInfos = [BulletinInfo]() + for info in sortedInfos { + + // Add Info Only When Conditions Are True + if let newVersion = newVersion, info.version <= newVersion { + filteredInfos.append(info) + } else if let oldVersion = oldVersion, info.version >= oldVersion { + filteredInfos.append(info) + } else { + filteredInfos.append(info) + } + + // Validation For Limit + if let limit = limit, filteredInfos.count >= limit { + return filteredInfos + } } - - return nil + + return filteredInfos } - } From 6d88be81e7b70b12ed795f6051b1a20340508388 Mon Sep 17 00:00:00 2001 From: Pranav Date: Wed, 12 Jul 2023 16:49:33 +0530 Subject: [PATCH 05/12] commit 12thJuly --- BulletinSDK/Modals/UI Modals/Title.swift | 10 ++- Demo/Bulletin/ViewController.swift | 99 ++++++++++-------------- 2 files changed, 49 insertions(+), 60 deletions(-) diff --git a/BulletinSDK/Modals/UI Modals/Title.swift b/BulletinSDK/Modals/UI Modals/Title.swift index b7bba97..07195c6 100644 --- a/BulletinSDK/Modals/UI Modals/Title.swift +++ b/BulletinSDK/Modals/UI Modals/Title.swift @@ -12,8 +12,11 @@ class Title: BulletinItem { // MARK: - Variable var preTitleText: String? + public var preTitleTextColor: UIColor? var titleText: String? + public var titleTextColor: UIColor? var subTitleText: String? + public var subTitleTextColor: UIColor? // MARK: - Initialisation Methods @@ -30,12 +33,17 @@ class Title: BulletinItem { // Set Pre Title preTitleText = attributes["preTitleText"] as? String + // Set Pre Title Color + if let preTitleTextColor = attributes["preTitleTextColor"] as? String { + self.preTitleTextColor = preTitleTextColor + } + // Set Title titleText = attributes["titleText"] as? String // Set Sub Title subTitleText = attributes["subTitleText"] as? String - + // Validations if preTitleText == nil && titleText == nil && subTitleText == nil { return nil diff --git a/Demo/Bulletin/ViewController.swift b/Demo/Bulletin/ViewController.swift index 360c4a8..08e48cf 100644 --- a/Demo/Bulletin/ViewController.swift +++ b/Demo/Bulletin/ViewController.swift @@ -13,74 +13,55 @@ class ViewController: UIViewController { super.viewDidLoad() // Do any additional setup after loading the view. + // Creage DataStore Object let dataSource = BulletinDataStore() - - /// v 1 .13 ////// - let preTitle = PreTitle() - preTitle.text = "Version 1.13" - - let title = Title() - title.text = "Introducing Crypto Gifts" - - let message = Message() - message.messageType = .text - message.text = "Vestibulum id ligula porta felis euismod semper. Morbi leo risus, porta ac consectetur ac, vestibulum at eros." - - let media = Media() - media.url = URL(string: "https://media.wazirx.com/action_cards/coin_reports.jpg") - media.size = CGSize(width: 1200, height: 500) - - let bullet = Bullet() - bullet.bulletType = .unicode - bullet.unicode = "U+0031" - let bulletPoint = BulletPoint() - bulletPoint.bullet = bullet - bulletPoint.titleText = "Vestibulum" - bulletPoint.subTitleText = "Etiam porta sem malesuada magna mollis euismod." - - let actionButton = ActionButton() - actionButton.title = "Take me to Crypto Gifts ->" - - let bulletItems = [preTitle, title, message, media, bulletPoint, actionButton] - - dataSource.registerVersionInfo(version: "1.13", items: bulletItems) - /// v 1 .13 ///// + // Register Multiple Versions + registerBulletingDetails(forVersion: Version("1.11")!, inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.12")!, inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.12.1")!, inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.12.2")!, inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.13")!, inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.13.1")!, inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.14")!, inDataStore: dataSource) - /// v 1 .13.1 ////// - let preTitle1 = PreTitle() - preTitle1.text = "Version 1.13.1" - - let title1 = Title() - title1.text = "In this update" - - let message1 = Message() - message1.messageType = .text - message1.text = "Vestibulum id ligula porta felis euismod semper." + let sdk = BulletinSDK(dataStore: dataSource) + let _ = sdk.showFullBulletin() + let _ = sdk.showLastBulletins(limit: 5) + let _ = sdk.showUnseenBulletins(limit: 4) + } + + private func registerBulletingDetails(forVersion version: Version, inDataStore dataStore: BulletinDataStore) { + + let title2 = Title() + title2.preTitleText = "Version " + version.version + title2.titleText = "In this update" + title2.subTitleText = "loreum ipsum loreum ipsum loreum ipsum" - let bullet1 = Bullet() - bullet1.bulletType = .unicode - bullet1.unicode = "U+0031" + let message2 = Message() + message2.messageType = .text + message2.text = "Vestibulum id ligula porta felis euismod semper." - let bulletPoint1 = BulletPoint() - bulletPoint1.bullet = bullet1 - bulletPoint1.titleText = "Vestibulum" - bulletPoint1.subTitleText = "Etiam porta sem malesuada magna mollis euismod." + let bullet2 = Bullet() + bullet2.bulletType = .unicode + bullet2.unicode = "U+0031" - let bulletPoint2 = BulletPoint() - bulletPoint2.bullet = bullet1 - bulletPoint2.titleText = "Justo Condimentum" - bulletPoint2.subTitleText = "Sed posuere consectetur est at lobortis. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor." - - let bulletItems1 = [preTitle1, title1, message1, bulletPoint1, bulletPoint2] + let bulletPoint21 = BulletPoint() + bulletPoint21.bullet = bullet2 + bulletPoint21.titleText = "Vestibulum" + bulletPoint21.subTitleText = "Etiam porta sem malesuada magna mollis euismod." - dataSource.registerVersionInfo(version: "1.13.1", items: bulletItems1) - /// v 1 .13 .1///// + let bulletPoint22 = BulletPoint() + bulletPoint22.bullet = bullet2 + bulletPoint22.titleText = "Justo Condimentum" + bulletPoint22.subTitleText = "Sed posuere consectetur est at lobortis. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor." + + // Create Items List + let bulletItems2 = [title2, message2, bulletPoint21, bulletPoint22] - let sdk = BulletinSDK(dataStore: dataSource) - sdk.showBulletin(fromVersion: "1.13") + // Register Item + dataStore.registerVersionInfo(version: version, items: bulletItems2) } - } From fddd308763b486fee8b25ebbe976c1f09c779142 Mon Sep 17 00:00:00 2001 From: Daxesh Date: Mon, 24 Jul 2023 16:55:56 +0530 Subject: [PATCH 06/12] Item cell added --- BulletinSDK/BulletinSDK.swift | 25 +- .../Base/View/BaseCollectionViewCell.swift | 45 + .../NSAttributedString+Utility.swift | 20 + .../Classes/Categories/String+HTML.swift | 37 + .../Categories/UILabel+FontStyle.swift | 146 + .../Classes/Constants/Appearance.swift | 540 ++++ BulletinSDK/Classes/Constants/Constants.swift | 91 + BulletinSDK/Classes/Constants/Text.swift | 28 + .../Classes/Helper/BulletinHelper.swift | 55 + .../Classes/Helper/LocalizationHelper.swift | 52 + BulletinSDK/Libraries/PopUp/AlertSegue.swift | 29 + .../Libraries/PopUp/BottomSheetSegue.swift | 29 + .../Libraries/PushButton/PushButton.swift | 199 ++ BulletinSDK/Modals/BulletinInfo.swift | 6 +- BulletinSDK/Modals/UI Modals/Title.swift | 6 +- .../Fonts/IBMPlexSans/IBMPlexSans-Bold.ttf | Bin 0 -> 193120 bytes .../IBMPlexSans/IBMPlexSans-BoldItalic.ttf | Bin 0 -> 202768 bytes .../Fonts/IBMPlexSans/IBMPlexSans-Medium.ttf | Bin 0 -> 194600 bytes .../Fonts/IBMPlexSans/IBMPlexSans-Regular.ttf | Bin 0 -> 192980 bytes .../IBMPlexSans/IBMPlexSans-SemiBold.ttf | Bin 0 -> 194760 bytes .../IBMPlexSans-SemiBoldItalic.ttf | Bin 0 -> 204792 bytes .../ActionButton/ActionButtonViewCell.swift | 74 + .../ActionButton/ActionButtonViewCell.xib | 56 + .../BulletPoint/BulletPointViewCell.swift | 106 + .../View/BulletPoint/BulletPointViewCell.xib | 99 + BulletinSDK/View/Media/MediaViewCell.swift | 61 + BulletinSDK/View/Media/MediaViewCell.xib | 45 + .../View/Message/MessageViewCell.swift | 72 + BulletinSDK/View/Message/MessageViewCell.xib | 48 + BulletinSDK/View/Title/TitleViewCell.swift | 84 + BulletinSDK/View/Title/TitleViewCell.xib | 67 + Demo/Bulletin.xcodeproj/project.pbxproj | 352 +++ .../UserInterfaceState.xcuserstate | Bin 0 -> 51118 bytes .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../UserInterfaceState.xcuserstate | Bin 0 -> 168418 bytes .../xcdebugger/Breakpoints_v2.xcbkptlist | 24 + Demo/Bulletin/Base.lproj/Main.storyboard | 28 +- .../BulletinListSectionController.swift | 285 ++ .../Controllers/BulletinListView.swift | 109 + .../Bulletin/Controllers/BulletinListView.xib | 49 + .../Controllers/BulletinSection.swift | 29 + Demo/Bulletin/Info.plist | 9 + Demo/Bulletin/ViewController.swift | 27 +- Demo/Podfile | 13 + Demo/Podfile.lock | 34 + Demo/Pods/IGListDiffKit/LICENSE.md | 21 + Demo/Pods/IGListDiffKit/README.md | 110 + .../Source/IGListDiffKit/IGListAssert.h | 22 + .../IGListDiffKit/IGListBatchUpdateData.h | 106 + .../IGListDiffKit/IGListBatchUpdateData.mm | 179 ++ .../IGListDiffKit/IGListCompatibility.h | 14 + .../Source/IGListDiffKit/IGListDiff.h | 63 + .../Source/IGListDiffKit/IGListDiff.mm | 348 +++ .../Source/IGListDiffKit/IGListDiffKit.h | 30 + .../Source/IGListDiffKit/IGListDiffable.h | 38 + .../Source/IGListDiffKit/IGListExperiments.h | 92 + .../IGListDiffKit/IGListIndexPathResult.h | 85 + .../IGListDiffKit/IGListIndexPathResult.m | 91 + .../IGListDiffKit/IGListIndexSetResult.h | 85 + .../IGListDiffKit/IGListIndexSetResult.m | 93 + .../Source/IGListDiffKit/IGListMacros.h | 28 + .../Source/IGListDiffKit/IGListMoveIndex.h | 40 + .../Source/IGListDiffKit/IGListMoveIndex.m | 52 + .../IGListDiffKit/IGListMoveIndexPath.h | 40 + .../IGListDiffKit/IGListMoveIndexPath.m | 46 + .../Internal/IGListIndexPathResultInternal.h | 27 + .../Internal/IGListIndexSetResultInternal.h | 27 + .../Internal/IGListMoveIndexInternal.h | 20 + .../Internal/IGListMoveIndexPathInternal.h | 18 + .../IGListDiffKit/NSNumber+IGListDiffable.h | 17 + .../IGListDiffKit/NSNumber+IGListDiffable.m | 20 + .../IGListDiffKit/NSString+IGListDiffable.h | 17 + .../IGListDiffKit/NSString+IGListDiffable.m | 20 + Demo/Pods/IGListKit/LICENSE.md | 21 + Demo/Pods/IGListKit/README.md | 110 + .../Internal/IGListIndexPathResultInternal.h | 27 + .../Internal/IGListIndexSetResultInternal.h | 27 + .../Internal/IGListMoveIndexInternal.h | 20 + .../Internal/IGListMoveIndexPathInternal.h | 18 + .../Source/IGListKit/IGListAdapter.h | 307 ++ .../Source/IGListKit/IGListAdapter.m | 1339 +++++++++ .../IGListKit/IGListAdapterDataSource.h | 64 + .../Source/IGListKit/IGListAdapterDelegate.h | 40 + .../IGListKit/IGListAdapterMoveDelegate.h | 35 + .../IGListAdapterPerformanceDelegate.h | 111 + .../IGListKit/IGListAdapterUpdateListener.h | 56 + .../Source/IGListKit/IGListAdapterUpdater.h | 68 + .../Source/IGListKit/IGListAdapterUpdater.m | 641 +++++ .../IGListKit/IGListAdapterUpdaterDelegate.h | 157 ++ .../Source/IGListKit/IGListBatchContext.h | 109 + .../Source/IGListKit/IGListBindable.h | 30 + .../IGListBindingSectionController.h | 91 + .../IGListBindingSectionController.m | 153 + ...IGListBindingSectionControllerDataSource.h | 65 + ...indingSectionControllerSelectionDelegate.h | 66 + .../IGListKit/IGListCollectionContext.h | 305 ++ .../IGListCollectionScrollingTraits.h | 22 + .../Source/IGListKit/IGListCollectionView.h | 49 + .../Source/IGListKit/IGListCollectionView.m | 105 + .../IGListCollectionViewDelegateLayout.h | 36 + .../IGListKit/IGListCollectionViewLayout.h | 142 + .../IGListKit/IGListCollectionViewLayout.mm | 681 +++++ .../IGListCollectionViewLayoutCompatible.h | 33 + .../Source/IGListKit/IGListDisplayDelegate.h | 65 + .../IGListGenericSectionController.h | 40 + .../IGListGenericSectionController.m | 16 + .../IGListKit/Source/IGListKit/IGListKit.h | 65 + .../IGListKit/IGListReloadDataUpdater.h | 27 + .../IGListKit/IGListReloadDataUpdater.m | 82 + .../Source/IGListKit/IGListScrollDelegate.h | 63 + .../IGListKit/IGListSectionController.h | 239 ++ .../IGListKit/IGListSectionController.m | 103 + .../IGListKit/IGListSingleSectionController.h | 144 + .../IGListKit/IGListSingleSectionController.m | 116 + .../IGListKit/IGListSupplementaryViewSource.h | 53 + .../IGListKit/IGListTransitionDelegate.h | 34 + .../Source/IGListKit/IGListUpdatingDelegate.h | 179 ++ .../IGListKit/IGListWorkingRangeDelegate.h | 44 + .../Internal/IGListAdapter+DebugDescription.h | 14 + .../Internal/IGListAdapter+DebugDescription.m | 76 + .../Internal/IGListAdapter+UICollectionView.h | 18 + .../Internal/IGListAdapter+UICollectionView.m | 307 ++ .../Internal/IGListAdapterInternal.h | 84 + .../IGListKit/Internal/IGListAdapterProxy.h | 48 + .../IGListKit/Internal/IGListAdapterProxy.m | 96 + .../IGListAdapterUpdater+DebugDescription.h | 14 + .../IGListAdapterUpdater+DebugDescription.m | 75 + .../Internal/IGListAdapterUpdaterInternal.h | 51 + .../Internal/IGListArrayUtilsInternal.h | 34 + .../IGListBatchUpdateData+DebugDescription.h | 14 + .../IGListBatchUpdateData+DebugDescription.m | 38 + .../Internal/IGListBatchUpdateState.h | 15 + .../IGListKit/Internal/IGListBatchUpdates.h | 29 + .../IGListKit/Internal/IGListBatchUpdates.m | 34 + ...indingSectionController+DebugDescription.h | 14 + ...indingSectionController+DebugDescription.m | 46 + .../IGListCollectionViewLayoutInternal.h | 14 + .../IGListKit/Internal/IGListDebugger.h | 28 + .../IGListKit/Internal/IGListDebugger.m | 41 + .../Internal/IGListDebuggingUtilities.h | 14 + .../Internal/IGListDebuggingUtilities.m | 20 + .../IGListKit/Internal/IGListDisplayHandler.h | 87 + .../IGListKit/Internal/IGListDisplayHandler.m | 122 + .../Internal/IGListReloadIndexPath.h | 52 + .../Internal/IGListReloadIndexPath.m | 21 + .../IGListSectionControllerInternal.h | 31 + .../IGListSectionMap+DebugDescription.h | 16 + .../IGListSectionMap+DebugDescription.m | 30 + .../IGListKit/Internal/IGListSectionMap.h | 115 + .../IGListKit/Internal/IGListSectionMap.m | 155 ++ .../Internal/IGListWorkingRangeHandler.h | 40 + .../Internal/IGListWorkingRangeHandler.mm | 154 ++ .../UICollectionView+DebugDescription.h | 14 + .../UICollectionView+DebugDescription.m | 43 + .../UICollectionView+IGListBatchUpdateData.h | 16 + .../UICollectionView+IGListBatchUpdateData.m | 31 + ...llectionViewLayout+InteractiveReordering.h | 24 + ...llectionViewLayout+InteractiveReordering.m | 195 ++ .../Internal/UIScrollView+IGListKit.h | 14 + .../Internal/UIScrollView+IGListKit.m | 21 + Demo/Pods/Kingfisher/LICENSE | 22 + Demo/Pods/Kingfisher/README.md | 258 ++ .../Sources/Cache/CacheSerializer.swift | 132 + .../Sources/Cache/DiskStorage.swift | 588 ++++ .../FormatIndicatedCacheSerializer.swift | 118 + .../Kingfisher/Sources/Cache/ImageCache.swift | 882 ++++++ .../Sources/Cache/MemoryStorage.swift | 283 ++ .../Kingfisher/Sources/Cache/Storage.swift | 110 + .../Extensions/CPListItem+Kingfisher.swift | 245 ++ .../Extensions/ImageView+Kingfisher.swift | 537 ++++ .../Extensions/NSButton+Kingfisher.swift | 362 +++ .../NSTextAttachment+Kingfisher.swift | 271 ++ .../TVMonogramView+Kingfisher.swift | 209 ++ .../Extensions/UIButton+Kingfisher.swift | 400 +++ .../WKInterfaceImage+Kingfisher.swift | 204 ++ .../AVAssetImageDataProvider.swift | 138 + .../ImageSource/ImageDataProvider.swift | 190 ++ .../General/ImageSource/Resource.swift | 115 + .../Sources/General/ImageSource/Source.swift | 116 + Demo/Pods/Kingfisher/Sources/General/KF.swift | 442 +++ .../Sources/General/KFOptionsSetter.swift | 706 +++++ .../Sources/General/Kingfisher.swift | 106 + .../Sources/General/KingfisherError.swift | 461 ++++ .../Sources/General/KingfisherManager.swift | 802 ++++++ .../General/KingfisherOptionsInfo.swift | 400 +++ .../Kingfisher/Sources/Image/Filter.swift | 146 + .../Sources/Image/GIFAnimatedImage.swift | 121 + .../Sources/Image/GraphicsContext.swift | 88 + .../Pods/Kingfisher/Sources/Image/Image.swift | 377 +++ .../Sources/Image/ImageDrawing.swift | 636 +++++ .../Sources/Image/ImageFormat.swift | 130 + .../Sources/Image/ImageProcessor.swift | 920 ++++++ .../Sources/Image/ImageProgressive.swift | 348 +++ .../Sources/Image/ImageTransition.swift | 118 + .../Sources/Image/Placeholder.swift | 82 + .../AuthenticationChallengeResponsable.swift | 94 + .../Networking/ImageDataProcessor.swift | 74 + .../Sources/Networking/ImageDownloader.swift | 488 ++++ .../Networking/ImageDownloaderDelegate.swift | 154 ++ .../Sources/Networking/ImageModifier.swift | 116 + .../Sources/Networking/ImagePrefetcher.swift | 442 +++ .../Sources/Networking/RedirectHandler.swift | 76 + .../Sources/Networking/RequestModifier.swift | 108 + .../Sources/Networking/RetryStrategy.swift | 153 + .../Sources/Networking/SessionDataTask.swift | 127 + .../Sources/Networking/SessionDelegate.swift | 262 ++ .../Sources/SwiftUI/ImageBinder.swift | 149 + .../Sources/SwiftUI/ImageContext.swift | 102 + .../Sources/SwiftUI/KFAnimatedImage.swift | 96 + .../Kingfisher/Sources/SwiftUI/KFImage.swift | 106 + .../Sources/SwiftUI/KFImageOptions.swift | 158 ++ .../Sources/SwiftUI/KFImageProtocol.swift | 112 + .../Sources/SwiftUI/KFImageRenderer.swift | 129 + .../Pods/Kingfisher/Sources/Utility/Box.swift | 34 + .../Sources/Utility/CallbackQueue.swift | 83 + .../Kingfisher/Sources/Utility/Delegate.swift | 132 + .../Sources/Utility/ExtensionHelpers.swift | 117 + .../Kingfisher/Sources/Utility/Result.swift | 50 + .../Kingfisher/Sources/Utility/Runtime.swift | 35 + .../Sources/Utility/SizeExtensions.swift | 110 + .../Sources/Utility/String+MD5.swift | 278 ++ .../Sources/Views/AnimatedImageView.swift | 686 +++++ .../Kingfisher/Sources/Views/Indicator.swift | 231 ++ Demo/Pods/Manifest.lock | 34 + Demo/Pods/Pods.xcodeproj/project.pbxproj | 2457 +++++++++++++++++ .../xcschemes/IGListDiffKit.xcscheme | 58 + .../xcschemes/IGListKit.xcscheme | 58 + .../xcschemes/Kingfisher.xcscheme | 58 + .../xcschemes/Pods-Bulletin.xcscheme | 58 + .../SwiftMessages-SwiftMessages.xcscheme | 58 + .../xcschemes/SwiftMessages.xcscheme | 58 + .../xcschemes/UIColor_Hex_Swift.xcscheme | 58 + Demo/Pods/SwiftMessages/LICENSE.md | 8 + Demo/Pods/SwiftMessages/README.md | 387 +++ .../SwiftMessages/AccessibleMessage.swift | 20 + .../SwiftMessages/Animator.swift | 78 + .../SwiftMessages/BackgroundViewable.swift | 24 + .../SwiftMessages/BaseView.swift | 375 +++ .../SwiftMessages/CALayer+Extensions.swift | 18 + .../SwiftMessages/CornerRoundingView.swift | 83 + .../SwiftMessages/SwiftMessages/Error.swift | 17 + .../SwiftMessages/Identifiable.swift | 22 + .../SwiftMessages/KeyboardTrackingView.swift | 143 + .../MarginAdjustable+Extensions.swift | 60 + .../SwiftMessages/MarginAdjustable.swift | 42 + .../SwiftMessages/MaskingView.swift | 79 + .../SwiftMessages/MessageView.swift | 440 +++ .../SwiftMessages/NSBundle+Extensions.swift | 48 + .../NSLayoutConstraint+Extensions.swift | 16 + .../SwiftMessages/PassthroughView.swift | 37 + .../SwiftMessages/PassthroughWindow.swift | 36 + .../SwiftMessages/PhysicsAnimation.swift | 125 + .../SwiftMessages/PhysicsPanHandler.swift | 175 ++ .../SwiftMessages/Presenter.swift | 423 +++ .../SwiftMessages/Resources/CardView.xib | 150 + .../SwiftMessages/Resources/CenteredView.xib | 153 + .../SwiftMessages/Resources/MessageView.xib | 104 + .../SwiftMessages/Resources/StatusLine.xib | 58 + .../SwiftMessages/Resources/TabView.xib | 152 + .../SwiftMessages/Resources/errorIcon.png | Bin 0 -> 698 bytes .../SwiftMessages/Resources/errorIcon@2x.png | Bin 0 -> 1338 bytes .../SwiftMessages/Resources/errorIcon@3x.png | Bin 0 -> 2074 bytes .../Resources/errorIconLight.png | Bin 0 -> 879 bytes .../Resources/errorIconLight@2x.png | Bin 0 -> 1911 bytes .../Resources/errorIconLight@3x.png | Bin 0 -> 2900 bytes .../Resources/errorIconSubtle.png | Bin 0 -> 225 bytes .../Resources/errorIconSubtle@2x.png | Bin 0 -> 402 bytes .../Resources/errorIconSubtle@3x.png | Bin 0 -> 543 bytes .../SwiftMessages/Resources/infoIcon.png | Bin 0 -> 495 bytes .../SwiftMessages/Resources/infoIcon@2x.png | Bin 0 -> 913 bytes .../SwiftMessages/Resources/infoIcon@3x.png | Bin 0 -> 1553 bytes .../SwiftMessages/Resources/infoIconLight.png | Bin 0 -> 640 bytes .../Resources/infoIconLight@2x.png | Bin 0 -> 1391 bytes .../Resources/infoIconLight@3x.png | Bin 0 -> 2441 bytes .../Resources/infoIconSubtle.png | Bin 0 -> 139 bytes .../Resources/infoIconSubtle@2x.png | Bin 0 -> 188 bytes .../Resources/infoIconSubtle@3x.png | Bin 0 -> 263 bytes .../SwiftMessages/Resources/successIcon.png | Bin 0 -> 639 bytes .../Resources/successIcon@2x.png | Bin 0 -> 1214 bytes .../Resources/successIcon@3x.png | Bin 0 -> 1930 bytes .../Resources/successIconLight.png | Bin 0 -> 808 bytes .../Resources/successIconLight@2x.png | Bin 0 -> 1809 bytes .../Resources/successIconLight@3x.png | Bin 0 -> 2743 bytes .../Resources/successIconSubtle.png | Bin 0 -> 235 bytes .../Resources/successIconSubtle@2x.png | Bin 0 -> 323 bytes .../Resources/successIconSubtle@3x.png | Bin 0 -> 418 bytes .../SwiftMessages/Resources/warningIcon.png | Bin 0 -> 493 bytes .../Resources/warningIcon@2x.png | Bin 0 -> 916 bytes .../Resources/warningIcon@3x.png | Bin 0 -> 1577 bytes .../Resources/warningIconLight.png | Bin 0 -> 660 bytes .../Resources/warningIconLight@2x.png | Bin 0 -> 1404 bytes .../Resources/warningIconLight@3x.png | Bin 0 -> 2479 bytes .../Resources/warningIconSubtle.png | Bin 0 -> 131 bytes .../Resources/warningIconSubtle@2x.png | Bin 0 -> 178 bytes .../Resources/warningIconSubtle@3x.png | Bin 0 -> 259 bytes .../SwiftMessages.Config+Extensions.swift | 44 + .../SwiftMessages/SwiftMessages.swift | 962 +++++++ .../SwiftMessages/SwiftMessagesSegue.swift | 399 +++ .../SwiftMessages/SwiftMessages/Theme.swift | 67 + .../SwiftMessages/TopBottomAnimation.swift | 233 ++ .../UIEdgeInsets+Extensions.swift | 27 + .../UIViewController+Extensions.swift | 101 + .../SwiftMessages/UIWindow+Extensions.swift | 38 + .../SwiftMessages/SwiftMessages/Weak.swift | 16 + .../SwiftMessages/WindowScene.swift | 9 + .../SwiftMessages/WindowViewController.swift | 95 + .../IGListDiffKit/IGListDiffKit-Info.plist | 26 + .../IGListDiffKit/IGListDiffKit-dummy.m | 5 + .../IGListDiffKit/IGListDiffKit-prefix.pch | 12 + .../IGListDiffKit/IGListDiffKit-umbrella.h | 30 + .../IGListDiffKit.debug.xcconfig | 15 + .../IGListDiffKit/IGListDiffKit.modulemap | 6 + .../IGListDiffKit.release.xcconfig | 15 + .../IGListKit/IGListKit-Info.plist | 26 + .../IGListKit/IGListKit-dummy.m | 5 + .../IGListKit/IGListKit-prefix.pch | 12 + .../IGListKit/IGListKit-umbrella.h | 46 + .../IGListKit/IGListKit.debug.xcconfig | 16 + .../IGListKit/IGListKit.modulemap | 6 + .../IGListKit/IGListKit.release.xcconfig | 16 + .../Kingfisher/Kingfisher-Info.plist | 26 + .../Kingfisher/Kingfisher-dummy.m | 5 + .../Kingfisher/Kingfisher-prefix.pch | 12 + .../Kingfisher/Kingfisher-umbrella.h | 16 + .../Kingfisher/Kingfisher.debug.xcconfig | 15 + .../Kingfisher/Kingfisher.modulemap | 6 + .../Kingfisher/Kingfisher.release.xcconfig | 15 + .../Pods-Bulletin/Pods-Bulletin-Info.plist | 26 + .../Pods-Bulletin-acknowledgements.markdown | 112 + .../Pods-Bulletin-acknowledgements.plist | 168 ++ .../Pods-Bulletin/Pods-Bulletin-dummy.m | 5 + ...in-frameworks-Debug-input-files.xcfilelist | 6 + ...n-frameworks-Debug-output-files.xcfilelist | 5 + ...-frameworks-Release-input-files.xcfilelist | 6 + ...frameworks-Release-output-files.xcfilelist | 5 + .../Pods-Bulletin/Pods-Bulletin-frameworks.sh | 194 ++ .../Pods-Bulletin/Pods-Bulletin-umbrella.h | 16 + .../Pods-Bulletin.debug.xcconfig | 15 + .../Pods-Bulletin/Pods-Bulletin.modulemap | 6 + .../Pods-Bulletin.release.xcconfig | 15 + ...dle-SwiftMessages-SwiftMessages-Info.plist | 24 + .../SwiftMessages/SwiftMessages-Info.plist | 26 + .../SwiftMessages/SwiftMessages-dummy.m | 5 + .../SwiftMessages/SwiftMessages-prefix.pch | 12 + .../SwiftMessages/SwiftMessages-umbrella.h | 16 + .../SwiftMessages.debug.xcconfig | 15 + .../SwiftMessages/SwiftMessages.modulemap | 6 + .../SwiftMessages.release.xcconfig | 15 + .../UIColor_Hex_Swift-Info.plist | 26 + .../UIColor_Hex_Swift-dummy.m | 5 + .../UIColor_Hex_Swift-prefix.pch | 12 + .../UIColor_Hex_Swift-umbrella.h | 17 + .../UIColor_Hex_Swift.debug.xcconfig | 15 + .../UIColor_Hex_Swift.modulemap | 6 + .../UIColor_Hex_Swift.release.xcconfig | 15 + .../UIColor_Hex_Swift/HEXColor/HEXColor.h | 17 + .../HEXColor/StringExtension.swift | 31 + .../HEXColor/UIColorExtension.swift | 175 ++ .../HEXColor/UIColorInputError.swift | 36 + Demo/Pods/UIColor_Hex_Swift/LICENSE | 21 + Demo/Pods/UIColor_Hex_Swift/README.md | 81 + 362 files changed, 38637 insertions(+), 24 deletions(-) create mode 100644 BulletinSDK/Classes/Base/View/BaseCollectionViewCell.swift create mode 100644 BulletinSDK/Classes/Categories/NSAttributedString+Utility.swift create mode 100644 BulletinSDK/Classes/Categories/String+HTML.swift create mode 100644 BulletinSDK/Classes/Categories/UILabel+FontStyle.swift create mode 100644 BulletinSDK/Classes/Constants/Appearance.swift create mode 100644 BulletinSDK/Classes/Constants/Constants.swift create mode 100644 BulletinSDK/Classes/Constants/Text.swift create mode 100644 BulletinSDK/Classes/Helper/BulletinHelper.swift create mode 100644 BulletinSDK/Classes/Helper/LocalizationHelper.swift create mode 100644 BulletinSDK/Libraries/PopUp/AlertSegue.swift create mode 100644 BulletinSDK/Libraries/PopUp/BottomSheetSegue.swift create mode 100644 BulletinSDK/Libraries/PushButton/PushButton.swift create mode 100644 BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-Bold.ttf create mode 100644 BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-BoldItalic.ttf create mode 100644 BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-Medium.ttf create mode 100644 BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-Regular.ttf create mode 100644 BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-SemiBold.ttf create mode 100644 BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-SemiBoldItalic.ttf create mode 100644 BulletinSDK/View/ActionButton/ActionButtonViewCell.swift create mode 100644 BulletinSDK/View/ActionButton/ActionButtonViewCell.xib create mode 100644 BulletinSDK/View/BulletPoint/BulletPointViewCell.swift create mode 100644 BulletinSDK/View/BulletPoint/BulletPointViewCell.xib create mode 100644 BulletinSDK/View/Media/MediaViewCell.swift create mode 100644 BulletinSDK/View/Media/MediaViewCell.xib create mode 100644 BulletinSDK/View/Message/MessageViewCell.swift create mode 100644 BulletinSDK/View/Message/MessageViewCell.xib create mode 100644 BulletinSDK/View/Title/TitleViewCell.swift create mode 100644 BulletinSDK/View/Title/TitleViewCell.xib create mode 100644 Demo/Bulletin.xcodeproj/project.xcworkspace/xcuserdata/daxeshnagar.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Demo/Bulletin.xcworkspace/contents.xcworkspacedata create mode 100644 Demo/Bulletin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 Demo/Bulletin.xcworkspace/xcuserdata/daxeshnagar.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 Demo/Bulletin.xcworkspace/xcuserdata/daxeshnagar.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist create mode 100644 Demo/Bulletin/Controllers/BulletinListSectionController.swift create mode 100644 Demo/Bulletin/Controllers/BulletinListView.swift create mode 100644 Demo/Bulletin/Controllers/BulletinListView.xib create mode 100644 Demo/Bulletin/Controllers/BulletinSection.swift create mode 100644 Demo/Podfile create mode 100644 Demo/Podfile.lock create mode 100755 Demo/Pods/IGListDiffKit/LICENSE.md create mode 100644 Demo/Pods/IGListDiffKit/README.md create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListAssert.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListBatchUpdateData.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListBatchUpdateData.mm create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListCompatibility.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiff.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiff.mm create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiffKit.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiffable.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListExperiments.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexPathResult.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexPathResult.m create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexSetResult.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexSetResult.m create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMacros.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndex.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndex.m create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndexPath.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndexPath.m create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSNumber+IGListDiffable.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSNumber+IGListDiffable.m create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSString+IGListDiffable.h create mode 100644 Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSString+IGListDiffable.m create mode 100755 Demo/Pods/IGListKit/LICENSE.md create mode 100644 Demo/Pods/IGListKit/README.md create mode 100644 Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h create mode 100644 Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h create mode 100644 Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h create mode 100644 Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapter.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapter.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterDataSource.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterMoveDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterPerformanceDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdateListener.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdater.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdater.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdaterDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListBatchContext.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListBindable.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionController.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionController.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionControllerDataSource.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionControllerSelectionDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionContext.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionScrollingTraits.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionView.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionView.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewDelegateLayout.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayout.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayout.mm create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayoutCompatible.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListDisplayDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListGenericSectionController.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListGenericSectionController.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListKit.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListReloadDataUpdater.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListReloadDataUpdater.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListScrollDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListSectionController.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListSectionController.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListSingleSectionController.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListSingleSectionController.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListSupplementaryViewSource.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListTransitionDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListUpdatingDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/IGListWorkingRangeDelegate.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+DebugDescription.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+DebugDescription.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+UICollectionView.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+UICollectionView.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterInternal.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterProxy.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterProxy.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListArrayUtilsInternal.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateState.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdates.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdates.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListCollectionViewLayoutInternal.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebugger.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebugger.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebuggingUtilities.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebuggingUtilities.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDisplayHandler.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDisplayHandler.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListReloadIndexPath.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListReloadIndexPath.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionControllerInternal.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap+DebugDescription.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap+DebugDescription.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListWorkingRangeHandler.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListWorkingRangeHandler.mm create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+DebugDescription.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+DebugDescription.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.m create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/UIScrollView+IGListKit.h create mode 100644 Demo/Pods/IGListKit/Source/IGListKit/Internal/UIScrollView+IGListKit.m create mode 100644 Demo/Pods/Kingfisher/LICENSE create mode 100644 Demo/Pods/Kingfisher/README.md create mode 100644 Demo/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Cache/DiskStorage.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Cache/ImageCache.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Cache/Storage.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/ImageSource/Source.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/KF.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/Kingfisher.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/KingfisherError.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/KingfisherManager.swift create mode 100644 Demo/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/Filter.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/GraphicsContext.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/Image.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/ImageDrawing.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/ImageFormat.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/ImageProcessor.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/ImageProgressive.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/ImageTransition.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Image/Placeholder.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/ImageModifier.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/RequestModifier.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift create mode 100644 Demo/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift create mode 100644 Demo/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift create mode 100644 Demo/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift create mode 100644 Demo/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift create mode 100644 Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift create mode 100644 Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift create mode 100644 Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Utility/Box.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Utility/Delegate.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Utility/Result.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Utility/Runtime.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Utility/String+MD5.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift create mode 100644 Demo/Pods/Kingfisher/Sources/Views/Indicator.swift create mode 100644 Demo/Pods/Manifest.lock create mode 100644 Demo/Pods/Pods.xcodeproj/project.pbxproj create mode 100644 Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/IGListDiffKit.xcscheme create mode 100644 Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/IGListKit.xcscheme create mode 100644 Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/Kingfisher.xcscheme create mode 100644 Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/Pods-Bulletin.xcscheme create mode 100644 Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/SwiftMessages-SwiftMessages.xcscheme create mode 100644 Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/SwiftMessages.xcscheme create mode 100644 Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/UIColor_Hex_Swift.xcscheme create mode 100644 Demo/Pods/SwiftMessages/LICENSE.md create mode 100644 Demo/Pods/SwiftMessages/README.md create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/AccessibleMessage.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Animator.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/BackgroundViewable.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/BaseView.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/CALayer+Extensions.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/CornerRoundingView.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Error.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Identifiable.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/KeyboardTrackingView.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/MarginAdjustable+Extensions.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/MarginAdjustable.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/MaskingView.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/MessageView.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/NSBundle+Extensions.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/NSLayoutConstraint+Extensions.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/PassthroughView.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/PassthroughWindow.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/PhysicsAnimation.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/PhysicsPanHandler.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Presenter.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/CardView.xib create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/CenteredView.xib create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/MessageView.xib create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/StatusLine.xib create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/TabView.xib create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIcon.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIcon@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIcon@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconLight.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconLight@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconLight@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconSubtle.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconSubtle@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconSubtle@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIcon.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIcon@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIcon@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconLight.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconLight@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconLight@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconSubtle.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconSubtle@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconSubtle@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIcon.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIcon@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIcon@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconLight.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconLight@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconLight@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconSubtle.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconSubtle@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconSubtle@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIcon.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIcon@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIcon@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconLight.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconLight@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconLight@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconSubtle.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconSubtle@2x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconSubtle@3x.png create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessages.Config+Extensions.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessages.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessagesSegue.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Theme.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/TopBottomAnimation.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/UIEdgeInsets+Extensions.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/UIViewController+Extensions.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/UIWindow+Extensions.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/Weak.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/WindowScene.swift create mode 100644 Demo/Pods/SwiftMessages/SwiftMessages/WindowViewController.swift create mode 100644 Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-Info.plist create mode 100644 Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-dummy.m create mode 100644 Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-prefix.pch create mode 100644 Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-umbrella.h create mode 100644 Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.debug.xcconfig create mode 100644 Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.modulemap create mode 100644 Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.release.xcconfig create mode 100644 Demo/Pods/Target Support Files/IGListKit/IGListKit-Info.plist create mode 100644 Demo/Pods/Target Support Files/IGListKit/IGListKit-dummy.m create mode 100644 Demo/Pods/Target Support Files/IGListKit/IGListKit-prefix.pch create mode 100644 Demo/Pods/Target Support Files/IGListKit/IGListKit-umbrella.h create mode 100644 Demo/Pods/Target Support Files/IGListKit/IGListKit.debug.xcconfig create mode 100644 Demo/Pods/Target Support Files/IGListKit/IGListKit.modulemap create mode 100644 Demo/Pods/Target Support Files/IGListKit/IGListKit.release.xcconfig create mode 100644 Demo/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist create mode 100644 Demo/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m create mode 100644 Demo/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch create mode 100644 Demo/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h create mode 100644 Demo/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig create mode 100644 Demo/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap create mode 100644 Demo/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-Info.plist create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-acknowledgements.markdown create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-acknowledgements.plist create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-dummy.m create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Debug-input-files.xcfilelist create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Debug-output-files.xcfilelist create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Release-input-files.xcfilelist create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Release-output-files.xcfilelist create mode 100755 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks.sh create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-umbrella.h create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.debug.xcconfig create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.modulemap create mode 100644 Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.release.xcconfig create mode 100644 Demo/Pods/Target Support Files/SwiftMessages/ResourceBundle-SwiftMessages-SwiftMessages-Info.plist create mode 100644 Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-Info.plist create mode 100644 Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-dummy.m create mode 100644 Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-prefix.pch create mode 100644 Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-umbrella.h create mode 100644 Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.debug.xcconfig create mode 100644 Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.modulemap create mode 100644 Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.release.xcconfig create mode 100644 Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-Info.plist create mode 100644 Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-dummy.m create mode 100644 Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-prefix.pch create mode 100644 Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-umbrella.h create mode 100644 Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.debug.xcconfig create mode 100644 Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.modulemap create mode 100644 Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.release.xcconfig create mode 100644 Demo/Pods/UIColor_Hex_Swift/HEXColor/HEXColor.h create mode 100644 Demo/Pods/UIColor_Hex_Swift/HEXColor/StringExtension.swift create mode 100644 Demo/Pods/UIColor_Hex_Swift/HEXColor/UIColorExtension.swift create mode 100644 Demo/Pods/UIColor_Hex_Swift/HEXColor/UIColorInputError.swift create mode 100644 Demo/Pods/UIColor_Hex_Swift/LICENSE create mode 100644 Demo/Pods/UIColor_Hex_Swift/README.md diff --git a/BulletinSDK/BulletinSDK.swift b/BulletinSDK/BulletinSDK.swift index 3c61f39..962df84 100644 --- a/BulletinSDK/BulletinSDK.swift +++ b/BulletinSDK/BulletinSDK.swift @@ -23,42 +23,47 @@ class BulletinSDK { // MARK: - Helper Methods - public func showFullBulletin() -> Bool { + public func getFullBulletin() -> BulletinListView? { // Get Bulletin Items let items = dataStore.getData(fromNewVersion: nil, toOldVersion: nil) // Show Bulletin - return showBulletin(items: items) + return getBulletin(items: items) } - public func showLastBulletins(limit: Int = 1) -> Bool { + public func getLastBulletins(limit: Int = 1) -> BulletinListView? { // Get Bulletin Items let items = dataStore.getData(fromNewVersion: nil, toOldVersion: nil, limit: limit) // Show Bulletin - return showBulletin(items: items) + return getBulletin(items: items) } - public func showUnseenBulletins(limit: Int? = nil) -> Bool { + public func getUnseenBulletins(limit: Int? = nil) -> BulletinListView? { // Get Last Seen Version - guard let lastSeenVersion = UserDefaults.lastSeenVersion else { return false } + guard let lastSeenVersion = UserDefaults.lastSeenVersion else { return nil } // Get Bulletin Items let items = dataStore.getData(fromNewVersion: lastSeenVersion, toOldVersion: nil, limit: limit) // Show Bulletin - return showBulletin(items: items) + return getBulletin(items: items) } - private func showBulletin(items: [BulletinInfo]?) -> Bool { + private func getBulletin(items: [BulletinInfo]?) -> BulletinListView? { // Validation guard let items = items, - items.isEmpty == false else { return false } + items.isEmpty == false else { return nil } - return false + let bulletinSection = BulletinSection() + bulletinSection.bulletinInfo = items + + let bulletinView = BulletinListView.instance(bulletinSection: bulletinSection) + + return bulletinView } } diff --git a/BulletinSDK/Classes/Base/View/BaseCollectionViewCell.swift b/BulletinSDK/Classes/Base/View/BaseCollectionViewCell.swift new file mode 100644 index 0000000..6e77506 --- /dev/null +++ b/BulletinSDK/Classes/Base/View/BaseCollectionViewCell.swift @@ -0,0 +1,45 @@ +// +// BaseTableViewCell.swift +// Bulletin +// +// Created by Daxesh Nagar on 17/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + +class BaseCollectionViewCell: UICollectionViewCell { + + // MARK: - Constants + public class func identifier() -> String { + return String(describing: self) + } + + + // MARK: - Initialisation Methods + override func awakeFromNib() { + super.awakeFromNib() + + if #available(iOS 13.0, *) { + layer.cornerCurve = .continuous + } + + // Load Variables + loadVariables() + + // Update Appearance + updateAppearance() + + } + + + // MARK: - Override Methods + open func loadVariables() { + // Override This Method In Subclass To Initialize Variables + } + + open func updateAppearance() { + // Override This Method In Subclass To Set/Update Appearance + } + +} diff --git a/BulletinSDK/Classes/Categories/NSAttributedString+Utility.swift b/BulletinSDK/Classes/Categories/NSAttributedString+Utility.swift new file mode 100644 index 0000000..0342a93 --- /dev/null +++ b/BulletinSDK/Classes/Categories/NSAttributedString+Utility.swift @@ -0,0 +1,20 @@ +// +// NSAttributedString+Utility.swift +// Bulletin +// +// Created by Daxesh Nagar on 21/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import Foundation + +extension NSAttributedString { + + var trailingNewlineChopped: NSAttributedString { + if string.hasSuffix("\n") { + return attributedSubstring(from: NSRange(location: 0, length: length - 1)) + } else { + return self + } + } +} diff --git a/BulletinSDK/Classes/Categories/String+HTML.swift b/BulletinSDK/Classes/Categories/String+HTML.swift new file mode 100644 index 0000000..88780ac --- /dev/null +++ b/BulletinSDK/Classes/Categories/String+HTML.swift @@ -0,0 +1,37 @@ +// +// String+HTML.swift +// Bulletin +// +// Created by Daxesh Nagar on 21/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + +extension String { + + public func html2AttributedString(usingFont font: UIFont?, color: UIColor = .black) -> NSMutableAttributedString? { + + let text : String + + // Add Font Attributes + if let font = font { + let colorHashCode = color.hexString() + let startTag = "" + let endTag = "" + text = (startTag + self + endTag) + } + else { + text = self as String + } + + // Convert HTML To Attributed String + do { + return try NSMutableAttributedString(data: Data(text.utf8), options: [.documentType: NSAttributedString.DocumentType.html, .characterEncoding: String.Encoding.utf8.rawValue], documentAttributes: nil) + } catch { + print("error:", error) + return nil + } + } +} + diff --git a/BulletinSDK/Classes/Categories/UILabel+FontStyle.swift b/BulletinSDK/Classes/Categories/UILabel+FontStyle.swift new file mode 100644 index 0000000..94f46e2 --- /dev/null +++ b/BulletinSDK/Classes/Categories/UILabel+FontStyle.swift @@ -0,0 +1,146 @@ +// +// UILabel+FontStyle.swift +// Bulletin +// +// Created by Daxesh Nagar on 18/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + + +extension UILabel { + + + // MARK: - Heading 1 Methods + public func heading1_regular() { + font = AppStyle.Font.Regular(size: 48) + } + public func heading1_bold() { + font = AppStyle.Font.Bold(size: 48) + } + public func heading1_semibold() { + font = AppStyle.Font.SemiBold(size: 48) + } + + + // MARK: - Heading 2 Methods + public func heading2_regular() { + font = AppStyle.Font.Regular(size: 32) + } + public func heading2_bold() { + font = AppStyle.Font.Bold(size: 32) + } + public func heading2_semibold() { + font = AppStyle.Font.SemiBold(size: 32) + } + + + // MARK: - Heading 3 Methods + public func heading3_regular() { + font = AppStyle.Font.Regular(size: 25) + } + public func heading3_bold() { + font = AppStyle.Font.Bold(size: 25) + } + public func heading3_semibold() { + font = AppStyle.Font.SemiBold(size: 25) + } + + + // MARK: - Heading 4 Methods + public func heading4_regular() { + font = AppStyle.Font.Regular(size: 22) + } + public func heading4_bold() { + font = AppStyle.Font.Bold(size: 22) + } + public func heading4_semibold() { + font = AppStyle.Font.SemiBold(size: 22) + } + + + // MARK: - Heading 5 Methods + public func heading5_regular() { + font = AppStyle.Font.Regular(size: 20) + } + public func heading5_bold() { + font = AppStyle.Font.Bold(size: 20) + } + public func heading5_semibold() { + font = AppStyle.Font.SemiBold(size: 20) + } + + + // MARK: - Heading 6 Methods + public func heading6_regular() { + font = AppStyle.Font.Regular(size: 18) + } + public func heading6_bold() { + font = AppStyle.Font.Bold(size: 18) + } + public func heading6_semibold() { + font = AppStyle.Font.SemiBold(size: 18) + } + + + // MARK: - Large Methods + public func large_regular() { + font = AppStyle.Font.Regular(size: 16) + } + public func large_bold() { + font = AppStyle.Font.Bold(size: 16) + } + public func large_semibold() { + font = AppStyle.Font.SemiBold(size: 16) + } + public func large_medium() { + font = AppStyle.Font.Medium(size: 16) + } + + + // MARK: - Base Methods + public func base_regular() { + font = AppStyle.Font.Regular(size: 14) + } + public func base_bold() { + font = AppStyle.Font.Bold(size: 14) + } + public func base_semibold() { + font = AppStyle.Font.SemiBold(size: 14) + } + public func base_medium() { + font = AppStyle.Font.Medium(size: 14) + } + + + // MARK: - Small Methods + public func small_regular() { + font = AppStyle.Font.Regular(size: 12) + } + public func small_bold() { + font = AppStyle.Font.Bold(size: 12) + } + public func small_semibold() { + font = AppStyle.Font.SemiBold(size: 12) + } + public func small_medium() { + font = AppStyle.Font.Medium(size: 12) + } + + + // MARK: - xSmall Methods + public func xsmall_regular() { + font = AppStyle.Font.Regular(size: 11) + } + public func xsmall_bold() { + font = AppStyle.Font.Bold(size: 11) + } + public func xsmall_semibold() { + font = AppStyle.Font.SemiBold(size: 11) + } + public func xsmall_medium() { + font = AppStyle.Font.Medium(size: 11) + } +} + diff --git a/BulletinSDK/Classes/Constants/Appearance.swift b/BulletinSDK/Classes/Constants/Appearance.swift new file mode 100644 index 0000000..336965a --- /dev/null +++ b/BulletinSDK/Classes/Constants/Appearance.swift @@ -0,0 +1,540 @@ +// +// WZAppearance.swift +// WazirX +// +// Created by Daxesh Nagar on 17/07/23. +// Copyright © 2017 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit +import UIColor_Hex_Swift + +public enum Appearance: String { + + // MARK: Types + case darkKnight + case whiteKnight + + + // MARK: Constants + public static func DefaultAnimationDuration() -> TimeInterval { return 0.3 } +// public static func Current() -> Appearance { +// return UserDefaults.lastSelectedAppearance() ?? .darkKnight +// } + + + // MARK: Conveniance Method To Apply Any Appearance + public func apply(shouldBroadcastUpdate: Bool) { + + // Apply Appropriate Themes + switch self { + case .darkKnight: + applyMidnightBlueAppearance() + case .whiteKnight: + applyDefaultAppearance() + } + + + // Update UISwitch Appearance + UISwitch.appearance().onTintColor = AppStyle.Color.SwitchOn + + // Update UITextField Appearance + UITextField.appearance().keyboardAppearance = AppStyle.KeyboardAppearance + UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedString.Key.foregroundColor: AppStyle.Color.PrimaryText] + UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = AppStyle.Color.SelectedElement + + // Update UIRefreshControl Appearance + UIRefreshControl.appearance().tintColor = AppStyle.Color.Loader + + // Update UIToolBar Appearance + UIToolbar.appearance().tintColor = AppStyle.Color.SelectedElement + UIToolbar.appearance().barTintColor = AppStyle.Color.Background + + + // Broadcast Appearance Update Notification + if shouldBroadcastUpdate == true { + NotificationCenter.default.post(name: .AppearanceDidUpdate, object: nil, userInfo: nil) + } + } + + + // MARK: Helper Methods + + private func applyDefaultAppearance() { + + // ------- New Color Styles ------- // + AppStyle.Color.MainTextPrimary = AppStyle.ColorTokens.Gray_100 + AppStyle.Color.MainTextSecondary = AppStyle.ColorTokens.Gray_50 + AppStyle.Color.MainTextTertiary = AppStyle.ColorTokens.Gray_40 + + AppStyle.Color.MainBrandPrimary = AppStyle.ColorTokens.Blue_60 + + AppStyle.Color.MainNavigationOnNavigation = AppStyle.ColorTokens.White + AppStyle.Color.MainNavigationBg = AppStyle.ColorTokens.Blue_60 + + AppStyle.Color.MainBgSurface = AppStyle.ColorTokens.Gray_20 + AppStyle.Color.MainBgSurface_Alt = AppStyle.ColorTokens.White + AppStyle.Color.MainBgPrimary = AppStyle.ColorTokens.White + AppStyle.Color.MainBgSecondary = AppStyle.ColorTokens.Gray_10 + AppStyle.Color.MainBgTertiary = AppStyle.ColorTokens.Gray_30 + + AppStyle.Color.BrandTextPrimary = AppStyle.ColorTokens.Blue_60 + AppStyle.Color.BrandTextSecondary = AppStyle.ColorTokens.Blue_30 + AppStyle.Color.BrandTextOnMuted = AppStyle.ColorTokens.Blue_60 + AppStyle.Color.BrandTextOnPrimary = AppStyle.ColorTokens.White + AppStyle.Color.BrandBgPrimary = AppStyle.ColorTokens.Blue_60 + AppStyle.Color.BrandBgMuted = AppStyle.ColorTokens.Blue_10 + AppStyle.Color.BrandBgMuted_Border = AppStyle.ColorTokens.Blue_20 + + AppStyle.Color.SuccessTextPrimary = AppStyle.ColorTokens.Green_60 + AppStyle.Color.SuccessTextOnMuted = AppStyle.ColorTokens.Green_60 + AppStyle.Color.SuccessTextOnPrimary = AppStyle.ColorTokens.White + AppStyle.Color.SuccessBgPrimary = AppStyle.ColorTokens.Green_60 + AppStyle.Color.SuccessBgMuted = AppStyle.ColorTokens.Green_10 + AppStyle.Color.SuccessBgMuted_Border = AppStyle.ColorTokens.Green_20 + + AppStyle.Color.DangerTextPrimary = AppStyle.ColorTokens.Red_60 + AppStyle.Color.DangerTextOnMuted = AppStyle.ColorTokens.Red_60 + AppStyle.Color.DangerTextOnPrimary = AppStyle.ColorTokens.White + AppStyle.Color.DangerBgPrimary = AppStyle.ColorTokens.Red_60 + AppStyle.Color.DangerBgMuted = AppStyle.ColorTokens.Red_10 + AppStyle.Color.DangerBgMuted_Border = AppStyle.ColorTokens.Red_20 + + AppStyle.Color.WarningTextPrimary = AppStyle.ColorTokens.Yellow_70 + AppStyle.Color.WarningTextOnMuted = AppStyle.ColorTokens.Yellow_90 + AppStyle.Color.WarningTextOnPrimary = AppStyle.ColorTokens.Yellow_100 + AppStyle.Color.WarningBgPrimary = AppStyle.ColorTokens.Yellow_50 + AppStyle.Color.WarningBgMuted = AppStyle.ColorTokens.Yellow_20 + AppStyle.Color.WarningBgMuted_Border = AppStyle.ColorTokens.Yellow_30 + + AppStyle.Color.Brand_AltTextPrimary = AppStyle.ColorTokens.Orange_60 + AppStyle.Color.Brand_AltTextSecondary = AppStyle.ColorTokens.Orange_30 + AppStyle.Color.Brand_AltTextOnMuted = AppStyle.ColorTokens.Orange_60 + AppStyle.Color.Brand_AltTextOnPrimary = AppStyle.ColorTokens.White + AppStyle.Color.Brand_AltBgPrimary = AppStyle.ColorTokens.Orange_60 + AppStyle.Color.Brand_AltBgMuted = AppStyle.ColorTokens.Orange_10 + AppStyle.Color.Brand_AltBgDisabled = AppStyle.ColorTokens.Orange_40 + + AppStyle.Color.AttentionTextPrimary = AppStyle.ColorTokens.Orange_60 + AppStyle.Color.AttentionTextOnMuted = AppStyle.ColorTokens.Orange_60 + AppStyle.Color.AttentionTextOnPrimary = AppStyle.ColorTokens.White + AppStyle.Color.AttentionBgPrimary = AppStyle.ColorTokens.Orange_60 + AppStyle.Color.AttentionBgMuted = AppStyle.ColorTokens.Orange_10 + AppStyle.Color.AttentionBgMuted_Border = AppStyle.ColorTokens.Orange_20 + + AppStyle.Color.MiscHome_Bar = AppStyle.ColorTokens.Black + } + + private func applyMidnightBlueAppearance() { + + + // ------- New Color Styles ------- // + AppStyle.Color.MainTextPrimary = AppStyle.ColorTokens.Gray_30 + AppStyle.Color.MainTextSecondary = AppStyle.ColorTokens.Gray_50 + AppStyle.Color.MainTextTertiary = AppStyle.ColorTokens.Gray_60 + + AppStyle.Color.MainBrandPrimary = AppStyle.ColorTokens.Blue_40 + + AppStyle.Color.MainNavigationOnNavigation = AppStyle.ColorTokens.Gray_30 + AppStyle.Color.MainNavigationBg = AppStyle.ColorTokens.Gray_80 + + AppStyle.Color.MainBgSurface = AppStyle.ColorTokens.Gray_100 + AppStyle.Color.MainBgSurface_Alt = AppStyle.ColorTokens.Gray_100 + AppStyle.Color.MainBgPrimary = AppStyle.ColorTokens.Gray_80 + AppStyle.Color.MainBgSecondary = AppStyle.ColorTokens.Gray_90 + AppStyle.Color.MainBgTertiary = AppStyle.ColorTokens.Gray_70 + + AppStyle.Color.BrandTextPrimary = AppStyle.ColorTokens.Blue_40 + AppStyle.Color.BrandTextSecondary = AppStyle.ColorTokens.Blue_20 + AppStyle.Color.BrandTextOnMuted = AppStyle.ColorTokens.Blue_40 + AppStyle.Color.BrandTextOnPrimary = AppStyle.ColorTokens.White + AppStyle.Color.BrandBgPrimary = AppStyle.ColorTokens.Blue_40 + AppStyle.Color.BrandBgMuted = AppStyle.ColorTokens.Blue_90 + AppStyle.Color.BrandBgMuted_Border = AppStyle.ColorTokens.Blue_80 + + AppStyle.Color.SuccessTextPrimary = AppStyle.ColorTokens.Green_40 + AppStyle.Color.SuccessTextOnMuted = AppStyle.ColorTokens.Green_30 + AppStyle.Color.SuccessTextOnPrimary = AppStyle.ColorTokens.White + AppStyle.Color.SuccessBgPrimary = AppStyle.ColorTokens.Green_50 + AppStyle.Color.SuccessBgMuted = AppStyle.ColorTokens.Green_80 + AppStyle.Color.SuccessBgMuted_Border = AppStyle.ColorTokens.Green_70 + + AppStyle.Color.DangerTextPrimary = AppStyle.ColorTokens.Red_50 + AppStyle.Color.DangerTextOnMuted = AppStyle.ColorTokens.Red_30 + AppStyle.Color.DangerTextOnPrimary = AppStyle.ColorTokens.White + AppStyle.Color.DangerBgPrimary = AppStyle.ColorTokens.Red_50 + AppStyle.Color.DangerBgMuted = AppStyle.ColorTokens.Red_100 + AppStyle.Color.DangerBgMuted_Border = AppStyle.ColorTokens.Red_90 + + AppStyle.Color.WarningTextPrimary = AppStyle.ColorTokens.Yellow_50 + AppStyle.Color.WarningTextOnMuted = AppStyle.ColorTokens.Yellow_20 + AppStyle.Color.WarningTextOnPrimary = AppStyle.ColorTokens.Yellow_100 + AppStyle.Color.WarningBgPrimary = AppStyle.ColorTokens.Yellow_50 + AppStyle.Color.WarningBgMuted = AppStyle.ColorTokens.Yellow_100 + AppStyle.Color.WarningBgMuted_Border = AppStyle.ColorTokens.Yellow_90 + + AppStyle.Color.Brand_AltTextPrimary = AppStyle.ColorTokens.Blue_40 + AppStyle.Color.Brand_AltTextSecondary = AppStyle.ColorTokens.Blue_20 + AppStyle.Color.Brand_AltTextOnMuted = AppStyle.ColorTokens.Blue_40 + AppStyle.Color.Brand_AltTextOnPrimary = AppStyle.ColorTokens.White + AppStyle.Color.Brand_AltBgPrimary = AppStyle.ColorTokens.Blue_40 + AppStyle.Color.Brand_AltBgMuted = AppStyle.ColorTokens.Blue_90 + AppStyle.Color.Brand_AltBgDisabled = AppStyle.ColorTokens.Blue_40 + + AppStyle.Color.AttentionTextPrimary = AppStyle.ColorTokens.Orange_50 + AppStyle.Color.AttentionTextOnMuted = AppStyle.ColorTokens.Orange_30 + AppStyle.Color.AttentionTextOnPrimary = AppStyle.ColorTokens.Orange_100 + AppStyle.Color.AttentionBgPrimary = AppStyle.ColorTokens.Orange_50 + AppStyle.Color.AttentionBgMuted = AppStyle.ColorTokens.Orange_100 + AppStyle.Color.AttentionBgMuted_Border = AppStyle.ColorTokens.Orange_90 + + AppStyle.Color.MiscHome_Bar = AppStyle.ColorTokens.White + } +} + + +public struct AppStyle { + + // MARK: - Colors + public struct ColorTokens { + + public static var Black = UIColor.black + + public static var White = UIColor.white + + public static var Gray_10 = UIColor("#FBFCFE") + public static var Gray_20 = UIColor("#F2F2F8") + public static var Gray_30 = UIColor("#D9E1EB") + public static var Gray_40 = UIColor("#A0AAB5") + public static var Gray_50 = UIColor("#6F7F90") + public static var Gray_60 = UIColor("#4C5C70") + public static var Gray_70 = UIColor("#2E3E54") + public static var Gray_80 = UIColor("#232F3F") + public static var Gray_90 = UIColor("#212C3A") + public static var Gray_100 = UIColor("#1A212B") + + public static var Red_10 = UIColor("#FFEFEF") + public static var Red_20 = UIColor("#fed7d7") + public static var Red_30 = UIColor("#feb2b2") + public static var Red_40 = UIColor("#fc8181") + public static var Red_50 = UIColor("#f56565") + public static var Red_60 = UIColor("#e53e3e") + public static var Red_70 = UIColor("#c53030") + public static var Red_80 = UIColor("#9b2c2c") + public static var Red_90 = UIColor("#742a2a") + public static var Red_100 = UIColor("#4B2830") + + public static var Yellow_10 = UIColor("#fffff0") + public static var Yellow_20 = UIColor("#fefcbf") + public static var Yellow_30 = UIColor("#faf089") + public static var Yellow_40 = UIColor("#f6e05e") + public static var Yellow_50 = UIColor("#ecc94b") + public static var Yellow_60 = UIColor("#d69e2e") + public static var Yellow_70 = UIColor("#b7791f") + public static var Yellow_80 = UIColor("#975a16") + public static var Yellow_90 = UIColor("#744210") + public static var Yellow_100 = UIColor("#4F3502") + + public static var Green_10 = UIColor("#E0FAEE") + public static var Green_20 = UIColor("#ABEDCF") + public static var Green_30 = UIColor("#7BDBB0") + public static var Green_40 = UIColor("#56C292") + public static var Green_50 = UIColor("#3BA073") + public static var Green_60 = UIColor("#287D57") + public static var Green_70 = UIColor("#1D5E41") + public static var Green_80 = UIColor("#16422E") + public static var Green_90 = UIColor("#123023") + + public static var Blue_10 = UIColor("#E8EEFD") + public static var Blue_20 = UIColor("#B6C9FA") + public static var Blue_30 = UIColor("#96B2F7") + public static var Blue_40 = UIColor("#2DA4FE") + public static var Blue_50 = UIColor("#167CF9") + public static var Blue_60 = UIColor("#3067F0") + public static var Blue_70 = UIColor("#1746BD") + public static var Blue_80 = UIColor("#2A5573") + public static var Blue_90 = UIColor("#19364A") + + public static var Orange_10 = UIColor("#FDEFE9") + public static var Orange_20 = UIColor("#FAE0D4") + public static var Orange_30 = UIColor("#F6C4AD") + public static var Orange_40 = UIColor("#F1A07B") + public static var Orange_50 = UIColor("#EE895B") + public static var Orange_60 = UIColor("#E86427") + public static var Orange_70 = UIColor("#BF5728") + public static var Orange_80 = UIColor("#964929") + public static var Orange_90 = UIColor("#713D29") + public static var Orange_100 = UIColor("#4B312A") + } + + public struct Color { + + public static var Primary = UIColor.black + public static var PrimaryContrast = UIColor.lightGray + public static var Accent = UIColor.lightGray + public static var Accent2 = UIColor.white + public static var Accent2Contrast = UIColor.black + + public static var Background = UIColor.lightGray + public static var BackgroundHighlight = UIColor.lightGray + public static var SegmentSelectedBackground = UIColor.lightGray + public static var SegmentUnselectedBackground = UIColor.lightGray + public static var SegmentUnselectedBackgroundOnNonWhiteBackground = UIColor.lightGray + public static var LightBackground = UIColor.white + public static var LightBackgroundContrast = UIColor.white + public static var DarkBackground = UIColor.lightGray + public static var WalletBallanceDetailsBackground = UIColor.lightGray + + public static var LowEmphasisBackground = UIColor.systemBlue + public static var LowEmphasisText = UIColor.lightText + public static var LowEmphasisSubText = UIColor.lightText + + public static var MediumEmphasisBackground = UIColor.yellow + public static var MediumEmphasisText = UIColor.lightText + public static var MediumEmphasisSubText = UIColor.lightText + + public static var MediumEmphasisButton = UIColor.yellow + public static var MediumEmphasisButtonText = UIColor.lightText + + public static var HighEmphasisBackground = UIColor.red + public static var HighEmphasisText = UIColor.red + public static var HighEmphasisSubText = UIColor.red + + public static var PrimaryText = UIColor.black + public static var SecondaryText = UIColor.lightGray + + public static var NavigationItem = UIColor.white + public static var BannerText = UIColor.black + public static var SecondaryBlueWithAlpha = UIColor("#27A8FF").withAlphaComponent(0.15) + + public static var Loader = UIColor.lightGray + + public static var TabBar = UIColor.white + public static var TabBarItemUnselected = UIColor.lightGray + public static var TabBarItemSelected = Primary + + public static var SwitchOn = UIColor.green + + public static var PrimaryGreen = UIColor.green + public static var PrimaryRed = UIColor.red + + public static var ChartBackground = UIColor.white + public static var ChartGreen = UIColor.green + public static var ChartRed = UIColor.red + public static var Chart2Green = UIColor.green + public static var Chart2Red = UIColor.red + public static var ChartGrid = UIColor.lightGray + public static var ChartText = UIColor.lightGray + public static var ChartAxisLineColor = UIColor.lightGray + + public static var OrderBookGreen = UIColor.green + public static var OrderBookRed = UIColor.red + public static var OrderBookModeButtonColor = UIColor.black + public static var OrderBookModeButtonTextColor = UIColor.white + + public static var TradeHistoryGreen = UIColor.green + public static var TradeHistoryRed = UIColor.red + + public static var CellBackground = UIColor.white + public static var CellBackgroundAlternate = UIColor.white + public static var CellHighlight = UIColor.lightGray + public static var CellIconBackground = Background + public static var CellSeparator = UIColor.lightGray + public static var CellSeparator2 = CellSeparator + + public static var TextFieldStrokeOnPrimaryColor = UIColor.lightGray + public static var TextFieldStrokeOnLightBackground = UIColor.lightGray + + public static var SelectedElement = UIColor.lightGray + public static var Favourite = UIColor.yellow + public static var FavouriteBackground = UIColor.lightGray + + public static var Error = UIColor.red + public static var Success = UIColor.green + + public static var GeneralStatus = UIColor.green + public static var PositiveStatus = UIColor.green + public static var NegativeStatus = UIColor.green + public static var WarningStatus = UIColor.green + + public static var ConnectionBar = UIColor.black + public static var Connecting = UIColor.yellow + public static var Connected = UIColor.green + public static var Disconnected = UIColor.red + + public static var SearchBarCancelButton = UIColor.white + + // For Widget + public static let WidgetPrimaryGreen = UIColor("#4DD964") + public static let WidgetPrimaryRed = UIColor("#FE3B2F") + + public static var PageControlTint = UIColor.blue.withAlphaComponent(0.3) + public static var PageControlSelectedTint = UIColor.blue.withAlphaComponent(0.3) + + // Gift Crypto + public static var GiftCryptoGreen = UIColor("#00C966") + public static var GiftCryptoPink = UIColor("#FF5AC6") + public static var GiftCryptoYellow = UIColor("#FFD410") + public static var GiftCryptoViolet = UIColor("#770CE7") + + // For Step Indicator + public static var CurrentStepIndicator = UIColor.gray + + // ------- New Color Styles ------- // + public static var MainTextPrimary = UIColor.gray + public static var MainTextSecondary = UIColor.gray + public static var MainTextTertiary = UIColor.gray + + public static var MainBrandPrimary = UIColor.blue + + public static var MainNavigationOnNavigation = UIColor.white + public static var MainNavigationBg = UIColor.lightGray + + public static var MainBgSurface = UIColor.gray + public static var MainBgSurface_Alt = UIColor.gray + public static var MainBgPrimary = UIColor.lightGray + public static var MainBgSecondary = UIColor.lightGray + public static var MainBgTertiary = UIColor.lightGray + + public static var BrandTextPrimary = UIColor.black + public static var BrandTextSecondary = UIColor.black + public static var BrandTextOnMuted = UIColor.black + public static var BrandTextOnPrimary = UIColor.black + public static var BrandBgPrimary = UIColor.lightGray + public static var BrandBgMuted = UIColor.lightGray + public static var BrandBgMuted_Border = UIColor.lightGray + + public static var SuccessTextPrimary = UIColor.green + public static var SuccessTextOnMuted = UIColor.green + public static var SuccessTextOnPrimary = UIColor.green + public static var SuccessBgPrimary = UIColor.lightGray + public static var SuccessBgMuted = UIColor.lightGray + public static var SuccessBgMuted_Border = UIColor.lightGray + + public static var DangerTextPrimary = UIColor.red + public static var DangerTextOnMuted = UIColor.red + public static var DangerTextOnPrimary = UIColor.red + public static var DangerBgPrimary = UIColor.lightGray + public static var DangerBgMuted = UIColor.lightGray + public static var DangerBgMuted_Border = UIColor.lightGray + + public static var WarningTextPrimary = UIColor.yellow + public static var WarningTextOnMuted = UIColor.yellow + public static var WarningTextOnPrimary = UIColor.yellow + public static var WarningBgPrimary = UIColor.lightGray + public static var WarningBgMuted = UIColor.lightGray + public static var WarningBgMuted_Border = UIColor.lightGray + + public static var Brand_AltTextPrimary = UIColor.black + public static var Brand_AltTextSecondary = UIColor.black + public static var Brand_AltTextOnMuted = UIColor.black + public static var Brand_AltTextOnPrimary = UIColor.black + public static var Brand_AltBgPrimary = UIColor.lightGray + public static var Brand_AltBgMuted = UIColor.lightGray + public static var Brand_AltBgDisabled = UIColor.lightGray + + public static var AttentionTextPrimary = UIColor.orange + public static var AttentionTextOnMuted = UIColor.orange + public static var AttentionTextOnPrimary = UIColor.orange + public static var AttentionBgPrimary = UIColor.lightGray + public static var AttentionBgMuted = UIColor.lightGray + public static var AttentionBgMuted_Border = UIColor.lightGray + + public static var MiscHome_Bar = UIColor.white + } + + // MARK: - Fonts + public struct Font { + + public static let MaterialDesignIconsRange = NSRange(location: 0xF001, length: 2302) + public static func MaterialDesignIconsFontName() -> String { + return "Material Design Icons" + } + public static func MaterialDesignIconsFont(size: CGFloat) -> UIFont { + return UIFont(name: MaterialDesignIconsFontName(), size: size)! + } + + public static let FamilyName = "IBM Plex Sans" + + public static let BoldName = "IBMPlexSans-Bold" + public static func Bold(size: CGFloat) -> UIFont { + return UIFont(name: BoldName, size: size)! + } + public static let ItalicBoldName = "IBMPlexSans-BoldItalic" + public static func ItalicBold(size: CGFloat) -> UIFont { + return UIFont(name: ItalicBoldName, size: size)! + } + public static let SemiBoldName = "IBMPlexSans-SmBld" + public static func SemiBold(size: CGFloat) -> UIFont { + return UIFont(name: SemiBoldName, size: size)! + } + public static let ItalicSemiBoldName = "IBMPlexSans-SmBldItalic" + public static func ItalicSemiBold(size: CGFloat) -> UIFont { + return UIFont(name: ItalicSemiBoldName, size: size)! + } + + public static let MediumName = "IBMPlexSans-Medm" + public static func Medium(size: CGFloat) -> UIFont { + return UIFont(name: MediumName, size: size)! + } + public static let RegularName = "IBMPlexSans" + public static func Regular(size: CGFloat) -> UIFont { + return UIFont(name: RegularName, size: size)! + } + + public enum Style { + + case heading1_regular + case heading1_bold + case heading1_semibold + + case heading2_regular + case heading2_bold + case heading2_semibold + + case heading3_regular + case heading3_bold + case heading3_semibold + + case heading4_regular + case heading4_bold + case heading4_semibold + + case heading5_regular + case heading5_bold + case heading5_semibold + + case heading6_regular + case heading6_bold + case heading6_semibold + + case large_regular + case large_bold + case large_semibold + case large_medium + + case base_regular + case base_bold + case base_semibold + case base_medium + + case small_regular + case small_bold + case small_semibold + case small_medium + + case xsmall_regular + case xsmall_bold + case xsmall_semibold + case xsmall_medium + } + } + + // MARK: Other + public static let ButtonCornerRadius : CGFloat = 6.0 + public static let BottomSheetCornerRadius : CGFloat = 10.0 + public static let LoadingIndicatorLineWidth : CGFloat = 3.0 + public static let LoadingIndicatorAnimationDuration : TimeInterval = 0.8 + public static var KeyboardAppearance: UIKeyboardAppearance = .default + public static var SelectedTabLineHeight: CGFloat = 4.0 +} diff --git a/BulletinSDK/Classes/Constants/Constants.swift b/BulletinSDK/Classes/Constants/Constants.swift new file mode 100644 index 0000000..841c5df --- /dev/null +++ b/BulletinSDK/Classes/Constants/Constants.swift @@ -0,0 +1,91 @@ +// +// WZConstants.swift +// WazirX +// +// Created by Daxesh Nagar on 17/07/23. +// Copyright © 2017 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + + + +// MARK: - +public struct App { + + // App Lock State + public enum LockState: Int { + case locked = 0 + case unlocked + } + + // MARK: Variables + public static let Platform = "iOS" + public static let Name = "WazirX" + public static let DevLanguage = "Swift" + public static let TeamID = "CAQ757V6Q3" + public static let BundleID = Bundle.main.bundleIdentifier! + public static let Version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String + public static let BuildNumber = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as! String + public static func URLScheme() -> String { return "wazirx://"} + + + + // Redirects User To App Permissions In Device Settings + public static func OpenAppPermissionsInDeviceSettings(completion: ((Bool) -> Swift.Void)?) { + if let url = URL(string: UIApplication.openSettingsURLString), + UIApplication.shared.canOpenURL(url) + { + UIApplication.shared.open(url, options: Dictionary(), completionHandler: completion) + } + } + +} + + +// MARK: - +extension NSNotification.Name { + public static let AppearanceDidUpdate = NSNotification.Name("AppearanceDidUpdate") + public static let UserDidLogin = NSNotification.Name("UserDidLogin") +} + + +// MARK: - +public struct HappinessPoints { + public static func Threshold() -> Int { return 20 } + public static func BuyOrder() -> Int { return 7 } + public static func SellOrder() -> Int { return 7 } + public static func ShareWithFriends() -> Int { return 5 } + public static func Error() -> Int { return 2 } +} + + +// MARK: - +public struct AppRating { + public static func DaysIntervalBetweenPointsReset() -> Int { return 14 } + public static func DaysIntervalBetweenTwoAppRatePopup() -> Int { return 40 } +} + + + +// MARK: - +public struct S3ImageName { + + // Create S3 Images Constants + public static func qbs_texture() -> String { return "qbs_texture" } + public static func no_token() -> String { return "no_token" } + public static func qbs_failure() -> String { return "qbs_failure" } + public static func qbs_success() -> String { return "qbs_success" } + public static func qbs_warning() -> String { return "qbs_warning" } + public static func sand_clock() -> String { return "sand_clock" } + +} + + +// MARK: - Regex +public struct Regex { + + public static func email() -> String { + return "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@" + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+" + } +} diff --git a/BulletinSDK/Classes/Constants/Text.swift b/BulletinSDK/Classes/Constants/Text.swift new file mode 100644 index 0000000..d3d209b --- /dev/null +++ b/BulletinSDK/Classes/Constants/Text.swift @@ -0,0 +1,28 @@ +// +// WZMessages.swift +// WazirX +// +// Created by Daxesh Nagar on 17/07/23. +// Copyright © 2017 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + +public struct Text { + + // MARK: - Common Texts + public static func WazirX() -> String { return "WazirX".localized() } + public static func Binance() -> String { return "Binance".localized() } + public static func Close() -> String { return "Close".localized() } + public static func Cancel() -> String { return "Cancel".localized() } + public static func Cancelled() -> String { return "Cancelled".localized() } + public static func CancelAll() -> String { return "Cancel all".localized() } + public static func Back() -> String { return "Back".localized() } + public static func Ok() -> String { return "Ok".localized() } + public static func Or() -> String { return "Or".localized() } + public static func PopularCoins() -> String { return "Popular coins".localized() } + public static func Buy() -> String { return "Buy".localized() } + public static func AverageBuyPrice() -> String { return "Average buy price".localized() } + public static func ProfitAndLoss() -> String { return "Profit & loss".localized() } + public static func ProfitAndLossProcessing() -> String { return "Updating P&L.. (processing recent transactions)".localized() } +} diff --git a/BulletinSDK/Classes/Helper/BulletinHelper.swift b/BulletinSDK/Classes/Helper/BulletinHelper.swift new file mode 100644 index 0000000..90e67a2 --- /dev/null +++ b/BulletinSDK/Classes/Helper/BulletinHelper.swift @@ -0,0 +1,55 @@ +// +// BulletinHelper.swift +// Bulletin +// +// Created by Daxesh Nagar on 24/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + +class BulletinHelper { } + +extension BulletinHelper { + + // MARK: - View Controller Helper Methods + public class func rootViewController(_ handler: ((_ rootVC: UIViewController?) -> Void)?) { + + if let handler = handler { + DispatchQueue.main.async { + handler(UIApplication.shared.keyWindow?.rootViewController) + } + } + } + + + public class func topMostViewController(_ handler: ((_ topMostVC: UIViewController?) -> Void)?) { + + if let handler = handler { + + DispatchQueue.main.async { + let rootVC = UIApplication.shared.keyWindow?.rootViewController + let topMostVC = BulletinHelper.topViewController(withRootViewController: rootVC) + handler(topMostVC) + } + } + } + + private class func topViewController(withRootViewController rootViewController: UIViewController?) -> UIViewController? { + + if let tabBarController = rootViewController as? UITabBarController { + return BulletinHelper.topViewController(withRootViewController: tabBarController.selectedViewController) + } + else if let navigationController = rootViewController as? UINavigationController { + return BulletinHelper.topViewController(withRootViewController: navigationController.visibleViewController) + } + else if let presentedViewController = rootViewController?.presentedViewController { + return BulletinHelper.topViewController(withRootViewController: presentedViewController) + } + else { + return rootViewController + } + } + +} + diff --git a/BulletinSDK/Classes/Helper/LocalizationHelper.swift b/BulletinSDK/Classes/Helper/LocalizationHelper.swift new file mode 100644 index 0000000..681a6e3 --- /dev/null +++ b/BulletinSDK/Classes/Helper/LocalizationHelper.swift @@ -0,0 +1,52 @@ +// +// LocalizationHelper.swift +// Bulletin +// +// Created by Daxesh Nagar on 18/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import Foundation + + +class LocalizationHelper { + + public class func string(forKey key: String) -> String { + return string(forKey: key, replaceOccurencesOfKeysWithValues: nil) + } + + public class func string(forKey key: String, + replaceOccurencesOfKeysWithValues keyValuePairs: [String:String]?) -> String + { + var localizedString = NSLocalizedString(key, tableName: "Localizable", bundle: Bundle.main, value: "", comment: "") + if localizedString == key { + localizedString = NSLocalizedString(key, comment: "") + } + + if let keyValuePairs = keyValuePairs, keyValuePairs.isEmpty == false { + for key in keyValuePairs.keys { + let keyToReplace = "~{" + key + "}" + if let replaceWithValue = keyValuePairs[key] { + localizedString = localizedString.replacingOccurrences(of: keyToReplace, with: replaceWithValue) + } + } + } + + return localizedString + } +} + + +extension String { + + // Localization Conveniance method for strings + public func localized() -> String { + return LocalizationHelper.string(forKey: self) + } + + public func localized(replaceOccurencesOfKeysWithValues keyValuePairs: [String: String]?) -> String { + return LocalizationHelper.string(forKey: self, replaceOccurencesOfKeysWithValues: keyValuePairs) + } +} + + diff --git a/BulletinSDK/Libraries/PopUp/AlertSegue.swift b/BulletinSDK/Libraries/PopUp/AlertSegue.swift new file mode 100644 index 0000000..2e0fb4d --- /dev/null +++ b/BulletinSDK/Libraries/PopUp/AlertSegue.swift @@ -0,0 +1,29 @@ +// +// AlertSegue.swift +// Bulletin +// +// Created by Daxesh Nagar on 24/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit +import SwiftMessages + +class AlertSegue: SwiftMessagesSegue { + + + // MARK: - Initialisation Method + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + + // Override Configurations + configure(layout: .centered) + interactiveHide = false + dimMode = .color(color: UIColor.black.withAlphaComponent(0.70), interactive: true) + source.view.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + source.view.layer.cornerRadius = AppStyle.BottomSheetCornerRadius + if #available(iOS 13.0, *) { + source.view.layer.cornerCurve = .continuous + } + } +} diff --git a/BulletinSDK/Libraries/PopUp/BottomSheetSegue.swift b/BulletinSDK/Libraries/PopUp/BottomSheetSegue.swift new file mode 100644 index 0000000..9d2c052 --- /dev/null +++ b/BulletinSDK/Libraries/PopUp/BottomSheetSegue.swift @@ -0,0 +1,29 @@ +// +// BottomSheetSegue.swift +// Bulletin +// +// Created by Daxesh Nagar on 24/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit +import SwiftMessages + +class BottomSheetSegue: SwiftMessagesSegue { + + + // MARK: - Initialisation Method + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + + // Override Configurations + configure(layout: .bottomTab) + interactiveHide = false + dimMode = .color(color: UIColor.black.withAlphaComponent(0.70), interactive: true) + source.view.layoutMargins = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + source.view.layer.cornerRadius = AppStyle.BottomSheetCornerRadius + if #available(iOS 13.0, *) { + source.view.layer.cornerCurve = .continuous + } + } +} diff --git a/BulletinSDK/Libraries/PushButton/PushButton.swift b/BulletinSDK/Libraries/PushButton/PushButton.swift new file mode 100644 index 0000000..61e84a2 --- /dev/null +++ b/BulletinSDK/Libraries/PushButton/PushButton.swift @@ -0,0 +1,199 @@ +// +// PushButton.swift +// Bulletin +// +// Created by Daxesh Nagar on 21/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + +open class PushButton: UIButton { + + // MARK: - Declarations + public static let PUSH_BUTTON_DEFAULT_TOUCH_DOWN_DURATION: CGFloat = 0.22 + public static let PUSH_BUTTON_DEFAULT_TOUCH_DOWN_DELAY: CGFloat = 0.0 + public static let PUSH_BUTTON_DEFAULT_TOUCH_DOWN_DAMPING: CGFloat = 0.6 + public static let PUSH_BUTTON_DEFAULT_TOUCH_DOWN_VELOCITY: CGFloat = 0.0 + public static let PUSH_BUTTON_DEFAULT_TOUCH_UP_DURATION: CGFloat = 0.7 + public static let PUSH_BUTTON_DEFAULT_TOUCH_UP_DELAY: CGFloat = 0.0 + public static let PUSH_BUTTON_DEFAULT_TOUCH_UP_DAMPING: CGFloat = 0.65 + public static let PUSH_BUTTON_DEFAULT_TOUCH_UP_VELOCITY: CGFloat = 0.0 + + + // MARK: - Variables + + // Original Transform Property + open var originalTransform = CGAffineTransform.identity { + didSet { + // Update Button Transform + transform = originalTransform + } + } + + // Set Highlight Property + @IBInspectable open var highlightStateBackgroundColor: UIColor? + + // Push Transform Property + @IBInspectable open var pushTransformScaleFactor: CGFloat = 0.8 + + // Touch Handler Blocks + open var touchDownHandler: ((_ button: PushButton) -> Void)? + open var touchUpHandler: ((_ button: PushButton) -> Void)? + + // Push Transition Animation Properties + @IBInspectable open var touchDownDuration: CGFloat = PUSH_BUTTON_DEFAULT_TOUCH_DOWN_DURATION + @IBInspectable open var touchDownDelay: CGFloat = PUSH_BUTTON_DEFAULT_TOUCH_DOWN_DELAY + @IBInspectable open var touchDownDamping: CGFloat = PUSH_BUTTON_DEFAULT_TOUCH_DOWN_DAMPING + @IBInspectable open var touchDownVelocity: CGFloat = PUSH_BUTTON_DEFAULT_TOUCH_DOWN_VELOCITY + + @IBInspectable open var touchUpDuration: CGFloat = PUSH_BUTTON_DEFAULT_TOUCH_UP_DURATION + @IBInspectable open var touchUpDelay: CGFloat = PUSH_BUTTON_DEFAULT_TOUCH_UP_DELAY + @IBInspectable open var touchUpDamping: CGFloat = PUSH_BUTTON_DEFAULT_TOUCH_UP_DAMPING + @IBInspectable open var touchUpVelocity: CGFloat = PUSH_BUTTON_DEFAULT_TOUCH_UP_VELOCITY + + // Add Extra Parameters + open var extraParam: Any? + + private var normalStateBackgroundColor: UIColor? + open override var backgroundColor: UIColor? { + didSet { + // Store Normal State Background Color + normalStateBackgroundColor = backgroundColor + } + } + + + // MARK: - Initialization Methods + public required init?(coder: NSCoder) { + super.init(coder: coder) + basicInitialisation() + } + + public override init(frame: CGRect) { + super.init(frame: frame) + basicInitialisation() + } + + open func basicInitialisation() { + + // Add Corner Curve + if #available(iOS 13.0, *) { + layer.cornerCurve = .continuous + } + + // Set Default Original Transform + originalTransform = CGAffineTransform.identity + } + + + // MARK: - Touch Events + open override func touchesBegan(_ touches: Set, with event: UIEvent?) { + super.touchesBegan(touches, with: event) + pushButton(pushButton: true, shouldAnimate: true, completion: nil) + } + + open override func touchesEnded(_ touches: Set, with event: UIEvent?) { + super.touchesEnded(touches, with: event) + pushButton(pushButton: false, shouldAnimate: true, completion: nil) + } + + open override func touchesCancelled(_ touches: Set, with event: UIEvent?) { + super.touchesCancelled(touches, with: event) + pushButton(pushButton: false, shouldAnimate: true, completion: nil) + } + + + // MARK: - Animation Method + open func pushButton(pushButton: Bool, shouldAnimate: Bool, completion: (() -> Void)?) { + + // Call Touch Events + if pushButton { + // Call Touch Down Handler + if let touchDownHandler = touchDownHandler { + touchDownHandler(self) + } + } + else { + // Call Touch Up Handler + if let touchUpHandler = touchUpHandler { + touchUpHandler(self) + } + } + + // Animation Block + let animate: (() -> Void)? = {() -> Void in + + if pushButton { + // Calculate Scale Factor For Current Frame + let scaleFactor : CGFloat = self.pushTransformScaleFactor + // Set Transform + self.transform = self.originalTransform.scaledBy(x: scaleFactor, y: scaleFactor) + // Update Background Color + if (self.highlightStateBackgroundColor != nil) { + super.backgroundColor = self.highlightStateBackgroundColor + } + } + else { + // Set Transform + self.transform = self.originalTransform + // Set Background Color + super.backgroundColor = self.normalStateBackgroundColor + } + // Layout + self.setNeedsLayout() + self.layoutIfNeeded() + } + + if shouldAnimate { + + // Configure Animation Properties + var duration: CGFloat + var delay: CGFloat + var damping: CGFloat + var velocity: CGFloat + if pushButton { + duration = touchDownDuration + delay = touchDownDelay + damping = touchDownDamping + velocity = touchDownVelocity + } + else { + duration = touchUpDuration + delay = touchUpDelay + damping = touchUpDamping + velocity = touchUpVelocity + } + + DispatchQueue.main.async { + // Animate + UIView.animate(withDuration: TimeInterval(duration), delay: TimeInterval(delay), usingSpringWithDamping: damping, initialSpringVelocity: velocity, options: [.curveEaseOut, .beginFromCurrentState, .allowUserInteraction], animations: {() -> Void in + animate!() + }, completion: {(_ finished: Bool) -> Void in + if let completion = completion, finished { + completion() + } + }) + } + } + else { + animate!() + + // Call Completion Block + if let completion = completion { + completion() + } + } + } + + + // MARK: - Dealloc + deinit { + + // Remove Touch Handlers + touchDownHandler = nil + touchUpHandler = nil + } +} + + diff --git a/BulletinSDK/Modals/BulletinInfo.swift b/BulletinSDK/Modals/BulletinInfo.swift index b93bbe5..a44a665 100644 --- a/BulletinSDK/Modals/BulletinInfo.swift +++ b/BulletinSDK/Modals/BulletinInfo.swift @@ -6,10 +6,9 @@ // Copyright © 2022 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. // -import Foundation +import UIKit - -class BulletinInfo { +class BulletinInfo { // MARK: - Variables @@ -27,3 +26,4 @@ class BulletinInfo { self.items = items } } + diff --git a/BulletinSDK/Modals/UI Modals/Title.swift b/BulletinSDK/Modals/UI Modals/Title.swift index 07195c6..88348e9 100644 --- a/BulletinSDK/Modals/UI Modals/Title.swift +++ b/BulletinSDK/Modals/UI Modals/Title.swift @@ -12,11 +12,11 @@ class Title: BulletinItem { // MARK: - Variable var preTitleText: String? - public var preTitleTextColor: UIColor? + public var preTitleTextColor: String? var titleText: String? - public var titleTextColor: UIColor? + public var titleTextColor: String? var subTitleText: String? - public var subTitleTextColor: UIColor? + public var subTitleTextColor: String? // MARK: - Initialisation Methods diff --git a/BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-Bold.ttf b/BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..72b190a849c9c275b44de7466c5bbd101e4e0403 GIT binary patch literal 193120 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 literal 0 HcmV?d00001 diff --git a/BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-BoldItalic.ttf b/BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-BoldItalic.ttf new file mode 100644 index 0000000000000000000000000000000000000000..75c2959570738e527b76aafa69884d4c8868b4f9 GIT binary patch literal 202768 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 Q)sY4A89)75+tAK`0X?0=I{*Lx literal 0 HcmV?d00001 diff --git a/BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-Medium.ttf b/BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..c80d08a90647bcd20d2760e128a6388523c46036 GIT binary patch literal 194600 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

u-SuK>exuAXddqXAyQC*eb!E*NeH=Mw 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 vEfB4=f; + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift new file mode 100644 index 0000000..938aa53 --- /dev/null +++ b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift @@ -0,0 +1,106 @@ +// +// BulletPointViewCell.swift +// Bulletin +// +// Created by Daxesh Nagar on 20/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + +protocol BulletPointViewCellDelegate: AnyObject { + func bulletPointViewCell(_ cell: BulletPointViewCell, ofItem item: BulletinItem) +} + +class BulletPointViewCell: BaseCollectionViewCell { + + // MARK: - Variables + @IBOutlet private var bulletTitle: UILabel! + @IBOutlet private var bulletDesc: UILabel! + @IBOutlet private var bulletName: UILabel! + @IBOutlet private var bulletImageView: UIImageView! + + public weak var delegate: BulletPointViewCellDelegate? + + public var item: BulletPoint? { + didSet { + + // Validation + guard let bulletPointItem = item else { + + // Reset To Nil + bulletTitle = nil + bulletDesc = nil + bulletImageView = nil + bulletName = nil + + return + } + + switch (bulletPointItem.bullet?.bulletType) { + + case .unicode: + + let htmlAttributedString = bulletPointItem.bullet?.unicode?.html2AttributedString(usingFont: AppStyle.Font.SemiBold(size: 14.0), color: AppStyle.Color.SecondaryText)?.trailingNewlineChopped + + if let attributedMessage = htmlAttributedString { + + // Set Description Message + bulletTitle.attributedText = attributedMessage + } + + case .image: + + // Set downloaded Image Icon + if let iconUrl = bulletPointItem.bullet?.imageUrl { + bulletImageView.kf.setImage(with: iconUrl) { [weak self] (result) in + switch result { + case .success(let value): + + // Image downloaded + if let sourceUrl = value.source.url, + sourceUrl == iconUrl { + self?.bulletImageView.image = value.image + } + case .failure(let error): + print("\(error.localizedDescription)") + } + } + } + + case .none: + bulletTitle.isHidden = true + bulletDesc.isHidden = true + } + + bulletTitle.text = bulletPointItem.titleText + bulletDesc.text = bulletPointItem.subTitleText + + } + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func updateAppearance() { + super.updateAppearance() + + // Set Background Color + backgroundColor = AppStyle.Color.Background + + + // Set preTitle Label + bulletTitle.small_medium() + bulletTitle.textColor = AppStyle.Color.SuccessTextPrimary + + // Set Title Label + bulletDesc.heading4_semibold() + bulletDesc.textColor = AppStyle.Color.MainTextPrimary + + bulletName.heading4_semibold() + } + +} + diff --git a/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib b/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib new file mode 100644 index 0000000..6bd7b1d --- /dev/null +++ b/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BulletinSDK/View/Media/MediaViewCell.swift b/BulletinSDK/View/Media/MediaViewCell.swift new file mode 100644 index 0000000..2d1484e --- /dev/null +++ b/BulletinSDK/View/Media/MediaViewCell.swift @@ -0,0 +1,61 @@ +// +// MediaViewCell.swift +// Bulletin +// +// Created by Daxesh Nagar on 20/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit +import Kingfisher + +protocol MediaViewCellDelegate: AnyObject { + func mediaViewCell(_ cell: MediaViewCell, ofItem item: BulletinItem) +} + +class MediaViewCell: BaseCollectionViewCell { + + // MARK: - Variables + @IBOutlet public var cardViewContainer: UIView! + @IBOutlet private var artworkImageView: UIImageView! + + public weak var delegate: MediaViewCellDelegate? + + public var item: Media? { + didSet { + + // Validation + guard let mediaItem = item else { + + // Reset To Nil + artworkImageView.image = nil + return + } + + // Set downloaded Image Icon + if let iconUrl = mediaItem.url { + artworkImageView.kf.setImage(with: iconUrl) { [weak self] (result) in + switch result { + case .success(let value): + + // Image downloaded + if let sourceUrl = value.source.url, + sourceUrl == iconUrl { + self?.artworkImageView.image = value.image + } + case .failure(let error): + print("\(error.localizedDescription)") + } + } + } + + } + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + +} diff --git a/BulletinSDK/View/Media/MediaViewCell.xib b/BulletinSDK/View/Media/MediaViewCell.xib new file mode 100644 index 0000000..c28774e --- /dev/null +++ b/BulletinSDK/View/Media/MediaViewCell.xib @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BulletinSDK/View/Message/MessageViewCell.swift b/BulletinSDK/View/Message/MessageViewCell.swift new file mode 100644 index 0000000..9b73ca6 --- /dev/null +++ b/BulletinSDK/View/Message/MessageViewCell.swift @@ -0,0 +1,72 @@ +// +// MessageViewCell.swift +// Bulletin +// +// Created by Daxesh Nagar on 20/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + +protocol MessageViewCellDelegate: AnyObject { + func messageViewCell(_ cell: MessageViewCell, ofItem item: BulletinItem) +} + +class MessageViewCell: BaseCollectionViewCell { + + // MARK: - Variables + @IBOutlet private var descTitle: UILabel! + + public weak var delegate: MessageViewCellDelegate? + + public var item: Message? { + didSet { + + // Validation + guard let messageItem = item else { + + // Reset To Nil + descTitle.text = nil + + return + } + + switch (messageItem.messageType) { + + case .html: + + let htmlAttributedString = messageItem.text?.html2AttributedString(usingFont: AppStyle.Font.SemiBold(size: 14.0), color: AppStyle.Color.SecondaryText)?.trailingNewlineChopped + + if let attributedMessage = htmlAttributedString { + + // Set Description Message + descTitle.attributedText = attributedMessage + } + + case .text: + + // Set Description Message + descTitle.text = messageItem.text + } + + } + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func updateAppearance() { + super.updateAppearance() + + // Set Background Color + backgroundColor = AppStyle.Color.Background + + // Set Description Label + descTitle.base_regular() + descTitle.textColor = AppStyle.Color.MainTextPrimary + + } + +} diff --git a/BulletinSDK/View/Message/MessageViewCell.xib b/BulletinSDK/View/Message/MessageViewCell.xib new file mode 100644 index 0000000..3c878cc --- /dev/null +++ b/BulletinSDK/View/Message/MessageViewCell.xib @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/BulletinSDK/View/Title/TitleViewCell.swift b/BulletinSDK/View/Title/TitleViewCell.swift new file mode 100644 index 0000000..4af4f1e --- /dev/null +++ b/BulletinSDK/View/Title/TitleViewCell.swift @@ -0,0 +1,84 @@ +// +// TitleViewCell.swift +// Bulletin +// +// Created by Daxesh Nagar on 20/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + +protocol TitleViewCellDelegate: AnyObject { + func titleViewCell(_ cell: TitleViewCell, ofItem item: BulletinItem) +} + +class TitleViewCell: BaseCollectionViewCell { + + // MARK: - Variables + @IBOutlet private var actionContainerStackView: UIStackView! + @IBOutlet private var preTitle: UILabel! + @IBOutlet private var Title: UILabel! + @IBOutlet private var subTitle: UILabel! + + public weak var delegate: TitleViewCellDelegate? + + + public var item: Title? { + didSet { + + // Validation + guard let titleItem = item else { + + // Reset To Nil + preTitle.text = nil + Title.text = nil + subTitle.text = nil + + return + } + + // Set Version Number + preTitle.text = titleItem.preTitleText + + // Set Version Title + Title.text = titleItem.titleText + + // Set Version SubTitle + subTitle.text = titleItem.subTitleText + + } + } + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + } + + override func loadVariables() { + super.loadVariables() + + } + + override func updateAppearance() { + super.updateAppearance() + + // Set Background Color + backgroundColor = AppStyle.Color.Background + + + // Set preTitle Label + preTitle.small_medium() + preTitle.textColor = AppStyle.Color.SuccessTextPrimary + + // Set Title Label + Title.heading4_semibold() + Title.textColor = AppStyle.Color.MainTextPrimary + + // Set subTitle Label + subTitle.base_regular() + subTitle.textColor = AppStyle.Color.MainTextPrimary + + } + +} + diff --git a/BulletinSDK/View/Title/TitleViewCell.xib b/BulletinSDK/View/Title/TitleViewCell.xib new file mode 100644 index 0000000..3f13804 --- /dev/null +++ b/BulletinSDK/View/Title/TitleViewCell.xib @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Bulletin.xcodeproj/project.pbxproj b/Demo/Bulletin.xcodeproj/project.pbxproj index deffa5f..632eeaf 100644 --- a/Demo/Bulletin.xcodeproj/project.pbxproj +++ b/Demo/Bulletin.xcodeproj/project.pbxproj @@ -7,6 +7,38 @@ objects = { /* Begin PBXBuildFile section */ + 7315D8492A696EFC00B7CE4F /* BulletPointViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7315D8472A696EFC00B7CE4F /* BulletPointViewCell.swift */; }; + 7315D84A2A696EFC00B7CE4F /* BulletPointViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7315D8482A696EFC00B7CE4F /* BulletPointViewCell.xib */; }; + 7315D84D2A696F4300B7CE4F /* ActionButtonViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7315D84B2A696F4300B7CE4F /* ActionButtonViewCell.swift */; }; + 7315D84E2A696F4300B7CE4F /* ActionButtonViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7315D84C2A696F4300B7CE4F /* ActionButtonViewCell.xib */; }; + 7315D8512A696FA100B7CE4F /* MessageViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7315D84F2A696FA100B7CE4F /* MessageViewCell.swift */; }; + 7315D8522A696FA100B7CE4F /* MessageViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7315D8502A696FA100B7CE4F /* MessageViewCell.xib */; }; + 7315D8552A696FE400B7CE4F /* MediaViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7315D8532A696FE400B7CE4F /* MediaViewCell.swift */; }; + 7315D8562A696FE400B7CE4F /* MediaViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7315D8542A696FE400B7CE4F /* MediaViewCell.xib */; }; + 7315D8592A69703700B7CE4F /* TitleViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7315D8572A69703700B7CE4F /* TitleViewCell.swift */; }; + 7315D85A2A69703700B7CE4F /* TitleViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7315D8582A69703700B7CE4F /* TitleViewCell.xib */; }; + 732D1C2E2A66BAEA00FB5BFE /* LocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 732D1C2D2A66BAEA00FB5BFE /* LocalizationHelper.swift */; }; + 732D1C312A66C6F500FB5BFE /* UILabel+FontStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 732D1C302A66C6F500FB5BFE /* UILabel+FontStyle.swift */; }; + 738B01EC2A67BCEE00CFDB09 /* BulletinListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B01EB2A67BCEE00CFDB09 /* BulletinListView.swift */; }; + 738B01EE2A67BCFB00CFDB09 /* BulletinListView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 738B01ED2A67BCFB00CFDB09 /* BulletinListView.xib */; }; + 738B01F02A6819C900CFDB09 /* BulletinListSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B01EF2A6819C900CFDB09 /* BulletinListSectionController.swift */; }; + 73B17D602A6649240085EB1A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B17D5D2A6649240085EB1A /* Constants.swift */; }; + 73B17D612A6649240085EB1A /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B17D5E2A6649240085EB1A /* Text.swift */; }; + 73B17D622A6649240085EB1A /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B17D5F2A6649240085EB1A /* Appearance.swift */; }; + 73CC10192A650D1600BB08C5 /* BaseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73CC10182A650D1600BB08C5 /* BaseCollectionViewCell.swift */; }; + 73E824F92A6A4F72007AA8D7 /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E824F82A6A4F72007AA8D7 /* String+HTML.swift */; }; + 73E824FB2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E824FA2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift */; }; + 73E824FF2A6A67F5007AA8D7 /* PushButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E824FE2A6A67F5007AA8D7 /* PushButton.swift */; }; + 73F62F782A6E2CF0007CBC36 /* BottomSheetSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F62F772A6E2CF0007CBC36 /* BottomSheetSegue.swift */; }; + 73F62F7A2A6E2D0E007CBC36 /* AlertSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F62F792A6E2D0E007CBC36 /* AlertSegue.swift */; }; + 73F62F7C2A6E4D79007CBC36 /* BulletinHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F62F7B2A6E4D79007CBC36 /* BulletinHelper.swift */; }; + 73F62F7E2A6E83AF007CBC36 /* BulletinSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F62F7D2A6E83AF007CBC36 /* BulletinSection.swift */; }; + 73F62F882A6E90B1007CBC36 /* IBMPlexSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73F62F822A6E90B1007CBC36 /* IBMPlexSans-Regular.ttf */; }; + 73F62F892A6E90B1007CBC36 /* IBMPlexSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73F62F832A6E90B1007CBC36 /* IBMPlexSans-SemiBold.ttf */; }; + 73F62F8A2A6E90B1007CBC36 /* IBMPlexSans-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73F62F842A6E90B1007CBC36 /* IBMPlexSans-SemiBoldItalic.ttf */; }; + 73F62F8B2A6E90B1007CBC36 /* IBMPlexSans-Medium.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73F62F852A6E90B1007CBC36 /* IBMPlexSans-Medium.ttf */; }; + 73F62F8C2A6E90B1007CBC36 /* IBMPlexSans-Bold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73F62F862A6E90B1007CBC36 /* IBMPlexSans-Bold.ttf */; }; + 73F62F8D2A6E90B1007CBC36 /* IBMPlexSans-BoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73F62F872A6E90B1007CBC36 /* IBMPlexSans-BoldItalic.ttf */; }; 815E3D7928DC85510042CEA5 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3D7828DC85510042CEA5 /* AppDelegate.swift */; }; 815E3D7B28DC85510042CEA5 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3D7A28DC85510042CEA5 /* SceneDelegate.swift */; }; 815E3D7D28DC85510042CEA5 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 815E3D7C28DC85510042CEA5 /* ViewController.swift */; }; @@ -26,9 +58,43 @@ 819772DE28E40B8100962913 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819772DD28E40B8100962913 /* Version.swift */; }; 819772E028E40C4800962913 /* String+Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = 819772DF28E40C4800962913 /* String+Version.swift */; }; 81E010E228DDA2D9003FFE79 /* UserDefaults+Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81E010E128DDA2D9003FFE79 /* UserDefaults+Storage.swift */; }; + 84A09C653687F3E6C276EB35 /* Pods_Bulletin.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C06C8F2A28D17F9F6D3D37D2 /* Pods_Bulletin.framework */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 3D5C61AE690BD66E176DA0F5 /* Pods-Bulletin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Bulletin.release.xcconfig"; path = "Target Support Files/Pods-Bulletin/Pods-Bulletin.release.xcconfig"; sourceTree = ""; }; + 7315D8472A696EFC00B7CE4F /* BulletPointViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletPointViewCell.swift; sourceTree = ""; }; + 7315D8482A696EFC00B7CE4F /* BulletPointViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BulletPointViewCell.xib; sourceTree = ""; }; + 7315D84B2A696F4300B7CE4F /* ActionButtonViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionButtonViewCell.swift; sourceTree = ""; }; + 7315D84C2A696F4300B7CE4F /* ActionButtonViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ActionButtonViewCell.xib; sourceTree = ""; }; + 7315D84F2A696FA100B7CE4F /* MessageViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageViewCell.swift; sourceTree = ""; }; + 7315D8502A696FA100B7CE4F /* MessageViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MessageViewCell.xib; sourceTree = ""; }; + 7315D8532A696FE400B7CE4F /* MediaViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaViewCell.swift; sourceTree = ""; }; + 7315D8542A696FE400B7CE4F /* MediaViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MediaViewCell.xib; sourceTree = ""; }; + 7315D8572A69703700B7CE4F /* TitleViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleViewCell.swift; sourceTree = ""; }; + 7315D8582A69703700B7CE4F /* TitleViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TitleViewCell.xib; sourceTree = ""; }; + 732D1C2D2A66BAEA00FB5BFE /* LocalizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationHelper.swift; sourceTree = ""; }; + 732D1C302A66C6F500FB5BFE /* UILabel+FontStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+FontStyle.swift"; sourceTree = ""; }; + 738B01EB2A67BCEE00CFDB09 /* BulletinListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinListView.swift; sourceTree = ""; }; + 738B01ED2A67BCFB00CFDB09 /* BulletinListView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BulletinListView.xib; sourceTree = ""; }; + 738B01EF2A6819C900CFDB09 /* BulletinListSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinListSectionController.swift; sourceTree = ""; }; + 73B17D5D2A6649240085EB1A /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + 73B17D5E2A6649240085EB1A /* Text.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; + 73B17D5F2A6649240085EB1A /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; + 73CC10182A650D1600BB08C5 /* BaseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCollectionViewCell.swift; sourceTree = ""; }; + 73E824F82A6A4F72007AA8D7 /* String+HTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = ""; }; + 73E824FA2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Utility.swift"; sourceTree = ""; }; + 73E824FE2A6A67F5007AA8D7 /* PushButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushButton.swift; sourceTree = ""; }; + 73F62F772A6E2CF0007CBC36 /* BottomSheetSegue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetSegue.swift; sourceTree = ""; }; + 73F62F792A6E2D0E007CBC36 /* AlertSegue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertSegue.swift; sourceTree = ""; }; + 73F62F7B2A6E4D79007CBC36 /* BulletinHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinHelper.swift; sourceTree = ""; }; + 73F62F7D2A6E83AF007CBC36 /* BulletinSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinSection.swift; sourceTree = ""; }; + 73F62F822A6E90B1007CBC36 /* IBMPlexSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSans-Regular.ttf"; sourceTree = ""; }; + 73F62F832A6E90B1007CBC36 /* IBMPlexSans-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSans-SemiBold.ttf"; sourceTree = ""; }; + 73F62F842A6E90B1007CBC36 /* IBMPlexSans-SemiBoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSans-SemiBoldItalic.ttf"; sourceTree = ""; }; + 73F62F852A6E90B1007CBC36 /* IBMPlexSans-Medium.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSans-Medium.ttf"; sourceTree = ""; }; + 73F62F862A6E90B1007CBC36 /* IBMPlexSans-Bold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSans-Bold.ttf"; sourceTree = ""; }; + 73F62F872A6E90B1007CBC36 /* IBMPlexSans-BoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSans-BoldItalic.ttf"; sourceTree = ""; }; 815E3D7528DC85510042CEA5 /* Bulletin.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Bulletin.app; sourceTree = BUILT_PRODUCTS_DIR; }; 815E3D7828DC85510042CEA5 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 815E3D7A28DC85510042CEA5 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; @@ -50,6 +116,8 @@ 819772DD28E40B8100962913 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; 819772DF28E40C4800962913 /* String+Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Version.swift"; sourceTree = ""; }; 81E010E128DDA2D9003FFE79 /* UserDefaults+Storage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Storage.swift"; sourceTree = ""; }; + C06C8F2A28D17F9F6D3D37D2 /* Pods_Bulletin.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Bulletin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + F5FFA406F95FB2E07D3D917C /* Pods-Bulletin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Bulletin.debug.xcconfig"; path = "Target Support Files/Pods-Bulletin/Pods-Bulletin.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -57,18 +125,217 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 84A09C653687F3E6C276EB35 /* Pods_Bulletin.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4BE0D836F189FD1FC7E5D668 /* Pods */ = { + isa = PBXGroup; + children = ( + F5FFA406F95FB2E07D3D917C /* Pods-Bulletin.debug.xcconfig */, + 3D5C61AE690BD66E176DA0F5 /* Pods-Bulletin.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 6BB9BBCB1B81AA5BB2878A15 /* Frameworks */ = { + isa = PBXGroup; + children = ( + C06C8F2A28D17F9F6D3D37D2 /* Pods_Bulletin.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 7315D82F2A68E7F100B7CE4F /* Media */ = { + isa = PBXGroup; + children = ( + 7315D8532A696FE400B7CE4F /* MediaViewCell.swift */, + 7315D8542A696FE400B7CE4F /* MediaViewCell.xib */, + ); + path = Media; + sourceTree = ""; + }; + 7315D8342A68E81D00B7CE4F /* Message */ = { + isa = PBXGroup; + children = ( + 7315D84F2A696FA100B7CE4F /* MessageViewCell.swift */, + 7315D8502A696FA100B7CE4F /* MessageViewCell.xib */, + ); + path = Message; + sourceTree = ""; + }; + 7315D83D2A68E88A00B7CE4F /* ActionButton */ = { + isa = PBXGroup; + children = ( + 7315D84B2A696F4300B7CE4F /* ActionButtonViewCell.swift */, + 7315D84C2A696F4300B7CE4F /* ActionButtonViewCell.xib */, + ); + path = ActionButton; + sourceTree = ""; + }; + 7315D8422A68E8A800B7CE4F /* BulletPoint */ = { + isa = PBXGroup; + children = ( + 7315D8472A696EFC00B7CE4F /* BulletPointViewCell.swift */, + 7315D8482A696EFC00B7CE4F /* BulletPointViewCell.xib */, + ); + path = BulletPoint; + sourceTree = ""; + }; + 732D1C2C2A66BA9200FB5BFE /* Helper */ = { + isa = PBXGroup; + children = ( + 732D1C2D2A66BAEA00FB5BFE /* LocalizationHelper.swift */, + 73F62F7B2A6E4D79007CBC36 /* BulletinHelper.swift */, + ); + path = Helper; + sourceTree = ""; + }; + 732D1C2F2A66C67600FB5BFE /* Categories */ = { + isa = PBXGroup; + children = ( + 732D1C302A66C6F500FB5BFE /* UILabel+FontStyle.swift */, + 73E824FA2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift */, + 73E824F82A6A4F72007AA8D7 /* String+HTML.swift */, + ); + path = Categories; + sourceTree = ""; + }; + 732FF0E32A67A2F800904913 /* Controllers */ = { + isa = PBXGroup; + children = ( + 738B01EB2A67BCEE00CFDB09 /* BulletinListView.swift */, + 738B01ED2A67BCFB00CFDB09 /* BulletinListView.xib */, + 738B01EF2A6819C900CFDB09 /* BulletinListSectionController.swift */, + 73F62F7D2A6E83AF007CBC36 /* BulletinSection.swift */, + ); + path = Controllers; + sourceTree = ""; + }; + 73B17D5C2A6648C90085EB1A /* Constants */ = { + isa = PBXGroup; + children = ( + 73B17D5F2A6649240085EB1A /* Appearance.swift */, + 73B17D5D2A6649240085EB1A /* Constants.swift */, + 73B17D5E2A6649240085EB1A /* Text.swift */, + ); + path = Constants; + sourceTree = ""; + }; + 73CC100D2A65009000BB08C5 /* View */ = { + isa = PBXGroup; + children = ( + 7315D8422A68E8A800B7CE4F /* BulletPoint */, + 7315D83D2A68E88A00B7CE4F /* ActionButton */, + 7315D8342A68E81D00B7CE4F /* Message */, + 7315D82F2A68E7F100B7CE4F /* Media */, + 73CC100E2A6509B800BB08C5 /* Title */, + ); + path = View; + sourceTree = ""; + }; + 73CC100E2A6509B800BB08C5 /* Title */ = { + isa = PBXGroup; + children = ( + 7315D8572A69703700B7CE4F /* TitleViewCell.swift */, + 7315D8582A69703700B7CE4F /* TitleViewCell.xib */, + ); + path = Title; + sourceTree = ""; + }; + 73CC10152A650CD800BB08C5 /* Classes */ = { + isa = PBXGroup; + children = ( + 732D1C2F2A66C67600FB5BFE /* Categories */, + 73CC10162A650CEE00BB08C5 /* Base */, + 73B17D5C2A6648C90085EB1A /* Constants */, + 732D1C2C2A66BA9200FB5BFE /* Helper */, + ); + path = Classes; + sourceTree = ""; + }; + 73CC10162A650CEE00BB08C5 /* Base */ = { + isa = PBXGroup; + children = ( + 73CC10172A650CF800BB08C5 /* View */, + ); + path = Base; + sourceTree = ""; + }; + 73CC10172A650CF800BB08C5 /* View */ = { + isa = PBXGroup; + children = ( + 73CC10182A650D1600BB08C5 /* BaseCollectionViewCell.swift */, + ); + path = View; + sourceTree = ""; + }; + 73E824FC2A6A6545007AA8D7 /* Libraries */ = { + isa = PBXGroup; + children = ( + 73F62F762A6E2CBC007CBC36 /* PopUp */, + 73E824FD2A6A67D4007AA8D7 /* PushButton */, + ); + path = Libraries; + sourceTree = ""; + }; + 73E824FD2A6A67D4007AA8D7 /* PushButton */ = { + isa = PBXGroup; + children = ( + 73E824FE2A6A67F5007AA8D7 /* PushButton.swift */, + ); + path = PushButton; + sourceTree = ""; + }; + 73F62F762A6E2CBC007CBC36 /* PopUp */ = { + isa = PBXGroup; + children = ( + 73F62F772A6E2CF0007CBC36 /* BottomSheetSegue.swift */, + 73F62F792A6E2D0E007CBC36 /* AlertSegue.swift */, + ); + path = PopUp; + sourceTree = ""; + }; + 73F62F7F2A6E9066007CBC36 /* Resources */ = { + isa = PBXGroup; + children = ( + 73F62F802A6E9077007CBC36 /* Fonts */, + ); + path = Resources; + sourceTree = ""; + }; + 73F62F802A6E9077007CBC36 /* Fonts */ = { + isa = PBXGroup; + children = ( + 73F62F812A6E9082007CBC36 /* IBMPlexSans */, + ); + path = Fonts; + sourceTree = ""; + }; + 73F62F812A6E9082007CBC36 /* IBMPlexSans */ = { + isa = PBXGroup; + children = ( + 73F62F862A6E90B1007CBC36 /* IBMPlexSans-Bold.ttf */, + 73F62F872A6E90B1007CBC36 /* IBMPlexSans-BoldItalic.ttf */, + 73F62F852A6E90B1007CBC36 /* IBMPlexSans-Medium.ttf */, + 73F62F822A6E90B1007CBC36 /* IBMPlexSans-Regular.ttf */, + 73F62F832A6E90B1007CBC36 /* IBMPlexSans-SemiBold.ttf */, + 73F62F842A6E90B1007CBC36 /* IBMPlexSans-SemiBoldItalic.ttf */, + ); + path = IBMPlexSans; + sourceTree = ""; + }; 815E3D6C28DC85510042CEA5 = { isa = PBXGroup; children = ( 815E3DA528DC884C0042CEA5 /* BulletinSDK */, 815E3D7728DC85510042CEA5 /* Bulletin */, 815E3D7628DC85510042CEA5 /* Products */, + 4BE0D836F189FD1FC7E5D668 /* Pods */, + 6BB9BBCB1B81AA5BB2878A15 /* Frameworks */, ); sourceTree = ""; }; @@ -83,6 +350,7 @@ 815E3D7728DC85510042CEA5 /* Bulletin */ = { isa = PBXGroup; children = ( + 732FF0E32A67A2F800904913 /* Controllers */, 815E3D7828DC85510042CEA5 /* AppDelegate.swift */, 815E3D7A28DC85510042CEA5 /* SceneDelegate.swift */, 815E3D7C28DC85510042CEA5 /* ViewController.swift */, @@ -97,8 +365,12 @@ 815E3DA528DC884C0042CEA5 /* BulletinSDK */ = { isa = PBXGroup; children = ( + 73CC10152A650CD800BB08C5 /* Classes */, 815E3DA928DC884C0042CEA5 /* Modals */, + 73CC100D2A65009000BB08C5 /* View */, 815E3DA628DC884C0042CEA5 /* Extensions */, + 73E824FC2A6A6545007AA8D7 /* Libraries */, + 73F62F7F2A6E9066007CBC36 /* Resources */, 815E3DB128DC884C0042CEA5 /* BulletinSDK.swift */, 815E3DA828DC884C0042CEA5 /* BulletinDataStore.swift */, ); @@ -146,9 +418,11 @@ isa = PBXNativeTarget; buildConfigurationList = 815E3D8928DC85510042CEA5 /* Build configuration list for PBXNativeTarget "Bulletin" */; buildPhases = ( + 3204137CC314034C6270CB20 /* [CP] Check Pods Manifest.lock */, 815E3D7128DC85510042CEA5 /* Sources */, 815E3D7228DC85510042CEA5 /* Frameworks */, 815E3D7328DC85510042CEA5 /* Resources */, + 21CE3D15751E0A8B38141E27 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -199,34 +473,108 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 73F62F892A6E90B1007CBC36 /* IBMPlexSans-SemiBold.ttf in Resources */, + 7315D8522A696FA100B7CE4F /* MessageViewCell.xib in Resources */, + 73F62F8C2A6E90B1007CBC36 /* IBMPlexSans-Bold.ttf in Resources */, + 73F62F8A2A6E90B1007CBC36 /* IBMPlexSans-SemiBoldItalic.ttf in Resources */, + 7315D85A2A69703700B7CE4F /* TitleViewCell.xib in Resources */, + 73F62F882A6E90B1007CBC36 /* IBMPlexSans-Regular.ttf in Resources */, + 7315D8562A696FE400B7CE4F /* MediaViewCell.xib in Resources */, + 73F62F8B2A6E90B1007CBC36 /* IBMPlexSans-Medium.ttf in Resources */, + 738B01EE2A67BCFB00CFDB09 /* BulletinListView.xib in Resources */, 815E3D8528DC85510042CEA5 /* LaunchScreen.storyboard in Resources */, 815E3D8228DC85510042CEA5 /* Assets.xcassets in Resources */, 815E3D8028DC85510042CEA5 /* Main.storyboard in Resources */, + 73F62F8D2A6E90B1007CBC36 /* IBMPlexSans-BoldItalic.ttf in Resources */, + 7315D84E2A696F4300B7CE4F /* ActionButtonViewCell.xib in Resources */, + 7315D84A2A696EFC00B7CE4F /* BulletPointViewCell.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ +/* Begin PBXShellScriptBuildPhase section */ + 21CE3D15751E0A8B38141E27 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3204137CC314034C6270CB20 /* [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-Bulletin-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 */ 815E3D7128DC85510042CEA5 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 732D1C2E2A66BAEA00FB5BFE /* LocalizationHelper.swift in Sources */, + 7315D8552A696FE400B7CE4F /* MediaViewCell.swift in Sources */, 815E3DB928DC884C0042CEA5 /* Media.swift in Sources */, + 73B17D602A6649240085EB1A /* Constants.swift in Sources */, + 73F62F782A6E2CF0007CBC36 /* BottomSheetSegue.swift in Sources */, + 7315D8492A696EFC00B7CE4F /* BulletPointViewCell.swift in Sources */, 815E3DB828DC884C0042CEA5 /* Title.swift in Sources */, + 732D1C312A66C6F500FB5BFE /* UILabel+FontStyle.swift in Sources */, + 73E824FF2A6A67F5007AA8D7 /* PushButton.swift in Sources */, + 73B17D622A6649240085EB1A /* Appearance.swift in Sources */, + 738B01EC2A67BCEE00CFDB09 /* BulletinListView.swift in Sources */, 815E3D7D28DC85510042CEA5 /* ViewController.swift in Sources */, + 7315D8512A696FA100B7CE4F /* MessageViewCell.swift in Sources */, + 73CC10192A650D1600BB08C5 /* BaseCollectionViewCell.swift in Sources */, + 73B17D612A6649240085EB1A /* Text.swift in Sources */, + 7315D84D2A696F4300B7CE4F /* ActionButtonViewCell.swift in Sources */, 815E3DB428DC884C0042CEA5 /* BulletinItem.swift in Sources */, 81E010E228DDA2D9003FFE79 /* UserDefaults+Storage.swift in Sources */, 815E3DB328DC884C0042CEA5 /* BulletinDataStore.swift in Sources */, 815E3DBB28DC884C0042CEA5 /* BulletinSDK.swift in Sources */, + 73F62F7E2A6E83AF007CBC36 /* BulletinSection.swift in Sources */, 819772DE28E40B8100962913 /* Version.swift in Sources */, + 738B01F02A6819C900CFDB09 /* BulletinListSectionController.swift in Sources */, + 7315D8592A69703700B7CE4F /* TitleViewCell.swift in Sources */, + 73E824F92A6A4F72007AA8D7 /* String+HTML.swift in Sources */, 815E3DB228DC884C0042CEA5 /* String+Class.swift in Sources */, 819772E028E40C4800962913 /* String+Version.swift in Sources */, 815E3DB728DC884C0042CEA5 /* BulletPoint.swift in Sources */, 819772DC28E2CBEF00962913 /* BulletinInfo.swift in Sources */, + 73F62F7A2A6E2D0E007CBC36 /* AlertSegue.swift in Sources */, 815E3DB628DC884C0042CEA5 /* Message.swift in Sources */, 815E3D7928DC85510042CEA5 /* AppDelegate.swift in Sources */, 815E3D7B28DC85510042CEA5 /* SceneDelegate.swift in Sources */, + 73F62F7C2A6E4D79007CBC36 /* BulletinHelper.swift in Sources */, + 73E824FB2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift in Sources */, 815E3DBA28DC884C0042CEA5 /* ActionButton.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -369,6 +717,7 @@ }; 815E3D8A28DC85510042CEA5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = F5FFA406F95FB2E07D3D917C /* Pods-Bulletin.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -382,6 +731,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -397,6 +747,7 @@ }; 815E3D8B28DC85510042CEA5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 3D5C61AE690BD66E176DA0F5 /* Pods-Bulletin.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; @@ -410,6 +761,7 @@ INFOPLIST_KEY_UIMainStoryboardFile = Main; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/Demo/Bulletin.xcodeproj/project.xcworkspace/xcuserdata/daxeshnagar.xcuserdatad/UserInterfaceState.xcuserstate b/Demo/Bulletin.xcodeproj/project.xcworkspace/xcuserdata/daxeshnagar.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..d758fbd8ab1e9b75e56968a8eec8d4b243d15295 GIT binary patch literal 51118 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 S2JY3T&wn}mzxe-*8~zvbhAs~P literal 0 HcmV?d00001 diff --git a/Demo/Bulletin.xcworkspace/contents.xcworkspacedata b/Demo/Bulletin.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21040ec --- /dev/null +++ b/Demo/Bulletin.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/Demo/Bulletin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/Demo/Bulletin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/Demo/Bulletin.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/Demo/Bulletin.xcworkspace/xcuserdata/daxeshnagar.xcuserdatad/UserInterfaceState.xcuserstate b/Demo/Bulletin.xcworkspace/xcuserdata/daxeshnagar.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..5ff5c643b7b01e64f27fe095b92913bfc28f0c74 GIT binary patch literal 168418 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 + + + + + + + + diff --git a/Demo/Bulletin/Base.lproj/Main.storyboard b/Demo/Bulletin/Base.lproj/Main.storyboard index 25a7638..07d81af 100644 --- a/Demo/Bulletin/Base.lproj/Main.storyboard +++ b/Demo/Bulletin/Base.lproj/Main.storyboard @@ -1,24 +1,42 @@ - + + - + + + - + - + - + + + + + + + + + + diff --git a/Demo/Bulletin/Controllers/BulletinListSectionController.swift b/Demo/Bulletin/Controllers/BulletinListSectionController.swift new file mode 100644 index 0000000..8b2d855 --- /dev/null +++ b/Demo/Bulletin/Controllers/BulletinListSectionController.swift @@ -0,0 +1,285 @@ +// +// BulletinListSectionController.swift +// Bulletin +// +// Created by Daxesh Nagar on 19/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import Foundation +import IGListKit +import UIKit + +internal protocol BulletinSectionControllerDelegate: AnyObject { + // func bulletinSectionController(_ sectionController: BulletinListSectionController, didClickItem item: WZSettingsSectionBaseItem, withEventType eventType: WZSettingsSectionItemEventType) +} + +class BulletinListSectionController: ListSectionController { + + // MARK: - Variable + private var bulletinListSection: BulletinInfo? + public weak var delegate: BulletinSectionControllerDelegate? + + private var bulletPointViewCell: BulletPointViewCell? + private var actionButtonViewCell: ActionButtonViewCell? + private var messageViewCell: MessageViewCell? + private var mediaViewCell: MediaViewCell? + private var titleViewCell: TitleViewCell? + + + // MARK: - Initialisation Methods + override init() { + super.init() + + // Init Sizing Cells + bulletPointViewCell = Bundle.main.loadNibNamed("BulletPointViewCell", owner: self, options: nil)?.first as? BulletPointViewCell + actionButtonViewCell = Bundle.main.loadNibNamed("ActionButtonViewCell", owner: self, options: nil)?.first as? ActionButtonViewCell + messageViewCell = Bundle.main.loadNibNamed("MessageViewCell", owner: self, options: nil)?.first as? MessageViewCell + mediaViewCell = Bundle.main.loadNibNamed("MediaViewCell", owner: self, options: nil)?.first as? MediaViewCell + titleViewCell = Bundle.main.loadNibNamed("TitleViewCell", owner: self, options: nil)?.first as? TitleViewCell + } + + + // MARK: - IGListSectionController + override func numberOfItems() -> Int { + return bulletinListSection?.items.count ?? 0 + } + + override func sizeForItem(at index: Int) -> CGSize { + + // Validation + guard let context = collectionContext, + let item = bulletinListSection?.items[index] else { return .zero } + + switch item.type { + + case .title: + + // Set Cell Item + titleViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) + titleViewCell?.item = item as? Title + + // Calculate Height + let height = max(titleViewCell?.intrinsicContentSize.height ?? 0 , 54) + + // Return Size + return CGSize(width: context.containerSize.width, height: height) + + case .message: + + // Set Cell Item + messageViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) + messageViewCell?.item = item as? Message + + // Calculate Height + let height = max(messageViewCell?.intrinsicContentSize.height ?? 0 , 54) + + // Return Size + return CGSize(width: context.containerSize.width, height: height) + + case .media: + + // Set Cell Item + mediaViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) + mediaViewCell?.item = item as? Media + + // Calculate Height + let height = max(mediaViewCell?.intrinsicContentSize.height ?? 0 , 54) + + // Return Size + return CGSize(width: context.containerSize.width, height: height) + + case .bulletPoint: + + // Set Cell Item + bulletPointViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) + bulletPointViewCell?.item = item as? BulletPoint + + // Calculate Height + let height = max(bulletPointViewCell?.intrinsicContentSize.height ?? 0 , 54) + + // Return Size + return CGSize(width: context.containerSize.width, height: height) + + case .actionButton: + + // Set Cell Item + actionButtonViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) + actionButtonViewCell?.item = item as? ActionButton + + // Calculate Height + let height = max(actionButtonViewCell?.intrinsicContentSize.height ?? 0 , 54) + + // Return Size + return CGSize(width: context.containerSize.width, height: height) + + case .undefined: + return CGSize(width: 0, height: 0) + } + } + + override func cellForItem(at index: Int) -> UICollectionViewCell { + + // Validation + guard let items = bulletinListSection?.items else { + fatalError() + } + + let item = items[index] + + switch item.type { + + case .title: + + // Get Cell + guard let cell = collectionContext?.dequeueReusableCell(withNibName: TitleViewCell.identifier(), + bundle: nil, for: self, at: index) as? TitleViewCell else + { fatalError() } + + // Item Validation + if let item = item as? Title { + + // Set Item + cell.item = item + } + + // Set Delegate + cell.delegate = self + + return cell + + case .message: + + // Get Cell + // Get Cell + guard let cell = collectionContext?.dequeueReusableCell(withNibName: MessageViewCell.identifier(), + bundle: nil, for: self, at: index) as? MessageViewCell else + { fatalError() } + + // Item Validation + if let item = item as? Message { + + // Set Item + cell.item = item + } + + // Set Delegate + cell.delegate = self + + return cell + + case .media: + + // Get Cell + guard let cell = collectionContext?.dequeueReusableCell(withNibName: MediaViewCell.identifier(), + bundle: nil, for: self, at: index) as? MediaViewCell else + { fatalError() } + + // Item Validation + if let item = item as? Media { + + // Set Item + cell.item = item + } + + // Set Delegate + cell.delegate = self + + return cell + + case .bulletPoint: + + // Get Cell + guard let cell = collectionContext?.dequeueReusableCell(withNibName: BulletPointViewCell.identifier(), + bundle: nil, for: self, at: index) as? BulletPointViewCell else + { fatalError() } + + // Item Validation + if let item = item as? BulletPoint { + + // Set Item + cell.item = item + } + + // Set Delegate + cell.delegate = self + + return cell + + case .actionButton: + + // Get Cell + guard let cell = collectionContext?.dequeueReusableCell(withNibName: ActionButtonViewCell.identifier(), + bundle: nil, for: self, at: index) as? ActionButtonViewCell else + { fatalError() } + + // Item Validation + if let item = item as? ActionButton { + + // Set Item + cell.item = item + } + + // Set Delegate + cell.delegate = self + + return cell + + case .undefined: + fatalError() + } + } + + override func didUpdate(to object: Any) { + bulletinListSection = object as? BulletinInfo + } +} + + + +// MARK: - BulletListTitleCellDelegate +extension BulletinListSectionController: TitleViewCellDelegate { + + func titleViewCell(_ cell: TitleViewCell, ofItem item: BulletinItem) { + + } + +} + +// MARK: - BulletListTitleCellDelegate +extension BulletinListSectionController: MediaViewCellDelegate { + + func mediaViewCell(_ cell: MediaViewCell, ofItem item: BulletinItem) { + + } + +} + + +// MARK: - BulletListTitleCellDelegate +extension BulletinListSectionController: MessageViewCellDelegate { + + func messageViewCell(_ cell: MessageViewCell, ofItem item: BulletinItem) { + + } + +} + +// MARK: - BulletListTitleCellDelegate +extension BulletinListSectionController: ActionButtonViewCellDelegate { + + func actionButtonViewCell(_ cell: ActionButtonViewCell, ofItem item: BulletinItem) { + + } + +} + +// MARK: - BulletListTitleCellDelegate +extension BulletinListSectionController: BulletPointViewCellDelegate { + + func bulletPointViewCell(_ cell: BulletPointViewCell, ofItem item: BulletinItem) { + + } + +} + diff --git a/Demo/Bulletin/Controllers/BulletinListView.swift b/Demo/Bulletin/Controllers/BulletinListView.swift new file mode 100644 index 0000000..80e7f20 --- /dev/null +++ b/Demo/Bulletin/Controllers/BulletinListView.swift @@ -0,0 +1,109 @@ +// +// BulletinListView.swift +// Bulletin +// +// Created by Daxesh Nagar on 19/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit +import IGListKit + +class BulletinListView: UIView, BulletinSectionControllerDelegate { + + // MARK: - Variables + @IBOutlet private var collectionView : UICollectionView! + + private var bulletinSection = BulletinSection() { + didSet { + // Update Collection View + DispatchQueue.main.async { [weak self] in + // Perform Updates + self?.adapter.performUpdates(animated: true, completion: nil) + } + } + } + + internal lazy var adapter: ListAdapter = { + return ListAdapter(updater: ListAdapterUpdater(), viewController: nil, workingRangeSize: 0) + }() + + + // MARK: - Initialisation Methods + public class func instance(bulletinSection: BulletinSection) -> BulletinListView { + + // Create Instance From XIB + let className = String(describing: BulletinListView.self) + let arrayOfViews = Bundle.main.loadNibNamed(className, owner: self, options: nil) + if let view = arrayOfViews?.first as? BulletinListView { + + // Set View Flexibility + view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + view.translatesAutoresizingMaskIntoConstraints = true + view.bulletinSection = bulletinSection + + return view + } + + // Manually Create Instance + let bulletinListView = BulletinListView() + bulletinListView.bulletinSection = bulletinSection + return bulletinListView + } + + // MARK: - View Lifecycle Methods + func loadVariables() { + + // Configure Adapter Properties + adapter.collectionView = collectionView + adapter.dataSource = self + adapter.collectionViewDelegate = self + } + + // MARK: - Initialisation Methods + override func awakeFromNib() { + super.awakeFromNib() + + // Add Corner Curve + if #available(iOS 13.0, *) { + layer.cornerCurve = .continuous + } + + // Load Variables + loadVariables() + + } + + // MARK: - Helper Method + internal func performUpdates(animated: Bool, completion: ListUpdaterCompletion? = nil) { + + DispatchQueue.main.async { [weak self] in + // Perform Updates + self?.adapter.performUpdates(animated: animated, completion: completion) + } + } + +} + +// MARK: - ListAdapterDataSource +extension BulletinListView: ListAdapterDataSource { + + func objects(for listAdapter: ListAdapter) -> [ListDiffable] { + var items = [ListDiffable]() + items += [bulletinSection] as [ListDiffable] + return items + } + + func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { + let sectionController = BulletinListSectionController() + sectionController.delegate = self + return sectionController + } + + func emptyView(for listAdapter: ListAdapter) -> UIView? { return nil } +} + +// MARK: - UICollectionViewDelegate +extension BulletinListView: UICollectionViewDelegate { + public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {} +} diff --git a/Demo/Bulletin/Controllers/BulletinListView.xib b/Demo/Bulletin/Controllers/BulletinListView.xib new file mode 100644 index 0000000..c765b4b --- /dev/null +++ b/Demo/Bulletin/Controllers/BulletinListView.xib @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Bulletin/Controllers/BulletinSection.swift b/Demo/Bulletin/Controllers/BulletinSection.swift new file mode 100644 index 0000000..2fef8b7 --- /dev/null +++ b/Demo/Bulletin/Controllers/BulletinSection.swift @@ -0,0 +1,29 @@ +// +// BulletinSection.swift +// Bulletin +// +// Created by Daxesh Nagar on 24/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit +import IGListKit + + +class BulletinSection : NSObject { + + + // MARK: - Variables + public var bulletinInfo = [BulletinInfo]() + +} + +extension BulletinSection : ListDiffable { + public func diffIdentifier() -> NSObjectProtocol { + return self + } + + public func isEqual(toDiffableObject object: ListDiffable?) -> Bool { + return isEqual(object) + } +} diff --git a/Demo/Bulletin/Info.plist b/Demo/Bulletin/Info.plist index dd3c9af..3d9fcc0 100644 --- a/Demo/Bulletin/Info.plist +++ b/Demo/Bulletin/Info.plist @@ -21,5 +21,14 @@ + UIAppFonts + + IBMPlexSans-Bold.ttf + IBMPlexSans-BoldItalic.ttf + IBMPlexSans-Medium.ttf + IBMPlexSans-Regular.ttf + IBMPlexSans-SemiBold.ttf + IBMPlexSans-SemiBoldItalic.ttf + diff --git a/Demo/Bulletin/ViewController.swift b/Demo/Bulletin/ViewController.swift index 08e48cf..4fc58fd 100644 --- a/Demo/Bulletin/ViewController.swift +++ b/Demo/Bulletin/ViewController.swift @@ -24,11 +24,32 @@ class ViewController: UIViewController { registerBulletingDetails(forVersion: Version("1.13")!, inDataStore: dataSource) registerBulletingDetails(forVersion: Version("1.13.1")!, inDataStore: dataSource) registerBulletingDetails(forVersion: Version("1.14")!, inDataStore: dataSource) + let sdk = BulletinSDK(dataStore: dataSource) - let _ = sdk.showFullBulletin() - let _ = sdk.showLastBulletins(limit: 5) - let _ = sdk.showUnseenBulletins(limit: 4) + let bulletinView = sdk.getFullBulletin() + let _ = sdk.getLastBulletins(limit: 5) + let _ = sdk.getUnseenBulletins(limit: 4) + + if let bulletinView = bulletinView { + bulletinView.backgroundColor = UIColor.red + self.view.addSubview(bulletinView) + } + + // Get Top Most View Controller +// BulletinHelper.topMostViewController { (topMostVC) in +// +// // Validation +// guard let topMostVC = topMostVC else { +// return +// } +// +// // Display Self Declaration PopUp VC +// let segue = AlertSegue(identifier: nil, source: topMostVC, destination: bulletinView) +// topMostVC.prepare(for: segue, sender: nil) +// segue.perform() +// +// } } private func registerBulletingDetails(forVersion version: Version, inDataStore dataStore: BulletinDataStore) { diff --git a/Demo/Podfile b/Demo/Podfile new file mode 100644 index 0000000..05ac606 --- /dev/null +++ b/Demo/Podfile @@ -0,0 +1,13 @@ +# Uncomment the next line to define a global platform for your project +# platform :ios, '9.0' + +target 'Bulletin' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + + # Pods for Bulletin + pod 'UIColor_Hex_Swift' + pod 'IGListKit' + pod 'Kingfisher','~> 7.6.2' + pod 'SwiftMessages' +end diff --git a/Demo/Podfile.lock b/Demo/Podfile.lock new file mode 100644 index 0000000..8a29c14 --- /dev/null +++ b/Demo/Podfile.lock @@ -0,0 +1,34 @@ +PODS: + - IGListDiffKit (4.0.0) + - IGListKit (4.0.0): + - IGListDiffKit (= 4.0.0) + - Kingfisher (7.6.2) + - SwiftMessages (9.0.6): + - SwiftMessages/App (= 9.0.6) + - SwiftMessages/App (9.0.6) + - UIColor_Hex_Swift (5.1.7) + +DEPENDENCIES: + - IGListKit + - Kingfisher (~> 7.6.2) + - SwiftMessages + - UIColor_Hex_Swift + +SPEC REPOS: + trunk: + - IGListDiffKit + - IGListKit + - Kingfisher + - SwiftMessages + - UIColor_Hex_Swift + +SPEC CHECKSUMS: + IGListDiffKit: 665d6cf43ce726e676013db9c7d6c4294259b6b2 + IGListKit: fd5a5d21935298f5849fa49d426843cff97b77c7 + Kingfisher: 6c5449c6450c5239166510ba04afe374a98afc4f + SwiftMessages: f0c7ef4705a570ad6c5e208b611f4333e660ed92 + UIColor_Hex_Swift: 31cd3e47440f07a20d2503a36bb0437a445b3c29 + +PODFILE CHECKSUM: 310645f0fa108e1099503961f2d58481962552ed + +COCOAPODS: 1.12.1 diff --git a/Demo/Pods/IGListDiffKit/LICENSE.md b/Demo/Pods/IGListDiffKit/LICENSE.md new file mode 100755 index 0000000..87cbf53 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +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. \ No newline at end of file diff --git a/Demo/Pods/IGListDiffKit/README.md b/Demo/Pods/IGListDiffKit/README.md new file mode 100644 index 0000000..22981eb --- /dev/null +++ b/Demo/Pods/IGListDiffKit/README.md @@ -0,0 +1,110 @@ +

+ +

+ +

+ + Build Status + + + Coverage Status + + + Pods Version + + + Platforms + + + Carthage Compatible + +

+ +---------------- + +A data-driven `UICollectionView` framework for building fast and flexible lists. + +| | Main Features | +----------|----------------- +🙅 | Never call `performBatchUpdates(_:, completion:)` or `reloadData()` again +🏠 | Better architecture with reusable cells and components +🔠 | Create collections with multiple data types +🔑 | Decoupled diffing algorithm +✅ | Fully unit tested +🔍 | Customize your diffing behavior for your models +📱 | Simply `UICollectionView` at its core +🚀 | Extendable API +🐦 | Written in Objective-C with full Swift interop support + +`IGListKit` is built and maintained with ❤️ by [Instagram engineering](https://engineering.instagram.com/). +We use the open source version `master` branch in the Instagram app. + +## Requirements + +- Xcode 9.0+ +- iOS 9.0+ +- tvOS 9.0+ +- macOS 10.11+ *(diffing algorithm components only)* +- Interoperability with Swift 3.0+ + +## Installation + +### CocoaPods + +The preferred installation method is with [CocoaPods](https://cocoapods.org). Add the following to your `Podfile`: + +```ruby +pod 'IGListKit', '~> 4.0.0' +``` + +### Carthage + +For [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`: + +```ogdl +github "Instagram/IGListKit" ~> 4.0.0 +``` + +> For advanced usage, see our [Installation Guide](https://instagram.github.io/IGListKit/installation.html). + +## Getting Started + +```bash +$ git clone https://github.com/Instagram/IGListKit.git +$ cd IGListKit/ +$ ./scripts/setup.sh +``` + +- Our [Getting Started guide](https://instagram.github.io/IGListKit/getting-started.html) +- Ray Wenderlich's [IGListKit Tutorial: Better UICollectionViews](https://www.raywenderlich.com/147162/iglistkit-tutorial-better-uicollectionviews) +- Our [example projects](https://github.com/Instagram/IGListKit/tree/master/Examples) +- Ryan Nystrom's [talk at try! Swift NYC](https://realm.io/news/tryswift-ryan-nystrom-refactoring-at-scale-lessons-learned-rewriting-instagram-feed/) (Note: this talk was for an earlier version. Some APIs have changed.) +- [Migrating an UITableView to IGListCollectionView](https://medium.com/cocoaacademymag/iglistkit-migrating-an-uitableview-to-iglistkitcollectionview-65a30cf9bac9), by Rodrigo Cavalcante +- [Keeping data fresh in Buffer for iOS with AsyncDisplayKit, IGListKit & Pusher](https://overflow.buffer.com/2017/04/10/keeping-data-fresh-buffer-ios-asyncdisplaykit-iglistkit-pusher/), Andy Yates, Buffer + +## Documentation + +You can find [the docs here](https://instagram.github.io/IGListKit). Documentation is generated with [jazzy](https://github.com/realm/jazzy) and hosted on [GitHub-Pages](https://pages.github.com). + +To regenerate docs, run `./scripts/build_docs.sh` from the root directory in the repo. + +## Vision + +For the long-term goals and "vision" of `IGListKit`, please read our [Vision](https://github.com/Instagram/IGListKit/blob/master/Guides/VISION.md) doc. + +## Contributing + +Please see the [CONTRIBUTING](https://github.com/Instagram/IGListKit/blob/master/.github/CONTRIBUTING.md) file for how to help. At Instagram, we sync the open source version of `IGListKit` daily, so we're always testing the latest changes. But that requires all changes be thoroughly tested and follow our style guide. + +We have a set of [starter tasks](https://github.com/Instagram/IGListKit/issues?q=is%3Aissue+is%3Aopen+label%3Astarter-task) that are great for beginners to jump in on and start contributing. + +## License + +`IGListKit` is [MIT-licensed](./LICENSE). + +The files in the `/Examples/` directory are licensed under a separate license as specified in each file. Documentation is licensed [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/). diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListAssert.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListAssert.h new file mode 100644 index 0000000..97466e2 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListAssert.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#ifndef IGAssert +#define IGAssert( condition, ... ) NSCAssert( (condition) , ##__VA_ARGS__) +#endif // IGAssert + +#ifndef IGFailAssert +#define IGFailAssert( ... ) IGAssert( (NO) , ##__VA_ARGS__) +#endif // IGFailAssert + +#ifndef IGParameterAssert +#define IGParameterAssert( condition ) IGAssert( (condition) , @"Invalid parameter not satisfying: %@", @#condition) +#endif // IGParameterAssert + +#ifndef IGAssertMainThread +#define IGAssertMainThread() IGAssert( ([NSThread isMainThread] == YES), @"Must be on the main thread") +#endif // IGAssertMainThread diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListBatchUpdateData.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListBatchUpdateData.h new file mode 100644 index 0000000..4482d9b --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListBatchUpdateData.h @@ -0,0 +1,106 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An instance of `IGListBatchUpdateData` takes section indexes and item index paths + and performs cleanup on init in order to perform a crash-free + update via `-[UICollectionView performBatchUpdates:completion:]`. + */ +IGLK_SUBCLASSING_RESTRICTED +NS_SWIFT_NAME(ListBatchUpdateData) +@interface IGListBatchUpdateData : NSObject + +/** + Section insert indexes. + */ +@property (nonatomic, strong, readonly) NSIndexSet *insertSections; + +/** + Section delete indexes. + */ +@property (nonatomic, strong, readonly) NSIndexSet *deleteSections; + +/** + Section moves. + */ +@property (nonatomic, strong, readonly) NSSet *moveSections; + +/** + Item insert index paths. + */ +@property (nonatomic, strong, readonly) NSArray *insertIndexPaths; + +/** + Item delete index paths. + */ +@property (nonatomic, strong, readonly) NSArray *deleteIndexPaths; + +/** + Item update index paths. + */ +@property (nonatomic, strong, readonly) NSArray *updateIndexPaths; + +/** + Item moves. + */ +@property (nonatomic, strong, readonly) NSArray *moveIndexPaths; + +/** + Creates a new batch update object with section and item operations. + + @param insertSections Section indexes to insert. + @param deleteSections Section indexes to delete. + @param moveSections Section moves. + @param insertIndexPaths Item index paths to insert. + @param deleteIndexPaths Item index paths to delete. + @param updateIndexPaths Item index paths to update. + @param moveIndexPaths Item index paths to move. + @param fixIndexPathImbalance When enabled, we remove duplicate NSIndexPath inserts to avoid insert/delete imbalance and a crash. + + @return A new batch update object. + */ +- (instancetype)initWithInsertSections:(NSIndexSet *)insertSections + deleteSections:(NSIndexSet *)deleteSections + moveSections:(NSSet *)moveSections + insertIndexPaths:(NSArray *)insertIndexPaths + deleteIndexPaths:(NSArray *)deleteIndexPaths + updateIndexPaths:(NSArray *)updateIndexPaths + moveIndexPaths:(NSArray *)moveIndexPaths + fixIndexPathImbalance:(BOOL)fixIndexPathImbalance NS_DESIGNATED_INITIALIZER; + +/** + Convenience initializer with fixIndexPathImbalance disabled. + */ +- (instancetype)initWithInsertSections:(NSIndexSet *)insertSections + deleteSections:(NSIndexSet *)deleteSections + moveSections:(NSSet *)moveSections + insertIndexPaths:(NSArray *)insertIndexPaths + deleteIndexPaths:(NSArray *)deleteIndexPaths + updateIndexPaths:(NSArray *)updateIndexPaths + moveIndexPaths:(NSArray *)moveIndexPaths; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListBatchUpdateData.mm b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListBatchUpdateData.mm new file mode 100644 index 0000000..7bdf32b --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListBatchUpdateData.mm @@ -0,0 +1,179 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListBatchUpdateData.h" + +#import + +#import +#import + +// Plucks the given move from available moves and turns it into a delete + insert +static void convertMoveToDeleteAndInsert(NSMutableSet *moves, + IGListMoveIndex *move, + NSMutableIndexSet *deletes, + NSMutableIndexSet *inserts) { + [moves removeObject:move]; + + // add a delete and insert respecting the move's from and to sections + // delete + insert will result in reloading the entire section + [deletes addIndex:move.from]; + [inserts addIndex:move.to]; +} + +@implementation IGListBatchUpdateData + +// Converts all section moves that have index path operations into a section delete + insert. ++ (void)_cleanIndexPathsWithMap:(const std::unordered_map &)map + moves:(NSMutableSet *)moves + indexPaths:(NSMutableArray *)indexPaths + deletes:(NSMutableIndexSet *)deletes + inserts:(NSMutableIndexSet *)inserts { + for (NSInteger i = indexPaths.count - 1; i >= 0; i--) { + NSIndexPath *path = indexPaths[i]; + const auto it = map.find(path.section); + if (it != map.end() && it->second != nil) { + [indexPaths removeObjectAtIndex:i]; + convertMoveToDeleteAndInsert(moves, it->second, deletes, inserts); + } + } +} + +- (instancetype)initWithInsertSections:(NSIndexSet *)insertSections + deleteSections:(NSIndexSet *)deleteSections + moveSections:(NSSet *)moveSections + insertIndexPaths:(NSArray *)insertIndexPaths + deleteIndexPaths:(NSArray *)deleteIndexPaths + updateIndexPaths:(NSArray *)updateIndexPaths + moveIndexPaths:(NSArray *)moveIndexPaths { + return [self initWithInsertSections:insertSections + deleteSections:deleteSections + moveSections:moveSections + insertIndexPaths:insertIndexPaths + deleteIndexPaths:deleteIndexPaths + updateIndexPaths:updateIndexPaths + moveIndexPaths:moveIndexPaths + fixIndexPathImbalance:NO]; +} + +/** + Converts all section moves that are also reloaded, or have index path inserts, deletes, or reloads into a section + delete + insert in order to avoid UICollectionView heap corruptions, exceptions, and animation/snapshot bugs. + */ +- (instancetype)initWithInsertSections:(nonnull NSIndexSet *)insertSections + deleteSections:(nonnull NSIndexSet *)deleteSections + moveSections:(nonnull NSSet *)moveSections + insertIndexPaths:(nonnull NSArray *)insertIndexPaths + deleteIndexPaths:(nonnull NSArray *)deleteIndexPaths + updateIndexPaths:(nonnull NSArray *)updateIndexPaths + moveIndexPaths:(nonnull NSArray *)moveIndexPaths + fixIndexPathImbalance:(BOOL)fixIndexPathImbalance { + IGParameterAssert(insertSections != nil); + IGParameterAssert(deleteSections != nil); + IGParameterAssert(moveSections != nil); + IGParameterAssert(insertIndexPaths != nil); + IGParameterAssert(deleteIndexPaths != nil); + IGParameterAssert(updateIndexPaths != nil); + IGParameterAssert(moveIndexPaths != nil); + if (self = [super init]) { + NSMutableSet *mMoveSections = [moveSections mutableCopy]; + NSMutableIndexSet *mDeleteSections = [deleteSections mutableCopy]; + NSMutableIndexSet *mInsertSections = [insertSections mutableCopy]; + NSMutableSet *mMoveIndexPaths = [moveIndexPaths mutableCopy]; + + // these collections should NEVER be mutated during cleanup passes, otherwise sections that have multiple item + // changes (e.g. a moved section that has a delete + reload on different index paths w/in the section) will only + // convert one of the item changes into a section delete+insert. this will fail hard and be VERY difficult to + // debug + const NSInteger moveCount = [moveSections count]; + std::unordered_map fromMap(moveCount); + std::unordered_map toMap(moveCount); + for (IGListMoveIndex *move in moveSections) { + const NSInteger from = move.from; + const NSInteger to = move.to; + + // if the move is already deleted or inserted, discard it because count-changing operations must match + // with data source changes + if ([deleteSections containsIndex:from] || [insertSections containsIndex:to]) { + [mMoveSections removeObject:move]; + } else { + fromMap[from] = move; + toMap[to] = move; + } + } + + // avoid a flaky UICollectionView bug when deleting from the same index path twice + // exposes a possible data source inconsistency issue + NSMutableArray *mDeleteIndexPaths = [[[NSSet setWithArray:deleteIndexPaths] allObjects] mutableCopy]; + + NSMutableArray *mInsertIndexPaths; + if (fixIndexPathImbalance) { + // Since we remove duplicate deletes (see above) we also need to remove inserts to keep the same insert/delete + // balance. For example, if we reload (insert & delete) the same NSIndexPath twice, we would otherwise end up + // with 2 inserts and 1 delete. + mInsertIndexPaths = [[[NSSet setWithArray:insertIndexPaths] allObjects] mutableCopy]; + } else { + mInsertIndexPaths = [insertIndexPaths mutableCopy]; + } + + // avoids a bug where a cell is animated twice and one of the snapshot cells is never removed from the hierarchy + [IGListBatchUpdateData _cleanIndexPathsWithMap:fromMap moves:mMoveSections indexPaths:mDeleteIndexPaths deletes:mDeleteSections inserts:mInsertSections]; + + // prevents a bug where UICollectionView corrupts the heap memory when inserting into a section that is moved + [IGListBatchUpdateData _cleanIndexPathsWithMap:toMap moves:mMoveSections indexPaths:mInsertIndexPaths deletes:mDeleteSections inserts:mInsertSections]; + + for (IGListMoveIndexPath *move in moveIndexPaths) { + // if the section w/ an index path move is deleted, just drop the move + if ([deleteSections containsIndex:move.from.section]) { + [mMoveIndexPaths removeObject:move]; + } + + // if a move is inside a section that is moved, convert the section move to a delete+insert + const auto it = fromMap.find(move.from.section); + if (it != fromMap.end() && it->second != nil) { + IGListMoveIndex *sectionMove = it->second; + [mMoveIndexPaths removeObject:move]; + [mMoveSections removeObject:sectionMove]; + [mDeleteSections addIndex:sectionMove.from]; + [mInsertSections addIndex:sectionMove.to]; + } + } + + _deleteSections = [mDeleteSections copy]; + _insertSections = [mInsertSections copy]; + _moveSections = [mMoveSections copy]; + _deleteIndexPaths = [mDeleteIndexPaths copy]; + _insertIndexPaths = [mInsertIndexPaths copy]; + _updateIndexPaths = [updateIndexPaths copy]; + _moveIndexPaths = [mMoveIndexPaths copy]; + } + return self; +} + +- (BOOL)isEqual:(id)object { + if (object == self) { + return YES; + } + if ([object isKindOfClass:[IGListBatchUpdateData class]]) { + return ([self.insertSections isEqual:[object insertSections]] + && [self.deleteSections isEqual:[object deleteSections]] + && [self.moveSections isEqual:[object moveSections]] + && [self.insertIndexPaths isEqual:[object insertIndexPaths]] + && [self.deleteIndexPaths isEqual:[object deleteIndexPaths]] + && [self.updateIndexPaths isEqual:[object updateIndexPaths]] + && [self.moveIndexPaths isEqual:[object moveIndexPaths]]); + } + return NO; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p; deleteSections: %lu; insertSections: %lu; moveSections: %lu; deleteIndexPaths: %lu; insertIndexPaths: %lu; updateIndexPaths: %lu>", + NSStringFromClass(self.class), self, (unsigned long)self.deleteSections.count, (unsigned long)self.insertSections.count, (unsigned long)self.moveSections.count, + (unsigned long)self.deleteIndexPaths.count, (unsigned long)self.insertIndexPaths.count, (unsigned long)self.updateIndexPaths.count]; +} + +@end diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListCompatibility.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListCompatibility.h new file mode 100644 index 0000000..e9d9726 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListCompatibility.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#if TARGET_OS_EMBEDDED || TARGET_OS_SIMULATOR +#import +#else +#import +#endif diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiff.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiff.h new file mode 100644 index 0000000..91fa26b --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiff.h @@ -0,0 +1,63 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An option for how to do comparisons between similar objects. + */ +NS_SWIFT_NAME(ListDiffOption) +typedef NS_ENUM(NSInteger, IGListDiffOption) { + /** + Compare objects using pointer personality. + */ + IGListDiffPointerPersonality, + /** + Compare objects using `-[IGListDiffable isEqualToDiffableObject:]`. + */ + IGListDiffEquality +}; + +/** + Creates a diff using indexes between two collections. + + @param oldArray The old objects to diff against. + @param newArray The new objects. + @param option An option on how to compare objects. + + @return A result object containing affected indexes. + */ +NS_SWIFT_NAME(ListDiff(oldArray:newArray:option:)) +FOUNDATION_EXTERN IGListIndexSetResult *IGListDiff(NSArray> *_Nullable oldArray, + NSArray> *_Nullable newArray, + IGListDiffOption option); + +/** + Creates a diff using index paths between two collections. + + @param fromSection The old section. + @param toSection The new section. + @param oldArray The old objects to diff against. + @param newArray The new objects. + @param option An option on how to compare objects. + + @return A result object containing affected indexes. + */ +NS_SWIFT_NAME(ListDiffPaths(fromSection:toSection:oldArray:newArray:option:)) +FOUNDATION_EXTERN IGListIndexPathResult *IGListDiffPaths(NSInteger fromSection, + NSInteger toSection, + NSArray> *_Nullable oldArray, + NSArray> *_Nullable newArray, + IGListDiffOption option); + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiff.mm b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiff.mm new file mode 100644 index 0000000..12ccc02 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiff.mm @@ -0,0 +1,348 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListDiff.h" + +#import +#import +#import + +#import +#import + +#import "IGListIndexPathResultInternal.h" +#import "IGListIndexSetResultInternal.h" +#import "IGListMoveIndexInternal.h" +#import "IGListMoveIndexPathInternal.h" + +using namespace std; + +/// Used to track data stats while diffing. +struct IGListEntry { + /// The number of times the data occurs in the old array + NSInteger oldCounter = 0; + /// The number of times the data occurs in the new array + NSInteger newCounter = 0; + /// The indexes of the data in the old array + stack oldIndexes; + /// Flag marking if the data has been updated between arrays by checking the isEqual: method + BOOL updated = NO; +}; + +/// Track both the entry and algorithm index. Default the index to NSNotFound +struct IGListRecord { + IGListEntry *entry; + mutable NSInteger index; + + IGListRecord() { + entry = NULL; + index = NSNotFound; + } +}; + +static id IGListTableKey(__unsafe_unretained id object) { + id key = [object diffIdentifier]; + NSCAssert(key != nil, @"Cannot use a nil key for the diffIdentifier of object %@", object); + return key; +} + +struct IGListEqualID { + bool operator()(const id a, const id b) const { + return (a == b) || [a isEqual: b]; + } +}; + +struct IGListHashID { + size_t operator()(const id o) const { + return (size_t)[o hash]; + } +}; + +static void addIndexToMap(BOOL useIndexPaths, NSInteger section, NSInteger index, __unsafe_unretained id object, __unsafe_unretained NSMapTable *map) { + id value; + if (useIndexPaths) { + value = [NSIndexPath indexPathForItem:index inSection:section]; + } else { + value = @(index); + } + [map setObject:value forKey:[object diffIdentifier]]; +} + +static void addIndexToCollection(BOOL useIndexPaths, __unsafe_unretained id collection, NSInteger section, NSInteger index) { + if (useIndexPaths) { + NSIndexPath *path = [NSIndexPath indexPathForItem:index inSection:section]; + [collection addObject:path]; + } else { + [collection addIndex:index]; + } +}; + +static NSArray *indexPathsAndPopulateMap(__unsafe_unretained NSArray> *array, NSInteger section, __unsafe_unretained NSMapTable *map) { + NSMutableArray *paths = [NSMutableArray new]; + [array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:section]; + [paths addObject:path]; + [map setObject:path forKey:[obj diffIdentifier]]; + }]; + return paths; +} + +static id IGListDiffing(BOOL returnIndexPaths, + NSInteger fromSection, + NSInteger toSection, + NSArray> *oldArray, + NSArray> *newArray, + IGListDiffOption option, + IGListExperiment experiments) { + const NSInteger newCount = newArray.count; + const NSInteger oldCount = oldArray.count; + + NSMapTable *oldMap = [NSMapTable strongToStrongObjectsMapTable]; + NSMapTable *newMap = [NSMapTable strongToStrongObjectsMapTable]; + + // if no new objects, everything from the oldArray is deleted + // take a shortcut and just build a delete-everything result + if (newCount == 0) { + if (returnIndexPaths) { + return [[IGListIndexPathResult alloc] initWithInserts:[NSArray new] + deletes:indexPathsAndPopulateMap(oldArray, fromSection, oldMap) + updates:[NSArray new] + moves:[NSArray new] + oldIndexPathMap:oldMap + newIndexPathMap:newMap]; + } else { + [oldArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + addIndexToMap(returnIndexPaths, fromSection, idx, obj, oldMap); + }]; + return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet new] + deletes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, oldCount)] + updates:[NSIndexSet new] + moves:[NSArray new] + oldIndexMap:oldMap + newIndexMap:newMap]; + } + } + + // if no old objects, everything from the newArray is inserted + // take a shortcut and just build an insert-everything result + if (oldCount == 0) { + if (returnIndexPaths) { + return [[IGListIndexPathResult alloc] initWithInserts:indexPathsAndPopulateMap(newArray, toSection, newMap) + deletes:[NSArray new] + updates:[NSArray new] + moves:[NSArray new] + oldIndexPathMap:oldMap + newIndexPathMap:newMap]; + } else { + [newArray enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) { + addIndexToMap(returnIndexPaths, toSection, idx, obj, newMap); + }]; + return [[IGListIndexSetResult alloc] initWithInserts:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newCount)] + deletes:[NSIndexSet new] + updates:[NSIndexSet new] + moves:[NSArray new] + oldIndexMap:oldMap + newIndexMap:newMap]; + } + } + + // symbol table uses the old/new array diffIdentifier as the key and IGListEntry as the value + // using id as the key provided by https://lists.gnu.org/archive/html/discuss-gnustep/2011-07/msg00019.html + unordered_map, IGListEntry, IGListHashID, IGListEqualID> table; + + // pass 1 + // create an entry for every item in the new array + // increment its new count for each occurence + vector newResultsArray(newCount); + for (NSInteger i = 0; i < newCount; i++) { + id key = IGListTableKey(newArray[i]); + IGListEntry &entry = table[key]; + entry.newCounter++; + + // add NSNotFound for each occurence of the item in the new array + entry.oldIndexes.push(NSNotFound); + + // note: the entry is just a pointer to the entry which is stack-allocated in the table + newResultsArray[i].entry = &entry; + } + + // pass 2 + // update or create an entry for every item in the old array + // increment its old count for each occurence + // record the original index of the item in the old array + // MUST be done in descending order to respect the oldIndexes stack construction + vector oldResultsArray(oldCount); + for (NSInteger i = oldCount - 1; i >= 0; i--) { + id key = IGListTableKey(oldArray[i]); + IGListEntry &entry = table[key]; + entry.oldCounter++; + + // push the original indices where the item occurred onto the index stack + entry.oldIndexes.push(i); + + // note: the entry is just a pointer to the entry which is stack-allocated in the table + oldResultsArray[i].entry = &entry; + } + + // pass 3 + // handle data that occurs in both arrays + for (NSInteger i = 0; i < newCount; i++) { + IGListEntry *entry = newResultsArray[i].entry; + + // grab and pop the top original index. if the item was inserted this will be NSNotFound + NSCAssert(!entry->oldIndexes.empty(), @"Old indexes is empty while iterating new item %li. Should have NSNotFound", (long)i); + const NSInteger originalIndex = entry->oldIndexes.top(); + entry->oldIndexes.pop(); + + if (originalIndex < oldCount) { + const id n = newArray[i]; + const id o = oldArray[originalIndex]; + switch (option) { + case IGListDiffPointerPersonality: + // flag the entry as updated if the pointers are not the same + if (n != o) { + entry->updated = YES; + } + break; + case IGListDiffEquality: + // use -[IGListDiffable isEqualToDiffableObject:] between both version of data to see if anything has changed + // skip the equality check if both indexes point to the same object + if (n != o && ![n isEqualToDiffableObject:o]) { + entry->updated = YES; + } + break; + } + } + if (originalIndex != NSNotFound + && entry->newCounter > 0 + && entry->oldCounter > 0) { + // if an item occurs in the new and old array, it is unique + // assign the index of new and old records to the opposite index (reverse lookup) + newResultsArray[i].index = originalIndex; + oldResultsArray[originalIndex].index = i; + } + } + + // storage for final NSIndexPaths or indexes + id mInserts, mMoves, mUpdates, mDeletes; + if (returnIndexPaths) { + mInserts = [NSMutableArray new]; + mMoves = [NSMutableArray new]; + mUpdates = [NSMutableArray new]; + mDeletes = [NSMutableArray new]; + } else { + mInserts = [NSMutableIndexSet new]; + mMoves = [NSMutableArray new]; + mUpdates = [NSMutableIndexSet new]; + mDeletes = [NSMutableIndexSet new]; + } + + // track offsets from deleted items to calculate where items have moved + vector deleteOffsets(oldCount), insertOffsets(newCount); + NSInteger runningOffset = 0; + + // iterate old array records checking for deletes + // incremement offset for each delete + for (NSInteger i = 0; i < oldCount; i++) { + deleteOffsets[i] = runningOffset; + const IGListRecord record = oldResultsArray[i]; + // if the record index in the new array doesn't exist, its a delete + if (record.index == NSNotFound) { + addIndexToCollection(returnIndexPaths, mDeletes, fromSection, i); + runningOffset++; + } + + addIndexToMap(returnIndexPaths, fromSection, i, oldArray[i], oldMap); + } + + // reset and track offsets from inserted items to calculate where items have moved + runningOffset = 0; + + for (NSInteger i = 0; i < newCount; i++) { + insertOffsets[i] = runningOffset; + const IGListRecord record = newResultsArray[i]; + const NSInteger oldIndex = record.index; + // add to inserts if the opposing index is NSNotFound + if (record.index == NSNotFound) { + addIndexToCollection(returnIndexPaths, mInserts, toSection, i); + runningOffset++; + } else { + // note that an entry can be updated /and/ moved + if (record.entry->updated) { + addIndexToCollection(returnIndexPaths, mUpdates, fromSection, oldIndex); + } + + // calculate the offset and determine if there was a move + // if the indexes match, ignore the index + const NSInteger insertOffset = insertOffsets[i]; + const NSInteger deleteOffset = deleteOffsets[oldIndex]; + if ((oldIndex - deleteOffset + insertOffset) != i) { + id move; + if (returnIndexPaths) { + NSIndexPath *from = [NSIndexPath indexPathForItem:oldIndex inSection:fromSection]; + NSIndexPath *to = [NSIndexPath indexPathForItem:i inSection:toSection]; + move = [[IGListMoveIndexPath alloc] initWithFrom:from to:to]; + } else { + move = [[IGListMoveIndex alloc] initWithFrom:oldIndex to:i]; + } + [mMoves addObject:move]; + } + } + + addIndexToMap(returnIndexPaths, toSection, i, newArray[i], newMap); + } + + NSCAssert((oldCount + [mInserts count] - [mDeletes count]) == newCount, + @"Sanity check failed applying %lu inserts and %lu deletes to old count %li equaling new count %li", + (unsigned long)[mInserts count], (unsigned long)[mDeletes count], (long)oldCount, (long)newCount); + + if (returnIndexPaths) { + return [[IGListIndexPathResult alloc] initWithInserts:mInserts + deletes:mDeletes + updates:mUpdates + moves:mMoves + oldIndexPathMap:oldMap + newIndexPathMap:newMap]; + } else { + return [[IGListIndexSetResult alloc] initWithInserts:mInserts + deletes:mDeletes + updates:mUpdates + moves:mMoves + oldIndexMap:oldMap + newIndexMap:newMap]; + } +} + +IGListIndexSetResult *IGListDiff(NSArray > *oldArray, + NSArray> *newArray, + IGListDiffOption option) { + return IGListDiffing(NO, 0, 0, oldArray, newArray, option, 0); +} + +IGListIndexPathResult *IGListDiffPaths(NSInteger fromSection, + NSInteger toSection, + NSArray> *oldArray, + NSArray> *newArray, + IGListDiffOption option) { + return IGListDiffing(YES, fromSection, toSection, oldArray, newArray, option, 0); +} + +IGListIndexSetResult *IGListDiffExperiment(NSArray> *_Nullable oldArray, + NSArray> *_Nullable newArray, + IGListDiffOption option, + IGListExperiment experiments) { + return IGListDiffing(NO, 0, 0, oldArray, newArray, option, experiments); +} + +IGListIndexPathResult *IGListDiffPathsExperiment(NSInteger fromSection, + NSInteger toSection, + NSArray> *_Nullable oldArray, + NSArray> *_Nullable newArray, + IGListDiffOption option, + IGListExperiment experiments) { + return IGListDiffing(YES, fromSection, toSection, oldArray, newArray, option, experiments); +} diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiffKit.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiffKit.h new file mode 100644 index 0000000..d889ba3 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiffKit.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** + * Project version number for IGListKit. + */ +FOUNDATION_EXPORT double IGListKitVersionNumber; + +/** + * Project version string for IGListKit. + */ +FOUNDATION_EXPORT const unsigned char IGListKitVersionString[]; + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiffable.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiffable.h new file mode 100644 index 0000000..5875daf --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListDiffable.h @@ -0,0 +1,38 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** + The `IGListDiffable` protocol provides methods needed to compare the identity and equality of two objects. + */ +NS_SWIFT_NAME(ListDiffable) +@protocol IGListDiffable + +/** + Returns a key that uniquely identifies the object. + + @return A key that can be used to uniquely identify the object. + + @note Two objects may share the same identifier, but are not equal. A common pattern is to use the `NSObject` + category for automatic conformance. However this means that objects will be identified on their + pointer value so finding updates becomes impossible. + + @warning This value should never be mutated. + */ +- (nonnull id)diffIdentifier; + +/** + Returns whether the receiver and a given object are equal. + + @param object The object to be compared to the receiver. + + @return `YES` if the receiver and object are equal, otherwise `NO`. + */ +- (BOOL)isEqualToDiffableObject:(nullable id)object; + +@end diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListExperiments.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListExperiments.h new file mode 100644 index 0000000..009f3df --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListExperiments.h @@ -0,0 +1,92 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +/** + Bitmask-able options used for pre-release feature testing. + */ +NS_SWIFT_NAME(ListExperiment) +typedef NS_OPTIONS (NSInteger, IGListExperiment) { + /// Specifies no experiments. + IGListExperimentNone = 1 << 1, + /// Test updater diffing performed on a background queue. + IGListExperimentBackgroundDiffing = 1 << 2, + /// Test fallback to reloadData when "too many" update operations. + IGListExperimentReloadDataFallback = 1 << 3, + /// Test removing the layout pass when calling scrollToObject to avoid creating off-screen cells. + IGListExperimentAvoidLayoutOnScrollToObject = 1 << 4, + /// Test fixing a crash when inserting and deleting the same NSIndexPath multiple times. + IGListExperimentFixIndexPathImbalance = 1 << 5, + /// Test deferring object creation until just before diffing. + IGListExperimentDeferredToObjectCreation = 1 << 6, + /// Test getting collection view at update time. + IGListExperimentGetCollectionViewAtUpdate = 1 << 7, + /// Test invalidating layout when cell reloads/updates in IGListBindingSectionController. + IGListExperimentInvalidateLayoutForUpdates = 1 << 8, + /// Test using the collection view when asking for layout instead of accessing the data source. Only apply to IGListCollectionViewLayout. + IGListExperimentUseCollectionViewInsteadOfDataSourceInLayout = 1 << 9 +}; + +/** + Check if an experiment is enabled in a bitmask. + + @param mask The bitmask of experiments. + @param option The option to compare with. + + @return `YES` if the option is in the bitmask, otherwise `NO`. + */ +NS_SWIFT_NAME(ListExperimentEnabled(mask:option:)) +static inline BOOL IGListExperimentEnabled(IGListExperiment mask, IGListExperiment option) { + return (mask & option) != 0; +} + +NS_ASSUME_NONNULL_BEGIN + +/** + Performs an index diff with an experiment bitmask. + + @param oldArray The old array of objects. + @param newArray The new array of objects. + @param option Option to specify the type of diff. + @param experiments Optional experiments. + + @return An index set result object contained the changed indexes. + + @see `IGListDiff()`. + */ +NS_SWIFT_NAME(ListDiffExperiment(oldArray:newArray:option:experiments:)) +FOUNDATION_EXTERN IGListIndexSetResult *IGListDiffExperiment(NSArray> *_Nullable oldArray, + NSArray> *_Nullable newArray, + IGListDiffOption option, + IGListExperiment experiments); + +/** + Performs a index path diff with an experiment bitmask. + + @param fromSection The old section. + @param toSection The new section. + @param oldArray The old array of objects. + @param newArray The new array of objects. + @param option Option to specify the type of diff. + @param experiments Optional experiments. + + @return An index path result object containing the changed indexPaths. + + @see `IGListDiffPaths()`. + */ +NS_SWIFT_NAME(ListDiffPathsExperiment(fromSection:toSection:oldArray:newArray:option:experiments:)) +FOUNDATION_EXTERN IGListIndexPathResult *IGListDiffPathsExperiment(NSInteger fromSection, + NSInteger toSection, + NSArray> *_Nullable oldArray, + NSArray> *_Nullable newArray, + IGListDiffOption option, + IGListExperiment experiments); + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexPathResult.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexPathResult.h new file mode 100644 index 0000000..d0baedb --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexPathResult.h @@ -0,0 +1,85 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A result object returned when diffing with sections. + */ +NS_SWIFT_NAME(ListIndexPathResult) +@interface IGListIndexPathResult : NSObject + +/** + The index paths inserted into the new collection. + */ +@property (nonatomic, copy, readonly) NSArray *inserts; + +/** + The index paths deleted from the old collection. + */ +@property (nonatomic, copy, readonly) NSArray *deletes; + +/** + The index paths in the old collection that need updated. + */ +@property (nonatomic, copy, readonly) NSArray *updates; + +/** + The moves from an index path in the old collection to an index path in the new collection. + */ +@property (nonatomic, copy, readonly) NSArray *moves; + +/** + A Read-only boolean that indicates whether the result has any changes or not. + `YES` if the result has changes, `NO` otherwise. + */ +@property (nonatomic, assign, readonly) BOOL hasChanges; + +/** + Returns the index path of the object with the specified identifier *before* the diff. + + @param identifier The diff identifier of the object. + + @return The index path of the object before the diff, or `nil`. + + @see `-[IGListDiffable diffIdentifier]`. + */ +- (nullable NSIndexPath *)oldIndexPathForIdentifier:(id)identifier; + +/** + Returns the index path of the object with the specified identifier *after* the diff. + + @param identifier The diff identifier of the object. + + @return The index path of the object after the diff, or `nil`. + + @see `-[IGListDiffable diffIdentifier]`. + */ +- (nullable NSIndexPath *)newIndexPathForIdentifier:(id)identifier; + +/** + Creates a new result object with operations safe for use in `UITableView` and `UICollectionView` batch updates. + */ +- (IGListIndexPathResult *)resultForBatchUpdates; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexPathResult.m b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexPathResult.m new file mode 100644 index 0000000..46450d1 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexPathResult.m @@ -0,0 +1,91 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListIndexPathResult.h" +#import "IGListIndexPathResultInternal.h" + +@implementation IGListIndexPathResult { + NSMapTable, NSIndexPath *> *_oldIndexPathMap; + NSMapTable, NSIndexPath *> *_newIndexPathMap; +} + +- (instancetype)initWithInserts:(NSArray *)inserts + deletes:(NSArray *)deletes + updates:(NSArray *)updates + moves:(NSArray *)moves + oldIndexPathMap:(NSMapTable, NSIndexPath *> *)oldIndexPathMap + newIndexPathMap:(NSMapTable, NSIndexPath *> *)newIndexPathMap { + if (self = [super init]) { + _inserts = inserts; + _deletes = deletes; + _updates = updates; + _moves = moves; + _oldIndexPathMap = oldIndexPathMap; + _newIndexPathMap = newIndexPathMap; + } + return self; +} + +- (BOOL)hasChanges { + return self.changeCount > 0; +} + +- (NSInteger)changeCount { + return self.inserts.count + self.deletes.count + self.updates.count + self.moves.count; +} + +- (IGListIndexPathResult *)resultForBatchUpdates { + NSMutableSet *deletes = [NSMutableSet setWithArray:self.deletes]; + NSMutableSet *inserts = [NSMutableSet setWithArray:self.inserts]; + NSMutableSet *filteredUpdates = [NSMutableSet setWithArray:self.updates]; + + NSArray *moves = self.moves; + NSMutableArray *filteredMoves = [moves mutableCopy]; + + // convert move+update to delete+insert, respecting the from/to of the move + const NSInteger moveCount = moves.count; + for (NSInteger i = moveCount - 1; i >= 0; i--) { + IGListMoveIndexPath *move = moves[i]; + if ([filteredUpdates containsObject:move.from]) { + [filteredMoves removeObjectAtIndex:i]; + [filteredUpdates removeObject:move.from]; + [deletes addObject:move.from]; + [inserts addObject:move.to]; + } + } + + // iterate all new identifiers. if its index is updated, delete from the old index and insert the new index + for (id key in [_oldIndexPathMap keyEnumerator]) { + NSIndexPath *indexPath = [_oldIndexPathMap objectForKey:key]; + if ([filteredUpdates containsObject:indexPath]) { + [deletes addObject:indexPath]; + [inserts addObject:(id)[_newIndexPathMap objectForKey:key]]; + } + } + + return [[IGListIndexPathResult alloc] initWithInserts:[inserts allObjects] + deletes:[deletes allObjects] + updates:[NSArray new] + moves:filteredMoves + oldIndexPathMap:_oldIndexPathMap + newIndexPathMap:_newIndexPathMap]; +} + +- (NSIndexPath *)oldIndexPathForIdentifier:(id)identifier { + return [_oldIndexPathMap objectForKey:identifier]; +} + +- (NSIndexPath *)newIndexPathForIdentifier:(id)identifier { + return [_newIndexPathMap objectForKey:identifier]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p; %lu inserts; %lu deletes; %lu updates; %lu moves>", + NSStringFromClass(self.class), self, (unsigned long)self.inserts.count, (unsigned long)self.deletes.count, (unsigned long)self.updates.count, (unsigned long)self.moves.count]; +} + +@end diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexSetResult.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexSetResult.h new file mode 100644 index 0000000..6c81d5f --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexSetResult.h @@ -0,0 +1,85 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A result object returned when diffing with indexes. + */ +NS_SWIFT_NAME(ListIndexSetResult) +@interface IGListIndexSetResult : NSObject + +/** + The indexes inserted into the new collection. + */ +@property (nonatomic, strong, readonly) NSIndexSet *inserts; + +/** + The indexes deleted from the old collection. + */ +@property (nonatomic, strong, readonly) NSIndexSet *deletes; + +/** + The indexes in the old collection that need updated. + */ +@property (nonatomic, strong, readonly) NSIndexSet *updates; + +/** + The moves from an index in the old collection to an index in the new collection. + */ +@property (nonatomic, copy, readonly) NSArray *moves; + +/** + A Read-only boolean that indicates whether the result has any changes or not. + `YES` if the result has changes, `NO` otherwise. + */ +@property (nonatomic, assign, readonly) BOOL hasChanges; + +/** + Returns the index of the object with the specified identifier *before* the diff. + + @param identifier The diff identifier of the object. + + @return The index of the object before the diff, or `NSNotFound`. + + @see `-[IGListDiffable diffIdentifier]`. + */ +- (NSInteger)oldIndexForIdentifier:(id)identifier; + +/** + Returns the index of the object with the specified identifier *after* the diff. + + @param identifier The diff identifier of the object. + + @return The index path of the object after the diff, or `NSNotFound`. + + @see `-[IGListDiffable diffIdentifier]`. + */ +- (NSInteger)newIndexForIdentifier:(id)identifier; + +/** + Creates a new result object with operations safe for use in `UITableView` and `UICollectionView` batch updates. + */ +- (IGListIndexSetResult *)resultForBatchUpdates; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexSetResult.m b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexSetResult.m new file mode 100644 index 0000000..74853e5 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListIndexSetResult.m @@ -0,0 +1,93 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListIndexSetResult.h" +#import "IGListIndexSetResultInternal.h" + +@implementation IGListIndexSetResult { + NSMapTable, NSNumber *> *_oldIndexMap; + NSMapTable, NSNumber *> *_newIndexMap; +} + +- (instancetype)initWithInserts:(NSIndexSet *)inserts + deletes:(NSIndexSet *)deletes + updates:(NSIndexSet *)updates + moves:(NSArray *)moves + oldIndexMap:(NSMapTable, NSNumber *> *)oldIndexMap + newIndexMap:(NSMapTable, NSNumber *> *)newIndexMap { + if (self = [super init]) { + _inserts = inserts; + _deletes = deletes; + _updates = updates; + _moves = moves; + _oldIndexMap = oldIndexMap; + _newIndexMap = newIndexMap; + } + return self; +} + +- (BOOL)hasChanges { + return self.changeCount > 0; +} + +- (NSInteger)changeCount { + return self.inserts.count + self.deletes.count + self.updates.count + self.moves.count; +} + +- (IGListIndexSetResult *)resultForBatchUpdates { + NSMutableIndexSet *deletes = [self.deletes mutableCopy]; + NSMutableIndexSet *inserts = [self.inserts mutableCopy]; + NSMutableIndexSet *filteredUpdates = [self.updates mutableCopy]; + + NSArray *moves = self.moves; + NSMutableArray *filteredMoves = [moves mutableCopy]; + + // convert all update+move to delete+insert + const NSInteger moveCount = moves.count; + for (NSInteger i = moveCount - 1; i >= 0; i--) { + IGListMoveIndex *move = moves[i]; + if ([filteredUpdates containsIndex:move.from]) { + [filteredMoves removeObjectAtIndex:i]; + [filteredUpdates removeIndex:move.from]; + [deletes addIndex:move.from]; + [inserts addIndex:move.to]; + } + } + + // iterate all new identifiers. if its index is updated, delete from the old index and insert the new index + for (id key in [_oldIndexMap keyEnumerator]) { + const NSInteger index = [[_oldIndexMap objectForKey:key] integerValue]; + if ([filteredUpdates containsIndex:index]) { + [deletes addIndex:index]; + [inserts addIndex:[[_newIndexMap objectForKey:key] integerValue]]; + } + } + + return [[IGListIndexSetResult alloc] initWithInserts:inserts + deletes:deletes + updates:[NSIndexSet new] + moves:filteredMoves + oldIndexMap:_oldIndexMap + newIndexMap:_newIndexMap]; +} + +- (NSInteger)oldIndexForIdentifier:(id)identifier { + NSNumber *index = [_oldIndexMap objectForKey:identifier]; + return index == nil ? NSNotFound : [index integerValue]; +} + +- (NSInteger)newIndexForIdentifier:(id)identifier { + NSNumber *index = [_newIndexMap objectForKey:identifier]; + return index == nil ? NSNotFound : [index integerValue]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p; %lu inserts; %lu deletes; %lu updates; %lu moves>", + NSStringFromClass(self.class), self, (unsigned long)self.inserts.count, (unsigned long)self.deletes.count, (unsigned long)self.updates.count, (unsigned long)self.moves.count]; +} + +@end diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMacros.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMacros.h new file mode 100644 index 0000000..5b5ef87 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMacros.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#ifndef IGLK_SUBCLASSING_RESTRICTED +#if defined(__has_attribute) && __has_attribute(objc_subclassing_restricted) +#define IGLK_SUBCLASSING_RESTRICTED __attribute__((objc_subclassing_restricted)) +#else +#define IGLK_SUBCLASSING_RESTRICTED +#endif // #if defined(__has_attribute) && __has_attribute(objc_subclassing_restricted) +#endif // #ifndef IGLK_SUBCLASSING_RESTRICTED + +#ifndef IGLK_UNAVAILABLE +#define IGLK_UNAVAILABLE(message) __attribute__((unavailable(message))) +#endif // #ifndef IGLK_UNAVAILABLE + +#if IGLK_LOGGING_ENABLED +#define IGLKLog( s, ... ) do { NSLog( @"IGListKit: %@", [NSString stringWithFormat: (s), ##__VA_ARGS__] ); } while(0) +#else +#define IGLKLog( s, ... ) +#endif + +#ifndef IGLK_DEBUG_DESCRIPTION_ENABLED +#define IGLK_DEBUG_DESCRIPTION_ENABLED DEBUG +#endif // #ifndef IGLK_DEBUG_DESCRIPTION_ENABLED diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndex.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndex.h new file mode 100644 index 0000000..1849bb8 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndex.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An object representing a move between indexes. + */ +NS_SWIFT_NAME(ListMoveIndex) +@interface IGListMoveIndex : NSObject + +/** + An index in the old collection. + */ +@property (nonatomic, assign, readonly) NSInteger from; + +/** + An index in the new collection. + */ +@property (nonatomic, assign, readonly) NSInteger to; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndex.m b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndex.m new file mode 100644 index 0000000..dee66f4 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndex.m @@ -0,0 +1,52 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListMoveIndex.h" + +@implementation IGListMoveIndex + +- (instancetype)initWithFrom:(NSInteger)from to:(NSInteger)to { + if (self = [super init]) { + _from = from; + _to = to; + } + return self; +} + +- (NSUInteger)hash { + return _from ^ _to; +} + +- (BOOL)isEqual:(id)object { + if (object == self) { + return YES; + } + if ([object isKindOfClass:[IGListMoveIndex class]]) { + const NSInteger f1 = self.from, f2 = [object from]; + const NSInteger t1 = self.to, t2 = [object to]; + return f1 == f2 && t1 == t2; + } + return NO; +} + +- (NSComparisonResult)compare:(id)object { + const NSInteger right = [object from]; + const NSInteger left = [self from]; + if (left == right) { + return NSOrderedSame; + } else if (left < right) { + return NSOrderedAscending; + } else { + return NSOrderedDescending; + } +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p; from: %li; to: %li;>", NSStringFromClass(self.class), self, (long)self.from, (long)self.to]; +} + +@end diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndexPath.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndexPath.h new file mode 100644 index 0000000..30b7070 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndexPath.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An object representing a move between indexes. + */ +NS_SWIFT_NAME(ListMoveIndexPath) +@interface IGListMoveIndexPath : NSObject + +/** + An index path in the old collection. + */ +@property (nonatomic, strong, readonly) NSIndexPath *from; + +/** + An index path in the new collection. + */ +@property (nonatomic, strong, readonly) NSIndexPath *to; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndexPath.m b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndexPath.m new file mode 100644 index 0000000..7517794 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/IGListMoveIndexPath.m @@ -0,0 +1,46 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListMoveIndexPath.h" + +@implementation IGListMoveIndexPath + +- (instancetype)initWithFrom:(NSIndexPath *)from to:(NSIndexPath *)to { + NSParameterAssert(from != nil); + NSParameterAssert(to != nil); + if (self = [super init]) { + _from = from; + _to = to; + } + return self; +} + +- (NSUInteger)hash { + return [_from hash] ^ [_to hash]; +} + +- (BOOL)isEqual:(id)object { + if (object == self) { + return YES; + } + if ([object isKindOfClass:[IGListMoveIndexPath class]]) { + NSIndexPath *f1 = self.from, *f2 = [object from]; + NSIndexPath *t1 = self.to, *t2 = [object to]; + return [f1 isEqual:f2] && [t1 isEqual:t2]; + } + return NO; +} + +- (NSComparisonResult)compare:(id)object { + return [[self from] compare:[object from]]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p; from: %@; to: %@;>", NSStringFromClass(self.class), self, self.from, self.to]; +} + +@end diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h new file mode 100644 index 0000000..8fec459 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListIndexPathResult() + +- (instancetype)initWithInserts:(NSArray *)inserts + deletes:(NSArray *)deletes + updates:(NSArray *)updates + moves:(NSArray *)moves + oldIndexPathMap:(NSMapTable, NSIndexPath *> *)oldIndexPathMap + newIndexPathMap:(NSMapTable, NSIndexPath *> *)newIndexPathMap; + +@property (nonatomic, assign, readonly) NSInteger changeCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h new file mode 100644 index 0000000..db7f14a --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListIndexSetResult() + +- (instancetype)initWithInserts:(NSIndexSet *)inserts + deletes:(NSIndexSet *)deletes + updates:(NSIndexSet *)updates + moves:(NSArray *)moves + oldIndexMap:(NSMapTable, NSNumber *> *)oldIndexMap + newIndexMap:(NSMapTable, NSNumber *> *)newIndexMap; + +@property (nonatomic, assign, readonly) NSInteger changeCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h new file mode 100644 index 0000000..069a545 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListMoveIndex () + +- (instancetype)initWithFrom:(NSInteger)from to:(NSInteger)to NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h new file mode 100644 index 0000000..0370bb9 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListMoveIndexPath () + +- (instancetype)initWithFrom:(NSIndexPath *)from to:(NSIndexPath *)to NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSNumber+IGListDiffable.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSNumber+IGListDiffable.h new file mode 100644 index 0000000..5f7396f --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSNumber+IGListDiffable.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +/** + This category provides default `IGListDiffable` conformance for `NSNumber`. + */ +@interface NSNumber (IGListDiffable) + +@end diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSNumber+IGListDiffable.m b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSNumber+IGListDiffable.m new file mode 100644 index 0000000..4f77466 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSNumber+IGListDiffable.m @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "NSNumber+IGListDiffable.h" + +@implementation NSNumber (IGListDiffable) + +- (id)diffIdentifier { + return self; +} + +- (BOOL)isEqualToDiffableObject:(id)object { + return [self isEqual:object]; +} + +@end diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSString+IGListDiffable.h b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSString+IGListDiffable.h new file mode 100644 index 0000000..aa5e95c --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSString+IGListDiffable.h @@ -0,0 +1,17 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +/** + This category provides default `IGListDiffable` conformance for `NSString`. + */ +@interface NSString (IGListDiffable) + +@end diff --git a/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSString+IGListDiffable.m b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSString+IGListDiffable.m new file mode 100644 index 0000000..71ac2f5 --- /dev/null +++ b/Demo/Pods/IGListDiffKit/Source/IGListDiffKit/NSString+IGListDiffable.m @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "NSString+IGListDiffable.h" + +@implementation NSString (IGListDiffable) + +- (id)diffIdentifier { + return self; +} + +- (BOOL)isEqualToDiffableObject:(id)object { + return [self isEqual:object]; +} + +@end diff --git a/Demo/Pods/IGListKit/LICENSE.md b/Demo/Pods/IGListKit/LICENSE.md new file mode 100755 index 0000000..87cbf53 --- /dev/null +++ b/Demo/Pods/IGListKit/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +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. \ No newline at end of file diff --git a/Demo/Pods/IGListKit/README.md b/Demo/Pods/IGListKit/README.md new file mode 100644 index 0000000..22981eb --- /dev/null +++ b/Demo/Pods/IGListKit/README.md @@ -0,0 +1,110 @@ +

+ +

+ +

+ + Build Status + + + Coverage Status + + + Pods Version + + + Platforms + + + Carthage Compatible + +

+ +---------------- + +A data-driven `UICollectionView` framework for building fast and flexible lists. + +| | Main Features | +----------|----------------- +🙅 | Never call `performBatchUpdates(_:, completion:)` or `reloadData()` again +🏠 | Better architecture with reusable cells and components +🔠 | Create collections with multiple data types +🔑 | Decoupled diffing algorithm +✅ | Fully unit tested +🔍 | Customize your diffing behavior for your models +📱 | Simply `UICollectionView` at its core +🚀 | Extendable API +🐦 | Written in Objective-C with full Swift interop support + +`IGListKit` is built and maintained with ❤️ by [Instagram engineering](https://engineering.instagram.com/). +We use the open source version `master` branch in the Instagram app. + +## Requirements + +- Xcode 9.0+ +- iOS 9.0+ +- tvOS 9.0+ +- macOS 10.11+ *(diffing algorithm components only)* +- Interoperability with Swift 3.0+ + +## Installation + +### CocoaPods + +The preferred installation method is with [CocoaPods](https://cocoapods.org). Add the following to your `Podfile`: + +```ruby +pod 'IGListKit', '~> 4.0.0' +``` + +### Carthage + +For [Carthage](https://github.com/Carthage/Carthage), add the following to your `Cartfile`: + +```ogdl +github "Instagram/IGListKit" ~> 4.0.0 +``` + +> For advanced usage, see our [Installation Guide](https://instagram.github.io/IGListKit/installation.html). + +## Getting Started + +```bash +$ git clone https://github.com/Instagram/IGListKit.git +$ cd IGListKit/ +$ ./scripts/setup.sh +``` + +- Our [Getting Started guide](https://instagram.github.io/IGListKit/getting-started.html) +- Ray Wenderlich's [IGListKit Tutorial: Better UICollectionViews](https://www.raywenderlich.com/147162/iglistkit-tutorial-better-uicollectionviews) +- Our [example projects](https://github.com/Instagram/IGListKit/tree/master/Examples) +- Ryan Nystrom's [talk at try! Swift NYC](https://realm.io/news/tryswift-ryan-nystrom-refactoring-at-scale-lessons-learned-rewriting-instagram-feed/) (Note: this talk was for an earlier version. Some APIs have changed.) +- [Migrating an UITableView to IGListCollectionView](https://medium.com/cocoaacademymag/iglistkit-migrating-an-uitableview-to-iglistkitcollectionview-65a30cf9bac9), by Rodrigo Cavalcante +- [Keeping data fresh in Buffer for iOS with AsyncDisplayKit, IGListKit & Pusher](https://overflow.buffer.com/2017/04/10/keeping-data-fresh-buffer-ios-asyncdisplaykit-iglistkit-pusher/), Andy Yates, Buffer + +## Documentation + +You can find [the docs here](https://instagram.github.io/IGListKit). Documentation is generated with [jazzy](https://github.com/realm/jazzy) and hosted on [GitHub-Pages](https://pages.github.com). + +To regenerate docs, run `./scripts/build_docs.sh` from the root directory in the repo. + +## Vision + +For the long-term goals and "vision" of `IGListKit`, please read our [Vision](https://github.com/Instagram/IGListKit/blob/master/Guides/VISION.md) doc. + +## Contributing + +Please see the [CONTRIBUTING](https://github.com/Instagram/IGListKit/blob/master/.github/CONTRIBUTING.md) file for how to help. At Instagram, we sync the open source version of `IGListKit` daily, so we're always testing the latest changes. But that requires all changes be thoroughly tested and follow our style guide. + +We have a set of [starter tasks](https://github.com/Instagram/IGListKit/issues?q=is%3Aissue+is%3Aopen+label%3Astarter-task) that are great for beginners to jump in on and start contributing. + +## License + +`IGListKit` is [MIT-licensed](./LICENSE). + +The files in the `/Examples/` directory are licensed under a separate license as specified in each file. Documentation is licensed [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/). diff --git a/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h b/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h new file mode 100644 index 0000000..8fec459 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListIndexPathResult() + +- (instancetype)initWithInserts:(NSArray *)inserts + deletes:(NSArray *)deletes + updates:(NSArray *)updates + moves:(NSArray *)moves + oldIndexPathMap:(NSMapTable, NSIndexPath *> *)oldIndexPathMap + newIndexPathMap:(NSMapTable, NSIndexPath *> *)newIndexPathMap; + +@property (nonatomic, assign, readonly) NSInteger changeCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h b/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h new file mode 100644 index 0000000..db7f14a --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListIndexSetResult() + +- (instancetype)initWithInserts:(NSIndexSet *)inserts + deletes:(NSIndexSet *)deletes + updates:(NSIndexSet *)updates + moves:(NSArray *)moves + oldIndexMap:(NSMapTable, NSNumber *> *)oldIndexMap + newIndexMap:(NSMapTable, NSNumber *> *)newIndexMap; + +@property (nonatomic, assign, readonly) NSInteger changeCount; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h b/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h new file mode 100644 index 0000000..069a545 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListMoveIndex () + +- (instancetype)initWithFrom:(NSInteger)from to:(NSInteger)to NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h b/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h new file mode 100644 index 0000000..0370bb9 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface IGListMoveIndexPath () + +- (instancetype)initWithFrom:(NSIndexPath *)from to:(NSIndexPath *)to NS_DESIGNATED_INITIALIZER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapter.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapter.h new file mode 100644 index 0000000..3960e67 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapter.h @@ -0,0 +1,307 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import +#import +#import +#import +#import +#import + +@protocol IGListUpdatingDelegate; + +@class IGListSectionController; + +NS_ASSUME_NONNULL_BEGIN + +/** + A block to execute when the list updates are completed. + + @param finished Specifies whether or not the update animations completed successfully. + */ +NS_SWIFT_NAME(ListUpdaterCompletion) +typedef void (^IGListUpdaterCompletion)(BOOL finished); + +/** + `IGListAdapter` objects provide an abstraction for feeds of objects in a `UICollectionView` by breaking each object + into individual sections, called "section controllers". These controllers (objects subclassing to + `IGListSectionController`) act as a data source and delegate for each section. + + Feed implementations must act as the data source for an `IGListAdapter` in order to drive the objects and section + controllers in a collection view. + */ +IGLK_SUBCLASSING_RESTRICTED +NS_SWIFT_NAME(ListAdapter) +@interface IGListAdapter : NSObject + +/** + The view controller that houses the adapter. + */ +@property (nonatomic, nullable, weak) UIViewController *viewController; + +/** + The collection view used with the adapter. + + @note Setting this property will automatically set isPrefetchingEnabled to `NO` for performance reasons. + */ +@property (nonatomic, nullable, weak) UICollectionView *collectionView; + +/** + The object that acts as the data source for the adapter. + */ +@property (nonatomic, nullable, weak) id dataSource; + +/** + The object that receives top-level events for section controllers. + */ +@property (nonatomic, nullable, weak) id delegate; + +/** + The object that receives `UICollectionViewDelegate` events. + + @note This object *will not* receive `UIScrollViewDelegate` events. Instead use scrollViewDelegate. + */ +@property (nonatomic, nullable, weak) id collectionViewDelegate; + +/** + The object that receives `UIScrollViewDelegate` events. + */ +@property (nonatomic, nullable, weak) id scrollViewDelegate; + +/** + The object that receives `IGListAdapterMoveDelegate` events resulting from interactive reordering of sections. + + @note This works with UICollectionView interactive reordering available on iOS 9.0+ + */ +@property (nonatomic, nullable, weak) id moveDelegate NS_AVAILABLE_IOS(9_0); + +/** + The object that receives `IGListAdapterPerformanceDelegate` events to measure performance. + */ +@property (nonatomic, nullable, weak) id performanceDelegate; + +/** + The updater for the adapter. + */ +@property (nonatomic, strong, readonly) id updater; + +/** + A bitmask of experiments to conduct on the adapter. + */ +@property (nonatomic, assign) IGListExperiment experiments; + +/** + Initializes a new `IGListAdapter` object. + + @param updater An object that manages updates to the collection view. + @param viewController The view controller that will house the adapter. + @param workingRangeSize The number of objects before and after the viewport to consider within the working range. + + @return A new list adapter object. + + @note The working range is the number of objects beyond the visible objects (plus and minus) that should be + notified when they are close to being visible. For instance, if you have 3 objects on screen and a working range of 2, + the previous and succeeding 2 objects will be notified that they are within the working range. As you scroll the list + the range is updated as objects enter and exit the working range. + + To opt out of using the working range, use `initWithUpdater:viewController:` or provide a working range of `0`. + */ +- (instancetype)initWithUpdater:(id )updater + viewController:(nullable UIViewController *)viewController + workingRangeSize:(NSInteger)workingRangeSize NS_DESIGNATED_INITIALIZER; + +/** + Initializes a new `IGListAdapter` object with a working range of `0`. + + @param updater An object that manages updates to the collection view. + @param viewController The view controller that will house the adapter. + + @return A new list adapter object. + */ +- (instancetype)initWithUpdater:(id )updater + viewController:(nullable UIViewController *)viewController; + +/** + Perform an update from the previous state of the data source. This is analogous to calling + `-[UICollectionView performBatchUpdates:completion:]`. + + @param animated A flag indicating if the transition should be animated. + @param completion The block to execute when the updates complete. + */ +- (void)performUpdatesAnimated:(BOOL)animated completion:(nullable IGListUpdaterCompletion)completion; + +/** + Perform an immediate reload of the data in the data source, discarding the old objects. + + @param completion The block to execute when the reload completes. + + @warning Do not use this method to update without animations as it can be very expensive to teardown and rebuild all + section controllers. Use `-[IGListAdapter performUpdatesAnimated:completion]` instead. + */ +- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion; + +/** + Reload the list for only the specified objects. + + @param objects The objects to reload. + */ +- (void)reloadObjects:(NSArray *)objects; + +/** + Query the section controller at a given section index. Constant time lookup. + + @param section A section in the list. + + @return A section controller or `nil` if the section does not exist. + */ +- (nullable IGListSectionController *)sectionControllerForSection:(NSInteger)section; + +/** + Query the section index of a list. Constant time lookup. + + @param sectionController A list object. + + @return The section index of the list if it exists, otherwise `NSNotFound`. + */ +- (NSInteger)sectionForSectionController:(IGListSectionController *)sectionController; + +/** + Returns the section controller for the specified object. Constant time lookup. + + @param object An object from the data source. + + @return A section controller or `nil` if `object` is not in the list. + + @see `-[IGListAdapterDataSource listAdapter:sectionControllerForObject:]` + */ +- (__kindof IGListSectionController * _Nullable)sectionControllerForObject:(id)object; + +/** + Returns the object corresponding to the specified section controller in the list. Constant time lookup. + + @param sectionController A section controller in the list. + + @return The object for the specified section controller, or `nil` if not found. + */ +- (nullable id)objectForSectionController:(IGListSectionController *)sectionController; + +/** + Returns the object corresponding to a section in the list. Constant time lookup. + + @param section A section in the list. + + @return The object for the specified section, or `nil` if the section does not exist. + */ +- (nullable id)objectAtSection:(NSInteger)section; + +/** + Returns the section corresponding to the specified object in the list. Constant time lookup. + + @param object An object in the list. + + @return The section index of `object` if found, otherwise `NSNotFound`. + */ +- (NSInteger)sectionForObject:(id)object; + +/** + Returns a copy of all the objects currently driving the adapter. + + @return An array of objects. + */ +- (NSArray *)objects; + +/** + An unordered array of the currently visible section controllers. + + @return An array of section controllers. + */ +- (NSArray *)visibleSectionControllers; + +/** + An unordered array of the currently visible objects. + + @return An array of objects + */ +- (NSArray *)visibleObjects; + +/** + An unordered array of the currently visible cells for a given object. + + @param object An object in the list + + @return An array of collection view cells. + */ +- (NSArray *)visibleCellsForObject:(id)object; + +/** + Scrolls to the specified object in the list adapter. + + @param object The object to which to scroll. + @param supplementaryKinds The types of supplementary views in the section. + @param scrollDirection An option indicating the direction to scroll. + @param scrollPosition An option that specifies where the item should be positioned when scrolling finishes. + @param animated A flag indicating if the scrolling should be animated. + */ +- (void)scrollToObject:(id)object + supplementaryKinds:(nullable NSArray *)supplementaryKinds + scrollDirection:(UICollectionViewScrollDirection)scrollDirection + scrollPosition:(UICollectionViewScrollPosition)scrollPosition + animated:(BOOL)animated; + +/** + Returns the size of a cell at the specified index path. + + @param indexPath The index path of the cell. + + @return The size of the cell. + */ +- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath; + +/** + Returns the size of a supplementary view in the list at the specified index path. + + @param elementKind The kind of supplementary view. + @param indexPath The index path of the supplementary view. + + @return The size of the supplementary view. + */ +- (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind + atIndexPath:(NSIndexPath *)indexPath; + +/** + Adds a listener to the list adapter. + + @param updateListener The object conforming to the `IGListAdapterUpdateListener` protocol. + + @note Listeners are held weakly so there is no need to call `-[IGListAdapter removeUpdateListener:]` on `dealloc`. + */ +- (void)addUpdateListener:(id)updateListener; + +/** + Removes a listener from the list adapter. + + @param updateListener The object conforming to the `IGListAdapterUpdateListener` protocol. + */ +- (void)removeUpdateListener:(id)updateListener; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapter.m b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapter.m new file mode 100644 index 0000000..328789f --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapter.m @@ -0,0 +1,1339 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListAdapterInternal.h" + +#import +#import +#import + +#import "IGListArrayUtilsInternal.h" +#import "IGListDebugger.h" +#import "IGListSectionControllerInternal.h" +#import "UICollectionViewLayout+InteractiveReordering.h" +#import "UIScrollView+IGListKit.h" + +@implementation IGListAdapter { + NSMapTable *_viewSectionControllerMap; + // An array of blocks to execute once batch updates are finished + NSMutableArray *_queuedCompletionBlocks; + NSHashTable> *_updateListeners; +} + +- (void)dealloc { + [self.sectionMap reset]; +} + + +#pragma mark - Init + +- (instancetype)initWithUpdater:(id )updater + viewController:(UIViewController *)viewController + workingRangeSize:(NSInteger)workingRangeSize { + IGAssertMainThread(); + IGParameterAssert(updater); + + if (self = [super init]) { + NSPointerFunctions *keyFunctions = [updater objectLookupPointerFunctions]; + NSPointerFunctions *valueFunctions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory]; + NSMapTable *table = [[NSMapTable alloc] initWithKeyPointerFunctions:keyFunctions valuePointerFunctions:valueFunctions capacity:0]; + _sectionMap = [[IGListSectionMap alloc] initWithMapTable:table]; + + _displayHandler = [IGListDisplayHandler new]; + _workingRangeHandler = [[IGListWorkingRangeHandler alloc] initWithWorkingRangeSize:workingRangeSize]; + _updateListeners = [NSHashTable weakObjectsHashTable]; + + _viewSectionControllerMap = [NSMapTable mapTableWithKeyOptions:NSMapTableObjectPointerPersonality | NSMapTableStrongMemory + valueOptions:NSMapTableStrongMemory]; + + _updater = updater; + _viewController = viewController; + + [IGListDebugger trackAdapter:self]; + } + return self; +} + +- (instancetype)initWithUpdater:(id)updater + viewController:(UIViewController *)viewController { + return [self initWithUpdater:updater + viewController:viewController + workingRangeSize:0]; +} + +- (UICollectionView *)collectionView { + return _collectionView; +} + +- (void)setCollectionView:(UICollectionView *)collectionView { + IGAssertMainThread(); + + // if collection view has been used by a different list adapter, treat it as if we were using a new collection view + // this happens when embedding a UICollectionView inside a UICollectionViewCell that is reused + if (_collectionView != collectionView || _collectionView.dataSource != self) { + // if the collection view was being used with another IGListAdapter (e.g. cell reuse) + // destroy the previous association so the old adapter doesn't update the wrong collection view + static NSMapTable *globalCollectionViewAdapterMap = nil; + if (globalCollectionViewAdapterMap == nil) { + globalCollectionViewAdapterMap = [NSMapTable weakToWeakObjectsMapTable]; + } + [globalCollectionViewAdapterMap removeObjectForKey:_collectionView]; + [[globalCollectionViewAdapterMap objectForKey:collectionView] setCollectionView:nil]; + [globalCollectionViewAdapterMap setObject:self forKey:collectionView]; + + // dump old registered section controllers in the case that we are changing collection views or setting for + // the first time + _registeredCellIdentifiers = [NSMutableSet new]; + _registeredNibNames = [NSMutableSet new]; + _registeredSupplementaryViewIdentifiers = [NSMutableSet new]; + _registeredSupplementaryViewNibNames = [NSMutableSet new]; + + const BOOL settingFirstCollectionView = _collectionView == nil; + + _collectionView = collectionView; + _collectionView.dataSource = self; + + if (@available(iOS 10.0, tvOS 10, *)) { + _collectionView.prefetchingEnabled = NO; + } + + [_collectionView.collectionViewLayout ig_hijackLayoutInteractiveReorderingMethodForAdapter:self]; + [_collectionView.collectionViewLayout invalidateLayout]; + + [self _updateCollectionViewDelegate]; + + // only construct + if (!IGListExperimentEnabled(self.experiments, IGListExperimentGetCollectionViewAtUpdate) + || settingFirstCollectionView) { + [self _updateAfterPublicSettingsChange]; + } + } +} + +- (void)setDataSource:(id)dataSource { + if (_dataSource != dataSource) { + _dataSource = dataSource; + [self _updateAfterPublicSettingsChange]; + } +} + +// reset and configure the delegate proxy whenever this property is set +- (void)setCollectionViewDelegate:(id)collectionViewDelegate { + IGAssertMainThread(); + IGAssert(![collectionViewDelegate conformsToProtocol:@protocol(UICollectionViewDelegateFlowLayout)], + @"UICollectionViewDelegateFlowLayout conformance is automatically handled by IGListAdapter."); + + if (_collectionViewDelegate != collectionViewDelegate) { + _collectionViewDelegate = collectionViewDelegate; + [self _createProxyAndUpdateCollectionViewDelegate]; + } +} + +- (void)setScrollViewDelegate:(id)scrollViewDelegate { + IGAssertMainThread(); + + if (_scrollViewDelegate != scrollViewDelegate) { + _scrollViewDelegate = scrollViewDelegate; + [self _createProxyAndUpdateCollectionViewDelegate]; + } +} + +- (void)_updateAfterPublicSettingsChange { + id dataSource = _dataSource; + if (_collectionView != nil && dataSource != nil) { + NSArray *uniqueObjects = objectsWithDuplicateIdentifiersRemoved([dataSource objectsForListAdapter:self]); + [self _updateObjects:uniqueObjects dataSource:dataSource]; + } +} + +- (void)_createProxyAndUpdateCollectionViewDelegate { + // there is a known bug with accessibility and using an NSProxy as the delegate that will cause EXC_BAD_ACCESS + // when voiceover is enabled. it will hold an unsafe ref to the delegate + _collectionView.delegate = nil; + + self.delegateProxy = [[IGListAdapterProxy alloc] initWithCollectionViewTarget:_collectionViewDelegate + scrollViewTarget:_scrollViewDelegate + interceptor:self]; + [self _updateCollectionViewDelegate]; +} + +- (void)_updateCollectionViewDelegate { + // set up the delegate to the proxy so the adapter can intercept events + // default to the adapter simply being the delegate + _collectionView.delegate = (id)self.delegateProxy ?: self; +} + + +#pragma mark - Scrolling + +- (void)scrollToObject:(id)object + supplementaryKinds:(NSArray *)supplementaryKinds + scrollDirection:(UICollectionViewScrollDirection)scrollDirection + scrollPosition:(UICollectionViewScrollPosition)scrollPosition + animated:(BOOL)animated { + IGAssertMainThread(); + IGParameterAssert(object != nil); + + const NSInteger section = [self sectionForObject:object]; + if (section == NSNotFound) { + return; + } + + UICollectionView *collectionView = self.collectionView; + const BOOL avoidLayout = IGListExperimentEnabled(self.experiments, IGListExperimentAvoidLayoutOnScrollToObject); + + // Experiment with skipping the forced layout to avoid creating off-screen cells. + // Calling [collectionView layoutIfNeeded] creates the current visible cells that will no longer be visible after the scroll. + // We can avoid that by asking the UICollectionView (not the layout object) for the attributes. So if the attributes are not + // ready, the UICollectionView will call -prepareLayout, return the attributes, but doesn't generate the cells just yet. + if (!avoidLayout) { + [collectionView setNeedsLayout]; + [collectionView layoutIfNeeded]; + } + + NSIndexPath *indexPathFirstElement = [NSIndexPath indexPathForItem:0 inSection:section]; + + // collect the layout attributes for the cell and supplementary views for the first index + // this will break if there are supplementary views beyond item 0 + NSMutableArray *attributes = nil; + + const NSInteger numberOfItems = [collectionView numberOfItemsInSection:section]; + if (numberOfItems > 0) { + attributes = [self _layoutAttributesForItemAndSupplementaryViewAtIndexPath:indexPathFirstElement + supplementaryKinds:supplementaryKinds].mutableCopy; + + if (numberOfItems > 1) { + NSIndexPath *indexPathLastElement = [NSIndexPath indexPathForItem:(numberOfItems - 1) inSection:section]; + UICollectionViewLayoutAttributes *lastElementattributes = [self _layoutAttributesForItemAndSupplementaryViewAtIndexPath:indexPathLastElement + supplementaryKinds:supplementaryKinds].firstObject; + if (lastElementattributes != nil) { + [attributes addObject:lastElementattributes]; + } + } + } else { + NSMutableArray *supplementaryAttributes = [NSMutableArray new]; + for (NSString* supplementaryKind in supplementaryKinds) { + UICollectionViewLayoutAttributes *supplementaryAttribute = [self _layoutAttributesForSupplementaryViewOfKind:supplementaryKind atIndexPath:indexPathFirstElement]; + if (supplementaryAttribute != nil) { + [supplementaryAttributes addObject: supplementaryAttribute]; + } + } + attributes = supplementaryAttributes; + } + + CGFloat offsetMin = 0.0; + CGFloat offsetMax = 0.0; + for (UICollectionViewLayoutAttributes *attribute in attributes) { + const CGRect frame = attribute.frame; + CGFloat originMin; + CGFloat endMax; + switch (scrollDirection) { + case UICollectionViewScrollDirectionHorizontal: + originMin = CGRectGetMinX(frame); + endMax = CGRectGetMaxX(frame); + break; + case UICollectionViewScrollDirectionVertical: + originMin = CGRectGetMinY(frame); + endMax = CGRectGetMaxY(frame); + break; + } + + // find the minimum origin value of all the layout attributes + if (attribute == attributes.firstObject || originMin < offsetMin) { + offsetMin = originMin; + } + // find the maximum end value of all the layout attributes + if (attribute == attributes.firstObject || endMax > offsetMax) { + offsetMax = endMax; + } + } + + const CGFloat offsetMid = (offsetMin + offsetMax) / 2.0; + const CGFloat collectionViewWidth = collectionView.bounds.size.width; + const CGFloat collectionViewHeight = collectionView.bounds.size.height; + const UIEdgeInsets contentInset = collectionView.ig_contentInset; + CGPoint contentOffset = collectionView.contentOffset; + switch (scrollDirection) { + case UICollectionViewScrollDirectionHorizontal: { + switch (scrollPosition) { + case UICollectionViewScrollPositionRight: + contentOffset.x = offsetMax - collectionViewWidth + contentInset.right; + break; + case UICollectionViewScrollPositionCenteredHorizontally: { + const CGFloat insets = (contentInset.left - contentInset.right) / 2.0; + contentOffset.x = offsetMid - collectionViewWidth / 2.0 - insets; + break; + } + case UICollectionViewScrollPositionLeft: + case UICollectionViewScrollPositionNone: + case UICollectionViewScrollPositionTop: + case UICollectionViewScrollPositionBottom: + case UICollectionViewScrollPositionCenteredVertically: + contentOffset.x = offsetMin - contentInset.left; + break; + } + const CGFloat maxOffsetX = collectionView.contentSize.width - collectionView.frame.size.width + contentInset.right; + const CGFloat minOffsetX = -contentInset.left; + contentOffset.x = MIN(contentOffset.x, maxOffsetX); + contentOffset.x = MAX(contentOffset.x, minOffsetX); + break; + } + case UICollectionViewScrollDirectionVertical: { + switch (scrollPosition) { + case UICollectionViewScrollPositionBottom: + contentOffset.y = offsetMax - collectionViewHeight + contentInset.bottom; + break; + case UICollectionViewScrollPositionCenteredVertically: { + const CGFloat insets = (contentInset.top - contentInset.bottom) / 2.0; + contentOffset.y = offsetMid - collectionViewHeight / 2.0 - insets; + break; + } + case UICollectionViewScrollPositionTop: + case UICollectionViewScrollPositionNone: + case UICollectionViewScrollPositionLeft: + case UICollectionViewScrollPositionRight: + case UICollectionViewScrollPositionCenteredHorizontally: + contentOffset.y = offsetMin - contentInset.top; + break; + } + CGFloat maxHeight; + if (avoidLayout) { + // If we don't call [collectionView layoutIfNeeded], the collectionView.contentSize does not get updated. + // So lets use the layout object, since it should have been updated by now. + maxHeight = collectionView.collectionViewLayout.collectionViewContentSize.height; + } else { + maxHeight = collectionView.contentSize.height; + } + const CGFloat maxOffsetY = maxHeight - collectionView.frame.size.height + contentInset.bottom; + const CGFloat minOffsetY = -contentInset.top; + contentOffset.y = MIN(contentOffset.y, maxOffsetY); + contentOffset.y = MAX(contentOffset.y, minOffsetY); + break; + } + } + + [collectionView setContentOffset:contentOffset animated:animated]; +} + +#pragma mark - Editing + +- (void)performUpdatesAnimated:(BOOL)animated completion:(IGListUpdaterCompletion)completion { + IGAssertMainThread(); + + id dataSource = self.dataSource; + UICollectionView *collectionView = self.collectionView; + if (dataSource == nil || collectionView == nil) { + IGLKLog(@"Warning: Your call to %s is ignored as dataSource or collectionView haven't been set.", __PRETTY_FUNCTION__); + if (completion) { + completion(NO); + } + return; + } + + NSArray *fromObjects = self.sectionMap.objects; + + IGListToObjectBlock toObjectsBlock; + __weak __typeof__(self) weakSelf = self; + if (IGListExperimentEnabled(self.experiments, IGListExperimentDeferredToObjectCreation)) { + toObjectsBlock = ^NSArray *{ + __typeof__(self) strongSelf = weakSelf; + if (strongSelf == nil) { + return nil; + } + return [dataSource objectsForListAdapter:strongSelf]; + }; + } else { + NSArray *newObjects = [dataSource objectsForListAdapter:self]; + toObjectsBlock = ^NSArray *{ + return newObjects; + }; + } + + [self _enterBatchUpdates]; + [self.updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock] + fromObjects:fromObjects + toObjectsBlock:toObjectsBlock + animated:animated + objectTransitionBlock:^(NSArray *toObjects) { + // temporarily capture the item map that we are transitioning from in case + // there are any item deletes at the same + weakSelf.previousSectionMap = [weakSelf.sectionMap copy]; + + [weakSelf _updateObjects:toObjects dataSource:dataSource]; + } completion:^(BOOL finished) { + // release the previous items + weakSelf.previousSectionMap = nil; + + [weakSelf _notifyDidUpdate:IGListAdapterUpdateTypePerformUpdates animated:animated]; + if (completion) { + completion(finished); + } + [weakSelf _exitBatchUpdates]; + }]; +} + +- (void)reloadDataWithCompletion:(nullable IGListUpdaterCompletion)completion { + IGAssertMainThread(); + + id dataSource = self.dataSource; + UICollectionView *collectionView = self.collectionView; + if (dataSource == nil || collectionView == nil) { + IGLKLog(@"Warning: Your call to %s is ignored as dataSource or collectionView haven't been set.", __PRETTY_FUNCTION__); + if (completion) { + completion(NO); + } + return; + } + + NSArray *uniqueObjects = objectsWithDuplicateIdentifiersRemoved([dataSource objectsForListAdapter:self]); + + __weak __typeof__(self) weakSelf = self; + [self.updater reloadDataWithCollectionViewBlock:[self _collectionViewBlock] + reloadUpdateBlock:^{ + // purge all section controllers from the item map so that they are regenerated + [weakSelf.sectionMap reset]; + [weakSelf _updateObjects:uniqueObjects dataSource:dataSource]; + } completion:^(BOOL finished) { + [weakSelf _notifyDidUpdate:IGListAdapterUpdateTypeReloadData animated:NO]; + if (completion) { + completion(finished); + } + }]; +} + +- (void)reloadObjects:(NSArray *)objects { + IGAssertMainThread(); + IGParameterAssert(objects); + + NSMutableIndexSet *sections = [NSMutableIndexSet new]; + + // use the item map based on whether or not we're in an update block + IGListSectionMap *map = [self _sectionMapUsingPreviousIfInUpdateBlock:YES]; + + for (id object in objects) { + // look up the item using the map's lookup function. might not be the same item + const NSInteger section = [map sectionForObject:object]; + const BOOL notFound = section == NSNotFound; + if (notFound) { + continue; + } + [sections addIndex:section]; + + // reverse lookup the item using the section. if the pointer has changed the trigger update events and swap items + if (object != [map objectForSection:section]) { + [map updateObject:object]; + [[map sectionControllerForSection:section] didUpdateToObject:object]; + } + } + + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Tried to reload the adapter without a collection view"); + + [self.updater reloadCollectionView:collectionView sections:sections]; +} + +- (void)addUpdateListener:(id)updateListener { + IGAssertMainThread(); + IGParameterAssert(updateListener != nil); + + [_updateListeners addObject:updateListener]; +} + +- (void)removeUpdateListener:(id)updateListener { + IGAssertMainThread(); + IGParameterAssert(updateListener != nil); + + [_updateListeners removeObject:updateListener]; +} + +- (void)_notifyDidUpdate:(IGListAdapterUpdateType)update animated:(BOOL)animated { + for (id listener in _updateListeners) { + [listener listAdapter:self didFinishUpdate:update animated:animated]; + } +} + + +#pragma mark - List Items & Sections + +- (nullable IGListSectionController *)sectionControllerForSection:(NSInteger)section { + IGAssertMainThread(); + + return [self.sectionMap sectionControllerForSection:section]; +} + +- (NSInteger)sectionForSectionController:(IGListSectionController *)sectionController { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + + return [self.sectionMap sectionForSectionController:sectionController]; +} + +- (IGListSectionController *)sectionControllerForObject:(id)object { + IGAssertMainThread(); + IGParameterAssert(object != nil); + + return [self.sectionMap sectionControllerForObject:object]; +} + +- (id)objectForSectionController:(IGListSectionController *)sectionController { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + + const NSInteger section = [self.sectionMap sectionForSectionController:sectionController]; + return [self.sectionMap objectForSection:section]; +} + +- (id)objectAtSection:(NSInteger)section { + IGAssertMainThread(); + + return [self.sectionMap objectForSection:section]; +} + +- (NSInteger)sectionForObject:(id)item { + IGAssertMainThread(); + IGParameterAssert(item != nil); + + return [self.sectionMap sectionForObject:item]; +} + +- (NSArray *)objects { + IGAssertMainThread(); + + return self.sectionMap.objects; +} + +- (id)_supplementaryViewSourceAtIndexPath:(NSIndexPath *)indexPath { + IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; + return [sectionController supplementaryViewSource]; +} + +- (NSArray *)visibleSectionControllers { + IGAssertMainThread(); + return [[self.displayHandler visibleListSections] allObjects]; +} + +- (NSArray *)visibleObjects { + IGAssertMainThread(); + NSArray *visibleCells = [self.collectionView visibleCells]; + NSMutableSet *visibleObjects = [NSMutableSet new]; + for (UICollectionViewCell *cell in visibleCells) { + IGListSectionController *sectionController = [self sectionControllerForView:cell]; + IGAssert(sectionController != nil, @"Section controller nil for cell %@", cell); + if (sectionController != nil) { + const NSInteger section = [self sectionForSectionController:sectionController]; + id object = [self objectAtSection:section]; + IGAssert(object != nil, @"Object not found for section controller %@ at section %li", sectionController, (long)section); + if (object != nil) { + [visibleObjects addObject:object]; + } + } + } + return [visibleObjects allObjects]; +} + +- (NSArray *)visibleCellsForObject:(id)object { + IGAssertMainThread(); + IGParameterAssert(object != nil); + + const NSInteger section = [self.sectionMap sectionForObject:object]; + if (section == NSNotFound) { + return [NSArray new]; + } + + NSArray *visibleCells = [self.collectionView visibleCells]; + UICollectionView *collectionView = self.collectionView; + NSPredicate *controllerPredicate = [NSPredicate predicateWithBlock:^BOOL(UICollectionViewCell* cell, NSDictionary* bindings) { + NSIndexPath *indexPath = [collectionView indexPathForCell:cell]; + return indexPath.section == section; + }]; + + return [visibleCells filteredArrayUsingPredicate:controllerPredicate]; +} + + +#pragma mark - Layout + +- (CGSize)sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + IGAssertMainThread(); + id performanceDelegate = self.performanceDelegate; + [performanceDelegate listAdapterWillCallSize:self]; + + IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; + const CGSize size = [sectionController sizeForItemAtIndex:indexPath.item]; + const CGSize positiveSize = CGSizeMake(MAX(size.width, 0.0), MAX(size.height, 0.0)); + + [performanceDelegate listAdapter:self didCallSizeOnSectionController:sectionController atIndex:indexPath.item]; + return positiveSize; +} + +- (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + IGAssertMainThread(); + id supplementaryViewSource = [self _supplementaryViewSourceAtIndexPath:indexPath]; + if ([[supplementaryViewSource supportedElementKinds] containsObject:elementKind]) { + const CGSize size = [supplementaryViewSource sizeForSupplementaryViewOfKind:elementKind atIndex:indexPath.item]; + return CGSizeMake(MAX(size.width, 0.0), MAX(size.height, 0.0)); + } + return CGSizeZero; +} + + +#pragma mark - Private API + +- (IGListCollectionViewBlock)_collectionViewBlock { + if (IGListExperimentEnabled(self.experiments, IGListExperimentGetCollectionViewAtUpdate)) { + __weak __typeof__(self) weakSelf = self; + return ^UICollectionView *{ return weakSelf.collectionView; }; + } else { + __weak UICollectionView *collectionView = _collectionView; + return ^UICollectionView *{ return collectionView; }; + } +} + +// this method is what updates the "source of truth" +// this should only be called just before the collection view is updated +- (void)_updateObjects:(NSArray *)objects dataSource:(id)dataSource { + IGParameterAssert(dataSource != nil); + +#if DEBUG + for (id object in objects) { + IGAssert([object isEqualToDiffableObject:object], @"Object instance %@ not equal to itself. This will break infra map tables.", object); + } +#endif + + NSMutableArray *sectionControllers = [NSMutableArray new]; + NSMutableArray *validObjects = [NSMutableArray new]; + + IGListSectionMap *map = self.sectionMap; + + // collect items that have changed since the last update + NSMutableSet *updatedObjects = [NSMutableSet new]; + + // push the view controller and collection context into a local thread container so they are available on init + // for IGListSectionController subclasses after calling [super init] + IGListSectionControllerPushThread(self.viewController, self); + + for (id object in objects) { + // infra checks to see if a controller exists + IGListSectionController *sectionController = [map sectionControllerForObject:object]; + + // if not, query the data source for a new one + if (sectionController == nil) { + sectionController = [dataSource listAdapter:self sectionControllerForObject:object]; + } + + if (sectionController == nil) { + IGLKLog(@"WARNING: Ignoring nil section controller returned by data source %@ for object %@.", + dataSource, object); + continue; + } + + // in case the section controller was created outside of -listAdapter:sectionControllerForObject: + sectionController.collectionContext = self; + sectionController.viewController = self.viewController; + + // check if the item has changed instances or is new + const NSInteger oldSection = [map sectionForObject:object]; + if (oldSection == NSNotFound || [map objectForSection:oldSection] != object) { + [updatedObjects addObject:object]; + } + + [sectionControllers addObject:sectionController]; + [validObjects addObject:object]; + } + +#if DEBUG + IGAssert([NSSet setWithArray:sectionControllers].count == sectionControllers.count, + @"Section controllers array is not filled with unique objects; section controllers are being reused"); +#endif + + // clear the view controller and collection context + IGListSectionControllerPopThread(); + + [map updateWithObjects:validObjects sectionControllers:sectionControllers]; + + // now that the maps have been created and contexts are assigned, we consider the section controller "fully loaded" + for (id object in updatedObjects) { + [[map sectionControllerForObject:object] didUpdateToObject:object]; + } + + NSInteger itemCount = 0; + for (IGListSectionController *sectionController in sectionControllers) { + itemCount += [sectionController numberOfItems]; + } + + [self _updateBackgroundViewShouldHide:itemCount > 0]; +} + +- (void)_updateBackgroundViewShouldHide:(BOOL)shouldHide { + if (self.isInUpdateBlock) { + return; // will be called again when update block completes + } + UIView *backgroundView = [self.dataSource emptyViewForListAdapter:self]; + // don't do anything if the client is using the same view + if (backgroundView != _collectionView.backgroundView) { + // collection view will just stack the background views underneath each other if we do not remove the previous + // one first. also fine if it is nil + [_collectionView.backgroundView removeFromSuperview]; + _collectionView.backgroundView = backgroundView; + } + _collectionView.backgroundView.hidden = shouldHide; +} + +- (BOOL)_itemCountIsZero { + __block BOOL isZero = YES; + [self.sectionMap enumerateUsingBlock:^(id _Nonnull object, IGListSectionController * _Nonnull sectionController, NSInteger section, BOOL * _Nonnull stop) { + if (sectionController.numberOfItems > 0) { + isZero = NO; + *stop = YES; + } + }]; + return isZero; +} + +- (IGListSectionMap *)_sectionMapUsingPreviousIfInUpdateBlock:(BOOL)usePreviousMapIfInUpdateBlock { + // if we are inside an update block, we may have to use the /previous/ item map for some operations + IGListSectionMap *previousSectionMap = self.previousSectionMap; + if (usePreviousMapIfInUpdateBlock && self.isInUpdateBlock && previousSectionMap != nil) { + return previousSectionMap; + } else { + return self.sectionMap; + } +} + +- (NSArray *)indexPathsFromSectionController:(IGListSectionController *)sectionController + indexes:(NSIndexSet *)indexes + usePreviousIfInUpdateBlock:(BOOL)usePreviousIfInUpdateBlock { + NSMutableArray *indexPaths = [NSMutableArray new]; + + IGListSectionMap *map = [self _sectionMapUsingPreviousIfInUpdateBlock:usePreviousIfInUpdateBlock]; + const NSInteger section = [map sectionForSectionController:sectionController]; + if (section != NSNotFound) { + [indexes enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + [indexPaths addObject:[NSIndexPath indexPathForItem:idx inSection:section]]; + }]; + } + return indexPaths; +} + +- (NSIndexPath *)indexPathForSectionController:(IGListSectionController *)controller + index:(NSInteger)index + usePreviousIfInUpdateBlock:(BOOL)usePreviousIfInUpdateBlock { + IGListSectionMap *map = [self _sectionMapUsingPreviousIfInUpdateBlock:usePreviousIfInUpdateBlock]; + const NSInteger section = [map sectionForSectionController:controller]; + if (section == NSNotFound) { + return nil; + } else { + return [NSIndexPath indexPathForItem:index inSection:section]; + } +} + +- (NSArray *)_layoutAttributesForItemAndSupplementaryViewAtIndexPath:(NSIndexPath *)indexPath + supplementaryKinds:(NSArray *)supplementaryKinds { + NSMutableArray *attributes = [NSMutableArray new]; + + UICollectionViewLayoutAttributes *cellAttributes = [self _layoutAttributesForItemAtIndexPath:indexPath]; + if (cellAttributes) { + [attributes addObject:cellAttributes]; + } + + for (NSString *kind in supplementaryKinds) { + UICollectionViewLayoutAttributes *supplementaryAttributes = [self _layoutAttributesForSupplementaryViewOfKind:kind atIndexPath:indexPath]; + if (supplementaryAttributes) { + [attributes addObject:supplementaryAttributes]; + } + } + + return attributes; +} + +- (nullable UICollectionViewLayoutAttributes *)_layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { + if (IGListExperimentEnabled(self.experiments, IGListExperimentAvoidLayoutOnScrollToObject)) { + return [self.collectionView layoutAttributesForItemAtIndexPath:indexPath]; + } else { + return [self.collectionView.collectionViewLayout layoutAttributesForItemAtIndexPath:indexPath]; + } +} + +- (nullable UICollectionViewLayoutAttributes *)_layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind + atIndexPath:(NSIndexPath *)indexPath { + if (IGListExperimentEnabled(self.experiments, IGListExperimentAvoidLayoutOnScrollToObject)) { + return [self.collectionView layoutAttributesForSupplementaryElementOfKind:elementKind atIndexPath:indexPath]; + } else { + return [self.collectionView.collectionViewLayout layoutAttributesForSupplementaryViewOfKind:elementKind atIndexPath:indexPath]; + } +} + +- (void)mapView:(UICollectionReusableView *)view toSectionController:(IGListSectionController *)sectionController { + IGAssertMainThread(); + IGParameterAssert(view != nil); + IGParameterAssert(sectionController != nil); + [_viewSectionControllerMap setObject:sectionController forKey:view]; +} + +- (nullable IGListSectionController *)sectionControllerForView:(UICollectionReusableView *)view { + IGAssertMainThread(); + return [_viewSectionControllerMap objectForKey:view]; +} + +- (void)removeMapForView:(UICollectionReusableView *)view { + IGAssertMainThread(); + [_viewSectionControllerMap removeObjectForKey:view]; +} + +- (void)_deferBlockBetweenBatchUpdates:(void (^)(void))block { + IGAssertMainThread(); + if (_queuedCompletionBlocks == nil) { + block(); + } else { + [_queuedCompletionBlocks addObject:block]; + } +} + +- (void)_enterBatchUpdates { + _queuedCompletionBlocks = [NSMutableArray new]; +} + +- (void)_exitBatchUpdates { + NSArray *blocks = [_queuedCompletionBlocks copy]; + _queuedCompletionBlocks = nil; + for (void (^block)(void) in blocks) { + block(); + } +} + +#pragma mark - UIScrollViewDelegate + +- (void)scrollViewDidScroll:(UIScrollView *)scrollView { + id performanceDelegate = self.performanceDelegate; + [performanceDelegate listAdapterWillCallScroll:self]; + + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id scrollViewDelegate = self.scrollViewDelegate; + if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidScroll:)]) { + [scrollViewDelegate scrollViewDidScroll:scrollView]; + } + NSArray *visibleSectionControllers = [self visibleSectionControllers]; + for (IGListSectionController *sectionController in visibleSectionControllers) { + [[sectionController scrollDelegate] listAdapter:self didScrollSectionController:sectionController]; + } + + [performanceDelegate listAdapter:self didCallScroll:scrollView]; +} + +- (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id scrollViewDelegate = self.scrollViewDelegate; + if ([scrollViewDelegate respondsToSelector:@selector(scrollViewWillBeginDragging:)]) { + [scrollViewDelegate scrollViewWillBeginDragging:scrollView]; + } + NSArray *visibleSectionControllers = [self visibleSectionControllers]; + for (IGListSectionController *sectionController in visibleSectionControllers) { + [[sectionController scrollDelegate] listAdapter:self willBeginDraggingSectionController:sectionController]; + } +} + +- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate { + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id scrollViewDelegate = self.scrollViewDelegate; + if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidEndDragging:willDecelerate:)]) { + [scrollViewDelegate scrollViewDidEndDragging:scrollView willDecelerate:decelerate]; + } + NSArray *visibleSectionControllers = [self visibleSectionControllers]; + for (IGListSectionController *sectionController in visibleSectionControllers) { + [[sectionController scrollDelegate] listAdapter:self didEndDraggingSectionController:sectionController willDecelerate:decelerate]; + } +} + +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id scrollViewDelegate = self.scrollViewDelegate; + if ([scrollViewDelegate respondsToSelector:@selector(scrollViewDidEndDecelerating:)]) { + [scrollViewDelegate scrollViewDidEndDecelerating:scrollView]; + } + NSArray *visibleSectionControllers = [self visibleSectionControllers]; + for (IGListSectionController *sectionController in visibleSectionControllers) { + id scrollDelegate = [sectionController scrollDelegate]; + if ([scrollDelegate respondsToSelector:@selector(listAdapter:didEndDeceleratingSectionController:)]) { + [scrollDelegate listAdapter:self didEndDeceleratingSectionController:sectionController]; + } + } +} + +#pragma mark - IGListCollectionContext + +- (CGSize)containerSize { + return self.collectionView.bounds.size; +} + +- (UIEdgeInsets)containerInset { + return self.collectionView.contentInset; +} + +- (UIEdgeInsets)adjustedContainerInset { + return self.collectionView.ig_contentInset; +} + +- (CGSize)insetContainerSize { + UICollectionView *collectionView = self.collectionView; + return UIEdgeInsetsInsetRect(collectionView.bounds, collectionView.ig_contentInset).size; +} + +- (IGListCollectionScrollingTraits)scrollingTraits { + UICollectionView *collectionView = self.collectionView; + return (IGListCollectionScrollingTraits) { + .isTracking = collectionView.isTracking, + .isDragging = collectionView.isDragging, + .isDecelerating = collectionView.isDecelerating, + }; +} + +- (CGSize)containerSizeForSectionController:(IGListSectionController *)sectionController { + const UIEdgeInsets inset = sectionController.inset; + return CGSizeMake(self.containerSize.width - inset.left - inset.right, + self.containerSize.height - inset.top - inset.bottom); +} + +- (NSInteger)indexForCell:(UICollectionViewCell *)cell sectionController:(nonnull IGListSectionController *)sectionController { + IGAssertMainThread(); + IGParameterAssert(cell != nil); + IGParameterAssert(sectionController != nil); + NSIndexPath *indexPath = [self.collectionView indexPathForCell:cell]; + IGAssert(indexPath == nil + || indexPath.section == [self sectionForSectionController:sectionController], + @"Requesting a cell from another section controller is not allowed."); + return indexPath != nil ? indexPath.item : NSNotFound; +} + +- (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index + sectionController:(IGListSectionController *)sectionController { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + + // if this is accessed while a cell is being dequeued or displaying working range elements, just return nil + if (_isDequeuingCell || _isSendingWorkingRangeDisplayUpdates) { + return nil; + } + + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:YES]; + // prevent querying the collection view if it isn't fully reloaded yet for the current data set + if (indexPath != nil + && indexPath.section < [self.collectionView numberOfSections]) { + // only return a cell if it belongs to the section controller + // this association is created in -collectionView:cellForItemAtIndexPath: + UICollectionViewCell *cell = [self.collectionView cellForItemAtIndexPath:indexPath]; + if ([self sectionControllerForView:cell] == sectionController) { + return cell; + } + } + return nil; +} + +- (NSArray *)visibleCellsForSectionController:(IGListSectionController *)sectionController { + NSMutableArray *cells = [NSMutableArray new]; + UICollectionView *collectionView = self.collectionView; + NSArray *visibleCells = [collectionView visibleCells]; + const NSInteger section = [self sectionForSectionController:sectionController]; + for (UICollectionViewCell *cell in visibleCells) { + if ([collectionView indexPathForCell:cell].section == section) { + [cells addObject:cell]; + } + } + return cells; +} + +- (NSArray *)visibleIndexPathsForSectionController:(IGListSectionController *) sectionController { + NSMutableArray *paths = [NSMutableArray new]; + UICollectionView *collectionView = self.collectionView; + NSArray *visiblePaths = [collectionView indexPathsForVisibleItems]; + const NSInteger section = [self sectionForSectionController:sectionController]; + for (NSIndexPath *path in visiblePaths) { + if (path.section == section) { + [paths addObject:path]; + } + } + return paths; +} + +- (void)deselectItemAtIndex:(NSInteger)index + sectionController:(IGListSectionController *)sectionController + animated:(BOOL)animated { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + [self.collectionView deselectItemAtIndexPath:indexPath animated:animated]; +} + +- (void)selectItemAtIndex:(NSInteger)index + sectionController:(IGListSectionController *)sectionController + animated:(BOOL)animated + scrollPosition:(UICollectionViewScrollPosition)scrollPosition { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + [self.collectionView selectItemAtIndexPath:indexPath animated:animated scrollPosition:scrollPosition]; +} + +- (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass + withReuseIdentifier:(NSString *)reuseIdentifier + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + IGParameterAssert(cellClass != nil); + IGParameterAssert(index >= 0); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Dequeueing cell of class %@ with reuseIdentifier %@ from section controller %@ without a collection view at index %li", NSStringFromClass(cellClass), reuseIdentifier, sectionController, (long)index); + NSString *identifier = IGListReusableViewIdentifier(cellClass, nil, reuseIdentifier); + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + if (![self.registeredCellIdentifiers containsObject:identifier]) { + [self.registeredCellIdentifiers addObject:identifier]; + [collectionView registerClass:cellClass forCellWithReuseIdentifier:identifier]; + } + return [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath]; +} + +- (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index { + return [self dequeueReusableCellOfClass:cellClass withReuseIdentifier:nil forSectionController:sectionController atIndex:index]; +} + +- (__kindof UICollectionViewCell *)dequeueReusableCellFromStoryboardWithIdentifier:(NSString *)identifier + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + IGParameterAssert(identifier.length > 0); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Reloading adapter without a collection view."); + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + return [collectionView dequeueReusableCellWithReuseIdentifier:identifier forIndexPath:indexPath]; +} + +- (UICollectionViewCell *)dequeueReusableCellWithNibName:(NSString *)nibName + bundle:(NSBundle *)bundle + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index { + IGAssertMainThread(); + IGParameterAssert([nibName length] > 0); + IGParameterAssert(sectionController != nil); + IGParameterAssert(index >= 0); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Dequeueing cell with nib name %@ and bundle %@ from section controller %@ without a collection view at index %li.", nibName, bundle, sectionController, (long)index); + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + if (![self.registeredNibNames containsObject:nibName]) { + [self.registeredNibNames addObject:nibName]; + UINib *nib = [UINib nibWithNibName:nibName bundle:bundle]; + [collectionView registerNib:nib forCellWithReuseIdentifier:nibName]; + } + return [collectionView dequeueReusableCellWithReuseIdentifier:nibName forIndexPath:indexPath]; +} + +- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind + forSectionController:(IGListSectionController *)sectionController + class:(Class)viewClass + atIndex:(NSInteger)index { + IGAssertMainThread(); + IGParameterAssert(elementKind.length > 0); + IGParameterAssert(sectionController != nil); + IGParameterAssert(viewClass != nil); + IGParameterAssert(index >= 0); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Dequeueing cell of class %@ from section controller %@ without a collection view at index %li with supplementary view %@", NSStringFromClass(viewClass), sectionController, (long)index, elementKind); + NSString *identifier = IGListReusableViewIdentifier(viewClass, elementKind, nil); + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + if (![self.registeredSupplementaryViewIdentifiers containsObject:identifier]) { + [self.registeredSupplementaryViewIdentifiers addObject:identifier]; + [collectionView registerClass:viewClass forSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier]; + } + return [collectionView dequeueReusableSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier forIndexPath:indexPath]; +} + +- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewFromStoryboardOfKind:(NSString *)elementKind + withIdentifier:(NSString *)identifier + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index { + IGAssertMainThread(); + IGParameterAssert(elementKind.length > 0); + IGParameterAssert(identifier.length > 0); + IGParameterAssert(sectionController != nil); + IGParameterAssert(index >= 0); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Dequeueing Supplementary View from storyboard of kind %@ with identifier %@ for section controller %@ without a collection view at index %li", elementKind, identifier, sectionController, (long)index); + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + return [collectionView dequeueReusableSupplementaryViewOfKind:elementKind withReuseIdentifier:identifier forIndexPath:indexPath]; +} + +- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind + forSectionController:(IGListSectionController *)sectionController + nibName:(NSString *)nibName + bundle:(NSBundle *)bundle + atIndex:(NSInteger)index { + IGAssertMainThread(); + IGParameterAssert([nibName length] > 0); + IGParameterAssert([elementKind length] > 0); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Reloading adapter without a collection view."); + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + if (![self.registeredSupplementaryViewNibNames containsObject:nibName]) { + [self.registeredSupplementaryViewNibNames addObject:nibName]; + UINib *nib = [UINib nibWithNibName:nibName bundle:bundle]; + [collectionView registerNib:nib forSupplementaryViewOfKind:elementKind withReuseIdentifier:nibName]; + } + return [collectionView dequeueReusableSupplementaryViewOfKind:elementKind withReuseIdentifier:nibName forIndexPath:indexPath]; +} + +- (void)performBatchAnimated:(BOOL)animated updates:(void (^)(id))updates completion:(void (^)(BOOL))completion { + IGAssertMainThread(); + IGParameterAssert(updates != nil); + IGAssert(self.collectionView != nil, @"Performing batch updates without a collection view."); + + [self _enterBatchUpdates]; + __weak __typeof__(self) weakSelf = self; + [self.updater performUpdateWithCollectionViewBlock:[self _collectionViewBlock] animated:animated itemUpdates:^{ + weakSelf.isInUpdateBlock = YES; + // the adapter acts as the batch context with its API stripped to just the IGListBatchContext protocol + updates(weakSelf); + weakSelf.isInUpdateBlock = NO; + } completion: ^(BOOL finished) { + [weakSelf _updateBackgroundViewShouldHide:![weakSelf _itemCountIsZero]]; + [weakSelf _notifyDidUpdate:IGListAdapterUpdateTypeItemUpdates animated:animated]; + if (completion) { + completion(finished); + } + [weakSelf _exitBatchUpdates]; + }]; +} + +- (void)scrollToSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index + scrollPosition:(UICollectionViewScrollPosition)scrollPosition + animated:(BOOL)animated { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + + NSIndexPath *indexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + [self.collectionView scrollToItemAtIndexPath:indexPath atScrollPosition:scrollPosition animated:animated]; +} + +- (void)invalidateLayoutForSectionController:(IGListSectionController *)sectionController + completion:(void (^)(BOOL finished))completion{ + const NSInteger section = [self sectionForSectionController:sectionController]; + const NSInteger items = [_collectionView numberOfItemsInSection:section]; + + NSMutableArray *indexPaths = [NSMutableArray new]; + for (NSInteger item = 0; item < items; item++) { + [indexPaths addObject:[NSIndexPath indexPathForItem:item inSection:section]]; + } + + UICollectionViewLayout *layout = _collectionView.collectionViewLayout; + UICollectionViewLayoutInvalidationContext *context = [[[layout.class invalidationContextClass] alloc] init]; + [context invalidateItemsAtIndexPaths:indexPaths]; + + __weak __typeof__(_collectionView) weakCollectionView = _collectionView; + + // do not call -[UICollectionView performBatchUpdates:completion:] while already updating. defer it until completed. + [self _deferBlockBetweenBatchUpdates:^{ + [weakCollectionView performBatchUpdates:^{ + [layout invalidateLayoutWithContext:context]; + } completion:completion]; + }]; +} + +#pragma mark - IGListBatchContext + +- (void)reloadInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes { + IGAssertMainThread(); + IGParameterAssert(indexes != nil); + IGParameterAssert(sectionController != nil); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Tried to reload the adapter from %@ without a collection view at indexes %@.", sectionController, indexes); + + if (indexes.count == 0) { + return; + } + + /** + UICollectionView is not designed to support -reloadSections: or -reloadItemsAtIndexPaths: during batch updates. + Internally it appears to convert these operations to a delete+insert. However the transformation is too simple + in that it doesn't account for the item's section being moved (naturally or explicitly) and can queue animation + collisions. + + If you have an object at section 2 with 4 items and attempt to reload item at index 1, you would create an + NSIndexPath at section: 2, item: 1. Within -performBatchUpdates:, UICollectionView converts this to a delete + and insert at the same NSIndexPath. + + If a section were inserted at position 2, the original section 2 has naturally shifted to section 3. However, + the insert NSIndexPath is section: 2, item: 1. Now the UICollectionView has a section animation at section 2, + as well as an item insert animation at section: 2, item: 1, and it will throw an exception. + + IGListAdapter tracks the before/after mapping of section controllers to make precise NSIndexPath conversions. + */ + [indexes enumerateIndexesUsingBlock:^(NSUInteger index, BOOL *stop) { + NSIndexPath *fromIndexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:YES]; + NSIndexPath *toIndexPath = [self indexPathForSectionController:sectionController index:index usePreviousIfInUpdateBlock:NO]; + // index paths could be nil if a section controller is prematurely reloading or a reload was batched with + // the section controller being deleted + if (fromIndexPath != nil && toIndexPath != nil) { + [self.updater reloadItemInCollectionView:collectionView fromIndexPath:fromIndexPath toIndexPath:toIndexPath]; + } + }]; +} + +- (void)insertInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes { + IGAssertMainThread(); + IGParameterAssert(indexes != nil); + IGParameterAssert(sectionController != nil); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Inserting items from %@ without a collection view at indexes %@.", sectionController, indexes); + + if (indexes.count == 0) { + return; + } + + NSArray *indexPaths = [self indexPathsFromSectionController:sectionController indexes:indexes usePreviousIfInUpdateBlock:NO]; + [self.updater insertItemsIntoCollectionView:collectionView indexPaths:indexPaths]; + [self _updateBackgroundViewShouldHide:![self _itemCountIsZero]]; +} + +- (void)deleteInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes { + IGAssertMainThread(); + IGParameterAssert(indexes != nil); + IGParameterAssert(sectionController != nil); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Deleting items from %@ without a collection view at indexes %@.", sectionController, indexes); + + if (indexes.count == 0) { + return; + } + + NSArray *indexPaths = [self indexPathsFromSectionController:sectionController indexes:indexes usePreviousIfInUpdateBlock:YES]; + [self.updater deleteItemsFromCollectionView:collectionView indexPaths:indexPaths]; + [self _updateBackgroundViewShouldHide:![self _itemCountIsZero]]; +} + +- (void)invalidateLayoutInSectionController:(IGListSectionController *)sectionController atIndexes:(NSIndexSet *)indexes { + IGAssertMainThread(); + IGParameterAssert(indexes != nil); + IGParameterAssert(sectionController != nil); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Invalidating items from %@ without a collection view at indexes %@.", sectionController, indexes); + + if (indexes.count == 0) { + return; + } + + NSArray *indexPaths = [self indexPathsFromSectionController:sectionController indexes:indexes usePreviousIfInUpdateBlock:NO]; + UICollectionViewLayout *layout = collectionView.collectionViewLayout; + UICollectionViewLayoutInvalidationContext *context = [[[layout.class invalidationContextClass] alloc] init]; + [context invalidateItemsAtIndexPaths:indexPaths]; + [layout invalidateLayoutWithContext:context]; +} + +- (void)moveInSectionController:(IGListSectionController *)sectionController fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + IGParameterAssert(fromIndex >= 0); + IGParameterAssert(toIndex >= 0); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Moving items from %@ without a collection view from index %li to index %li.", + sectionController, (long)fromIndex, (long)toIndex); + + NSIndexPath *fromIndexPath = [self indexPathForSectionController:sectionController index:fromIndex usePreviousIfInUpdateBlock:YES]; + NSIndexPath *toIndexPath = [self indexPathForSectionController:sectionController index:toIndex usePreviousIfInUpdateBlock:NO]; + + if (fromIndexPath == nil || toIndexPath == nil) { + return; + } + + [self.updater moveItemInCollectionView:collectionView fromIndexPath:fromIndexPath toIndexPath:toIndexPath]; +} + +- (void)reloadSectionController:(IGListSectionController *)sectionController { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Reloading items from %@ without a collection view.", sectionController); + + IGListSectionMap *map = [self _sectionMapUsingPreviousIfInUpdateBlock:YES]; + const NSInteger section = [map sectionForSectionController:sectionController]; + if (section == NSNotFound) { + return; + } + + NSIndexSet *sections = [NSIndexSet indexSetWithIndex:section]; + [self.updater reloadCollectionView:collectionView sections:sections]; + [self _updateBackgroundViewShouldHide:![self _itemCountIsZero]]; +} + +- (void)moveSectionControllerInteractive:(IGListSectionController *)sectionController + fromIndex:(NSInteger)fromIndex + toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0) { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + IGParameterAssert(fromIndex >= 0); + IGParameterAssert(toIndex >= 0); + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Moving section %@ without a collection view from index %li to index %li.", + sectionController, (long)fromIndex, (long)toIndex); + IGAssert(self.moveDelegate != nil, @"Moving section %@ without a moveDelegate set", sectionController); + + if (fromIndex != toIndex) { + id dataSource = self.dataSource; + + NSArray *previousObjects = [self.sectionMap objects]; + + if (self.isLastInteractiveMoveToLastSectionIndex) { + self.isLastInteractiveMoveToLastSectionIndex = NO; + } + else if (fromIndex < toIndex) { + toIndex -= 1; + } + + NSMutableArray *mutObjects = [previousObjects mutableCopy]; + id object = [previousObjects objectAtIndex:fromIndex]; + [mutObjects removeObjectAtIndex:fromIndex]; + [mutObjects insertObject:object atIndex:toIndex]; + + NSArray *objects = [mutObjects copy]; + + // inform the data source to update its model + [self.moveDelegate listAdapter:self moveObject:object from:previousObjects to:objects]; + + // update our model based on that provided by the data source + NSArray> *updatedObjects = [dataSource objectsForListAdapter:self]; + [self _updateObjects:updatedObjects dataSource:dataSource]; + } + + // even if from and to index are equal, we need to perform the "move" + // iOS interactively moves items, not sections, so we might have actually moved the item + // to the end of the preceeding section or beginning of the following section + [self.updater moveSectionInCollectionView:collectionView fromIndex:fromIndex toIndex:toIndex]; +} + +- (void)moveInSectionControllerInteractive:(IGListSectionController *)sectionController + fromIndex:(NSInteger)fromIndex + toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0) { + IGAssertMainThread(); + IGParameterAssert(sectionController != nil); + IGParameterAssert(fromIndex >= 0); + IGParameterAssert(toIndex >= 0); + + [sectionController moveObjectFromIndex:fromIndex toIndex:toIndex]; +} + +- (void)revertInvalidInteractiveMoveFromIndexPath:(NSIndexPath *)sourceIndexPath + toIndexPath:(NSIndexPath *)destinationIndexPath NS_AVAILABLE_IOS(9_0) { + UICollectionView *collectionView = self.collectionView; + IGAssert(collectionView != nil, @"Reverting move without a collection view from %@ to %@.", + sourceIndexPath, destinationIndexPath); + + // revert by moving back in the opposite direction + [collectionView moveItemAtIndexPath:destinationIndexPath toIndexPath:sourceIndexPath]; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterDataSource.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterDataSource.h new file mode 100644 index 0000000..3118c94 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterDataSource.h @@ -0,0 +1,64 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@class IGListAdapter; +@class IGListSectionController; + +NS_ASSUME_NONNULL_BEGIN + +/** + Implement this protocol to provide data to an `IGListAdapter`. + */ +NS_SWIFT_NAME(ListAdapterDataSource) +@protocol IGListAdapterDataSource + +/** + Asks the data source for the objects to display in the list. + + @param listAdapter The list adapter requesting this information. + + @return An array of objects for the list. + */ +- (NSArray> *)objectsForListAdapter:(IGListAdapter *)listAdapter; + +/** + Asks the data source for a section controller for the specified object in the list. + + @param listAdapter The list adapter requesting this information. + @param object An object in the list. + + @return A new section controller instance that can be displayed in the list. + + @note New section controllers should be initialized here for objects when asked. You may pass any other data to + the section controller at this time. + + Section controllers are initialized for all objects whenever the `IGListAdapter` is created, updated, or reloaded. + Section controllers are reused when objects are moved or updated. Maintaining the `-[IGListDiffable diffIdentifier]` + guarantees this. + */ +- (IGListSectionController *)listAdapter:(IGListAdapter *)listAdapter sectionControllerForObject:(id)object; + +/** + Asks the data source for a view to use as the collection view background when the list is empty. + + @param listAdapter The list adapter requesting this information. + + @return A view to use as the collection view background, or `nil` if you don't want a background view. + + @note This method is called every time the list adapter is updated. You are free to return new views every time, + but for performance reasons you may want to retain the view and return it here. The infra is only responsible for + adding the background view and maintaining its visibility. + */ +- (nullable UIView *)emptyViewForListAdapter:(IGListAdapter *)listAdapter; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterDelegate.h new file mode 100644 index 0000000..0db122e --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterDelegate.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListAdapter; + +NS_ASSUME_NONNULL_BEGIN + +/** + Conform to `IGListAdapterDelegate` to receive display events for objects in a list. + */ +NS_SWIFT_NAME(ListAdapterDelegate) +@protocol IGListAdapterDelegate + +/** + Notifies the delegate that a list object is about to be displayed. + + @param listAdapter The list adapter sending this information. + @param object The object that will display. + @param index The index of the object in the list. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter willDisplayObject:(id)object atIndex:(NSInteger)index; + +/** + Notifies the delegate that a list object is no longer being displayed. + + @param listAdapter The list adapter sending this information. + @param object The object that ended display. + @param index The index of the object in the list. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingObject:(id)object atIndex:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterMoveDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterMoveDelegate.h new file mode 100644 index 0000000..e46812e --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterMoveDelegate.h @@ -0,0 +1,35 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListAdapter; + +NS_ASSUME_NONNULL_BEGIN + +/** + Conform to `IGListAdapterMoveDelegate` to receive interactive reordering requests. + */ +NS_SWIFT_NAME(ListAdapterMoveDelegate) +@protocol IGListAdapterMoveDelegate + +/** + Asks the delegate to move a section object as the result of interactive reordering. + + @param listAdapter The list adapter sending this information. + @param object the object that was moved + @param previousObjects The array of objects prior to the move. + @param objects The array of objects after the move. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter + moveObject:(id)object + from:(NSArray *)previousObjects + to:(NSArray *)objects; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterPerformanceDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterPerformanceDelegate.h new file mode 100644 index 0000000..aee811c --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterPerformanceDelegate.h @@ -0,0 +1,111 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListAdapter; +@class IGListSectionController; + +NS_ASSUME_NONNULL_BEGIN + +/** + `IGListAdapterPerformanceDelegate` can be used to measure cell dequeue, display, size, and scroll callbacks. + */ +NS_SWIFT_NAME(ListAdapterPerformanceDelegate) +@protocol IGListAdapterPerformanceDelegate + +/** + Will call `-[IGListAdapter collectionView:cellForItemAtIndexPath:]`. + + @param listAdapter The list adapter sending this information. + */ +- (void)listAdapterWillCallDequeueCell:(IGListAdapter *)listAdapter; + +/** + Did finish calling `-[IGListAdapter collectionView:cellForItemAtIndexPath:]`. + + @param listAdapter The list adapter sending this information. + @param cell A cell that was dequeued. + @param sectionController The section controller providing the cell. + @param index Item index of the cell. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didCallDequeueCell:(UICollectionViewCell *)cell onSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; + +/** + Will call `-[IGListAdapter collectionView:willDisplayCell:forItemAtIndexPath:]`. + + @param listAdapter The list adapter sending this information. + */ +- (void)listAdapterWillCallDisplayCell:(IGListAdapter *)listAdapter; + +/** + Did finish calling `-[IGListAdapter collectionView:willDisplayCell:forItemAtIndexPath:]`. + + @param listAdapter The list adapter sending this information. + @param cell A cell that will be displayed. + @param sectionController The section controller for that cell. + @param index Item index of the cell. + + @note Keep in mind this also includes calling the `IGListAdapter`'s collectionViewDelegate and workingRangeHandler. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didCallDisplayCell:(UICollectionViewCell *)cell onSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; + +/** + Will call `-[IGListAdapter collectionView:didEndDisplayingCell:forItemAtIndexPath:]`. + + @param listAdapter The list adapter sending this information. + */ +- (void)listAdapterWillCallEndDisplayCell:(IGListAdapter *)listAdapter; + +/** + Did finish calling `-[IGListAdapter collectionView:didEndDisplayingCell:forItemAtIndexPath:]`. + + @param listAdapter The list adapter sending this information. + @param cell A cell that was displayed. + @param sectionController The section controller for that cell. + @param index Item index of the cell. + + @note Keep in mind this also includes calling the `IGListAdapter`'s collectionViewDelegate and workingRangeHandler. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didCallEndDisplayCell:(UICollectionViewCell *)cell onSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; + +/** + Will call `-[IGListAdapter collectionView:collectionViewLayout:sizeForItemAtIndexPath:]`. + + @param listAdapter The list adapter sending this information. + */ +- (void)listAdapterWillCallSize:(IGListAdapter *)listAdapter; + +/** + Did finish calling `-[IGListAdapter collectionView:collectionViewLayout:sizeForItemAtIndexPath:]`. + + @param listAdapter The list adapter sending this information. + @param sectionController The section controller providing the size. + @param index Item index used to calculate the size. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didCallSizeOnSectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; + +/** + Will call `-[IGListAdapter scrollViewDidScroll:]`. + + @param listAdapter The list adapter sending this information. + */ +- (void)listAdapterWillCallScroll:(IGListAdapter *)listAdapter; + +/** + Did finish calling `-[IGListAdapter scrollViewDidScroll:]`. + + @param listAdapter The list adapter sending this information. + @param scrollView The scroll view backing the UICollectionView. + + @note Keep in mind this also includes calling the `IGListAdapter`'s scrollViewDelegate and all visible `IGListSectioControllers`. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didCallScroll:(UIScrollView *)scrollView; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdateListener.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdateListener.h new file mode 100644 index 0000000..bf698e5 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdateListener.h @@ -0,0 +1,56 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListAdapter; + +NS_ASSUME_NONNULL_BEGIN + +/** + The type of update that was performed by an `IGListAdapter`. + */ +NS_SWIFT_NAME(ListAdapterUpdateType) +typedef NS_ENUM(NSInteger, IGListAdapterUpdateType) { + /** + `-[IGListAdapter performUpdatesAnimated:completion:]` was executed. + */ + IGListAdapterUpdateTypePerformUpdates, + /** + `-[IGListAdapter reloadDataWithCompletion:]` was executed. + */ + IGListAdapterUpdateTypeReloadData, + /** + `-[IGListCollectionContext performBatchAnimated:updates:completion:]` was executed by an `IGListSectionController`. + */ + IGListAdapterUpdateTypeItemUpdates, +}; + +/** + Conform to this protocol to receive events about `IGListAdapter` updates. + */ +NS_SWIFT_NAME(ListAdapterUpdateListener) +@protocol IGListAdapterUpdateListener + +/** + Notifies a listener that the listAdapter was updated. + + @param listAdapter The `IGListAdapter` that updated. + @param update The type of update executed. + @param animated A flag indicating if the update was animated. Always `NO` for `IGListAdapterUpdateTypeReloadData`. + + @note This event is sent before the completion block in `-[IGListAdapter performUpdatesAnimated:completion:]` and + `-[IGListAdapter reloadDataWithCompletion:]` is executed. This event is also delivered when an + `IGListSectionController` updates via `-[IGListCollectionContext performBatchAnimated:updates:completion:]`. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter + didFinishUpdate:(IGListAdapterUpdateType)update + animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdater.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdater.h new file mode 100644 index 0000000..bf4bf8e --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdater.h @@ -0,0 +1,68 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An `IGListAdapterUpdater` is a concrete type that conforms to `IGListUpdatingDelegate`. + It is an out-of-box updater for `IGListAdapter` objects to use. + + @note This updater performs re-entrant, coalesced updating for a list. It also uses a least-minimal diff + for calculating UI updates when `IGListAdapter` calls + `-performUpdateWithCollectionView:fromObjects:toObjects:completion:`. + */ +IGLK_SUBCLASSING_RESTRICTED +NS_SWIFT_NAME(ListAdapterUpdater) +@interface IGListAdapterUpdater : NSObject + +/** + The delegate that receives events with data on the performance of a transition. + */ +@property (nonatomic, weak) id delegate; + +/** + A flag indicating if a move should be treated as a "delete, then insert" operation. + */ +@property (nonatomic, assign) BOOL movesAsDeletesInserts; + +/** + A flag indicating that section reloads should be treated as item reloads, instead of converting them to "delete, then insert" operations. + This only applies if the number of items for the section is unchanged. + + @note If the number of items for the section is changed, we would fallback to the default behavior and convert it to "delete + insert", + because the collectionView can crash otherwise. + + Default is NO. + */ +@property (nonatomic, assign) BOOL preferItemReloadsForSectionReloads; + +/** + A flag indicating whether this updater should skip diffing and simply call + `reloadData` for updates when the collection view is not in a window. The default value is `YES`. + + @note This will result in better performance, but will not generate the same delegate + callbacks. If using a custom layout, it will not receive `prepareForCollectionViewUpdates:`. + + @warning On iOS < 8.3, this behavior is unsupported and will always be treated as `NO`. + */ +@property (nonatomic, assign) BOOL allowsBackgroundReloading; + +/** + A bitmask of experiments to conduct on the updater. + */ +@property (nonatomic, assign) IGListExperiment experiments; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdater.m b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdater.m new file mode 100644 index 0000000..04aa357 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdater.m @@ -0,0 +1,641 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListAdapterUpdater.h" +#import "IGListAdapterUpdaterInternal.h" + +#import + +#import "IGListArrayUtilsInternal.h" +#import "IGListIndexSetResultInternal.h" +#import "IGListMoveIndexPathInternal.h" +#import "IGListReloadIndexPath.h" +#import "UICollectionView+IGListBatchUpdateData.h" + +@implementation IGListAdapterUpdater + +- (instancetype)init { + IGAssertMainThread(); + + if (self = [super init]) { + // the default is to use animations unless NO is passed + _queuedUpdateIsAnimated = YES; + _completionBlocks = [NSMutableArray new]; + _batchUpdates = [IGListBatchUpdates new]; + _allowsBackgroundReloading = YES; + } + return self; +} + +#pragma mark - Private API + +- (BOOL)hasChanges { + return self.hasQueuedReloadData + || [self.batchUpdates hasChanges] + || self.fromObjects != nil + || self.toObjectsBlock != nil; +} + +- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { + IGAssertMainThread(); + + id delegate = self.delegate; + void (^reloadUpdates)(void) = self.reloadUpdates; + IGListBatchUpdates *batchUpdates = self.batchUpdates; + NSMutableArray *completionBlocks = [self.completionBlocks mutableCopy]; + + [self cleanStateBeforeUpdates]; + + void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) { + for (IGListUpdatingCompletion block in completionBlocks) { + block(finished); + } + + self.state = IGListBatchUpdateStateIdle; + }; + + // bail early if the collection view has been deallocated in the time since the update was queued + UICollectionView *collectionView = collectionViewBlock(); + if (collectionView == nil) { + [self _cleanStateAfterUpdates]; + executeCompletionBlocks(NO); + return; + } + + // item updates must not send mutations to the collection view while we are reloading + self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock; + + if (reloadUpdates) { + reloadUpdates(); + } + + // execute all stored item update blocks even if we are just calling reloadData. the actual collection view + // mutations will be discarded, but clients are encouraged to put their actual /data/ mutations inside the + // update block as well, so if we don't execute the block the changes will never happen + for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) { + itemUpdateBlock(); + } + + // add any completion blocks from item updates. added after item blocks are executed in order to capture any + // re-entrant updates + [completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks]; + + self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; + + [self _cleanStateAfterUpdates]; + + [delegate listAdapterUpdater:self willReloadDataWithCollectionView:collectionView]; + [collectionView reloadData]; + [collectionView.collectionViewLayout invalidateLayout]; + [collectionView layoutIfNeeded]; + [delegate listAdapterUpdater:self didReloadDataWithCollectionView:collectionView]; + + executeCompletionBlocks(YES); +} + +- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { + IGAssertMainThread(); + IGAssert(self.state == IGListBatchUpdateStateIdle, @"Should not call batch updates when state isn't idle"); + + // create local variables so we can immediately clean our state but pass these items into the batch update block + id delegate = self.delegate; + NSArray *fromObjects = [self.fromObjects copy]; + IGListToObjectBlock toObjectsBlock = [self.toObjectsBlock copy]; + NSMutableArray *completionBlocks = [self.completionBlocks mutableCopy]; + void (^objectTransitionBlock)(NSArray *) = [self.objectTransitionBlock copy]; + const BOOL animated = self.queuedUpdateIsAnimated; + IGListBatchUpdates *batchUpdates = self.batchUpdates; + + // clean up all state so that new updates can be coalesced while the current update is in flight + [self cleanStateBeforeUpdates]; + + void (^executeCompletionBlocks)(BOOL) = ^(BOOL finished) { + self.applyingUpdateData = nil; + self.state = IGListBatchUpdateStateIdle; + + for (IGListUpdatingCompletion block in completionBlocks) { + block(finished); + } + }; + + // bail early if the collection view has been deallocated in the time since the update was queued + UICollectionView *collectionView = collectionViewBlock(); + if (collectionView == nil) { + [self _cleanStateAfterUpdates]; + executeCompletionBlocks(NO); + return; + } + + NSArray *toObjects = nil; + if (toObjectsBlock != nil) { + toObjects = objectsWithDuplicateIdentifiersRemoved(toObjectsBlock()); + } +#ifdef DEBUG + for (id obj in toObjects) { + IGAssert([obj conformsToProtocol:@protocol(IGListDiffable)], + @"In order to use IGListAdapterUpdater, object %@ must conform to IGListDiffable", obj); + IGAssert([obj diffIdentifier] != nil, + @"Cannot have a nil diffIdentifier for object %@", obj); + } +#endif + + void (^executeUpdateBlocks)(void) = ^{ + self.state = IGListBatchUpdateStateExecutingBatchUpdateBlock; + + // run the update block so that the adapter can set its items. this makes sure that just before the update is + // committed that the data source is updated to the /latest/ "toObjects". this makes the data source in sync + // with the items that the updater is transitioning to + if (objectTransitionBlock != nil) { + objectTransitionBlock(toObjects); + } + + // execute each item update block which should make calls like insert, delete, and reload for index paths + // we collect all mutations in corresponding sets on self, then filter based on UICollectionView shortcomings + // call after the objectTransitionBlock so section level mutations happen before any items + for (IGListItemUpdateBlock itemUpdateBlock in batchUpdates.itemUpdateBlocks) { + itemUpdateBlock(); + } + + // add any completion blocks from item updates. added after item blocks are executed in order to capture any + // re-entrant updates + [completionBlocks addObjectsFromArray:batchUpdates.itemCompletionBlocks]; + + self.state = IGListBatchUpdateStateExecutedBatchUpdateBlock; + }; + + void (^reloadDataFallback)(void) = ^{ + executeUpdateBlocks(); + [self _cleanStateAfterUpdates]; + [self _performBatchUpdatesItemBlockApplied]; + [collectionView reloadData]; + [collectionView layoutIfNeeded]; + + executeCompletionBlocks(YES); + }; + + // if the collection view isn't in a visible window, skip diffing and batch updating. execute all transition blocks, + // reload data, execute completion blocks, and get outta here + if (self.allowsBackgroundReloading && collectionView.window == nil) { + [self _beginPerformBatchUpdatesToObjects:toObjects]; + reloadDataFallback(); + return; + } + + // disables multiple performBatchUpdates: from happening at the same time + [self _beginPerformBatchUpdatesToObjects:toObjects]; + + const IGListExperiment experiments = self.experiments; + + IGListIndexSetResult *(^performDiff)(void) = ^{ + return IGListDiffExperiment(fromObjects, toObjects, IGListDiffEquality, experiments); + }; + + // block executed in the first param block of -[UICollectionView performBatchUpdates:completion:] + void (^batchUpdatesBlock)(IGListIndexSetResult *result) = ^(IGListIndexSetResult *result){ + executeUpdateBlocks(); + + self.applyingUpdateData = [self _flushCollectionView:collectionView + withDiffResult:result + batchUpdates:self.batchUpdates + fromObjects:fromObjects]; + + [self _cleanStateAfterUpdates]; + [self _performBatchUpdatesItemBlockApplied]; + }; + + // block used as the second param of -[UICollectionView performBatchUpdates:completion:] + void (^batchUpdatesCompletionBlock)(BOOL) = ^(BOOL finished) { + IGListBatchUpdateData *oldApplyingUpdateData = self.applyingUpdateData; + executeCompletionBlocks(finished); + + [delegate listAdapterUpdater:self didPerformBatchUpdates:oldApplyingUpdateData collectionView:collectionView]; + + // queue another update in case something changed during batch updates. this method will bail next runloop if + // there are no changes + [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; + }; + + // block that executes the batch update and exception handling + void (^performUpdate)(IGListIndexSetResult *) = ^(IGListIndexSetResult *result){ + [collectionView layoutIfNeeded]; + + @try { + [delegate listAdapterUpdater:self +willPerformBatchUpdatesWithCollectionView:collectionView + fromObjects:fromObjects + toObjects:toObjects + listIndexSetResult:result]; + if (collectionView.dataSource == nil) { + // If the data source is nil, we should not call any collection view update. + batchUpdatesCompletionBlock(NO); + } else if (result.changeCount > 100 && IGListExperimentEnabled(experiments, IGListExperimentReloadDataFallback)) { + reloadDataFallback(); + } else if (animated) { + [collectionView performBatchUpdates:^{ + batchUpdatesBlock(result); + } completion:batchUpdatesCompletionBlock]; + } else { + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [collectionView performBatchUpdates:^{ + batchUpdatesBlock(result); + } completion:^(BOOL finished) { + [CATransaction commit]; + batchUpdatesCompletionBlock(finished); + }]; + } + } @catch (NSException *exception) { + [delegate listAdapterUpdater:self + collectionView:collectionView + willCrashWithException:exception + fromObjects:fromObjects + toObjects:toObjects + diffResult:result + updates:(id)self.applyingUpdateData]; + @throw exception; + } + }; + + if (IGListExperimentEnabled(experiments, IGListExperimentBackgroundDiffing)) { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + IGListIndexSetResult *result = performDiff(); + dispatch_async(dispatch_get_main_queue(), ^{ + performUpdate(result); + }); + }); + } else { + IGListIndexSetResult *result = performDiff(); + performUpdate(result); + } +} + +void convertReloadToDeleteInsert(NSMutableIndexSet *reloads, + NSMutableIndexSet *deletes, + NSMutableIndexSet *inserts, + IGListIndexSetResult *result, + NSArray> *fromObjects) { + // reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts + const BOOL hasObjects = [fromObjects count] > 0; + [[reloads copy] enumerateIndexesUsingBlock:^(NSUInteger idx, BOOL *stop) { + // if a diff was not performed, there are no changes. instead use the same index that was originally queued + id diffIdentifier = hasObjects ? [fromObjects[idx] diffIdentifier] : nil; + const NSInteger from = hasObjects ? [result oldIndexForIdentifier:diffIdentifier] : idx; + const NSInteger to = hasObjects ? [result newIndexForIdentifier:diffIdentifier] : idx; + [reloads removeIndex:from]; + + // if a reload is queued outside the diff and the object was inserted or deleted it cannot be + if (from != NSNotFound && to != NSNotFound) { + [deletes addIndex:from]; + [inserts addIndex:to]; + } else { + IGAssert([result.deletes containsIndex:idx], + @"Reloaded section %lu was not found in deletes with from: %li, to: %li, deletes: %@, fromClass: %@", + (unsigned long)idx, (long)from, (long)to, deletes, [(id)fromObjects[idx] class]); + } + }]; +} + +static NSArray *convertSectionReloadToItemUpdates(NSIndexSet *sectionReloads, UICollectionView *collectionView) { + NSMutableArray *updates = [NSMutableArray new]; + [sectionReloads enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL * _Nonnull stop) { + NSUInteger numberOfItems = [collectionView numberOfItemsInSection:sectionIndex]; + for (NSUInteger itemIndex = 0; itemIndex < numberOfItems; itemIndex++) { + [updates addObject:[NSIndexPath indexPathForItem:itemIndex inSection:sectionIndex]]; + } + }]; + return [updates copy]; +} + +- (IGListBatchUpdateData *)_flushCollectionView:(UICollectionView *)collectionView + withDiffResult:(IGListIndexSetResult *)diffResult + batchUpdates:(IGListBatchUpdates *)batchUpdates + fromObjects:(NSArray > *)fromObjects { + NSSet *moves = [[NSSet alloc] initWithArray:diffResult.moves]; + + // combine section reloads from the diff and manual reloads via reloadItems: + NSMutableIndexSet *reloads = [diffResult.updates mutableCopy]; + [reloads addIndexes:batchUpdates.sectionReloads]; + + NSMutableIndexSet *inserts = [diffResult.inserts mutableCopy]; + NSMutableIndexSet *deletes = [diffResult.deletes mutableCopy]; + NSMutableArray *itemUpdates = [NSMutableArray new]; + if (self.movesAsDeletesInserts) { + for (IGListMoveIndex *move in moves) { + [deletes addIndex:move.from]; + [inserts addIndex:move.to]; + } + // clear out all moves + moves = [NSSet new]; + } + + // Item reloads are not safe, if any section moves happened or there are inserts/deletes. + if (self.preferItemReloadsForSectionReloads + && moves.count == 0 && inserts.count == 0 && deletes.count == 0 && reloads.count > 0) { + [reloads enumerateIndexesUsingBlock:^(NSUInteger sectionIndex, BOOL * _Nonnull stop) { + NSMutableIndexSet *localIndexSet = [NSMutableIndexSet indexSetWithIndex:sectionIndex]; + if (sectionIndex < [collectionView numberOfSections] + && sectionIndex < [collectionView.dataSource numberOfSectionsInCollectionView:collectionView] + && [collectionView numberOfItemsInSection:sectionIndex] == [collectionView.dataSource collectionView:collectionView numberOfItemsInSection:sectionIndex]) { + // Perfer to do item reloads instead, if the number of items in section is unchanged. + [itemUpdates addObjectsFromArray:convertSectionReloadToItemUpdates(localIndexSet, collectionView)]; + } else { + // Otherwise, fallback to convert into delete+insert section operation. + convertReloadToDeleteInsert(localIndexSet, deletes, inserts, diffResult, fromObjects); + } + }]; + } else { + // reloadSections: is unsafe to use within performBatchUpdates:, so instead convert all reloads into deletes+inserts + convertReloadToDeleteInsert(reloads, deletes, inserts, diffResult, fromObjects); + } + + NSMutableArray *itemInserts = batchUpdates.itemInserts; + NSMutableArray *itemDeletes = batchUpdates.itemDeletes; + NSMutableArray *itemMoves = batchUpdates.itemMoves; + + NSSet *uniqueDeletes = [NSSet setWithArray:itemDeletes]; + NSMutableSet *reloadDeletePaths = [NSMutableSet new]; + NSMutableSet *reloadInsertPaths = [NSMutableSet new]; + for (IGListReloadIndexPath *reload in batchUpdates.itemReloads) { + if (![uniqueDeletes containsObject:reload.fromIndexPath]) { + [reloadDeletePaths addObject:reload.fromIndexPath]; + [reloadInsertPaths addObject:reload.toIndexPath]; + } + } + [itemDeletes addObjectsFromArray:[reloadDeletePaths allObjects]]; + [itemInserts addObjectsFromArray:[reloadInsertPaths allObjects]]; + + const BOOL fixIndexPathImbalance = IGListExperimentEnabled(self.experiments, IGListExperimentFixIndexPathImbalance); + IGListBatchUpdateData *updateData = [[IGListBatchUpdateData alloc] initWithInsertSections:inserts + deleteSections:deletes + moveSections:moves + insertIndexPaths:itemInserts + deleteIndexPaths:itemDeletes + updateIndexPaths:itemUpdates + moveIndexPaths:itemMoves + fixIndexPathImbalance:fixIndexPathImbalance]; + [collectionView ig_applyBatchUpdateData:updateData]; + return updateData; +} + +- (void)_beginPerformBatchUpdatesToObjects:(NSArray *)toObjects { + self.pendingTransitionToObjects = toObjects; + self.state = IGListBatchUpdateStateQueuedBatchUpdate; +} + +- (void)_performBatchUpdatesItemBlockApplied { + self.pendingTransitionToObjects = nil; +} + +- (void)cleanStateBeforeUpdates { + self.queuedUpdateIsAnimated = YES; + + // destroy to/from transition items + self.fromObjects = nil; + self.toObjectsBlock = nil; + + // destroy reloadData state + self.reloadUpdates = nil; + self.queuedReloadData = NO; + + // remove indexpath/item changes + self.objectTransitionBlock = nil; + + // removes all object completion blocks. done before updates to start collecting completion blocks for coalesced + // or re-entrant object updates + [self.completionBlocks removeAllObjects]; +} + +- (void)_cleanStateAfterUpdates { + self.batchUpdates = [IGListBatchUpdates new]; +} + +- (void)_queueUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock { + IGAssertMainThread(); + + __weak __typeof__(self) weakSelf = self; + + // dispatch_async to give the main queue time to collect more batch updates so that a minimum amount of work + // (diffing, etc) is done on main. dispatch_async does not garauntee a full runloop turn will pass though. + // see -performUpdateWithCollectionView:fromObjects:toObjects:animated:objectTransitionBlock:completion: for more + // details on how coalescence is done. + dispatch_async(dispatch_get_main_queue(), ^{ + if (weakSelf.state != IGListBatchUpdateStateIdle + || ![weakSelf hasChanges]) { + return; + } + + if (weakSelf.hasQueuedReloadData) { + [weakSelf performReloadDataWithCollectionViewBlock:collectionViewBlock]; + } else { + [weakSelf performBatchUpdatesWithCollectionViewBlock:collectionViewBlock]; + } + }); +} + + +#pragma mark - IGListUpdatingDelegate + +static BOOL IGListIsEqual(const void *a, const void *b, NSUInteger (*size)(const void *item)) { + const id left = (__bridge id)a; + const id right = (__bridge id)b; + return [left class] == [right class] + && [[left diffIdentifier] isEqual:[right diffIdentifier]]; +} + +// since the diffing algo used in this updater keys items based on their -diffIdentifier, we must use a map table that +// precisely mimics this behavior +static NSUInteger IGListIdentifierHash(const void *item, NSUInteger (*size)(const void *item)) { + return [[(__bridge id)item diffIdentifier] hash]; +} + +- (NSPointerFunctions *)objectLookupPointerFunctions { + NSPointerFunctions *functions = [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsStrongMemory]; + functions.hashFunction = IGListIdentifierHash; + functions.isEqualFunction = IGListIsEqual; + return functions; +} + +- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + fromObjects:(NSArray *)fromObjects + toObjectsBlock:(IGListToObjectBlock)toObjectsBlock + animated:(BOOL)animated + objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock + completion:(IGListUpdatingCompletion)completion { + IGAssertMainThread(); + IGParameterAssert(collectionViewBlock != nil); + IGParameterAssert(objectTransitionBlock != nil); + + // only update the items that we are coming from if it has not been set + // this allows multiple updates to be called while an update is already in progress, and the transition from > to + // will be done on the first "fromObjects" received and the last "toObjects" + // if performBatchUpdates: hasn't applied the update block, then data source hasn't transitioned its state. if an + // update is queued in between then we must use the pending toObjects + self.fromObjects = self.fromObjects ?: self.pendingTransitionToObjects ?: fromObjects; + self.toObjectsBlock = toObjectsBlock; + + // disabled animations will always take priority + // reset to YES in -cleanupState + self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated; + + // always use the last update block, even though this should always do the exact same thing + self.objectTransitionBlock = objectTransitionBlock; + + IGListUpdatingCompletion localCompletion = completion; + if (localCompletion) { + [self.completionBlocks addObject:localCompletion]; + } + + [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; +} + +- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + animated:(BOOL)animated + itemUpdates:(void (^)(void))itemUpdates + completion:(void (^)(BOOL))completion { + IGAssertMainThread(); + IGParameterAssert(collectionViewBlock != nil); + IGParameterAssert(itemUpdates != nil); + + IGListBatchUpdates *batchUpdates = self.batchUpdates; + if (completion != nil) { + [batchUpdates.itemCompletionBlocks addObject:completion]; + } + + // if already inside the execution of the update block, immediately unload the itemUpdates block. + // the completion blocks are executed later in the lifecycle, so that still needs to be added to the batch + if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + itemUpdates(); + } else { + [batchUpdates.itemUpdateBlocks addObject:itemUpdates]; + + // disabled animations will always take priority + // reset to YES in -cleanupState + self.queuedUpdateIsAnimated = self.queuedUpdateIsAnimated && animated; + + [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; + } +} + +- (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { + IGAssertMainThread(); + IGParameterAssert(collectionView != nil); + IGParameterAssert(indexPaths != nil); + if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.batchUpdates.itemInserts addObjectsFromArray:indexPaths]; + } else { + [self.delegate listAdapterUpdater:self willInsertIndexPaths:indexPaths collectionView:collectionView]; + [collectionView insertItemsAtIndexPaths:indexPaths]; + } +} + +- (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { + IGAssertMainThread(); + IGParameterAssert(collectionView != nil); + IGParameterAssert(indexPaths != nil); + if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.batchUpdates.itemDeletes addObjectsFromArray:indexPaths]; + } else { + [self.delegate listAdapterUpdater:self willDeleteIndexPaths:indexPaths collectionView:collectionView]; + [collectionView deleteItemsAtIndexPaths:indexPaths]; + } +} + +- (void)moveItemInCollectionView:(UICollectionView *)collectionView + fromIndexPath:(NSIndexPath *)fromIndexPath + toIndexPath:(NSIndexPath *)toIndexPath { + if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + IGListMoveIndexPath *move = [[IGListMoveIndexPath alloc] initWithFrom:fromIndexPath to:toIndexPath]; + [self.batchUpdates.itemMoves addObject:move]; + } else { + [self.delegate listAdapterUpdater:self willMoveFromIndexPath:fromIndexPath toIndexPath:toIndexPath collectionView:collectionView]; + [collectionView moveItemAtIndexPath:fromIndexPath toIndexPath:toIndexPath]; + } +} + +- (void)reloadItemInCollectionView:(UICollectionView *)collectionView + fromIndexPath:(NSIndexPath *)fromIndexPath + toIndexPath:(NSIndexPath *)toIndexPath { + if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + IGListReloadIndexPath *reload = [[IGListReloadIndexPath alloc] initWithFromIndexPath:fromIndexPath toIndexPath:toIndexPath]; + [self.batchUpdates.itemReloads addObject:reload]; + } else { + [self.delegate listAdapterUpdater:self willReloadIndexPaths:@[fromIndexPath] collectionView:collectionView]; + [collectionView reloadItemsAtIndexPaths:@[fromIndexPath]]; + } +} + +- (void)moveSectionInCollectionView:(UICollectionView *)collectionView + fromIndex:(NSInteger)fromIndex + toIndex:(NSInteger)toIndex { + IGAssertMainThread(); + IGParameterAssert(collectionView != nil); + + // iOS expects interactive reordering to be movement of items not sections + // after moving a single-item section controller, + // you end up with two items in the section for the drop location, + // and zero items in the section originating at the drag location + // so, we have to reload data rather than doing a section move + + [collectionView reloadData]; + + // It seems that reloadData called during UICollectionView's moveItemAtIndexPath + // delegate call does not reload all cells as intended + // So, we further reload all visible sections to make sure none of our cells + // are left with data that's out of sync with our dataSource + + id delegate = self.delegate; + + NSMutableIndexSet *visibleSections = [NSMutableIndexSet new]; + NSArray *visibleIndexPaths = [collectionView indexPathsForVisibleItems]; + for (NSIndexPath *visibleIndexPath in visibleIndexPaths) { + [visibleSections addIndex:visibleIndexPath.section]; + } + + [delegate listAdapterUpdater:self willReloadSections:visibleSections collectionView:collectionView]; + + // prevent double-animation from reloadData + reloadSections + + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [collectionView performBatchUpdates:^{ + [collectionView reloadSections:visibleSections]; + } completion:^(BOOL finished) { + [CATransaction commit]; + }]; +} + +- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock + completion:(nullable IGListUpdatingCompletion)completion { + IGAssertMainThread(); + IGParameterAssert(collectionViewBlock != nil); + IGParameterAssert(reloadUpdateBlock != nil); + + IGListUpdatingCompletion localCompletion = completion; + if (localCompletion) { + [self.completionBlocks addObject:localCompletion]; + } + + self.reloadUpdates = reloadUpdateBlock; + self.queuedReloadData = YES; + [self _queueUpdateWithCollectionViewBlock:collectionViewBlock]; +} + +- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections { + IGAssertMainThread(); + IGParameterAssert(collectionView != nil); + IGParameterAssert(sections != nil); + if (self.state == IGListBatchUpdateStateExecutingBatchUpdateBlock) { + [self.batchUpdates.sectionReloads addIndexes:sections]; + } else { + [self.delegate listAdapterUpdater:self willReloadSections:sections collectionView:collectionView]; + [collectionView reloadSections:sections]; + } +} + + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdaterDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdaterDelegate.h new file mode 100644 index 0000000..15384b9 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListAdapterUpdaterDelegate.h @@ -0,0 +1,157 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@class IGListAdapterUpdater; +@class IGListBatchUpdates; +@class IGListIndexSetResult; +@protocol IGListDiffable; + +NS_ASSUME_NONNULL_BEGIN + +/** + A protocol that receives events about `IGListAdapterUpdater` operations. + */ +NS_SWIFT_NAME(ListAdapterUpdaterDelegate) +@protocol IGListAdapterUpdaterDelegate + +/** + Notifies the delegate that the updater will call `-[UICollectionView performBatchUpdates:completion:]`. + + @param listAdapterUpdater The adapter updater owning the transition. + @param collectionView The collection view that will perform the batch updates. + @param fromObjects The items transitioned from in the batch updates, if any. + @param toObjects The items transitioned to in the batch updates, if any. + @param listIndexSetResults The diffing result of indices to be inserted/removed/updated/moved/etc. + */ +- (void) listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater +willPerformBatchUpdatesWithCollectionView:(UICollectionView *)collectionView + fromObjects:(nullable NSArray > *)fromObjects + toObjects:(nullable NSArray > *)toObjects + listIndexSetResult:(nullable IGListIndexSetResult *)listIndexSetResults; + +/** + Notifies the delegate that the updater successfully finished `-[UICollectionView performBatchUpdates:completion:]`. + + @param listAdapterUpdater The adapter updater owning the transition. + @param updates The batch updates that were applied to the collection view. + @param collectionView The collection view that performed the batch updates. + + @note This event is called in the completion block of the batch update. + */ +- (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater + didPerformBatchUpdates:(IGListBatchUpdateData *)updates + collectionView:(UICollectionView *)collectionView; + +/** + Notifies the delegate that the updater will call `-[UICollectionView insertItemsAtIndexPaths:]`. + + @param listAdapterUpdater The adapter updater owning the transition. + @param indexPaths An array of index paths that will be inserted. + @param collectionView The collection view that will perform the insert. + + @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. + */ +- (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater + willInsertIndexPaths:(NSArray *)indexPaths + collectionView:(UICollectionView *)collectionView; + +/** + Notifies the delegate that the updater will call `-[UICollectionView deleteItemsAtIndexPaths:]`. + + @param listAdapterUpdater The adapter updater owning the transition. + @param indexPaths An array of index paths that will be deleted. + @param collectionView The collection view that will perform the delete. + + @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. + */ +- (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater + willDeleteIndexPaths:(NSArray *)indexPaths + collectionView:(UICollectionView *)collectionView; + +/** + Notifies the delegate that the updater will call `-[UICollectionView moveItemAtIndexPath:toIndexPath:]` + + @param listAdapterUpdater The adapter updater owning the transition. + @param fromIndexPath The index path of the item that will be moved. + @param toIndexPath The index path to move the item to. + @param collectionView The collection view that will perform the move. + + @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. + */ +- (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater + willMoveFromIndexPath:(NSIndexPath *)fromIndexPath + toIndexPath:(NSIndexPath *)toIndexPath + collectionView:(UICollectionView *)collectionView; + +/** + Notifies the delegate that the updater will call `-[UICollectionView reloadItemsAtIndexPaths:]`. + + @param listAdapterUpdater The adapter updater owning the transition. + @param indexPaths An array of index paths that will be reloaded. + @param collectionView The collection view that will perform the reload. + + @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. + */ +- (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater + willReloadIndexPaths:(NSArray *)indexPaths + collectionView:(UICollectionView *)collectionView; + +/** + Notifies the delegate that the updater will call `-[UICollectionView reloadSections:]`. + + @param listAdapterUpdater The adapter updater owning the transition. + @param sections The sections that will be reloaded + @param collectionView The collection view that will perform the reload. + + @note This event is only sent when outside of `-[UICollectionView performBatchUpdates:completion:]`. + */ +- (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater + willReloadSections:(NSIndexSet *)sections + collectionView:(UICollectionView *)collectionView; + +/** + Notifies the delegate that the updater will call `-[UICollectionView reloadData]`. + + @param listAdapterUpdater The adapter updater owning the transition. + @param collectionView The collection view that will be reloaded. + */ +- (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater willReloadDataWithCollectionView:(UICollectionView *)collectionView; + +/** + Notifies the delegate that the updater successfully called `-[UICollectionView reloadData]`. + + @param listAdapterUpdater The adapter updater owning the transition. + @param collectionView The collection view that reloaded. + */ +- (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater didReloadDataWithCollectionView:(UICollectionView *)collectionView; + +/** + Notifies the delegate that the collection view threw an exception in `-[UICollectionView performBatchUpdates:completion:]`. + + @param listAdapterUpdater The adapter updater owning the transition. + @param collectionView The collection view being updated. + @param exception The exception thrown by the collection view. + @param fromObjects The items transitioned from in the diff, if any. + @param toObjects The items transitioned to in the diff, if any. + @param diffResult The diff result that were computed from `fromObjects` and `toObjects`. + @param updates The batch updates that were applied to the collection view. + */ +- (void)listAdapterUpdater:(IGListAdapterUpdater *)listAdapterUpdater + collectionView:(UICollectionView *)collectionView + willCrashWithException:(NSException *)exception + fromObjects:(nullable NSArray *)fromObjects + toObjects:(nullable NSArray *)toObjects + diffResult:(IGListIndexSetResult *)diffResult + updates:(IGListBatchUpdateData *)updates; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListBatchContext.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListBatchContext.h new file mode 100644 index 0000000..59f166a --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListBatchContext.h @@ -0,0 +1,109 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListSectionController; + + + +NS_ASSUME_NONNULL_BEGIN + +/** + Objects conforming to the IGListBatchContext protocol provide a way for section controllers to mutate their cells or + reload everything within the section. + */ +NS_SWIFT_NAME(ListBatchContext) +@protocol IGListBatchContext + +/** + Reloads cells in the section controller. + + @param sectionController The section controller who's cells need reloading. + @param indexes The indexes of items that need reloading. + */ +- (void)reloadInSectionController:(IGListSectionController *)sectionController + atIndexes:(NSIndexSet *)indexes; + +/** + Inserts cells in the list. + + @param sectionController The section controller who's cells need inserting. + @param indexes The indexes of items that need inserting. + */ +- (void)insertInSectionController:(IGListSectionController *)sectionController + atIndexes:(NSIndexSet *)indexes; + +/** + Deletes cells in the list. + + @param sectionController The section controller who's cells need deleted. + @param indexes The indexes of items that need deleting. + */ +- (void)deleteInSectionController:(IGListSectionController *)sectionController + atIndexes:(NSIndexSet *)indexes; + +/** + Invalidates layouts of cells at specific in the section controller. + + @param sectionController The section controller who's cells need invalidating. + @param indexes The indexes of items that need invalidating. + */ +- (void)invalidateLayoutInSectionController:(IGListSectionController *)sectionController + atIndexes:(NSIndexSet *)indexes; + +/** + Moves a cell from one index to another within the section controller. + + @param sectionController The section controller who's cell needs moved. + @param fromIndex The index the cell is currently in. + @param toIndex The index the cell should move to. + */ +- (void)moveInSectionController:(IGListSectionController *)sectionController + fromIndex:(NSInteger)fromIndex + toIndex:(NSInteger)toIndex; + +/** + Reloads the entire section controller. + + @param sectionController The section controller who's cells need reloading. + */ +- (void)reloadSectionController:(IGListSectionController *)sectionController; + +/** + Moves a section controller from one index to another during interactive reordering. + + @param sectionController The section controller to move. + @param fromIndex The index where the section currently resides. + @param toIndex The index the section should move to. + */ +- (void)moveSectionControllerInteractive:(IGListSectionController *)sectionController + fromIndex:(NSInteger)fromIndex + toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0); + +/** + Moves an object within a section controller from one index to another during interactive reordering. + + @param sectionController The section controller containing the object to move. + @param fromIndex The index where the object currently resides. + @param toIndex The index the object should move to. + */ +- (void)moveInSectionControllerInteractive:(IGListSectionController *)sectionController + fromIndex:(NSInteger)fromIndex + toIndex:(NSInteger)toIndex NS_AVAILABLE_IOS(9_0); + +/** + Reverts an move from one indexPath to another during interactive reordering. + + @param sourceIndexPath The indexPath the item was originally in. + @param destinationIndexPath The indexPath the item was moving to. + */ +- (void)revertInvalidInteractiveMoveFromIndexPath:(NSIndexPath *)sourceIndexPath + toIndexPath:(NSIndexPath *)destinationIndexPath NS_AVAILABLE_IOS(9_0); +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListBindable.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindable.h new file mode 100644 index 0000000..29d1e50 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindable.h @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A protocol for cells that configure themselves given a view model. + */ +NS_SWIFT_NAME(ListBindable) +@protocol IGListBindable + +/** + Tells the cell to configure itself with the given view model. + + @param viewModel The view model for the cell. + + @note The view model can change many times throughout the lifetime of a cell as the model values change and the cell + is reused. Implementations should use only this method to do their configuration. + */ +- (void)bindViewModel:(id)viewModel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionController.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionController.h new file mode 100644 index 0000000..3c3dac9 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionController.h @@ -0,0 +1,91 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import +#import + +@protocol IGListDiffable; +@protocol IGListBindable; + +@class IGListBindingSectionController; + +NS_ASSUME_NONNULL_BEGIN + +/** + This section controller uses a data source to transform its "top level" object into an array of diffable view models. + It then automatically binds each view model to cells via the `IGListBindable` protocol. + + Models used with `IGListBindingSectionController` should take special care to always return `YES` for identical + objects. That is, any objects with matching `-diffIdentifier`s should always be equal, that way the section controller + can create new view models via the data source, create a diff, and update the specific cells that have changed. + + In Objective-C, your `-isEqualToDiffableObject:` can simply be: + ``` + - (BOOL)isEqualToDiffableObject:(id)object { + return YES; + } + ``` + + In Swift: + ``` + func isEqual(toDiffableObject object: IGListDiffable?) -> Bool { + return true + } + ``` + + Only when `-diffIdentifier`s match is object equality compared, so you can assume the class is the same, and the + instance has already been checked. + */ +NS_SWIFT_NAME(ListBindingSectionController) +@interface IGListBindingSectionController<__covariant ObjectType : id> : IGListSectionController + +/** + A data source that transforms a top-level object into view models, and returns cells and sizes for given view models. + */ +@property (nonatomic, weak, nullable) id dataSource; + +/** + A delegate that receives selection events from cells in an `IGListBindingSectionController` instance. + */ +@property (nonatomic, weak, nullable) id selectionDelegate; + +/** + The object currently assigned to the section controller, if any. + */ +@property (nonatomic, strong, readonly, nullable) ObjectType object; + +/** + The array of view models created from the data source. Values are changed when the top-level object changes or by + calling `-updateAnimated:completion:` manually. + */ +@property (nonatomic, strong, readonly) NSArray> *viewModels; + +/** + Tells the section controller to query for new view models, diff the changes, and update its cells. + + @param animated A flag indicating if the transition should be animated or not. + @param completion An optional completion block executed after updates finish. Parameter is YES if updates were applied. + */ +- (void)updateAnimated:(BOOL)animated completion:(nullable void (^)(BOOL updated))completion; + +/** + Notifies the section that a list object should move within a section as the result of interactive reordering. + + @param sourceIndex The starting index of the object. + @param destinationIndex The ending index of the object. + + @note this method must be implemented if interactive reordering is enabled. To ensure updating the internal viewModels array, **calling super is required**, preferably before your own implementation. + */ +- (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex NS_REQUIRES_SUPER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionController.m b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionController.m new file mode 100644 index 0000000..85c9b3e --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionController.m @@ -0,0 +1,153 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListBindingSectionController.h" + +#import +#import + +#import "IGListArrayUtilsInternal.h" + +typedef NS_ENUM(NSInteger, IGListDiffingSectionState) { + IGListDiffingSectionStateIdle = 0, + IGListDiffingSectionStateUpdateQueued, + IGListDiffingSectionStateUpdateApplied +}; + +@interface IGListBindingSectionController() + +@property (nonatomic, strong, readwrite) NSArray> *viewModels; + +@property (nonatomic, strong) id object; +@property (nonatomic, assign) IGListDiffingSectionState state; + +@end + +@implementation IGListBindingSectionController + +#pragma mark - Public API + +- (void)updateAnimated:(BOOL)animated completion:(void (^)(BOOL))completion { + IGAssertMainThread(); + + if (self.state != IGListDiffingSectionStateIdle) { + if (completion != nil) { + completion(NO); + } + return; + } + self.state = IGListDiffingSectionStateUpdateQueued; + + __block IGListIndexSetResult *result = nil; + __block NSArray> *oldViewModels = nil; + + id collectionContext = self.collectionContext; + [self.collectionContext performBatchAnimated:animated updates:^(id batchContext) { + if (self.state != IGListDiffingSectionStateUpdateQueued) { + return; + } + + oldViewModels = self.viewModels; + + id object = self.object; + IGAssert(object != nil, @"Expected IGListBindingSectionController object to be non-nil before updating."); + + NSArray *newViewModels = [self.dataSource sectionController:self viewModelsForObject:object]; + self.viewModels = objectsWithDuplicateIdentifiersRemoved(newViewModels); + result = IGListDiff(oldViewModels, self.viewModels, IGListDiffEquality); + + [result.updates enumerateIndexesUsingBlock:^(NSUInteger oldUpdatedIndex, BOOL *stop) { + id identifier = [oldViewModels[oldUpdatedIndex] diffIdentifier]; + const NSInteger indexAfterUpdate = [result newIndexForIdentifier:identifier]; + if (indexAfterUpdate != NSNotFound) { + UICollectionViewCell *cell = [collectionContext cellForItemAtIndex:oldUpdatedIndex + sectionController:self]; + [cell bindViewModel:self.viewModels[indexAfterUpdate]]; + } + }]; + + if (IGListExperimentEnabled(self.collectionContext.experiments, IGListExperimentInvalidateLayoutForUpdates)) { + [batchContext invalidateLayoutInSectionController:self atIndexes:result.updates]; + } + [batchContext deleteInSectionController:self atIndexes:result.deletes]; + [batchContext insertInSectionController:self atIndexes:result.inserts]; + + for (IGListMoveIndex *move in result.moves) { + [batchContext moveInSectionController:self fromIndex:move.from toIndex:move.to]; + } + + self.state = IGListDiffingSectionStateUpdateApplied; + } completion:^(BOOL finished) { + self.state = IGListDiffingSectionStateIdle; + if (completion != nil) { + completion(YES); + } + }]; +} + +#pragma mark - IGListSectionController Overrides + +- (NSInteger)numberOfItems { + return self.viewModels.count; +} + +- (CGSize)sizeForItemAtIndex:(NSInteger)index { + return [self.dataSource sectionController:self sizeForViewModel:self.viewModels[index] atIndex:index]; +} + +- (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { + id viewModel = self.viewModels[index]; + UICollectionViewCell *cell = [self.dataSource sectionController:self cellForViewModel:viewModel atIndex:index]; + [cell bindViewModel:viewModel]; + return cell; +} + +- (void)didUpdateToObject:(id)object { + id oldObject = self.object; + self.object = object; + + if (oldObject == nil) { + NSArray *viewModels = [self.dataSource sectionController:self viewModelsForObject:object]; + self.viewModels = objectsWithDuplicateIdentifiersRemoved(viewModels); + } else { +#if IGLK_LOGGING_ENABLED + if (![oldObject isEqualToDiffableObject:object]) { + IGLKLog(@"Warning: Unequal objects %@ and %@ will cause IGListBindingSectionController to reload the entire section", + oldObject, object); + } +#endif + [self updateAnimated:YES completion:nil]; + } +} + +- (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex { + NSMutableArray *viewModels = [self.viewModels mutableCopy]; + + id modelAtSource = [viewModels objectAtIndex:sourceIndex]; + [viewModels removeObjectAtIndex:sourceIndex]; + [viewModels insertObject:modelAtSource atIndex:destinationIndex]; + + self.viewModels = viewModels; +} + +- (void)didSelectItemAtIndex:(NSInteger)index { + [self.selectionDelegate sectionController:self didSelectItemAtIndex:index viewModel:self.viewModels[index]]; +} + +- (void)didDeselectItemAtIndex:(NSInteger)index { + [self.selectionDelegate sectionController:self didDeselectItemAtIndex:index viewModel:self.viewModels[index]]; +} + +- (void)didHighlightItemAtIndex:(NSInteger)index { + [self.selectionDelegate sectionController:self didHighlightItemAtIndex:index viewModel:self.viewModels[index]]; +} + +- (void)didUnhighlightItemAtIndex:(NSInteger)index { + [self.selectionDelegate sectionController:self didUnhighlightItemAtIndex:index viewModel:self.viewModels[index]]; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionControllerDataSource.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionControllerDataSource.h new file mode 100644 index 0000000..dcab7b8 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionControllerDataSource.h @@ -0,0 +1,65 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListBindingSectionController; + +@protocol IGListBindable; +@protocol IGListDiffable; + +NS_ASSUME_NONNULL_BEGIN + +/** + A protocol that returns data to power cells in an `IGListBindingSectionController`. + */ +NS_SWIFT_NAME(ListBindingSectionControllerDataSource) +@protocol IGListBindingSectionControllerDataSource + +/** + Create an array of view models given a top-level object. + + @param sectionController The section controller requesting view models. + @param object The top-level object that powers the section controller. + + @return A new array of view models. + */ +- (NSArray> *)sectionController:(IGListBindingSectionController *)sectionController + viewModelsForObject:(id)object; + +/** + Return a dequeued cell for a given view model. + + @param sectionController The section controller requesting a cell. + @param viewModel The view model for the cell. + @param index The index of the view model. + + @return A dequeued cell. + + @note The section controller will call `-bindViewModel:` with the provided view model after the cell is dequeued. You + should handle cell configuration using this method. However, you can do additional configuration at this stage as well. + */ +- (UICollectionViewCell *)sectionController:(IGListBindingSectionController *)sectionController + cellForViewModel:(id)viewModel + atIndex:(NSInteger)index; + +/** + Return a cell size for a given view model. + + @param sectionController The section controller requesting a size. + @param viewModel The view model for the cell. + @param index The index of the view model. + + @return A size for the view model. + */ +- (CGSize)sectionController:(IGListBindingSectionController *)sectionController + sizeForViewModel:(id)viewModel + atIndex:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionControllerSelectionDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionControllerSelectionDelegate.h new file mode 100644 index 0000000..f12b7ce --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListBindingSectionControllerSelectionDelegate.h @@ -0,0 +1,66 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListBindingSectionController; + +NS_ASSUME_NONNULL_BEGIN + +/** + A protocol that handles cell selection events in an `IGListBindingSectionController`. + */ +NS_SWIFT_NAME(ListBindingSectionControllerSelectionDelegate) +@protocol IGListBindingSectionControllerSelectionDelegate + +/** + Tells the delegate that a cell at a given index was selected. + + @param sectionController The section controller the selection occurred in. + @param index The index of the selected cell. + @param viewModel The view model that was bound to the cell. + */ +- (void)sectionController:(IGListBindingSectionController *)sectionController + didSelectItemAtIndex:(NSInteger)index + viewModel:(id)viewModel; + +/** + Tells the delegate that a cell at a given index was deselected. + + @param sectionController The section controller the deselection occurred in. + @param index The index of the deselected cell. + @param viewModel The view model that was bound to the cell. + */ +- (void)sectionController:(IGListBindingSectionController *)sectionController + didDeselectItemAtIndex:(NSInteger)index + viewModel:(id)viewModel; + +/** + Tells the delegate that a cell at a given index was highlighted. + + @param sectionController The section controller the highlight occurred in. + @param index The index of the highlighted cell. + @param viewModel The view model that was bound to the cell. + */ +- (void)sectionController:(IGListBindingSectionController *)sectionController + didHighlightItemAtIndex:(NSInteger)index + viewModel:(id)viewModel; + +/** + Tells the delegate that a cell at a given index was unhighlighted. + + @param sectionController The section controller the unhighlight occurred in. + @param index The index of the unhighlighted cell. + @param viewModel The view model that was bound to the cell. + */ +- (void)sectionController:(IGListBindingSectionController *)sectionController +didUnhighlightItemAtIndex:(NSInteger)index + viewModel:(id)viewModel; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionContext.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionContext.h new file mode 100644 index 0000000..6dfee68 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionContext.h @@ -0,0 +1,305 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class IGListSectionController; + +/** + The collection context provides limited access to the collection-related information that + section controllers need for operations like sizing, dequeuing cells, inserting, deleting, reloading, etc. + */ +NS_SWIFT_NAME(ListCollectionContext) +@protocol IGListCollectionContext + +/** + The size of the collection view. You can use this for sizing cells. + */ +@property (nonatomic, readonly) CGSize containerSize; + +/** + The content insets of the collection view. You can use this for sizing cells. + */ +@property (nonatomic, readonly) UIEdgeInsets containerInset; + +/** + The adjusted content insets of the collection view. Equivalent to containerInset under iOS 11. + */ +@property (nonatomic, readonly) UIEdgeInsets adjustedContainerInset; + +/** + The size of the collection view with content insets applied. + */ +@property (nonatomic, readonly) CGSize insetContainerSize; + +/** + The current scrolling traits of the underlying collection view. + */ +@property (nonatomic, readonly) IGListCollectionScrollingTraits scrollingTraits; + +/** + A bitmask of experiments to conduct on the section controller. + */ +@property (nonatomic, assign) IGListExperiment experiments; + +/** + Returns size of the collection view relative to the section controller. + + @param sectionController The section controller requesting this information. + + @return The size of the collection view minus the given section controller's insets. + */ +- (CGSize)containerSizeForSectionController:(IGListSectionController *)sectionController; + +/** + Returns the index of the specified cell in the collection relative to the section controller. + + @param cell An existing cell in the collection. + @param sectionController The section controller requesting this information. + + @return The index of the cell or `NSNotFound` if it does not exist in the collection. + */ +- (NSInteger)indexForCell:(UICollectionViewCell *)cell + sectionController:(IGListSectionController *)sectionController; + +/** + Returns the cell in the collection at the specified index for the section controller. + + @param index The index of the desired cell. + @param sectionController The section controller requesting this information. + + @return The collection view cell, or `nil` if not found. + + @warning This method may return `nil` if the cell is offscreen. + */ +- (nullable __kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index + sectionController:(IGListSectionController *)sectionController; + +/** + Returns the visible cells for the given section controller. + + @param sectionController The section controller requesting this information. + + @return An array of visible cells, or an empty array if none are found. + */ +- (NSArray *)visibleCellsForSectionController:(IGListSectionController *)sectionController; + +/** + Returns the visible paths for the given section controller. + + @param sectionController The section controller requesting this information. + + @return An array of visible index paths, or an empty array if none are found. + */ +- (NSArray *)visibleIndexPathsForSectionController:(IGListSectionController *) sectionController; + +/** + Deselects a cell in the collection. + + @param index The index of the item to deselect. + @param sectionController The section controller requesting this information. + @param animated Pass `YES` to animate the change, `NO` otherwise. + */ +- (void)deselectItemAtIndex:(NSInteger)index + sectionController:(IGListSectionController *)sectionController + animated:(BOOL)animated; + +/** + Selects a cell in the collection. + + @param index The index of the item to select. + @param sectionController The section controller requesting this information. + @param animated Pass `YES` to animate the change, `NO` otherwise. + @param scrollPosition An option that specifies where the item should be positioned when scrolling finishes. + */ +- (void)selectItemAtIndex:(NSInteger)index + sectionController:(IGListSectionController *)sectionController + animated:(BOOL)animated + scrollPosition:(UICollectionViewScrollPosition)scrollPosition; + +/** + Dequeues a cell from the collection view reuse pool. + + @param cellClass The class of the cell you want to dequeue. + @param reuseIdentifier A reuse identifier for the specified cell. This parameter may be `nil`. + @param sectionController The section controller requesting this information. + @param index The index of the cell. + + @return A cell dequeued from the reuse pool or a newly created one. + + @note This method uses a string representation of the cell class as the identifier. + */ +- (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass + withReuseIdentifier:(nullable NSString *)reuseIdentifier + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index; + +/** + Dequeues a cell from the collection view reuse pool. + + @param cellClass The class of the cell you want to dequeue. + @param sectionController The section controller requesting this information. + @param index The index of the cell. + + @return A cell dequeued from the reuse pool or a newly created one. + + @note This method uses a string representation of the cell class as the identifier. + */ +- (__kindof UICollectionViewCell *)dequeueReusableCellOfClass:(Class)cellClass + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index; + +/** + Dequeues a cell from the collection view reuse pool. + + @param nibName The name of the nib file. + @param bundle The bundle in which to search for the nib file. If `nil`, this method searches the main bundle. + @param sectionController The section controller requesting this information. + @param index The index of the cell. + + @return A cell dequeued from the reuse pool or a newly created one. + + @note This method uses a string representation of the cell class as the identifier. + */ +- (__kindof UICollectionViewCell *)dequeueReusableCellWithNibName:(NSString *)nibName + bundle:(nullable NSBundle *)bundle + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index; + +/** + Dequeues a storyboard prototype cell from the collection view reuse pool. + + @param identifier The identifier of the cell prototype in storyboard. + @param sectionController The section controller requesting this information. + @param index The index of the cell. + + @return A cell dequeued from the reuse pool or a newly created one. + */ +- (__kindof UICollectionViewCell *)dequeueReusableCellFromStoryboardWithIdentifier:(NSString *)identifier + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index; + +/** + Dequeues a supplementary view from the collection view reuse pool. + + @param elementKind The kind of supplementary view. + @param sectionController The section controller requesting this information. + @param viewClass The class of the supplementary view. + @param index The index of the supplementary view. + + @return A supplementary view dequeued from the reuse pool or a newly created one. + + @note This method uses a string representation of the view class as the identifier. + */ +- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind + forSectionController:(IGListSectionController *)sectionController + class:(Class)viewClass + atIndex:(NSInteger)index; + +/** + Dequeues a supplementary view from the collection view reuse pool. + + @param elementKind The kind of supplementary view. + @param identifier The identifier of the supplementary view in storyboard. + @param sectionController The section controller requesting this information. + @param index The index of the supplementary view. + + @return A supplementary view dequeued from the reuse pool or a newly created one. + */ +- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewFromStoryboardOfKind:(NSString *)elementKind + withIdentifier:(NSString *)identifier + forSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index; +/** + Dequeues a supplementary view from the collection view reuse pool. + + @param elementKind The kind of supplementary view. + @param sectionController The section controller requesting this information. + @param nibName The name of the nib file. + @param bundle The bundle in which to search for the nib file. If `nil`, this method searches the main bundle. + @param index The index of the supplementary view. + + @return A supplementary view dequeued from the reuse pool or a newly created one. + + @note This method uses a string representation of the view class as the identifier. + */ +- (__kindof UICollectionReusableView *)dequeueReusableSupplementaryViewOfKind:(NSString *)elementKind + forSectionController:(IGListSectionController *)sectionController + nibName:(NSString *)nibName + bundle:(nullable NSBundle *)bundle + atIndex:(NSInteger)index; + +/** + Invalidate the backing `UICollectionViewLayout` for all items in the section controller. + + @param sectionController The section controller that needs invalidating. + @param completion An optional completion block to execute when the updates are finished. + + @note This method can be wrapped in `UIView` animation APIs to control the duration or perform without animations. This + will end up calling `-[UICollectionView performBatchUpdates:completion:]` internally, so invalidated changes may not be + reflected in the cells immediately. + */ +- (void)invalidateLayoutForSectionController:(IGListSectionController *)sectionController + completion:(nullable void (^)(BOOL finished))completion; + +/** + Batches and performs many cell-level updates in a single transaction. + + @param animated A flag indicating if the transition should be animated. + @param updates A block with a context parameter to make mutations. + @param completion An optional completion block to execute when the updates are finished. + + @note You should make state changes that impact the number of items in your section controller within the updates + block alongside changes on the context object. + + For example, inside your section controllers, you may want to delete *and* insert into the data source that backs your + section controller. For example: + + ``` + [self.collectionContext performBatchItemUpdates:^ (id batchContext>){ + // perform data source changes inside the update block + [self.items addObject:newItem]; + [self.items removeObjectAtIndex:0]; + + NSIndexSet *inserts = [NSIndexSet indexSetWithIndex:[self.items count] - 1]; + [batchContext insertInSectionController:self atIndexes:inserts]; + + NSIndexSet *deletes = [NSIndexSet indexSetWithIndex:0]; + [batchContext deleteInSectionController:self atIndexes:deletes]; + } completion:nil]; + ``` + + @warning You **must** perform data modifications **inside** the update block. Updates will not be performed + synchronously, so you should make sure that your data source changes only when necessary. + */ +- (void)performBatchAnimated:(BOOL)animated + updates:(void (^)(id batchContext))updates + completion:(nullable void (^)(BOOL finished))completion; + + +/** + Scrolls to the specified section controller in the list. + + @param sectionController The section controller. + @param index The index of the item in the section controller to which to scroll. + @param scrollPosition An option that specifies where the item should be positioned when scrolling finishes. + @param animated A flag indicating if the scrolling should be animated. + */ +- (void)scrollToSectionController:(IGListSectionController *)sectionController + atIndex:(NSInteger)index + scrollPosition:(UICollectionViewScrollPosition)scrollPosition + animated:(BOOL)animated; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionScrollingTraits.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionScrollingTraits.h new file mode 100644 index 0000000..d93a30f --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionScrollingTraits.h @@ -0,0 +1,22 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** + The current scrolling traits of the underlying collection view. + The attributes are always equal to their corresponding properties on the underlying collection view. + */ +NS_SWIFT_NAME(ListCollectionScrollingTraits) +typedef struct IGListCollectionScrollingTraits { + /// returns YES if user has touched. may not yet have started dragging. + bool isTracking; + /// returns YES if user has started scrolling. this may require some time and or distance to move to initiate dragging + bool isDragging; + /// returns YES if user isn't dragging (touch up) but scroll view is still moving. + bool isDecelerating; +} IGListCollectionScrollingTraits; diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionView.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionView.h new file mode 100644 index 0000000..b7e880b --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionView.h @@ -0,0 +1,49 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@protocol IGListCollectionViewLayoutCompatible; + +NS_ASSUME_NONNULL_BEGIN + +/** + This `UICollectionView` subclass allows for partial layout invalidation using `IGListCollectionViewLayout`, + or custom layout classes that conform to IGListCollectionViewLayoutCompatible. + + @note When updating a collection view (ex: calling `-insertSections`), `-invalidateLayoutWithContext` gets called on + the layout object. However, the invalidation context doesn't provide details on which index paths are being modified, + which typically forces a full layout re-calculation. `IGListCollectionView` gives `IGListCollectionViewLayout` the + missing information to re-calculate only the modified layout attributes. + */ +NS_SWIFT_NAME(ListCollectionView) +@interface IGListCollectionView : UICollectionView + +/** + Create a new view with an `IGListcollectionViewLayout` class or subclass. + + @param frame The frame to initialize with. + @param collectionViewLayout The layout to use with the collection view. You can use IGListCollectionViewLayout + here, or a custom layout class that conforms to IGListCollectionViewLayoutCompatible. + + @note You can initialize a new view with a base layout by simply calling `-[IGListCollectionView initWithFrame:]`. + */ +- (instancetype)initWithFrame:(CGRect)frame listCollectionViewLayout:(UICollectionViewLayout *)collectionViewLayout NS_DESIGNATED_INITIALIZER; + +/** + :nodoc: + */ +- (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)collectionViewLayout NS_UNAVAILABLE; + +/** + :nodoc: + */ +- (instancetype)initWithCoder:(NSCoder *)aDecoder NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionView.m b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionView.m new file mode 100644 index 0000000..e54e02a --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionView.m @@ -0,0 +1,105 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListCollectionView.h" + +#import "IGListCollectionViewLayout.h" +#import "IGListCollectionViewLayoutCompatible.h" + +@implementation IGListCollectionView + +#pragma mark - Init + +- (instancetype)initWithFrame:(CGRect)frame { + IGListCollectionViewLayout *layout = [[IGListCollectionViewLayout alloc] initWithStickyHeaders:NO topContentInset:0 stretchToEdge:YES]; + return [self initWithFrame:frame listCollectionViewLayout:layout]; +} + +- (instancetype)initWithFrame:(CGRect)frame listCollectionViewLayout:(UICollectionViewLayout *)collectionViewLayout { + return [super initWithFrame:frame collectionViewLayout:collectionViewLayout]; +} + +#pragma mark - IGListCollectionViewLayout + +- (UICollectionViewLayout *)_listLayout { + if ([self.collectionViewLayout conformsToProtocol:@protocol(IGListCollectionViewLayoutCompatible)]) { + return (UICollectionViewLayout *)self.collectionViewLayout; + } + + return nil; +} + +#pragma mark - Overides reloads + +- (void)reloadItemsAtIndexPaths:(NSArray *)indexPaths { + [self _didModifyIndexPaths:indexPaths]; + [super reloadItemsAtIndexPaths:indexPaths]; +} + +- (void)reloadSections:(NSIndexSet *)sections { + [self _didModifySections:sections]; + [super reloadSections:sections]; +} + +#pragma mark - Override deletes + +- (void)deleteItemsAtIndexPaths:(NSArray *)indexPaths { + [self _didModifyIndexPaths:indexPaths]; + [super deleteItemsAtIndexPaths:indexPaths]; +} + +- (void)deleteSections:(NSIndexSet *)sections { + [self _didModifySections:sections]; + [super deleteSections:sections]; +} + +#pragma mark - Override inserts + +- (void)insertItemsAtIndexPaths:(NSArray *)indexPaths { + [self _didModifyIndexPaths:indexPaths]; + [super insertItemsAtIndexPaths:indexPaths]; +} + +- (void)insertSections:(NSIndexSet *)sections { + [self _didModifySections:sections]; + [super insertSections:sections]; +} + +#pragma mark - Override moves + +- (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath { + [self _didModifyIndexPaths:@[indexPath, newIndexPath]]; + [super moveItemAtIndexPath:indexPath toIndexPath:newIndexPath]; +} + +- (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection { + [self _didModifySection:MIN(section, newSection)]; + [super moveSection:section toSection:newSection]; +} + +#pragma mark - Modify section + +- (void)_didModifySections:(NSIndexSet *)sections { + if (sections.count == 0) { + return; + } + [self _didModifySection:sections.firstIndex]; +} + +- (void)_didModifySection:(NSUInteger)section { + [self._listLayout didModifySection:section]; +} + +#pragma mark - Modified index path + +- (void)_didModifyIndexPaths:(NSArray *)indexPaths { + for (NSIndexPath *indexPath in indexPaths) { + [self _didModifySection:indexPath.section]; + } +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewDelegateLayout.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewDelegateLayout.h new file mode 100644 index 0000000..6c360d9 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewDelegateLayout.h @@ -0,0 +1,36 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** + Conform to `IGListCollectionViewDelegateLayout` to provide customized layout information for a collection view. + */ +@protocol IGListCollectionViewDelegateLayout + +/** + Asks the delegate to customize and return the starting layout information for an item being inserted into the collection view. + + @param collectionView The collection view to perform the transition on. + @param collectionViewLayout The layout to use with the collection view. + @param attributes The starting layout information for an item being inserted into the collection view. + @param indexPath The index path of the item being inserted. + */ +- (UICollectionViewLayoutAttributes *)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout customizedInitialLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes atIndexPath:(NSIndexPath *)indexPath; + +/** + Asks the delegate to customize and return the final layout information for an item that is about to be removed from the collection view. + + @param collectionView The collection view to perform the transition on. + @param collectionViewLayout The layout to use with the collection view. + @param attributes The final layout information for an item that is about to be removed from the collection view. + @param indexPath The index path of the item being deleted. + */ +- (UICollectionViewLayoutAttributes *)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout customizedFinalLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes atIndexPath:(NSIndexPath *)indexPath; + +@end + diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayout.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayout.h new file mode 100644 index 0000000..71cfbd7 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayout.h @@ -0,0 +1,142 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +#import "IGListCollectionViewLayoutCompatible.h" + +NS_ASSUME_NONNULL_BEGIN + +/** + This UICollectionViewLayout subclass is for vertically or horizontally scrolling lists of data with variable widths and + heights. It supports an infinite number of sections and items. All work is done on the main thread, and while extremely efficient, + care must be taken not to stall the main thread in sizing delegate methods. + + This layout piggybacks on the mechanics of UICollectionViewFlowLayout in that: + + - Your UICollectionView data source must also conform to UICollectionViewDelegateFlowLayout + - Header support given via UICollectionElementKindSectionHeader + + All UICollectionViewDelegateFlowLayout methods are required and used by this layout: + + ``` + - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath; + - (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section; + - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section; + - (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section; + - (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section; + ``` + + In a vertically scrolling layout, sections and items are put into the same horizontal row until the max-x position + of an item extends beyond the width of the collection view. When that happens, the item is "newlined" to the next row. + The y position of that row is determined by the maximum height (including section insets) of the section/item of the previous row. + + Ex. of a section (2,0) with a large width causing a newline. + ``` + |[ 0,0 ][ 1,0 ] | + |[ 2,0 ]| + ``` + + A section with a non-zero height header will always cause that section to newline. Headers are always stretched to the + width of the collection view, pinched with the section insets. + + Ex. of a section (2,0) with a header inset on the left/right. + ``` + |[ 0,0 ][ 1,0 ] | + | >======header=======< | + | [ 2,0 ] | + ``` + + Section insets apply to items in the section no matter if they begin on a new row or are on the same row as a previous + section. + + Ex. of a section (2) with multiple items and a left inset. + ``` + |[ 0,0 ][ 1,0 ] >[ 2,0 ]| + | >[ 2,1 ][ 2,2 ][ 2,3 ]| + ``` + + Interitem spacing applies to items and sections within the same row. Line spacing only applies to items within the same + section. + + In a horizontally scrolling layout, sections and items are flowed vertically until they need to be "newlined" to the + next column. Headers, if used, are stretched to the height of the collection view, minus the section insets. + + Please see the unit tests for more configuration examples and expected output. + */ +NS_SWIFT_NAME(ListCollectionViewLayout) +@interface IGListCollectionViewLayout : UICollectionViewLayout + +/** + Direction in which layout will be scrollable; items will be flowed in the perpendicular direction, "newlining" when they + run out of space along that axis or when a non-zero header is found. + */ +@property (nonatomic, readonly) UICollectionViewScrollDirection scrollDirection; + +/** + Set this to adjust the offset of the sticky headers in the scrolling direction. Can be used to change the sticky + header position as UI like the navigation bar is scrolled offscreen. In a vertically scrolling layout, changing + this to the height of the navigation bar will give the effect of the headers sticking to the nav as it is collapsed. + + @note Changing the value on this method will invalidate the layout every time. + */ +@property (nonatomic, assign) CGFloat stickyHeaderYOffset; + +/** + Set this to `YES` to show sticky header when a section had no item. Default is `NO`. +*/ +@property (nonatomic, assign) BOOL showHeaderWhenEmpty; + +/** + A bitmask of experiments to conduct on the adapter. + */ +@property (nonatomic, assign) IGListExperiment experiments; + +/** + Create and return a new collection view layout. + + @param stickyHeaders Set to `YES` to stick section headers to the top of the bounds while scrolling. + @param scrollDirection Direction along which the collection view will be scrollable (if content size exceeds the frame size) + @param topContentInset The content inset (top or left, depending on scrolling direction) used to offset the sticky headers. Ignored if stickyHeaders is `NO`. + @param stretchToEdge Specifies whether to stretch width (in vertically scrolling layout) or height (horizontally scrolling) of last item to right/bottom edge when distance from last item to right/bottom edge < epsilon(1) + + @return A new collection view layout. + */ +- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders + scrollDirection:(UICollectionViewScrollDirection)scrollDirection + topContentInset:(CGFloat)topContentInset + stretchToEdge:(BOOL)stretchToEdge NS_DESIGNATED_INITIALIZER; + +/** + Create and return a new vertically scrolling collection view layout. + + @param stickyHeaders Set to `YES` to stick section headers to the top of the bounds while scrolling. + @param topContentInset The top content inset used to offset the sticky headers. Ignored if stickyHeaders is `NO`. + @param stretchToEdge Specifies whether to stretch width of last item to right edge when distance from last item to right edge < epsilon(1) + + @return A new collection view layout. + */ +- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders + topContentInset:(CGFloat)topContentInset + stretchToEdge:(BOOL)stretchToEdge; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayout.mm b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayout.mm new file mode 100644 index 0000000..46869c8 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayout.mm @@ -0,0 +1,681 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListCollectionViewLayout.h" +#import "IGListCollectionViewLayoutInternal.h" + +#import + +#import +#import + +#import "UIScrollView+IGListKit.h" + +static CGFloat UIEdgeInsetsLeadingInsetInDirection(UIEdgeInsets insets, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return insets.top; + case UICollectionViewScrollDirectionHorizontal: return insets.left; + } +} + +static CGFloat UIEdgeInsetsTrailingInsetInDirection(UIEdgeInsets insets, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return insets.bottom; + case UICollectionViewScrollDirectionHorizontal: return insets.right; + } +} + +static CGFloat CGPointGetCoordinateInDirection(CGPoint point, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return point.y; + case UICollectionViewScrollDirectionHorizontal: return point.x; + } +} + +static CGFloat CGRectGetLengthInDirection(CGRect rect, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return rect.size.height; + case UICollectionViewScrollDirectionHorizontal: return rect.size.width; + } +} + +static CGFloat CGRectGetMaxInDirection(CGRect rect, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return CGRectGetMaxY(rect); + case UICollectionViewScrollDirectionHorizontal: return CGRectGetMaxX(rect); + } +} + +static CGFloat CGRectGetMinInDirection(CGRect rect, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return CGRectGetMinY(rect); + case UICollectionViewScrollDirectionHorizontal: return CGRectGetMinX(rect); + } +} + +static CGFloat CGSizeGetLengthInDirection(CGSize size, UICollectionViewScrollDirection direction) { + switch (direction) { + case UICollectionViewScrollDirectionVertical: return size.height; + case UICollectionViewScrollDirectionHorizontal: return size.width; + } +} + +static NSIndexPath *indexPathForSection(NSInteger section) { + return [NSIndexPath indexPathForItem:0 inSection:section]; +} + +static NSInteger IGListMergeMinimumInvalidatedSection(NSInteger section, NSInteger otherSection) { + if (section == NSNotFound && otherSection == NSNotFound) { + return NSNotFound; + } else if (section == NSNotFound) { + return otherSection; + } else if (otherSection == NSNotFound) { + return section; + } + + return MIN(section, otherSection); +} + +struct IGListSectionEntry { + /** + Represents the minimum-bounding box of every element in the section. This includes all item frames as well as the + header bounds. It is made simply by unioning all item and header frames. Use this to find section intersections + to build layout attributes given a rect. + */ + CGRect bounds; + + // The insets for the section. Used to find total content size of the section. + UIEdgeInsets insets; + + // The RESTING frame of the header view (e.g. when the header is not sticking to the top of the scroll view). + CGRect headerBounds; + + // The RESTING frame of the footer view + CGRect footerBounds; + + // An array of frames for each cell in the section. + std::vector itemBounds; + + // last item distance in scroll direction, used for partial invalidation + CGFloat lastItemCoordInScrollDirection; + + // last item distance in fixed direction, used for partial invalidation + CGFloat lastItemCoordInFixedDirection; + + // last next row distance in scroll direction, used for partial invalidation + CGFloat lastNextRowCoordInScrollDirection; + + // Returns YES when the section has visible content (header and/or items). + BOOL isValid() { + return !CGSizeEqualToSize(bounds.size, CGSizeZero); + } +}; + +// Each section has a base zIndex of section * maxZIndexPerSection; +// section header adds (maxZIndexPerSection - 1) to the base zIndex; +// other cells adds (item) to the base zIndex. +// This allows us to present tooltips that can grow from the cell to its top. +static void adjustZIndexForAttributes(UICollectionViewLayoutAttributes *attributes) { + const NSInteger maxZIndexPerSection = 1000; + const NSInteger baseZIndex = attributes.indexPath.section * maxZIndexPerSection; + + switch (attributes.representedElementCategory) { + case UICollectionElementCategoryCell: + attributes.zIndex = baseZIndex + attributes.indexPath.item; + break; + case UICollectionElementCategorySupplementaryView: + attributes.zIndex = baseZIndex + maxZIndexPerSection - 1; + break; + case UICollectionElementCategoryDecorationView: + attributes.zIndex = baseZIndex - 1; + break; + } +} + +@interface IGListCollectionViewLayoutInvalidationContext : UICollectionViewLayoutInvalidationContext +@property (nonatomic, assign) BOOL ig_invalidateSupplementaryAttributes; +@property (nonatomic, assign) BOOL ig_invalidateAllAttributes; +@end + +@implementation IGListCollectionViewLayoutInvalidationContext +@end + +@interface IGListCollectionViewLayout () + +@property (nonatomic, assign, readonly) BOOL stickyHeaders; +@property (nonatomic, assign, readonly) CGFloat topContentInset; +@property (nonatomic, assign, readonly) BOOL stretchToEdge; + +@end + +@implementation IGListCollectionViewLayout { + std::vector _sectionData; + NSMutableDictionary *_attributesCache; + + // invalidate starting at this section + NSInteger _minimumInvalidatedSection; + + /** + The workflow for getting sticky headers working: + 1. Use a custom invalidation context to mark supplementary attributes invalid. + 2. Return YES from -shouldInvalidateLayoutForBoundsChange: + 3. In -invalidationContextForBoundsChange: mark supplementary attributes invalid on the custom context. + 4. Purge supplementary caches in -invalidateLayoutWithContext: if context says they are invalid + 5. Use cached attributes in -layoutAttributesForSupplementaryViewOfKind:atIndexPath: if they exist, else rebuild + 6. Make sure -layoutAttributesForElementsInRect: always uses the attributes returned from + -layoutAttributesForSupplementaryViewOfKind:atIndexPath:. + */ + NSMutableDictionary *> *_supplementaryAttributesCache; +} + +- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders + topContentInset:(CGFloat)topContentInset + stretchToEdge:(BOOL)stretchToEdge { + return [self initWithStickyHeaders:stickyHeaders + scrollDirection:UICollectionViewScrollDirectionVertical + topContentInset:topContentInset + stretchToEdge:stretchToEdge]; +} + +- (instancetype)initWithStickyHeaders:(BOOL)stickyHeaders + scrollDirection:(UICollectionViewScrollDirection)scrollDirection + topContentInset:(CGFloat)topContentInset + stretchToEdge:(BOOL)stretchToEdge { + if (self = [super init]) { + _scrollDirection = scrollDirection; + _stickyHeaders = stickyHeaders; + _topContentInset = topContentInset; + _stretchToEdge = stretchToEdge; + _attributesCache = [NSMutableDictionary new]; + _supplementaryAttributesCache = [NSMutableDictionary dictionaryWithDictionary:@{ + UICollectionElementKindSectionHeader: [NSMutableDictionary new], + UICollectionElementKindSectionFooter: [NSMutableDictionary new], + }]; + _minimumInvalidatedSection = NSNotFound; + } + return self; +} + +- (instancetype)initWithCoder:(NSCoder *)aDecoder { + return [self initWithStickyHeaders:NO topContentInset:0 stretchToEdge:NO]; +} + +#pragma mark - UICollectionViewLayout + +- (UICollectionViewLayoutAttributes *)initialLayoutAttributesForAppearingItemAtIndexPath:(NSIndexPath *)itemIndexPath { + UICollectionViewLayoutAttributes *attributes = [super initialLayoutAttributesForAppearingItemAtIndexPath:itemIndexPath]; + id delegate = (id)self.collectionView.delegate; + if ([delegate respondsToSelector:@selector(collectionView:layout:customizedInitialLayoutAttributes:atIndexPath:)]) { + return [delegate collectionView:self.collectionView + layout:self + customizedInitialLayoutAttributes:attributes + atIndexPath:itemIndexPath]; + } + return attributes; +} + +- (UICollectionViewLayoutAttributes *)finalLayoutAttributesForDisappearingItemAtIndexPath:(NSIndexPath *)itemIndexPath{ + UICollectionViewLayoutAttributes *attributes = [super finalLayoutAttributesForDisappearingItemAtIndexPath:itemIndexPath]; + id delegate = (id)self.collectionView.delegate; + if ([delegate respondsToSelector:@selector(collectionView:layout:customizedFinalLayoutAttributes:atIndexPath:)]) { + return [delegate collectionView:self.collectionView + layout:self + customizedFinalLayoutAttributes:attributes + atIndexPath:itemIndexPath]; + } + return attributes; +} + +- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect { + IGAssertMainThread(); + + NSMutableArray *result = [NSMutableArray new]; + + const NSRange range = [self _rangeOfSectionsInRect:rect]; + if (range.location == NSNotFound) { + return nil; + } + + for (NSInteger section = range.location; section < NSMaxRange(range); section++) { + const NSInteger itemCount = _sectionData[section].itemBounds.size(); + + // do not add headers if there are no items + if (itemCount > 0 || self.showHeaderWhenEmpty) { + for (NSString *elementKind in _supplementaryAttributesCache.allKeys) { + NSIndexPath *indexPath = indexPathForSection(section); + UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForSupplementaryViewOfKind:elementKind + atIndexPath:indexPath]; + // do not add zero height headers/footers or headers/footers that are outside the rect + const CGRect frame = attributes.frame; + const CGRect intersection = CGRectIntersection(frame, rect); + if (attributes && !CGRectIsEmpty(intersection) && CGRectGetLengthInDirection(frame, self.scrollDirection) > 0.0) { + [result addObject:attributes]; + } + } + } + + // add all cells within the rect, return early if it starts iterating outside + for (NSInteger item = 0; item < itemCount; item++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section]; + UICollectionViewLayoutAttributes *attributes = [self layoutAttributesForItemAtIndexPath:indexPath]; + if (CGRectIntersectsRect(attributes.frame, rect)) { + [result addObject:attributes]; + } + } + } + + return result; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath { + IGAssertMainThread(); + IGParameterAssert(indexPath != nil); + + UICollectionViewLayoutAttributes *attributes = _attributesCache[indexPath]; + if (attributes != nil) { + return attributes; + } + + // avoid OOB errors + const NSInteger section = indexPath.section; + const NSInteger item = indexPath.item; + if (section >= _sectionData.size() + || item >= _sectionData[section].itemBounds.size()) { + return nil; + } + + attributes = [[[self class] layoutAttributesClass] layoutAttributesForCellWithIndexPath:indexPath]; + attributes.frame = _sectionData[indexPath.section].itemBounds[indexPath.item]; + adjustZIndexForAttributes(attributes); + _attributesCache[indexPath] = attributes; + return attributes; +} + +- (UICollectionViewLayoutAttributes *)layoutAttributesForSupplementaryViewOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + IGAssertMainThread(); + IGParameterAssert(indexPath != nil); + + UICollectionViewLayoutAttributes *attributes = _supplementaryAttributesCache[elementKind][indexPath]; + if (attributes != nil) { + return attributes; + } + + // avoid OOB errors + const NSInteger section = indexPath.section; + if (section >= _sectionData.size()) { + return nil; + } + + UICollectionView *collectionView = self.collectionView; + const IGListSectionEntry entry = _sectionData[section]; + const CGFloat minOffset = CGRectGetMinInDirection(entry.bounds, self.scrollDirection); + + CGRect frame = CGRectZero; + + if ([elementKind isEqualToString:UICollectionElementKindSectionHeader]) { + frame = entry.headerBounds; + + if (self.stickyHeaders) { + CGFloat offset = CGPointGetCoordinateInDirection(collectionView.contentOffset, self.scrollDirection) + self.topContentInset + self.stickyHeaderYOffset; + + if (section + 1 == _sectionData.size()) { + offset = MAX(minOffset, offset); + } else { + const CGFloat maxOffset = CGRectGetMinInDirection(_sectionData[section + 1].bounds, self.scrollDirection) - CGRectGetLengthInDirection(frame, self.scrollDirection); + offset = MIN(MAX(minOffset, offset), maxOffset); + } + switch (self.scrollDirection) { + case UICollectionViewScrollDirectionVertical: + frame.origin.y = offset; + break; + case UICollectionViewScrollDirectionHorizontal: + frame.origin.x = offset; + break; + } + } + } else if ([elementKind isEqualToString:UICollectionElementKindSectionFooter]) { + frame = entry.footerBounds; + } + + if (CGRectIsEmpty(frame)) { + // Just like UICollectionViewFlowLayout, if the header/footer size is empty, do not not return an attribute. + // If we did return something, calling [UICollectionView layoutAttributesForSupplementaryElementOfKind...] would too, + // which could then crash if the UICollectionViewDelegate is not expecting to actually return a supplimentary view. + return nil; + } else { + attributes = [UICollectionViewLayoutAttributes layoutAttributesForSupplementaryViewOfKind:elementKind withIndexPath:indexPath]; + attributes.frame = frame; + adjustZIndexForAttributes(attributes); + _supplementaryAttributesCache[elementKind][indexPath] = attributes; + return attributes; + } +} + +- (CGSize)collectionViewContentSize { + IGAssertMainThread(); + + const NSInteger sectionCount = _sectionData.size(); + + if (sectionCount == 0) { + return CGSizeZero; + } + + const IGListSectionEntry section = _sectionData[sectionCount - 1]; + UICollectionView *collectionView = self.collectionView; + const UIEdgeInsets contentInset = collectionView.ig_contentInset; + switch (self.scrollDirection) { + case UICollectionViewScrollDirectionVertical: { + const CGFloat height = CGRectGetMaxY(section.bounds) + section.insets.bottom; + return CGSizeMake(CGRectGetWidth(collectionView.bounds) - contentInset.left - contentInset.right, height); + } + case UICollectionViewScrollDirectionHorizontal: { + const CGFloat width = CGRectGetMaxX(section.bounds) + section.insets.right; + return CGSizeMake(width, CGRectGetHeight(collectionView.bounds) - contentInset.top - contentInset.bottom); + } + } + +} + +- (void)invalidateLayoutWithContext:(IGListCollectionViewLayoutInvalidationContext *)context { + BOOL hasInvalidatedItemIndexPaths = NO; + if ([context respondsToSelector:@selector(invalidatedItemIndexPaths)]) { + hasInvalidatedItemIndexPaths = [context invalidatedItemIndexPaths].count > 0; + } + + if (hasInvalidatedItemIndexPaths + || [context invalidateEverything] + || context.ig_invalidateAllAttributes) { + // invalidates all + _minimumInvalidatedSection = 0; + } else if ([context invalidateDataSourceCounts] && _minimumInvalidatedSection == NSNotFound) { + // invalidate all if count changed and we don't have information on the minimum invalidated section + _minimumInvalidatedSection = 0; + } + + if (context.ig_invalidateSupplementaryAttributes) { + [self _resetSupplementaryAttributesCache]; + } + + [super invalidateLayoutWithContext:context]; +} + ++ (Class)invalidationContextClass { + return [IGListCollectionViewLayoutInvalidationContext class]; +} + +- (UICollectionViewLayoutInvalidationContext *)invalidationContextForBoundsChange:(CGRect)newBounds { + const CGRect oldBounds = self.collectionView.bounds; + + IGListCollectionViewLayoutInvalidationContext *context = + (IGListCollectionViewLayoutInvalidationContext *)[super invalidationContextForBoundsChange:newBounds]; + context.ig_invalidateSupplementaryAttributes = YES; + if (!CGSizeEqualToSize(oldBounds.size, newBounds.size)) { + context.ig_invalidateAllAttributes = YES; + } + return context; +} + +- (BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds { + const CGRect oldBounds = self.collectionView.bounds; + + // always invalidate for size changes + if (!CGSizeEqualToSize(oldBounds.size, newBounds.size)) { + return YES; + } + + // if the y origin has changed, only invalidate when using sticky headers + if (CGRectGetMinInDirection(newBounds, self.scrollDirection) != CGRectGetMinInDirection(oldBounds, self.scrollDirection)) { + return self.stickyHeaders; + } + + return NO; +} + +- (void)prepareLayout { + [self _calculateLayoutIfNeeded]; +} + +#pragma mark - Public API + +- (void)setStickyHeaderYOffset:(CGFloat)stickyHeaderYOffset { + IGAssertMainThread(); + + if (_stickyHeaderYOffset != stickyHeaderYOffset) { + _stickyHeaderYOffset = stickyHeaderYOffset; + + IGListCollectionViewLayoutInvalidationContext *invalidationContext = [IGListCollectionViewLayoutInvalidationContext new]; + invalidationContext.ig_invalidateSupplementaryAttributes = YES; + [self invalidateLayoutWithContext:invalidationContext]; + } +} + +#pragma mark - Private API + +- (void)_calculateLayoutIfNeeded { + if (_minimumInvalidatedSection == NSNotFound) { + return; + } + + // purge attribute caches so they are rebuilt + [_attributesCache removeAllObjects]; + [self _resetSupplementaryAttributesCache]; + + UICollectionView *collectionView = self.collectionView; + id dataSource = collectionView.dataSource; + id delegate = (id)collectionView.delegate; + + const NSInteger sectionCount = (IGListExperimentEnabled(_experiments, IGListExperimentUseCollectionViewInsteadOfDataSourceInLayout) + ? [collectionView numberOfSections] + : [dataSource numberOfSectionsInCollectionView:collectionView]); + const UIEdgeInsets contentInset = collectionView.ig_contentInset; + const CGRect contentInsetAdjustedCollectionViewBounds = UIEdgeInsetsInsetRect(collectionView.bounds, contentInset); + + _sectionData.resize(sectionCount); + + CGFloat itemCoordInScrollDirection = 0.0; + CGFloat itemCoordInFixedDirection = 0.0; + CGFloat nextRowCoordInScrollDirection = 0.0; + + // union item frames and optionally the header to find a bounding box of the entire section + CGRect rollingSectionBounds = CGRectZero; + + // populate last valid section information + const NSInteger lastValidSection = _minimumInvalidatedSection - 1; + if (lastValidSection >= 0 && lastValidSection < sectionCount) { + itemCoordInScrollDirection = _sectionData[lastValidSection].lastItemCoordInScrollDirection; + itemCoordInFixedDirection = _sectionData[lastValidSection].lastItemCoordInFixedDirection; + nextRowCoordInScrollDirection = _sectionData[lastValidSection].lastNextRowCoordInScrollDirection; + rollingSectionBounds = _sectionData[lastValidSection].bounds; + } + + for (NSInteger section = _minimumInvalidatedSection; section < sectionCount; section++) { + const NSInteger itemCount = (IGListExperimentEnabled(_experiments, IGListExperimentUseCollectionViewInsteadOfDataSourceInLayout) + ? [collectionView numberOfItemsInSection:section] + : [dataSource collectionView:collectionView numberOfItemsInSection:section]); + const BOOL itemsEmpty = itemCount == 0; + const BOOL hideHeaderWhenItemsEmpty = itemsEmpty && !self.showHeaderWhenEmpty; + _sectionData[section].itemBounds = std::vector(itemCount); + + const CGSize headerSize = [delegate collectionView:collectionView layout:self referenceSizeForHeaderInSection:section]; + const CGSize footerSize = [delegate collectionView:collectionView layout:self referenceSizeForFooterInSection:section]; + const UIEdgeInsets insets = [delegate collectionView:collectionView layout:self insetForSectionAtIndex:section]; + const CGFloat lineSpacing = [delegate collectionView:collectionView layout:self minimumLineSpacingForSectionAtIndex:section]; + const CGFloat interitemSpacing = [delegate collectionView:collectionView layout:self minimumInteritemSpacingForSectionAtIndex:section]; + + const CGSize paddedCollectionViewSize = UIEdgeInsetsInsetRect(contentInsetAdjustedCollectionViewBounds, insets).size; + const UICollectionViewScrollDirection fixedDirection = self.scrollDirection == UICollectionViewScrollDirectionHorizontal ? UICollectionViewScrollDirectionVertical : UICollectionViewScrollDirectionHorizontal; + const CGFloat paddedLengthInFixedDirection = CGSizeGetLengthInDirection(paddedCollectionViewSize, fixedDirection); + const CGFloat headerLengthInScrollDirection = hideHeaderWhenItemsEmpty ? 0 : CGSizeGetLengthInDirection(headerSize, self.scrollDirection); + const CGFloat footerLengthInScrollDirection = hideHeaderWhenItemsEmpty ? 0 : CGSizeGetLengthInDirection(footerSize, self.scrollDirection); + const BOOL headerExists = headerLengthInScrollDirection > 0; + const BOOL footerExists = footerLengthInScrollDirection > 0; + + // start the section accounting for the header size + // header length in scroll direction is subtracted from the sectionBounds when calculating the header bounds after items are done + // this bumps the first row of items over enough to make room for the header + itemCoordInScrollDirection += headerLengthInScrollDirection; + nextRowCoordInScrollDirection += headerLengthInScrollDirection; + + // add the leading inset in fixed direction in case the section falls on the same row as the previous + // if the section is newlined then the coord in fixed direction is reset + itemCoordInFixedDirection += UIEdgeInsetsLeadingInsetInDirection(insets, fixedDirection); + + // the farthest in the fixed direction the frame of an item in this section can go + const CGFloat maxCoordinateInFixedDirection = CGRectGetLengthInDirection(contentInsetAdjustedCollectionViewBounds, fixedDirection) - UIEdgeInsetsTrailingInsetInDirection(insets, fixedDirection); + + for (NSInteger item = 0; item < itemCount; item++) { + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:item inSection:section]; + const CGSize size = [delegate collectionView:collectionView layout:self sizeForItemAtIndexPath:indexPath]; + + IGAssert(CGSizeGetLengthInDirection(size, fixedDirection) <= paddedLengthInFixedDirection + || fabs(CGSizeGetLengthInDirection(size, fixedDirection) - paddedLengthInFixedDirection) < FLT_EPSILON, + @"%@ of item %li in section %li (%.0f pt) must be less than or equal to container (%.0f pt) accounting for section insets %@", + self.scrollDirection == UICollectionViewScrollDirectionVertical ? @"Width" : @"Height", + (long)item, + (long)section, + CGSizeGetLengthInDirection(size, fixedDirection), + CGRectGetLengthInDirection(contentInsetAdjustedCollectionViewBounds, fixedDirection), + NSStringFromUIEdgeInsets(insets)); + + CGFloat itemLengthInFixedDirection = MIN(CGSizeGetLengthInDirection(size, fixedDirection), paddedLengthInFixedDirection); + + // if the origin and length in fixed direction of the item busts the size of the container + // or if this is the first item and the header has a non-zero size + // newline to the next row and reset + // define epsilon to avoid float overflow issue + const CGFloat epsilon = 1.0; + if (itemCoordInFixedDirection + itemLengthInFixedDirection > maxCoordinateInFixedDirection + epsilon + || (item == 0 && headerExists)) { + itemCoordInScrollDirection = nextRowCoordInScrollDirection; + itemCoordInFixedDirection = UIEdgeInsetsLeadingInsetInDirection(insets, fixedDirection); + + + // if newlining, always append line spacing unless its the very first item of the section + if (item > 0) { + itemCoordInScrollDirection += lineSpacing; + } + } + + const CGFloat distanceToEdge = paddedLengthInFixedDirection - (itemCoordInFixedDirection + itemLengthInFixedDirection); + if (self.stretchToEdge && distanceToEdge > 0 && distanceToEdge <= epsilon) { + itemLengthInFixedDirection = paddedLengthInFixedDirection - itemCoordInFixedDirection; + } + + const CGRect rawFrame = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ? + CGRectMake(itemCoordInFixedDirection, + itemCoordInScrollDirection + insets.top, + itemLengthInFixedDirection, + size.height) : + CGRectMake(itemCoordInScrollDirection + insets.left, + itemCoordInFixedDirection, + size.width, + itemLengthInFixedDirection); + const CGRect frame = IGListRectIntegralScaled(rawFrame); + + _sectionData[section].itemBounds[item] = frame; + + // track the max size of the row to find the coord of the next row, adjust for leading inset while iterating items + nextRowCoordInScrollDirection = MAX(CGRectGetMaxInDirection(frame, self.scrollDirection) - UIEdgeInsetsLeadingInsetInDirection(insets, self.scrollDirection), nextRowCoordInScrollDirection); + + // increase the rolling coord in fixed direction appropriately and add item spacing for all items on the same row + itemCoordInFixedDirection += itemLengthInFixedDirection + interitemSpacing; + + // union the rolling section bounds + if (item == 0) { + rollingSectionBounds = frame; + } else { + rollingSectionBounds = CGRectUnion(rollingSectionBounds, frame); + } + } + + const CGRect headerBounds = self.scrollDirection == UICollectionViewScrollDirectionVertical ? + CGRectMake(insets.left, + itemsEmpty ? CGRectGetMaxY(rollingSectionBounds) : CGRectGetMinY(rollingSectionBounds) - headerSize.height, + paddedLengthInFixedDirection, + hideHeaderWhenItemsEmpty ? 0 : headerSize.height) : + CGRectMake(itemsEmpty ? CGRectGetMaxX(rollingSectionBounds) : CGRectGetMinX(rollingSectionBounds) - headerSize.width, + insets.top, + hideHeaderWhenItemsEmpty ? 0 : headerSize.width, + paddedLengthInFixedDirection); + + _sectionData[section].headerBounds = headerBounds; + + if (itemsEmpty) { + rollingSectionBounds = headerBounds; + } + + const CGRect footerBounds = (self.scrollDirection == UICollectionViewScrollDirectionVertical) ? + CGRectMake(insets.left, + CGRectGetMaxY(rollingSectionBounds), + paddedLengthInFixedDirection, + hideHeaderWhenItemsEmpty ? 0 : footerSize.height) : + CGRectMake(CGRectGetMaxX(rollingSectionBounds) + insets.right, + insets.top, + hideHeaderWhenItemsEmpty ? 0 : footerSize.width, + paddedLengthInFixedDirection); + + _sectionData[section].footerBounds = footerBounds; + + // union the header before setting the bounds of the section + // only do this when the header has a size, otherwise the union stretches to box empty space + if (headerExists) { + rollingSectionBounds = CGRectUnion(rollingSectionBounds, headerBounds); + } + if (footerExists) { + rollingSectionBounds = CGRectUnion(rollingSectionBounds, footerBounds); + } + + _sectionData[section].bounds = rollingSectionBounds; + _sectionData[section].insets = insets; + + // bump the coord for the next section with the right insets + itemCoordInFixedDirection += UIEdgeInsetsTrailingInsetInDirection(insets, fixedDirection); + + // find the farthest point in the section and add the trailing inset to find the next row's coord + nextRowCoordInScrollDirection = MAX(nextRowCoordInScrollDirection, CGRectGetMaxInDirection(rollingSectionBounds, self.scrollDirection) + UIEdgeInsetsTrailingInsetInDirection(insets, self.scrollDirection)); + + // keep track of coordinates for partial invalidation + _sectionData[section].lastItemCoordInScrollDirection = itemCoordInScrollDirection; + _sectionData[section].lastItemCoordInFixedDirection = itemCoordInFixedDirection; + _sectionData[section].lastNextRowCoordInScrollDirection = nextRowCoordInScrollDirection; + } + + _minimumInvalidatedSection = NSNotFound; +} + +- (NSRange)_rangeOfSectionsInRect:(CGRect)rect { + NSRange result = NSMakeRange(NSNotFound, 0); + + const NSInteger sectionCount = _sectionData.size(); + for (NSInteger section = 0; section < sectionCount; section++) { + IGListSectionEntry entry = _sectionData[section]; + if (entry.isValid() && CGRectIntersectsRect(entry.bounds, rect)) { + const NSRange sectionRange = NSMakeRange(section, 1); + if (result.location == NSNotFound) { + result = sectionRange; + } else { + result = NSUnionRange(result, sectionRange); + } + } + } + + return result; +} + +- (void)_resetSupplementaryAttributesCache { + [_supplementaryAttributesCache enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSMutableDictionary * _Nonnull attributesCache, BOOL * _Nonnull stop) { + [attributesCache removeAllObjects]; + }]; +} + +#pragma mark - Minimum Invalidated Section + +- (void)didModifySection:(NSInteger)modifiedSection { + _minimumInvalidatedSection = IGListMergeMinimumInvalidatedSection(_minimumInvalidatedSection, modifiedSection); +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayoutCompatible.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayoutCompatible.h new file mode 100644 index 0000000..716ab65 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListCollectionViewLayoutCompatible.h @@ -0,0 +1,33 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + A protocol for layouts that defines interaction with an IGListCollectionView, for recieving updated section indexes. + */ +NS_SWIFT_NAME(ListCollectionViewLayoutCompatible) +@protocol IGListCollectionViewLayoutCompatible + +/** + Called to notify the layout that a specific section was modified before invalidation. This can be used to optimize + layout re-calculation. + + @note When updating a collection view (ex: calling `-insertSections`), `-invalidateLayoutWithContext` gets called on + the layout object. However, the invalidation context doesn't provide details on which index paths are being modified, + which typically forces a full layout re-calculation. Layouts can use this method to keep track of which section + actually needs to be updated on the following `-invalidateLayoutWithContext`. See `IGListCollectionView`. + + @param modifiedSection The section that was modified. + */ +- (void)didModifySection:(NSInteger)modifiedSection; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListDisplayDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListDisplayDelegate.h new file mode 100644 index 0000000..8315f4a --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListDisplayDelegate.h @@ -0,0 +1,65 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListAdapter; +@class IGListSectionController; + + + +NS_ASSUME_NONNULL_BEGIN + +/** + Implement this protocol to receive display events for a section controller when it is on screen. + */ +NS_SWIFT_NAME(ListDisplayDelegate) +@protocol IGListDisplayDelegate + +/** + Tells the delegate that the specified section controller is about to be displayed. + + @param listAdapter The list adapter for the section controller. + @param sectionController The section controller about to be displayed. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController; + +/** + Tells the delegate that the specified section controller is no longer being displayed. + + @param listAdapter The list adapter for the section controller. + @param sectionController The section controller that is no longer displayed. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController *)sectionController; + +/** + Tells the delegate that a cell in the specified list is about to be displayed. + + @param listAdapter The list adapter in which the cell will display. + @param sectionController The section controller that is displaying the cell. + @param cell The cell about to be displayed. + @param index The index of the cell in the section. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter willDisplaySectionController:(IGListSectionController *)sectionController + cell:(UICollectionViewCell *)cell + atIndex:(NSInteger)index; + +/** + Tells the delegate that a cell in the specified list is no longer being displayed. + + @param listAdapter The list adapter in which the cell was displayed. + @param sectionController The section controller that is no longer displaying the cell. + @param cell The cell that is no longer displayed. + @param index The index of the cell in the section. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didEndDisplayingSectionController:(IGListSectionController *)sectionController + cell:(UICollectionViewCell *)cell + atIndex:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListGenericSectionController.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListGenericSectionController.h new file mode 100644 index 0000000..3fa8953 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListGenericSectionController.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + This class adds a helper layer to `IGListSectionController` to automatically store a generic object in + `didUpdateToObject:`. + */ +NS_SWIFT_NAME(ListGenericSectionController) +@interface IGListGenericSectionController<__covariant ObjectType> : IGListSectionController + +/** + The object mapped to this section controller. Matches the object provided in + `[IGListAdapterDataSource listAdapter:sectionControllerForObject:]` when this section controller was created and + returned. + + @note This object is briefly `nil` between initialization and the first call to `didUpdateToObject:`. After that, it is + safe to assume that this is non-`nil`. + */ +@property (nonatomic, strong, nullable, readonly) ObjectType object; + +/** + Updates the section controller to a new object. + + @param object The object mapped to this section controller. + + @note This `IGListSectionController` subclass sets its object in this method, so any overrides **must call super**. + */ +- (void)didUpdateToObject:(id)object NS_REQUIRES_SUPER; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListGenericSectionController.m b/Demo/Pods/IGListKit/Source/IGListKit/IGListGenericSectionController.m new file mode 100644 index 0000000..8d15af8 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListGenericSectionController.m @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListGenericSectionController.h" + +@implementation IGListGenericSectionController + +- (void)didUpdateToObject:(id)object { + _object = object; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListKit.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListKit.h new file mode 100644 index 0000000..c352c4e --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListKit.h @@ -0,0 +1,65 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +/** + * Project version number for IGListKit. + */ +FOUNDATION_EXPORT double IGListKitVersionNumber; + +/** + * Project version string for IGListKit. + */ +FOUNDATION_EXPORT const unsigned char IGListKitVersionString[]; + +#if TARGET_OS_EMBEDDED || TARGET_OS_SIMULATOR + +// iOS and tvOS only: + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#endif + +// Shared (iOS, tvOS, macOS compatible): + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListReloadDataUpdater.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListReloadDataUpdater.h new file mode 100644 index 0000000..3dc98da --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListReloadDataUpdater.h @@ -0,0 +1,27 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An `IGListReloadDataUpdater` is a concrete type that conforms to `IGListUpdatingDelegate`. + It is an out-of-box updater for `IGListAdapter` objects to use. + + @note This updater performs simple, synchronous updates using `-[UICollectionView reloadData]`. + */ +IGLK_SUBCLASSING_RESTRICTED +NS_SWIFT_NAME(ListReloadDataUpdater) +@interface IGListReloadDataUpdater : NSObject + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListReloadDataUpdater.m b/Demo/Pods/IGListKit/Source/IGListKit/IGListReloadDataUpdater.m new file mode 100644 index 0000000..bf1fe07 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListReloadDataUpdater.m @@ -0,0 +1,82 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@implementation IGListReloadDataUpdater + +#pragma mark - IGListUpdatingDelegate + +- (NSPointerFunctions *)objectLookupPointerFunctions { + return [NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsObjectPersonality]; +} + +- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + fromObjects:(NSArray *)fromObjects + toObjectsBlock:(IGListToObjectBlock)toObjectsBlock + animated:(BOOL)animated + objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock + completion:(IGListUpdatingCompletion)completion { + if (toObjectsBlock != nil) { + NSArray *toObjects = toObjectsBlock() ?: @[]; + objectTransitionBlock(toObjects); + } + [self _synchronousReloadDataWithCollectionView:collectionViewBlock()]; + if (completion) { + completion(YES); + } +} + +- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + animated:(BOOL)animated + itemUpdates:(IGListItemUpdateBlock)itemUpdates + completion:(IGListUpdatingCompletion)completion { + itemUpdates(); + [self _synchronousReloadDataWithCollectionView:collectionViewBlock()]; + if (completion) { + completion(YES); + } +} + +- (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { + [self _synchronousReloadDataWithCollectionView:collectionView]; +} + +- (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths { + [self _synchronousReloadDataWithCollectionView:collectionView]; +} + +- (void)moveItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { + [self _synchronousReloadDataWithCollectionView:collectionView]; +} + +- (void)reloadItemInCollectionView:(UICollectionView *)collectionView fromIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath { + [self _synchronousReloadDataWithCollectionView:collectionView]; +} + +- (void)moveSectionInCollectionView:(UICollectionView *)collectionView fromIndex:(NSInteger)fromIndex toIndex:(NSInteger)toIndex { + [self _synchronousReloadDataWithCollectionView:collectionView]; +} + +- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock completion:(IGListUpdatingCompletion)completion { + reloadUpdateBlock(); + [self _synchronousReloadDataWithCollectionView:collectionViewBlock()]; + if (completion) { + completion(YES); + } +} + +- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections { + [self _synchronousReloadDataWithCollectionView:collectionView]; +} + +- (void)_synchronousReloadDataWithCollectionView:(UICollectionView *)collectionView { + [collectionView reloadData]; + [collectionView layoutIfNeeded]; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListScrollDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListScrollDelegate.h new file mode 100644 index 0000000..b53d2b3 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListScrollDelegate.h @@ -0,0 +1,63 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListAdapter; +@class IGListSectionController; + + + +NS_ASSUME_NONNULL_BEGIN + +/** + Implement this protocol to receive display events for a section controller when it is on screen. + */ +NS_SWIFT_NAME(ListScrollDelegate) +@protocol IGListScrollDelegate + +/** + Tells the delegate that the section controller was scrolled on screen. + + @param listAdapter The list adapter whose collection view was scrolled. + @param sectionController The visible section controller that was scrolled. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didScrollSectionController:(IGListSectionController *)sectionController; + +/** + Tells the delegate that the section controller will be dragged on screen. + + @param listAdapter The list adapter whose collection view will drag. + @param sectionController The visible section controller that will drag. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter willBeginDraggingSectionController:(IGListSectionController *)sectionController; + +/** + Tells the delegate that the section controller did end dragging on screen. + + @param listAdapter The list adapter whose collection view ended dragging. + @param sectionController The visible section controller that ended dragging. + @param decelerate 'Yes' if the scrolling movement will continue, but decelerate, after a touch-up gesture during a + dragging operation. If the value is 'No', scrolling stops immediately upon touch-up. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didEndDraggingSectionController:(IGListSectionController *)sectionController willDecelerate:(BOOL)decelerate; + +@optional + +/** + Tells the delegate that the section controller did end decelerating on screen. + + @param listAdapter The list adapter whose collection view ended decelerating. + @param sectionController The visible section controller that ended decelerating. + + @note This method is `@optional` until the next breaking-change release. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter didEndDeceleratingSectionController:(IGListSectionController *)sectionController; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListSectionController.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListSectionController.h new file mode 100644 index 0000000..d6e5824 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListSectionController.h @@ -0,0 +1,239 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + The base class for section controllers used in a list. This class is intended to be subclassed. + */ +NS_SWIFT_NAME(ListSectionController) +@interface IGListSectionController : NSObject + +/** + Returns the number of items in the section. + + @return A count of items in the list. + + @note The count returned is used to drive the number of cells displayed for this section controller. The default + implementation returns 1. **Calling super is not required.** + */ +- (NSInteger)numberOfItems; + +/** + The specific size for the item at the specified index. + + @param index The row index of the item. + + @return The size for the item at index. + + @note The returned size is not guaranteed to be used. The implementation may query sections for their + layout information at will, or use its own layout metrics. For example, consider a dynamic-text sized list versus a + fixed height-and-width grid. The former will ask each section for a size, and the latter will likely not. The default + implementation returns size zero. **Calling super is not required.** + */ +- (CGSize)sizeForItemAtIndex:(NSInteger)index; + +/** + Return a dequeued cell for a given index. + + @param index The index of the requested row. + + @return A configured `UICollectionViewCell` subclass. + + @note This is your opportunity to do any cell setup and configuration. The infrastructure requests a cell when it + will be used on screen. You should never allocate new cells in this method, instead use the provided adapter to call + one of the dequeue methods on the IGListCollectionContext. The default implementation will assert. **You must override + this method without calling super.** + + @warning Don't call this method to obtain a reference to currently dequeued cells: a new cell will be dequeued + and returned, rather than the existing cell that you may have intended to retrieve. Instead, you can call + `-cellForItemAtIndex:sectionController:` on `IGListCollectionContext` to obtain active cell references. + */ +- (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index; + +/** + Updates the section controller to a new object. + + @param object The object mapped to this section controller. + + @note When this method is called, all available contexts and configurations have been set for the section + controller. This method will only be called when the object instance has changed, including from `nil` or a previous + object. **Calling super is not required.** + */ +- (void)didUpdateToObject:(id)object; + +/** + Tells the section controller that the cell at the specified index path was selected. + + @param index The index of the selected cell. + + @note The default implementation does nothing. **Calling super is not required.** + */ +- (void)didSelectItemAtIndex:(NSInteger)index; + +/** + Tells the section controller that the cell at the specified index path was deselected. + + @param index The index of the deselected cell. + + @note The default implementation does nothing. **Calling super is not required.** + */ +- (void)didDeselectItemAtIndex:(NSInteger)index; + +/** + Tells the section controller that the cell at the specified index path was highlighted. + + @param index The index of the highlighted cell. + + @note The default implementation does nothing. **Calling super is not required.** + */ +- (void)didHighlightItemAtIndex:(NSInteger)index; + +/** + Tells the section controller that the cell at the specified index path was unhighlighted. + + @param index The index of the unhighlighted cell. + + @note The default implementation does nothing. **Calling super is not required.** + */ +- (void)didUnhighlightItemAtIndex:(NSInteger)index; + +/** + Identifies whether an object can be moved through interactive reordering. + + @param index The index of the object in the list. + + @return `YES` if the object is allowed to move, otherwise `NO`. + + @note Interactive reordering is supported both for items within a single section, as well as for reordering sections + themselves when sections contain only one item. The default implementation returns false. + */ +- (BOOL)canMoveItemAtIndex:(NSInteger)index; + +/** + Notifies the section that a list object should move within a section as the result of interactive reordering. + + @param sourceIndex The starting index of the object. + @param destinationIndex The ending index of the object. + + @note this method must be implemented if interactive reordering is enabled. + */ +- (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex NS_AVAILABLE_IOS(9_0); + +/** + The view controller housing the adapter that created this section controller. + + @note Use this view controller to push, pop, present, or do other custom transitions. + + @warning It is considered very bad practice to cast this to a known view controller + and call methods on it other than for navigations and transitions. + */ +@property (nonatomic, weak, nullable, readonly) UIViewController *viewController; + +/** + A context object for interacting with the collection. + + Use this property for accessing the collection size, dequeuing cells, reloading, inserting, deleting, etc. + */ +@property (nonatomic, weak, nullable, readonly) id collectionContext; + +/** + Returns the section within the list for this section controller. + + @note This value also relates to the section within a `UICollectionView` that this section controller's cells belong. + It also relates to the `-[NSIndexPath section]` value for individual cells within the collection view. + */ +@property (nonatomic, assign, readonly) NSInteger section; + +/** + Returns `YES` if the section controller is the first section in the list, `NO` otherwise. + */ +@property (nonatomic, assign, readonly) BOOL isFirstSection; + +/** + Returns `YES` if the section controller is the last section in the list, `NO` otherwise. + */ +@property (nonatomic, assign, readonly) BOOL isLastSection; + +/** + The margins used to lay out content in the section controller. + + @see `-[UICollectionViewFlowLayout sectionInset]`. + */ +@property (nonatomic, assign) UIEdgeInsets inset; + +/** + The minimum spacing to use between rows of items. + + @see `-[UICollectionViewFlowLayout minimumLineSpacing]`. + */ +@property (nonatomic, assign) CGFloat minimumLineSpacing; + +/** + The minimum spacing to use between items in the same row. + + @see `-[UICollectionViewFlowLayout minimumInteritemSpacing]`. + */ +@property (nonatomic, assign) CGFloat minimumInteritemSpacing; + +/** + The supplementary view source for the section controller. Can be `nil`. + + @return An object that conforms to `IGListSupplementaryViewSource` or `nil`. + + @note You may wish to return `self` if your section controller implements this protocol. + */ +@property (nonatomic, weak, nullable) id supplementaryViewSource; + +/** + An object that handles display events for the section controller. Can be `nil`. + + @return An object that conforms to `IGListDisplayDelegate` or `nil`. + + @note You may wish to return `self` if your section controller implements this protocol. + */ +@property (nonatomic, weak, nullable) id displayDelegate; + +/** + An object that handles working range events for the section controller. Can be `nil`. + + @return An object that conforms to `IGListWorkingRangeDelegate` or `nil`. + + @note You may wish to return `self` if your section controller implements this protocol. + */ +@property (nonatomic, weak, nullable) id workingRangeDelegate; + +/** + An object that handles scroll events for the section controller. Can be `nil`. + + @return An object that conforms to `IGListScrollDelegate` or `nil`. + + @note You may wish to return `self` if your section controller implements this protocol. + */ +@property (nonatomic, weak, nullable) id scrollDelegate; + +/** + An object that handles transition events for the section controller. Can be `nil`. + + @return An object that conforms to `IGListTransitionDelegat` or `nil`. + + @note You may wish to return `self` if your section controller implements this protocol. + */ +@property (nonatomic, weak, nullable) id transitionDelegate; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListSectionController.m b/Demo/Pods/IGListKit/Source/IGListKit/IGListSectionController.m new file mode 100644 index 0000000..f880339 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListSectionController.m @@ -0,0 +1,103 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListSectionControllerInternal.h" + +#import +#import + +static NSString * const kIGListSectionControllerThreadKey = @"kIGListSectionControllerThreadKey"; + +@interface IGListSectionControllerThreadContext : NSObject +@property (nonatomic, weak) UIViewController *viewController; +@property (nonatomic, weak) id collectionContext; +@end +@implementation IGListSectionControllerThreadContext +@end + +static NSMutableArray *threadContextStack(void) { + IGAssertMainThread(); + NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary]; + NSMutableArray *stack = threadDictionary[kIGListSectionControllerThreadKey]; + if (stack == nil) { + stack = [NSMutableArray new]; + threadDictionary[kIGListSectionControllerThreadKey] = stack; + } + return stack; +} + +void IGListSectionControllerPushThread(UIViewController *viewController, id collectionContext) { + IGListSectionControllerThreadContext *context = [IGListSectionControllerThreadContext new]; + context.viewController = viewController; + context.collectionContext = collectionContext; + + [threadContextStack() addObject:context]; +} + +void IGListSectionControllerPopThread(void) { + NSMutableArray *stack = threadContextStack(); + IGAssert(stack.count > 0, @"IGListSectionController thread stack is empty"); + [stack removeLastObject]; +} + +@implementation IGListSectionController + +- (instancetype)init { + if (self = [super init]) { + IGListSectionControllerThreadContext *context = [threadContextStack() lastObject]; + _viewController = context.viewController; + _collectionContext = context.collectionContext; + + if (_collectionContext == nil) { + IGLKLog(@"Warning: Creating %@ outside of -[IGListAdapterDataSource listAdapter:sectionControllerForObject:]. Collection context and view controller will be set later.", + NSStringFromClass([self class])); + } + + _minimumInteritemSpacing = 0.0; + _minimumLineSpacing = 0.0; + _inset = UIEdgeInsetsZero; + _section = NSNotFound; + } + return self; +} + +- (NSInteger)numberOfItems { + return 1; +} + +- (CGSize)sizeForItemAtIndex:(NSInteger)index { + return CGSizeZero; +} + +- (__kindof UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { + IGFailAssert(@"Section controller %@ must override %s:", self, __PRETTY_FUNCTION__); + return nil; +} + +- (void)didUpdateToObject:(id)object {} + +- (void)didSelectItemAtIndex:(NSInteger)index {} + +- (void)didDeselectItemAtIndex:(NSInteger)index {} + +- (void)didHighlightItemAtIndex:(NSInteger)index {} + +- (void)didUnhighlightItemAtIndex:(NSInteger)index {} + +- (BOOL)canMoveItemAtIndex:(NSInteger)index { + return NO; +} + +- (BOOL)canMoveItemAtIndex:(NSInteger)sourceItemIndex toIndex:(NSInteger)destinationItemIndex { + return [self canMoveItemAtIndex:sourceItemIndex]; +} + +- (void)moveObjectFromIndex:(NSInteger)sourceIndex toIndex:(NSInteger)destinationIndex { + IGFailAssert(@"Section controller %@ must override %s if interactive reordering is enabled.", self, __PRETTY_FUNCTION__); +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListSingleSectionController.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListSingleSectionController.h new file mode 100644 index 0000000..2618fcf --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListSingleSectionController.h @@ -0,0 +1,144 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + + +/** + A block used to configure cells. + + @param item The model with which to configure the cell. + @param cell The cell to configure. + */ +NS_SWIFT_NAME(ListSingleSectionCellConfigureBlock) +typedef void (^IGListSingleSectionCellConfigureBlock)(id item, __kindof UICollectionViewCell *cell); + +/** + A block that returns the size for the cell given the collection context. + + @param item The model for the section. + @param collectionContext The collection context for the section. + + @return The size for the cell. + */ +NS_SWIFT_NAME(ListSingleSectionCellSizeBlock) +typedef CGSize (^IGListSingleSectionCellSizeBlock)(id item, id _Nullable collectionContext); + +@class IGListSingleSectionController; + +/** + A delegate that can receive selection events on an `IGListSingleSectionController`. + */ +NS_SWIFT_NAME(ListSingleSectionControllerDelegate) +@protocol IGListSingleSectionControllerDelegate + +/** + Tells the delegate that the section controller was selected. + + @param sectionController The section controller that was selected. + @param object The model for the given section. + */ +- (void)didSelectSectionController:(IGListSingleSectionController *)sectionController + withObject:(id)object; + +@optional + +/** + Tells the delegate that the section controller was deselected. + + @param sectionController The section controller that was deselected. + @param object The model for the given section. + + @note Method is `@optional` until the 4.0.0 release where it will become required. + */ +- (void)didDeselectSectionController:(IGListSingleSectionController *)sectionController + withObject:(id)object; + +@end + +/** + This section controller is meant to make building simple, single-cell lists easier. By providing the type of cell, a block + to configure the cell, and a block to return the size of a cell, you can use an `IGListAdapter`-powered list with a + simpler architecture. + */ +IGLK_SUBCLASSING_RESTRICTED +NS_SWIFT_NAME(ListSingleSectionController) +@interface IGListSingleSectionController : IGListSectionController + +/** + Creates a new section controller for a given cell type that will always have only one cell when present in a list. + + @param cellClass The `UICollectionViewCell` subclass for the single cell. + @param configureBlock A block that configures the cell with the item given to the section controller. + @param sizeBlock A block that returns the size for the cell given the collection context. + + @return A new section controller. + + @warning Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter + (usually `self`) or the `IGListAdapter`. Pass in locally scoped objects or use `weak` references! + */ +- (instancetype)initWithCellClass:(Class)cellClass + configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock + sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock; + +/** + Creates a new section controller for a given nib name and bundle that will always have only one cell when present in a list. + + @param nibName The name of the nib file for the single cell. + @param bundle The bundle in which to search for the nib file. If `nil`, this method looks for the file in the main bundle. + @param configureBlock A block that configures the cell with the item given to the section controller. + @param sizeBlock A block that returns the size for the cell given the collection context. + + @return A new section controller. + + @warning Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter + (usually `self`) or the `IGListAdapter`. Pass in locally scoped objects or use `weak` references! + */ +- (instancetype)initWithNibName:(NSString *)nibName + bundle:(nullable NSBundle *)bundle + configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock + sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock; + +/** + Creates a new section controller for a given storyboard cell identifier that will always have only one cell when present in a list. + + @param identifier The identifier of the cell prototype in storyboard. + @param configureBlock A block that configures the cell with the item given to the section controller. + @param sizeBlock A block that returns the size for the cell given the collection context. + + @return A new section controller. + + @warning Be VERY CAREFUL not to create retain cycles by holding strong references to: the object that owns the adapter + (usually `self`) or the `IGListAdapter`. Pass in locally scoped objects or use `weak` references! + */ +- (instancetype)initWithStoryboardCellIdentifier:(NSString *)identifier + configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock + sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock; + +/** + An optional delegate that handles selection and deselection. + */ +@property (nonatomic, weak, nullable) id selectionDelegate; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListSingleSectionController.m b/Demo/Pods/IGListKit/Source/IGListKit/IGListSingleSectionController.m new file mode 100644 index 0000000..5206279 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListSingleSectionController.m @@ -0,0 +1,116 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListSingleSectionController.h" + +#import + +@interface IGListSingleSectionController () + +@property (nonatomic, strong, readonly) NSString *nibName; +@property (nonatomic, strong, readonly) NSBundle *bundle; +@property (nonatomic, strong, readonly) NSString *identifier; +@property (nonatomic, strong, readonly) Class cellClass; +@property (nonatomic, strong, readonly) IGListSingleSectionCellConfigureBlock configureBlock; +@property (nonatomic, strong, readonly) IGListSingleSectionCellSizeBlock sizeBlock; + +@property (nonatomic, strong) id item; + +@end + +@implementation IGListSingleSectionController + +- (instancetype)initWithCellClass:(Class)cellClass + configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock + sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock { + IGParameterAssert(cellClass != nil); + IGParameterAssert(configureBlock != nil); + IGParameterAssert(sizeBlock != nil); + if (self = [super init]) { + _cellClass = cellClass; + _configureBlock = [configureBlock copy]; + _sizeBlock = [sizeBlock copy]; + } + return self; +} + +- (instancetype)initWithNibName:(NSString *)nibName + bundle:(NSBundle *)bundle + configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock + sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock { + IGParameterAssert(nibName != nil); + IGParameterAssert(configureBlock != nil); + IGParameterAssert(sizeBlock != nil); + if (self = [super init]) { + _nibName = [nibName copy]; + _bundle = bundle; + _configureBlock = [configureBlock copy]; + _sizeBlock = [sizeBlock copy]; + } + return self; +} + +- (instancetype)initWithStoryboardCellIdentifier:(NSString *)identifier + configureBlock:(IGListSingleSectionCellConfigureBlock)configureBlock + sizeBlock:(IGListSingleSectionCellSizeBlock)sizeBlock { + IGParameterAssert(identifier.length > 0); + IGParameterAssert(configureBlock != nil); + IGParameterAssert(sizeBlock != nil); + if (self = [super init]) { + _identifier = [identifier copy]; + _configureBlock = [configureBlock copy]; + _sizeBlock = [sizeBlock copy]; + } + return self; + +} + +#pragma mark - IGListSectionController Overrides + +- (NSInteger)numberOfItems { + return 1; +} + +- (CGSize)sizeForItemAtIndex:(NSInteger)index { + return self.sizeBlock(self.item, self.collectionContext); +} + +- (UICollectionViewCell *)cellForItemAtIndex:(NSInteger)index { + IGParameterAssert(index == 0); + id cell; + id collectionContext = self.collectionContext; + if ([self.nibName length] > 0) { + cell = [collectionContext dequeueReusableCellWithNibName:self.nibName + bundle:self.bundle + forSectionController:self + atIndex:index]; + } else if ([self.identifier length] > 0) { + cell = [collectionContext dequeueReusableCellFromStoryboardWithIdentifier:self.identifier + forSectionController:self + atIndex:index]; + } else { + cell = [collectionContext dequeueReusableCellOfClass:self.cellClass forSectionController:self atIndex:index]; + } + self.configureBlock(self.item, cell); + return cell; +} + +- (void)didUpdateToObject:(id)object { + self.item = object; +} + +- (void)didSelectItemAtIndex:(NSInteger)index { + [self.selectionDelegate didSelectSectionController:self withObject:self.item]; +} + +- (void)didDeselectItemAtIndex:(NSInteger)index { + if ([self.selectionDelegate respondsToSelector:@selector(didDeselectSectionController:withObject:)]) { + [self.selectionDelegate didDeselectSectionController:self withObject:self.item]; + } +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListSupplementaryViewSource.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListSupplementaryViewSource.h new file mode 100644 index 0000000..4c22f03 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListSupplementaryViewSource.h @@ -0,0 +1,53 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + Conform to this protocol to provide information about a list's supplementary views. This data is used in + `IGListAdapter` which then configures and maintains a `UICollectionView`. The supplementary API reflects that in + `UICollectionView`, `UICollectionViewLayout`, and `UICollectionViewDataSource`. + */ +NS_SWIFT_NAME(ListSupplementaryViewSource) +@protocol IGListSupplementaryViewSource + +/** + Asks the SupplementaryViewSource for an array of supported element kinds. + + @return An array of element kind strings that the supplementary source handles. + */ +- (NSArray *)supportedElementKinds; + +/** + Asks the SupplementaryViewSource for a configured supplementary view for the specified kind and index. + + @param elementKind The kind of supplementary view being requested + @param index The index for the supplementary veiw being requested. + + @note This is your opportunity to do any supplementary view setup and configuration. + + @warning You should never allocate new views in this method. Instead deque a view from the `IGListCollectionContext`. + */ +- (__kindof UICollectionReusableView *)viewForSupplementaryElementOfKind:(NSString *)elementKind + atIndex:(NSInteger)index; + +/** + Asks the SupplementaryViewSource for the size of a supplementary view for the given kind and index path. + + @param elementKind The kind of supplementary view. + @param index The index of the requested view. + + @return The size for the supplementary view. + */ +- (CGSize)sizeForSupplementaryViewOfKind:(NSString *)elementKind + atIndex:(NSInteger)index; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListTransitionDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListTransitionDelegate.h new file mode 100644 index 0000000..db87ff6 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListTransitionDelegate.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + Conform to `IGListTransitionDelegate` to provide customized layout information for a collection view. + */ +@protocol IGListTransitionDelegate + +/** + Asks the delegate to customize and return the starting layout information for an item being inserted into the collection view. + + @param listAdapter The adapter controlling the list. + @param attributes The starting layout information for an item being inserted into the collection view. + @param sectionController The section controller to perform the transition on. + @param index The index of the item being inserted. + */ +- (UICollectionViewLayoutAttributes *)listAdapter:(IGListAdapter *)listAdapter customizedInitialLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes sectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; + +/** + Asks the delegate to customize and return the final layout information for an item that is about to be removed from the collection view. + + @param listAdapter The adapter controlling the list. + @param attributes The final layout information for an item that is about to be removed from the collection view. + @param sectionController The section controller to perform the transition on. + @param index The index of the item being deleted. + */ +- (UICollectionViewLayoutAttributes *)listAdapter:(IGListAdapter *)listAdapter customizedFinalLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes sectionController:(IGListSectionController *)sectionController atIndex:(NSInteger)index; + +@end + diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListUpdatingDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListUpdatingDelegate.h new file mode 100644 index 0000000..dc8af39 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListUpdatingDelegate.h @@ -0,0 +1,179 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@protocol IGListDiffable; + +NS_ASSUME_NONNULL_BEGIN + +/** + A completion block to execute when updates are finished. + + @param finished Specifies whether or not the update finished. + */ +NS_SWIFT_NAME(ListUpdatingCompletion) +typedef void (^IGListUpdatingCompletion)(BOOL finished); + +/** + A block to be called when the adapter applies changes to the collection view. + + @param toObjects The new objects in the collection. + */ +NS_SWIFT_NAME(ListObjectTransitionBlock) +typedef void (^IGListObjectTransitionBlock)(NSArray *toObjects); + +/// A block that contains all of the updates. +NS_SWIFT_NAME(ListItemUpdateBlock) +typedef void (^IGListItemUpdateBlock)(void); + +/// A block to be called when an adapter reloads the collection view. +NS_SWIFT_NAME(ListReloadUpdateBlock) +typedef void (^IGListReloadUpdateBlock)(void); + +/// A block that returns an array of objects to transition to. +NS_SWIFT_NAME(ListToObjectBlock) +typedef NSArray * _Nullable (^IGListToObjectBlock)(void); + +/// A block that returns a collection view to perform updates on. +NS_SWIFT_NAME(ListCollectionViewBlock) +typedef UICollectionView * _Nullable (^IGListCollectionViewBlock)(void); + +/** + Implement this protocol in order to handle both section and row based update events. Implementation should forward or + coalesce these events to a backing store or collection. + */ +NS_SWIFT_NAME(ListUpdatingDelegate) +@protocol IGListUpdatingDelegate + +/** + Asks the delegate for the pointer functions for looking up an object in a collection. + + @return Pointer functions for looking up an object in a collection. + + @note Since the updating delegate is responsible for transitioning between object sets, it becomes the "source of + truth" for how objects and their corresponding section controllers are mapped. This allows the updater to control if + objects are looked up by pointer, or more traditionally, with `-hash`/`-isEqual`. + + For behavior similar to `NSDictionary`, simply return + `+[NSPointerFunctions pointerFunctionsWithOptions:NSPointerFunctionsObjectPersonality]`. + */ +- (NSPointerFunctions *)objectLookupPointerFunctions; + +/** + Tells the delegate to perform a section transition from an old array of objects to a new one. + + @param collectionViewBlock A block returning the collecion view to perform updates on. + @param fromObjects The previous objects in the collection view. Objects must conform to `IGListDiffable`. + @param toObjectsBlock A block returning the new objects in the collection view. Objects must conform to `IGListDiffable`. + @param animated A flag indicating if the transition should be animated. + @param objectTransitionBlock A block that must be called when the adapter applies changes to the collection view. + @param completion A completion block to execute when the update is finished. + + @note Implementations determine how to transition between objects. You can perform a diff on the objects, reload + each section, or simply call `-reloadData` on the collection view. In the end, the collection view must be setup with a + section for each object in the `toObjects` array. + + The `objectTransitionBlock` block should be called prior to making any `UICollectionView` updates, passing in the `toObjects` + that the updater is applying. + */ +- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + fromObjects:(nullable NSArray> *)fromObjects + toObjectsBlock:(nullable IGListToObjectBlock)toObjectsBlock + animated:(BOOL)animated + objectTransitionBlock:(IGListObjectTransitionBlock)objectTransitionBlock + completion:(nullable IGListUpdatingCompletion)completion; + +/** + Tells the delegate to perform item inserts at the given index paths. + + @param collectionView The collection view on which to perform the transition. + @param indexPaths The index paths to insert items into. + */ +- (void)insertItemsIntoCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths; + +/** + Tells the delegate to perform item deletes at the given index paths. + + @param collectionView The collection view on which to perform the transition. + @param indexPaths The index paths to delete items from. + */ +- (void)deleteItemsFromCollectionView:(UICollectionView *)collectionView indexPaths:(NSArray *)indexPaths; + +/** + Tells the delegate to move an item from and to given index paths. + + @param collectionView The collection view on which to perform the transition. + @param fromIndexPath The source index path of the item to move. + @param toIndexPath The destination index path of the item to move. + */ +- (void)moveItemInCollectionView:(UICollectionView *)collectionView + fromIndexPath:(NSIndexPath *)fromIndexPath + toIndexPath:(NSIndexPath *)toIndexPath; + +/** + Tells the delegate to reload an item from and to given index paths. + + @param collectionView The collection view on which to perform the transition. + @param fromIndexPath The source index path of the item to reload. + @param toIndexPath The destination index path of the item to reload. + + @note Since UICollectionView is unable to handle calling -[UICollectionView reloadItemsAtIndexPaths:] safely while also + executing insert and delete operations in the same batch updates, the updater must know about the origin and + destination of the reload to perform a safe transition. + */ +- (void)reloadItemInCollectionView:(UICollectionView *)collectionView + fromIndexPath:(NSIndexPath *)fromIndexPath + toIndexPath:(NSIndexPath *)toIndexPath; + +/** + Tells the delegate to move a section from and to given indexes. + + @param collectionView The collection view on which to perform the transition. + @param fromIndex The source index of the section to move. + @param toIndex The destination index of the section to move. + */ +- (void)moveSectionInCollectionView:(UICollectionView *)collectionView + fromIndex:(NSInteger)fromIndex + toIndex:(NSInteger)toIndex; + +/** + Completely reload data in the collection. + + @param collectionViewBlock A block returning the collecion view to reload. + @param reloadUpdateBlock A block that must be called when the adapter reloads the collection view. + @param completion A completion block to execute when the reload is finished. + */ +- (void)reloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + reloadUpdateBlock:(IGListReloadUpdateBlock)reloadUpdateBlock + completion:(nullable IGListUpdatingCompletion)completion; + +/** + Completely reload each section in the collection view. + + @param collectionView The collection view to reload. + @param sections The sections to reload. + */ +- (void)reloadCollectionView:(UICollectionView *)collectionView sections:(NSIndexSet *)sections; + +/** + Perform an item update block in the collection view. + + @param collectionViewBlock A block returning the collecion view to perform updates on. + @param animated A flag indicating if the transition should be animated. + @param itemUpdates A block containing all of the updates. + @param completion A completion block to execute when the update is finished. + */ +- (void)performUpdateWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock + animated:(BOOL)animated + itemUpdates:(IGListItemUpdateBlock)itemUpdates + completion:(nullable IGListUpdatingCompletion)completion; + +@end + +NS_ASSUME_NONNULL_END + diff --git a/Demo/Pods/IGListKit/Source/IGListKit/IGListWorkingRangeDelegate.h b/Demo/Pods/IGListKit/Source/IGListKit/IGListWorkingRangeDelegate.h new file mode 100644 index 0000000..40bd292 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/IGListWorkingRangeDelegate.h @@ -0,0 +1,44 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListAdapter; +@class IGListSectionController; + + + +NS_ASSUME_NONNULL_BEGIN + +/** + Implement this protocol to receive working range events for a list. + + The working range is a range *near* the viewport in which you can begin preparing content for display. For example, + you could begin decoding images, or warming text caches. + */ +NS_SWIFT_NAME(ListWorkingRangeDelegate) +@protocol IGListWorkingRangeDelegate + +/** + Notifies the delegate that an section controller will enter the working range. + + @param listAdapter The adapter controlling the list. + @param sectionController The section controller entering the range. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerWillEnterWorkingRange:(IGListSectionController *)sectionController; + +/** + Notifies the delegate that an section controller exited the working range. + + @param listAdapter The adapter controlling the list. + @param sectionController The section controller that exited the range. + */ +- (void)listAdapter:(IGListAdapter *)listAdapter sectionControllerDidExitWorkingRange:(IGListSectionController *)sectionController; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+DebugDescription.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+DebugDescription.h new file mode 100644 index 0000000..e5474df --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+DebugDescription.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface IGListAdapter (DebugDescription) + +- (NSArray *)debugDescriptionLines; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+DebugDescription.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+DebugDescription.m new file mode 100644 index 0000000..55e4164 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+DebugDescription.m @@ -0,0 +1,76 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListAdapter+DebugDescription.h" + +#import "IGListAdapterInternal.h" +#import "IGListAdapterUpdater+DebugDescription.h" +#import "IGListDebuggingUtilities.h" +#import "IGListSectionMap+DebugDescription.h" +#import "UICollectionView+DebugDescription.h" + +@implementation IGListAdapter (DebugDescription) + +- (NSString *)debugDescription { + NSMutableArray *lines = [NSMutableArray arrayWithObject:[NSString stringWithFormat:@"IGListAdapter %p:", self]]; + [lines addObjectsFromArray:IGListDebugIndentedLines([self debugDescriptionLines])]; + return [lines componentsJoinedByString:@"\n"]; +} + +- (NSArray *)debugDescriptionLines { + NSMutableArray *debug = [NSMutableArray new]; +#if IGLK_DEBUG_DESCRIPTION_ENABLED + [debug addObject:[NSString stringWithFormat:@"Updater type: %@", NSStringFromClass(self.updater.class)]]; + [debug addObject:[NSString stringWithFormat:@"Data source: %@", self.dataSource]]; + [debug addObject:[NSString stringWithFormat:@"Collection view delegate: %@", self.collectionViewDelegate]]; + [debug addObject:[NSString stringWithFormat:@"Scroll view delegate: %@", self.scrollViewDelegate]]; + [debug addObject:[NSString stringWithFormat:@"Is in update block: %@", IGListDebugBOOL(self.isInUpdateBlock)]]; + [debug addObject:[NSString stringWithFormat:@"View controller: %@", self.viewController]]; + if (@available(iOS 10.0, tvOS 10, *)) { + [debug addObject:[NSString stringWithFormat:@"Is prefetching enabled: %@", IGListDebugBOOL(self.collectionView.isPrefetchingEnabled)]]; + } + + if (self.registeredCellIdentifiers.count > 0) { + [debug addObject:@"Registered cell identifiers:"]; + [debug addObject:[self.registeredCellIdentifiers description]]; + } + + if (self.registeredNibNames.count > 0) { + [debug addObject:@"Registered nib names:"]; + [debug addObject:[self.registeredNibNames description]]; + } + + if (self.registeredSupplementaryViewIdentifiers.count > 0) { + [debug addObject:@"Registered supplementary view identifiers:"]; + [debug addObject:[self.registeredSupplementaryViewIdentifiers description]]; + } + + if (self.registeredSupplementaryViewNibNames.count > 0) { + [debug addObject:@"Registered supplementary view nib names:"]; + [debug addObject:self.registeredSupplementaryViewNibNames]; + } + + if ([self.updater isKindOfClass:[IGListAdapterUpdater class]]) { + [debug addObject:[NSString stringWithFormat:@"IGListAdapterUpdater instance %p:", self.updater]]; + [debug addObjectsFromArray:IGListDebugIndentedLines([(IGListAdapterUpdater *)self.updater debugDescriptionLines])]; + } + + [debug addObject:@"Section map details:"]; + [debug addObjectsFromArray:IGListDebugIndentedLines([self.sectionMap debugDescriptionLines])]; + + if (self.previousSectionMap != nil) { + [debug addObject:@"Previous section map details:"]; + [debug addObjectsFromArray:IGListDebugIndentedLines([self.previousSectionMap debugDescriptionLines])]; + } + + [debug addObject:@"Collection view details:"]; + [debug addObjectsFromArray:IGListDebugIndentedLines([self.collectionView debugDescriptionLines])]; +#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED + return debug; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+UICollectionView.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+UICollectionView.h new file mode 100644 index 0000000..e3a8aef --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+UICollectionView.h @@ -0,0 +1,18 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import +#import + +@interface IGListAdapter (UICollectionView) +< +UICollectionViewDataSource, +IGListCollectionViewDelegateLayout +> +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+UICollectionView.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+UICollectionView.m new file mode 100644 index 0000000..c382f1a --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapter+UICollectionView.m @@ -0,0 +1,307 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListAdapter+UICollectionView.h" + +#import +#import +#import +#import + +@implementation IGListAdapter (UICollectionView) + +#pragma mark - UICollectionViewDataSource + +- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView { + return self.sectionMap.objects.count; +} + +- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section { + IGListSectionController * sectionController = [self sectionControllerForSection:section]; + IGAssert(sectionController != nil, @"Nil section controller for section %li for item %@. Check your -diffIdentifier and -isEqual: implementations.", + (long)section, [self.sectionMap objectForSection:section]); + const NSInteger numberOfItems = [sectionController numberOfItems]; + IGAssert(numberOfItems >= 0, @"Cannot return negative number of items %li for section controller %@.", (long)numberOfItems, sectionController); + return numberOfItems; +} + +- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath { + id performanceDelegate = self.performanceDelegate; + [performanceDelegate listAdapterWillCallDequeueCell:self]; + + IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; + + // flag that a cell is being dequeued in case it tries to access a cell in the process + _isDequeuingCell = YES; + UICollectionViewCell *cell = [sectionController cellForItemAtIndex:indexPath.item]; + _isDequeuingCell = NO; + + IGAssert(cell != nil, @"Returned a nil cell at indexPath <%@> from section controller: <%@>", indexPath, sectionController); + + // associate the section controller with the cell so that we know which section controller is using it + [self mapView:cell toSectionController:sectionController]; + + [performanceDelegate listAdapter:self didCallDequeueCell:cell onSectionController:sectionController atIndex:indexPath.item]; + return cell; +} + +- (UICollectionReusableView *)collectionView:(UICollectionView *)collectionView viewForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { + IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; + id supplementarySource = [sectionController supplementaryViewSource]; + UICollectionReusableView *view = [supplementarySource viewForSupplementaryElementOfKind:kind atIndex:indexPath.item]; + IGAssert(view != nil, @"Returned a nil supplementary view at indexPath <%@> from section controller: <%@>, supplementary source: <%@>", indexPath, sectionController, supplementarySource); + + // associate the section controller with the cell so that we know which section controller is using it + [self mapView:view toSectionController:sectionController]; + + return view; +} + +- (BOOL)collectionView:(UICollectionView *)collectionView canMoveItemAtIndexPath:(NSIndexPath *)indexPath { + const NSInteger sectionIndex = indexPath.section; + const NSInteger itemIndex = indexPath.item; + + IGListSectionController *sectionController = [self sectionControllerForSection:sectionIndex]; + return [sectionController canMoveItemAtIndex:itemIndex]; +} + +- (void)collectionView:(UICollectionView *)collectionView + moveItemAtIndexPath:(NSIndexPath *)sourceIndexPath + toIndexPath:(NSIndexPath *)destinationIndexPath { + + const NSInteger sourceSectionIndex = sourceIndexPath.section; + const NSInteger destinationSectionIndex = destinationIndexPath.section; + const NSInteger sourceItemIndex = sourceIndexPath.item; + const NSInteger destinationItemIndex = destinationIndexPath.item; + + IGListSectionController *sourceSectionController = [self sectionControllerForSection:sourceSectionIndex]; + IGListSectionController *destinationSectionController = [self sectionControllerForSection:destinationSectionIndex]; + + // this is a move within a section + if (sourceSectionController == destinationSectionController) { + + if ([sourceSectionController canMoveItemAtIndex:sourceItemIndex toIndex:destinationItemIndex]) { + [self moveInSectionControllerInteractive:sourceSectionController + fromIndex:sourceItemIndex + toIndex:destinationItemIndex]; + } else { + // otherwise this is a move of an _item_ from one section to another section + // we need to revert the change as it's too late to cancel + [self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; + } + return; + } + + // this is a reordering of sections themselves + if ([sourceSectionController numberOfItems] == 1 && [destinationSectionController numberOfItems] == 1) { + + // perform view changes in the collection view + [self moveSectionControllerInteractive:sourceSectionController + fromIndex:sourceSectionIndex + toIndex:destinationSectionIndex]; + return; + } + + // otherwise this is a move of an _item_ from one section to another section + // this is not currently supported, so we need to revert the change as it's too late to cancel + [self revertInvalidInteractiveMoveFromIndexPath:sourceIndexPath toIndexPath:destinationIndexPath]; +} + +#pragma mark - UICollectionViewDelegate + +- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath { + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView didSelectItemAtIndexPath:indexPath]; + } + + IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; + [sectionController didSelectItemAtIndex:indexPath.item]; +} + +- (void)collectionView:(UICollectionView *)collectionView didDeselectItemAtIndexPath:(NSIndexPath *)indexPath { + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didDeselectItemAtIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView didDeselectItemAtIndexPath:indexPath]; + } + + IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; + [sectionController didDeselectItemAtIndex:indexPath.item]; +} + +- (void)collectionView:(UICollectionView *)collectionView willDisplayCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { + id performanceDelegate = self.performanceDelegate; + [performanceDelegate listAdapterWillCallDisplayCell:self]; + + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:willDisplayCell:forItemAtIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView willDisplayCell:cell forItemAtIndexPath:indexPath]; + } + + IGListSectionController *sectionController = [self sectionControllerForView:cell]; + // if the section controller relationship was destroyed, reconnect it + // this happens with iOS 10 UICollectionView display range changes + if (sectionController == nil) { + sectionController = [self sectionControllerForSection:indexPath.section]; + [self mapView:cell toSectionController:sectionController]; + } + + id object = [self.sectionMap objectForSection:indexPath.section]; + [self.displayHandler willDisplayCell:cell forListAdapter:self sectionController:sectionController object:object indexPath:indexPath]; + + _isSendingWorkingRangeDisplayUpdates = YES; + [self.workingRangeHandler willDisplayItemAtIndexPath:indexPath forListAdapter:self]; + _isSendingWorkingRangeDisplayUpdates = NO; + + [performanceDelegate listAdapter:self didCallDisplayCell:cell onSectionController:sectionController atIndex:indexPath.item]; +} + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingCell:(UICollectionViewCell *)cell forItemAtIndexPath:(NSIndexPath *)indexPath { + id performanceDelegate = self.performanceDelegate; + [performanceDelegate listAdapterWillCallEndDisplayCell:self]; + + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView didEndDisplayingCell:cell forItemAtIndexPath:indexPath]; + } + + IGListSectionController *sectionController = [self sectionControllerForView:cell]; + [self.displayHandler didEndDisplayingCell:cell forListAdapter:self sectionController:sectionController indexPath:indexPath]; + [self.workingRangeHandler didEndDisplayingItemAtIndexPath:indexPath forListAdapter:self]; + + // break the association between the cell and the section controller + [self removeMapForView:cell]; + + [performanceDelegate listAdapter:self didCallEndDisplayCell:cell onSectionController:sectionController atIndex:indexPath.item]; +} + +- (void)collectionView:(UICollectionView *)collectionView willDisplaySupplementaryView:(UICollectionReusableView *)view forElementKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:willDisplaySupplementaryView:forElementKind:atIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView willDisplaySupplementaryView:view forElementKind:elementKind atIndexPath:indexPath]; + } + + IGListSectionController *sectionController = [self sectionControllerForView:view]; + // if the section controller relationship was destroyed, reconnect it + // this happens with iOS 10 UICollectionView display range changes + if (sectionController == nil) { + sectionController = [self.sectionMap sectionControllerForSection:indexPath.section]; + [self mapView:view toSectionController:sectionController]; + } + + id object = [self.sectionMap objectForSection:indexPath.section]; + [self.displayHandler willDisplaySupplementaryView:view forListAdapter:self sectionController:sectionController object:object indexPath:indexPath]; +} + +- (void)collectionView:(UICollectionView *)collectionView didEndDisplayingSupplementaryView:(UICollectionReusableView *)view forElementOfKind:(NSString *)elementKind atIndexPath:(NSIndexPath *)indexPath { + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didEndDisplayingSupplementaryView:forElementOfKind:atIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView didEndDisplayingSupplementaryView:view forElementOfKind:elementKind atIndexPath:indexPath]; + } + + IGListSectionController *sectionController = [self sectionControllerForView:view]; + [self.displayHandler didEndDisplayingSupplementaryView:view forListAdapter:self sectionController:sectionController indexPath:indexPath]; + + [self removeMapForView:view]; +} + +- (void)collectionView:(UICollectionView *)collectionView didHighlightItemAtIndexPath:(NSIndexPath *)indexPath { + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didHighlightItemAtIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView didHighlightItemAtIndexPath:indexPath]; + } + + IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; + [sectionController didHighlightItemAtIndex:indexPath.item]; +} + +- (void)collectionView:(UICollectionView *)collectionView didUnhighlightItemAtIndexPath:(NSIndexPath *)indexPath { + // forward this method to the delegate b/c this implementation will steal the message from the proxy + id collectionViewDelegate = self.collectionViewDelegate; + if ([collectionViewDelegate respondsToSelector:@selector(collectionView:didUnhighlightItemAtIndexPath:)]) { + [collectionViewDelegate collectionView:collectionView didUnhighlightItemAtIndexPath:indexPath]; + } + + IGListSectionController * sectionController = [self sectionControllerForSection:indexPath.section]; + [sectionController didUnhighlightItemAtIndex:indexPath.item]; +} + +#pragma mark - UICollectionViewDelegateFlowLayout + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath { + IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); + + CGSize size = [self sizeForItemAtIndexPath:indexPath]; + IGAssert(!isnan(size.height), @"IGListAdapter returned NaN height = %f for item at indexPath <%@>", size.height, indexPath); + IGAssert(!isnan(size.width), @"IGListAdapter returned NaN width = %f for item at indexPath <%@>", size.width, indexPath); + + return size; +} + +- (UIEdgeInsets)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout insetForSectionAtIndex:(NSInteger)section { + IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); + return [[self sectionControllerForSection:section] inset]; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumLineSpacingForSectionAtIndex:(NSInteger)section { + IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); + return [[self sectionControllerForSection:section] minimumLineSpacing]; +} + +- (CGFloat)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout minimumInteritemSpacingForSectionAtIndex:(NSInteger)section { + IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); + return [[self sectionControllerForSection:section] minimumInteritemSpacing]; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout referenceSizeForHeaderInSection:(NSInteger)section { + IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; + return [self sizeForSupplementaryViewOfKind:UICollectionElementKindSectionHeader atIndexPath:indexPath]; +} + +- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout*)collectionViewLayout referenceSizeForFooterInSection:(NSInteger)section { + IGAssert(![self.collectionViewDelegate respondsToSelector:_cmd], @"IGListAdapter is consuming method also implemented by the collectionViewDelegate: %@", NSStringFromSelector(_cmd)); + NSIndexPath *indexPath = [NSIndexPath indexPathForItem:0 inSection:section]; + return [self sizeForSupplementaryViewOfKind:UICollectionElementKindSectionFooter atIndexPath:indexPath]; +} + +#pragma mark - IGListCollectionViewDelegateLayout + +- (UICollectionViewLayoutAttributes *)collectionView:(UICollectionView *)collectionView + layout:(UICollectionViewLayout*)collectionViewLayout + customizedInitialLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes + atIndexPath:(NSIndexPath *)indexPath { + IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; + if (sectionController.transitionDelegate) { + return [sectionController.transitionDelegate listAdapter:self + customizedInitialLayoutAttributes:attributes + sectionController:sectionController + atIndex:indexPath.item]; + } + return attributes; +} + +- (UICollectionViewLayoutAttributes *)collectionView:(UICollectionView *)collectionView + layout:(UICollectionViewLayout*)collectionViewLayout + customizedFinalLayoutAttributes:(UICollectionViewLayoutAttributes *)attributes + atIndexPath:(NSIndexPath *)indexPath { + IGListSectionController *sectionController = [self sectionControllerForSection:indexPath.section]; + if (sectionController.transitionDelegate) { + return [sectionController.transitionDelegate listAdapter:self + customizedFinalLayoutAttributes:attributes + sectionController:sectionController + atIndex:indexPath.item]; + } + return attributes; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterInternal.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterInternal.h new file mode 100644 index 0000000..08fe1c5 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterInternal.h @@ -0,0 +1,84 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import +#import + +#import "IGListAdapter+UICollectionView.h" +#import "IGListAdapterProxy.h" +#import "IGListDisplayHandler.h" +#import "IGListSectionMap.h" +#import "IGListWorkingRangeHandler.h" + +NS_ASSUME_NONNULL_BEGIN + +/// Generate a string representation of a reusable view class when registering with a UICollectionView. +NS_INLINE NSString *IGListReusableViewIdentifier(Class viewClass, NSString * _Nullable kind, NSString * _Nullable givenReuseIdentifier) { + return [NSString stringWithFormat:@"%@%@%@", kind ?: @"", givenReuseIdentifier ?: @"", NSStringFromClass(viewClass)]; +} + +@interface IGListAdapter () +< +IGListCollectionContext, +IGListBatchContext +> +{ + __weak UICollectionView *_collectionView; + BOOL _isDequeuingCell; + BOOL _isSendingWorkingRangeDisplayUpdates; +} + +@property (nonatomic, strong) id updater; + +@property (nonatomic, strong, readonly) IGListSectionMap *sectionMap; +@property (nonatomic, strong, readonly) IGListDisplayHandler *displayHandler; +@property (nonatomic, strong, readonly) IGListWorkingRangeHandler *workingRangeHandler; + +@property (nonatomic, strong, nullable) IGListAdapterProxy *delegateProxy; + +@property (nonatomic, strong, nullable) UIView *emptyBackgroundView; + +// we need to special case interactive section moves that are moved to the last position +@property (nonatomic) BOOL isLastInteractiveMoveToLastSectionIndex; + +/** + When making object updates inside a batch update block, delete operations must use the section /before/ any moves take + place. This includes when other objects are deleted or inserted ahead of the section controller making the mutations. + In order to account for this we must track when the adapter is in the middle of an update block as well as the section + controller mapping prior to the transition. + + Note that the previous section controller map is destroyed as soon as a transition is finished so there is no dangling + objects or section controllers. + */ +@property (nonatomic, assign) BOOL isInUpdateBlock; +@property (nonatomic, strong, nullable) IGListSectionMap *previousSectionMap; + +/** + Set of cell identifiers registered with the list context. + Identifiers are constructed with the `IGListReusableViewIdentifier` function. + */ +@property (nonatomic, strong) NSMutableSet *registeredCellIdentifiers; +@property (nonatomic, strong) NSMutableSet *registeredNibNames; +@property (nonatomic, strong) NSMutableSet *registeredSupplementaryViewIdentifiers; +@property (nonatomic, strong) NSMutableSet *registeredSupplementaryViewNibNames; + +- (void)mapView:(__kindof UIView *)view toSectionController:(IGListSectionController *)sectionController; +- (nullable IGListSectionController *)sectionControllerForView:(__kindof UIView *)view; +- (void)removeMapForView:(__kindof UIView *)view; + +- (NSArray *)indexPathsFromSectionController:(IGListSectionController *)sectionController + indexes:(NSIndexSet *)indexes + usePreviousIfInUpdateBlock:(BOOL)usePreviousIfInUpdateBlock; + +- (nullable NSIndexPath *)indexPathForSectionController:(IGListSectionController *)controller + index:(NSInteger)index + usePreviousIfInUpdateBlock:(BOOL)usePreviousIfInUpdateBlock; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterProxy.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterProxy.h new file mode 100644 index 0000000..8122391 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterProxy.h @@ -0,0 +1,48 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@class IGListAdapter; + +NS_ASSUME_NONNULL_BEGIN + +/** + A proxy that sends a custom set of selectors to an IGListAdapter object and the rest to a UICollectionViewDelegate + target. + */ +IGLK_SUBCLASSING_RESTRICTED +@interface IGListAdapterProxy : NSProxy + +/** + Create a new proxy object with targets and interceptor. + + @param collectionViewTarget A UICollectionViewDelegate conforming object that receives non-intercepted messages. + @param scrollViewTarget A UIScrollViewDelegate conforming object that receives non-intercepted messages. + @param interceptor An IGListAdapter object that intercepts a set of messages. + + @return A new IGListAdapterProxy object. + */ +- (instancetype)initWithCollectionViewTarget:(nullable id)collectionViewTarget + scrollViewTarget:(nullable id)scrollViewTarget + interceptor:(IGListAdapter *)interceptor; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterProxy.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterProxy.m new file mode 100644 index 0000000..0884b2f --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterProxy.m @@ -0,0 +1,96 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListAdapterProxy.h" + +#import + +#import "IGListCollectionViewDelegateLayout.h" + +/** + Define messages that you want the IGListAdapter object to intercept. Pattern copied from + https://github.com/facebook/AsyncDisplayKit/blob/7b112a2dcd0391ddf3671f9dcb63521f554b78bd/AsyncDisplayKit/ASCollectionView.mm#L34-L53 + */ +static BOOL isInterceptedSelector(SEL sel) { + return ( + // UIScrollViewDelegate + sel == @selector(scrollViewDidScroll:) || + sel == @selector(scrollViewWillBeginDragging:) || + sel == @selector(scrollViewDidEndDragging:willDecelerate:) || + sel == @selector(scrollViewDidEndDecelerating:) || + // UICollectionViewDelegate + sel == @selector(collectionView:willDisplayCell:forItemAtIndexPath:) || + sel == @selector(collectionView:didEndDisplayingCell:forItemAtIndexPath:) || + sel == @selector(collectionView:didSelectItemAtIndexPath:) || + sel == @selector(collectionView:didDeselectItemAtIndexPath:) || + sel == @selector(collectionView:didHighlightItemAtIndexPath:) || + sel == @selector(collectionView:didUnhighlightItemAtIndexPath:) || + // UICollectionViewDelegateFlowLayout + sel == @selector(collectionView:layout:sizeForItemAtIndexPath:) || + sel == @selector(collectionView:layout:insetForSectionAtIndex:) || + sel == @selector(collectionView:layout:minimumInteritemSpacingForSectionAtIndex:) || + sel == @selector(collectionView:layout:minimumLineSpacingForSectionAtIndex:) || + sel == @selector(collectionView:layout:referenceSizeForFooterInSection:) || + sel == @selector(collectionView:layout:referenceSizeForHeaderInSection:) || + + // IGListCollectionViewDelegateLayout + sel == @selector(collectionView:layout:customizedInitialLayoutAttributes:atIndexPath:) || + sel == @selector(collectionView:layout:customizedFinalLayoutAttributes:atIndexPath:) + ); +} + +@interface IGListAdapterProxy () { + __weak id _collectionViewTarget; + __weak id _scrollViewTarget; + __weak IGListAdapter *_interceptor; +} + +@end + +@implementation IGListAdapterProxy + +- (instancetype)initWithCollectionViewTarget:(nullable id)collectionViewTarget + scrollViewTarget:(nullable id)scrollViewTarget + interceptor:(IGListAdapter *)interceptor { + IGParameterAssert(interceptor != nil); + // -[NSProxy init] is undefined + if (self) { + _collectionViewTarget = collectionViewTarget; + _scrollViewTarget = scrollViewTarget; + _interceptor = interceptor; + } + return self; +} + +- (BOOL)respondsToSelector:(SEL)aSelector { + return isInterceptedSelector(aSelector) + || [_collectionViewTarget respondsToSelector:aSelector] + || [_scrollViewTarget respondsToSelector:aSelector]; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector { + if (isInterceptedSelector(aSelector)) { + return _interceptor; + } + + // since UICollectionViewDelegate is a superset of UIScrollViewDelegate, first check if the method exists in + // _scrollViewTarget, otherwise use the _collectionViewTarget + return [_scrollViewTarget respondsToSelector:aSelector] ? _scrollViewTarget : _collectionViewTarget; +} + +// handling unimplemented methods and nil target/interceptor +// https://github.com/Flipboard/FLAnimatedImage/blob/76a31aefc645cc09463a62d42c02954a30434d7d/FLAnimatedImage/FLAnimatedImage.m#L786-L807 +- (void)forwardInvocation:(NSInvocation *)invocation { + void *nullPointer = NULL; + [invocation setReturnValue:&nullPointer]; +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { + return [NSObject instanceMethodSignatureForSelector:@selector(init)]; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.h new file mode 100644 index 0000000..b7ee299 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface IGListAdapterUpdater (DebugDescription) + +- (NSArray *)debugDescriptionLines; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m new file mode 100644 index 0000000..809561a --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m @@ -0,0 +1,75 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListAdapterUpdater+DebugDescription.h" + +#import "IGListAdapterUpdaterInternal.h" +#import "IGListBatchUpdateData+DebugDescription.h" +#import "IGListDebuggingUtilities.h" + +#if IGLK_DEBUG_DESCRIPTION_ENABLED +static NSMutableArray *linesFromObjects(NSArray *objects) { + NSMutableArray *lines = [NSMutableArray new]; + for (id object in objects) { + [lines addObject:[NSString stringWithFormat:@"Object %p of type %@ with identifier %@", + object, NSStringFromClass([object class]), [object diffIdentifier]]]; + } + return lines; +} +#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED + +@implementation IGListAdapterUpdater (DebugDescription) + +- (NSArray *)debugDescriptionLines { + NSMutableArray *debug = [NSMutableArray new]; +#if IGLK_DEBUG_DESCRIPTION_ENABLED + [debug addObject:[NSString stringWithFormat:@"Moves as deletes+inserts: %@", IGListDebugBOOL(self.movesAsDeletesInserts)]]; + [debug addObject:[NSString stringWithFormat:@"Allows background reloading: %@", IGListDebugBOOL(self.allowsBackgroundReloading)]]; + [debug addObject:[NSString stringWithFormat:@"Has queued reload data: %@", IGListDebugBOOL(self.hasQueuedReloadData)]]; + [debug addObject:[NSString stringWithFormat:@"Queued update is animated: %@", IGListDebugBOOL(self.queuedUpdateIsAnimated)]]; + + NSString *stateString; + switch (self.state) { + case IGListBatchUpdateStateIdle: + stateString = @"Idle"; + break; + case IGListBatchUpdateStateQueuedBatchUpdate: + stateString = @"Queued batch update"; + break; + case IGListBatchUpdateStateExecutedBatchUpdateBlock: + stateString = @"Executed batch update block"; + break; + case IGListBatchUpdateStateExecutingBatchUpdateBlock: + stateString = @"Executing batch update block"; + break; + } + [debug addObject:[NSString stringWithFormat:@"State: %@", stateString]]; + + if (self.applyingUpdateData != nil) { + [debug addObject:@"Batch update data:"]; + [debug addObjectsFromArray:IGListDebugIndentedLines([self.applyingUpdateData debugDescriptionLines])]; + } + + if (self.fromObjects != nil) { + [debug addObject:@"From objects:"]; + [debug addObjectsFromArray:IGListDebugIndentedLines(linesFromObjects(self.fromObjects))]; + } + + if (self.toObjectsBlock != nil) { + [debug addObject:@"To objects:"]; + [debug addObjectsFromArray:IGListDebugIndentedLines(linesFromObjects(self.toObjectsBlock()))]; + } + + if (self.pendingTransitionToObjects != nil) { + [debug addObject:@"Pending objects:"]; + [debug addObjectsFromArray:IGListDebugIndentedLines(linesFromObjects(self.pendingTransitionToObjects))]; + } +#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED + return debug; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h new file mode 100644 index 0000000..df02271 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h @@ -0,0 +1,51 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import +#import + +#import + +#import "IGListAdapterUpdater.h" +#import "IGListBatchUpdateState.h" +#import "IGListBatchUpdates.h" + +NS_ASSUME_NONNULL_BEGIN + +FOUNDATION_EXTERN void convertReloadToDeleteInsert(NSMutableIndexSet *reloads, + NSMutableIndexSet *deletes, + NSMutableIndexSet *inserts, + IGListIndexSetResult *result, + NSArray> *fromObjects); + +@interface IGListAdapterUpdater () + +@property (nonatomic, copy, nullable) NSArray *fromObjects; +@property (nonatomic, copy, nullable) IGListToObjectBlock toObjectsBlock; +@property (nonatomic, copy, nullable) NSArray *pendingTransitionToObjects; +@property (nonatomic, strong) NSMutableArray *completionBlocks; + +@property (nonatomic, assign) BOOL queuedUpdateIsAnimated; + +@property (nonatomic, strong) IGListBatchUpdates *batchUpdates; + +@property (nonatomic, copy, nullable) IGListObjectTransitionBlock objectTransitionBlock; + +@property (nonatomic, copy, nullable) IGListReloadUpdateBlock reloadUpdates; +@property (nonatomic, assign, getter=hasQueuedReloadData) BOOL queuedReloadData; + +@property (nonatomic, assign) IGListBatchUpdateState state; +@property (nonatomic, strong, nullable) IGListBatchUpdateData *applyingUpdateData; + +- (void)performReloadDataWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock; +- (void)performBatchUpdatesWithCollectionViewBlock:(IGListCollectionViewBlock)collectionViewBlock; +- (void)cleanStateBeforeUpdates; +- (BOOL)hasChanges; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListArrayUtilsInternal.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListArrayUtilsInternal.h new file mode 100644 index 0000000..f3025b0 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListArrayUtilsInternal.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#ifndef IGListArrayUtilsInternal_h +#define IGListArrayUtilsInternal_h + +#import + +static NSArray *objectsWithDuplicateIdentifiersRemoved(NSArray> *objects) { + if (objects == nil) { + return nil; + } + + NSMapTable *identifierMap = [NSMapTable strongToStrongObjectsMapTable]; + NSMutableArray *uniqueObjects = [NSMutableArray new]; + for (id object in objects) { + id diffIdentifier = [object diffIdentifier]; + id previousObject = [identifierMap objectForKey:diffIdentifier]; + if (diffIdentifier != nil + && previousObject == nil) { + [identifierMap setObject:object forKey:diffIdentifier]; + [uniqueObjects addObject:object]; + } else { + IGLKLog(@"Duplicate identifier %@ for object %@ with object %@", diffIdentifier, object, previousObject); + } + } + return uniqueObjects; +} + +#endif /* IGListArrayUtilsInternal_h */ diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.h new file mode 100644 index 0000000..095e221 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface IGListBatchUpdateData (DebugDescription) + +- (NSArray *)debugDescriptionLines; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.m new file mode 100644 index 0000000..652529c --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.m @@ -0,0 +1,38 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListBatchUpdateData+DebugDescription.h" + +@implementation IGListBatchUpdateData (DebugDescription) + +- (NSArray *)debugDescriptionLines { + NSMutableArray *debug = [NSMutableArray new]; +#if IGLK_DEBUG_DESCRIPTION_ENABLED + [debug addObject:[NSString stringWithFormat:@"Insert sections: %@", self.insertSections]]; + [debug addObject:[NSString stringWithFormat:@"Delete sections: %@", self.deleteSections]]; + + for (IGListMoveIndex *move in self.moveSections) { + [debug addObject:[NSString stringWithFormat:@"Move from section %li to %li", (long)move.from, (long)move.to]]; + } + + for (NSIndexPath *path in self.deleteIndexPaths) { + [debug addObject:[NSString stringWithFormat:@"Delete section %li item %li", (long)path.section, (long)path.item]]; + } + + for (NSIndexPath *path in self.insertIndexPaths) { + [debug addObject:[NSString stringWithFormat:@"Insert section %li item %li", (long)path.section, (long)path.item]]; + } + + for (IGListMoveIndexPath *move in self.moveIndexPaths) { + [debug addObject:[NSString stringWithFormat:@"Move from section %li item %li to section %li item %li", + (long)move.from.section, (long)move.from.item, (long)move.to.section, (long)move.to.item]]; + } +#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED + return debug; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateState.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateState.h new file mode 100644 index 0000000..00fd977 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdateState.h @@ -0,0 +1,15 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +typedef NS_ENUM (NSInteger, IGListBatchUpdateState) { + IGListBatchUpdateStateIdle, + IGListBatchUpdateStateQueuedBatchUpdate, + IGListBatchUpdateStateExecutingBatchUpdateBlock, + IGListBatchUpdateStateExecutedBatchUpdateBlock, +}; diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdates.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdates.h new file mode 100644 index 0000000..263341d --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdates.h @@ -0,0 +1,29 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@class IGListMoveIndexPath; +@class IGListReloadIndexPath; + +IGLK_SUBCLASSING_RESTRICTED +@interface IGListBatchUpdates : NSObject + +@property (nonatomic, strong, readonly) NSMutableIndexSet *sectionReloads; +@property (nonatomic, strong, readonly) NSMutableArray *itemInserts; +@property (nonatomic, strong, readonly) NSMutableArray *itemDeletes; +@property (nonatomic, strong, readonly) NSMutableArray *itemReloads; +@property (nonatomic, strong, readonly) NSMutableArray *itemMoves; + +@property (nonatomic, strong, readonly) NSMutableArray *itemUpdateBlocks; +@property (nonatomic, strong, readonly) NSMutableArray *itemCompletionBlocks; + +- (BOOL)hasChanges; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdates.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdates.m new file mode 100644 index 0000000..25aeb53 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBatchUpdates.m @@ -0,0 +1,34 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListBatchUpdates.h" + +@implementation IGListBatchUpdates + +- (instancetype)init { + if (self = [super init]) { + _sectionReloads = [NSMutableIndexSet new]; + _itemInserts = [NSMutableArray new]; + _itemMoves = [NSMutableArray new]; + _itemDeletes = [NSMutableArray new]; + _itemReloads = [NSMutableArray new]; + _itemUpdateBlocks = [NSMutableArray new]; + _itemCompletionBlocks = [NSMutableArray new]; + } + return self; +} + +- (BOOL)hasChanges { + return [self.itemUpdateBlocks count] > 0 + || [self.sectionReloads count] > 0 + || [self.itemInserts count] > 0 + || [self.itemMoves count] > 0 + || [self.itemReloads count] > 0 + || [self.itemDeletes count] > 0; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.h new file mode 100644 index 0000000..f49a8d3 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface IGListBindingSectionController (DebugDescription) + +- (NSArray *)debugDescriptionLines; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.m new file mode 100644 index 0000000..48711aa --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.m @@ -0,0 +1,46 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListBindingSectionController+DebugDescription.h" + +#import "IGListDebuggingUtilities.h" + +@implementation IGListBindingSectionController (DebugDescription) + +- (NSString *)debugDescription { + NSMutableArray *lines = [NSMutableArray arrayWithObject:[NSString stringWithFormat:@"IGListBindingSectionController %p:", self]]; + [lines addObjectsFromArray:IGListDebugIndentedLines([self debugDescriptionLines])]; + return [lines componentsJoinedByString:@"\n"]; +} + +- (NSArray *)debugDescriptionLines { + NSMutableArray *debug = [NSMutableArray new]; +#if IGLK_DEBUG_DESCRIPTION_ENABLED + [debug addObject:[NSString stringWithFormat:@"Data source: %@", self.dataSource]]; + [debug addObject:[NSString stringWithFormat:@"Selection delegate: %@", self.selectionDelegate]]; + [debug addObject:[NSString stringWithFormat:@"Object: %@", self.object]]; + [debug addObject:@"View models:"]; + for (id viewModel in self.viewModels) { + [debug addObject:[NSString stringWithFormat:@"%@: %@", viewModel, viewModel.diffIdentifier]]; + } + [debug addObject:[NSString stringWithFormat:@"Number of items: %ld", (long)self.numberOfItems]]; + [debug addObject:[NSString stringWithFormat:@"View controller: %@", self.viewController]]; + [debug addObject:[NSString stringWithFormat:@"Collection context: %@", self.collectionContext]]; + [debug addObject:[NSString stringWithFormat:@"Section: %ld", (long)self.section]]; + [debug addObject:[NSString stringWithFormat:@"Is first section: %@", IGListDebugBOOL(self.isFirstSection)]]; + [debug addObject:[NSString stringWithFormat:@"Is last section: %@", IGListDebugBOOL(self.isLastSection)]]; + [debug addObject:[NSString stringWithFormat:@"Supplementary view source: %@", self.supplementaryViewSource]]; + [debug addObject:[NSString stringWithFormat:@"Display delegate: %@", self.displayDelegate]]; + [debug addObject:[NSString stringWithFormat:@"Working range delegate: %@", self.workingRangeDelegate]]; + [debug addObject:[NSString stringWithFormat:@"Scroll delegate: %@", self.scrollDelegate]]; + +#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED + return debug; +} + +@end + diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListCollectionViewLayoutInternal.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListCollectionViewLayoutInternal.h new file mode 100644 index 0000000..4191bcb --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListCollectionViewLayoutInternal.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +static CGRect IGListRectIntegralScaled(CGRect rect) { + CGFloat scale = [[UIScreen mainScreen] scale]; + return CGRectMake(floorf(rect.origin.x * scale) / scale, + floorf(rect.origin.y * scale) / scale, + ceilf(rect.size.width * scale) / scale, + ceilf(rect.size.height * scale) / scale); +} diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebugger.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebugger.h new file mode 100644 index 0000000..b917acb --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebugger.h @@ -0,0 +1,28 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@class IGListAdapter; + +IGLK_SUBCLASSING_RESTRICTED +@interface IGListDebugger : NSObject + ++ (void)trackAdapter:(IGListAdapter *)adapter; + ++ (NSArray *)adapterDescriptions; + ++ (void)clear; + ++ (NSString *)dump; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebugger.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebugger.m new file mode 100644 index 0000000..bb55efd --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebugger.m @@ -0,0 +1,41 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListDebugger.h" + +#import "IGListAdapter+DebugDescription.h" + +@implementation IGListDebugger + +static NSHashTable *livingAdaptersTable = nil; + ++ (void)trackAdapter:(IGListAdapter *)adapter { +#if IGLK_DEBUG_DESCRIPTION_ENABLED + if (livingAdaptersTable == nil) { + livingAdaptersTable = [NSHashTable weakObjectsHashTable]; + } + [livingAdaptersTable addObject:adapter]; +#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED +} + ++ (NSArray *)adapterDescriptions { + NSMutableArray *descriptions = [NSMutableArray new]; + for (IGListAdapter *adapter in livingAdaptersTable) { + [descriptions addObject:[adapter debugDescription]]; + } + return descriptions; +} + ++ (void)clear { + [livingAdaptersTable removeAllObjects]; +} + ++ (NSString *)dump { + return [[self adapterDescriptions] componentsJoinedByString:@"\n"]; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebuggingUtilities.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebuggingUtilities.h new file mode 100644 index 0000000..5edfb4c --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebuggingUtilities.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListAdapter; + +extern NSString *IGListDebugBOOL(BOOL b); + +extern NSArray *IGListDebugIndentedLines(NSArray *lines); diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebuggingUtilities.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebuggingUtilities.m new file mode 100644 index 0000000..f4deb0d --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDebuggingUtilities.m @@ -0,0 +1,20 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListDebuggingUtilities.h" + +NSString *IGListDebugBOOL(BOOL b) { + return b ? @"Yes" : @"No"; +} + +NSArray *IGListDebugIndentedLines(NSArray *lines) { + NSMutableArray *newLines = [NSMutableArray new]; + for (NSString *line in lines) { + [newLines addObject:[NSString stringWithFormat:@" %@", line]]; + } + return newLines; +} diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDisplayHandler.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDisplayHandler.h new file mode 100644 index 0000000..d7ec4fb --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDisplayHandler.h @@ -0,0 +1,87 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@class IGListAdapter; +@class IGListSectionController; + + + +NS_ASSUME_NONNULL_BEGIN + +IGLK_SUBCLASSING_RESTRICTED +@interface IGListDisplayHandler : NSObject + +/** + Counted set of the currently visible section controllers. + */ +@property (nonatomic, strong, readonly) NSCountedSet *visibleListSections; + +/** + Tells the handler that a cell will be displayed in the IGListAdapter. + + @param cell A cell that will be displayed. + @param listAdapter The adapter the cell will display in. + @param sectionController The section controller that manages the cell. + @param object The object that powers the section controller. + @param indexPath The index path of the cell in the UICollectionView. + */ +- (void)willDisplayCell:(UICollectionViewCell *)cell + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + object:(id)object + indexPath:(NSIndexPath *)indexPath; + +/** + Tells the handler that a cell did end display in the IGListAdapter. + + @param cell A cell that will be displayed. + @param listAdapter The adapter the cell will display in. + @param sectionController The section controller that manages the cell. + @param indexPath The index path of the cell in the UICollectionView. + */ +- (void)didEndDisplayingCell:(UICollectionViewCell *)cell + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + indexPath:(NSIndexPath *)indexPath; + + +/** + Tells the handler that a supplementary view will be displayed in the IGListAdapter. + + @param view A supplementary view that will be displayed. + @param listAdapter The adapter the supplementary view will display in. + @param sectionController The section controller that manages the supplementary view. + @param object The object that powers the section controller. + @param indexPath The index path of the supplementary view in the UICollectionView. + */ +- (void)willDisplaySupplementaryView:(UICollectionReusableView *)view + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + object:(id)object + indexPath:(NSIndexPath *)indexPath; + + +/** + Tells the handler that a supplementary view did end display in the IGListAdapter. + + @param view A supplementary view that will be displayed. + @param listAdapter The adapter the supplementary view will display in. + @param sectionController The section controller that manages the supplementary view. + @param indexPath The index path of the supplementary view in the UICollectionView. + */ +- (void)didEndDisplayingSupplementaryView:(UICollectionReusableView *)view + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + indexPath:(NSIndexPath *)indexPath; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDisplayHandler.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDisplayHandler.m new file mode 100644 index 0000000..ee28942 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListDisplayHandler.m @@ -0,0 +1,122 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListDisplayHandler.h" + +#import +#import +#import +#import + +@interface IGListDisplayHandler () + +@property (nonatomic, strong) NSMapTable *visibleViewObjectMap; + +@end + +@implementation IGListDisplayHandler + +- (instancetype)init { + if (self = [super init]) { + _visibleListSections = [NSCountedSet new]; + _visibleViewObjectMap = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory capacity:0]; + } + return self; +} + +- (id)_pluckObjectForView:(UICollectionReusableView *)view { + NSMapTable *viewObjectMap = self.visibleViewObjectMap; + id object = [viewObjectMap objectForKey:view]; + [viewObjectMap removeObjectForKey:view]; + return object; +} + +- (void)_willDisplayReusableView:(UICollectionReusableView *)view + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + object:(id)object + indexPath:(NSIndexPath *)indexPath { + IGParameterAssert(view != nil); + IGParameterAssert(listAdapter != nil); + IGParameterAssert(object != nil); + IGParameterAssert(indexPath != nil); + + [self.visibleViewObjectMap setObject:object forKey:view]; + NSCountedSet *visibleListSections = self.visibleListSections; + if ([visibleListSections countForObject:sectionController] == 0) { + [sectionController.displayDelegate listAdapter:listAdapter willDisplaySectionController:sectionController]; + [listAdapter.delegate listAdapter:listAdapter willDisplayObject:object atIndex:indexPath.section]; + } + [visibleListSections addObject:sectionController]; +} + +- (void)_didEndDisplayingReusableView:(UICollectionReusableView *)view + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + object:(id)object + indexPath:(NSIndexPath *)indexPath { + IGParameterAssert(view != nil); + IGParameterAssert(listAdapter != nil); + IGParameterAssert(indexPath != nil); + + if (object == nil || sectionController == nil) { + return; + } + + const NSInteger section = indexPath.section; + + NSCountedSet *visibleSections = self.visibleListSections; + [visibleSections removeObject:sectionController]; + + if ([visibleSections countForObject:sectionController] == 0) { + [sectionController.displayDelegate listAdapter:listAdapter didEndDisplayingSectionController:sectionController]; + [listAdapter.delegate listAdapter:listAdapter didEndDisplayingObject:object atIndex:section]; + } +} + +- (void)willDisplaySupplementaryView:(UICollectionReusableView *)view + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + object:(id)object + indexPath:(NSIndexPath *)indexPath { + [self _willDisplayReusableView:view forListAdapter:listAdapter sectionController:sectionController object:object indexPath:indexPath]; +} + +- (void)didEndDisplayingSupplementaryView:(UICollectionReusableView *)view + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + indexPath:(NSIndexPath *)indexPath { + // if cell display events break, don't send display events when the object has disappeared + id object = [self _pluckObjectForView:view]; + [self _didEndDisplayingReusableView:view forListAdapter:listAdapter sectionController:sectionController object:object indexPath:indexPath]; +} + +- (void)willDisplayCell:(UICollectionViewCell *)cell + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + object:(id)object + indexPath:(NSIndexPath *)indexPath { + id displayDelegate = [sectionController displayDelegate]; + [displayDelegate listAdapter:listAdapter willDisplaySectionController:sectionController cell:cell atIndex:indexPath.item]; + [self _willDisplayReusableView:cell forListAdapter:listAdapter sectionController:sectionController object:object indexPath:indexPath]; +} + +- (void)didEndDisplayingCell:(UICollectionViewCell *)cell + forListAdapter:(IGListAdapter *)listAdapter + sectionController:(IGListSectionController *)sectionController + indexPath:(NSIndexPath *)indexPath { + // if cell display events break, don't send cell events to the displayDelegate when the object has disappeared + id object = [self _pluckObjectForView:cell]; + if (object == nil) { + return; + } + + [sectionController.displayDelegate listAdapter:listAdapter didEndDisplayingSectionController:sectionController cell:cell atIndex:indexPath.item]; + [self _didEndDisplayingReusableView:cell forListAdapter:listAdapter sectionController:sectionController object:object indexPath:indexPath]; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListReloadIndexPath.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListReloadIndexPath.h new file mode 100644 index 0000000..279b22e --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListReloadIndexPath.h @@ -0,0 +1,52 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + An object with index path information for reloading an item during a batch update. + */ +IGLK_SUBCLASSING_RESTRICTED +@interface IGListReloadIndexPath : NSObject + +/** + The index path of the item before batch updates are applied. + */ +@property (nonatomic, strong, readonly) NSIndexPath *fromIndexPath; + +/** + The index path of the item after batch updates are applied. + */ +@property (nonatomic, strong, readonly) NSIndexPath *toIndexPath; + +/** + Creates a new reload object. + + @param fromIndexPath The index path of the item before batch updates. + @param toIndexPath The index path of the item after batch updates. + @return A new reload object. + */ +- (instancetype)initWithFromIndexPath:(NSIndexPath *)fromIndexPath + toIndexPath:(NSIndexPath *)toIndexPath NS_DESIGNATED_INITIALIZER; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListReloadIndexPath.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListReloadIndexPath.m new file mode 100644 index 0000000..4660a43 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListReloadIndexPath.m @@ -0,0 +1,21 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListReloadIndexPath.h" + +@implementation IGListReloadIndexPath + +- (instancetype)initWithFromIndexPath:(NSIndexPath *)fromIndexPath + toIndexPath:(NSIndexPath *)toIndexPath { + if (self = [super init]) { + _fromIndexPath = fromIndexPath; + _toIndexPath = toIndexPath; + } + return self; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionControllerInternal.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionControllerInternal.h new file mode 100644 index 0000000..40aa3e5 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionControllerInternal.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListSectionController.h" + +FOUNDATION_EXTERN void IGListSectionControllerPushThread(UIViewController *viewController, id collectionContext); + +FOUNDATION_EXTERN void IGListSectionControllerPopThread(void); + +@interface IGListSectionController() + +@property (nonatomic, weak, readwrite) id collectionContext; + +@property (nonatomic, weak, readwrite) UIViewController *viewController; + +@property (nonatomic, assign, readwrite) NSInteger section; + +@property (nonatomic, assign, readwrite) BOOL isFirstSection; + +@property (nonatomic, assign, readwrite) BOOL isLastSection; + +/* + Provides a way for specialized section controllers (like the stacked section controller) to reject invalid moves + */ +- (BOOL)canMoveItemAtIndex:(NSInteger)sourceItemIndex toIndex:(NSInteger)destinationItemIndex; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap+DebugDescription.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap+DebugDescription.h new file mode 100644 index 0000000..9f7207e --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap+DebugDescription.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import "IGListSectionMap.h" + +@interface IGListSectionMap (DebugDescription) + +- (NSArray *)debugDescriptionLines; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap+DebugDescription.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap+DebugDescription.m new file mode 100644 index 0000000..2df0984 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap+DebugDescription.m @@ -0,0 +1,30 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListSectionMap+DebugDescription.h" + +#import "IGListBindingSectionController.h" + +@implementation IGListSectionMap (DebugDescription) + +- (NSArray *)debugDescriptionLines { + NSMutableArray *debug = [NSMutableArray new]; +#if IGLK_DEBUG_DESCRIPTION_ENABLED + [self enumerateUsingBlock:^(id object, IGListSectionController *sectionController, NSInteger section, BOOL *stop) { + if ([sectionController isKindOfClass:[IGListBindingSectionController class]]) { + [debug addObject:[sectionController debugDescription]]; + } else { + [debug addObject:[NSString stringWithFormat:@"Object and section controller at section: %li:", (long)section]]; + [debug addObject:[NSString stringWithFormat:@" %@", object]]; + [debug addObject:[NSString stringWithFormat:@" %@", sectionController]]; + } + }]; +#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED + return debug; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap.h new file mode 100644 index 0000000..1fd16aa --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap.h @@ -0,0 +1,115 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +@class IGListSectionController; + + +NS_ASSUME_NONNULL_BEGIN + +/** + The IGListSectionMap provides a way to map a collection of objects to a collection of section controllers and achieve + constant-time lookups O(1). + + IGListSectionMap is a mutable object and does not guarantee thread safety. + */ +IGLK_SUBCLASSING_RESTRICTED +@interface IGListSectionMap : NSObject + +- (instancetype)initWithMapTable:(NSMapTable *)mapTable NS_DESIGNATED_INITIALIZER; + +/** + The objects stored in the map. + */ +@property (nonatomic, strong, readonly) NSArray *objects; + +/** + Update the map with objects and the section controller counterparts. + + @param objects The objects in the collection. + @param sectionControllers The section controllers that map to each object. + */ +- (void)updateWithObjects:(NSArray > *)objects sectionControllers:(NSArray *)sectionControllers; + +/** + Fetch a section controller given a section. + + @param section The section index of the section controller. + + @return A section controller. + */ +- (nullable IGListSectionController *)sectionControllerForSection:(NSInteger)section; + +/** + Fetch the object for a section + + @param section The section index of the object. + + @return The object corresponding to the section. + */ +- (nullable id)objectForSection:(NSInteger)section; + +/** + Fetch a section controller given an object. Can return nil. + + @param object The object that maps to a section controller. + + @return A section controller. + */ +- (nullable id)sectionControllerForObject:(id)object; + +/** + Look up the section index for a section controller. + + @param sectionController The list to look up. + + @return The section index of the given section controller if it exists, NSNotFound otherwise. + */ +- (NSInteger)sectionForSectionController:(id)sectionController; + +/** + Look up the section index for an object. + + @param object The object to look up. + + @return The section index of the given object if it exists, NSNotFound otherwise. + */ +- (NSInteger)sectionForObject:(id)object; + +/** + Remove all saved objects and section controllers. + */ +- (void)reset; + +/** + Update an object with a new instance. + */ +- (void)updateObject:(id)object; + +/** + Applies a given block object to the entries of the section controller map. + + @param block A block object to operate on entries in the section controller map. + */ +- (void)enumerateUsingBlock:(void (^)(id object, IGListSectionController *sectionController, NSInteger section, BOOL *stop))block; + +/** + :nodoc: + */ +- (instancetype)init NS_UNAVAILABLE; + +/** + :nodoc: + */ ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap.m new file mode 100644 index 0000000..0b856e8 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListSectionMap.m @@ -0,0 +1,155 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListSectionMap.h" + +#import + +#import "IGListSectionControllerInternal.h" + +@interface IGListSectionMap () + +// both of these maps allow fast lookups of objects, list objects, and indexes +@property (nonatomic, strong, readonly, nonnull) NSMapTable *objectToSectionControllerMap; +@property (nonatomic, strong, readonly, nonnull) NSMapTable *sectionControllerToSectionMap; + +@property (nonatomic, strong, nonnull) NSMutableArray *mObjects; + +@end + +@implementation IGListSectionMap + +- (instancetype)initWithMapTable:(NSMapTable *)mapTable { + IGParameterAssert(mapTable != nil); + + if (self = [super init]) { + _objectToSectionControllerMap = [mapTable copy]; + + // lookup list objects by pointer equality + _sectionControllerToSectionMap = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory | NSMapTableObjectPointerPersonality + valueOptions:NSMapTableStrongMemory + capacity:0]; + _mObjects = [NSMutableArray new]; + } + return self; +} + + +#pragma mark - Public API + +- (NSArray *)objects { + return [self.mObjects copy]; +} + +- (NSInteger)sectionForSectionController:(IGListSectionController *)sectionController { + IGParameterAssert(sectionController != nil); + + NSNumber *index = [self.sectionControllerToSectionMap objectForKey:sectionController]; + return index != nil ? [index integerValue] : NSNotFound; +} + +- (IGListSectionController *)sectionControllerForSection:(NSInteger)section { + return [self.objectToSectionControllerMap objectForKey:[self objectForSection:section]]; +} + +- (void)updateWithObjects:(NSArray *)objects sectionControllers:(NSArray *)sectionControllers { + IGParameterAssert(objects.count == sectionControllers.count); + + [self reset]; + + self.mObjects = [objects mutableCopy]; + + id firstObject = objects.firstObject; + id lastObject = objects.lastObject; + + [objects enumerateObjectsUsingBlock:^(id object, NSUInteger idx, BOOL *stop) { + IGListSectionController *sectionController = sectionControllers[idx]; + + // set the index of the list for easy reverse lookup + [self.sectionControllerToSectionMap setObject:@(idx) forKey:sectionController]; + [self.objectToSectionControllerMap setObject:sectionController forKey:object]; + + sectionController.isFirstSection = (object == firstObject); + sectionController.isLastSection = (object == lastObject); + sectionController.section = (NSInteger)idx; + }]; +} + +- (nullable IGListSectionController *)sectionControllerForObject:(id)object { + IGParameterAssert(object != nil); + + return [self.objectToSectionControllerMap objectForKey:object]; +} + +- (nullable id)objectForSection:(NSInteger)section { + NSArray *objects = self.mObjects; + if (section < objects.count) { + return objects[section]; + } else { + return nil; + } +} + +- (NSInteger)sectionForObject:(id)object { + IGParameterAssert(object != nil); + + id sectionController = [self sectionControllerForObject:object]; + if (sectionController == nil) { + return NSNotFound; + } else { + return [self sectionForSectionController:sectionController]; + } +} + +- (void)reset { + [self enumerateUsingBlock:^(id _Nonnull object, IGListSectionController * _Nonnull sectionController, NSInteger section, BOOL * _Nonnull stop) { + sectionController.section = NSNotFound; + sectionController.isFirstSection = NO; + sectionController.isLastSection = NO; + }]; + + [self.sectionControllerToSectionMap removeAllObjects]; + [self.objectToSectionControllerMap removeAllObjects]; +} + +- (void)updateObject:(id)object { + IGParameterAssert(object != nil); + const NSInteger section = [self sectionForObject:object]; + id sectionController = [self sectionControllerForObject:object]; + [self.sectionControllerToSectionMap setObject:@(section) forKey:sectionController]; + [self.objectToSectionControllerMap setObject:sectionController forKey:object]; + self.mObjects[section] = object; +} + +- (void)enumerateUsingBlock:(void (^)(id object, IGListSectionController *sectionController, NSInteger section, BOOL *stop))block { + IGParameterAssert(block != nil); + + BOOL stop = NO; + NSArray *objects = self.objects; + for (NSInteger section = 0; section < objects.count; section++) { + id object = objects[section]; + IGListSectionController *sectionController = [self sectionControllerForObject:object]; + block(object, sectionController, section, &stop); + if (stop) { + break; + } + } +} + + +#pragma mark - NSCopying + +- (id)copyWithZone:(NSZone *)zone { + IGListSectionMap *copy = [[IGListSectionMap allocWithZone:zone] initWithMapTable:self.objectToSectionControllerMap]; + if (copy != nil) { + copy->_sectionControllerToSectionMap = [self.sectionControllerToSectionMap copy]; + copy->_mObjects = [self.mObjects mutableCopy]; + } + return copy; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListWorkingRangeHandler.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListWorkingRangeHandler.h new file mode 100644 index 0000000..c37580d --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListWorkingRangeHandler.h @@ -0,0 +1,40 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListAdapter; + +@interface IGListWorkingRangeHandler : NSObject + +/** + Initializes the working range handler. + + @param workingRangeSize the number of sections beyond the visible viewport that should be considered within the working + range. Applies equally in both directions above and below the viewport. + */ +- (instancetype)initWithWorkingRangeSize:(NSInteger)workingRangeSize; + +/** + Tells the handler that a cell will be displayed in the IGListKit infra. + + @param indexPath The index path of the cell in the UICollectionView. + @param listAdapter The adapter managing the infra. + */ +- (void)willDisplayItemAtIndexPath:(NSIndexPath *)indexPath + forListAdapter:(IGListAdapter *)listAdapter; + +/** + Tells the handler that a cell did end display in the IGListKit infra. + + @param indexPath The index path of the cell in the UICollectionView. + @param listAdapter The adapter managing the infra. + */ +- (void)didEndDisplayingItemAtIndexPath:(NSIndexPath *)indexPath + forListAdapter:(IGListAdapter *)listAdapter; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListWorkingRangeHandler.mm b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListWorkingRangeHandler.mm new file mode 100644 index 0000000..3dd9d91 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/IGListWorkingRangeHandler.mm @@ -0,0 +1,154 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "IGListWorkingRangeHandler.h" + +#import +#import + +#import +#import +#import + +struct _IGListWorkingRangeHandlerIndexPath { + NSInteger section; + NSInteger row; + size_t hash; + + bool operator==(const _IGListWorkingRangeHandlerIndexPath &other) const { + return (section == other.section && row == other.row); + } +}; + +struct _IGListWorkingRangeHandlerSectionControllerWrapper { + IGListSectionController *sectionController; + + bool operator==(const _IGListWorkingRangeHandlerSectionControllerWrapper &other) const { + return (sectionController == other.sectionController); + } +}; + +struct _IGListWorkingRangeHandlerIndexPathHash { + size_t operator()(const _IGListWorkingRangeHandlerIndexPath& o) const { + return (size_t)o.hash; + } +}; + +struct _IGListWorkingRangeHashID { + size_t operator()(const _IGListWorkingRangeHandlerSectionControllerWrapper &o) const { + return (size_t)[o.sectionController hash]; + } +}; + +typedef std::unordered_set<_IGListWorkingRangeHandlerSectionControllerWrapper, _IGListWorkingRangeHashID> _IGListWorkingRangeSectionControllerSet; +typedef std::unordered_set<_IGListWorkingRangeHandlerIndexPath, _IGListWorkingRangeHandlerIndexPathHash> _IGListWorkingRangeIndexPathSet; + +@interface IGListWorkingRangeHandler () + +@property (nonatomic, assign, readonly) NSInteger workingRangeSize; + +@end + +@implementation IGListWorkingRangeHandler { + _IGListWorkingRangeIndexPathSet _visibleSectionIndices; + _IGListWorkingRangeSectionControllerSet _workingRangeSectionControllers; +} + +- (instancetype)initWithWorkingRangeSize:(NSInteger)workingRangeSize { + if (self = [super init]) { + _workingRangeSize = workingRangeSize; + } + return self; +} + +- (void)willDisplayItemAtIndexPath:(NSIndexPath *)indexPath + forListAdapter:(IGListAdapter *)listAdapter { + IGParameterAssert(indexPath != nil); + IGParameterAssert(listAdapter != nil); + + _visibleSectionIndices.insert({ + .section = indexPath.section, + .row = indexPath.row, + .hash = indexPath.hash + }); + + [self _updateWorkingRangesWithListAdapter:listAdapter]; +} + +- (void)didEndDisplayingItemAtIndexPath:(NSIndexPath *)indexPath + forListAdapter:(IGListAdapter *)listAdapter { + IGParameterAssert(indexPath != nil); + IGParameterAssert(listAdapter != nil); + + _visibleSectionIndices.erase({ + .section = indexPath.section, + .row = indexPath.row, + .hash = indexPath.hash + }); + + [self _updateWorkingRangesWithListAdapter:listAdapter]; +} + +#pragma mark - Working Ranges + +- (void)_updateWorkingRangesWithListAdapter:(IGListAdapter *)listAdapter { + IGAssertMainThread(); + // This method is optimized C++ to improve straight-line speed of these operations. Change at your peril. + + // We use a std::set because it is ordered. + std::set visibleSectionSet = std::set(); + for (const _IGListWorkingRangeHandlerIndexPath &indexPath : _visibleSectionIndices) { + visibleSectionSet.insert(indexPath.section); + } + + NSInteger start; + NSInteger end; + if (visibleSectionSet.size() == 0) { + // We're now devoid of any visible sections. Bail + start = 0; + end = 0; + } else { + start = MAX(*visibleSectionSet.begin() - _workingRangeSize, 0); + end = MIN(*visibleSectionSet.rbegin() + 1 + _workingRangeSize, (NSInteger)listAdapter.objects.count); + } + + // Build the current set of working range section controllers + _IGListWorkingRangeSectionControllerSet workingRangeSectionControllers (visibleSectionSet.size()); + for (NSInteger idx = start; idx < end; idx++) { + id item = [listAdapter objectAtSection:idx]; + IGListSectionController *sectionController = [listAdapter sectionControllerForObject:item]; + workingRangeSectionControllers.insert({sectionController}); + } + + IGAssert(workingRangeSectionControllers.size() < 1000, @"This algorithm is way too slow with so many objects:%lu", workingRangeSectionControllers.size()); + + // Tell any new section controllers that they have entered the working range + for (const _IGListWorkingRangeHandlerSectionControllerWrapper &wrapper : workingRangeSectionControllers) { + // Check if the item exists in the old working range item array. + auto it = _workingRangeSectionControllers.find(wrapper); + if (it == _workingRangeSectionControllers.end()) { + // The section controller isn't in the existing list, so it's new. + id workingRangeDelegate = wrapper.sectionController.workingRangeDelegate; + [workingRangeDelegate listAdapter:listAdapter sectionControllerWillEnterWorkingRange:wrapper.sectionController]; + } + } + + // Tell any removed section controllers that they have exited the working range + for (const _IGListWorkingRangeHandlerSectionControllerWrapper &wrapper : _workingRangeSectionControllers) { + // Check if the item exists in the new list of section controllers + auto it = workingRangeSectionControllers.find(wrapper); + if (it == workingRangeSectionControllers.end()) { + // If the item does not exist in the new list, then it's been removed. + id workingRangeDelegate = wrapper.sectionController.workingRangeDelegate; + [workingRangeDelegate listAdapter:listAdapter sectionControllerDidExitWorkingRange:wrapper.sectionController]; + } + } + + _workingRangeSectionControllers = workingRangeSectionControllers; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+DebugDescription.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+DebugDescription.h new file mode 100644 index 0000000..9613142 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+DebugDescription.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface UICollectionView (DebugDescription) + +- (NSArray *)debugDescriptionLines; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+DebugDescription.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+DebugDescription.m new file mode 100644 index 0000000..a70758a --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+DebugDescription.m @@ -0,0 +1,43 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "UICollectionView+DebugDescription.h" + +#import + +@implementation UICollectionView (DebugDescription) + +- (NSArray *)debugDescriptionLines { + NSMutableArray *debug = [NSMutableArray new]; +#if IGLK_DEBUG_DESCRIPTION_ENABLED + [debug addObject:[NSString stringWithFormat:@"Class: %@, instance: %p", NSStringFromClass(self.class), self]]; + [debug addObject:[NSString stringWithFormat:@"Data source: %@", self.dataSource]]; + [debug addObject:[NSString stringWithFormat:@"Delegate: %@", self.delegate]]; + [debug addObject:[NSString stringWithFormat:@"Layout: %@", self.collectionViewLayout]]; + [debug addObject:[NSString stringWithFormat:@"Frame: %@, bounds: %@", + NSStringFromCGRect(self.frame), NSStringFromCGRect(self.bounds)]]; + + const NSInteger sections = [self numberOfSections]; + [debug addObject:[NSString stringWithFormat:@"Number of sections: %lld", (long long)sections]]; + + for (NSInteger section = 0; section < sections; section++) { + [debug addObject:[NSString stringWithFormat:@" %lld items in section %lld", + (long long)[self numberOfItemsInSection:section], (long long)section]]; + } + + [debug addObject:@"Visible cell details:"]; + NSArray *visibleIndexPaths = [[self indexPathsForVisibleItems] sortedArrayUsingSelector:@selector(compare:)]; + for (NSIndexPath *path in visibleIndexPaths) { + [debug addObject:[NSString stringWithFormat:@" Visible cell at section %lld, item %lld:", + (long long)path.section, (long long)path.item]]; + [debug addObject:[NSString stringWithFormat:@" %@", [[self cellForItemAtIndexPath:path] description] ?: @""]]; + } +#endif // #if IGLK_DEBUG_DESCRIPTION_ENABLED + return debug; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.h new file mode 100644 index 0000000..acaf6e5 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.h @@ -0,0 +1,16 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@class IGListBatchUpdateData; + +@interface UICollectionView (IGListBatchUpdateData) + +- (void)ig_applyBatchUpdateData:(IGListBatchUpdateData *)updateData; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.m new file mode 100644 index 0000000..3101be1 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.m @@ -0,0 +1,31 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "UICollectionView+IGListBatchUpdateData.h" + +#import + +@implementation UICollectionView (IGListBatchUpdateData) + +- (void)ig_applyBatchUpdateData:(IGListBatchUpdateData *)updateData { + [self deleteItemsAtIndexPaths:updateData.deleteIndexPaths]; + [self insertItemsAtIndexPaths:updateData.insertIndexPaths]; + [self reloadItemsAtIndexPaths:updateData.updateIndexPaths]; + + for (IGListMoveIndexPath *move in updateData.moveIndexPaths) { + [self moveItemAtIndexPath:move.from toIndexPath:move.to]; + } + + for (IGListMoveIndex *move in updateData.moveSections) { + [self moveSection:move.from toSection:move.to]; + } + + [self deleteSections:updateData.deleteSections]; + [self insertSections:updateData.insertSections]; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.h new file mode 100644 index 0000000..bb7e0ff --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.h @@ -0,0 +1,24 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface UICollectionViewLayout (InteractiveReordering) + +- (void)ig_hijackLayoutInteractiveReorderingMethodForAdapter:(IGListAdapter *)adapter; + +- (nullable NSIndexPath *)updatedTargetForInteractivelyMovingItem:(NSIndexPath *)previousIndexPath + toIndexPath:(NSIndexPath *)originalTarget + adapter:(IGListAdapter *)adapter; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.m new file mode 100644 index 0000000..aebe200 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.m @@ -0,0 +1,195 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "UICollectionViewLayout+InteractiveReordering.h" + +#import + +#import +#import + +@implementation UICollectionViewLayout (InteractiveReordering) + +static void * kIGListAdapterKey = &kIGListAdapterKey; + ++ (void)load +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + Class layoutClass = [self class]; + + // override implementation for targetIndexPathForInteractivelyMovingItem:withPosition: + SEL userMoveSelector = @selector(targetIndexPathForInteractivelyMovingItem:withPosition:); + SEL overrideSelector = @selector(ig_targetIndexPathForInteractivelyMovingItem:withPosition:); + Method userLayoutMethod = class_getInstanceMethod(layoutClass, userMoveSelector); + Method overrideLayoutMethod = class_getInstanceMethod(layoutClass, overrideSelector); + method_exchangeImplementations(userLayoutMethod, overrideLayoutMethod); + + // override implementation for + // invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition: + SEL userInvalidationSelector = + @selector(invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition:); + SEL overrideInvalidationSelector = + @selector(ig_invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition:); + Method userInvalidationMethod = class_getInstanceMethod(layoutClass, userInvalidationSelector); + Method overrideInvalidationMethod = class_getInstanceMethod(layoutClass, overrideInvalidationSelector); + method_exchangeImplementations(userInvalidationMethod, overrideInvalidationMethod); + + // override implementation for + // invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition: + SEL userEndInvalidationSelector = + @selector(invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:previousIndexPaths:movementCancelled:); + SEL overrideEndInvalidationSelector = + @selector(ig_invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:previousIndexPaths:movementCancelled:); + Method userEndInvalidationMethod = class_getInstanceMethod(layoutClass, userEndInvalidationSelector); + Method overrideEndInvalidationMethod = class_getInstanceMethod(layoutClass, overrideEndInvalidationSelector); + method_exchangeImplementations(userEndInvalidationMethod, overrideEndInvalidationMethod); + }); +} + +- (void)ig_hijackLayoutInteractiveReorderingMethodForAdapter:(IGListAdapter *)adapter { + objc_setAssociatedObject(self, kIGListAdapterKey, adapter, OBJC_ASSOCIATION_ASSIGN); +} + +- (NSIndexPath *)ig_targetIndexPathForInteractivelyMovingItem:(NSIndexPath *)previousIndexPath + withPosition:(CGPoint)position NS_AVAILABLE_IOS(9_0) { + // call looks recursive, but through swizzling is calling the original implementation for + // targetIndexPathForInteractivelyMovingItem:withPosition: + NSIndexPath *originalTarget = [self ig_targetIndexPathForInteractivelyMovingItem:previousIndexPath + withPosition:position]; + + IGListAdapter *adapter = (IGListAdapter *)objc_getAssociatedObject(self, kIGListAdapterKey); + if (adapter == nil) { + return originalTarget; + } + + NSIndexPath *updatedTarget = [self updatedTargetForInteractivelyMovingItem:previousIndexPath + toIndexPath:originalTarget + adapter:adapter]; + if (updatedTarget) { + return updatedTarget; + } + return originalTarget; +} + +- (nullable NSIndexPath *)updatedTargetForInteractivelyMovingItem:(NSIndexPath *)previousIndexPath + toIndexPath:(NSIndexPath *)originalTarget + adapter:(IGListAdapter *)adapter { + const NSInteger sourceSectionIndex = previousIndexPath.section; + NSInteger destinationSectionIndex = originalTarget.section; + NSInteger destinationItemIndex = originalTarget.item; + + IGListSectionController *sourceSectionController = [adapter sectionControllerForSection:sourceSectionIndex]; + IGListSectionController *destinationSectionController = [adapter sectionControllerForSection:destinationSectionIndex]; + + adapter.isLastInteractiveMoveToLastSectionIndex = NO; + + // this is a reordering of sections themselves + if ([sourceSectionController numberOfItems] == 1 + && [destinationSectionController numberOfItems] == 1) { + + if (destinationItemIndex == 1) { + // the "item" representing our section was dropped + // into the end of a destination section rather than the beginning + // so it really belongs one section after the section where it landed + if (destinationSectionIndex < [[adapter objects] count] - 1) { + destinationSectionIndex += 1; + destinationItemIndex = 0; + } + else { + // if we're moving an item to the last spot, our index would exceed the number of sections available + // so we have to special case this scenario. iOS doesnt allow an item move to "create" a new section + adapter.isLastInteractiveMoveToLastSectionIndex = YES; + } + NSIndexPath *updatedTarget = [NSIndexPath indexPathForItem:destinationItemIndex + inSection:destinationSectionIndex]; + return updatedTarget; + } + } + + return nil; +} + +- (UICollectionViewLayoutInvalidationContext *)ig_invalidationContextForInteractivelyMovingItems:(NSArray *)targetIndexPaths withTargetPosition:(CGPoint)targetPosition previousIndexPaths:(NSArray *)previousIndexPaths previousPosition:(CGPoint)previousPosition { + + // call looks recursive, but through swizzling is calling the original implementation for + // invalidationContextForInteractivelyMovingItems:withTargetPosition:previousIndexPaths:previousPosition: + UICollectionViewLayoutInvalidationContext *originalContext = + [self ig_invalidationContextForInteractivelyMovingItems:targetIndexPaths withTargetPosition:targetPosition previousIndexPaths:previousIndexPaths previousPosition:previousPosition]; + + return [self ig_cleanupInvalidationContext:originalContext]; +} + +- (UICollectionViewLayoutInvalidationContext *)ig_invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:(NSArray *)indexPaths previousIndexPaths:(NSArray *)previousIndexPaths movementCancelled:(BOOL)movementCancelled { + + // call looks recursive, but through swizzling is calling the original implementation for + // invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:previousIndexPaths:movementCancelled: + UICollectionViewLayoutInvalidationContext *originalContext = + [self ig_invalidationContextForEndingInteractiveMovementOfItemsToFinalIndexPaths:indexPaths previousIndexPaths:previousIndexPaths movementCancelled:movementCancelled]; + + return [self ig_cleanupInvalidationContext:originalContext]; +} + +- (UICollectionViewLayoutInvalidationContext *)ig_cleanupInvalidationContext:(UICollectionViewLayoutInvalidationContext *)originalContext { + IGListAdapter *adapter = (IGListAdapter *)objc_getAssociatedObject(self, kIGListAdapterKey); + if (adapter == nil || !self.collectionView) { + return originalContext; + } + + const NSInteger numSections = [adapter numberOfSectionsInCollectionView:(UICollectionView * _Nonnull)self.collectionView]; + + // protect against invalidating an index path that no longer exists + // (like item 1 in the last section after interactively reordering an item to the end of a list of 1 item sections) + if ([originalContext.invalidatedItemIndexPaths count] > 0) { + NSUInteger indexToRemove = NSNotFound; + + indexToRemove = [originalContext.invalidatedItemIndexPaths indexOfObjectPassingTest: + ^BOOL(NSIndexPath * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (obj.section == numSections-1) { + IGListSectionController *section = [adapter sectionControllerForSection:obj.section]; + return obj.item > [section numberOfItems] - 1; + } + return NO; + }]; + + if (indexToRemove != NSNotFound) { + NSMutableArray *invalidatedItemIndexPaths = [originalContext.invalidatedItemIndexPaths mutableCopy]; + [invalidatedItemIndexPaths removeObjectAtIndex:indexToRemove]; + + UICollectionViewLayoutInvalidationContext *modifiedContext; + if ([originalContext isKindOfClass:[UICollectionViewFlowLayoutInvalidationContext class]]) { + // UICollectionViewFlowLayout has a special invalidation context subclass + UICollectionViewFlowLayoutInvalidationContext *flowModifiedContext = + [[self.class invalidationContextClass] new]; + + flowModifiedContext.invalidateFlowLayoutDelegateMetrics = + [(UICollectionViewFlowLayoutInvalidationContext *)originalContext invalidateFlowLayoutDelegateMetrics]; + flowModifiedContext.invalidateFlowLayoutAttributes = + [(UICollectionViewFlowLayoutInvalidationContext *)originalContext invalidateFlowLayoutAttributes]; + modifiedContext = flowModifiedContext; + } + else { + modifiedContext = [[self.class invalidationContextClass] new]; + } + + [modifiedContext invalidateItemsAtIndexPaths:invalidatedItemIndexPaths]; + [originalContext.invalidatedSupplementaryIndexPaths enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSArray * _Nonnull obj, BOOL * _Nonnull stop) { + [modifiedContext invalidateSupplementaryElementsOfKind:key atIndexPaths:obj]; + }]; + [originalContext.invalidatedDecorationIndexPaths enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSArray * _Nonnull obj, BOOL * _Nonnull stop) { + [modifiedContext invalidateDecorationElementsOfKind:key atIndexPaths:obj]; + }]; + modifiedContext.contentOffsetAdjustment = originalContext.contentOffsetAdjustment; + modifiedContext.contentSizeAdjustment = originalContext.contentSizeAdjustment; + + return modifiedContext; + } + } + return originalContext; +} + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/UIScrollView+IGListKit.h b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UIScrollView+IGListKit.h new file mode 100644 index 0000000..22d1992 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UIScrollView+IGListKit.h @@ -0,0 +1,14 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import + +@interface UIScrollView (IGListKit) + +- (UIEdgeInsets) ig_contentInset; + +@end diff --git a/Demo/Pods/IGListKit/Source/IGListKit/Internal/UIScrollView+IGListKit.m b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UIScrollView+IGListKit.m new file mode 100644 index 0000000..afc7d08 --- /dev/null +++ b/Demo/Pods/IGListKit/Source/IGListKit/Internal/UIScrollView+IGListKit.m @@ -0,0 +1,21 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#import "UIScrollView+IGListKit.h" + +@implementation UIScrollView (IGListKit) + +- (UIEdgeInsets) ig_contentInset +{ + if (@available(iOS 11.0, tvOS 11.0, *)) { + return self.adjustedContentInset; + } else { + return self.contentInset; + } +} + +@end diff --git a/Demo/Pods/Kingfisher/LICENSE b/Demo/Pods/Kingfisher/LICENSE new file mode 100644 index 0000000..80888ba --- /dev/null +++ b/Demo/Pods/Kingfisher/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2019 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/Demo/Pods/Kingfisher/README.md b/Demo/Pods/Kingfisher/README.md new file mode 100644 index 0000000..572b847 --- /dev/null +++ b/Demo/Pods/Kingfisher/README.md @@ -0,0 +1,258 @@ +

+Kingfisher +

+ +

+ + + + + +
+ + +

+ +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 the cache to boost your app. +- [x] Extensions for `UIImageView`, `NSImageView`, `NSButton`, `UIButton`, `NSTextAttachment`, `WKInterfaceImage`, `TVMonogramView` and `CPListItem` 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. +- [x] Low Data Mode support. +- [x] SwiftUI support. + +### Kingfisher 101 + +The simplest use-case is setting an image to an image view with the `UIImageView` extension: + +```swift +import Kingfisher + +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 it with the same URL later, the image will be retrieved from the cache and shown immediately. + +It also works if you use SwiftUI: + +```swift +var body: some View { + KFImage(URL(string: "https://example.com/image.png")!) +} +``` + +### 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.bounds.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 a common situation I can meet in my daily work. Think about how many lines you need to write without +Kingfisher! + +### Method Chaining + +If you are not a fan of the `kf` extension, you can also prefer to use the `KF` builder and chained the method +invocations. The code below is doing the same thing: + +```swift +// Use `kf` extension +imageView.kf.setImage( + with: url, + placeholder: placeholderImage, + options: [ + .processor(processor), + .loadDiskFileSynchronously, + .cacheOriginalImage, + .transition(.fade(0.25)), + .lowDataMode(.network(lowResolutionURL)) + ], + progressBlock: { receivedSize, totalSize in + // Progress updated + }, + completionHandler: { result in + // Done + } +) + +// Use `KF` builder +KF.url(url) + .placeholder(placeholderImage) + .setProcessor(processor) + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .fade(duration: 0.25) + .lowDataModeSource(.network(lowResolutionURL)) + .onProgress { receivedSize, totalSize in } + .onSuccess { result in } + .onFailure { error in } + .set(to: imageView) +``` + +And even better, if later you want to switch to SwiftUI, just change the `KF` above to `KFImage`, and you've done: + +```swift +struct ContentView: View { + var body: some View { + KFImage.url(url) + .placeholder(placeholderImage) + .setProcessor(processor) + .loadDiskFileSynchronously() + .cacheMemoryOnly() + .fade(duration: 0.25) + .lowDataModeSource(.network(lowResolutionURL)) + .onProgress { receivedSize, totalSize in } + .onSuccess { result in } + .onFailure { error in } + } +} +``` + +### Learn More + +To learn the use of Kingfisher by more examples, take a look at the well-prepared [Cheat Sheet](https://github.com/onevcat/Kingfisher/wiki/Cheat-Sheet). +There we summarized the most common tasks in Kingfisher, you can get a better idea of what this framework can do. +There are also some performance tips, remember to check them too. + +## Requirements + +- iOS 12.0+ / macOS 10.14+ / tvOS 12.0+ / watchOS 5.0+ (if you use only UIKit/AppKit) +- iOS 14.0+ / macOS 11.0+ / tvOS 14.0+ / watchOS 7.0+ (if you use it in SwiftUI) +- Swift 5.0+ + +> If you need support from iOS 10 (UIKit/AppKit) or iOS 13 (SwiftUI), use Kingfisher version 6.x. But it won't work +> with Xcode 13.0 and Xcode 13.1 [#1802](https://github.com/onevcat/Kingfisher/issues/1802). +> +> If you need to use Xcode 13.0 and 13.1 but cannot upgrade to v7, use the `version6-xcode13` branch. However, you have to drop +> iOS 10 support due to another Xcode 13 bug. +> +> | UIKit | SwiftUI | Xcode | Kingfisher | +> |---|---|---|---| +> | iOS 10+ | iOS 13+ | 12 | ~> 6.3.1 | +> | iOS 11+ | iOS 13+ | 13 | `version6-xcode13` | +> | iOS 12+ | iOS 14+ | 13 | ~> 7.0 | + +### Installation + +A detailed guide for installation can be found in [Installation Guide](https://github.com/onevcat/Kingfisher/wiki/Installation-Guide). + +#### Swift Package Manager + +- File > Swift Packages > Add Package Dependency +- Add `https://github.com/onevcat/Kingfisher.git` +- Select "Up to Next Major" with "7.0.0" + +#### CocoaPods + +```ruby +source 'https://github.com/CocoaPods/Specs.git' +platform :ios, '12.0' +use_frameworks! + +target 'MyApp' do + pod 'Kingfisher', '~> 7.0' +end +``` + +#### Carthage + +``` +github "onevcat/Kingfisher" ~> 7.0 +``` + + +### Migrating + +[Kingfisher 7.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-7.0-Migration-Guide) - Kingfisher 7.x is NOT fully compatible with the previous version. However, changes should be trivial or not required at all. Please follow the [migration guide](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-7.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 6.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-6.0-Migration-Guide) - Kingfisher 6.x is NOT fully compatible with the previous version. However, 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-6.0-Migration-Guide) when you prepare to upgrade Kingfisher in your project. +> - [Kingfisher 5.0 Migration](https://github.com/onevcat/Kingfisher/wiki/Kingfisher-5.0-Migration-Guide) - If you are upgrading to Kingfisher 5.x from 4.x, please read this for more information. +> - 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 were removed, so please ensure you have no warning left before you migrate from Kingfisher 3 with Kingfisher 4. If you have any trouble when 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](https://swiftpackageindex.com/onevcat/Kingfisher/master/documentation/kingfisher) - Lastly, please remember to read the full API reference whenever you need more detailed documentation. + +## Other + +### Future of Kingfisher + +I want to keep Kingfisher lightweight. This framework focuses 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 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, [open a ticket](https://github.com/onevcat/Kingfisher/issues/new). Pull requests are warmly welcome as well. + +## Backers & Sponsors + +Open-source projects cannot live long without your help. If you find Kingfisher to be useful, please consider supporting this +project by becoming a sponsor. Your user icon or company logo shows up [on my blog](https://onevcat.com/tabs/about/) with a link to your home page. + +Become a sponsor through [GitHub Sponsors](https://github.com/sponsors/onevcat). :heart: + +Special thanks to: + +[![imgly](https://user-images.githubusercontent.com/1812216/106253726-271ed000-6218-11eb-98e0-c9c681925770.png)](https://img.ly/) + +### License + +Kingfisher is released under the MIT license. See LICENSE for details. diff --git a/Demo/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift b/Demo/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift new file mode 100644 index 0000000..f3a94bd --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Cache/CacheSerializer.swift @@ -0,0 +1,132 @@ +// +// CacheSerializer.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/02. +// +// Copyright (c) 2019 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. + +import Foundation +import CoreGraphics + +/// 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: KFCrossPlatformImage, 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) -> KFCrossPlatformImage? + + /// Whether this serializer prefers to cache the original data in its implementation. + /// If `true`, after creating the image from the disk data, Kingfisher will continue to apply the processor to get + /// the final image. + /// + /// By default, it is `false` and the actual processed image is assumed to be serialized to the disk. + var originalDataUsed: Bool { get } +} + +public extension CacheSerializer { + var originalDataUsed: Bool { false } +} + +/// 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() + + /// The compression quality when converting image to a lossy format data. Default is 1.0. + public var compressionQuality: CGFloat = 1.0 + + /// Whether the original data should be preferred when serializing the image. + /// If `true`, the input original data will be checked first and used unless the data is `nil`. + /// In that case, the serialization will fall back to creating data from image. + public var preferCacheOriginalData: Bool = false + + /// Returnes the `preferCacheOriginalData` value. When the original data is used, Kingfisher needs to re-apply the + /// processors to get the desired final image. + public var originalDataUsed: Bool { preferCacheOriginalData } + + /// Creates a cache serializer that serialize and deserialize images in PNG, JPEG and GIF format. + /// + /// - Note: + /// Use `DefaultCacheSerializer.default` unless you need to specify your own properties. + /// + public 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 + /// If `original` is `nil`, the input `image` will be encoded as PNG data. + public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { + if preferCacheOriginalData { + return original ?? + image.kf.data( + format: original?.kf.imageFormat ?? .unknown, + compressionQuality: compressionQuality + ) + } else { + return image.kf.data( + format: original?.kf.imageFormat ?? .unknown, + compressionQuality: compressionQuality + ) + } + } + + /// 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) -> KFCrossPlatformImage? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Cache/DiskStorage.swift b/Demo/Pods/Kingfisher/Sources/Cache/DiskStorage.swift new file mode 100644 index 0000000..89853ce --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Cache/DiskStorage.swift @@ -0,0 +1,588 @@ +// +// DiskStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 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. + +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 { + /// 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 + + var maybeCached : Set? + let maybeCachedCheckingQueue = DispatchQueue(label: "com.onevcat.Kingfisher.maybeCachedCheckingQueue") + + // `false` if the storage initialized with an error. This prevents unexpected forcibly crash when creating + // storage in the default cache. + private var storageReady: Bool = true + + /// 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 convenience init(config: Config) throws { + self.init(noThrowConfig: config, creatingDirectory: false) + try prepareDirectory() + } + + // If `creatingDirectory` is `false`, the directory preparation will be skipped. + // We need to call `prepareDirectory` manually after this returns. + init(noThrowConfig config: Config, creatingDirectory: Bool) { + var config = config + + let creation = Creation(config) + self.directoryURL = creation.directoryURL + + // Break any possible retain cycle set by outside. + config.cachePathBlock = nil + self.config = config + + metaChangingQueue = DispatchQueue(label: creation.cacheName) + setupCacheChecking() + + if creatingDirectory { + try? prepareDirectory() + } + } + + private func setupCacheChecking() { + maybeCachedCheckingQueue.async { + do { + self.maybeCached = Set() + try self.config.fileManager.contentsOfDirectory(atPath: self.directoryURL.path).forEach { fileName in + self.maybeCached?.insert(fileName) + } + } catch { + // Just disable the functionality if we fail to initialize it properly. This will just revert to + // the behavior which is to check file existence on disk directly. + self.maybeCached = nil + } + } + } + + // Creates the storage folder. + private 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 { + self.storageReady = false + throw KingfisherError.cacheError(reason: .cannotCreateDirectory(path: path, error: error)) + } + } + + /// Stores a value to the storage under the specified key and expiration policy. + /// - Parameters: + /// - value: The value to be stored. + /// - key: The key to which the `value` will be stored. If there is already a value under the key, + /// the old value will be overwritten by `value`. + /// - expiration: The expiration policy used by this store action. + /// - writeOptions: Data writing options used the new files. + /// - Throws: An error during converting the value to a data format or during writing it to disk. + public func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil, + writeOptions: Data.WritingOptions = []) throws + { + guard storageReady else { + throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) + } + + 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) + do { + try data.write(to: fileURL, options: writeOptions) + } catch { + throw KingfisherError.cacheError( + reason: .cannotCreateCacheFile(fileURL: fileURL, key: key, data: data, error: error) + ) + } + + let now = Date() + let attributes: [FileAttributeKey : Any] = [ + // The last access date. + .creationDate: now.fileAttributeDate, + // The estimated expiration date. + .modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate + ] + do { + try config.fileManager.setAttributes(attributes, ofItemAtPath: fileURL.path) + } catch { + try? config.fileManager.removeItem(at: fileURL) + throw KingfisherError.cacheError( + reason: .cannotSetCacheFileAttribute( + filePath: fileURL.path, + attributes: attributes, + error: error + ) + ) + } + + maybeCachedCheckingQueue.async { + self.maybeCached?.insert(fileURL.lastPathComponent) + } + } + + /// Gets a value from the storage. + /// - Parameters: + /// - key: The cache key of value. + /// - extendingExpiration: The expiration policy used by this getting action. + /// - Throws: An error during converting the data to a value or during operation of disk files. + /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`. + public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) throws -> T? { + return try value(forKey: key, referenceDate: Date(), actuallyLoad: true, extendingExpiration: extendingExpiration) + } + + func value( + forKey key: String, + referenceDate: Date, + actuallyLoad: Bool, + extendingExpiration: ExpirationExtending) throws -> T? + { + guard storageReady else { + throw KingfisherError.cacheError(reason: .diskStorageIsNotReady(cacheURL: directoryURL)) + } + + let fileManager = config.fileManager + let fileURL = cacheFileURL(forKey: key) + let filePath = fileURL.path + + let fileMaybeCached = maybeCachedCheckingQueue.sync { + return maybeCached?.contains(fileURL.lastPathComponent) ?? true + } + guard fileMaybeCached else { + return nil + } + guard fileManager.fileExists(atPath: filePath) else { + return nil + } + + let meta: FileMeta + do { + let resourceKeys: Set = [.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, extendingExpiration: extendingExpiration) + } + return obj + } catch { + throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error)) + } + } + + /// Whether there is valid cached data under a given key. + /// - Parameter key: The cache key of value. + /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`. + /// + /// - Note: + /// This method does not actually load the data from disk, so it is faster than directly loading the cached value + /// by checking the nullability of `value(forKey:extendingExpiration:)` method. + /// + public func isCached(forKey key: String) -> Bool { + return isCached(forKey: key, referenceDate: Date()) + } + + /// Whether there is valid cached data under a given key and a reference date. + /// - Parameters: + /// - key: The cache key of value. + /// - referenceDate: A reference date to check whether the cache is still valid. + /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`. + /// + /// - Note: + /// If you pass `Date()` to `referenceDate`, this method is identical to `isCached(forKey:)`. Use the + /// `referenceDate` to determine whether the cache is still valid for a future date. + public func isCached(forKey key: String, referenceDate: Date) -> Bool { + do { + let result = try value( + forKey: key, + referenceDate: referenceDate, + actuallyLoad: false, + extendingExpiration: .none + ) + return result != nil + } catch { + return false + } + } + + /// Removes a value from a specified key. + /// - Parameter key: The cache key of value. + /// - Throws: An error during removing the value. + public 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) + } + + /// Removes all values in this storage. + /// - Throws: An error during removing the values. + public 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`. + /// + /// - 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. + /// + /// - 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. + /// + public func cacheFileURL(forKey key: String) -> URL { + let fileName = cacheFileName(forKey: key) + return directoryURL.appendingPathComponent(fileName, isDirectory: false) + } + + func cacheFileName(forKey key: String) -> String { + if config.usesHashedFileName { + let hashedKey = key.kf.md5 + if let ext = config.pathExtension { + return "\(hashedKey).\(ext)" + } else if config.autoExtAfterHashedFileName, + let ext = key.kf.ext { + 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 + } + + /// Removes all expired values from this storage. + /// - Throws: A file manager error during removing the file. + /// - Returns: The URLs for removed files. + public func removeExpiredValues() throws -> [URL] { + return try removeExpiredValues(referenceDate: Date()) + } + + func removeExpiredValues(referenceDate: 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 + } + + /// Removes all size exceeded values from this storage. + /// - Throws: A file manager error during removing the file. + /// - Returns: The URLs for removed files. + /// + /// - Note: This method checks `config.sizeLimit` and remove cached files in an LRU (Least Recently Used) way. + 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 + } + + /// Gets the total file size of the folder in bytes. + public 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 + + /// Default is `false` + /// If set to `true`, image extension will be extracted from original file name and append to + /// the hased file name and used as the cache key on disk. + public var autoExtAfterHashedFileName = false + + /// Closure that takes in initial directory path and generates + /// the final disk cache path. You can use it to fully customize your cache path. + public var cachePathBlock: ((_ directory: URL, _ cacheName: String) -> URL)! = { + (directory, cacheName) in + return directory.appendingPathComponent(cacheName, isDirectory: true) + } + + let name: String + let fileManager: FileManager + let directory: URL? + + /// 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) 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, extendingExpiration: ExpirationExtending) { + guard let lastAccessDate = lastAccessDate, + let lastEstimatedExpiration = estimatedExpirationDate else + { + return + } + + let attributes: [FileAttributeKey : Any] + + switch extendingExpiration { + case .none: + // not extending expiration time here + return + case .cacheTime: + let originalExpiration: StorageExpiration = + .seconds(lastEstimatedExpiration.timeIntervalSince(lastAccessDate)) + attributes = [ + .creationDate: Date().fileAttributeDate, + .modificationDate: originalExpiration.estimatedExpirationSinceNow.fileAttributeDate + ] + case .expirationTime(let expirationTime): + attributes = [ + .creationDate: Date().fileAttributeDate, + .modificationDate: expirationTime.estimatedExpirationSinceNow.fileAttributeDate + ] + } + + try? fileManager.setAttributes(attributes, ofItemAtPath: url.path) + } + } +} + +extension DiskStorage { + struct Creation { + let directoryURL: URL + let cacheName: String + + init(_ config: Config) { + let url: URL + if let directory = config.directory { + url = directory + } else { + url = config.fileManager.urls(for: .cachesDirectory, in: .userDomainMask)[0] + } + + cacheName = "com.onevcat.Kingfisher.ImageCache.\(config.name)" + directoryURL = config.cachePathBlock(url, cacheName) + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift b/Demo/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift new file mode 100644 index 0000000..cdfb7c3 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Cache/FormatIndicatedCacheSerializer.swift @@ -0,0 +1,118 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Junyu Kuang on 5/28/17. +// +// Copyright (c) 2019 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. + +import Foundation +import CoreGraphics + +/// `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, jpegCompressionQuality: nil) + + /// 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. + /// The compression quality is 1.0 when using this serializer. If you need to set a customized compression quality, + /// use `jpeg(compressionQuality:)`. + public static let jpeg = FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: 1.0) + + /// A `FormatIndicatedCacheSerializer` which converts image from and to JPEG format with a settable compression + /// quality. If the image cannot be represented by JPEG format, it will fallback to its real format which is + /// determined by `original` data. + /// - Parameter compressionQuality: The compression quality when converting image to JPEG data. + public static func jpeg(compressionQuality: CGFloat) -> FormatIndicatedCacheSerializer { + return FormatIndicatedCacheSerializer(imageFormat: .JPEG, jpegCompressionQuality: compressionQuality) + } + + /// 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, jpegCompressionQuality: nil) + + /// The indicated image format. + private let imageFormat: ImageFormat + + /// The compression quality used for loss image format (like JPEG). + private let jpegCompressionQuality: CGFloat? + + /// Creates data which represents the given `image` under a format. + public func data(with image: KFCrossPlatformImage, original: Data?) -> Data? { + + func imageData(withFormat imageFormat: ImageFormat) -> Data? { + return autoreleasepool { () -> Data? in + switch imageFormat { + case .PNG: return image.kf.pngRepresentation() + case .JPEG: return image.kf.jpegRepresentation(compressionQuality: jpegCompressionQuality ?? 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) -> KFCrossPlatformImage? { + return KingfisherWrapper.image(data: data, options: options.imageCreatingOptions) + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Cache/ImageCache.swift b/Demo/Pods/Kingfisher/Sources/Cache/ImageCache.swift new file mode 100644 index 0000000..508c79a --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Cache/ImageCache.swift @@ -0,0 +1,882 @@ +// +// ImageCache.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 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. + +#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 KFCrossPlatformImage: 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(KFCrossPlatformImage) + + /// The image can be retrieved memory cache. + case memory(KFCrossPlatformImage) + + /// 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: KFCrossPlatformImage? { + 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 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 + + /// 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 + + 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, + diskStorage: DiskStorage.Backend) + { + 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) + notifications = [ + (UIApplication.didReceiveMemoryWarningNotification, #selector(clearMemoryCache)), + (UIApplication.willTerminateNotification, #selector(cleanExpiredDiskCache)), + (UIApplication.didEnterBackgroundNotification, #selector(backgroundCleanExpiredDiskCache)) + ] + #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) { + self.init(noThrowName: 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 memoryStorage = ImageCache.createMemoryStorage() + + let config = ImageCache.createConfig( + name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure + ) + let diskStorage = try DiskStorage.Backend(config: config) + self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) + } + + convenience init( + noThrowName name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? + ) + { + if name.isEmpty { + fatalError("[Kingfisher] You should specify a name for the cache. A cache with empty name is not permitted.") + } + + let memoryStorage = ImageCache.createMemoryStorage() + + let config = ImageCache.createConfig( + name: name, cacheDirectoryURL: cacheDirectoryURL, diskCachePathClosure: diskCachePathClosure + ) + let diskStorage = DiskStorage.Backend(noThrowConfig: config, creatingDirectory: true) + self.init(memoryStorage: memoryStorage, diskStorage: diskStorage) + } + + private static func createMemoryStorage() -> MemoryStorage.Backend { + let totalMemory = ProcessInfo.processInfo.physicalMemory + let costLimit = totalMemory / 4 + let memoryStorage = MemoryStorage.Backend(config: + .init(totalCostLimit: (costLimit > Int.max) ? Int.max : Int(costLimit))) + return memoryStorage + } + + private static func createConfig( + name: String, + cacheDirectoryURL: URL?, + diskCachePathClosure: DiskCachePathClosure? = nil + ) -> DiskStorage.Config + { + var diskConfig = DiskStorage.Config( + name: name, + sizeLimit: 0, + directory: cacheDirectoryURL + ) + if let closure = diskCachePathClosure { + diskConfig.cachePathBlock = closure + } + return diskConfig + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + // MARK: Storing Images + + open func store(_ image: KFCrossPlatformImage, + 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, + writeOptions: options.diskStoreWriteOptions, + 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: KFCrossPlatformImage, + 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) -> KFCrossPlatformImage? { + 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, + writeOptions: Data.WritingOptions = [], + completionHandler: ((CacheStoreResult) -> Void)? = nil) + { + let computedKey = key.computedKey(with: identifier) + let result: CacheStoreResult + do { + try self.diskStorage.store(value: data, forKey: computedKey, expiration: expiration, writeOptions: writeOptions) + 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 { + 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() } + } + } + } + + // 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 `KingfisherParsedOptionsInfo` options setting used for retrieving the image. + /// - callbackQueue: The callback queue on which `completionHandler` is invoked. Default is `.mainCurrentOrAsync`. + /// - 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: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .mainCurrentOrAsync, + completionHandler: ((Result) -> 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) { + 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 + switch result { + case .success(let image): + + guard let image = image else { + // No image found in disk storage. + callbackQueue.execute { completionHandler(.success(.none)) } + return + } + + // 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( + image, + forKey: key, + options: cacheOptions, + toDisk: false) + { + _ in + callbackQueue.execute { completionHandler(.success(.disk(image))) } + } + case .failure(let error): + callbackQueue.execute { completionHandler(.failure(error)) } + } + } + } + } + + /// 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 `.mainCurrentOrAsync`. + /// - 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. + /// + /// Note: This method is marked as `open` for only compatible purpose. Do not overide this method. Instead, override + /// the version receives `KingfisherParsedOptionsInfo` instead. + open func retrieveImage(forKey key: String, + options: KingfisherOptionsInfo? = nil, + callbackQueue: CallbackQueue = .mainCurrentOrAsync, + completionHandler: ((Result) -> Void)?) + { + retrieveImage( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + /// Gets an image for a given key from the memory storage. + /// + /// - Parameters: + /// - key: The key used for caching the image. + /// - options: The `KingfisherParsedOptionsInfo` 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: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? + { + 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. + /// + /// Note: This method is marked as `open` for only compatible purpose. Do not overide this method. Instead, override + /// the version receives `KingfisherParsedOptionsInfo` instead. + open func retrieveImageInMemoryCache( + forKey key: String, + options: KingfisherOptionsInfo? = nil) -> KFCrossPlatformImage? + { + return retrieveImageInMemoryCache(forKey: key, options: KingfisherParsedOptionsInfo(options)) + } + + func retrieveImageInDiskCache( + forKey key: String, + options: KingfisherParsedOptionsInfo, + callbackQueue: CallbackQueue = .untouch, + completionHandler: @escaping (Result) -> Void) + { + let computedKey = key.computedKey(with: options.processor.identifier) + let loadingQueue: CallbackQueue = options.loadDiskFileSynchronously ? .untouch : .dispatch(ioQueue) + loadingQueue.execute { + do { + var image: KFCrossPlatformImage? = nil + if let data = try self.diskStorage.value(forKey: computedKey, extendingExpiration: options.diskCacheAccessExtendingExpiration) { + image = options.cacheSerializer.image(with: data, options: options) + } + if options.backgroundDecode { + image = image?.kf.decoded(scale: options.scaleFactor) + } + callbackQueue.execute { completionHandler(.success(image)) } + } catch let error as KingfisherError { + callbackQueue.execute { completionHandler(.failure(error)) } + } catch { + 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) -> Void) + { + retrieveImageInDiskCache( + forKey: key, + options: KingfisherParsedOptionsInfo(options), + callbackQueue: callbackQueue, + completionHandler: completionHandler) + } + + // MARK: Cleaning + /// Clears the memory & 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. + public func clearCache(completion handler: (() -> Void)? = nil) { + clearMemoryCache() + clearDiskCache(completion: handler) + } + + /// Clears the memory storage of this cache. + @objc public func clearMemoryCache() { + 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: (() -> Void)? = nil) { + ioQueue.async { + do { + try self.diskStorage.removeAll() + } catch _ { } + if let handler = handler { + DispatchQueue.main.async { handler() } + } + } + } + + /// Clears the expired images from memory & disk storage. This is an async operation. + open func cleanExpiredCache(completion handler: (() -> Void)? = nil) { + cleanExpiredMemoryCache() + cleanExpiredDiskCache(completion: handler) + } + + /// Clears the expired images from disk storage. + 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.shared else { return } + + func endBackgroundTask(_ task: inout UIBackgroundTaskIdentifier) { + sharedApplication.endBackgroundTask(task) + task = UIBackgroundTaskIdentifier.invalid + } + + 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) -> Void)) { + ioQueue.async { + do { + let size = try self.diskStorage.totalSize() + DispatchQueue.main.async { handler(.success(size)) } + } catch let error as KingfisherError { + DispatchQueue.main.async { handler(.failure(error)) } + } catch { + assertionFailure("The internal thrown error should be a `KingfisherError`.") + } + } + } + + #if swift(>=5.5) + #if canImport(_Concurrency) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + open var diskStorageSize: UInt { + get async throws { + try await withCheckedThrowingContinuation { continuation in + calculateDiskStorageSize { result in + continuation.resume(with: result) + } + } + } + } + #endif + #endif + + /// 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 `` 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 + } +} + +#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)") + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift b/Demo/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift new file mode 100644 index 0000000..10d92b1 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Cache/MemoryStorage.swift @@ -0,0 +1,283 @@ +// +// MemoryStorage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 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. + +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 { + let storage = NSCache>() + + // Keys trackes the objects once inside the storage. For object removing triggered by user, the corresponding + // key would be also removed. However, for the object removing triggered by cache rule/policy of system, the + // key will be remained there until next `removeExpired` happens. + // + // Breaking the strict tracking could save additional locking behaviors. + // See https://github.com/onevcat/Kingfisher/issues/1233 + var keys = Set() + + private var cleanTimer: Timer? = nil + private let lock = NSLock() + + /// 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 + + cleanTimer = .scheduledTimer(withTimeInterval: config.cleanInterval, repeats: true) { [weak self] _ in + guard let self = self else { return } + self.removeExpired() + } + } + + /// Removes the expired values from the storage. + public func removeExpired() { + lock.lock() + defer { lock.unlock() } + for key in keys { + let nsKey = key as NSString + guard let object = storage.object(forKey: nsKey) else { + // This could happen if the object is moved by cache `totalCostLimit` or `countLimit` rule. + // We didn't remove the key yet until now, since we do not want to introduce additional lock. + // See https://github.com/onevcat/Kingfisher/issues/1233 + keys.remove(key) + continue + } + if object.isExpired { + storage.removeObject(forKey: nsKey) + keys.remove(key) + } + } + } + + /// Stores a value to the storage under the specified key and expiration policy. + /// - Parameters: + /// - value: The value to be stored. + /// - key: The key to which the `value` will be stored. + /// - expiration: The expiration policy used by this store action. + /// - Throws: No error will + public func store( + value: T, + forKey key: String, + expiration: StorageExpiration? = nil) + { + 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 + if config.keepWhenEnteringBackground { + object = BackgroundKeepingStorageObject(value, expiration: expiration) + } else { + object = StorageObject(value, expiration: expiration) + } + storage.setObject(object, forKey: key as NSString, cost: value.cacheCost) + keys.insert(key) + } + + /// Gets a value from the storage. + /// + /// - Parameters: + /// - key: The cache key of value. + /// - extendingExpiration: The expiration policy used by this getting action. + /// - Returns: The value under `key` if it is valid and found in the storage. Otherwise, `nil`. + public func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? { + guard let object = storage.object(forKey: key as NSString) else { + return nil + } + if object.isExpired { + return nil + } + object.extendExpiration(extendingExpiration) + return object.value + } + + /// Whether there is valid cached data under a given key. + /// - Parameter key: The cache key of value. + /// - Returns: If there is valid data under the key, `true`. Otherwise, `false`. + public func isCached(forKey key: String) -> Bool { + guard let _ = value(forKey: key, extendingExpiration: .none) else { + return false + } + return true + } + + /// Removes a value from a specified key. + /// - Parameter key: The cache key of value. + public func remove(forKey key: String) { + lock.lock() + defer { lock.unlock() } + storage.removeObject(forKey: key as NSString) + keys.remove(key) + } + + /// Removes all values in this storage. + public func removeAll() { + lock.lock() + defer { lock.unlock() } + storage.removeAllObjects() + keys.removeAll() + } + } +} + +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 var cleanInterval: TimeInterval + + /// Whether the newly added items to memory cache should be purged when the app goes to background. + /// + /// By default, the cached items in memory will be purged as soon as the app goes to background to ensure + /// least memory footprint. Enabling this would prevent this behavior and keep the items alive in cache even + /// when your app is not in foreground anymore. + /// + /// Default is `false`. After setting `true`, only the newly added cache objects are affected. Existing + /// objects which are already in the cache while this value was `false` will be still be purged when entering + /// background. + public var keepWhenEnteringBackground: Bool = false + + /// 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 BackgroundKeepingStorageObject: StorageObject, NSDiscardableContent { + var accessing = true + func beginContentAccess() -> Bool { + if value != nil { + accessing = true + } else { + accessing = false + } + return accessing + } + + func endContentAccess() { + accessing = false + } + + func discardContentIfPossible() { + value = nil + } + + func isContentDiscarded() -> Bool { + return value == nil + } + } + + class StorageObject { + var value: T? + let expiration: StorageExpiration + + private(set) var estimatedExpiration: Date + + init(_ value: T, expiration: StorageExpiration) { + self.value = value + 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 isExpired: Bool { + return estimatedExpiration.isPast + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Cache/Storage.swift b/Demo/Pods/Kingfisher/Sources/Cache/Storage.swift new file mode 100644 index 0000000..ad9d326 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Cache/Storage.swift @@ -0,0 +1,110 @@ +// +// Storage.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/15. +// +// Copyright (c) 2019 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. + +import Foundation + +/// Constants for some time intervals +struct TimeConstants { + static let secondsInOneDay = 86_400 +} + +/// 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): + let duration: TimeInterval = TimeInterval(TimeConstants.secondsInOneDay) * TimeInterval(days) + return date.addingTimeInterval(duration) + 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) * TimeInterval(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/Demo/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift b/Demo/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift new file mode 100644 index 0000000..085bead --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Extensions/CPListItem+Kingfisher.swift @@ -0,0 +1,245 @@ + +// +// CPListItem+Kingfisher.swift +// Kingfisher +// +// Created by Wayne Hartman on 2021-08-29. +// +// Copyright (c) 2019 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. + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay + +@available(iOS 14.0, *) +extension KingfisherWrapper where Base: CPListItem { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - 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 setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? [])) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the image. + /// - 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?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(placeholder) + #else + if let placeholder = placeholder { + self.base.setImage(placeholder) + } + #endif + + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(placeholder) + #else // Let older SDK users deal with the older behavior. + if let placeholder = placeholder { + self.base.setImage(placeholder) + } + #endif + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + progressiveImageSetter: { self.base.setImage($0) }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + 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): + self.base.setImage(value.image) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + /** + * In iOS SDK 14.0-14.4 the image param was non-`nil`. The SDK changed in 14.5 + * to allow `nil`. The compiler version 5.4 was introduced in this same SDK, + * which allows >=14.5 SDK to set a `nil` image. This compile check allows + * newer SDK users to set the image to `nil`, while still allowing older SDK + * users to compile the framework. + */ + #if compiler(>=5.4) + self.base.setImage(image) + #else // Let older SDK users deal with the older behavior. + if let unwrapped = image { + self.base.setImage(unwrapped) + } + #endif + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: CPListItem { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift b/Demo/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift new file mode 100644 index 0000000..9378f19 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Extensions/ImageView+Kingfisher.swift @@ -0,0 +1,537 @@ +// +// ImageView+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 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. + +#if !os(watchOS) + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension KingfisherWrapper where Base: KFCrossPlatformImageView { + + // 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) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage(with: source, placeholder: placeholder, parsedOptions: options, progressBlock: progressBlock, completionHandler: completionHandler) + } + + /// 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. + /// - 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. + /// The `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with source: Source?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: source, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + /// 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) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + /// 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. + /// - 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. + /// The `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with resource: Resource?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource, + placeholder: placeholder, + options: options, + progressBlock: nil, + 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) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: provider.map { .provider($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. + /// - 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. + /// The `completionHandler` will be also executed in the main thread. + /// + @discardableResult + public func setImage( + with provider: ImageDataProvider?, + placeholder: Placeholder? = nil, + options: KingfisherOptionsInfo? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: provider, + placeholder: placeholder, + options: options, + progressBlock: nil, + completionHandler: completionHandler + ) + } + + + func setImage( + with source: Source?, + placeholder: Placeholder? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> 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 = parsedOptions + + let isEmptyImage = base.image == nil && self.placeholder == nil + if !options.keepCurrentImageWhileLoading || isEmptyImage { + // 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)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + progressiveImageSetter: { self.base.image = $0 }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, + 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 { + mutatingSelf.placeholder = nil + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + mutatingSelf.imageTask = task + return task + } + + // 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) + case .fade: // Fade is only a placeholder for SwiftUI on macOS. + return false + #else + default: + if options.forceTransition { return true } + if cacheType == .none { return true } + return false + #endif + } + } + + private func makeTransition(image: KFCrossPlatformImage, 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: KFCrossPlatformImageView { + + // MARK: Properties + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = 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? = 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 + + switch newIndicator.sizeStrategy(in: base) { + case .intrinsicSize: + break + case .full: + view.heightAnchor.constraint(equalTo: base.heightAnchor, constant: 0).isActive = true + view.widthAnchor.constraint(equalTo: base.widthAnchor, constant: 0).isActive = true + case .size(let size): + view.heightAnchor.constraint(equalToConstant: size.height).isActive = true + view.widthAnchor.constraint(equalToConstant: size.width).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) + } + } +} + + +extension KFCrossPlatformImageView { + @objc func shouldPreloadAllAnimation() -> Bool { return true } +} + +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift b/Demo/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift new file mode 100644 index 0000000..d33d557 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Extensions/NSButton+Kingfisher.swift @@ -0,0 +1,362 @@ +// +// NSButton+Kingfisher.swift +// Kingfisher +// +// Created by Jie Zhang on 14/04/2016. +// +// Copyright (c) 2019 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. + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) + +import AppKit + +extension KingfisherWrapper where Base: NSButton { + + // MARK: Setting Image + + /// Sets an image to the button with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about how to get the image. + /// - 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 setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the button 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: + /// 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: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + progressiveImageSetter: { self.base.image = $0 }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + 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): + self.base.image = value.image + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // 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 Alternate Image + + @discardableResult + public func setAlternateImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setAlternateImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an alternate image to the button 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: + /// 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 setAlternateImage( + with resource: Resource?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setAlternateImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setAlternateImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.alternateImage = placeholder + mutatingSelf.alternateTaskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.alternateImage = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.alternateTaskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + if let provider = ImageProgressiveProvider(options, refresh: { image in + self.base.alternateImage = image + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + + options.onDataReceived?.forEach { + $0.onShouldApply = { issuedIdentifier == self.alternateTaskIdentifier } + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.alternateImageTask = $0 }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + guard issuedIdentifier == self.alternateTaskIdentifier 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.alternateImageTask = nil + mutatingSelf.alternateTaskIdentifier = nil + + switch result { + case .success(let value): + self.base.alternateImage = value.image + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.alternateImage = image + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.alternateImageTask = task + return task + } + + // MARK: Cancelling Alternate Image Downloading Task + + /// Cancels the alternate image download task of the button if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelAlternateImageDownloadTask() { + alternateImageTask?.cancel() + } +} + + +// MARK: - Associated Object +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +private var alternateTaskIdentifierKey: Void? +private var alternateImageTaskKey: Void? + +extension KingfisherWrapper where Base: NSButton { + + // MARK: Properties + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } + + public private(set) var alternateTaskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &alternateTaskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &alternateTaskIdentifierKey, box) + } + } + + private var alternateImageTask: DownloadTask? { + get { return getAssociatedObject(base, &alternateImageTaskKey) } + set { setRetainedAssociatedObject(base, &alternateImageTaskKey, newValue)} + } +} +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift b/Demo/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift new file mode 100644 index 0000000..23f627e --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Extensions/NSTextAttachment+Kingfisher.swift @@ -0,0 +1,271 @@ +// +// NSTextAttachment+Kingfisher.swift +// Kingfisher +// +// Created by Benjamin Briggs on 22/07/2019. +// +// Copyright (c) 2019 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. + +#if !os(watchOS) + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +extension KingfisherWrapper where Base: NSTextAttachment { + + // MARK: Setting Image + + /// Sets an image to the text attachment with a source. + /// + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - 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. + /// + /// The retrieved image will be set to `NSTextAttachment.image` property. Because it is not an image view based + /// rendering, options related to view, such as `.transition`, are not supported. + /// + /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task done. It gives the view a + /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an + /// attributed with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. + /// + /// Here is a typical use case: + /// + /// ```swift + /// let attributedText = NSMutableAttributedString(string: "Hello World") + /// let textAttachment = NSTextAttachment() + /// + /// textAttachment.kf.setImage( + /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, + /// attributedView: label, + /// options: [ + /// .processor( + /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) + /// |> RoundCornerImageProcessor(cornerRadius: 15)) + /// ] + /// ) + /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) + /// label.attributedText = attributedText + /// ``` + /// + @discardableResult + public func setImage( + with source: Source?, + attributedView: @autoclosure @escaping () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + attributedView: attributedView, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the text attachment with a source. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the resource. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - 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. + /// + /// The retrieved image will be set to `NSTextAttachment.image` property. Because it is not an image view based + /// rendering, options related to view, such as `.transition`, are not supported. + /// + /// Kingfisher will call `setNeedsDisplay` on the `attributedView` when the image task done. It gives the view a + /// chance to render the attributed string again for displaying the downloaded image. For example, if you set an + /// attributed with this `NSTextAttachment` to a `UILabel` object, pass it as the `attributedView` parameter. + /// + /// Here is a typical use case: + /// + /// ```swift + /// let attributedText = NSMutableAttributedString(string: "Hello World") + /// let textAttachment = NSTextAttachment() + /// + /// textAttachment.kf.setImage( + /// with: URL(string: "https://onevcat.com/assets/images/avatar.jpg")!, + /// attributedView: label, + /// options: [ + /// .processor( + /// ResizingImageProcessor(referenceSize: .init(width: 30, height: 30)) + /// |> RoundCornerImageProcessor(cornerRadius: 15)) + /// ] + /// ) + /// attributedText.replaceCharacters(in: NSRange(), with: NSAttributedString(attachment: textAttachment)) + /// label.attributedText = attributedText + /// ``` + /// + @discardableResult + public func setImage( + with resource: Resource?, + attributedView: @autoclosure @escaping () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: resource.map { .network($0) }, + attributedView: attributedView, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + func setImage( + with source: Source?, + attributedView: @escaping () -> KFCrossPlatformView, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + progressiveImageSetter: { self.base.image = $0 }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + 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): + self.base.image = value.image + let view = attributedView() + #if canImport(UIKit) + view.setNeedsDisplay() + #else + view.setNeedsDisplay(view.bounds) + #endif + case .failure: + if let image = options.onFailureImage { + self.base.image = image + } + } + completionHandler?(result) + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the text attachment if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: NSTextAttachment { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift b/Demo/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift new file mode 100644 index 0000000..c0993cd --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Extensions/TVMonogramView+Kingfisher.swift @@ -0,0 +1,209 @@ +// +// TVMonogramView+Kingfisher.swift +// Kingfisher +// +// Created by Marvin Nazari on 2020-12-07. +// +// Copyright (c) 2020 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. + +import Foundation + +#if canImport(TVUIKit) + +import TVUIKit + +@available(tvOS 12.0, *) +extension KingfisherWrapper where Base: TVMonogramView { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - 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 setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.image = placeholder + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.image = placeholder + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + progressiveImageSetter: { self.base.image = $0 }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + 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): + self.base.image = value.image + 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 image. + /// - 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?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +@available(tvOS 12.0, *) +extension KingfisherWrapper where Base: TVMonogramView { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} + +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift b/Demo/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift new file mode 100644 index 0000000..d1e2dcb --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Extensions/UIButton+Kingfisher.swift @@ -0,0 +1,400 @@ +// +// UIButton+Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/13. +// +// Copyright (c) 2019 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. + +#if !os(watchOS) + +#if canImport(UIKit) +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) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + for: state, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// 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) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + @discardableResult + public func setImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> 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 = parsedOptions + 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)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + progressiveImageSetter: { self.base.setImage($0, for: state) }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier(for: state) }, + 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 + } + + // 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) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setBackgroundImage( + with: source, + for: state, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// 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) -> Void)? = nil) -> DownloadTask? + { + return setBackgroundImage( + with: resource?.convertToSource(), + for: state, + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setBackgroundImage( + with source: Source?, + for state: UIControl.State, + placeholder: UIImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> 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 = parsedOptions + 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)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.backgroundImageTask = $0 }, + progressiveImageSetter: { self.base.setBackgroundImage($0, for: state) }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.backgroundTaskIdentifier(for: state) }, + 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 + } + + // 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) } + } +} +#endif + +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift b/Demo/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift new file mode 100644 index 0000000..a24feeb --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Extensions/WKInterfaceImage+Kingfisher.swift @@ -0,0 +1,204 @@ +// +// WKInterfaceImage+Kingfisher.swift +// Kingfisher +// +// Created by Rodrigo Borges Soares on 04/05/18. +// +// Copyright (c) 2019 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. + +#if canImport(WatchKit) + +import WatchKit + +extension KingfisherWrapper where Base: WKInterfaceImage { + + // MARK: Setting Image + + /// Sets an image to the image view with a source. + /// + /// - Parameters: + /// - source: The `Source` object contains information about the image. + /// - 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 setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + let options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty)) + return setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: completionHandler + ) + } + + /// Sets an image to the image view with a requested resource. + /// + /// - Parameters: + /// - resource: The `Resource` object contains information about the image. + /// - 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?, + placeholder: KFCrossPlatformImage? = nil, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + return setImage( + with: resource?.convertToSource(), + placeholder: placeholder, + options: options, + progressBlock: progressBlock, + completionHandler: completionHandler) + } + + func setImage( + with source: Source?, + placeholder: KFCrossPlatformImage? = nil, + parsedOptions: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var mutatingSelf = self + guard let source = source else { + base.setImage(placeholder) + mutatingSelf.taskIdentifier = nil + completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource))) + return nil + } + + var options = parsedOptions + if !options.keepCurrentImageWhileLoading { + base.setImage(placeholder) + } + + let issuedIdentifier = Source.Identifier.next() + mutatingSelf.taskIdentifier = issuedIdentifier + + if let block = progressBlock { + options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + + let task = KingfisherManager.shared.retrieveImage( + with: source, + options: options, + downloadTaskUpdated: { mutatingSelf.imageTask = $0 }, + progressiveImageSetter: { self.base.setImage($0) }, + referenceTaskIdentifierChecker: { issuedIdentifier == self.taskIdentifier }, + completionHandler: { result in + CallbackQueue.mainCurrentOrAsync.execute { + 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): + self.base.setImage(value.image) + completionHandler?(result) + + case .failure: + if let image = options.onFailureImage { + self.base.setImage(image) + } + completionHandler?(result) + } + } + } + ) + + mutatingSelf.imageTask = task + return task + } + + // MARK: Cancelling Image + + /// Cancel the image download task bounded to the image view if it is running. + /// Nothing will happen if the downloading has already finished. + public func cancelDownloadTask() { + imageTask?.cancel() + } +} + +private var taskIdentifierKey: Void? +private var imageTaskKey: Void? + +// MARK: Properties +extension KingfisherWrapper where Base: WKInterfaceImage { + + public private(set) var taskIdentifier: Source.Identifier.Value? { + get { + let box: Box? = getAssociatedObject(base, &taskIdentifierKey) + return box?.value + } + set { + let box = newValue.map { Box($0) } + setRetainedAssociatedObject(base, &taskIdentifierKey, box) + } + } + + private var imageTask: DownloadTask? { + get { return getAssociatedObject(base, &imageTaskKey) } + set { setRetainedAssociatedObject(base, &imageTaskKey, newValue)} + } +} +#endif diff --git a/Demo/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift b/Demo/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift new file mode 100644 index 0000000..d13023b --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/ImageSource/AVAssetImageDataProvider.swift @@ -0,0 +1,138 @@ +// +// AVAssetImageDataProvider.swift +// Kingfisher +// +// Created by onevcat on 2020/08/09. +// +// Copyright (c) 2020 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. + +#if !os(watchOS) + +import Foundation +import AVKit + +#if canImport(MobileCoreServices) +import MobileCoreServices +#else +import CoreServices +#endif + +/// A data provider to provide thumbnail data from a given AVKit asset. +public struct AVAssetImageDataProvider: ImageDataProvider { + + /// The possible error might be caused by the `AVAssetImageDataProvider`. + /// - userCancelled: The data provider process is cancelled. + /// - invalidImage: The retrieved image is invalid. + public enum AVAssetImageDataProviderError: Error { + case userCancelled + case invalidImage(_ image: CGImage?) + } + + /// The asset image generator bound to `self`. + public let assetImageGenerator: AVAssetImageGenerator + + /// The time at which the image should be generate in the asset. + public let time: CMTime + + private var internalKey: String { + return (assetImageGenerator.asset as? AVURLAsset)?.url.absoluteString ?? UUID().uuidString + } + + /// The cache key used by `self`. + public var cacheKey: String { + return "\(internalKey)_\(time.seconds)" + } + + /// Creates an asset image data provider. + /// - Parameters: + /// - assetImageGenerator: The asset image generator controls data providing behaviors. + /// - time: At which time in the asset the image should be generated. + public init(assetImageGenerator: AVAssetImageGenerator, time: CMTime) { + self.assetImageGenerator = assetImageGenerator + self.time = time + } + + /// Creates an asset image data provider. + /// - Parameters: + /// - assetURL: The URL of asset for providing image data. + /// - time: At which time in the asset the image should be generated. + /// + /// This method uses `assetURL` to create an `AVAssetImageGenerator` object and calls + /// the `init(assetImageGenerator:time:)` initializer. + /// + public init(assetURL: URL, time: CMTime) { + let asset = AVAsset(url: assetURL) + let generator = AVAssetImageGenerator(asset: asset) + generator.appliesPreferredTrackTransform = true + self.init(assetImageGenerator: generator, time: time) + } + + /// Creates an asset image data provider. + /// + /// - Parameters: + /// - assetURL: The URL of asset for providing image data. + /// - seconds: At which time in seconds in the asset the image should be generated. + /// + /// This method uses `assetURL` to create an `AVAssetImageGenerator` object, uses `seconds` to create a `CMTime`, + /// and calls the `init(assetImageGenerator:time:)` initializer. + /// + public init(assetURL: URL, seconds: TimeInterval) { + let time = CMTime(seconds: seconds, preferredTimescale: 600) + self.init(assetURL: assetURL, time: time) + } + + public func data(handler: @escaping (Result) -> Void) { + assetImageGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: time)]) { + (requestedTime, image, imageTime, result, error) in + if let error = error { + handler(.failure(error)) + return + } + + if result == .cancelled { + handler(.failure(AVAssetImageDataProviderError.userCancelled)) + return + } + + guard let cgImage = image, let data = cgImage.jpegData else { + handler(.failure(AVAssetImageDataProviderError.invalidImage(image))) + return + } + + handler(.success(data)) + } + } +} + +extension CGImage { + var jpegData: Data? { + guard let mutableData = CFDataCreateMutable(nil, 0), + let destination = CGImageDestinationCreateWithData(mutableData, kUTTypeJPEG, 1, nil) + else { + return nil + } + CGImageDestinationAddImage(destination, self, nil) + guard CGImageDestinationFinalize(destination) else { return nil } + return mutableData as Data + } +} + +#endif diff --git a/Demo/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift b/Demo/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift new file mode 100644 index 0000000..dd34150 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/ImageSource/ImageDataProvider.swift @@ -0,0 +1,190 @@ +// +// ImageDataProvider.swift +// Kingfisher +// +// Created by onevcat on 2018/11/13. +// +// Copyright (c) 2019 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. + +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) -> Void) + + /// The content URL represents this provider, if exists. + var contentURL: URL? { get } +} + +public extension ImageDataProvider { + var contentURL: URL? { return nil } + func convertToSource() -> Source { + .provider(self) + } +} + +/// 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 + private let loadingQueue: ExecutionQueue + + // 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. + /// - loadingQueue: The queue where the file loading should happen. By default, the dispatch queue of + /// `.global(qos: .userInitiated)` will be used. + public init( + fileURL: URL, + cacheKey: String? = nil, + loadingQueue: ExecutionQueue = .dispatch(DispatchQueue.global(qos: .userInitiated)) + ) { + self.fileURL = fileURL + self.cacheKey = cacheKey ?? fileURL.localFileCacheKey + self.loadingQueue = loadingQueue + } + + // MARK: Protocol Conforming + + /// The key used in cache. + public var cacheKey: String + + public func data(handler:@escaping (Result) -> Void) { + loadingQueue.execute { + handler(Result(catching: { try Data(contentsOf: fileURL) })) + } + } + + #if swift(>=5.5) + #if canImport(_Concurrency) + @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *) + public var data: Data { + get async throws { + try await withCheckedThrowingContinuation { continuation in + loadingQueue.execute { + do { + let data = try Data(contentsOf: fileURL) + continuation.resume(returning: data) + } catch { + continuation.resume(throwing: error) + } + } + } + } + } + #endif + #endif + + /// The URL of the local file on the disk. + public var contentURL: URL? { + return 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) -> 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) -> Void) { + handler(.success(data)) + } +} diff --git a/Demo/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift b/Demo/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift new file mode 100644 index 0000000..1496f3d --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/ImageSource/Resource.swift @@ -0,0 +1,115 @@ +// +// Resource.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 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. + +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 } +} + +extension Resource { + + /// Converts `self` to a valid `Source` based on its `downloadURL` scheme. A `.provider` with + /// `LocalFileImageDataProvider` associated will be returned if the URL points to a local file. Otherwise, + /// `.network` is returned. + public func convertToSource(overrideCacheKey: String? = nil) -> Source { + let key = overrideCacheKey ?? cacheKey + return downloadURL.isFileURL ? + .provider(LocalFileImageDataProvider(fileURL: downloadURL, cacheKey: key)) : + .network(ImageResource(downloadURL: downloadURL, cacheKey: key)) + } +} + +/// 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.cacheKey + } + + // 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 isFileURL ? localFileCacheKey : absoluteString } + public var downloadURL: URL { return self } +} + +extension URL { + static let localFileCacheKeyPrefix = "kingfisher.local.cacheKey" + + // The special version of cache key for a local file on disk. Every time the app is reinstalled on the disk, + // the system assigns a new container folder to hold the .app (and the extensions, .appex) folder. So the URL for + // the same image in bundle might be different. + // + // This getter only uses the fixed part in the URL (until the bundle name folder) to provide a stable cache key + // for the image under the same path inside the bundle. + // + // See #1825 (https://github.com/onevcat/Kingfisher/issues/1825) + var localFileCacheKey: String { + var validComponents: [String] = [] + for part in pathComponents.reversed() { + validComponents.append(part) + if part.hasSuffix(".app") || part.hasSuffix(".appex") { + break + } + } + let fixedPath = "\(Self.localFileCacheKeyPrefix)/\(validComponents.reversed().joined(separator: "/"))" + if let q = query { + return "\(fixedPath)?\(q)" + } else { + return fixedPath + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/General/ImageSource/Source.swift b/Demo/Pods/Kingfisher/Sources/General/ImageSource/Source.swift new file mode 100644 index 0000000..13622e4 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/ImageSource/Source.swift @@ -0,0 +1,116 @@ +// +// Source.swift +// Kingfisher +// +// Created by onevcat on 2018/11/17. +// +// Copyright (c) 2019 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. + +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 + case .provider(let provider): return provider.contentURL + } + } +} + +extension Source: Hashable { + public static func == (lhs: Source, rhs: Source) -> Bool { + switch (lhs, rhs) { + case (.network(let r1), .network(let r2)): + return r1.cacheKey == r2.cacheKey && r1.downloadURL == r2.downloadURL + case (.provider(let p1), .provider(let p2)): + return p1.cacheKey == p2.cacheKey && p1.contentURL == p2.contentURL + case (.provider(_), .network(_)): + return false + case (.network(_), .provider(_)): + return false + } + } + + public func hash(into hasher: inout Hasher) { + switch self { + case .network(let r): + hasher.combine(r.cacheKey) + hasher.combine(r.downloadURL) + case .provider(let p): + hasher.combine(p.cacheKey) + hasher.combine(p.contentURL) + } + } +} + +extension Source { + var asResource: Resource? { + guard case .network(let resource) = self else { + return nil + } + return resource + } +} diff --git a/Demo/Pods/Kingfisher/Sources/General/KF.swift b/Demo/Pods/Kingfisher/Sources/General/KF.swift new file mode 100644 index 0000000..8f3f028 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/KF.swift @@ -0,0 +1,442 @@ +// +// KF.swift +// Kingfisher +// +// Created by onevcat on 2020/09/21. +// +// Copyright (c) 2020 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. + +#if canImport(UIKit) +import UIKit +#endif + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay +#endif + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +#if canImport(WatchKit) +import WatchKit +#endif + +#if canImport(TVUIKit) +import TVUIKit +#endif + +/// A helper type to create image setting tasks in a builder pattern. +/// Use methods in this type to create a `KF.Builder` instance and configure image tasks there. +public enum KF { + + /// Creates a builder for a given `Source`. + /// - Parameter source: The `Source` object defines data information from network or a data provider. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func source(_ source: Source?) -> KF.Builder { + Builder(source: source) + } + + /// Creates a builder for a given `Resource`. + /// - Parameter resource: The `Resource` object defines data information like key or URL. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func resource(_ resource: Resource?) -> KF.Builder { + source(resource?.convertToSource()) + } + + /// Creates a builder for a given `URL` and an optional cache key. + /// - Parameters: + /// - url: The URL where the image should be downloaded. + /// - cacheKey: The key used to store the downloaded image in cache. + /// If `nil`, the `absoluteString` of `url` is used as the cache key. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func url(_ url: URL?, cacheKey: String? = nil) -> KF.Builder { + source(url?.convertToSource(overrideCacheKey: cacheKey)) + } + + /// Creates a builder for a given `ImageDataProvider`. + /// - Parameter provider: The `ImageDataProvider` object contains information about the data. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func dataProvider(_ provider: ImageDataProvider?) -> KF.Builder { + source(provider?.convertToSource()) + } + + /// Creates a builder for some given raw data and a cache key. + /// - Parameters: + /// - data: The data object from which the image should be created. + /// - cacheKey: The key used to store the downloaded image in cache. + /// - Returns: A `KF.Builder` for future configuration. After configuring the builder, call `set(to:)` + /// to start the image loading. + public static func data(_ data: Data?, cacheKey: String) -> KF.Builder { + if let data = data { + return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) + } else { + return dataProvider(nil) + } + } +} + + +extension KF { + + /// A builder class to configure an image retrieving task and set it to a holder view or component. + public class Builder { + private let source: Source? + + #if os(watchOS) + private var placeholder: KFCrossPlatformImage? + #else + private var placeholder: Placeholder? + #endif + + public var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions) + + public let onFailureDelegate = Delegate() + public let onSuccessDelegate = Delegate() + public let onProgressDelegate = Delegate<(Int64, Int64), Void>() + + init(source: Source?) { + self.source = source + } + + private var resultHandler: ((Result) -> Void)? { + { + switch $0 { + case .success(let result): + self.onSuccessDelegate(result) + case .failure(let error): + self.onFailureDelegate(error) + } + } + } + + private var progressBlock: DownloadProgressBlock { + { self.onProgressDelegate(($0, $1)) } + } + } +} + +extension KF.Builder { + #if !os(watchOS) + + /// Builds the image task request and sets it to an image view. + /// - Parameter imageView: The image view which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to imageView: KFCrossPlatformImageView) -> DownloadTask? { + imageView.kf.setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to an `NSTextAttachment` object. + /// - Parameters: + /// - attachment: The text attachment object which loads the task and should be set with the image. + /// - attributedView: The owner of the attributed string which this `NSTextAttachment` is added. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to attachment: NSTextAttachment, attributedView: @autoclosure @escaping () -> KFCrossPlatformView) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return attachment.kf.setImage( + with: source, + attributedView: attributedView, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + #if canImport(UIKit) + + /// Builds the image task request and sets it to a button. + /// - Parameters: + /// - button: The button which loads the task and should be set with the image. + /// - state: The button state to which the image should be set. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to button: UIButton, for state: UIControl.State) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setImage( + with: source, + for: state, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to the background image for a button. + /// - Parameters: + /// - button: The button which loads the task and should be set with the image. + /// - state: The button state to which the image should be set. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func setBackground(to button: UIButton, for state: UIControl.State) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setBackgroundImage( + with: source, + for: state, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(UIKit) + + #if canImport(CarPlay) && !targetEnvironment(macCatalyst) + + /// Builds the image task request and sets it to the image for a list item. + /// - Parameters: + /// - listItem: The list item which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @available(iOS 14.0, *) + @discardableResult + public func set(to listItem: CPListItem) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return listItem.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + + } + + #endif + + #if canImport(AppKit) && !targetEnvironment(macCatalyst) + /// Builds the image task request and sets it to a button. + /// - Parameter button: The button which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to button: NSButton) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + + /// Builds the image task request and sets it to the alternative image for a button. + /// - Parameter button: The button which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func setAlternative(to button: NSButton) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return button.kf.setAlternateImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(AppKit) + #endif // end of !os(watchOS) + + #if canImport(WatchKit) + /// Builds the image task request and sets it to a `WKInterfaceImage` object. + /// - Parameter interfaceImage: The watch interface image which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @discardableResult + public func set(to interfaceImage: WKInterfaceImage) -> DownloadTask? { + return interfaceImage.kf.setImage( + with: source, + placeholder: placeholder, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(WatchKit) + + #if canImport(TVUIKit) + /// Builds the image task request and sets it to a TV monogram view. + /// - Parameter monogramView: The monogram view which loads the task and should be set with the image. + /// - Returns: A task represents the image downloading, if initialized. + /// This value is `nil` if the image is being loaded from cache. + @available(tvOS 12.0, *) + @discardableResult + public func set(to monogramView: TVMonogramView) -> DownloadTask? { + let placeholderImage = placeholder as? KFCrossPlatformImage ?? nil + return monogramView.kf.setImage( + with: source, + placeholder: placeholderImage, + parsedOptions: options, + progressBlock: progressBlock, + completionHandler: resultHandler + ) + } + #endif // end of canImport(TVUIKit) +} + +#if !os(watchOS) +extension KF.Builder { + #if os(iOS) || os(tvOS) + + /// Sets a placeholder which is used while retrieving the image. + /// - Parameter placeholder: A placeholder to show while retrieving the image from its source. + /// - Returns: A `KF.Builder` with changes applied. + public func placeholder(_ placeholder: Placeholder?) -> Self { + self.placeholder = placeholder + return self + } + #endif + + /// Sets a placeholder image which is used while retrieving the image. + /// - Parameter placeholder: An image to show while retrieving the image from its source. + /// - Returns: A `KF.Builder` with changes applied. + public func placeholder(_ image: KFCrossPlatformImage?) -> Self { + self.placeholder = image + return self + } +} +#endif + +extension KF.Builder { + + #if os(iOS) || os(tvOS) + /// Sets the transition for the image task. + /// - Parameter transition: The desired transition effect when setting the image to image view. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// Kingfisher will use the `transition` 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, also call `forceRefresh()` on the returned `KF.Builder`. + public func transition(_ transition: ImageTransition) -> Self { + options.transition = transition + return self + } + + /// Sets a fade transition for the image task. + /// - Parameter duration: The duration of the fade transition. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// Kingfisher will use the fade transition 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, also call `forceRefresh()` on the returned `KF.Builder`. + public func fade(duration: TimeInterval) -> Self { + options.transition = .fade(duration) + return self + } + #endif + + /// Sets whether keeping the existing image of image view while setting another image to it. + /// - Parameter enabled: Whether the existing image should be kept. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// 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. + /// + public func keepCurrentImageWhileLoading(_ enabled: Bool = true) -> Self { + options.keepCurrentImageWhileLoading = enabled + return self + } + + /// Sets whether only the first frame from an animated image file should be loaded as a single image. + /// - Parameter enabled: Whether the only the first frame should be loaded. + /// - Returns: A `KF.Builder` with changes applied. + /// + /// 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 an animated image. + /// + /// This option will be ignored if the target image is not animated image data. + /// + public func onlyLoadFirstFrame(_ enabled: Bool = true) -> Self { + options.onlyLoadFirstFrame = enabled + return self + } + + /// Enables progressive image loading with a specified `ImageProgressive` setting to process the + /// progressive JPEG data and display it in a progressive way. + /// - Parameter progressive: The progressive settings which is used while loading. + /// - Returns: A `KF.Builder` with changes applied. + public func progressiveJPEG(_ progressive: ImageProgressive? = .init()) -> Self { + options.progressiveJPEG = progressive + return self + } +} + +// MARK: - Deprecated +extension KF.Builder { + /// Starts the loading process of `self` immediately. + /// + /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading + /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a + /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once + /// could help avoiding the flickering, with some performance trade-off. + /// + /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. + /// It does nothing now and please just remove it. + /// + /// - Returns: The `Self` value with changes applied. + @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") + public func loadImmediately(_ start: Bool = true) -> Self { + return self + } +} + +// MARK: - Redirect Handler +extension KF { + + /// Represents the detail information when a task redirect happens. It is wrapping necessary information for a + /// `ImageDownloadRedirectHandler`. See that protocol for more information. + public struct RedirectPayload { + + /// The related session data task when the redirect happens. It is + /// the current `SessionDataTask` which triggers this redirect. + public let task: SessionDataTask + + /// The response received during redirection. + public let response: HTTPURLResponse + + /// The request for redirection which can be modified. + public let newRequest: URLRequest + + /// A closure for being called with modified request. + public let completionHandler: (URLRequest?) -> Void + } +} diff --git a/Demo/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift b/Demo/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift new file mode 100644 index 0000000..9a07753 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/KFOptionsSetter.swift @@ -0,0 +1,706 @@ +// +// KFOptionsSetter.swift +// Kingfisher +// +// Created by onevcat on 2020/12/22. +// +// Copyright (c) 2020 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. + +import Foundation +import CoreGraphics + +public protocol KFOptionSetter { + var options: KingfisherParsedOptionsInfo { get nonmutating set } + + var onFailureDelegate: Delegate { get } + var onSuccessDelegate: Delegate { get } + var onProgressDelegate: Delegate<(Int64, Int64), Void> { get } + + var delegateObserver: AnyObject { get } +} + +extension KF.Builder: KFOptionSetter { + public var delegateObserver: AnyObject { self } +} + +// MARK: - Life cycles +extension KFOptionSetter { + /// Sets the progress block to current builder. + /// - Parameter block: Called when the image downloading progress gets updated. If the response does not contain an + /// `expectedContentLength`, this block will not be called. If `block` is `nil`, the callback + /// will be reset. + /// - Returns: A `Self` value with changes applied. + public func onProgress(_ block: DownloadProgressBlock?) -> Self { + onProgressDelegate.delegate(on: delegateObserver) { (observer, result) in + block?(result.0, result.1) + } + return self + } + + /// Sets the the done block to current builder. + /// - Parameter block: Called when the image task successfully completes and the the image set is done. If `block` + /// is `nil`, the callback will be reset. + /// - Returns: A `KF.Builder` with changes applied. + public func onSuccess(_ block: ((RetrieveImageResult) -> Void)?) -> Self { + onSuccessDelegate.delegate(on: delegateObserver) { (observer, result) in + block?(result) + } + return self + } + + /// Sets the catch block to current builder. + /// - Parameter block: Called when an error happens during the image task. If `block` + /// is `nil`, the callback will be reset. + /// - Returns: A `KF.Builder` with changes applied. + public func onFailure(_ block: ((KingfisherError) -> Void)?) -> Self { + onFailureDelegate.delegate(on: delegateObserver) { (observer, error) in + block?(error) + } + return self + } +} + +// MARK: - Basic options settings. +extension KFOptionSetter { + /// Sets the target image cache for this task. + /// - Parameter cache: The target cache is about to be used for the task. + /// - Returns: A `Self` value with changes applied. + /// + /// 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. + /// + public func targetCache(_ cache: ImageCache) -> Self { + options.targetCache = cache + return self + } + + /// Sets the target image cache to store the original downloaded image for this task. + /// - Parameter cache: The target cache is about to be used for storing the original downloaded image from the task. + /// - Returns: A `Self` value with changes applied. + /// + /// 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. + /// + public func originalCache(_ cache: ImageCache) -> Self { + options.originalCache = cache + return self + } + + /// Sets the downloader used to perform the image download task. + /// - Parameter downloader: The downloader which is about to be used for downloading. + /// - Returns: A `Self` value with changes applied. + /// + /// Kingfisher will use the set `ImageDownloader` object to download the requested images. + public func downloader(_ downloader: ImageDownloader) -> Self { + options.downloader = downloader + return self + } + + /// Sets the download priority for the image task. + /// - Parameter priority: The download priority of image download task. + /// - Returns: A `Self` value with changes applied. + /// + /// The `priority` value will be set as the priority of the image download task. The value for it should be + /// between 0.0~1.0. You can choose a value between `URLSessionTask.defaultPriority`, `URLSessionTask.lowPriority` + /// or `URLSessionTask.highPriority`. If this option not set, the default value (`URLSessionTask.defaultPriority`) + /// will be used. + public func downloadPriority(_ priority: Float) -> Self { + options.downloadPriority = priority + return self + } + + /// Sets whether Kingfisher should ignore the cache and try to start a download task for the image source. + /// - Parameter enabled: Enable the force refresh or not. + /// - Returns: A `Self` value with changes applied. + public func forceRefresh(_ enabled: Bool = true) -> Self { + options.forceRefresh = enabled + return self + } + + /// Sets whether Kingfisher should try to retrieve the image from memory cache first. If not found, it ignores the + /// disk cache and starts a download task for the image source. + /// - Parameter enabled: Enable the memory-only cache searching or not. + /// - Returns: A `Self` value with changes applied. + /// + /// 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. + public func fromMemoryCacheOrRefresh(_ enabled: Bool = true) -> Self { + options.fromMemoryCacheOrRefresh = enabled + return self + } + + /// Sets whether the image should only be cached in memory but not in disk. + /// - Parameter enabled: Whether the image should be only cache in memory or not. + /// - Returns: A `Self` value with changes applied. + public func cacheMemoryOnly(_ enabled: Bool = true) -> Self { + options.cacheMemoryOnly = enabled + return self + } + + /// Sets whether Kingfisher should wait for caching operation to be completed before calling the + /// `onSuccess` or `onFailure` block. + /// - Parameter enabled: Whether Kingfisher should wait for caching operation. + /// - Returns: A `Self` value with changes applied. + public func waitForCache(_ enabled: Bool = true) -> Self { + options.waitForCache = enabled + return self + } + + /// Sets whether Kingfisher should only try to retrieve the image from cache, but not from network. + /// - Parameter enabled: Whether Kingfisher should only try to retrieve the image from cache. + /// - Returns: A `Self` value with changes applied. + /// + /// If the image is not in cache, the image retrieving will fail with the + /// `KingfisherError.cacheError` with `.imageNotExisting` as its reason. + public func onlyFromCache(_ enabled: Bool = true) -> Self { + options.onlyFromCache = enabled + return self + } + + /// Sets whether the image should be decoded in a background thread before using. + /// - Parameter enabled: Whether the image should be decoded in a background thread before using. + /// - Returns: A `Self` value with changes applied. + /// + /// Setting to `true` 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 and memory to prepare the image + /// for using. + public func backgroundDecode(_ enabled: Bool = true) -> Self { + options.backgroundDecode = enabled + return self + } + + /// Sets the callback queue which is used as the target queue of dispatch callbacks when retrieving images from + /// cache. If not set, Kingfisher will use main queue for callbacks. + /// - Parameter queue: The target queue which the cache retrieving callback will be invoked on. + /// - Returns: A `Self` value with changes applied. + /// + /// - Note: + /// This option does not affect the callbacks for UI related extension methods or `KFImage` result handlers. + /// You will always get the callbacks called from main queue. + public func callbackQueue(_ queue: CallbackQueue) -> Self { + options.callbackQueue = queue + return self + } + + /// Sets the scale factor value when converting retrieved data to an image. + /// - Parameter factor: The scale factor value. + /// - Returns: A `Self` value with changes applied. + /// + /// 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. + /// + public func scaleFactor(_ factor: CGFloat) -> Self { + options.scaleFactor = factor + return self + } + + /// Sets whether the original image should be cached even when the original image has been processed by any other + /// `ImageProcessor`s. + /// - Parameter enabled: Whether the original image should be cached. + /// - Returns: A `Self` value with changes applied. + /// + /// 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. + /// + public func cacheOriginalImage(_ enabled: Bool = true) -> Self { + options.cacheOriginalImage = enabled + return self + } + + /// Sets writing options for an original image on a first write + /// - Parameter writingOptions: Options to control the writing of data to a disk storage. + /// - Returns: A `Self` value with changes applied. + /// If set, options will be passed the store operation for a new files. + /// + /// This is useful if you want to implement file enctyption on first write - eg [.completeFileProtection] + /// + public func diskStoreWriteOptions(_ writingOptions: Data.WritingOptions) -> Self { + options.diskStoreWriteOptions = writingOptions + return self + } + + /// Sets whether the disk storage loading should happen in the same calling queue. + /// - Parameter enabled: Whether the disk storage loading should happen in the same calling queue. + /// - Returns: A `Self` value with changes applied. + /// + /// 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. + /// + public func loadDiskFileSynchronously(_ enabled: Bool = true) -> Self { + options.loadDiskFileSynchronously = enabled + return self + } + + /// Sets a queue on which the image processing should happen. + /// - Parameter queue: The queue on which the image processing should happen. + /// - Returns: A `Self` value with changes applied. + /// + /// 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). + public func processingQueue(_ queue: CallbackQueue?) -> Self { + options.processingQueue = queue + return self + } + + /// Sets the alternative sources that will be used when loading of the original input `Source` fails. + /// - Parameter sources: The alternative sources will be used. + /// - Returns: A `Self` value with changes applied. + /// + /// Values of the `sources` array will be used to start a new image loading task if the previous task + /// fails due to an error. The image source loading process will stop as soon as a source is loaded successfully. + /// If all `sources` are used but the loading is still failing, an `imageSettingError` with + /// `alternativeSourcesExhausted` as its reason will be given out in the `catch` block. + /// + /// This is useful if you want to implement a fallback solution for setting image. + /// + /// User cancellation will not trigger the alternative source loading. + public func alternativeSources(_ sources: [Source]?) -> Self { + options.alternativeSources = sources + return self + } + + /// Sets a retry strategy that will be used when something gets wrong during the image retrieving. + /// - Parameter strategy: The provided strategy to define how the retrying should happen. + /// - Returns: A `Self` value with changes applied. + public func retry(_ strategy: RetryStrategy?) -> Self { + options.retryStrategy = strategy + return self + } + + /// Sets a retry strategy with a max retry count and retrying interval. + /// - Parameters: + /// - maxCount: The maximum count before the retry stops. + /// - interval: The time interval between each retry attempt. + /// - Returns: A `Self` value with changes applied. + /// + /// This defines the simplest retry strategy, which retry a failing request for several times, with some certain + /// interval between each time. For example, `.retry(maxCount: 3, interval: .second(3))` means attempt for at most + /// three times, and wait for 3 seconds if a previous retry attempt fails, then start a new attempt. + public func retry(maxCount: Int, interval: DelayRetryStrategy.Interval = .seconds(3)) -> Self { + let strategy = DelayRetryStrategy(maxRetryCount: maxCount, retryInterval: interval) + options.retryStrategy = strategy + return self + } + + /// Sets the `Source` should be loaded when user enables Low Data Mode and the original source fails with an + /// `NSURLErrorNetworkUnavailableReason.constrained` error. + /// - Parameter source: The `Source` will be loaded under low data mode. + /// - Returns: A `Self` value with changes applied. + /// + /// When this option is set, the + /// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the + /// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a + /// low-resolution version of your image or a local image provider to display a placeholder. + /// + /// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will + /// be loaded following the system default behavior, in a normal way. + public func lowDataModeSource(_ source: Source?) -> Self { + options.lowDataModeSource = source + return self + } + + /// Sets whether the image setting for an image view should happen with transition even when retrieved from cache. + /// - Parameter enabled: Enable the force transition or not. + /// - Returns: A `Self` with changes applied. + public func forceTransition(_ enabled: Bool = true) -> Self { + options.forceTransition = enabled + return self + } + + /// Sets the image that will be used if an image retrieving task fails. + /// - Parameter image: The image that will be used when something goes wrong. + /// - Returns: A `Self` with changes applied. + /// + /// If set and an image retrieving 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. + /// + public func onFailureImage(_ image: KFCrossPlatformImage?) -> Self { + options.onFailureImage = .some(image) + return self + } +} + +// MARK: - Request Modifier +extension KFOptionSetter { + /// Sets an `ImageDownloadRequestModifier` to change the image download request before it being sent. + /// - Parameter modifier: The modifier will be used to change the request before it being sent. + /// - Returns: A `Self` value with changes applied. + /// + /// 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. + /// + public func requestModifier(_ modifier: AsyncImageDownloadRequestModifier) -> Self { + options.requestModifier = modifier + return self + } + + /// Sets a block to change the image download request before it being sent. + /// - Parameter modifyBlock: The modifying block will be called to change the request before it being sent. + /// - Returns: A `Self` value with changes applied. + /// + /// 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. + /// + public func requestModifier(_ modifyBlock: @escaping (inout URLRequest) -> Void) -> Self { + options.requestModifier = AnyModifier { r -> URLRequest? in + var request = r + modifyBlock(&request) + return request + } + return self + } +} + +// MARK: - Redirect Handler +extension KFOptionSetter { + /// The `ImageDownloadRedirectHandler` argument will be used to change the request before redirection. + /// This is the possibility 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. + /// - Parameter handler: The handler will be used for redirection. + /// - Returns: A `Self` value with changes applied. + public func redirectHandler(_ handler: ImageDownloadRedirectHandler) -> Self { + options.redirectHandler = handler + return self + } + + /// The `block` will be used to change the request before redirection. + /// This is the possibility 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. + /// - Parameter block: The block will be used for redirection. + /// - Returns: A `Self` value with changes applied. + public func redirectHandler(_ block: @escaping (KF.RedirectPayload) -> Void) -> Self { + let redirectHandler = AnyRedirectHandler { (task, response, request, handler) in + let payload = KF.RedirectPayload( + task: task, response: response, newRequest: request, completionHandler: handler + ) + block(payload) + } + options.redirectHandler = redirectHandler + return self + } +} + +// MARK: - Processor +extension KFOptionSetter { + + /// Sets an image processor for the image task. It replaces the current image processor settings. + /// + /// - Parameter processor: The processor you want to use to process the image after it is downloaded. + /// - Returns: A `Self` value with changes applied. + /// + /// - Note: + /// To append a processor to current ones instead of replacing them all, use `appendProcessor(_:)`. + public func setProcessor(_ processor: ImageProcessor) -> Self { + options.processor = processor + return self + } + + /// Sets an array of image processors for the image task. It replaces the current image processor settings. + /// - Parameter processors: An array of processors. The processors inside this array will be concatenated one by one + /// to form a processor pipeline. + /// - Returns: A `Self` value with changes applied. + /// + /// - Note: To append processors to current ones instead of replacing them all, concatenate them by `|>`, then use + /// `appendProcessor(_:)`. + public func setProcessors(_ processors: [ImageProcessor]) -> Self { + switch processors.count { + case 0: + options.processor = DefaultImageProcessor.default + case 1...: + options.processor = processors.dropFirst().reduce(processors[0]) { $0 |> $1 } + default: + assertionFailure("Never happen") + } + return self + } + + /// Appends a processor to the current set processors. + /// - Parameter processor: The processor which will be appended to current processor settings. + /// - Returns: A `Self` value with changes applied. + public func appendProcessor(_ processor: ImageProcessor) -> Self { + options.processor = options.processor |> processor + return self + } + + /// Appends a `RoundCornerImageProcessor` to current processors. + /// - Parameters: + /// - radius: The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction + /// of the target image with `.widthFraction`. or `.heightFraction`. For example, given a square image + /// with width and height equals, `.widthFraction(0.5)` means use half of the length of size and makes + /// the final image a round one. + /// - targetSize: Target size of output image should be. If `nil`, the image will keep its original size after processing. + /// - corners: The target corners which will be applied rounding. + /// - backgroundColor: Background color of the output image. If `nil`, it will use a transparent background. + /// - Returns: A `Self` value with changes applied. + public func roundCorner( + radius: Radius, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> Self + { + let processor = RoundCornerImageProcessor( + radius: radius, + targetSize: targetSize, + roundingCorners: corners, + backgroundColor: backgroundColor + ) + return appendProcessor(processor) + } + + /// Appends a `BlurImageProcessor` to current processors. + /// - Parameter radius: Blur radius for the simulated Gaussian blur. + /// - Returns: A `Self` value with changes applied. + public func blur(radius: CGFloat) -> Self { + appendProcessor( + BlurImageProcessor(blurRadius: radius) + ) + } + + /// Appends a `OverlayImageProcessor` to current processors. + /// - Parameters: + /// - color: Overlay color will be used to overlay the input image. + /// - fraction: Fraction will be used when overlay the color to image. + /// - Returns: A `Self` value with changes applied. + public func overlay(color: KFCrossPlatformColor, fraction: CGFloat = 0.5) -> Self { + appendProcessor( + OverlayImageProcessor(overlay: color, fraction: fraction) + ) + } + + /// Appends a `TintImageProcessor` to current processors. + /// - Parameter color: Tint color will be used to tint the input image. + /// - Returns: A `Self` value with changes applied. + public func tint(color: KFCrossPlatformColor) -> Self { + appendProcessor( + TintImageProcessor(tint: color) + ) + } + + /// Appends a `BlackWhiteProcessor` to current processors. + /// - Returns: A `Self` value with changes applied. + public func blackWhite() -> Self { + appendProcessor( + BlackWhiteProcessor() + ) + } + + /// Appends a `CroppingImageProcessor` to current processors. + /// - Parameters: + /// - size: Target size of output image should be. + /// - anchor: 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. + /// - Returns: A `Self` value with changes applied. + public func cropping(size: CGSize, anchor: CGPoint = .init(x: 0.5, y: 0.5)) -> Self { + appendProcessor( + CroppingImageProcessor(size: size, anchor: anchor) + ) + } + + /// Appends a `DownsamplingImageProcessor` to current processors. + /// + /// Compared to `ResizingImageProcessor`, the `DownsamplingImageProcessor` does not render the original images and + /// then resize it. Instead, it downsamples the input data directly to a thumbnail image. So it is a more efficient + /// than `ResizingImageProcessor`. Prefer to use `DownsamplingImageProcessor` as possible + /// as you can than the `ResizingImageProcessor`. + /// + /// Only CG-based images are supported. Animated images (like GIF) is not supported. + /// + /// - Parameter size: 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. + /// - Returns: A `Self` value with changes applied. + public func downsampling(size: CGSize) -> Self { + let processor = DownsamplingImageProcessor(size: size) + if options.processor == DefaultImageProcessor.default { + return setProcessor(processor) + } else { + return appendProcessor(processor) + } + } + + + /// Appends a `ResizingImageProcessor` to current processors. + /// + /// If you need to resize a data represented image to a smaller size, use `DownsamplingImageProcessor` + /// instead, which is more efficient and uses less memory. + /// + /// - Parameters: + /// - referenceSize: The reference size for resizing operation in point. + /// - mode: Target content mode of output image should be. Default is `.none`. + /// - Returns: A `Self` value with changes applied. + public func resizing(referenceSize: CGSize, mode: ContentMode = .none) -> Self { + appendProcessor( + ResizingImageProcessor(referenceSize: referenceSize, mode: mode) + ) + } +} + +// MARK: - Cache Serializer +extension KFOptionSetter { + + /// Uses a given `CacheSerializer` to convert some data to an image object for retrieving from disk cache or vice + /// versa for storing to disk cache. + /// - Parameter cacheSerializer: The `CacheSerializer` which will be used. + /// - Returns: A `Self` value with changes applied. + public func serialize(by cacheSerializer: CacheSerializer) -> Self { + options.cacheSerializer = cacheSerializer + return self + } + + /// Uses a given format to serializer the image data to disk. It converts the image object to the give data format. + /// - Parameters: + /// - format: The desired data encoding format when store the image on disk. + /// - jpegCompressionQuality: If the format is `.JPEG`, it specify the compression quality when converting the + /// image to a JPEG data. Otherwise, it is ignored. + /// - Returns: A `Self` value with changes applied. + public func serialize(as format: ImageFormat, jpegCompressionQuality: CGFloat? = nil) -> Self { + let cacheSerializer: FormatIndicatedCacheSerializer + switch format { + case .JPEG: + cacheSerializer = .jpeg(compressionQuality: jpegCompressionQuality ?? 1.0) + case .PNG: + cacheSerializer = .png + case .GIF: + cacheSerializer = .gif + case .unknown: + cacheSerializer = .png + } + options.cacheSerializer = cacheSerializer + return self + } +} + +// MARK: - Image Modifier +extension KFOptionSetter { + + /// Sets an `ImageModifier` to the image task. Use this to modify the fetched image object properties if needed. + /// + /// 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`. + /// - Parameter modifier: The `ImageModifier` which will be used to modify the image object. + /// - Returns: A `Self` value with changes applied. + public func imageModifier(_ modifier: ImageModifier?) -> Self { + options.imageModifier = modifier + return self + } + + /// Sets a block to modify the image object. Use this to modify the fetched image object properties if needed. + /// + /// If the image was fetched directly from the downloader, the modifier block will run directly after the + /// `ImageProcessor`. If the image is being fetched from a cache, the modifier will run after the `CacheSerializer`. + /// + /// - Parameter block: The block which is used to modify the image object. + /// - Returns: A `Self` value with changes applied. + public func imageModifier(_ block: @escaping (inout KFCrossPlatformImage) throws -> Void) -> Self { + let modifier = AnyImageModifier { image -> KFCrossPlatformImage in + var image = image + try block(&image) + return image + } + options.imageModifier = modifier + return self + } +} + + +// MARK: - Cache Expiration +extension KFOptionSetter { + + /// Sets the expiration setting for memory cache of this image task. + /// + /// By default, the underlying `MemoryStorage.Backend` uses the + /// expiration in its config for all items. If set, the `MemoryStorage.Backend` will use this value to overwrite + /// the config setting for this caching item. + /// + /// - Parameter expiration: The expiration setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func memoryCacheExpiration(_ expiration: StorageExpiration?) -> Self { + options.memoryCacheExpiration = expiration + return self + } + + /// Sets 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, sets `.none` to it. + /// + /// - Parameter extending: The expiration extending setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func memoryCacheAccessExtending(_ extending: ExpirationExtending) -> Self { + options.memoryCacheAccessExtendingExpiration = extending + return self + } + + /// Sets the expiration setting for disk cache of this image task. + /// + /// By default, the underlying `DiskStorage.Backend` uses the expiration in its config for all items. If set, + /// the `DiskStorage.Backend` will use this value to overwrite the config setting for this caching item. + /// + /// - Parameter expiration: The expiration setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func diskCacheExpiration(_ expiration: StorageExpiration?) -> Self { + options.diskCacheExpiration = expiration + return self + } + + /// Sets the expiration extending setting for disk cache. The item expiration time will be incremented by this + /// value after access. + /// + /// By default, the underlying `DiskStorage.Backend` uses the initial cache expiration as extending + /// value: .cacheTime. + /// + /// To disable extending option at all, sets `.none` to it. + /// + /// - Parameter extending: The expiration extending setting used in cache storage. + /// - Returns: A `Self` value with changes applied. + public func diskCacheAccessExtending(_ extending: ExpirationExtending) -> Self { + options.diskCacheAccessExtendingExpiration = extending + return self + } +} diff --git a/Demo/Pods/Kingfisher/Sources/General/Kingfisher.swift b/Demo/Pods/Kingfisher/Sources/General/Kingfisher.swift new file mode 100644 index 0000000..f875e2a --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/Kingfisher.swift @@ -0,0 +1,106 @@ +// +// Kingfisher.swift +// Kingfisher +// +// Created by Wei Wang on 16/9/14. +// +// Copyright (c) 2019 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. + +import Foundation +import ImageIO + +#if os(macOS) +import AppKit +public typealias KFCrossPlatformImage = NSImage +public typealias KFCrossPlatformView = NSView +public typealias KFCrossPlatformColor = NSColor +public typealias KFCrossPlatformImageView = NSImageView +public typealias KFCrossPlatformButton = NSButton +#else +import UIKit +public typealias KFCrossPlatformImage = UIImage +public typealias KFCrossPlatformColor = UIColor +#if !os(watchOS) +public typealias KFCrossPlatformImageView = UIImageView +public typealias KFCrossPlatformView = UIView +public typealias KFCrossPlatformButton = UIButton +#if canImport(TVUIKit) +import TVUIKit +#endif +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +import CarPlay +#endif +#else +import WatchKit +#endif +#endif + +/// Wrapper for Kingfisher compatible types. This type provides an extension point for +/// convenience methods in Kingfisher. +public struct KingfisherWrapper { + 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 { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension KingfisherCompatibleValue { + /// Gets a namespace holder for Kingfisher compatible types. + public var kf: KingfisherWrapper { + get { return KingfisherWrapper(self) } + set { } + } +} + +extension KFCrossPlatformImage: KingfisherCompatible { } +#if !os(watchOS) +extension KFCrossPlatformImageView: KingfisherCompatible { } +extension KFCrossPlatformButton: KingfisherCompatible { } +extension NSTextAttachment: KingfisherCompatible { } +#else +extension WKInterfaceImage: KingfisherCompatible { } +#endif + +#if os(tvOS) && canImport(TVUIKit) +@available(tvOS 12.0, *) +extension TVMonogramView: KingfisherCompatible { } +#endif + +#if canImport(CarPlay) && !targetEnvironment(macCatalyst) +@available(iOS 14.0, *) +extension CPListItem: KingfisherCompatible { } +#endif diff --git a/Demo/Pods/Kingfisher/Sources/General/KingfisherError.swift b/Demo/Pods/Kingfisher/Sources/General/KingfisherError.swift new file mode 100644 index 0000000..83d20a1 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/KingfisherError.swift @@ -0,0 +1,461 @@ +// +// KingfisherError.swift +// Kingfisher +// +// Created by onevcat on 2018/09/26. +// +// Copyright (c) 2019 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. + +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. + /// - cannotCreateCacheFile: Cannot create the cache file at a certain fileURL under a key. Code 3009. + /// - cannotSetCacheFileAttribute: Cannot set file attributes to a cached file. Code 3010. + 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: KFCrossPlatformImage?, original: Data?, serializer: CacheSerializer) + + /// Cannot create the cache file at a certain fileURL under a key. Code 3009. + /// - fileURL: The url where the cache file should be created. + /// - key: The cache key used for the cache. When caching a file through `KingfisherManager` and Kingfisher's + /// extension method, it is the resolved cache key based on your input `Source` and the image processors. + /// - data: The data to be cached. + /// - error: The underlying error originally thrown by Foundation when writing the `data` to the disk file at + /// `fileURL`. + case cannotCreateCacheFile(fileURL: URL, key: String, data: Data, error: Error) + + /// Cannot set file attributes to a cached file. Code 3010. + /// - filePath: The path of target cache file. + /// - attributes: The file attribute to be set to the target file. + /// - error: The underlying error originally thrown by Foundation when setting the `attributes` to the disk + /// file at `filePath`. + case cannotSetCacheFileAttribute(filePath: String, attributes: [FileAttributeKey : Any], error: Error) + + /// The disk storage of cache is not ready. Code 3011. + /// + /// This is usually due to extremely lack of space on disk storage, and + /// Kingfisher failed even when creating the cache folder. The disk storage will be in unusable state. Normally, + /// ask user to free some spaces and restart the app to make the disk storage work again. + /// - cacheURL: The intended URL which should be the storage folder. + case diskStorageIsNotReady(cacheURL: URL) + } + + + /// 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 task. + 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) + + /// No more alternative `Source` can be used in current loading process. It means that the + /// `.alternativeSources` are used and Kingfisher tried to recovery from the original error, but still + /// fails for all the given alternative sources. The associated value holds all the errors encountered during + /// the load process, including the original source loading error and all the alternative sources errors. + /// Code 5004. + case alternativeSourcesExhausted([PropagationError]) + } + + // 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 + } + + var isLowDataModeConstrained: Bool { + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *), + case .responseError(reason: .URLSessionError(let sessionError)) = self, + let urlError = sessionError as? URLError, + urlError.networkUnavailableReason == .constrained + { + 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)." + case .cannotCreateCacheFile(let fileURL, let key, let data, let error): + return "Cannot create cache file at url: \(fileURL), key: \(key), data length: \(data.count). " + + "Underlying foundation error: \(error)." + case .cannotSetCacheFileAttribute(let filePath, let attributes, let error): + return "Cannot set file attribute for the cache file at path: \(filePath), attributes: \(attributes)." + + "Underlying foundation error: \(error)." + case .diskStorageIsNotReady(let cacheURL): + return "The disk storage is not ready to use yet at URL: '\(cacheURL)'. " + + "This is usually caused by extremely lack of disk space. Ask users to free up some space and restart the app." + } + } + + 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 + case .cannotCreateCacheFile: return 3009 + case .cannotSetCacheFileAttribute: return 3010 + case .diskStorageIsNotReady: return 3011 + } + } +} + +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)" + case .alternativeSourcesExhausted(let errors): + return "Image setting from alternaive sources failed: \(errors)" + } + } + + var errorCode: Int { + switch self { + case .emptySource: return 5001 + case .notCurrentSourceTask: return 5002 + case .dataProviderError: return 5003 + case .alternativeSourcesExhausted: return 5004 + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/General/KingfisherManager.swift b/Demo/Pods/Kingfisher/Sources/General/KingfisherManager.swift new file mode 100644 index 0000000..b0b9db1 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/KingfisherManager.swift @@ -0,0 +1,802 @@ +// +// KingfisherManager.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 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. + + +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: KFCrossPlatformImage + + /// 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` which this result is related to. This indicated where the `image` of `self` is referring. + public let source: Source + + /// The original `Source` from which the retrieve task begins. It can be different from the `source` property. + /// When an alternative source loading happened, the `source` will be the replacing loading target, while the + /// `originalSource` will be kept as the initial `source` which issued the image loading process. + public let originalSource: Source + + /// Gets the data behind the result. + /// + /// If this result is from a network downloading (when `cacheType == .none`), calling this returns the downloaded + /// data. If the reuslt is from cache, it serializes the image with the given cache serializer in the loading option + /// and returns the result. + /// + /// - Note: + /// This can be a time-consuming action, so if you need to use the data for multiple times, it is suggested to hold + /// it and prevent keeping calling this too frequently. + public let data: () -> Data? +} + +/// A struct that stores some related information of an `KingfisherError`. It provides some context information for +/// a pure error so you can identify the error easier. +public struct PropagationError { + + /// The `Source` to which current `error` is bound. + public let source: Source + + /// The actual error happens in framework. + public let error: KingfisherError +} + + +/// The downloading task updated block type. The parameter `newTask` is the updated new task of image setting process. +/// It is a `nil` if the image loading does not require an image downloading process. If an image downloading is issued, +/// this value will contain the actual `DownloadTask` for you to keep and cancel it later if you need. +public typealias DownloadTaskUpdatedBlock = ((_ newTask: DownloadTask?) -> Void) + +/// 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 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. + /// - downloadTaskUpdated: Called when a new image downloading task is created for current image retrieving. This + /// usually happens when an alternative source is used to replace the original (failed) + /// task. You can update your reference of `DownloadTask` if you want to manually `cancel` + /// the new task. + /// - 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, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + return retrieveImage( + with: resource.convertToSource(), + options: options, + progressBlock: progressBlock, + downloadTaskUpdated: downloadTaskUpdated, + 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 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. + /// - downloadTaskUpdated: Called when a new image downloading task is created for current image retrieving. This + /// usually happens when an alternative source is used to replace the original (failed) + /// task. You can update your reference of `DownloadTask` if you want to manually `cancel` + /// the new task. + /// - 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`. + /// + @discardableResult + public func retrieveImage( + with source: Source, + options: KingfisherOptionsInfo? = nil, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + let options = currentDefaultOptions + (options ?? .empty) + let info = KingfisherParsedOptionsInfo(options) + return retrieveImage( + with: source, + options: info, + progressBlock: progressBlock, + downloadTaskUpdated: downloadTaskUpdated, + completionHandler: completionHandler) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + progressBlock: DownloadProgressBlock? = nil, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + var info = options + if let block = progressBlock { + info.onDataReceived = (info.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)] + } + return retrieveImage( + with: source, + options: info, + downloadTaskUpdated: downloadTaskUpdated, + progressiveImageSetter: nil, + completionHandler: completionHandler) + } + + func retrieveImage( + with source: Source, + options: KingfisherParsedOptionsInfo, + downloadTaskUpdated: DownloadTaskUpdatedBlock? = nil, + progressiveImageSetter: ((KFCrossPlatformImage?) -> Void)? = nil, + referenceTaskIdentifierChecker: (() -> Bool)? = nil, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + var options = options + if let provider = ImageProgressiveProvider(options, refresh: { image in + guard let setter = progressiveImageSetter else { + return + } + guard let strategy = options.progressiveJPEG?.onImageUpdated(image) else { + setter(image) + return + } + switch strategy { + case .default: setter(image) + case .keepCurrent: break + case .replace(let newImage): setter(newImage) + } + }) { + options.onDataReceived = (options.onDataReceived ?? []) + [provider] + } + if let checker = referenceTaskIdentifierChecker { + options.onDataReceived?.forEach { + $0.onShouldApply = checker + } + } + + let retrievingContext = RetrievingContext(options: options, originalSource: source) + var retryContext: RetryContext? + + func startNewRetrieveTask( + with source: Source, + downloadTaskUpdated: DownloadTaskUpdatedBlock? + ) { + let newTask = self.retrieveImage(with: source, context: retrievingContext) { result in + handler(currentSource: source, result: result) + } + downloadTaskUpdated?(newTask) + } + + func failCurrentSource(_ source: Source, with error: KingfisherError) { + // Skip alternative sources if the user cancelled it. + guard !error.isTaskCancelled else { + completionHandler?(.failure(error)) + return + } + // When low data mode constrained error, retry with the low data mode source instead of use alternative on fly. + guard !error.isLowDataModeConstrained else { + if let source = retrievingContext.options.lowDataModeSource { + retrievingContext.options.lowDataModeSource = nil + startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated) + } else { + // This should not happen. + completionHandler?(.failure(error)) + } + return + } + if let nextSource = retrievingContext.popAlternativeSource() { + retrievingContext.appendError(error, to: source) + startNewRetrieveTask(with: nextSource, downloadTaskUpdated: downloadTaskUpdated) + } else { + // No other alternative source. Finish with error. + if retrievingContext.propagationErrors.isEmpty { + completionHandler?(.failure(error)) + } else { + retrievingContext.appendError(error, to: source) + let finalError = KingfisherError.imageSettingError( + reason: .alternativeSourcesExhausted(retrievingContext.propagationErrors) + ) + completionHandler?(.failure(finalError)) + } + } + } + + func handler(currentSource: Source, result: (Result)) -> Void { + switch result { + case .success: + completionHandler?(result) + case .failure(let error): + if let retryStrategy = options.retryStrategy { + let context = retryContext?.increaseRetryCount() ?? RetryContext(source: source, error: error) + retryContext = context + + retryStrategy.retry(context: context) { decision in + switch decision { + case .retry(let userInfo): + retryContext?.userInfo = userInfo + startNewRetrieveTask(with: source, downloadTaskUpdated: downloadTaskUpdated) + case .stop: + failCurrentSource(currentSource, with: error) + } + } + } else { + failCurrentSource(currentSource, with: error) + } + } + } + + return retrieveImage( + with: source, + context: retrievingContext) + { + result in + handler(currentSource: source, result: result) + } + + } + + private func retrieveImage( + with source: Source, + context: RetrievingContext, + completionHandler: ((Result) -> Void)?) -> DownloadTask? + { + let options = context.options + if options.forceRefresh { + return loadAndCacheImage( + source: source, + context: context, + completionHandler: completionHandler)?.value + + } else { + let loadedFromCache = retrieveImageFromCache( + source: source, + context: context, + 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, + context: context, + completionHandler: completionHandler)?.value + } + } + + func provideImage( + provider: ImageDataProvider, + options: KingfisherParsedOptionsInfo, + completionHandler: ((Result) -> 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)) + } + + } + } + } + + private func cacheImage( + source: Source, + options: KingfisherParsedOptionsInfo, + context: RetrievingContext, + result: Result, + completionHandler: ((Result) -> Void)? + ) + { + switch result { + case .success(let value): + let needToCacheOriginalImage = options.cacheOriginalImage && + options.processor != DefaultImageProcessor.default + let coordinator = CacheCallbackCoordinator( + shouldWaitForCache: options.waitForCache, shouldCacheOriginal: needToCacheOriginalImage) + let result = RetrieveImageResult( + image: options.imageModifier?.modify(value.image) ?? value.image, + cacheType: .none, + source: source, + originalSource: context.originalSource, + data: { value.originalData } + ) + // 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 + coordinator.apply(.cachingImage) { + completionHandler?(.success(result)) + } + } + + // Add original image to cache if necessary. + + if needToCacheOriginalImage { + let originalCache = options.originalCache ?? targetCache + originalCache.storeToDisk( + value.originalData, + forKey: source.cacheKey, + processorIdentifier: DefaultImageProcessor.default.identifier, + expiration: options.diskCacheExpiration) + { + _ in + coordinator.apply(.cachingOriginalImage) { + completionHandler?(.success(result)) + } + } + } + + coordinator.apply(.cacheInitiated) { + completionHandler?(.success(result)) + } + + case .failure(let error): + completionHandler?(.failure(error)) + } + } + + @discardableResult + func loadAndCacheImage( + source: Source, + context: RetrievingContext, + completionHandler: ((Result) -> Void)?) -> DownloadTask.WrappedTask? + { + let options = context.options + func _cacheImage(_ result: Result) { + cacheImage( + source: source, + options: options, + context: context, + result: result, + completionHandler: completionHandler + ) + } + + switch source { + case .network(let resource): + let downloader = options.downloader ?? self.downloader + let task = downloader.downloadImage( + with: resource.downloadURL, options: options, completionHandler: _cacheImage + ) + + + // The code below is neat, but it fails the Swift 5.2 compiler with a runtime crash when + // `BUILD_LIBRARY_FOR_DISTRIBUTION` is turned on. I believe it is a bug in the compiler. + // Let's fallback to a traditional style before it can be fixed in Swift. + // + // https://github.com/onevcat/Kingfisher/issues/1436 + // + // return task.map(DownloadTask.WrappedTask.download) + + if let task = task { + return .download(task) + } else { + return nil + } + + 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, + context: RetrievingContext, + completionHandler: ((Result) -> Void)?) -> Bool + { + let options = context.options + // 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 } + + // TODO: Optimize it when we can use async across all the project. + func checkResultImageAndCallback(_ inputImage: KFCrossPlatformImage) { + var image = inputImage + if image.kf.imageFrameCount != nil && image.kf.imageFrameCount != 1, let data = image.kf.animatedImageData { + // Always recreate animated image representation since it is possible to be loaded in different options. + // https://github.com/onevcat/Kingfisher/issues/1923 + image = KingfisherWrapper.animatedImage(data: data, options: options.imageCreatingOptions) ?? .init() + } + if let modifier = options.imageModifier { + image = modifier.modify(image) + } + let value = result.map { + RetrieveImageResult( + image: image, + cacheType: $0.cacheType, + source: source, + originalSource: context.originalSource, + data: { options.cacheSerializer.data(with: image, original: nil) } + ) + } + completionHandler(value) + } + + result.match { cacheResult in + options.callbackQueue.execute { + guard let image = cacheResult.image else { + completionHandler(.failure(KingfisherError.cacheError(reason: .imageNotExisting(key: key)))) + return + } + + if options.cacheSerializer.originalDataUsed { + 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 + } + options.callbackQueue.execute { + checkResultImageAndCallback(processedImage) + } + } + } else { + checkResultImageAndCallback(image) + } + } + } onFailure: { error in + options.callbackQueue.execute { + 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 originalImageCacheType = originalCache.imageCachedType( + forKey: key, processorIdentifier: DefaultImageProcessor.default.identifier) + let canAcceptDiskCache = !options.fromMemoryCacheOrRefresh + + let canUseOriginalImageCache = + (canAcceptDiskCache && originalImageCacheType.cached) || + (!canAcceptDiskCache && originalImageCacheType == .memory) + + if canUseOriginalImageCache { + // 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 { + assertionFailure("The image (under key: \(key) should be existing in the original cache.") + 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 + + let coordinator = CacheCallbackCoordinator( + shouldWaitForCache: options.waitForCache, shouldCacheOriginal: false) + + let image = options.imageModifier?.modify(processedImage) ?? processedImage + let result = RetrieveImageResult( + image: image, + cacheType: .none, + source: source, + originalSource: context.originalSource, + data: { options.cacheSerializer.data(with: processedImage, original: nil) } + ) + + targetCache.store( + processedImage, + forKey: key, + options: cacheOptions, + toDisk: !options.cacheMemoryOnly) + { + _ in + coordinator.apply(.cachingImage) { + options.callbackQueue.execute { completionHandler?(.success(result)) } + } + } + + coordinator.apply(.cacheInitiated) { + options.callbackQueue.execute { completionHandler?(.success(result)) } + } + } + }, + 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 + } +} + +class RetrievingContext { + + var options: KingfisherParsedOptionsInfo + + let originalSource: Source + var propagationErrors: [PropagationError] = [] + + init(options: KingfisherParsedOptionsInfo, originalSource: Source) { + self.originalSource = originalSource + self.options = options + } + + func popAlternativeSource() -> Source? { + guard var alternativeSources = options.alternativeSources, !alternativeSources.isEmpty else { + return nil + } + let nextSource = alternativeSources.removeFirst() + options.alternativeSources = alternativeSources + return nextSource + } + + @discardableResult + func appendError(_ error: KingfisherError, to source: Source) -> [PropagationError] { + let item = PropagationError(source: source, error: error) + propagationErrors.append(item) + return propagationErrors + } +} + +class CacheCallbackCoordinator { + + enum State { + case idle + case imageCached + case originalImageCached + case done + } + + enum Action { + case cacheInitiated + case cachingImage + case cachingOriginalImage + } + + private let shouldWaitForCache: Bool + private let shouldCacheOriginal: Bool + private let stateQueue: DispatchQueue + private var threadSafeState: State = .idle + + private (set) var state: State { + set { stateQueue.sync { threadSafeState = newValue } } + get { stateQueue.sync { threadSafeState } } + } + + init(shouldWaitForCache: Bool, shouldCacheOriginal: Bool) { + self.shouldWaitForCache = shouldWaitForCache + self.shouldCacheOriginal = shouldCacheOriginal + let stateQueueName = "com.onevcat.Kingfisher.CacheCallbackCoordinator.stateQueue.\(UUID().uuidString)" + self.stateQueue = DispatchQueue(label: stateQueueName) + } + + func apply(_ action: Action, trigger: () -> Void) { + switch (state, action) { + case (.done, _): + break + + // From .idle + case (.idle, .cacheInitiated): + if !shouldWaitForCache { + state = .done + trigger() + } + case (.idle, .cachingImage): + if shouldCacheOriginal { + state = .imageCached + } else { + state = .done + trigger() + } + case (.idle, .cachingOriginalImage): + state = .originalImageCached + + // From .imageCached + case (.imageCached, .cachingOriginalImage): + state = .done + trigger() + + // From .originalImageCached + case (.originalImageCached, .cachingImage): + state = .done + trigger() + + default: + assertionFailure("This case should not happen in CacheCallbackCoordinator: \(state) - \(action)") + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift b/Demo/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift new file mode 100644 index 0000000..5f2aea6 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/General/KingfisherOptionsInfo.swift @@ -0,0 +1,400 @@ +// +// KingfisherOptionsInfo.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/23. +// +// Copyright (c) 2019 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. + +#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 start a download task for the image source. + 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 the `KingfisherError.cacheError` with `.imageNotExisting` as its + /// reason. + 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 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(AsyncImageDownloadRequestModifier) + + /// The `ImageDownloadRedirectHandler` contained will be used to change the request before redirection. + /// This is the possibility 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) + + /// Provides 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 an 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 an image retrieving 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(KFCrossPlatformImage?) + + /// 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 + + /// Options to control the writing of data to disk storage + /// If set, options will be passed the store operation for a new files. + case diskStoreWriteOptions(Data.WritingOptions) + + /// 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 disk 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) + + /// The expiration extending setting for disk cache. The item expiration time will be incremented by this value after access. + /// By default, the underlying `DiskStorage.Backend` uses the initial cache expiration as extending value: .cacheTime. + /// To disable extending option at all add diskCacheAccessExtendingExpiration(.none) to options. + case diskCacheAccessExtendingExpiration(ExpirationExtending) + + /// 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 associated `ImageProgressive` value to process the + /// progressive JPEG data and display it in a progressive way. + case progressiveJPEG(ImageProgressive) + + /// The alternative sources will be used when the original input `Source` fails. The `Source`s in the associated + /// array will be used to start a new image loading task if the previous task fails due to an error. The image + /// source loading process will stop as soon as a source is loaded successfully. If all `[Source]`s are used but + /// the loading is still failing, an `imageSettingError` with `alternativeSourcesExhausted` as its reason will be + /// thrown out. + /// + /// This option is useful if you want to implement a fallback solution for setting image. + /// + /// User cancellation will not trigger the alternative source loading. + case alternativeSources([Source]) + + /// Provide a retry strategy which will be used when something gets wrong during the image retrieving process from + /// `KingfisherManager`. You can define a strategy by create a type conforming to the `RetryStrategy` protocol. + /// + /// - Note: + /// + /// All extension methods of Kingfisher (`kf` extensions on `UIImageView` or `UIButton`) retrieve images through + /// `KingfisherManager`, so the retry strategy also applies when using them. However, this option does not apply + /// when pass to an `ImageDownloader` or `ImageCache`. + /// + case retryStrategy(RetryStrategy) + + /// The `Source` should be loaded when user enables Low Data Mode and the original source fails with an + /// `NSURLErrorNetworkUnavailableReason.constrained` error. When this option is set, the + /// `allowsConstrainedNetworkAccess` property of the request for the original source will be set to `false` and the + /// `Source` in associated value will be used to retrieve the image for low data mode. Usually, you can provide a + /// low-resolution version of your image or a local image provider to display a placeholder. + /// + /// If not set or the `source` is `nil`, the device Low Data Mode will be ignored and the original source will + /// be loaded following the system default behavior, in a normal way. + case lowDataMode(Source?) +} + +// 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: AsyncImageDownloadRequestModifier? = 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 = .none + public var alsoPrefetchToMemory = false + public var loadDiskFileSynchronously = false + public var diskStoreWriteOptions: Data.WritingOptions = [] + public var memoryCacheExpiration: StorageExpiration? = nil + public var memoryCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime + public var diskCacheExpiration: StorageExpiration? = nil + public var diskCacheAccessExtendingExpiration: ExpirationExtending = .cacheTime + public var processingQueue: CallbackQueue? = nil + public var progressiveJPEG: ImageProgressive? = nil + public var alternativeSources: [Source]? = nil + public var retryStrategy: RetryStrategy? = nil + public var lowDataModeSource: Source? = 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 .diskStoreWriteOptions(let options): diskStoreWriteOptions = options + case .memoryCacheExpiration(let expiration): memoryCacheExpiration = expiration + case .memoryCacheAccessExtendingExpiration(let expirationExtending): memoryCacheAccessExtendingExpiration = expirationExtending + case .diskCacheExpiration(let expiration): diskCacheExpiration = expiration + case .diskCacheAccessExtendingExpiration(let expirationExtending): diskCacheAccessExtendingExpiration = expirationExtending + case .processingQueue(let queue): processingQueue = queue + case .progressiveJPEG(let value): progressiveJPEG = value + case .alternativeSources(let sources): alternativeSources = sources + case .retryStrategy(let strategy): retryStrategy = strategy + case .lowDataMode(let source): lowDataModeSource = source + } + } + + 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 self.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/Demo/Pods/Kingfisher/Sources/Image/Filter.swift b/Demo/Pods/Kingfisher/Sources/Image/Filter.swift new file mode 100644 index 0000000..6e4b386 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Image/Filter.swift @@ -0,0 +1,146 @@ +// +// Filter.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/31. +// +// Copyright (c) 2019 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. + +#if !os(watchOS) + +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) -> KFCrossPlatformImage? { + 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: (KFCrossPlatformColor) -> 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: KFCrossPlatformImage { + + /// 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) -> KFCrossPlatformImage { + + 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 KFCrossPlatformImage(cgImage: result, scale: base.scale, orientation: base.imageOrientation) + #endif + } + +} + +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift b/Demo/Pods/Kingfisher/Sources/Image/GIFAnimatedImage.swift new file mode 100644 index 0000000..8b2480f --- /dev/null +++ b/Demo/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 +// +// 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. +public class GIFAnimatedImage { + let images: [KFCrossPlatformImage] + let duration: TimeInterval + + init?(from imageSource: CGImageSource, for info: [String: Any], options: ImageCreatingOptions) { + let frameCount = CGImageSourceGetCount(imageSource) + var images = [KFCrossPlatformImage]() + 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. + public 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`. + public 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/Demo/Pods/Kingfisher/Sources/Image/GraphicsContext.swift b/Demo/Pods/Kingfisher/Sources/Image/GraphicsContext.swift new file mode 100644 index 0000000..6d8443c --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Image/GraphicsContext.swift @@ -0,0 +1,88 @@ +// +// GraphicsContext.swift +// Kingfisher +// +// Created by taras on 19/04/2021. +// +// Copyright (c) 2021 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. + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +enum GraphicsContext { + static func begin(size: CGSize, scale: CGFloat) { + #if os(macOS) + NSGraphicsContext.saveGraphicsState() + #else + UIGraphicsBeginImageContextWithOptions(size, false, scale) + #endif + } + + static func current(size: CGSize, scale: CGFloat, inverting: Bool, cgImage: CGImage?) -> 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 + guard let context = NSGraphicsContext(bitmapImageRep: rep) else { + assertionFailure("[Kingfisher] Image context cannot be created.") + return nil + } + + NSGraphicsContext.current = context + return context.cgContext + #else + 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 + } + + static func end() { + #if os(macOS) + NSGraphicsContext.restoreGraphicsState() + #else + UIGraphicsEndImageContext() + #endif + } +} + diff --git a/Demo/Pods/Kingfisher/Sources/Image/Image.swift b/Demo/Pods/Kingfisher/Sources/Image/Image.swift new file mode 100644 index 0000000..5e6c9a3 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Image/Image.swift @@ -0,0 +1,377 @@ +// +// Image.swift +// Kingfisher +// +// Created by Wei Wang on 16/1/6. +// +// Copyright (c) 2019 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. + + +#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? +private var imageFrameCountKey: Void? + +// MARK: - Image Properties +extension KingfisherWrapper where Base: KFCrossPlatformImage { + private(set) var animatedImageData: Data? { + get { return getAssociatedObject(base, &animatedImageDataKey) } + set { setRetainedAssociatedObject(base, &animatedImageDataKey, newValue) } + } + + public var imageFrameCount: Int? { + get { return getAssociatedObject(base, &imageFrameCountKey) } + set { setRetainedAssociatedObject(base, &imageFrameCountKey, 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: [KFCrossPlatformImage]? { + 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: [KFCrossPlatformImage]? { return base.images } + var duration: TimeInterval { return base.duration } + var size: CGSize { return base.size } + + /// The image source reference of current image. + public 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 + } + let bytesPerPixel = cgImage.bitsPerPixel / 8 + guard let imageCount = images?.count else { + return pixel * bytesPerPixel + } + return pixel * bytesPerPixel * imageCount + } +} + +// MARK: - Image Conversion +extension KingfisherWrapper where Base: KFCrossPlatformImage { + #if os(macOS) + static func image(cgImage: CGImage, scale: CGFloat, refImage: KFCrossPlatformImage?) -> KFCrossPlatformImage { + return KFCrossPlatformImage(cgImage: cgImage, size: .zero) + } + + /// Normalize the image. This getter does nothing on macOS but return the image itself. + public var normalized: KFCrossPlatformImage { 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: KFCrossPlatformImage?) -> KFCrossPlatformImage { + return KFCrossPlatformImage(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: KFCrossPlatformImage { + // prevent animated image (GIF) lose it's images + guard images == nil else { return base.copy() as! KFCrossPlatformImage } + // No need to do anything if already up + guard base.imageOrientation != .up else { return base.copy() as! KFCrossPlatformImage } + + return draw(to: size, inverting: true, refImage: KFCrossPlatformImage()) { + fixOrientation(in: $0) + return true + } + } + + 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: KFCrossPlatformImage { + /// 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 + return base.pngData() + #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 + return base.jpegData(compressionQuality: compressionQuality) + #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. + /// - Parameters: + /// - format: The format in which the output data should be. If `unknown`, the `base` image will be + /// converted in the PNG representation. + /// - compressionQuality: The compression quality when converting image to a lossy format data. + /// + /// - Returns: The output data representing. + public func data(format: ImageFormat, compressionQuality: CGFloat = 1.0) -> Data? { + return autoreleasepool { () -> Data? in + let data: Data? + switch format { + case .PNG: data = pngRepresentation() + case .JPEG: data = jpegRepresentation(compressionQuality: compressionQuality) + case .GIF: data = gifRepresentation() + case .unknown: data = normalized.kf.pngRepresentation() + } + + return data + } + } +} + +// MARK: - Creating Images +extension KingfisherWrapper where Base: KFCrossPlatformImage { + + /// 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) -> KFCrossPlatformImage? { + 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: KFCrossPlatformImage? + if options.onlyFirstFrame { + image = animatedImage.images.first + } else { + image = KFCrossPlatformImage(data: data) + var kf = image?.kf + kf?.images = animatedImage.images + kf?.duration = animatedImage.duration + } + image?.kf.animatedImageData = data + image?.kf.imageFrameCount = Int(CGImageSourceGetCount(imageSource)) + return image + #else + + var image: KFCrossPlatformImage? + 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 = KFCrossPlatformImage(data: data, scale: options.scale) + var kf = image?.kf + kf?.imageSource = imageSource + kf?.animatedImageData = data + } + + image?.kf.imageFrameCount = Int(CGImageSourceGetCount(imageSource)) + 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) -> KFCrossPlatformImage? { + var image: KFCrossPlatformImage? + switch data.kf.imageFormat { + case .JPEG: + image = KFCrossPlatformImage(data: data, scale: options.scale) + case .PNG: + image = KFCrossPlatformImage(data: data, scale: options.scale) + case .GIF: + image = KingfisherWrapper.animatedImage(data: data, options: options) + case .unknown: + image = KFCrossPlatformImage(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 pointsize 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) -> KFCrossPlatformImage? { + 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/Demo/Pods/Kingfisher/Sources/Image/ImageDrawing.swift b/Demo/Pods/Kingfisher/Sources/Image/ImageDrawing.swift new file mode 100644 index 0000000..63f135a --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Image/ImageDrawing.swift @@ -0,0 +1,636 @@ +// +// ImageDrawing.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 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. + +import Accelerate + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif +#if canImport(UIKit) +import UIKit +#endif + +// MARK: - Image Transforming +extension KingfisherWrapper where Base: KFCrossPlatformImage { + // 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: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage + { + 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, inverting: false) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + UIRectFill(rect) + } + + base.draw(in: rect, blendMode: blendMode, alpha: alpha) + return false + } + } + #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: KFCrossPlatformColor? = nil) -> KFCrossPlatformImage + { + 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, inverting: false) { _ in + if let backgroundColor = backgroundColor { + backgroundColor.setFill() + rect.fill() + } + base.draw(in: rect, from: .zero, operation: compositingOperation, fraction: alpha) + return false + } + } + #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( + withRadius radius: Radius, + fit size: CGSize, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) -> KFCrossPlatformImage + { + + 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, inverting: false) { _ in + #if os(macOS) + if let backgroundColor = backgroundColor { + let rectPath = NSBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) + path.addClip() + base.draw(in: rect) + #else + guard let context = UIGraphicsGetCurrentContext() else { + assertionFailure("[Kingfisher] Failed to create CG context for image.") + return false + } + + if let backgroundColor = backgroundColor { + let rectPath = UIBezierPath(rect: rect) + backgroundColor.setFill() + rectPath.fill() + } + + let path = pathForRoundCorner(rect: rect, radius: radius, corners: corners) + context.addPath(path.cgPath) + context.clip() + base.draw(in: rect) + #endif + return false + } + } + + /// 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: KFCrossPlatformColor? = nil + ) -> KFCrossPlatformImage + { + image(withRadius: .point(radius), fit: size, roundingCorners: corners, backgroundColor: backgroundColor) + } + + #if os(macOS) + func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> NSBezierPath { + let cornerRadius = radius.compute(with: rect.size) + let path = NSBezierPath(roundedRect: rect, byRoundingCorners: corners, radius: cornerRadius - offsetBase / 2) + path.windingRule = .evenOdd + return path + } + #else + func pathForRoundCorner(rect: CGRect, radius: Radius, corners: RectCorner, offsetBase: CGFloat = 0) -> UIBezierPath { + let cornerRadius = radius.compute(with: rect.size) + return UIBezierPath( + roundedRect: rect, + byRoundingCorners: corners.uiRectCorner, + cornerRadii: CGSize( + width: cornerRadius - offsetBase / 2, + height: cornerRadius - offsetBase / 2 + ) + ) + } + #endif + + #if os(iOS) || os(tvOS) + func resize(to size: CGSize, for contentMode: UIView.ContentMode) -> KFCrossPlatformImage { + 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) -> KFCrossPlatformImage { + 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, inverting: false) { _ in + #if os(macOS) + base.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + #else + base.draw(in: rect) + #endif + return false + } + } + + /// 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) -> KFCrossPlatformImage { + 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) -> KFCrossPlatformImage { + 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) -> KFCrossPlatformImage { + + 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 = 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 * CGFloat.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) + + 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) + } + GraphicsContext.begin(size: size, scale: scale) + guard let context = GraphicsContext.current(size: size, scale: scale, inverting: true, cgImage: cgImage) 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)) + GraphicsContext.end() + + var inBuffer = createEffectBuffer(context) + + GraphicsContext.begin(size: size, scale: scale) + guard let outContext = GraphicsContext.current(size: size, scale: scale, inverting: true, cgImage: cgImage) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + defer { GraphicsContext.end() } + 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 { + KFCrossPlatformImage(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 + } + + public func addingBorder(_ border: Border) -> KFCrossPlatformImage + { + 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, inverting: false) { context in + + #if os(macOS) + base.draw(in: rect) + #else + base.draw(in: rect, blendMode: .normal, alpha: 1.0) + #endif + + + let strokeRect = rect.insetBy(dx: border.lineWidth / 2, dy: border.lineWidth / 2) + context.setStrokeColor(border.color.cgColor) + context.setAlpha(border.color.rgba.a) + + let line = pathForRoundCorner( + rect: strokeRect, + radius: border.radius, + corners: border.roundingCorners, + offsetBase: border.lineWidth + ) + line.lineCapStyle = .square + line.lineWidth = border.lineWidth + line.stroke() + + return false + } + } + + // 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: KFCrossPlatformColor, fraction: CGFloat) -> KFCrossPlatformImage { + + 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, inverting: false) { 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 + return false + } + } + + // 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: KFCrossPlatformColor) -> KFCrossPlatformImage { + #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) -> KFCrossPlatformImage { + #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) -> KFCrossPlatformImage { + 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: KFCrossPlatformImage { + + /// 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: KFCrossPlatformImage { 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) -> KFCrossPlatformImage { + // 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)) + return true + } + } + + /// 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 context: The context for drawing. + /// - 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(on context: CGContext) -> KFCrossPlatformImage { + // 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 refImage = cgImage, + let decodedRefImage = refImage.decoded(on: context, scale: scale) else + { + assertionFailure("[Kingfisher] Decoding only works for CG-based image.") + return base + } + return KingfisherWrapper.image(cgImage: decodedRefImage, scale: scale, refImage: base) + } +} + +extension CGImage { + func decoded(on context: CGContext, scale: CGFloat) -> CGImage? { + let size = CGSize(width: CGFloat(self.width) / scale, height: CGFloat(self.height) / scale) + context.draw(self, in: CGRect(origin: .zero, size: size)) + guard let decodedImageRef = context.makeImage() else { + return nil + } + return decodedImageRef + } +} + +extension KingfisherWrapper where Base: KFCrossPlatformImage { + func draw( + to size: CGSize, + inverting: Bool, + scale: CGFloat? = nil, + refImage: KFCrossPlatformImage? = nil, + draw: (CGContext) -> Bool // Whether use the refImage (`true`) or ignore image orientation (`false`) + ) -> KFCrossPlatformImage + { + #if os(macOS) || os(watchOS) + let targetScale = scale ?? self.scale + GraphicsContext.begin(size: size, scale: targetScale) + guard let context = GraphicsContext.current(size: size, scale: targetScale, inverting: inverting, cgImage: cgImage) else { + assertionFailure("[Kingfisher] Failed to create CG context for blurring image.") + return base + } + defer { GraphicsContext.end() } + let useRefImage = draw(context) + guard let cgImage = context.makeImage() else { + return base + } + let ref = useRefImage ? (refImage ?? base) : nil + return KingfisherWrapper.image(cgImage: cgImage, scale: targetScale, refImage: ref) + #else + + let format = UIGraphicsImageRendererFormat.preferred() + format.scale = scale ?? self.scale + let renderer = UIGraphicsImageRenderer(size: size, format: format) + + var useRefImage: Bool = false + let image = renderer.image { rendererContext in + + let context = rendererContext.cgContext + 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) + } + + useRefImage = draw(context) + } + if useRefImage { + guard let cgImage = image.cgImage else { + return base + } + let ref = refImage ?? base + return KingfisherWrapper.image(cgImage: cgImage, scale: format.scale, refImage: ref) + } else { + return image + } + #endif + } + + #if os(macOS) + func fixedForRetinaPixel(cgImage: CGImage, to size: CGSize) -> KFCrossPlatformImage { + + let image = KFCrossPlatformImage(cgImage: cgImage, size: base.size) + let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size) + + return draw(to: self.size, inverting: false) { context in + image.draw(in: rect, from: .zero, operation: .copy, fraction: 1.0) + return false + } + } + #endif +} diff --git a/Demo/Pods/Kingfisher/Sources/Image/ImageFormat.swift b/Demo/Pods/Kingfisher/Sources/Image/ImageFormat.swift new file mode 100644 index 0000000..464a855 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Image/ImageFormat.swift @@ -0,0 +1,130 @@ +// +// ImageFormat.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 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. + +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 + } + + let bytes = [UInt8](base) + let markerBytes = marker.bytes + for (index, item) in bytes.enumerated() where bytes.count > index + 1 { + guard + item == markerBytes.first, + bytes[index + 1] == markerBytes[1] else { + continue + } + return true + } + return false + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Image/ImageProcessor.swift b/Demo/Pods/Kingfisher/Sources/Image/ImageProcessor.swift new file mode 100644 index 0000000..60798a9 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Image/ImageProcessor.swift @@ -0,0 +1,920 @@ +// +// ImageProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2016/08/26. +// +// Copyright (c) 2019 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. + +import Foundation +import CoreGraphics + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +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 `data` 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(KFCrossPlatformImage) + + /// Input data. The processor should provide a way to apply + /// processing on this `data` 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: 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) -> KFCrossPlatformImage? +} + +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) -> KFCrossPlatformImage?) +struct GeneralProcessor: ImageProcessor { + let identifier: String + let p: ProcessorImp + func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + 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) -> KFCrossPlatformImage? { + 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: KFCrossPlatformColor? + + /// 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: KFCrossPlatformColor? = 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.rgbaDescription)") + } + 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) -> KFCrossPlatformImage? { + 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: KFCrossPlatformColor? + + /// 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: KFCrossPlatformColor? = 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.rgbaDescription)") + } + 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) -> KFCrossPlatformImage? { + 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 + +/// Represents a radius specified in a `RoundCornerImageProcessor`. +public enum Radius { + /// The radius should be calculated as a fraction of the image width. Typically the associated value should be + /// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image width. + case widthFraction(CGFloat) + /// The radius should be calculated as a fraction of the image height. Typically the associated value should be + /// between 0 and 0.5, where 0 represents no radius and 0.5 represents using half of the image height. + case heightFraction(CGFloat) + /// Use a fixed point value as the round corner radius. + case point(CGFloat) + + var radiusIdentifier: String { + switch self { + case .widthFraction(let f): + return "w_frac_\(f)" + case .heightFraction(let f): + return "h_frac_\(f)" + case .point(let p): + return p.description + } + } + + public func compute(with size: CGSize) -> CGFloat { + let cornerRadius: CGFloat + switch self { + case .point(let point): + cornerRadius = point + case .widthFraction(let widthFraction): + cornerRadius = size.width * widthFraction + case .heightFraction(let heightFraction): + cornerRadius = size.height * heightFraction + } + return cornerRadius + } +} + +/// 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 to disk, Kingfisher respects the original image format by default. That +/// means the alpha channel will be removed for these images. 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 + + /// The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction of the + /// target image with `.widthFraction`. or `.heightFraction`. For example, given a square image with width and + /// height equals, `.widthFraction(0.5)` means use half of the length of size and makes the final image a round one. + public let radius: Radius + + /// 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: KFCrossPlatformColor? + + /// Creates a `RoundCornerImageProcessor`. + /// + /// - Parameters: + /// - cornerRadius: Corner radius in point 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`. + /// + /// - Note: + /// + /// This initializer accepts a concrete point value for `cornerRadius`. If you do not know the image size, but still + /// want to apply a full round-corner (making the final image a round one), or specify the corner radius as a + /// fraction of one dimension of the target image, use the `Radius` version instead. + /// + public init( + cornerRadius: CGFloat, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) + { + let radius = Radius.point(cornerRadius) + self.init(radius: radius, targetSize: targetSize, roundingCorners: corners, backgroundColor: backgroundColor) + } + + /// Creates a `RoundCornerImageProcessor`. + /// + /// - Parameters: + /// - radius: The 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( + radius: Radius, + targetSize: CGSize? = nil, + roundingCorners corners: RectCorner = .all, + backgroundColor: KFCrossPlatformColor? = nil + ) + { + self.radius = radius + self.targetSize = targetSize + self.roundingCorners = corners + self.backgroundColor = backgroundColor + + self.identifier = { + var identifier = "" + + if let size = targetSize { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(radius.radiusIdentifier)_\(size)\(corners.cornerIdentifier))" + } else { + identifier = "com.onevcat.Kingfisher.RoundCornerImageProcessor" + + "(\(radius.radiusIdentifier)\(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) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + let size = targetSize ?? image.kf.size + return image.kf.scaled(to: options.scaleFactor) + .kf.image( + withRadius: radius, + fit: size, + roundingCorners: roundingCorners, + backgroundColor: backgroundColor) + case .data: + return (DefaultImageProcessor.default |> self).process(item: item, options: options) + } + } +} + +public struct Border { + public var color: KFCrossPlatformColor + public var lineWidth: CGFloat + + /// The radius will be applied in processing. Specify a certain point value with `.point`, or a fraction of the + /// target image with `.widthFraction`. or `.heightFraction`. For example, given a square image with width and + /// height equals, `.widthFraction(0.5)` means use half of the length of size and makes the final image a round one. + public var radius: Radius + + /// The target corners which will be applied rounding. + public var roundingCorners: RectCorner + + public init( + color: KFCrossPlatformColor = .black, + lineWidth: CGFloat = 4, + radius: Radius = .point(0), + roundingCorners: RectCorner = .all + ) { + self.color = color + self.lineWidth = lineWidth + self.radius = radius + self.roundingCorners = roundingCorners + } + + var identifier: String { + "\(color.rgbaDescription)_\(lineWidth)_\(radius.radiusIdentifier)_\(roundingCorners.cornerIdentifier)" + } +} + +public struct BorderImageProcessor: ImageProcessor { + public var identifier: String { "com.onevcat.Kingfisher.RoundCornerImageProcessor(\(border)" } + public let border: Border + + public init(border: Border) { + self.border = border + } + + public func process(item: ImageProcessItem, options: KingfisherParsedOptionsInfo) -> KFCrossPlatformImage? { + switch item { + case .image(let image): + return image.kf.addingBorder(border) + 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 uses 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) -> KFCrossPlatformImage? { + 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) -> KFCrossPlatformImage? { + 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: KFCrossPlatformColor + + /// 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: KFCrossPlatformColor, fraction: CGFloat = 0.5) { + self.overlay = overlay + self.fraction = fraction + self.identifier = "com.onevcat.Kingfisher.OverlayImageProcessor(\(overlay.rgbaDescription)_\(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) -> KFCrossPlatformImage? { + 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: KFCrossPlatformColor + + /// Creates a `TintImageProcessor` + /// + /// - parameter tint: Tint color will be used to tint the input image. + public init(tint: KFCrossPlatformColor) { + self.tint = tint + self.identifier = "com.onevcat.Kingfisher.TintImageProcessor(\(tint.rgbaDescription))" + } + + /// 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) -> KFCrossPlatformImage? { + 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) -> KFCrossPlatformImage? { + 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) -> KFCrossPlatformImage? { + 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) -> KFCrossPlatformImage? { + 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 downsamples the input data directly to an +/// image. It is a more efficient than `ResizingImageProcessor`. Prefer to use `DownsamplingImageProcessor` as possible +/// as you can than the `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) -> KFCrossPlatformImage? { + 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) + } + } +} + +infix operator |>: AdditionPrecedence +public func |>(left: ImageProcessor, right: ImageProcessor) -> ImageProcessor { + return left.append(another: right) +} + +extension KFCrossPlatformColor { + + var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + #if os(macOS) + (usingColorSpace(.extendedSRGB) ?? self).getRed(&r, green: &g, blue: &b, alpha: &a) + #else + getRed(&r, green: &g, blue: &b, alpha: &a) + #endif + + return (r, g, b, a) + } + + var rgbaDescription: String { + let components = self.rgba + return String(format: "(%.2f,%.2f,%.2f,%.2f)", components.r, components.g, components.b, components.a) + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Image/ImageProgressive.swift b/Demo/Pods/Kingfisher/Sources/Image/ImageProgressive.swift new file mode 100644 index 0000000..a916300 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Image/ImageProgressive.swift @@ -0,0 +1,348 @@ +// +// ImageProgressive.swift +// Kingfisher +// +// Created by lixiang on 2019/5/10. +// +// Copyright (c) 2019 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. + +import Foundation +import CoreGraphics + +private let sharedProcessingQueue: CallbackQueue = + .dispatch(DispatchQueue(label: "com.onevcat.Kingfisher.ImageDownloader.Process")) + +public struct ImageProgressive { + + /// The updating strategy when an intermediate progressive image is generated and about to be set to the hosting view. + /// + /// - default: Use the progressive image as it is. It is the standard behavior when handling the progressive image. + /// - keepCurrent: Discard this progressive image and keep the current displayed one. + /// - replace: Replace the image to a new one. If the progressive loading is initialized by a view extension in + /// Kingfisher, the replacing image will be used to update the view. + public enum UpdatingStrategy { + case `default` + case keepCurrent + case replace(KFCrossPlatformImage?) + } + + /// A default `ImageProgressive` could be used across. It blurs the progressive loading with the fastest + /// scan enabled and scan interval as 0. + @available(*, deprecated, message: "Getting a default `ImageProgressive` is deprecated due to its syntax symatic is not clear. Use `ImageProgressive.init` instead.", renamed: "init()") + 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 + + /// Called when an intermediate image is prepared and about to be set to the image view. The return value of this + /// delegate will be used to update the hosting view, if any. Otherwise, if there is no hosting view (a.k.a the + /// image retrieving is not happening from a view extension method), the returned `UpdatingStrategy` is ignored. + public let onImageUpdated = Delegate() + + /// Creates an `ImageProgressive` value with default sets. It blurs the progressive loading with the fastest + /// scan enabled and scan interval as 0. + public init() { + self.init(isBlur: true, isFastestScan: true, scanInterval: 0) + } + + /// Creates an `ImageProgressive` value the given values. + /// - Parameters: + /// - isBlur: Whether to enable blur effect processing. + /// - isFastestScan: Whether to enable the fastest scan. + /// - scanInterval: Minimum time interval for each scan. + public init(isBlur: Bool, + isFastestScan: Bool, + scanInterval: TimeInterval + ) + { + self.isBlur = isBlur + self.isFastestScan = isFastestScan + self.scanInterval = scanInterval + } +} + +final class ImageProgressiveProvider: DataReceivingSideEffect { + + var onShouldApply: () -> Bool = { return true } + + func onDataReceived(_ session: URLSession, task: SessionDataTask, data: Data) { + + DispatchQueue.main.async { + guard self.onShouldApply() else { return } + self.update(data: task.mutableData, with: task.callbacks) + } + } + + private let option: ImageProgressive + private let refresh: (KFCrossPlatformImage) -> Void + + private let decoder: ImageProgressiveDecoder + private let queue = ImageProgressiveSerialQueue() + + init?(_ options: KingfisherParsedOptionsInfo, + refresh: @escaping (KFCrossPlatformImage) -> 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 + + 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) + } + } + + let semaphore = DispatchSemaphore(value: 0) + var onShouldApply: Bool = false + + CallbackQueue.mainAsync.execute { + onShouldApply = self.onShouldApply() + semaphore.signal() + } + semaphore.wait() + guard onShouldApply else { + self.queue.clean() + completion() + return + } + + 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 (KFCrossPlatformImage?) -> 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( + 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 + private var items: [DispatchWorkItem] = [] + private var notify: (() -> Void)? + private var lastTime: TimeInterval? + + init() { + self.queue = DispatchQueue(label: "com.onevcat.Kingfisher.ImageProgressive.SerialQueue") + } + + 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 clean() { + queue.async { [weak self] in + guard let self = self else { return } + self.items.forEach { $0.cancel() } + self.items.removeAll() + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Image/ImageTransition.swift b/Demo/Pods/Kingfisher/Sources/Image/ImageTransition.swift new file mode 100644 index 0000000..4d042df --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Image/ImageTransition.swift @@ -0,0 +1,118 @@ +// +// ImageTransition.swift +// Kingfisher +// +// Created by Wei Wang on 15/9/18. +// +// Copyright (c) 2019 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. + +import Foundation +#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 + /// This is a placeholder on macOS now. It is for SwiftUI (KFImage) to identify the fade option only. + case fade(TimeInterval) +} +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Image/Placeholder.swift b/Demo/Pods/Kingfisher/Sources/Image/Placeholder.swift new file mode 100644 index 0000000..94d9e3a --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Image/Placeholder.swift @@ -0,0 +1,82 @@ +// +// Placeholder.swift +// Kingfisher +// +// Created by Tieme van Veen on 28/08/2017. +// +// Copyright (c) 2019 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. + +#if !os(watchOS) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +import AppKit +#endif + +#if canImport(UIKit) +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: KFCrossPlatformImageView) + + /// How the placeholder should be removed from a given image view. + func remove(from imageView: KFCrossPlatformImageView) +} + +/// Default implementation of an image placeholder. The image will be set or +/// reset directly for `image` property of the image view. +extension KFCrossPlatformImage: Placeholder { + /// How the placeholder should be added to a given image view. + public func add(to imageView: KFCrossPlatformImageView) { imageView.image = self } + + /// How the placeholder should be removed from a given image view. + public func remove(from imageView: KFCrossPlatformImageView) { 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: KFCrossPlatformView { + + /// How the placeholder should be added to a given image view. + public func add(to imageView: KFCrossPlatformImageView) { + 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: KFCrossPlatformImageView) { + removeFromSuperview() + } +} + +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift b/Demo/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift new file mode 100644 index 0000000..d3b3ea1 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Networking/AuthenticationChallengeResponsable.swift @@ -0,0 +1,94 @@ +// +// AuthenticationChallengeResponsable.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 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. + +import Foundation + +@available(*, deprecated, message: "Typo. Use `AuthenticationChallengeResponsible` instead", renamed: "AuthenticationChallengeResponsible") +public typealias AuthenticationChallengeResponsable = AuthenticationChallengeResponsible + +/// Protocol indicates that an authentication challenge could be handled. +public protocol AuthenticationChallengeResponsible: 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 AuthenticationChallengeResponsible { + + 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/Demo/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift b/Demo/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift new file mode 100644 index 0000000..b368972 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Networking/ImageDataProcessor.swift @@ -0,0 +1,74 @@ +// +// ImageDataProcessor.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 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. + +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, 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: KFCrossPlatformImage]() + 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 + if let image = image { + let finalImage = callback.options.backgroundDecode ? image.kf.decoded : image + 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/Demo/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift b/Demo/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift new file mode 100644 index 0000000..2ab9a5f --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Networking/ImageDownloader.swift @@ -0,0 +1,488 @@ +// +// ImageDownloader.swift +// Kingfisher +// +// Created by Wei Wang on 15/4/6. +// +// Copyright (c) 2019 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. + +#if os(macOS) +import AppKit +#else +import UIKit +#endif + +typealias DownloadResult = Result + +/// Represents a success result of an image downloading progress. +public struct ImageLoadingResult { + + /// The downloaded image. + public let image: KFCrossPlatformImage + + /// 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? + + /// 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) + } + } + open var sessionDelegate: SessionDelegate { + didSet { + session.invalidateAndCancel() + session = URLSession(configuration: sessionConfiguration, delegate: sessionDelegate, delegateQueue: nil) + setupSessionHandler() + } + } + + /// 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: AuthenticationChallengeResponsible? + + private let name: String + 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 + return (self.delegate ?? self).imageDownloader(self, didDownload: task.mutableData, with: task) + } + } + + // Wraps `completionHandler` to `onCompleted` respectively. + private func createCompletionCallBack(_ completionHandler: ((DownloadResult) -> Void)?) -> Delegate? { + return completionHandler.map { block -> Delegate in + + let delegate = Delegate, Void>() + delegate.delegate(on: self) { (self, callback) in + block(callback) + } + return delegate + } + } + + private func createTaskCallback( + _ completionHandler: ((DownloadResult) -> Void)?, + options: KingfisherParsedOptionsInfo + ) -> SessionDataTask.TaskCallback + { + return SessionDataTask.TaskCallback( + onCompleted: createCompletionCallBack(completionHandler), + options: options + ) + } + + private func createDownloadContext( + with url: URL, + options: KingfisherParsedOptionsInfo, + done: @escaping ((Result) -> Void) + ) + { + func checkRequestAndDone(r: URLRequest) { + + // There is a possibility that request modifier changed the url to `nil` or empty. + // In this case, throw an error. + guard let url = r.url, !url.absoluteString.isEmpty else { + done(.failure(KingfisherError.requestError(reason: .invalidURL(request: r)))) + return + } + + done(.success(DownloadingContext(url: url, request: r, options: options))) + } + + // Creates default request. + var request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: downloadTimeout) + request.httpShouldUsePipelining = requestsUsePipelining + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) , options.lowDataModeSource != nil { + request.allowsConstrainedNetworkAccess = false + } + + if let requestModifier = options.requestModifier { + // Modifies request before sending. + requestModifier.modified(for: request) { result in + guard let finalRequest = result else { + done(.failure(KingfisherError.requestError(reason: .emptyRequest))) + return + } + checkRequestAndDone(r: finalRequest) + } + } else { + checkRequestAndDone(r: request) + } + } + + private func addDownloadTask( + context: DownloadingContext, + callback: SessionDataTask.TaskCallback + ) -> DownloadTask + { + // Ready to start download. Add it to session task manager (`sessionHandler`) + let downloadTask: DownloadTask + if let existingTask = sessionDelegate.task(for: context.url) { + downloadTask = sessionDelegate.append(existingTask, callback: callback) + } else { + let sessionDataTask = session.dataTask(with: context.request) + sessionDataTask.priority = context.options.downloadPriority + downloadTask = sessionDelegate.add(sessionDataTask, url: context.url, callback: callback) + } + return downloadTask + } + + + private func reportWillDownloadImage(url: URL, request: URLRequest) { + delegate?.imageDownloader(self, willDownloadImageForURL: url, with: request) + } + + private func reportDidDownloadImageData(result: Result<(Data, URLResponse?), KingfisherError>, url: URL) { + var response: URLResponse? + var err: Error? + do { + response = try result.get().1 + } catch { + err = error + } + self.delegate?.imageDownloader( + self, + didFinishDownloadingImageForURL: url, + with: response, + error: err + ) + } + + private func reportDidProcessImage( + result: Result, url: URL, response: URLResponse? + ) + { + if let image = try? result.get() { + self.delegate?.imageDownloader(self, didDownload: image, for: url, with: response) + } + + } + + private func startDownloadTask( + context: DownloadingContext, + callback: SessionDataTask.TaskCallback + ) -> DownloadTask + { + + let downloadTask = addDownloadTask(context: context, callback: callback) + + let sessionTask = downloadTask.sessionTask + guard !sessionTask.started else { + return downloadTask + } + + 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. + self.reportDidDownloadImageData(result: result, url: context.url) + + switch result { + // Download finished. Now process the data to an image. + case .success(let (data, response)): + let processor = ImageDataProcessor( + data: data, callbacks: callbacks, processingQueue: context.options.processingQueue + ) + processor.onImageProcessed.delegate(on: self) { (self, done) in + // `onImageProcessed` will be called for `callbacks.count` times, with each + // `SessionDataTask.TaskCallback` as the input parameter. + // result: Result, callback: SessionDataTask.TaskCallback + let (result, callback) = done + + self.reportDidProcessImage(result: result, url: context.url, response: response) + + let imageResult = result.map { ImageLoadingResult(image: $0, url: context.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)) } + } + } + } + + reportWillDownloadImage(url: context.url, request: context.request) + sessionTask.resume() + return downloadTask + } + + // MARK: Downloading Task + /// Downloads an image with a URL and option. Invoked internally by Kingfisher. Subclasses must invoke super. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. + /// - 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: KingfisherParsedOptionsInfo, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + var downloadTask: DownloadTask? + createDownloadContext(with: url, options: options) { result in + switch result { + case .success(let context): + // `downloadTask` will be set if the downloading started immediately. This is the case when no request + // modifier or a sync modifier (`ImageDownloadRequestModifier`) is used. Otherwise, when an + // `AsyncImageDownloadRequestModifier` is used the returned `downloadTask` of this method will be `nil` + // and the actual "delayed" task is given in `AsyncImageDownloadRequestModifier.onDownloadTaskStarted` + // callback. + downloadTask = self.startDownloadTask( + context: context, + callback: self.createTaskCallback(completionHandler, options: options) + ) + if let modifier = options.requestModifier { + modifier.onDownloadTaskStarted?(downloadTask) + } + case .failure(let error): + options.callbackQueue.execute { + completionHandler?(.failure(error)) + } + } + } + + return downloadTask + } + + /// 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) -> 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) + } + + /// Downloads an image with a URL and option. + /// + /// - Parameters: + /// - url: Target URL. + /// - options: The options could control download behavior. See `KingfisherOptionsInfo`. + /// - 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, + completionHandler: ((Result) -> Void)? = nil) -> DownloadTask? + { + downloadImage( + with: url, + options: KingfisherParsedOptionsInfo(options), + 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 `AuthenticationChallengeResponsible`. +extension ImageDownloader: AuthenticationChallengeResponsible {} + +// Use the default implementation from extension of `ImageDownloaderDelegate`. +extension ImageDownloader: ImageDownloaderDelegate {} + +extension ImageDownloader { + struct DownloadingContext { + let url: URL + let request: URLRequest + let options: KingfisherParsedOptionsInfo + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift b/Demo/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift new file mode 100644 index 0000000..7f98640 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Networking/ImageDownloaderDelegate.swift @@ -0,0 +1,154 @@ +// +// ImageDownloaderDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/10/11. +// +// Copyright (c) 2019 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. + +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. + /// - dataTask: The data task contains request and response information of the download. + /// - 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. + /// + /// If this method is implemented, `imageDownloader(_:didDownload:for:)` will not be called anymore. + func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, with dataTask: SessionDataTask) -> Data? + + /// 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. + /// + /// If `imageDownloader(_:didDownload:with:)` is implemented, this method will not be called anymore. + 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: KFCrossPlatformImage, + 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: KFCrossPlatformImage, + 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, with task: SessionDataTask) -> Data? { + guard let url = task.originalURL else { + return data + } + return imageDownloader(downloader, didDownload: data, for: url) + } + + public func imageDownloader(_ downloader: ImageDownloader, didDownload data: Data, for url: URL) -> Data? { + return data + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Networking/ImageModifier.swift b/Demo/Pods/Kingfisher/Sources/Networking/ImageModifier.swift new file mode 100644 index 0000000..0acd0e8 --- /dev/null +++ b/Demo/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 +// +// 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 between cache serialization and the actual use of +/// the image. The `modify(_:)` method will be called after the image retrieved from its source and before it returned +/// to the caller. This modified image is expected to be only used for rendering purpose, any changes applied by the +/// `ImageModifier` will not be serialized or cached. +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: KFCrossPlatformImage) -> KFCrossPlatformImage +} + +/// 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: (KFCrossPlatformImage) throws -> KFCrossPlatformImage + + /// Creates an `AnyImageModifier` with a given `modify` block. + public init(modify: @escaping (KFCrossPlatformImage) throws -> KFCrossPlatformImage) { + block = modify + } + + /// Modify an input `Image`. See `ImageModifier` protocol for more. + public func modify(_ image: KFCrossPlatformImage) -> KFCrossPlatformImage { + 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: KFCrossPlatformImage) -> KFCrossPlatformImage { + 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: KFCrossPlatformImage) -> KFCrossPlatformImage { + 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: KFCrossPlatformImage) -> KFCrossPlatformImage { + return image.withAlignmentRectInsets(alignmentInsets) + } +} +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift b/Demo/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift new file mode 100644 index 0000000..3fce14c --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Networking/ImagePrefetcher.swift @@ -0,0 +1,442 @@ +// +// ImagePrefetcher.swift +// Kingfisher +// +// Created by Claire Knight on 24/02/2016 +// +// Copyright (c) 2019 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. + + +#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 + 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 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. + /// - 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, + completionHandler: PrefetcherCompletionHandler? = nil) + { + let resources: [Resource] = urls.map { $0 } + self.init( + resources: resources, + options: options, + progressBlock: nil, + 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 { $0.convertToSource() }, options: options) + self.progressBlock = progressBlock + self.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. + /// - 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, + completionHandler: PrefetcherCompletionHandler? = nil) + { + self.init(sources: resources.map { $0.convertToSource() }, options: options) + 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 + } + + /// 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. + /// - 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, + completionHandler: PrefetcherSourceCompletionHandler? = nil) + { + self.init(sources: sources, options: options) + 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) -> 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 { + let context = RetrievingContext( + options: optionsInfo, originalSource: source + ) + downloadTask = manager.loadAndCacheImage( + source: source, + context: context, + 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 { + let context = RetrievingContext(options: optionsInfo, originalSource: source) + _ = manager.retrieveImageFromCache( + source: source, + context: context) + { + _ 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/Demo/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift b/Demo/Pods/Kingfisher/Sources/Networking/RedirectHandler.swift new file mode 100644 index 0000000..0d13cbe --- /dev/null +++ b/Demo/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 +// +// 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 a redirect request modify block. +public struct AnyRedirectHandler: ImageDownloadRedirectHandler { + + let block: (SessionDataTask, HTTPURLResponse, URLRequest, @escaping (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, @escaping (URLRequest?) -> Void) -> Void) { + block = handle + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Networking/RequestModifier.swift b/Demo/Pods/Kingfisher/Sources/Networking/RequestModifier.swift new file mode 100644 index 0000000..a1d972d --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Networking/RequestModifier.swift @@ -0,0 +1,108 @@ +// +// RequestModifier.swift +// Kingfisher +// +// Created by Wei Wang on 2016/09/05. +// +// Copyright (c) 2019 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. + +import Foundation + +/// Represents and wraps a method for modifying request before an image download request starts in an asynchronous way. +public protocol AsyncImageDownloadRequestModifier { + + /// This 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. + /// When you have done with the modification, call the `reportModified` block with the modified request and the data + /// download will happen with this request. + /// + /// Usually, you pass an `AsyncImageDownloadRequestModifier` 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. + /// + /// - Parameters: + /// - request: The input request contains necessary information like `url`. This request is generated + /// according to your resource url as a GET request. + /// - reportModified: The callback block you need to call after the asynchronous modifying done. + /// + func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) + + /// A block will be called when the download task started. + /// + /// If an `AsyncImageDownloadRequestModifier` and the asynchronous modification happens before the download, the + /// related download method will not return a valid `DownloadTask` value. Instead, you can get one from this method. + var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { get } +} + +/// Represents and wraps a method for modifying request before an image download request starts. +public protocol ImageDownloadRequestModifier: AsyncImageDownloadRequestModifier { + + /// This 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? +} + +extension ImageDownloadRequestModifier { + public func modified(for request: URLRequest, reportModified: @escaping (URLRequest?) -> Void) { + let request = modified(for: request) + reportModified(request) + } + + /// This is `nil` for a sync `ImageDownloadRequestModifier` by default. You can get the `DownloadTask` from the + /// return value of downloader method. + public var onDownloadTaskStarted: ((DownloadTask?) -> Void)? { return nil } +} + +/// 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/Demo/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift b/Demo/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift new file mode 100644 index 0000000..1ab5a2e --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Networking/RetryStrategy.swift @@ -0,0 +1,153 @@ +// +// RetryStrategy.swift +// Kingfisher +// +// Created by onevcat on 2020/05/04. +// +// Copyright (c) 2020 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. + +import Foundation + +/// Represents a retry context which could be used to determine the current retry status. +public class RetryContext { + + /// The source from which the target image should be retrieved. + public let source: Source + + /// The last error which caused current retry behavior. + public let error: KingfisherError + + /// The retried count before current retry happens. This value is `0` if the current retry is for the first time. + public var retriedCount: Int + + /// A user set value for passing any other information during the retry. If you choose to use `RetryDecision.retry` + /// as the retry decision for `RetryStrategy.retry(context:retryHandler:)`, the associated value of + /// `RetryDecision.retry` will be delivered to you in the next retry. + public internal(set) var userInfo: Any? = nil + + init(source: Source, error: KingfisherError) { + self.source = source + self.error = error + self.retriedCount = 0 + } + + @discardableResult + func increaseRetryCount() -> RetryContext { + retriedCount += 1 + return self + } +} + +/// Represents decision of behavior on the current retry. +public enum RetryDecision { + /// A retry should happen. The associated `userInfo` will be pass to the next retry in the `RetryContext` parameter. + case retry(userInfo: Any?) + /// There should be no more retry attempt. The image retrieving process will fail with an error. + case stop +} + +/// Defines a retry strategy can be applied to a `.retryStrategy` option. +public protocol RetryStrategy { + + /// Kingfisher calls this method if an error happens during the image retrieving process from a `KingfisherManager`. + /// You implement this method to provide necessary logic based on the `context` parameter. Then you need to call + /// `retryHandler` to pass the retry decision back to Kingfisher. + /// + /// - Parameters: + /// - context: The retry context containing information of current retry attempt. + /// - retryHandler: A block you need to call with a decision of whether the retry should happen or not. + func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) +} + +/// A retry strategy that guides Kingfisher to retry when a `.responseError` happens, with a specified max retry count +/// and a certain interval mechanism. +public struct DelayRetryStrategy: RetryStrategy { + + /// Represents the interval mechanism which used in a `DelayRetryStrategy`. + public enum Interval { + /// The next retry attempt should happen in fixed seconds. For example, if the associated value is 3, the + /// attempts happens after 3 seconds after the previous decision is made. + case seconds(TimeInterval) + /// The next retry attempt should happen in an accumulated duration. For example, if the associated value is 3, + /// the attempts happens with interval of 3, 6, 9, 12, ... seconds. + case accumulated(TimeInterval) + /// Uses a block to determine the next interval. The current retry count is given as a parameter. + case custom(block: (_ retriedCount: Int) -> TimeInterval) + + func timeInterval(for retriedCount: Int) -> TimeInterval { + let retryAfter: TimeInterval + switch self { + case .seconds(let interval): + retryAfter = interval + case .accumulated(let interval): + retryAfter = Double(retriedCount + 1) * interval + case .custom(let block): + retryAfter = block(retriedCount) + } + return retryAfter + } + } + + /// The max retry count defined for the retry strategy + public let maxRetryCount: Int + + /// The retry interval mechanism defined for the retry strategy. + public let retryInterval: Interval + + /// Creates a delay retry strategy. + /// - Parameters: + /// - maxRetryCount: The max retry count. + /// - retryInterval: The retry interval mechanism. By default, `.seconds(3)` is used to provide a constant retry + /// interval. + public init(maxRetryCount: Int, retryInterval: Interval = .seconds(3)) { + self.maxRetryCount = maxRetryCount + self.retryInterval = retryInterval + } + + public func retry(context: RetryContext, retryHandler: @escaping (RetryDecision) -> Void) { + // Retry count exceeded. + guard context.retriedCount < maxRetryCount else { + retryHandler(.stop) + return + } + + // User cancel the task. No retry. + guard !context.error.isTaskCancelled else { + retryHandler(.stop) + return + } + + // Only retry for a response error. + guard case KingfisherError.responseError = context.error else { + retryHandler(.stop) + return + } + + let interval = retryInterval.timeInterval(for: context.retriedCount) + if interval == 0 { + retryHandler(.retry(userInfo: nil)) + } else { + DispatchQueue.main.asyncAfter(deadline: .now() + interval) { + retryHandler(.retry(userInfo: nil)) + } + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift b/Demo/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift new file mode 100644 index 0000000..8932bd5 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Networking/SessionDataTask.swift @@ -0,0 +1,127 @@ +// +// SessionDataTask.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 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. + +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, Void>? + let options: KingfisherParsedOptionsInfo + } + + /// Downloaded raw data of current task. + public private(set) var mutableData: Data + + // This is a copy of `task.originalRequest?.url`. It is for getting a race-safe behavior for a pitfall on iOS 13. + // Ref: https://github.com/onevcat/Kingfisher/issues/1511 + public let originalURL: URL? + + /// 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 + self.originalURL = task.originalRequest?.url + 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 removeAllCallbacks() -> Void { + lock.lock() + defer { lock.unlock() } + callbacksStore.removeAll() + } + + func resume() { + guard !started else { return } + started = true + task.resume() + } + + func cancel(token: CancelToken) { + guard let callback = removeCallback(token) else { + return + } + onCallbackCancelled.call((token, callback)) + } + + func forceCancel() { + for token in callbacksStore.keys { + cancel(token: token) + } + } + + func didReceiveData(_ data: Data) { + mutableData.append(data) + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift b/Demo/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift new file mode 100644 index 0000000..d2582ff --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Networking/SessionDelegate.swift @@ -0,0 +1,262 @@ +// +// SessionDelegate.swift +// Kingfisher +// +// Created by Wei Wang on 2018/11/1. +// +// Copyright (c) 2019 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. + +import Foundation + +// Represents the delegate object of downloader session. It also behave like a task manager for downloading. +@objc(KFSessionDelegate) // Fix for ObjC header name conflicting. https://github.com/onevcat/Kingfisher/issues/1530 +open 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() + let onDownloadingFinished = Delegate<(URL, Result), Void>() + let onDidDownloadData = Delegate() + + let onReceiveSessionChallenge = Delegate() + let onReceiveSessionTaskChallenge = Delegate() + + 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) { [weak task] (self, value) in + guard let task = task else { return } + + 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.cancelTask(dataTask) + self.remove(task) + } + } + let token = task.addCallback(callback) + tasks[url] = task + return DownloadTask(sessionTask: task, cancelToken: token) + } + + private func cancelTask(_ dataTask: URLSessionDataTask) { + lock.lock() + defer { lock.unlock() } + dataTask.cancel() + } + + func append( + _ task: SessionDataTask, + callback: SessionDataTask.TaskCallback) -> DownloadTask + { + let token = task.addCallback(callback) + return DownloadTask(sessionTask: task, cancelToken: token) + } + + private func remove(_ task: SessionDataTask) { + lock.lock() + defer { lock.unlock() } + + guard let url = task.originalURL else { + return + } + task.removeAllCallbacks() + tasks[url] = nil + } + + private func task(for task: URLSessionTask) -> SessionDataTask? { + lock.lock() + defer { lock.unlock() } + + guard let url = task.originalRequest?.url else { + return nil + } + 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 { + + open 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) + } + + open 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) + } + } + } + + open func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) { + guard let sessionTask = self.task(for: task) else { return } + + if let url = sessionTask.originalURL { + let result: Result + 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) { + result = .success((data, task.response)) + } else { + result = .failure(KingfisherError.responseError(reason: .dataModifyingFailed(task: sessionTask))) + } + } + onCompleted(task: task, result: result) + } + + open func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + onReceiveSessionChallenge.call((session, challenge, completionHandler)) + } + + open func urlSession( + _ session: URLSession, + task: URLSessionTask, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) + { + onReceiveSessionTaskChallenge.call((session, task, challenge, completionHandler)) + } + + open 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 + } + sessionTask.onTaskDone.call((result, sessionTask.callbacks)) + remove(sessionTask) + } +} diff --git a/Demo/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift b/Demo/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift new file mode 100644 index 0000000..6114479 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/SwiftUI/ImageBinder.swift @@ -0,0 +1,149 @@ +// +// ImageBinder.swift +// Kingfisher +// +// Created by onevcat on 2019/06/27. +// +// Copyright (c) 2019 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + + /// Represents a binder for `KFImage`. It takes responsibility as an `ObjectBinding` and performs + /// image downloading and progress reporting based on `KingfisherManager`. + class ImageBinder: ObservableObject { + + init() {} + + var downloadTask: DownloadTask? + private var loading = false + + var loadingOrSucceeded: Bool { + return loading || loadedImage != nil + } + + // Do not use @Published due to https://github.com/onevcat/Kingfisher/issues/1717. Revert to @Published once + // we can drop iOS 12. + private(set) var loaded = false + + private(set) var animating = false + + var loadedImage: KFCrossPlatformImage? = nil { willSet { objectWillChange.send() } } + var progress: Progress = .init() + + func markLoading() { + loading = true + } + + func markLoaded(sendChangeEvent: Bool) { + loaded = true + if sendChangeEvent { + objectWillChange.send() + } + } + + func start(context: Context) { + guard let source = context.source else { + CallbackQueue.mainCurrentOrAsync.execute { + context.onFailureDelegate.call(KingfisherError.imageSettingError(reason: .emptySource)) + if let image = context.options.onFailureImage { + self.loadedImage = image + } + self.loading = false + self.markLoaded(sendChangeEvent: false) + } + return + } + + loading = true + + progress = .init() + downloadTask = KingfisherManager.shared + .retrieveImage( + with: source, + options: context.options, + progressBlock: { size, total in + self.updateProgress(downloaded: size, total: total) + context.onProgressDelegate.call((size, total)) + }, + completionHandler: { [weak self] result in + + guard let self = self else { return } + + CallbackQueue.mainCurrentOrAsync.execute { + self.downloadTask = nil + self.loading = false + } + + switch result { + case .success(let value): + CallbackQueue.mainCurrentOrAsync.execute { + if let fadeDuration = context.fadeTransitionDuration(cacheType: value.cacheType) { + self.animating = true + let animation = Animation.linear(duration: fadeDuration) + withAnimation(animation) { + // Trigger the view render to apply the animation. + self.markLoaded(sendChangeEvent: true) + } + } else { + self.markLoaded(sendChangeEvent: false) + } + self.loadedImage = value.image + self.animating = false + } + + CallbackQueue.mainAsync.execute { + context.onSuccessDelegate.call(value) + } + case .failure(let error): + CallbackQueue.mainCurrentOrAsync.execute { + if let image = context.options.onFailureImage { + self.loadedImage = image + } + self.markLoaded(sendChangeEvent: true) + } + + CallbackQueue.mainAsync.execute { + context.onFailureDelegate.call(error) + } + } + }) + } + + private func updateProgress(downloaded: Int64, total: Int64) { + progress.totalUnitCount = total + progress.completedUnitCount = downloaded + objectWillChange.send() + } + + /// Cancels the download task if it is in progress. + func cancel() { + downloadTask?.cancel() + downloadTask = nil + loading = false + } + } +} +#endif diff --git a/Demo/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift b/Demo/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift new file mode 100644 index 0000000..730477b --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/SwiftUI/ImageContext.swift @@ -0,0 +1,102 @@ +// +// ImageContext.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + public class Context { + let source: Source? + var options = KingfisherParsedOptionsInfo( + KingfisherManager.shared.defaultOptions + [.loadDiskFileSynchronously] + ) + + var configurations: [(HoldingView) -> HoldingView] = [] + var renderConfigurations: [(HoldingView.RenderingView) -> Void] = [] + var contentConfiguration: ((HoldingView) -> AnyView)? = nil + + var cancelOnDisappear: Bool = false + var placeholder: ((Progress) -> AnyView)? = nil + + let onFailureDelegate = Delegate() + let onSuccessDelegate = Delegate() + let onProgressDelegate = Delegate<(Int64, Int64), Void>() + + var startLoadingBeforeViewAppear: Bool = false + + init(source: Source?) { + self.source = source + } + + func shouldApplyFade(cacheType: CacheType) -> Bool { + options.forceTransition || cacheType == .none + } + + func fadeTransitionDuration(cacheType: CacheType) -> TimeInterval? { + shouldApplyFade(cacheType: cacheType) + ? options.transition.fadeDuration + : nil + } + } +} + +extension ImageTransition { + // Only for fade effect in SwiftUI. + fileprivate var fadeDuration: TimeInterval? { + switch self { + case .fade(let duration): + return duration + default: + return nil + } + } +} + + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage.Context: Hashable { + public static func == (lhs: KFImage.Context, rhs: KFImage.Context) -> Bool { + lhs.source == rhs.source && + lhs.options.processor.identifier == rhs.options.processor.identifier + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(source) + hasher.combine(options.processor.identifier) + } +} + +#if canImport(UIKit) && !os(watchOS) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFAnimatedImage { + public typealias Context = KFImage.Context + typealias ImageBinder = KFImage.ImageBinder +} +#endif + +#endif diff --git a/Demo/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift new file mode 100644 index 0000000..ad25eb2 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFAnimatedImage.swift @@ -0,0 +1,96 @@ +// +// KFAnimatedImage.swift +// Kingfisher +// +// Created by wangxingbin on 2021/4/29. +// +// Copyright (c) 2021 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. + +#if canImport(SwiftUI) && canImport(Combine) && canImport(UIKit) && !os(watchOS) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFAnimatedImage: KFImageProtocol { + public typealias HoldingView = KFAnimatedImageViewRepresenter + public var context: Context + public init(context: KFImage.Context) { + self.context = context + } + + /// Configures current rendering view with a `block`. This block will be applied when the under-hood + /// `AnimatedImageView` is created in `UIViewRepresentable.makeUIView(context:)` + /// + /// - Parameter block: The block applies to the animated image view. + /// - Returns: A `KFAnimatedImage` view that being configured by the `block`. + public func configure(_ block: @escaping (HoldingView.RenderingView) -> Void) -> Self { + context.renderConfigurations.append(block) + return self + } +} + +/// A wrapped `UIViewRepresentable` of `AnimatedImageView` +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFAnimatedImageViewRepresenter: UIViewRepresentable, KFImageHoldingView { + public typealias RenderingView = AnimatedImageView + public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> KFAnimatedImageViewRepresenter { + KFAnimatedImageViewRepresenter(image: image, context: context) + } + + var image: KFCrossPlatformImage? + let context: KFImage.Context + + public func makeUIView(context: Context) -> AnimatedImageView { + let view = AnimatedImageView() + + self.context.renderConfigurations.forEach { $0(view) } + + view.image = image + + // Allow SwiftUI scale (fit/fill) working fine. + view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) + view.setContentCompressionResistancePriority(.defaultLow, for: .vertical) + return view + } + + public func updateUIView(_ uiView: AnimatedImageView, context: Context) { + uiView.image = image + } +} + +#if DEBUG +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFAnimatedImage_Previews: PreviewProvider { + static var previews: some View { + Group { + KFAnimatedImage(source: .network(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher-TestImages/master/DemoAppImage/GIF/1.gif")!)) + .onSuccess { r in + print(r) + } + .placeholder { + ProgressView() + } + .padding() + } + } +} +#endif +#endif diff --git a/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift new file mode 100644 index 0000000..67f1c19 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImage.swift @@ -0,0 +1,106 @@ +// +// KFImage.swift +// Kingfisher +// +// Created by onevcat on 2019/06/26. +// +// Copyright (c) 2019 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public struct KFImage: KFImageProtocol { + public var context: Context + public init(context: Context) { + self.context = context + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension Image: KFImageHoldingView { + public typealias RenderingView = Image + public static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Image { + Image(crossPlatformImage: image) + } +} + +// MARK: - Image compatibility. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImage { + + public func resizable( + capInsets: EdgeInsets = EdgeInsets(), + resizingMode: Image.ResizingMode = .stretch) -> KFImage + { + configure { $0.resizable(capInsets: capInsets, resizingMode: resizingMode) } + } + + public func renderingMode(_ renderingMode: Image.TemplateRenderingMode?) -> KFImage { + configure { $0.renderingMode(renderingMode) } + } + + public func interpolation(_ interpolation: Image.Interpolation) -> KFImage { + configure { $0.interpolation(interpolation) } + } + + public func antialiased(_ isAntialiased: Bool) -> KFImage { + configure { $0.antialiased(isAntialiased) } + } + + /// Starts the loading process of `self` immediately. + /// + /// By default, a `KFImage` will not load its source until the `onAppear` is called. This is a lazily loading + /// behavior and provides better performance. However, when you refresh the view, the lazy loading also causes a + /// flickering since the loading does not happen immediately. Call this method if you want to start the load at once + /// could help avoiding the flickering, with some performance trade-off. + /// + /// - Deprecated: This is not necessary anymore since `@StateObject` is used for holding the image data. + /// It does nothing now and please just remove it. + /// + /// - Returns: The `Self` value with changes applied. + @available(*, deprecated, message: "This is not necessary anymore since `@StateObject` is used. It does nothing now and please just remove it.") + public func loadImmediately(_ start: Bool = true) -> KFImage { + return self + } +} + +#if DEBUG +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFImage_Previews: PreviewProvider { + static var previews: some View { + Group { + KFImage.url(URL(string: "https://raw.githubusercontent.com/onevcat/Kingfisher/master/images/logo.png")!) + .onSuccess { r in + print(r) + } + .placeholder { p in + ProgressView(p) + } + .resizable() + .aspectRatio(contentMode: .fit) + .padding() + } + } +} +#endif +#endif diff --git a/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift new file mode 100644 index 0000000..a63d909 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageOptions.swift @@ -0,0 +1,158 @@ +// +// KFImageOptions.swift +// Kingfisher +// +// Created by onevcat on 2020/12/20. +// +// Copyright (c) 2020 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +// MARK: - KFImage creating. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + + /// Creates a `KFImage` for a given `Source`. + /// - Parameters: + /// - source: The `Source` object defines data information from network or a data provider. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func source( + _ source: Source? + ) -> Self + { + Self.init(source: source) + } + + /// Creates a `KFImage` for a given `Resource`. + /// - Parameters: + /// - source: The `Resource` object defines data information like key or URL. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func resource( + _ resource: Resource? + ) -> Self + { + source(resource?.convertToSource()) + } + + /// Creates a `KFImage` for a given `URL`. + /// - Parameters: + /// - url: The URL where the image should be downloaded. + /// - cacheKey: The key used to store the downloaded image in cache. + /// If `nil`, the `absoluteString` of `url` is used as the cache key. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func url( + _ url: URL?, cacheKey: String? = nil + ) -> Self + { + source(url?.convertToSource(overrideCacheKey: cacheKey)) + } + + /// Creates a `KFImage` for a given `ImageDataProvider`. + /// - Parameters: + /// - provider: The `ImageDataProvider` object contains information about the data. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func dataProvider( + _ provider: ImageDataProvider? + ) -> Self + { + source(provider?.convertToSource()) + } + + /// Creates a builder for some given raw data and a cache key. + /// - Parameters: + /// - data: The data object from which the image should be created. + /// - cacheKey: The key used to store the downloaded image in cache. + /// - Returns: A `KFImage` for future configuration or embedding to a `SwiftUI.View`. + public static func data( + _ data: Data?, cacheKey: String + ) -> Self + { + if let data = data { + return dataProvider(RawImageDataProvider(data: data, cacheKey: cacheKey)) + } else { + return dataProvider(nil) + } + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + /// Sets a placeholder `View` which shows when loading the image, with a progress parameter as input. + /// - Parameter content: A view that describes the placeholder. + /// - Returns: A `KFImage` view that contains `content` as its placeholder. + public func placeholder(@ViewBuilder _ content: @escaping (Progress) -> P) -> Self { + context.placeholder = { progress in + return AnyView(content(progress)) + } + return self + } + + /// Sets a placeholder `View` which shows when loading the image. + /// - Parameter content: A view that describes the placeholder. + /// - Returns: A `KFImage` view that contains `content` as its placeholder. + public func placeholder(@ViewBuilder _ content: @escaping () -> P) -> Self { + placeholder { _ in content() } + } + + /// Sets cancelling the download task bound to `self` when the view disappearing. + /// - Parameter flag: Whether cancel the task or not. + /// - Returns: A `KFImage` view that cancels downloading task when disappears. + public func cancelOnDisappear(_ flag: Bool) -> Self { + context.cancelOnDisappear = flag + return self + } + + /// Sets a fade transition for the image task. + /// - Parameter duration: The duration of the fade transition. + /// - Returns: A `KFImage` with changes applied. + /// + /// Kingfisher will use the fade transition 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, also call `forceRefresh()` on the returned `KFImage`. + public func fade(duration: TimeInterval) -> Self { + context.options.transition = .fade(duration) + return self + } + + /// Sets whether to start the image loading before the view actually appears. + /// + /// By default, Kingfisher performs a lazy loading for `KFImage`. The image loading won't start until the view's + /// `onAppear` is called. However, sometimes you may want to trigger an aggressive loading for the view. By enabling + /// this, the `KFImage` will try to load the view when its `body` is evaluated when the image loading is not yet + /// started or a previous loading did fail. + /// + /// - Parameter flag: Whether the image loading should happen before view appear. Default is `true`. + /// - Returns: A `KFImage` with changes applied. + /// + /// - Note: This is a temporary workaround for an issue from iOS 16, where the SwiftUI view's `onAppear` is not + /// called when it is deeply embedded inside a `List` or `ForEach`. + /// See [#1988](https://github.com/onevcat/Kingfisher/issues/1988). It may cause performance regression, especially + /// if you have a lot of images to load in the view. Use it as your own risk. + /// + public func startLoadingBeforeViewAppear(_ flag: Bool = true) -> Self { + context.startLoadingBeforeViewAppear = flag + return self + } +} +#endif diff --git a/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift new file mode 100644 index 0000000..5a5ad4b --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageProtocol.swift @@ -0,0 +1,112 @@ +// +// KFImageProtocol.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public protocol KFImageProtocol: View, KFOptionSetter { + associatedtype HoldingView: KFImageHoldingView + var context: KFImage.Context { get set } + init(context: KFImage.Context) +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + public var body: some View { + ZStack { + KFImageRenderer( + context: context + ).id(context) + } + } + + /// Creates a Kingfisher compatible image view to load image from the given `Source`. + /// - Parameters: + /// - source: The image `Source` defining where to load the target image. + public init(source: Source?) { + let context = KFImage.Context(source: source) + self.init(context: context) + } + + /// Creates a Kingfisher compatible image view to load image from the given `URL`. + /// - Parameters: + /// - source: The image `Source` defining where to load the target image. + public init(_ url: URL?) { + self.init(source: url?.convertToSource()) + } + + /// Configures current image with a `block` and return another `Image` to use as the final content. + /// + /// This block will be lazily applied when creating the final `Image`. + /// + /// If multiple `configure` modifiers are added to the image, they will be evaluated by order. If you want to + /// configure the input image (which is usually an `Image` value) to a non-`Image` value, use `contentConfigure`. + /// + /// - Parameter block: The block applies to loaded image. The block should return an `Image` that is configured. + /// - Returns: A `KFImage` view that configures internal `Image` with `block`. + public func configure(_ block: @escaping (HoldingView) -> HoldingView) -> Self { + context.configurations.append(block) + return self + } + + /// Configures current image with a `block` and return a `View` to use as the final content. + /// + /// This block will be lazily applied when creating the final `Image`. + /// + /// If multiple `contentConfigure` modifiers are added to the image, only the last one will be stored and used. + /// + /// - Parameter block: The block applies to the loaded image. The block should return a `View` that is configured. + /// - Returns: A `KFImage` view that configures internal `Image` with `block`. + public func contentConfigure(_ block: @escaping (HoldingView) -> V) -> Self { + context.contentConfiguration = { AnyView(block($0)) } + return self + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +public protocol KFImageHoldingView: View { + associatedtype RenderingView + static func created(from image: KFCrossPlatformImage?, context: KFImage.Context) -> Self +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension KFImageProtocol { + public var options: KingfisherParsedOptionsInfo { + get { context.options } + nonmutating set { context.options = newValue } + } + + public var onFailureDelegate: Delegate { context.onFailureDelegate } + public var onSuccessDelegate: Delegate { context.onSuccessDelegate } + public var onProgressDelegate: Delegate<(Int64, Int64), Void> { context.onProgressDelegate } + + public var delegateObserver: AnyObject { context } +} + + +#endif diff --git a/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift new file mode 100644 index 0000000..5203b4e --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/SwiftUI/KFImageRenderer.swift @@ -0,0 +1,129 @@ +// +// KFImageRenderer.swift +// Kingfisher +// +// Created by onevcat on 2021/05/08. +// +// Copyright (c) 2021 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. + +#if canImport(SwiftUI) && canImport(Combine) +import SwiftUI +import Combine + +/// A Kingfisher compatible SwiftUI `View` to load an image from a `Source`. +/// Declaring a `KFImage` in a `View`'s body to trigger loading from the given `Source`. +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +struct KFImageRenderer : View where HoldingView: KFImageHoldingView { + + @StateObject var binder: KFImage.ImageBinder = .init() + let context: KFImage.Context + + var body: some View { + if context.startLoadingBeforeViewAppear && !binder.loadingOrSucceeded && !binder.animating { + binder.markLoading() + DispatchQueue.main.async { binder.start(context: context) } + } + + return ZStack { + renderedImage().opacity(binder.loaded ? 1.0 : 0.0) + if binder.loadedImage == nil { + ZStack { + if let placeholder = context.placeholder { + placeholder(binder.progress) + } else { + Color.clear + } + } + .onAppear { [weak binder = self.binder] in + guard let binder = binder else { + return + } + if !binder.loadingOrSucceeded { + binder.start(context: context) + } + } + .onDisappear { [weak binder = self.binder] in + guard let binder = binder else { + return + } + if context.cancelOnDisappear { + binder.cancel() + } + } + } + } + // Workaround for https://github.com/onevcat/Kingfisher/issues/1988 + // on iOS 16 there seems to be a bug that when in a List, the `onAppear` of the `ZStack` above in the + // `binder.loadedImage == nil` not get called. Adding this empty `onAppear` fixes it and the life cycle can + // work again. + // + // There is another "fix": adding an `else` clause and put a `Color.clear` there. But I believe this `onAppear` + // should work better. + // + // It should be a bug in iOS 16, I guess it is some kinds of over-optimization in list cell loading caused it. + .onAppear() + } + + @ViewBuilder + private func renderedImage() -> some View { + let configuredImage = context.configurations + .reduce(HoldingView.created(from: binder.loadedImage, context: context)) { + current, config in config(current) + } + if let contentConfiguration = context.contentConfiguration { + contentConfiguration(configuredImage) + } else { + configuredImage + } + } +} + +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension Image { + // Creates an Image with either UIImage or NSImage. + init(crossPlatformImage: KFCrossPlatformImage?) { + #if canImport(UIKit) + self.init(uiImage: crossPlatformImage ?? KFCrossPlatformImage()) + #elseif canImport(AppKit) + self.init(nsImage: crossPlatformImage ?? KFCrossPlatformImage()) + #endif + } +} + +#if canImport(UIKit) +@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *) +extension UIImage.Orientation { + func toSwiftUI() -> Image.Orientation { + switch self { + case .down: return .down + case .up: return .up + case .left: return .left + case .right: return .right + case .upMirrored: return .upMirrored + case .downMirrored: return .downMirrored + case .leftMirrored: return .leftMirrored + case .rightMirrored: return .rightMirrored + @unknown default: return .up + } + } +} +#endif +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Utility/Box.swift b/Demo/Pods/Kingfisher/Sources/Utility/Box.swift new file mode 100644 index 0000000..0303a6e --- /dev/null +++ b/Demo/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 +// +// 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 { + var value: T + + init(_ value: T) { + self.value = value + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift b/Demo/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift new file mode 100644 index 0000000..822af28 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Utility/CallbackQueue.swift @@ -0,0 +1,83 @@ +// +// CallbackQueue.swift +// Kingfisher +// +// Created by onevcat on 2018/10/15. +// +// Copyright (c) 2019 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. + +import Foundation + +public typealias ExecutionQueue = CallbackQueue + +/// 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 () -> Void) { + if self === DispatchQueue.main && Thread.isMainThread { + block() + } else { + async { block() } + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Utility/Delegate.swift b/Demo/Pods/Kingfisher/Sources/Utility/Delegate.swift new file mode 100644 index 0000000..9caa1a6 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Utility/Delegate.swift @@ -0,0 +1,132 @@ +// +// Delegate.swift +// Kingfisher +// +// Created by onevcat on 2018/10/10. +// +// Copyright (c) 2019 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. + +import Foundation +/// A class that keeps a weakly reference for `self` when implementing `onXXX` behaviors. +/// Instead of remembering to keep `self` as weak in a stored closure: +/// +/// ```swift +/// // MyClass.swift +/// var onDone: (() -> Void)? +/// func done() { +/// onDone?() +/// } +/// +/// // ViewController.swift +/// var obj: MyClass? +/// +/// func doSomething() { +/// obj = MyClass() +/// obj!.onDone = { [weak self] in +/// self?.reportDone() +/// } +/// } +/// ``` +/// +/// You can create a `Delegate` and observe on `self`. Now, there is no retain cycle inside: +/// +/// ```swift +/// // MyClass.swift +/// let onDone = Delegate<(), Void>() +/// func done() { +/// onDone.call() +/// } +/// +/// // ViewController.swift +/// var obj: MyClass? +/// +/// func doSomething() { +/// obj = MyClass() +/// obj!.onDone.delegate(on: self) { (self, _) +/// // `self` here is shadowed and does not keep a strong ref. +/// // So you can release both `MyClass` instance and `ViewController` instance. +/// self.reportDone() +/// } +/// } +/// ``` +/// +public class Delegate { + public init() {} + + private var block: ((Input) -> Output?)? + public func delegate(on target: T, block: ((T, Input) -> Output)?) { + self.block = { [weak target] input in + guard let target = target else { return nil } + return block?(target, input) + } + } + + public func call(_ input: Input) -> Output? { + return block?(input) + } + + public func callAsFunction(_ input: Input) -> Output? { + return call(input) + } +} + +extension Delegate where Input == Void { + public func call() -> Output? { + return call(()) + } + + public func callAsFunction() -> Output? { + return call() + } +} + +extension Delegate where Input == Void, Output: OptionalProtocol { + public func call() -> Output { + return call(()) + } + + public func callAsFunction() -> Output { + return call() + } +} + +extension Delegate where Output: OptionalProtocol { + public func call(_ input: Input) -> Output { + if let result = block?(input) { + return result + } else { + return Output._createNil + } + } + + public func callAsFunction(_ input: Input) -> Output { + return call(input) + } +} + +public protocol OptionalProtocol { + static var _createNil: Self { get } +} +extension Optional : OptionalProtocol { + public static var _createNil: Optional { + return nil + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift b/Demo/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift new file mode 100644 index 0000000..22b2063 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Utility/ExtensionHelpers.swift @@ -0,0 +1,117 @@ +// +// ExtensionHelpers.swift +// Kingfisher +// +// Created by onevcat on 2018/09/28. +// +// Copyright (c) 2019 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. + +import Foundation + +extension CGFloat { + var isEven: Bool { + return truncatingRemainder(dividingBy: 2.0) == 0 + } +} + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +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 KFCrossPlatformImage { + // 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()) + } + + func isPast(referenceDate: Date) -> Bool { + return timeIntervalSince(referenceDate) <= 0 + } + + // `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/Demo/Pods/Kingfisher/Sources/Utility/Result.swift b/Demo/Pods/Kingfisher/Sources/Utility/Result.swift new file mode 100644 index 0000000..dcdb08b --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Utility/Result.swift @@ -0,0 +1,50 @@ +// +// Result.swift +// Kingfisher +// +// Created by onevcat on 2018/09/22. +// +// Copyright (c) 2019 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. + +import Foundation + +// 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( + onSuccess: (Success) -> Output, + onFailure: (Failure) -> Output) -> Output + { + switch self { + case let .success(value): + return onSuccess(value) + case let .failure(error): + return onFailure(error) + } + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Utility/Runtime.swift b/Demo/Pods/Kingfisher/Sources/Utility/Runtime.swift new file mode 100644 index 0000000..d5818e2 --- /dev/null +++ b/Demo/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 +// +// 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(_ object: Any, _ key: UnsafeRawPointer) -> T? { + return objc_getAssociatedObject(object, key) as? T +} + +func setRetainedAssociatedObject(_ object: Any, _ key: UnsafeRawPointer, _ value: T) { + objc_setAssociatedObject(object, key, value, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) +} diff --git a/Demo/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift b/Demo/Pods/Kingfisher/Sources/Utility/SizeExtensions.swift new file mode 100644 index 0000000..19d05d6 --- /dev/null +++ b/Demo/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 +// +// 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 { + return min(max(self, limits.lowerBound), limits.upperBound) + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Utility/String+MD5.swift b/Demo/Pods/Kingfisher/Sources/Utility/String+MD5.swift new file mode 100644 index 0000000..59586b5 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Utility/String+MD5.swift @@ -0,0 +1,278 @@ +// +// String+MD5.swift +// Kingfisher +// +// Created by Wei Wang on 18/09/25. +// +// Copyright (c) 2019 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. + +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 + } + + let message = data.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) in + return [UInt8](bytes) + } + + let MD5Calculator = MD5(message) + let MD5Data = MD5Calculator.calculate() + + var MD5String = String() + for c in MD5Data { + MD5String += String(format: "%02x", c) + } + return MD5String + } + + var ext: String? { + var ext = "" + if let index = base.lastIndex(of: ".") { + let extRange = base.index(index, offsetBy: 1).. 0 ? String(firstSeg) : nil + } +} + +// array of bytes, little-endian representation +func arrayOfBytes(_ value: T, length: Int? = nil) -> [UInt8] { + let totalBytes = length ?? (MemoryLayout.size * 8) + + let valuePointer = UnsafeMutablePointer.allocate(capacity: 1) + valuePointer.pointee = value + + let bytes = valuePointer.withMemoryRebound(to: UInt8.self, capacity: totalBytes) { (bytesPointer) -> [UInt8] in + var bytes = [UInt8](repeating: 0, count: totalBytes) + for j in 0...size, totalBytes) { + bytes[totalBytes - 1 - j] = (bytesPointer + j).pointee + } + return bytes + } + + valuePointer.deinitialize(count: 1) + valuePointer.deallocate() + + return bytes +} + +extension Int { + // Array of bytes with optional padding (little-endian) + func bytes(_ totalBytes: Int = MemoryLayout.size) -> [UInt8] { + return arrayOfBytes(self, length: totalBytes) + } + +} + +protocol HashProtocol { + var message: [UInt8] { get } + // Common part for hash calculation. Prepare header data. + func prepare(_ len: Int) -> [UInt8] +} + +extension HashProtocol { + + func prepare(_ len: Int) -> [UInt8] { + var tmpMessage = message + + // Step 1. Append Padding Bits + tmpMessage.append(0x80) // append one bit (UInt8 with one bit) to message + + // append "0" bit until message length in bits ≡ 448 (mod 512) + var msgLength = tmpMessage.count + var counter = 0 + + while msgLength % len != (len - 8) { + counter += 1 + msgLength += 1 + } + + tmpMessage += [UInt8](repeating: 0, count: counter) + return tmpMessage + } +} + +func toUInt32Array(_ slice: ArraySlice) -> [UInt32] { + var result = [UInt32]() + result.reserveCapacity(16) + + for idx in stride(from: slice.startIndex, to: slice.endIndex, by: MemoryLayout.size) { + let d0 = UInt32(slice[idx.advanced(by: 3)]) << 24 + let d1 = UInt32(slice[idx.advanced(by: 2)]) << 16 + let d2 = UInt32(slice[idx.advanced(by: 1)]) << 8 + let d3 = UInt32(slice[idx]) + let val: UInt32 = d0 | d1 | d2 | d3 + + result.append(val) + } + return result +} + +struct BytesIterator: IteratorProtocol { + + let chunkSize: Int + let data: [UInt8] + + init(chunkSize: Int, data: [UInt8]) { + self.chunkSize = chunkSize + self.data = data + } + + var offset = 0 + + mutating func next() -> ArraySlice? { + let end = min(chunkSize, data.count - offset) + let result = data[offset.. 0 ? result : nil + } +} + +struct BytesSequence: Sequence { + let chunkSize: Int + let data: [UInt8] + + func makeIterator() -> BytesIterator { + return BytesIterator(chunkSize: chunkSize, data: data) + } +} + +func rotateLeft(_ value: UInt32, bits: UInt32) -> UInt32 { + return ((value << bits) & 0xFFFFFFFF) | (value >> (32 - bits)) +} + +class MD5: HashProtocol { + + let message: [UInt8] + + init (_ message: [UInt8]) { + self.message = message + } + + // specifies the per-round shift amounts + private let shifts: [UInt32] = [7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, + 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, + 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, + 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21] + + // binary integer part of the sines of integers (Radians) + private let sines: [UInt32] = [0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee, + 0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501, + 0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be, + 0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821, + 0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa, + 0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8, + 0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed, + 0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a, + 0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c, + 0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70, + 0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x4881d05, + 0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665, + 0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039, + 0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1, + 0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1, + 0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391] + + private let hashes: [UInt32] = [0x67452301, 0xefcdab89, 0x98badcfe, 0x10325476] + + func calculate() -> [UInt8] { + var tmpMessage = prepare(64) + tmpMessage.reserveCapacity(tmpMessage.count + 4) + + // hash values + var hh = hashes + + // Step 2. Append Length a 64-bit representation of lengthInBits + let lengthInBits = (message.count * 8) + let lengthBytes = lengthInBits.bytes(64 / 8) + tmpMessage += lengthBytes.reversed() + + // Process the message in successive 512-bit chunks: + let chunkSizeBytes = 512 / 8 // 64 + + for chunk in BytesSequence(chunkSize: chunkSizeBytes, data: tmpMessage) { + // break chunk into sixteen 32-bit words M[j], 0 ≤ j ≤ 15 + let M = toUInt32Array(chunk) + assert(M.count == 16, "Invalid array") + + // Initialize hash value for this chunk: + var A: UInt32 = hh[0] + var B: UInt32 = hh[1] + var C: UInt32 = hh[2] + var D: UInt32 = hh[3] + + var dTemp: UInt32 = 0 + + // Main loop + for j in 0 ..< sines.count { + var g = 0 + var F: UInt32 = 0 + + switch j { + case 0...15: + F = (B & C) | ((~B) & D) + g = j + case 16...31: + F = (D & B) | (~D & C) + g = (5 * j + 1) % 16 + case 32...47: + F = B ^ C ^ D + g = (3 * j + 5) % 16 + case 48...63: + F = C ^ (B | (~D)) + g = (7 * j) % 16 + default: + break + } + dTemp = D + D = C + C = B + B = B &+ rotateLeft((A &+ F &+ sines[j] &+ M[g]), bits: shifts[j]) + A = dTemp + } + + hh[0] = hh[0] &+ A + hh[1] = hh[1] &+ B + hh[2] = hh[2] &+ C + hh[3] = hh[3] &+ D + } + var result = [UInt8]() + result.reserveCapacity(hh.count / 4) + + hh.forEach { + let itemLE = $0.littleEndian + let r1 = UInt8(itemLE & 0xff) + let r2 = UInt8((itemLE >> 8) & 0xff) + let r3 = UInt8((itemLE >> 16) & 0xff) + let r4 = UInt8((itemLE >> 24) & 0xff) + result += [r1, r2, r3, r4] + } + return result + } +} diff --git a/Demo/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift b/Demo/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift new file mode 100644 index 0000000..f679340 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Views/AnimatedImageView.swift @@ -0,0 +1,686 @@ +// +// 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. + +#if !os(watchOS) +#if canImport(UIKit) +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) {} +} + +let KFRunLoopModeCommon = RunLoop.Mode.common + +/// 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? + + /// The `Animator` instance that holds the frames of a specific image in memory. + public private(set) var animator: Animator? + + // MARK: - Private property + // 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: KFCrossPlatformImage? { + didSet { + if image != oldValue { + reset() + } + setNeedsDisplay() + layer.setNeedsDisplay() + } + } + + open override var isHighlighted: Bool { + get { + super.isHighlighted + } + set { + // Highlighted image is unsupported for animated images. + // See https://github.com/onevcat/Kingfisher/issues/1679 + if displayLink.isPaused { + super.isHighlighted = newValue + } + } + } + + 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 } + guard let animator = animator else { return } + guard !animator.isReachMaxRepeatCount else { return } + + displayLink.isPaused = false + } + + /// Stops the animation. + override open func stopAnimating() { + super.stopAnimating() + if isDisplayLinkInitialized { + displayLink.isPaused = true + } + } + + override open func display(_ layer: CALayer) { + layer.contents = animator?.currentFrameImage?.cgImage ?? 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 image = image, let imageSource = image.kf.imageSource { + let targetSize = bounds.scaled(UIScreen.main.scale).size + let animator = Animator( + imageSource: imageSource, + contentMode: contentMode, + size: targetSize, + imageSize: image.kf.size, + imageScale: image.kf.scale, + 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 + let preferredFramesPerSecond = displayLink.preferredFramesPerSecond + if preferredFramesPerSecond == 0 { + duration = displayLink.duration + } else { + // Some devices (like iPad Pro 10.5) will have a different FPS. + duration = 1.0 / TimeInterval(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 + + /// An animator which used to drive the data behind `AnimatedImageView`. + public class Animator { + private let size: CGSize + + private let imageSize: CGSize + private let imageScale: CGFloat + + /// The maximum count of image frames that needs preload. + public let maxFrameCount: Int + + private let imageSource: CGImageSource + private let maxRepeatCount: RepeatCount + + private let maxTimeStep: TimeInterval = 1.0 + private let animatedFrames = SafeArray() + 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 + + /// The image of the current frame. + public var currentFrameImage: UIImage? { + return frame(at: currentFrameIndex) + } + + /// The duration of the current active frame duration. + public var currentFrameDuration: TimeInterval { + return duration(at: currentFrameIndex) + } + + /// The index of the current animation frame. + public internal(set) 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 + } + } + + /// Whether the current frame is the last frame or not in the animation sequence. + public 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`. + /// - imageSize: Size of the `KingfisherWrapper`. + /// - imageScale: Scale of the `KingfisherWrapper`. + /// - count: Count of frames needed to be preloaded. + /// - repeatCount: The repeat count should this animator uses. + /// - preloadQueue: Dispatch queue used for preloading images. + init(imageSource source: CGImageSource, + contentMode mode: UIView.ContentMode, + size: CGSize, + imageSize: CGSize, + imageScale: CGFloat, + framePreloadCount count: Int, + repeatCount: RepeatCount, + preloadQueue: DispatchQueue) { + self.imageSource = source + self.contentMode = mode + self.size = size + self.imageSize = imageSize + self.imageScale = imageScale + self.maxFrameCount = count + self.maxRepeatCount = repeatCount + self.preloadQueue = preloadQueue + + GraphicsContext.begin(size: imageSize, scale: imageScale) + } + + deinit { + resetAnimatedFrames() + GraphicsContext.end() + } + + /// Gets the image frame of a given index. + /// - Parameter index: The index of desired image. + /// - Returns: The decoded image at the frame. `nil` if the index is out of bound or the image is not yet loaded. + public func frame(at index: Int) -> KFCrossPlatformImage? { + return animatedFrames[index]?.image + } + + public func duration(at index: Int) -> TimeInterval { + return animatedFrames[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.. maxFrameCount { return } + animatedFrames[index] = animatedFrames[index]?.makeAnimatedFrame(image: loadFrame(at: index)) + } + + self.loopDuration = duration + } + + private func resetAnimatedFrames() { + animatedFrames.removeAll() + } + + private func loadFrame(at index: Int) -> UIImage? { + let resize = needsPrescaling && size != .zero + let options: [CFString: Any]? + if resize { + options = [ + kCGImageSourceCreateThumbnailFromImageIfAbsent: true, + kCGImageSourceCreateThumbnailWithTransform: true, + kCGImageSourceShouldCacheImmediately: true, + kCGImageSourceThumbnailMaxPixelSize: max(size.width, size.height) + ] + } else { + options = nil + } + + guard let cgImage = CGImageSourceCreateImageAtIndex(imageSource, index, options as CFDictionary?) else { + return nil + } + + if #available(iOS 15, tvOS 15, *) { + // From iOS 15, a plain image loading causes iOS calling `-[_UIImageCGImageContent initWithCGImage:scale:]` + // in ImageIO, which holds the image ref on the creating thread. + // To get a workaround, create another image ref and use that to create the final image. This leads to + // some performance loss, but there is little we can do. + // https://github.com/onevcat/Kingfisher/issues/1844 + guard let context = GraphicsContext.current(size: imageSize, scale: imageScale, inverting: true, cgImage: cgImage), + let decodedImageRef = cgImage.decoded(on: context, scale: imageScale) + else { + return KFCrossPlatformImage(cgImage: cgImage) + } + + return KFCrossPlatformImage(cgImage: decodedImageRef) + } else { + let image = KFCrossPlatformImage(cgImage: cgImage) + if backgroundDecode { + guard let context = GraphicsContext.current(size: imageSize, scale: imageScale, inverting: true, cgImage: cgImage) else { + return image + } + return image.kf.decoded(on: context) + } else { + return image + } + } + } + + private func updatePreloadedFrames() { + guard preloadingIsNeeded else { + return + } + + let previousFrame = animatedFrames[previousFrameIndex] + animatedFrames[previousFrameIndex] = previousFrame?.placeholderFrame + // ensure the image dealloc in main thread + defer { + if let image = previousFrame?.image { + DispatchQueue.main.async { + _ = image + } + } + } + + preloadIndexes(start: currentFrameIndex).forEach { index in + guard let currentAnimatedFrame = animatedFrames[index] else { return } + if !currentAnimatedFrame.isPlaceholder { return } + animatedFrames[index] = currentAnimatedFrame.makeAnimatedFrame(image: loadFrame(at: index)) + } + } + + private func incrementCurrentFrameIndex() { + let wasLastFrame = isLastFrame + currentFrameIndex = increment(frameIndex: currentFrameIndex) + if isLastFrame { + currentRepeatCount += 1 + if isReachMaxRepeatCount { + isFinished = true + + // Notify the delegate here because the animation is stopping. + delegate?.animator(self, didPlayAnimationLoops: currentRepeatCount) + } + } else if wasLastFrame { + + // Notify the delegate that the loop completed + 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.. { + private var array: Array = [] + private let lock = NSLock() + + subscript(index: Int) -> Element? { + get { + lock.lock() + defer { lock.unlock() } + return array.indices ~= index ? array[index] : nil + } + + set { + lock.lock() + defer { lock.unlock() } + if let newValue = newValue, array.indices ~= index { + array[index] = newValue + } + } + } + + var count : Int { + lock.lock() + defer { lock.unlock() } + return array.count + } + + func reserveCapacity(_ count: Int) { + lock.lock() + defer { lock.unlock() } + array.reserveCapacity(count) + } + + func append(_ element: Element) { + lock.lock() + defer { lock.unlock() } + array += [element] + } + + func removeAll() { + lock.lock() + defer { lock.unlock() } + array = [] + } +} +#endif +#endif diff --git a/Demo/Pods/Kingfisher/Sources/Views/Indicator.swift b/Demo/Pods/Kingfisher/Sources/Views/Indicator.swift new file mode 100644 index 0000000..a3fa5a4 --- /dev/null +++ b/Demo/Pods/Kingfisher/Sources/Views/Indicator.swift @@ -0,0 +1,231 @@ +// +// Indicator.swift +// Kingfisher +// +// Created by João D. Moreira on 30/08/16. +// +// Copyright (c) 2019 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. + +#if !os(watchOS) + +#if canImport(AppKit) && !targetEnvironment(macCatalyst) +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 } + + /// The size strategy used when adding the indicator to image view. + /// - Parameter imageView: The super view of indicator. + func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy +} + +public enum IndicatorSizeStrategy { + case intrinsicSize + case full + case size(CGSize) +} + +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 } + + /// Default implementation of `centerOffset` of `Indicator`. The default value is `.full`, means that the indicator + /// will pin to the same height and width as the image view. + public func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { + return .full + } +} + +// 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 + } + } + + func sizeStrategy(in imageView: KFCrossPlatformImageView) -> IndicatorSizeStrategy { + return .intrinsicSize + } + + init() { + #if os(macOS) + activityIndicatorView = NSProgressIndicator(frame: CGRect(x: 0, y: 0, width: 16, height: 16)) + activityIndicatorView.controlSize = .small + activityIndicatorView.style = .spinning + #else + let indicatorStyle: UIActivityIndicatorView.Style + + #if os(tvOS) + if #available(tvOS 13.0, *) { + indicatorStyle = UIActivityIndicatorView.Style.large + } else { + indicatorStyle = UIActivityIndicatorView.Style.white + } + #else + if #available(iOS 13.0, * ) { + indicatorStyle = UIActivityIndicatorView.Style.medium + } else { + indicatorStyle = UIActivityIndicatorView.Style.gray + } + #endif + + activityIndicatorView = UIActivityIndicatorView(style: indicatorStyle) + #endif + } +} + +#if canImport(UIKit) +extension UIActivityIndicatorView.Style { + #if compiler(>=5.1) + #else + static let large = UIActivityIndicatorView.Style.white + #if !os(tvOS) + static let medium = UIActivityIndicatorView.Style.gray + #endif + #endif +} +#endif + +// MARK: - ImageIndicator +// Displays an ImageView. Supports gif +final class ImageIndicator: Indicator { + private let animatedImageIndicatorView: KFCrossPlatformImageView + + 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 = KFCrossPlatformImageView() + 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 + } +} + +#endif diff --git a/Demo/Pods/Manifest.lock b/Demo/Pods/Manifest.lock new file mode 100644 index 0000000..8a29c14 --- /dev/null +++ b/Demo/Pods/Manifest.lock @@ -0,0 +1,34 @@ +PODS: + - IGListDiffKit (4.0.0) + - IGListKit (4.0.0): + - IGListDiffKit (= 4.0.0) + - Kingfisher (7.6.2) + - SwiftMessages (9.0.6): + - SwiftMessages/App (= 9.0.6) + - SwiftMessages/App (9.0.6) + - UIColor_Hex_Swift (5.1.7) + +DEPENDENCIES: + - IGListKit + - Kingfisher (~> 7.6.2) + - SwiftMessages + - UIColor_Hex_Swift + +SPEC REPOS: + trunk: + - IGListDiffKit + - IGListKit + - Kingfisher + - SwiftMessages + - UIColor_Hex_Swift + +SPEC CHECKSUMS: + IGListDiffKit: 665d6cf43ce726e676013db9c7d6c4294259b6b2 + IGListKit: fd5a5d21935298f5849fa49d426843cff97b77c7 + Kingfisher: 6c5449c6450c5239166510ba04afe374a98afc4f + SwiftMessages: f0c7ef4705a570ad6c5e208b611f4333e660ed92 + UIColor_Hex_Swift: 31cd3e47440f07a20d2503a36bb0437a445b3c29 + +PODFILE CHECKSUM: 310645f0fa108e1099503961f2d58481962552ed + +COCOAPODS: 1.12.1 diff --git a/Demo/Pods/Pods.xcodeproj/project.pbxproj b/Demo/Pods/Pods.xcodeproj/project.pbxproj new file mode 100644 index 0000000..8ac5866 --- /dev/null +++ b/Demo/Pods/Pods.xcodeproj/project.pbxproj @@ -0,0 +1,2457 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 0052866F22FC243E2A20CC326BFF3F58 /* IGListSectionMap.h in Headers */ = {isa = PBXBuildFile; fileRef = 68248F05BB6436D9514BD865E5AEFD53 /* IGListSectionMap.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 00BEA6029C428FEE644AC3D42AD83282 /* ImagePrefetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E261DDB018DEF41EF92A4794D0CC81 /* ImagePrefetcher.swift */; }; + 017FE38FF3607E56CABEF7C6F2D6D141 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007F365850B5C8A7EEA9E7F41F325B11 /* UIKit.framework */; }; + 0285857A24F66E925987A5876F0BE63B /* ImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19002AA8D6680BDDAC1C8C5DDE64CD49 /* ImageDataProvider.swift */; }; + 0510E8EA51914CB2176AD0F173937FAB /* KFImageRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 225916A84FF5B787DDE7EB83441E3470 /* KFImageRenderer.swift */; }; + 05228565AAA7FCED4BAFB2B7EF71D53D /* KingfisherOptionsInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = D242674C42297EEEBD97DEE7B159B1AE /* KingfisherOptionsInfo.swift */; }; + 059639E700DEFAEF08F56484E5F67BE7 /* NSButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEDF7F99C596705FC4F572460885EBCD /* NSButton+Kingfisher.swift */; }; + 05DF336E6DB67BB0621CCF0C9463ED4A /* IGListCollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 7C7F743B6CB541B1A49D0F89CDE913C6 /* IGListCollectionView.m */; }; + 07ED65FC8B25E7420799D68D2CC59E7D /* IGListAdapterUpdateListener.h in Headers */ = {isa = PBXBuildFile; fileRef = 67EB0E60915C51E53FA40EB4CB81BAC9 /* IGListAdapterUpdateListener.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 08107140946FA9287B2C63728BFABB2E /* SwiftMessages-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 25FFBB29D9878FEFD402D15B13F96F2F /* SwiftMessages-dummy.m */; }; + 082EDC820D76DF95C71A5018112DE512 /* UIButton+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD245F26200BAD0F0C9385C5DEA429B7 /* UIButton+Kingfisher.swift */; }; + 08CA1A7B0A9F6B96CA4DFCFF977759CC /* errorIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 8F8C5C6E34EBE4F7FEFFD3E815727A10 /* errorIconLight.png */; }; + 0ABF3498CC7F1EB2B3B3EA0648268D58 /* NSNumber+IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 51E9A430A8CB278156CC969E105579DA /* NSNumber+IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0B5893ABAF3F5EB6896FF6748BA24B9D /* IGListMoveIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 92CF5980D57F9F674AE272CB46916FF0 /* IGListMoveIndexPath.m */; }; + 0BC00DD25D3C4FF42C37433E81CA6F77 /* IGListCollectionViewLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8051E3132AD533F0FFDD835E592EBC46 /* IGListCollectionViewLayout.mm */; }; + 0C5AD6C37D168B6D4CEC37F24AEFEF9C /* UICollectionView+IGListBatchUpdateData.h in Headers */ = {isa = PBXBuildFile; fileRef = E8FA93108377D1E5B3A047566835F3D3 /* UICollectionView+IGListBatchUpdateData.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 0C5C1FCD4460D91B069B29E4E4267EBF /* UICollectionView+IGListBatchUpdateData.m in Sources */ = {isa = PBXBuildFile; fileRef = 4EB7D9E14485B011E821B2552487BFFF /* UICollectionView+IGListBatchUpdateData.m */; }; + 0CFD8E5542E0804B92A255670C94AB24 /* IGListDisplayDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = B6FCEA9D7060088104B1ACB5E98DC48F /* IGListDisplayDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0D293ABB2BD3C45D748001FF3929AD4B /* IGListCollectionViewLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BC79CF900824AEA4ED4B584BE07F257 /* IGListCollectionViewLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0ED8FBFD9A86D21BF69137EC9350E575 /* SessionDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D467CB3DCF65BE9C4B4067D5FFDD6859 /* SessionDelegate.swift */; }; + 0F9B2F409EF8A5AADE05FC337C4F4B20 /* IGListMoveIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = DF5E502A140B8C7136E1F676C002042C /* IGListMoveIndexPath.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 106E77E6CCA0360F0EBCC653774E25F5 /* CardView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 73660183B3F95FADC4C3F189620E5797 /* CardView.xib */; }; + 1185A2B40E14F2FCBC761FC99777CAD8 /* ExtensionHelpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = AD155FF57E053DCC59222AD9E9006734 /* ExtensionHelpers.swift */; }; + 1199F40D641095A06DC44FA62F589282 /* IGListDebuggingUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 6AD3632ED786F7D034F9E1A6EF5B3DAE /* IGListDebuggingUtilities.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 122980E44B15C64CF0B14DC94D7EB5C9 /* Kingfisher-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = EE7F5B9CFE3D767F3DB305EE26C2283F /* Kingfisher-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 12D9C86DE5B35895C8C4C2F66D98EF81 /* warningIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 063A43A32CE98A3E1C417772EB31679E /* warningIconSubtle.png */; }; + 13786D2762D17C2F6715CA62D64AE76B /* infoIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 7288C4A89C8BEFB77574C7581276301B /* infoIconSubtle@3x.png */; }; + 137EF15CD62928D2E8C32B18E35CDA84 /* SwiftMessagesSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 86D541130E17C7E352A8977E69E4D3DC /* SwiftMessagesSegue.swift */; }; + 145318846A4197187343A7223A8366BF /* IGListReloadDataUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = 8720FC3CB9A344DF6C3C51A81CE61E8F /* IGListReloadDataUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 194E2F707D9AE87266C08DDD38BAC2C5 /* UICollectionView+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 424235E351E1EEF73A585E53FCCE5931 /* UICollectionView+DebugDescription.m */; }; + 1952485AFF7A1BCCA4D4B142E82FE627 /* AnimatedImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 52372AE9D7E296FCED90D2D5BA9ECB4D /* AnimatedImageView.swift */; }; + 1A510118417546D747FA5B2DEE7D2009 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9060464159E13D7209F7D64BACF83DAD /* Foundation.framework */; }; + 1AA89F327105C026976BF6E831B193A2 /* ImageBinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE009ADA0FD378470D467A226946D15F /* ImageBinder.swift */; }; + 1B17C9041C0794A94A39D5B31C62CEDE /* IGListDisplayHandler.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E3CE7DF25AAEC00FF8F06896801111B /* IGListDisplayHandler.m */; }; + 1B2DDB8AD9504EB5AF857E4A45E3A979 /* NSString+IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = EF2222773C495CA6BA87AAAA93B049B9 /* NSString+IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 1BCE5E9382038062602EC3B882B0B1C1 /* errorIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 3B2A150E6AA79141831952D08C9CA44A /* errorIconLight@3x.png */; }; + 1D79FA4B26B801D9043A4BF8224939B9 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007F365850B5C8A7EEA9E7F41F325B11 /* UIKit.framework */; }; + 1EEBEB3E8E43888EFF440D810F4FE61F /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6232B2401FA0928C2269BFD66C421986 /* Weak.swift */; }; + 1F727CB62A71B2ABE2CA4FBDC563A4A1 /* UIViewController+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9E43A922FEECF4031283D9C0CCEAD71 /* UIViewController+Extensions.swift */; }; + 1FD2928BC156D990D68B105F518C60B6 /* MemoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92E5783214B75AAE3D5847FC4443333D /* MemoryStorage.swift */; }; + 1FE693B5ACC6AD7320CEFC20B64546E4 /* KingfisherManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65601298C6B1A422CDB2DC07123D1E51 /* KingfisherManager.swift */; }; + 1FEE89BF952BE7ACA46E642DA2E48CA2 /* DiskStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = A99E6797E0FD83202E415971B87BC2AE /* DiskStorage.swift */; }; + 20A92FC736588CF2C5688AFD384EAD9D /* UIColorExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A600EBFC4D5E663BCCFC8D3C2BEF24A6 /* UIColorExtension.swift */; }; + 21191DB3B6E3981B1D9682644743FF7E /* infoIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 648D06A574C08D26489A675BF3F8CC97 /* infoIcon@3x.png */; }; + 22216C300C763044344B9DBF97317E63 /* RetryStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C401DDB5E824718372BEB559155F340E /* RetryStrategy.swift */; }; + 22BD1346F66BFCB129AAA44EEF322AC9 /* Resource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B0D3AE0230F985AC1ED5587C52B99DB /* Resource.swift */; }; + 243D7CFE1D56ED80ACB2B3E71B4CB603 /* AuthenticationChallengeResponsable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D5B3DE73BA8320B88507E03E2B50D19 /* AuthenticationChallengeResponsable.swift */; }; + 248A5D0758579F45393A48F7F1A816EE /* IGListAdapterDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = 27B420D1608AC6A991CAF82584A9805B /* IGListAdapterDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 256BA1F1360654F69778E29BBE25B04D /* IGListSupplementaryViewSource.h in Headers */ = {isa = PBXBuildFile; fileRef = B190AE5FF026FBD8339F4E3193D1A49E /* IGListSupplementaryViewSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 25FC036BEA33CAB5D80F5A41644535D3 /* Storage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA9BBA4DCEA951FBE5842187767A324A /* Storage.swift */; }; + 26A9A1D5029BDEB2C0AF0ECFE65ECBB6 /* BackgroundViewable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F138F35B611E2FD49AEFCF69028D4AB7 /* BackgroundViewable.swift */; }; + 290CE4B2273A8DD7501C883649D798F4 /* IGListAdapterUpdater+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = C8BFDA32ECAAAC6F60280C6BFAEAEE57 /* IGListAdapterUpdater+DebugDescription.m */; }; + 29A7C0BC88D85F89318C67E6293CFFB4 /* IGListBatchUpdates.h in Headers */ = {isa = PBXBuildFile; fileRef = BF7F03396FC5C7FFAB84FF05B036B970 /* IGListBatchUpdates.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 29A9E81F33421D1E32648B12D20B4F68 /* UIScrollView+IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 1351EB6B5E88098E59C3F20E89B2004C /* UIScrollView+IGListKit.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 29CE33F429B08D390973ADE3F05A097F /* warningIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9A5FA7FE39BE7656FDDEB10178642B00 /* warningIconLight@3x.png */; }; + 29FF13E23FD52E46D30530549410AD7C /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F789B805ED414860F2DC4C723AFE42D /* ImageTransition.swift */; }; + 2A44FC42DA4E309A6CD0C23845E62FDD /* MaskingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DFF373D32B02827C3E52EB2CD2A3FE /* MaskingView.swift */; }; + 2BE89C24BFD3FB663E37C607C289B3B6 /* RedirectHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = C107FC92873BF8C1318FB8BAB48B491D /* RedirectHandler.swift */; }; + 2CC46C11380D37A6DE73F58D4DEFF754 /* successIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 17CAC30BEB11B3613D6C1DFA0950F7E9 /* successIconLight.png */; }; + 2CDD1385DB4E94B72AA923A2DB34D030 /* MarginAdjustable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09BA0051E9F4F47215585B6787C85C96 /* MarginAdjustable.swift */; }; + 2D2A77A53A45AA03F16F2411E6F7FEB7 /* IGListBatchUpdateData.h in Headers */ = {isa = PBXBuildFile; fileRef = C904783C0CEAE96552A679C695669DBC /* IGListBatchUpdateData.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2D4F11BB21F21B0F1687BDA71200EB74 /* IGListIndexSetResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 07BC0E95B093644150AD1D0260560C1D /* IGListIndexSetResultInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 2D7DD1D4C822CE247530FAFBF36B41B4 /* successIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = DC15D52D5B4A8353B7433818CFC755C3 /* successIcon@3x.png */; }; + 2EB12379C6E40E835D1AC08FAADFC392 /* UIColor_Hex_Swift-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = D5B77C4DD4EF76E7FC4D29F3BF7F1AAD /* UIColor_Hex_Swift-dummy.m */; }; + 2F0CA6E635295AA692FAEB9393783F4B /* IGListDebugger.m in Sources */ = {isa = PBXBuildFile; fileRef = 709704C7D88AAF115EDAB039595D266D /* IGListDebugger.m */; }; + 31D2A0320106459AC5D7A31F8C41646B /* IGListAdapter+UICollectionView.m in Sources */ = {isa = PBXBuildFile; fileRef = 39CF63638810AE1B4B6F37AF97A30C81 /* IGListAdapter+UICollectionView.m */; }; + 33634B64A37F709874AB14578331B98D /* Presenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CDF76905EE491F6C6CEC1B70BC440D6 /* Presenter.swift */; }; + 36AA900737FE6B23825191A9011181DB /* MessageView.xib in Resources */ = {isa = PBXBuildFile; fileRef = CB4BCAA40F9050DCE2E7581EECE3B199 /* MessageView.xib */; }; + 37E2C53DDA53EE7696F3C78A095A70C2 /* IGListMoveIndexPathInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 996DBB5DDFA8515120D75647C8498AE8 /* IGListMoveIndexPathInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 395296368FAD6FEE70CE19FBCE22C316 /* IGListIndexPathResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = D2E866BD1073307B39C555AA72687DED /* IGListIndexPathResultInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 39A7FD94C1F455775CBBBC49131CDE98 /* successIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = AED77699F9B880BBE2EA3CCA45FEE504 /* successIcon.png */; }; + 3A16D398DAB6AFE26767E5B5E9A37767 /* infoIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9833C8624EAC2FC205C021937F16B47D /* infoIcon@2x.png */; }; + 3AD5DBB915C2623991F7DBACD173BBB4 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9060464159E13D7209F7D64BACF83DAD /* Foundation.framework */; }; + 3AF7DB9AEFF47F1F7F91AF28440E4AC6 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = A481C64506F5FE39DE63847F8959BE5D /* Filter.swift */; }; + 3DB2320AF24E5F4C92900577EE44878C /* UIColor_Hex_Swift-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = A4F2B2BFBEFB50E3208CD5BC30F9573C /* UIColor_Hex_Swift-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3E9D03B9D56D435F1387EEE5B848D0C7 /* IGListBindable.h in Headers */ = {isa = PBXBuildFile; fileRef = BBE21E109176CA5683819D426970A5E5 /* IGListBindable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 3EEE359E3EA05A87BEABA426D6092571 /* StringExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2283864C5BFAC9FC50299F4F6071A9 /* StringExtension.swift */; }; + 3F2E1FB46259BAF163598529C1925BCB /* IGListBatchUpdates.m in Sources */ = {isa = PBXBuildFile; fileRef = B2DFA0063F8757E8167F805F39A2E3E1 /* IGListBatchUpdates.m */; }; + 3F69A921AA08DB133E702F3F7C395405 /* IGListIndexSetResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = DB6D5C8DD9281BFC32CE9E0A12F507D9 /* IGListIndexSetResultInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 404DCA74511EDA44B896202CD64458A0 /* errorIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6A7C4CC9232AC5706CBAB7721F33DE5B /* errorIcon@3x.png */; }; + 420C200A05BB29E1D299D1BADE9139D2 /* CFNetwork.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7388592716E23DB7918BA1CEF956BA19 /* CFNetwork.framework */; }; + 456D4E89F94D07D831D2D06212DD3A21 /* IGListCollectionViewLayoutCompatible.h in Headers */ = {isa = PBXBuildFile; fileRef = F36F51D00377325A46E8AFB150E6113A /* IGListCollectionViewLayoutCompatible.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 457BE444ED617FA7D6851D6DAA9D7234 /* Delegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C09F9726B03BEFB76DB54470EFA079E /* Delegate.swift */; }; + 4735A168EFDF0E56425C926BCEF42F33 /* IGListBindingSectionControllerSelectionDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 30513773A49E99A4C75E6115DD8934EC /* IGListBindingSectionControllerSelectionDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 48065C2580FF9E5D2DE0E6447AACB110 /* errorIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = AA5472E591CE0BA4D0BA9BD216E9BBB0 /* errorIcon@2x.png */; }; + 4A3FACE084B0154043BD4015B3E8FCC2 /* errorIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 0E783AAB1D182A47CE9A4C70AEA15DC7 /* errorIconSubtle.png */; }; + 4AD71B305F0FFBCD1859562AA2F86A29 /* warningIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 67E342B558FA9E3757D37EEB2E4D12A3 /* warningIconLight.png */; }; + 4B7802B665FAD2255F4A3D5FADE64B08 /* StatusLine.xib in Resources */ = {isa = PBXBuildFile; fileRef = C1B6942E8551B1ABFBEBCA492FA29A1F /* StatusLine.xib */; }; + 4B86DE37B3B553A574A54CCDDBC13680 /* IGListSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = F6ACCD5DC25939E8ED2EC86296E56DF6 /* IGListSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4DCA9775E5CCF599460BDB46E77F6FA4 /* Kingfisher-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 10FD14B174196E8692A3FF6CE273842B /* Kingfisher-dummy.m */; }; + 4E69250DFE230B7D9134B5360A91EC8D /* IGListDiffKit-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = CC497F9013BFBD2093E1EC78B0273F64 /* IGListDiffKit-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4EE2EC2F10C2D2BC999CE30C679575FB /* HEXColor.h in Headers */ = {isa = PBXBuildFile; fileRef = 2F78D97A20A66B010C3E1ABDA41FFA5F /* HEXColor.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 4F37E521D341C47CE73DDCF21BA95A52 /* KingfisherError.swift in Sources */ = {isa = PBXBuildFile; fileRef = CF130AF5100D6FFE94E45CAEC5B8222E /* KingfisherError.swift */; }; + 4F7ADAB76F659FA15973E8EE2D390D36 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007F365850B5C8A7EEA9E7F41F325B11 /* UIKit.framework */; }; + 500C34AE9C3CBFFE6854C3627328C849 /* warningIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 2A7E3CFE0BA27926F6ABFC0513EF1282 /* warningIconSubtle@2x.png */; }; + 504FC6E2898BB2EAA2099072FBECB8B8 /* IGListAdapter+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = B714F2FA5DE937147702D595D7104ACC /* IGListAdapter+DebugDescription.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 506128E1CC424E40E2691546D9547549 /* Placeholder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53BF7416BD4A970D1B5E4B01F294E586 /* Placeholder.swift */; }; + 509490FB1D30FEC59AE4BC21AEEBB7BB /* RequestModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB4552B30B4BEA7C253712647396FB87 /* RequestModifier.swift */; }; + 52800834B090075CA6F35EF7306B5AE5 /* CenteredView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 1091A7C7D19C45920D87A3EAA1AA6C0F /* CenteredView.xib */; }; + 53526734412F401A087EC20F9A3CC7EB /* IGListMoveIndex.h in Headers */ = {isa = PBXBuildFile; fileRef = 896B6B102742772B634D79305645CA79 /* IGListMoveIndex.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5456CE7488C7FE316C00605531107BFD /* IGListIndexPathResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 4BED2EF6A3B24B9C0BF0E9524B7B6436 /* IGListIndexPathResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5752BA2732399E70CFA4026649B32D3B /* IGListDiffKit-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 2713DB42F5385245C2EC0D72720C5BE6 /* IGListDiffKit-dummy.m */; }; + 57FC31B14C753B5C63CEF00560F8A6EF /* SizeExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1F3E90B88F5618F4D313769ED312089B /* SizeExtensions.swift */; }; + 582D59E0D2EF62E0575933C99B393704 /* GraphicsContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31363CA02DABFE037A9597E66D774E50 /* GraphicsContext.swift */; }; + 59BC9047F4BEBBC06235608D974E230D /* NSTextAttachment+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED7503AECAE5B4ACEDDA8E6EF1740523 /* NSTextAttachment+Kingfisher.swift */; }; + 5ADB30DD9A03859018550A999ACB0652 /* KFImageOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 793410A19FBFCCE0BF378A5C8CF49BFA /* KFImageOptions.swift */; }; + 5C1B342A3F54657EEA7FC2105EF75523 /* IGListMoveIndexPathInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = D9CEFE6DEF6BE44242643753E89BE983 /* IGListMoveIndexPathInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 5E27DD292D3A55657712DD7AFA7B8FCA /* KFImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF93862F9459B5B5101367844C76ACFC /* KFImage.swift */; }; + 5EBA64E274109B68134E970A1C831236 /* IGListCollectionScrollingTraits.h in Headers */ = {isa = PBXBuildFile; fileRef = 2A1E479F8F71FB71F3C62C8450C4DC84 /* IGListCollectionScrollingTraits.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 5F852F38CBC282496CCBE37C51324B2F /* ImageProgressive.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9615C14A8AD14AEA71CE750A1DA1EDF8 /* ImageProgressive.swift */; }; + 61976AFFB89BE9B4FFCCE448B16B895D /* UICollectionView+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = C1B7F17AB35439D95BB2D5B84B8FF273 /* UICollectionView+DebugDescription.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 627B24A21C799D9876E8EDA4C203EB5C /* IGListDisplayHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = F007DFBDE1351AA0BB0A55C234B06A7E /* IGListDisplayHandler.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 6354E790FC26E29FFFAB6F298404FE52 /* TabView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 34EC4406FF95B83EF37D06F08D66A633 /* TabView.xib */; }; + 637DFADEF4F664E6E6932F0E27472FD5 /* IGListDiffKit.h in Headers */ = {isa = PBXBuildFile; fileRef = E9EF2C50B2F9F3FC8C75CE92032462AA /* IGListDiffKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 645463E624115A42FE45DD945C41DB40 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 461E8E5C8D92714274A6A58953D195C9 /* Theme.swift */; }; + 653E4F4EE5A4AE7C8E2C2BD9BE9AD111 /* IGListBindingSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 118EB5557B6BA57C54ECE70977225895 /* IGListBindingSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 69A39E2DCEE7140DF3654C581C401637 /* KeyboardTrackingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54AF051AB7BCD460987153D0685F73B2 /* KeyboardTrackingView.swift */; }; + 6A3B187375746C0888E5CFCDD868FCCB /* IGListMacros.h in Headers */ = {isa = PBXBuildFile; fileRef = 51A70695551771573DC2AA2EBA4C1856 /* IGListMacros.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 6B60128BF181756B5A05576C6068566F /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9060464159E13D7209F7D64BACF83DAD /* Foundation.framework */; }; + 6C4C53377507F9D77E67A19F3B0DAE69 /* Identifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E56A83EDE9F898293353CD761ED323A /* Identifiable.swift */; }; + 6D093916DC5318912596E0AD1AA26238 /* successIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = CE29AFF1B5B06DB5F46307D8CA2F94A2 /* successIcon@2x.png */; }; + 6D3253DE7778CCCB009EFB375AA84800 /* UIScrollView+IGListKit.m in Sources */ = {isa = PBXBuildFile; fileRef = DB1BF169705DB0B275CE4D023A9F5097 /* UIScrollView+IGListKit.m */; }; + 6E3E76E4A7F77A3973EE6449CE275137 /* UICollectionViewLayout+InteractiveReordering.h in Headers */ = {isa = PBXBuildFile; fileRef = 8C74304E2D1E7F3E9241DE00AF3868A9 /* UICollectionViewLayout+InteractiveReordering.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 6E5BC0F0DF1D4D4C3A81A3B1ABEB2E52 /* SwiftMessages-SwiftMessages in Resources */ = {isa = PBXBuildFile; fileRef = 753DD909979478A0C1B33E0524504EF1 /* SwiftMessages-SwiftMessages */; }; + 70FEC06F54286257E1BA1ECA0C99198D /* Image.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3934169222833A8921F287D6737C3D94 /* Image.swift */; }; + 724DBA853B4874B96363B04EA98D4147 /* infoIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 645A2EAB5E61532B3ABBCEBC6AF722C8 /* infoIconSubtle.png */; }; + 73448A3812B760B85DD6B64B85B49165 /* IGListTransitionDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 3F4E7F4C4B0ED07AA5930C2E3D5B8FD6 /* IGListTransitionDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 73BC2BAEAFC76A79BF2A6E23CA54044F /* IGListCollectionViewLayoutInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 14D8F73E1EEE92B0BB6C4A4862D834A9 /* IGListCollectionViewLayoutInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 74021AE8DA8068962E566374E4097561 /* IGListBatchUpdateState.h in Headers */ = {isa = PBXBuildFile; fileRef = 0A2F39EF1CE1C157494607E6C1FE66A7 /* IGListBatchUpdateState.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 7448C55AC852D9DD05F91D03E7430E4C /* IGListScrollDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 7ED99FCB6BB287CC3833E78421A0E4C1 /* IGListScrollDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 75240B35A5337C0362A81BA4E19C50A8 /* UIWindow+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8D3E03494034124977566245810EF836 /* UIWindow+Extensions.swift */; }; + 7597FD2790FC3E3002CC855FDF1EEFAB /* IGListArrayUtilsInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 77CDFE36E22AF91849D4D87D3DD75B16 /* IGListArrayUtilsInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 7598EFC472D8E80C444EAF66B3D53DDA /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9060464159E13D7209F7D64BACF83DAD /* Foundation.framework */; }; + 760BE3F4C975ECE6B3AFDB55F66F0A78 /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 007F365850B5C8A7EEA9E7F41F325B11 /* UIKit.framework */; }; + 769764555AF9B0D77F7D1BDF6694A0F3 /* IGListDiff.h in Headers */ = {isa = PBXBuildFile; fileRef = 0495A674038D17605C95A9A5B9213100 /* IGListDiff.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 76E95598BA0A70C68E8782BC178A0B86 /* successIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 08510568CA8F1AEEDE5B0E32306DC71A /* successIconLight@3x.png */; }; + 770A7C634F7BBC9E2C030AAC217A383C /* IGListAdapterUpdaterInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 67AD88EDD5E2B4BE3251E36338822D75 /* IGListAdapterUpdaterInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 7947B831C0A0B1D6E01E862603BB6369 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80AA6B070EDF3B36ED949C1DE413B445 /* Animator.swift */; }; + 7A5320B15F6789F272E61C44F9F4DBB0 /* SwiftMessages.Config+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517254B0F9ADBAC88606A8124B700550 /* SwiftMessages.Config+Extensions.swift */; }; + 7BB3B41A692D42272CEB3CD6023A97B8 /* IGListIndexSetResult.m in Sources */ = {isa = PBXBuildFile; fileRef = F1D5521BDDA1A0242A36C1929596B475 /* IGListIndexSetResult.m */; }; + 7C7418FF01DD7BB909719682B634A8A5 /* SessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F5679710D252BC32A66875E79EE9BEB /* SessionDataTask.swift */; }; + 7CFC6E5C81DB4F42F1C469C8194FE64C /* IGListAdapterPerformanceDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 111AD5BA834CFAE0B8CDB2B897047D06 /* IGListAdapterPerformanceDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7D6EB166CD4530B9E1CB943798269D5B /* IGListIndexSetResult.h in Headers */ = {isa = PBXBuildFile; fileRef = 7AC803E40D56EEDCA7EE11F40F5D2873 /* IGListIndexSetResult.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 7E08A94F10299EADB052CAE34F2BFA00 /* TopBottomAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 49FFAA7BEFB9D929E25C31979D44D129 /* TopBottomAnimation.swift */; }; + 7E98E18B747F60326E4BA87E15839F29 /* UIEdgeInsets+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9DD8DD2702C161767569A953B694435D /* UIEdgeInsets+Extensions.swift */; }; + 7FFE4021A4F14124342AD41CE1117B3E /* KFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D034498F4799F076DA40C62E1C1CF6A /* KFAnimatedImage.swift */; }; + 80738D8956C9987CCCEDF551961E5069 /* ImageDataProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC751C1D0408841BADB8D95359DEDD0 /* ImageDataProcessor.swift */; }; + 80AF29D95B77B1DFCBF7F674954A8513 /* warningIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 635E1C7D580143C90A7C99F9B6145CC7 /* warningIconLight@2x.png */; }; + 82925050E10EBBC55386CBB189824F09 /* successIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 2987551134D451FC8A1024048B845790 /* successIconSubtle@2x.png */; }; + 83557F428BB0AF138061AD66390FAE57 /* errorIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = F1EB9AD47A886439B7A26D12430A76C1 /* errorIconSubtle@3x.png */; }; + 840F7452FC8C95F23BF04DD115213F2D /* IGListAdapter.h in Headers */ = {isa = PBXBuildFile; fileRef = F3A0944D15D667441A61E0CE8B2F5567 /* IGListAdapter.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8500F9C0DFC843EB5A4ED4FF0664B213 /* IGListReloadDataUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = B0560DAEDE227C49E7CE1C22076E70AC /* IGListReloadDataUpdater.m */; }; + 857AB5F39FCA5E1F81A5D20A3A16DD19 /* IGListAdapterMoveDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = C3BAE6351018C0F1408D0BBB486AF803 /* IGListAdapterMoveDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 86A6370F297F87A34ABC316470B564C3 /* IGListReloadIndexPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 6594CBDE5CC688CF8DBC51236163605D /* IGListReloadIndexPath.m */; }; + 881A35B28D93C56E46E305F6138B1A76 /* ImageDownloaderDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2FFEB06874D80B62E4BA3E418B346091 /* ImageDownloaderDelegate.swift */; }; + 8933125F14AA552ACB4B02ACEAC95A46 /* IGListCollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C2D15A0D67EAE136052527797313C80 /* IGListCollectionView.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8A465CC2B4BB341735A5AEB15B28C587 /* IGListAdapterUpdater.m in Sources */ = {isa = PBXBuildFile; fileRef = 4D59E7CA06D24D19520DA2615F034822 /* IGListAdapterUpdater.m */; }; + 8AC9E9ACB2C52077863D538C75C1E430 /* IGListWorkingRangeDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 8212866E5B322E817F4E1594B146D11E /* IGListWorkingRangeDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8AF999CCB198F34068E9AC558864262D /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBAC4E1C27713FF52E2D11B717422EC0 /* Error.swift */; }; + 8EF72BE56C31A8A67872154F61605B81 /* IGListExperiments.h in Headers */ = {isa = PBXBuildFile; fileRef = 759C0577310E0EFEB52904517125DCF7 /* IGListExperiments.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 8FB6113E9C4FFEB7A580060F3040B53C /* errorIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 4DB2CFEA6FDD21B6081185B3729B51D3 /* errorIconSubtle@2x.png */; }; + 92A554C6D0F616D62454C16FA59D8183 /* Pods-Bulletin-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = 98C5BC08D7F9EC2BBA718B8840AE0B46 /* Pods-Bulletin-dummy.m */; }; + 93149C816CBB4ED433D3A3EE6F5B9372 /* warningIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 1899BB102CCB03C961D392531F94109C /* warningIcon.png */; }; + 93427084C7854C572B84FF0C78CAD7C5 /* PassthroughWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = ABA9CF4FA93B0C6F6B68554063B72197 /* PassthroughWindow.swift */; }; + 94E07ACBB6D1F91A84009EE52CA247AD /* infoIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 72EDEFFEFD9FA17167C3024ED8D81D74 /* infoIconLight.png */; }; + 96BA7BD41B99BCBE403CFC49DF217990 /* infoIconLight@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 7E32BABB4A5EC5D4E83FB5D95173A917 /* infoIconLight@3x.png */; }; + 983C97E8D8F2AA85CAEDDB6D74EBA866 /* IGListSectionMap.m in Sources */ = {isa = PBXBuildFile; fileRef = BD49496D6720223D5BD32660F3566682 /* IGListSectionMap.m */; }; + 9B0A78AC22E7EDA755F51D86527E2D9C /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80E936467024EE1D60D40AF511570E73 /* Source.swift */; }; + 9BDCB75960A60DD0DEE0FA6101987B26 /* IGListAdapterUpdater+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = D79BB4ADBD345BEC6C9D1C903BDEC902 /* IGListAdapterUpdater+DebugDescription.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 9C5254A71019F6D346E643611192AAF4 /* IGListCollectionViewDelegateLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 4A38804BDF6E084423F6ED17E2119797 /* IGListCollectionViewDelegateLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9CE4B00AD0EFD1C0A048270DA64986B2 /* IGListAdapter+UICollectionView.h in Headers */ = {isa = PBXBuildFile; fileRef = 5CF48A29FE13ED204720994FA4997255 /* IGListAdapter+UICollectionView.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 9CFB36995618A21B72917C70D913E41A /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9060464159E13D7209F7D64BACF83DAD /* Foundation.framework */; }; + 9E2B220A2A3223F08334BBCBDFEE5BC3 /* NSNumber+IGListDiffable.m in Sources */ = {isa = PBXBuildFile; fileRef = 4E92132F1FBCAEC55FD846EB03A29C5C /* NSNumber+IGListDiffable.m */; }; + 9ED0DFF2B7B21C647DDFC8559C9479C2 /* IGListAdapterUpdater.h in Headers */ = {isa = PBXBuildFile; fileRef = A558F0E26DB1E403F2BA4D285835649B /* IGListAdapterUpdater.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 9F5FE22DA95B66B8DC21CB13BE25EC9B /* WKInterfaceImage+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3B6D0D716E13A355BB46955460FFB80 /* WKInterfaceImage+Kingfisher.swift */; }; + 9F9F378DCF1582EBC63B23D9C8E2F238 /* successIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9166C75ED413481507005F77A9BEE4AF /* successIconLight@2x.png */; }; + A0778F3A26137BE9262E2F4E042D04DF /* IGListIndexPathResultInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 526AFE8B2C5F857BB1C8ACF879C0ECCE /* IGListIndexPathResultInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + A122D2A3C925C48C11A200271047323E /* WindowViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8D88827F487BE149C1B8D4C8F21A887 /* WindowViewController.swift */; }; + A19366C6BC7101DC193D92176EB89C37 /* IGListKit-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = E314F5C9EA1F8E0F93CB7A5863EF4352 /* IGListKit-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + A1FFF4B8FB3410D63594007BBA288632 /* IGListReloadIndexPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 72DAB1212901F5B52E7093E822D23577 /* IGListReloadIndexPath.h */; settings = {ATTRIBUTES = (Private, ); }; }; + A306349DCF43F00C535D8A7C2B5DDE55 /* IGListSectionMap+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = A7228A433C789CABC0C81CB754A42B8B /* IGListSectionMap+DebugDescription.h */; settings = {ATTRIBUTES = (Private, ); }; }; + A316388A35648CB2987E761771456087 /* KFOptionsSetter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4FD74B7B2B30A494ADC03256687A532 /* KFOptionsSetter.swift */; }; + A39D3555EC8B45B7D6B9505DDAF0F117 /* Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEDF019A7D273CA293038AC2E7E27AF7 /* Kingfisher.swift */; }; + A404C0924ECF70F20F76E1AA5F32876D /* IGListDebugger.h in Headers */ = {isa = PBXBuildFile; fileRef = D8DBF71C0BE4BA02FEE918097192B2B7 /* IGListDebugger.h */; settings = {ATTRIBUTES = (Private, ); }; }; + A426AB76106748CDFB137114A6F63935 /* warningIcon@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 95A39432E536B69E1F62334D0007FAC2 /* warningIcon@3x.png */; }; + A55902F47A9DACFEAC7D0833A569911E /* CALayer+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA656E9DD3994DB5C569165361FFF7EE /* CALayer+Extensions.swift */; }; + A88A844D5356E1690E445024CB796E09 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6688907CE0F1BF85B4A1ED07A4FD73BB /* Result.swift */; }; + A9CCA2DCDE1D1EA45BAD74C5CB3E9253 /* NSLayoutConstraint+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 912F4E96A8B3BB480A20592F7C6B709A /* NSLayoutConstraint+Extensions.swift */; }; + AB3D4BBA18DEA9DDCB82E15EE6DA80FE /* IGListDiffable.h in Headers */ = {isa = PBXBuildFile; fileRef = 03C406CF79037D94F19036ADEC84DAC7 /* IGListDiffable.h */; settings = {ATTRIBUTES = (Public, ); }; }; + ABAD2E2E0682AB7FF1C6213388E4EBA2 /* BaseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21A539BD413B6B92DD2DD6FBD9368394 /* BaseView.swift */; }; + AC348A3AA450BFA43A0B4869FB82772A /* SwiftMessages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6BEB7CE647AFB6BEE5DF438AE86B2F54 /* SwiftMessages.swift */; }; + AD707CFC669380B926AFB1FE7AE9B867 /* PhysicsAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 53D11C1A637638ADF43AE1EB8BD508A5 /* PhysicsAnimation.swift */; }; + AD951EA8C76B4AE7CFB4669804B8275B /* IGListBindingSectionControllerDataSource.h in Headers */ = {isa = PBXBuildFile; fileRef = AD580318635BBFC7B1A532C8F86EE9B8 /* IGListBindingSectionControllerDataSource.h */; settings = {ATTRIBUTES = (Public, ); }; }; + AEB007C7128575842880AD5559D2D5AC /* MessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8452BF3EA61C1D6EC6A9DCAB1C65173D /* MessageView.swift */; }; + AFCAEA6F64C31BBCA51EDE0A22C971FF /* CornerRoundingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 321AEF56ADD9B574B8E97C6E37AE6C36 /* CornerRoundingView.swift */; }; + B1DAC65FFC831F6D51EAC90BD4B4D92D /* IGListWorkingRangeHandler.h in Headers */ = {isa = PBXBuildFile; fileRef = CFFD614E723A7CA1CF56C4F27C14BB2C /* IGListWorkingRangeHandler.h */; settings = {ATTRIBUTES = (Private, ); }; }; + B25E07EA645911443A38DA1E68166156 /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3BE8BFF931E90A8B8F751859A979D9FF /* Box.swift */; }; + B5E02A83F6C06F6D285E2EB1365A07BE /* IGListSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = FF11DBFDB6B6CF5A0D1EA3D416156A68 /* IGListSectionController.m */; }; + B707A3EC8581E09D0BA10908C235D401 /* IGListKit-dummy.m in Sources */ = {isa = PBXBuildFile; fileRef = CFAF508ECC19E5ED7EE57D68482D8B2D /* IGListKit-dummy.m */; }; + B7384B67D262049B7A3D9A5F44CF3AD8 /* successIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = DBE15455BF19E1F22DA0EA9923EDDD39 /* successIconSubtle.png */; }; + BB2B1C8268535C8BB056EB34B907FE16 /* WindowScene.swift in Sources */ = {isa = PBXBuildFile; fileRef = E947DADE1C4D71798D03004B57C09B12 /* WindowScene.swift */; }; + BC34CECDF734877FE9651B831504D3F2 /* infoIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6AAB51521257D54CBB370D9F3D32C816 /* infoIconLight@2x.png */; }; + BD382E78580D295D10100678D4F66A76 /* String+MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37B13934C4541B84DCD0CE345BE17813 /* String+MD5.swift */; }; + BF8F8C64DB7DBEC09546AE2C68B1929B /* IGListSectionControllerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = BFA65400774FF943C82530BA1C6FBFD5 /* IGListSectionControllerInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + BFAB4C4DAEFAAE316F3F7006D0E7F45B /* NSBundle+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 98C6563DD55C21E71E4E2CCB99C78C95 /* NSBundle+Extensions.swift */; }; + C0BB4FD34FBDE2C9E8571DD42F56FDD3 /* IGListAdapterInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 7DAFB222C9BA9663B8F1E40709DBDCA6 /* IGListAdapterInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + C0E2F479332ED0297D2152651EEBBC32 /* IGListAdapter.m in Sources */ = {isa = PBXBuildFile; fileRef = 1DC6890537D77DABA2EDB7D0C82839D8 /* IGListAdapter.m */; }; + C111B17154D65AFC9862B663697D1C5C /* warningIcon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = B1E88B937D987546110BF9126ADB22B0 /* warningIcon@2x.png */; }; + C3CE74410F050DCA5E2C8E1FF26A1125 /* IGListAdapter+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 9200674997CD712EE42F9FEF1B4F1D25 /* IGListAdapter+DebugDescription.m */; }; + C3F1FE6BB2F3E83DF225DB091D768ED3 /* IGListBatchContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 23A7B6D2D0BE30FBDCFEFC0841849D05 /* IGListBatchContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C5814CF9B5EB63D89CBE27EE2A19520A /* IGListGenericSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 38C2E0D5356E1E9F90038322D59A5320 /* IGListGenericSectionController.m */; }; + C78B03AD94E9ADA8E4A492D852A4036D /* IGListBindingSectionController+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 6EAD7EC2840F995AE8ABAC9C2B4CF5EA /* IGListBindingSectionController+DebugDescription.h */; settings = {ATTRIBUTES = (Private, ); }; }; + C8E736F2681D5A47031328BE6DBAF0D6 /* IGListWorkingRangeHandler.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05BF6B6F8ADAC5E7BC99DE8A118BF6B6 /* IGListWorkingRangeHandler.mm */; }; + C8ED20CFCDFF904961EA7D6FCFAD1AAF /* errorIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = 5088061C5D0ECA51E28E1D5C9C05CDF2 /* errorIcon.png */; }; + CC2D622535AED6D39919BAAD8B55B828 /* IGListSectionMap+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 93DFC745DDF011893D575891F77993E4 /* IGListSectionMap+DebugDescription.m */; }; + CD7AC3E1C98EA54F7C05C36C52805220 /* CacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92E3930B2051A71F0FD7F48F8E97C348 /* CacheSerializer.swift */; }; + D005E9A215793DD5E318F94052C45350 /* UIColorInputError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCCD5F85BBD560CCB2EB64FAB0B1F00B /* UIColorInputError.swift */; }; + D05EC2EA72C86F0F25AC384102DE7238 /* IGListDiff.mm in Sources */ = {isa = PBXBuildFile; fileRef = 974BF1702E4695985E60FBB45338A8A7 /* IGListDiff.mm */; }; + D0D540212AFD7154B4C9207A9E5C911A /* IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 6D1F660EA325793B882E173B84016B9B /* IGListKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + D1354255D2A30A71D2915BD7A0D10960 /* successIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = CFA306987494C26AB6494A2C70A32AE3 /* successIconSubtle@3x.png */; }; + D603AA58EF97D461A57B2B1BCB883868 /* AVAssetImageDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 802CFB873BF930917357D60645A37371 /* AVAssetImageDataProvider.swift */; }; + D8332E09169A7A9D00E0398D4392DB70 /* IGListCollectionContext.h in Headers */ = {isa = PBXBuildFile; fileRef = CA7B91D095A2146E7CB26C435BB87D78 /* IGListCollectionContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DA36BB36699075B706863F8E6462F524 /* IGListBatchUpdateData+DebugDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 37B5D75335F8694F04630AECE5892B47 /* IGListBatchUpdateData+DebugDescription.h */; settings = {ATTRIBUTES = (Private, ); }; }; + DA537286B85140EC0B108E803DFB545E /* IGListMoveIndex.m in Sources */ = {isa = PBXBuildFile; fileRef = 28E5CF6FD87B92B31A35248BCCC7BDBE /* IGListMoveIndex.m */; }; + DAFC6CE6321395CF4523DD66DADBB9BA /* ImageDrawing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24C3AE2697C2C777471A4F671EFB544B /* ImageDrawing.swift */; }; + DBB8088E14A2ADEDB1CD840BAC835267 /* CPListItem+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = A653BDF17479F16550C74F6CE6EA62F6 /* CPListItem+Kingfisher.swift */; }; + DC576EBDA8822307BFAA01B1E07EB6B2 /* IGListSingleSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 2050EB0365A402B74F479EC3044072B2 /* IGListSingleSectionController.m */; }; + DD46E684526BF2A6315775DEBCA54960 /* warningIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 01CD4B0B843A3B4A985651F253653952 /* warningIconSubtle@3x.png */; }; + DD72DC30CF19FFC81AB19CD0B074000D /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCD553D3612DC6CC76F894AF0823A794 /* ImageDownloader.swift */; }; + DD87BCC0696946A029E25B638BFB1971 /* AccessibleMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = D1F58388A6DDB3EF0B970B865F5EDB38 /* AccessibleMessage.swift */; }; + DDA74D6EBFE54AC3941181507F8060AB /* IGListDebuggingUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = AC0554DE66BE45142D0D9A060832228E /* IGListDebuggingUtilities.m */; }; + DE532EF7D50A9CF68587DAD4C1A02BD7 /* FormatIndicatedCacheSerializer.swift in Sources */ = {isa = PBXBuildFile; fileRef = EC56036E713E5F3E1D5CF743FB2E30CE /* FormatIndicatedCacheSerializer.swift */; }; + DF12FA0162AEB2360E9D896FFCE3D8FD /* IGListAdapterUpdaterDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 21DAC60AD8416D4C4DC7ECBCA6C41E55 /* IGListAdapterUpdaterDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + DF4563832C19B8582C810BF502A5CA29 /* KF.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0150DA8D834C8B8F37D02689D618EB7 /* KF.swift */; }; + DFCDE4638265B4CCD494ECA5D560DBEE /* Indicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDB7B3024FA7207F2C442C4AB05907D7 /* Indicator.swift */; }; + E0B5B9AA33DBB50133BF1886B076688A /* IGListSingleSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = BB9D49792750AB8422232444B4D6E947 /* IGListSingleSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E1128C6D632504B9D7AF99B3838D3DCC /* IGListAdapterProxy.h in Headers */ = {isa = PBXBuildFile; fileRef = 6A2524ADEFD63F868610552BEE2C047D /* IGListAdapterProxy.h */; settings = {ATTRIBUTES = (Private, ); }; }; + E290630326514476CA3D4651D3FB2C12 /* SwiftMessages-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = 881690ABF91DF5E1AFB107C1A817B51D /* SwiftMessages-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E362C21CF43363A849410617D9B0A524 /* IGListBatchUpdateData.mm in Sources */ = {isa = PBXBuildFile; fileRef = 369EB2D2F32E873F3665AE2FB0A59DB9 /* IGListBatchUpdateData.mm */; }; + E4B7D3D7779F1394C8104CF3615EC223 /* IGListBatchUpdateData+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = DF5C02A0D1A2BFFC1A6EE72656191F77 /* IGListBatchUpdateData+DebugDescription.m */; }; + E5B664771063F1A9A372519A8466860B /* ImageView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 453ED79EFF2CFCB0BB28AAFB6728A746 /* ImageView+Kingfisher.swift */; }; + E5FEA90E76B8D19728AE8D30969EB744 /* IGListAdapterProxy.m in Sources */ = {isa = PBXBuildFile; fileRef = 35E7C9FAC1444B886148B40E4E03A10A /* IGListAdapterProxy.m */; }; + E6D6C7D5E458A05CC736C340F853E9F6 /* ImageFormat.swift in Sources */ = {isa = PBXBuildFile; fileRef = A88C71355EB0F8098D7C610310E5ADF9 /* ImageFormat.swift */; }; + E719A3B025B9DACE693130120BD9B927 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = EB56C884B7F74971F1D47C11641BD905 /* Accelerate.framework */; }; + E7E6D0A1C2F66770BE1A484A01B01A63 /* IGListAssert.h in Headers */ = {isa = PBXBuildFile; fileRef = 969EC8B144DCFDBB8D6B20563FD767D8 /* IGListAssert.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EB11BE0DBFDA8DFB6DA4A0C9AA39DEAE /* IGListUpdatingDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 36AA5F43CA08202B3651464A17D2DC8E /* IGListUpdatingDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EBB32304E8DD4BA115454E0050D47DED /* Runtime.swift in Sources */ = {isa = PBXBuildFile; fileRef = EBDB16CE65088B7BAADD99ED81716273 /* Runtime.swift */; }; + ECFA80EB6B070B2B500F725449368B48 /* UICollectionViewLayout+InteractiveReordering.m in Sources */ = {isa = PBXBuildFile; fileRef = BF5FCF915C68DC413EF7C3DC3908EA10 /* UICollectionViewLayout+InteractiveReordering.m */; }; + ED0C8BA7560D7324587B353E0960479F /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3579EDF0F2B23FDC5BD85C5B34DC0AD6 /* ImageCache.swift */; }; + EE2007369A32179A1540374E29C343AE /* MarginAdjustable+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB06B0638434865F6E2DA73FF648F3A6 /* MarginAdjustable+Extensions.swift */; }; + EE3740A3D6C42F3766288ED5BCB8B95D /* errorIconLight@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 8079F476C78D97C2FAE6BD81EEBC744F /* errorIconLight@2x.png */; }; + EE402590604403A03F734B8AD641AAD9 /* IGListCompatibility.h in Headers */ = {isa = PBXBuildFile; fileRef = 15FF74DFD7DA6973B4D076F529572592 /* IGListCompatibility.h */; settings = {ATTRIBUTES = (Public, ); }; }; + EF73165F049DB4864538A394FFCBE874 /* PassthroughView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E008BF5B47FEF861254F08D2F41B94E2 /* PassthroughView.swift */; }; + EF9C4588CDA85AED8BBCF77451B2A35B /* ImageProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74DA03441BD1BDF74130264922D71B19 /* ImageProcessor.swift */; }; + F105D53A3A5A848EA428305ACF3FDBBB /* IGListBindingSectionController+DebugDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = EB6036A78A4C36D50745E215A018095B /* IGListBindingSectionController+DebugDescription.m */; }; + F17B1F8F2B6580343025237455A29D61 /* TVMonogramView+Kingfisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71985F3B3B4297E61C48A92C40B847D8 /* TVMonogramView+Kingfisher.swift */; }; + F209D62202BD5D087E33685E52308500 /* IGListIndexPathResult.m in Sources */ = {isa = PBXBuildFile; fileRef = E8101380E5E87B4F4EC6DDD9D53C9667 /* IGListIndexPathResult.m */; }; + F24021BDE9B42D604E3341CAD8E34759 /* GIFAnimatedImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1D250E60DB977C9D0BFF53ABCA2A997C /* GIFAnimatedImage.swift */; }; + F28F7B608086B6126003F02D8F79D79F /* IGListAdapterDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = A3B9660742F1460FA534B5C6DD0333B8 /* IGListAdapterDelegate.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F2DC134E310796CDE38903CADC436726 /* Pods-Bulletin-umbrella.h in Headers */ = {isa = PBXBuildFile; fileRef = AEA97C6B1B634B2233D0ABCD79202C8E /* Pods-Bulletin-umbrella.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F4852EA1DC3E1F1F346402EAEFE1109F /* infoIcon.png in Resources */ = {isa = PBXBuildFile; fileRef = D6597118FC7A651AD1C1B8186C857884 /* infoIcon.png */; }; + F5414F8A5B40521D0E4AEEB28378CB49 /* ImageContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = E69D9C2CA746D767CAFF47CD3F8F0BB8 /* ImageContext.swift */; }; + F54DE563418B1783D6EC491A0C3A05DB /* ImageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41EE63D598C9A869E2FAAF99E10B90 /* ImageModifier.swift */; }; + F5FC561E72803649ED1023E463AB9D17 /* IGListBindingSectionController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3CD8D6758B3642D5415A451F5CB22167 /* IGListBindingSectionController.m */; }; + F6CA0DA52CCF6EB9B47B22196FA14E3E /* IGListGenericSectionController.h in Headers */ = {isa = PBXBuildFile; fileRef = 8578B293CF58ED79CF3BB15C593A1811 /* IGListGenericSectionController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + F7EF7184D41C22A77D70A2AE233D4E53 /* IGListMoveIndexInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 5892D28E2B92F79C8909C1A4304EDE19 /* IGListMoveIndexInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + F9537B023E24AC4A724E301F7E372491 /* KFImageProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0662908419CF488E8FE10FE90D80D493 /* KFImageProtocol.swift */; }; + FA006B42B99DAF8263341881FD510FF9 /* PhysicsPanHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1510A0DCF8427B970410D4A435FA3984 /* PhysicsPanHandler.swift */; }; + FB1227598B1A07A4E0FC9AEC4F60E144 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9060464159E13D7209F7D64BACF83DAD /* Foundation.framework */; }; + FC9E1CBF800DFB8FE8F0E29D1CB8EA1A /* IGListMoveIndexInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 308B9DAEAE417FCC8AB9D385110BAB88 /* IGListMoveIndexInternal.h */; settings = {ATTRIBUTES = (Private, ); }; }; + FF09824309346665E2F1F7F5A45FB10F /* CallbackQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCB4CB332EC3A05BBA96845240C3B5C9 /* CallbackQueue.swift */; }; + FF3EB79404B1F1AF27E7298F4DF62FA2 /* NSString+IGListDiffable.m in Sources */ = {isa = PBXBuildFile; fileRef = 588967168C815A5579F4C6C0A3ADF4CA /* NSString+IGListDiffable.m */; }; + FF467117653668E74E2DA504B3BC3547 /* infoIconSubtle@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = D9B697009E9B2897D4182C96CC40388B /* infoIconSubtle@2x.png */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 1B3792504F0EED9EE18F016548B44086 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 674FDCAB4D51E702521F4CD31807F659; + remoteInfo = IGListKit; + }; + 55802A5FB5EBEB01607325E334C83585 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 93789445511E3FEAB3D09A6F552EEE2D; + remoteInfo = "SwiftMessages-SwiftMessages"; + }; + 66837020B415409BD9EC27EC5943B9A9 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2D07A05F514BF9C5434DAC907988C708; + remoteInfo = IGListDiffKit; + }; + 800A33BC263765E54E7FBE37BFEC3000 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = DAB613A18652334F6BFC5F27BADF515D; + remoteInfo = SwiftMessages; + }; + BD3BFF283976737AF4A31E0233395338 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E8022D22FAA6690B5E1C379C1BCE1491; + remoteInfo = Kingfisher; + }; + BECFFC24423DFC78A02F544B59390655 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2D07A05F514BF9C5434DAC907988C708; + remoteInfo = IGListDiffKit; + }; + E2E98D973191FE598F2C7D4D813CDBD5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = BFDFE7DC352907FC980B868725387E98 /* Project object */; + proxyType = 1; + remoteGlobalIDString = A0CA3B0841360687C00D0E48D3E4C1B9; + remoteInfo = UIColor_Hex_Swift; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 007F365850B5C8A7EEA9E7F41F325B11 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; + 01CD4B0B843A3B4A985651F253653952 /* warningIconSubtle@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIconSubtle@3x.png"; path = "SwiftMessages/Resources/warningIconSubtle@3x.png"; sourceTree = ""; }; + 01DFF373D32B02827C3E52EB2CD2A3FE /* MaskingView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MaskingView.swift; path = SwiftMessages/MaskingView.swift; sourceTree = ""; }; + 03C406CF79037D94F19036ADEC84DAC7 /* IGListDiffable.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListDiffable.h; path = Source/IGListDiffKit/IGListDiffable.h; sourceTree = ""; }; + 0495A674038D17605C95A9A5B9213100 /* IGListDiff.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListDiff.h; path = Source/IGListDiffKit/IGListDiff.h; sourceTree = ""; }; + 05BF6B6F8ADAC5E7BC99DE8A118BF6B6 /* IGListWorkingRangeHandler.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = IGListWorkingRangeHandler.mm; path = Source/IGListKit/Internal/IGListWorkingRangeHandler.mm; sourceTree = ""; }; + 063A43A32CE98A3E1C417772EB31679E /* warningIconSubtle.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = warningIconSubtle.png; path = SwiftMessages/Resources/warningIconSubtle.png; sourceTree = ""; }; + 0662908419CF488E8FE10FE90D80D493 /* KFImageProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageProtocol.swift; path = Sources/SwiftUI/KFImageProtocol.swift; sourceTree = ""; }; + 07017C809FA0F80CF1A57230031F6028 /* UIColor_Hex_Swift.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = UIColor_Hex_Swift.modulemap; sourceTree = ""; }; + 07BC0E95B093644150AD1D0260560C1D /* IGListIndexSetResultInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListIndexSetResultInternal.h; path = Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h; sourceTree = ""; }; + 08510568CA8F1AEEDE5B0E32306DC71A /* successIconLight@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIconLight@3x.png"; path = "SwiftMessages/Resources/successIconLight@3x.png"; sourceTree = ""; }; + 09BA0051E9F4F47215585B6787C85C96 /* MarginAdjustable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MarginAdjustable.swift; path = SwiftMessages/MarginAdjustable.swift; sourceTree = ""; }; + 0A2F39EF1CE1C157494607E6C1FE66A7 /* IGListBatchUpdateState.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListBatchUpdateState.h; path = Source/IGListKit/Internal/IGListBatchUpdateState.h; sourceTree = ""; }; + 0C09F9726B03BEFB76DB54470EFA079E /* Delegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Delegate.swift; path = Sources/Utility/Delegate.swift; sourceTree = ""; }; + 0C41EE63D598C9A869E2FAAF99E10B90 /* ImageModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageModifier.swift; path = Sources/Networking/ImageModifier.swift; sourceTree = ""; }; + 0E56A83EDE9F898293353CD761ED323A /* Identifiable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Identifiable.swift; path = SwiftMessages/Identifiable.swift; sourceTree = ""; }; + 0E783AAB1D182A47CE9A4C70AEA15DC7 /* errorIconSubtle.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = errorIconSubtle.png; path = SwiftMessages/Resources/errorIconSubtle.png; sourceTree = ""; }; + 0F789B805ED414860F2DC4C723AFE42D /* ImageTransition.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageTransition.swift; path = Sources/Image/ImageTransition.swift; sourceTree = ""; }; + 0F78C89D7312191E330B806A85A69672 /* Kingfisher-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Kingfisher-Info.plist"; sourceTree = ""; }; + 1091A7C7D19C45920D87A3EAA1AA6C0F /* CenteredView.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = CenteredView.xib; path = SwiftMessages/Resources/CenteredView.xib; sourceTree = ""; }; + 10FD14B174196E8692A3FF6CE273842B /* Kingfisher-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Kingfisher-dummy.m"; sourceTree = ""; }; + 111AD5BA834CFAE0B8CDB2B897047D06 /* IGListAdapterPerformanceDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterPerformanceDelegate.h; path = Source/IGListKit/IGListAdapterPerformanceDelegate.h; sourceTree = ""; }; + 118EB5557B6BA57C54ECE70977225895 /* IGListBindingSectionController.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListBindingSectionController.h; path = Source/IGListKit/IGListBindingSectionController.h; sourceTree = ""; }; + 1351EB6B5E88098E59C3F20E89B2004C /* UIScrollView+IGListKit.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UIScrollView+IGListKit.h"; path = "Source/IGListKit/Internal/UIScrollView+IGListKit.h"; sourceTree = ""; }; + 14D8F73E1EEE92B0BB6C4A4862D834A9 /* IGListCollectionViewLayoutInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListCollectionViewLayoutInternal.h; path = Source/IGListKit/Internal/IGListCollectionViewLayoutInternal.h; sourceTree = ""; }; + 1510A0DCF8427B970410D4A435FA3984 /* PhysicsPanHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PhysicsPanHandler.swift; path = SwiftMessages/PhysicsPanHandler.swift; sourceTree = ""; }; + 15FF74DFD7DA6973B4D076F529572592 /* IGListCompatibility.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListCompatibility.h; path = Source/IGListDiffKit/IGListCompatibility.h; sourceTree = ""; }; + 17CAC30BEB11B3613D6C1DFA0950F7E9 /* successIconLight.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = successIconLight.png; path = SwiftMessages/Resources/successIconLight.png; sourceTree = ""; }; + 1899BB102CCB03C961D392531F94109C /* warningIcon.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = warningIcon.png; path = SwiftMessages/Resources/warningIcon.png; sourceTree = ""; }; + 18E261DDB018DEF41EF92A4794D0CC81 /* ImagePrefetcher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImagePrefetcher.swift; path = Sources/Networking/ImagePrefetcher.swift; sourceTree = ""; }; + 19002AA8D6680BDDAC1C8C5DDE64CD49 /* ImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProvider.swift; path = Sources/General/ImageSource/ImageDataProvider.swift; sourceTree = ""; }; + 195AD71F7938FEA988FDCC5B0A10FFDF /* IGListDiffKit */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = IGListDiffKit; path = IGListDiffKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1B08200E58B617675FE96006778840FB /* IGListDiffKit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = IGListDiffKit.release.xcconfig; sourceTree = ""; }; + 1CCB2C2A7F8FDEAB42ECE286206B4CF3 /* IGListKit-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "IGListKit-Info.plist"; sourceTree = ""; }; + 1D250E60DB977C9D0BFF53ABCA2A997C /* GIFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GIFAnimatedImage.swift; path = Sources/Image/GIFAnimatedImage.swift; sourceTree = ""; }; + 1DC6890537D77DABA2EDB7D0C82839D8 /* IGListAdapter.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListAdapter.m; path = Source/IGListKit/IGListAdapter.m; sourceTree = ""; }; + 1F3E90B88F5618F4D313769ED312089B /* SizeExtensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SizeExtensions.swift; path = Sources/Utility/SizeExtensions.swift; sourceTree = ""; }; + 2050EB0365A402B74F479EC3044072B2 /* IGListSingleSectionController.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListSingleSectionController.m; path = Source/IGListKit/IGListSingleSectionController.m; sourceTree = ""; }; + 20F98B848D2BF72F16DA1BDACF869D1A /* Pods-Bulletin */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = "Pods-Bulletin"; path = Pods_Bulletin.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 21A539BD413B6B92DD2DD6FBD9368394 /* BaseView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BaseView.swift; path = SwiftMessages/BaseView.swift; sourceTree = ""; }; + 21DAC60AD8416D4C4DC7ECBCA6C41E55 /* IGListAdapterUpdaterDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterUpdaterDelegate.h; path = Source/IGListKit/IGListAdapterUpdaterDelegate.h; sourceTree = ""; }; + 225916A84FF5B787DDE7EB83441E3470 /* KFImageRenderer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageRenderer.swift; path = Sources/SwiftUI/KFImageRenderer.swift; sourceTree = ""; }; + 23A7B6D2D0BE30FBDCFEFC0841849D05 /* IGListBatchContext.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListBatchContext.h; path = Source/IGListKit/IGListBatchContext.h; sourceTree = ""; }; + 24C3AE2697C2C777471A4F671EFB544B /* ImageDrawing.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDrawing.swift; path = Sources/Image/ImageDrawing.swift; sourceTree = ""; }; + 25FFBB29D9878FEFD402D15B13F96F2F /* SwiftMessages-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "SwiftMessages-dummy.m"; sourceTree = ""; }; + 2713DB42F5385245C2EC0D72720C5BE6 /* IGListDiffKit-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "IGListDiffKit-dummy.m"; sourceTree = ""; }; + 27B420D1608AC6A991CAF82584A9805B /* IGListAdapterDataSource.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterDataSource.h; path = Source/IGListKit/IGListAdapterDataSource.h; sourceTree = ""; }; + 28E5CF6FD87B92B31A35248BCCC7BDBE /* IGListMoveIndex.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListMoveIndex.m; path = Source/IGListDiffKit/IGListMoveIndex.m; sourceTree = ""; }; + 2987551134D451FC8A1024048B845790 /* successIconSubtle@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIconSubtle@2x.png"; path = "SwiftMessages/Resources/successIconSubtle@2x.png"; sourceTree = ""; }; + 2A1E479F8F71FB71F3C62C8450C4DC84 /* IGListCollectionScrollingTraits.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListCollectionScrollingTraits.h; path = Source/IGListKit/IGListCollectionScrollingTraits.h; sourceTree = ""; }; + 2A7E3CFE0BA27926F6ABFC0513EF1282 /* warningIconSubtle@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIconSubtle@2x.png"; path = "SwiftMessages/Resources/warningIconSubtle@2x.png"; sourceTree = ""; }; + 2B50C6FFEC0097EFE6FB31764A42CAFA /* Pods-Bulletin-acknowledgements.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Bulletin-acknowledgements.plist"; sourceTree = ""; }; + 2F78D97A20A66B010C3E1ABDA41FFA5F /* HEXColor.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = HEXColor.h; path = HEXColor/HEXColor.h; sourceTree = ""; }; + 2FA03600975416DCA7ED5BA333AC99DB /* Kingfisher-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-prefix.pch"; sourceTree = ""; }; + 2FFEB06874D80B62E4BA3E418B346091 /* ImageDownloaderDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloaderDelegate.swift; path = Sources/Networking/ImageDownloaderDelegate.swift; sourceTree = ""; }; + 30513773A49E99A4C75E6115DD8934EC /* IGListBindingSectionControllerSelectionDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListBindingSectionControllerSelectionDelegate.h; path = Source/IGListKit/IGListBindingSectionControllerSelectionDelegate.h; sourceTree = ""; }; + 308B9DAEAE417FCC8AB9D385110BAB88 /* IGListMoveIndexInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListMoveIndexInternal.h; path = Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h; sourceTree = ""; }; + 31363CA02DABFE037A9597E66D774E50 /* GraphicsContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = GraphicsContext.swift; path = Sources/Image/GraphicsContext.swift; sourceTree = ""; }; + 321AEF56ADD9B574B8E97C6E37AE6C36 /* CornerRoundingView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CornerRoundingView.swift; path = SwiftMessages/CornerRoundingView.swift; sourceTree = ""; }; + 34EC4406FF95B83EF37D06F08D66A633 /* TabView.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = TabView.xib; path = SwiftMessages/Resources/TabView.xib; sourceTree = ""; }; + 3579EDF0F2B23FDC5BD85C5B34DC0AD6 /* ImageCache.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageCache.swift; path = Sources/Cache/ImageCache.swift; sourceTree = ""; }; + 35E7C9FAC1444B886148B40E4E03A10A /* IGListAdapterProxy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListAdapterProxy.m; path = Source/IGListKit/Internal/IGListAdapterProxy.m; sourceTree = ""; }; + 369EB2D2F32E873F3665AE2FB0A59DB9 /* IGListBatchUpdateData.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = IGListBatchUpdateData.mm; path = Source/IGListDiffKit/IGListBatchUpdateData.mm; sourceTree = ""; }; + 36AA5F43CA08202B3651464A17D2DC8E /* IGListUpdatingDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListUpdatingDelegate.h; path = Source/IGListKit/IGListUpdatingDelegate.h; sourceTree = ""; }; + 37B13934C4541B84DCD0CE345BE17813 /* String+MD5.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "String+MD5.swift"; path = "Sources/Utility/String+MD5.swift"; sourceTree = ""; }; + 37B5D75335F8694F04630AECE5892B47 /* IGListBatchUpdateData+DebugDescription.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "IGListBatchUpdateData+DebugDescription.h"; path = "Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.h"; sourceTree = ""; }; + 38C2E0D5356E1E9F90038322D59A5320 /* IGListGenericSectionController.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListGenericSectionController.m; path = Source/IGListKit/IGListGenericSectionController.m; sourceTree = ""; }; + 3934169222833A8921F287D6737C3D94 /* Image.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Image.swift; path = Sources/Image/Image.swift; sourceTree = ""; }; + 39CF63638810AE1B4B6F37AF97A30C81 /* IGListAdapter+UICollectionView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "IGListAdapter+UICollectionView.m"; path = "Source/IGListKit/Internal/IGListAdapter+UICollectionView.m"; sourceTree = ""; }; + 3A977FC7DDB65D48636DAD282F3D1FB8 /* IGListKit-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "IGListKit-prefix.pch"; sourceTree = ""; }; + 3B0A6054CE428C4606ABFE6CB328CD2A /* IGListDiffKit-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "IGListDiffKit-prefix.pch"; sourceTree = ""; }; + 3B2A150E6AA79141831952D08C9CA44A /* errorIconLight@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIconLight@3x.png"; path = "SwiftMessages/Resources/errorIconLight@3x.png"; sourceTree = ""; }; + 3BE8BFF931E90A8B8F751859A979D9FF /* Box.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Box.swift; path = Sources/Utility/Box.swift; sourceTree = ""; }; + 3CD8D6758B3642D5415A451F5CB22167 /* IGListBindingSectionController.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListBindingSectionController.m; path = Source/IGListKit/IGListBindingSectionController.m; sourceTree = ""; }; + 3F4E7F4C4B0ED07AA5930C2E3D5B8FD6 /* IGListTransitionDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListTransitionDelegate.h; path = Source/IGListKit/IGListTransitionDelegate.h; sourceTree = ""; }; + 424235E351E1EEF73A585E53FCCE5931 /* UICollectionView+DebugDescription.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UICollectionView+DebugDescription.m"; path = "Source/IGListKit/Internal/UICollectionView+DebugDescription.m"; sourceTree = ""; }; + 453ED79EFF2CFCB0BB28AAFB6728A746 /* ImageView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "ImageView+Kingfisher.swift"; path = "Sources/Extensions/ImageView+Kingfisher.swift"; sourceTree = ""; }; + 461E8E5C8D92714274A6A58953D195C9 /* Theme.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Theme.swift; path = SwiftMessages/Theme.swift; sourceTree = ""; }; + 4824F23D80FF9070A5F8A452DB11EB9A /* SwiftMessages */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = SwiftMessages; path = SwiftMessages.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4891D530CDAA004230740CADC793F34C /* UIColor_Hex_Swift-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "UIColor_Hex_Swift-Info.plist"; sourceTree = ""; }; + 49FFAA7BEFB9D929E25C31979D44D129 /* TopBottomAnimation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = TopBottomAnimation.swift; path = SwiftMessages/TopBottomAnimation.swift; sourceTree = ""; }; + 4A38804BDF6E084423F6ED17E2119797 /* IGListCollectionViewDelegateLayout.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListCollectionViewDelegateLayout.h; path = Source/IGListKit/IGListCollectionViewDelegateLayout.h; sourceTree = ""; }; + 4B0D3AE0230F985AC1ED5587C52B99DB /* Resource.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Resource.swift; path = Sources/General/ImageSource/Resource.swift; sourceTree = ""; }; + 4BC79CF900824AEA4ED4B584BE07F257 /* IGListCollectionViewLayout.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListCollectionViewLayout.h; path = Source/IGListKit/IGListCollectionViewLayout.h; sourceTree = ""; }; + 4BED2EF6A3B24B9C0BF0E9524B7B6436 /* IGListIndexPathResult.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListIndexPathResult.h; path = Source/IGListDiffKit/IGListIndexPathResult.h; sourceTree = ""; }; + 4CDF76905EE491F6C6CEC1B70BC440D6 /* Presenter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Presenter.swift; path = SwiftMessages/Presenter.swift; sourceTree = ""; }; + 4D59E7CA06D24D19520DA2615F034822 /* IGListAdapterUpdater.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListAdapterUpdater.m; path = Source/IGListKit/IGListAdapterUpdater.m; sourceTree = ""; }; + 4DB2CFEA6FDD21B6081185B3729B51D3 /* errorIconSubtle@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIconSubtle@2x.png"; path = "SwiftMessages/Resources/errorIconSubtle@2x.png"; sourceTree = ""; }; + 4E3CE7DF25AAEC00FF8F06896801111B /* IGListDisplayHandler.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListDisplayHandler.m; path = Source/IGListKit/Internal/IGListDisplayHandler.m; sourceTree = ""; }; + 4E92132F1FBCAEC55FD846EB03A29C5C /* NSNumber+IGListDiffable.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSNumber+IGListDiffable.m"; path = "Source/IGListDiffKit/NSNumber+IGListDiffable.m"; sourceTree = ""; }; + 4EB7D9E14485B011E821B2552487BFFF /* UICollectionView+IGListBatchUpdateData.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UICollectionView+IGListBatchUpdateData.m"; path = "Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.m"; sourceTree = ""; }; + 5088061C5D0ECA51E28E1D5C9C05CDF2 /* errorIcon.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = errorIcon.png; path = SwiftMessages/Resources/errorIcon.png; sourceTree = ""; }; + 517254B0F9ADBAC88606A8124B700550 /* SwiftMessages.Config+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "SwiftMessages.Config+Extensions.swift"; path = "SwiftMessages/SwiftMessages.Config+Extensions.swift"; sourceTree = ""; }; + 51A70695551771573DC2AA2EBA4C1856 /* IGListMacros.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListMacros.h; path = Source/IGListDiffKit/IGListMacros.h; sourceTree = ""; }; + 51E9A430A8CB278156CC969E105579DA /* NSNumber+IGListDiffable.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSNumber+IGListDiffable.h"; path = "Source/IGListDiffKit/NSNumber+IGListDiffable.h"; sourceTree = ""; }; + 52372AE9D7E296FCED90D2D5BA9ECB4D /* AnimatedImageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AnimatedImageView.swift; path = Sources/Views/AnimatedImageView.swift; sourceTree = ""; }; + 526AFE8B2C5F857BB1C8ACF879C0ECCE /* IGListIndexPathResultInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListIndexPathResultInternal.h; path = Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h; sourceTree = ""; }; + 53BF7416BD4A970D1B5E4B01F294E586 /* Placeholder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Placeholder.swift; path = Sources/Image/Placeholder.swift; sourceTree = ""; }; + 53D11C1A637638ADF43AE1EB8BD508A5 /* PhysicsAnimation.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PhysicsAnimation.swift; path = SwiftMessages/PhysicsAnimation.swift; sourceTree = ""; }; + 54AF051AB7BCD460987153D0685F73B2 /* KeyboardTrackingView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KeyboardTrackingView.swift; path = SwiftMessages/KeyboardTrackingView.swift; sourceTree = ""; }; + 5637AD2FC80473D851AC233FF4B3B506 /* SwiftMessages-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "SwiftMessages-Info.plist"; sourceTree = ""; }; + 588967168C815A5579F4C6C0A3ADF4CA /* NSString+IGListDiffable.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "NSString+IGListDiffable.m"; path = "Source/IGListDiffKit/NSString+IGListDiffable.m"; sourceTree = ""; }; + 5892D28E2B92F79C8909C1A4304EDE19 /* IGListMoveIndexInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListMoveIndexInternal.h; path = Source/IGListDiffKit/Internal/IGListMoveIndexInternal.h; sourceTree = ""; }; + 58FA08FD4595D3CA9B2AA81405DE2EFA /* UIColor_Hex_Swift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = UIColor_Hex_Swift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5CF48A29FE13ED204720994FA4997255 /* IGListAdapter+UICollectionView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "IGListAdapter+UICollectionView.h"; path = "Source/IGListKit/Internal/IGListAdapter+UICollectionView.h"; sourceTree = ""; }; + 6232B2401FA0928C2269BFD66C421986 /* Weak.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Weak.swift; path = SwiftMessages/Weak.swift; sourceTree = ""; }; + 635E1C7D580143C90A7C99F9B6145CC7 /* warningIconLight@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIconLight@2x.png"; path = "SwiftMessages/Resources/warningIconLight@2x.png"; sourceTree = ""; }; + 645A2EAB5E61532B3ABBCEBC6AF722C8 /* infoIconSubtle.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = infoIconSubtle.png; path = SwiftMessages/Resources/infoIconSubtle.png; sourceTree = ""; }; + 648D06A574C08D26489A675BF3F8CC97 /* infoIcon@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIcon@3x.png"; path = "SwiftMessages/Resources/infoIcon@3x.png"; sourceTree = ""; }; + 650F2EE29CA64B40F86449904824F62E /* UIColor_Hex_Swift.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = UIColor_Hex_Swift.debug.xcconfig; sourceTree = ""; }; + 65601298C6B1A422CDB2DC07123D1E51 /* KingfisherManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherManager.swift; path = Sources/General/KingfisherManager.swift; sourceTree = ""; }; + 6594CBDE5CC688CF8DBC51236163605D /* IGListReloadIndexPath.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListReloadIndexPath.m; path = Source/IGListKit/Internal/IGListReloadIndexPath.m; sourceTree = ""; }; + 65A2E0145D001B9CF4A7EAD17835A07F /* SwiftMessages.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = SwiftMessages.modulemap; sourceTree = ""; }; + 6688907CE0F1BF85B4A1ED07A4FD73BB /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Sources/Utility/Result.swift; sourceTree = ""; }; + 67AD88EDD5E2B4BE3251E36338822D75 /* IGListAdapterUpdaterInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterUpdaterInternal.h; path = Source/IGListKit/Internal/IGListAdapterUpdaterInternal.h; sourceTree = ""; }; + 67E342B558FA9E3757D37EEB2E4D12A3 /* warningIconLight.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = warningIconLight.png; path = SwiftMessages/Resources/warningIconLight.png; sourceTree = ""; }; + 67EB0E60915C51E53FA40EB4CB81BAC9 /* IGListAdapterUpdateListener.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterUpdateListener.h; path = Source/IGListKit/IGListAdapterUpdateListener.h; sourceTree = ""; }; + 68248F05BB6436D9514BD865E5AEFD53 /* IGListSectionMap.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListSectionMap.h; path = Source/IGListKit/Internal/IGListSectionMap.h; sourceTree = ""; }; + 6A2524ADEFD63F868610552BEE2C047D /* IGListAdapterProxy.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterProxy.h; path = Source/IGListKit/Internal/IGListAdapterProxy.h; sourceTree = ""; }; + 6A7C4CC9232AC5706CBAB7721F33DE5B /* errorIcon@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIcon@3x.png"; path = "SwiftMessages/Resources/errorIcon@3x.png"; sourceTree = ""; }; + 6AAB51521257D54CBB370D9F3D32C816 /* infoIconLight@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIconLight@2x.png"; path = "SwiftMessages/Resources/infoIconLight@2x.png"; sourceTree = ""; }; + 6AD3632ED786F7D034F9E1A6EF5B3DAE /* IGListDebuggingUtilities.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListDebuggingUtilities.h; path = Source/IGListKit/Internal/IGListDebuggingUtilities.h; sourceTree = ""; }; + 6BEB7CE647AFB6BEE5DF438AE86B2F54 /* SwiftMessages.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwiftMessages.swift; path = SwiftMessages/SwiftMessages.swift; sourceTree = ""; }; + 6D1F660EA325793B882E173B84016B9B /* IGListKit.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListKit.h; path = Source/IGListKit/IGListKit.h; sourceTree = ""; }; + 6DEBD199402AB8F5B24357217344B70B /* Pods-Bulletin.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Bulletin.debug.xcconfig"; sourceTree = ""; }; + 6E32E82CDA2470BB9DD50373EE56E2EC /* Pods-Bulletin.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = "Pods-Bulletin.modulemap"; sourceTree = ""; }; + 6EAD7EC2840F995AE8ABAC9C2B4CF5EA /* IGListBindingSectionController+DebugDescription.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "IGListBindingSectionController+DebugDescription.h"; path = "Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.h"; sourceTree = ""; }; + 709704C7D88AAF115EDAB039595D266D /* IGListDebugger.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListDebugger.m; path = Source/IGListKit/Internal/IGListDebugger.m; sourceTree = ""; }; + 71985F3B3B4297E61C48A92C40B847D8 /* TVMonogramView+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "TVMonogramView+Kingfisher.swift"; path = "Sources/Extensions/TVMonogramView+Kingfisher.swift"; sourceTree = ""; }; + 7288C4A89C8BEFB77574C7581276301B /* infoIconSubtle@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIconSubtle@3x.png"; path = "SwiftMessages/Resources/infoIconSubtle@3x.png"; sourceTree = ""; }; + 72DAB1212901F5B52E7093E822D23577 /* IGListReloadIndexPath.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListReloadIndexPath.h; path = Source/IGListKit/Internal/IGListReloadIndexPath.h; sourceTree = ""; }; + 72EDEFFEFD9FA17167C3024ED8D81D74 /* infoIconLight.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = infoIconLight.png; path = SwiftMessages/Resources/infoIconLight.png; sourceTree = ""; }; + 73660183B3F95FADC4C3F189620E5797 /* CardView.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = CardView.xib; path = SwiftMessages/Resources/CardView.xib; sourceTree = ""; }; + 7388592716E23DB7918BA1CEF956BA19 /* CFNetwork.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CFNetwork.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/CFNetwork.framework; sourceTree = DEVELOPER_DIR; }; + 74DA03441BD1BDF74130264922D71B19 /* ImageProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProcessor.swift; path = Sources/Image/ImageProcessor.swift; sourceTree = ""; }; + 74F450974763BCC065DF7162314F180D /* IGListKit.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = IGListKit.modulemap; sourceTree = ""; }; + 753DD909979478A0C1B33E0524504EF1 /* SwiftMessages-SwiftMessages */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; name = "SwiftMessages-SwiftMessages"; path = SwiftMessages.bundle; sourceTree = BUILT_PRODUCTS_DIR; }; + 759C0577310E0EFEB52904517125DCF7 /* IGListExperiments.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListExperiments.h; path = Source/IGListDiffKit/IGListExperiments.h; sourceTree = ""; }; + 77CDFE36E22AF91849D4D87D3DD75B16 /* IGListArrayUtilsInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListArrayUtilsInternal.h; path = Source/IGListKit/Internal/IGListArrayUtilsInternal.h; sourceTree = ""; }; + 793410A19FBFCCE0BF378A5C8CF49BFA /* KFImageOptions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImageOptions.swift; path = Sources/SwiftUI/KFImageOptions.swift; sourceTree = ""; }; + 7AC803E40D56EEDCA7EE11F40F5D2873 /* IGListIndexSetResult.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListIndexSetResult.h; path = Source/IGListDiffKit/IGListIndexSetResult.h; sourceTree = ""; }; + 7C7F743B6CB541B1A49D0F89CDE913C6 /* IGListCollectionView.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListCollectionView.m; path = Source/IGListKit/IGListCollectionView.m; sourceTree = ""; }; + 7CB626D861306564520283F74F854D64 /* Kingfisher.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.debug.xcconfig; sourceTree = ""; }; + 7DAFB222C9BA9663B8F1E40709DBDCA6 /* IGListAdapterInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterInternal.h; path = Source/IGListKit/Internal/IGListAdapterInternal.h; sourceTree = ""; }; + 7DFA141F9E4B0E2D3D78DA1215079EE0 /* IGListKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = IGListKit.debug.xcconfig; sourceTree = ""; }; + 7E32BABB4A5EC5D4E83FB5D95173A917 /* infoIconLight@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIconLight@3x.png"; path = "SwiftMessages/Resources/infoIconLight@3x.png"; sourceTree = ""; }; + 7ED99FCB6BB287CC3833E78421A0E4C1 /* IGListScrollDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListScrollDelegate.h; path = Source/IGListKit/IGListScrollDelegate.h; sourceTree = ""; }; + 802CFB873BF930917357D60645A37371 /* AVAssetImageDataProvider.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AVAssetImageDataProvider.swift; path = Sources/General/ImageSource/AVAssetImageDataProvider.swift; sourceTree = ""; }; + 8051E3132AD533F0FFDD835E592EBC46 /* IGListCollectionViewLayout.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = IGListCollectionViewLayout.mm; path = Source/IGListKit/IGListCollectionViewLayout.mm; sourceTree = ""; }; + 8079F476C78D97C2FAE6BD81EEBC744F /* errorIconLight@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIconLight@2x.png"; path = "SwiftMessages/Resources/errorIconLight@2x.png"; sourceTree = ""; }; + 80AA6B070EDF3B36ED949C1DE413B445 /* Animator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Animator.swift; path = SwiftMessages/Animator.swift; sourceTree = ""; }; + 80E936467024EE1D60D40AF511570E73 /* Source.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Source.swift; path = Sources/General/ImageSource/Source.swift; sourceTree = ""; }; + 82030DF9E124050C32BD06D505EE5FED /* SwiftMessages-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftMessages-prefix.pch"; sourceTree = ""; }; + 8212866E5B322E817F4E1594B146D11E /* IGListWorkingRangeDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListWorkingRangeDelegate.h; path = Source/IGListKit/IGListWorkingRangeDelegate.h; sourceTree = ""; }; + 841EA054CFC907C24018C45ADC5F3E63 /* Kingfisher.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = Kingfisher.modulemap; sourceTree = ""; }; + 8452BF3EA61C1D6EC6A9DCAB1C65173D /* MessageView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MessageView.swift; path = SwiftMessages/MessageView.swift; sourceTree = ""; }; + 8578B293CF58ED79CF3BB15C593A1811 /* IGListGenericSectionController.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListGenericSectionController.h; path = Source/IGListKit/IGListGenericSectionController.h; sourceTree = ""; }; + 86D541130E17C7E352A8977E69E4D3DC /* SwiftMessagesSegue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SwiftMessagesSegue.swift; path = SwiftMessages/SwiftMessagesSegue.swift; sourceTree = ""; }; + 8720FC3CB9A344DF6C3C51A81CE61E8F /* IGListReloadDataUpdater.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListReloadDataUpdater.h; path = Source/IGListKit/IGListReloadDataUpdater.h; sourceTree = ""; }; + 881690ABF91DF5E1AFB107C1A817B51D /* SwiftMessages-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "SwiftMessages-umbrella.h"; sourceTree = ""; }; + 896B6B102742772B634D79305645CA79 /* IGListMoveIndex.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListMoveIndex.h; path = Source/IGListDiffKit/IGListMoveIndex.h; sourceTree = ""; }; + 8A43FF2E5825C8BA65CB38432F0C25D4 /* Pods-Bulletin-acknowledgements.markdown */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text; path = "Pods-Bulletin-acknowledgements.markdown"; sourceTree = ""; }; + 8C74304E2D1E7F3E9241DE00AF3868A9 /* UICollectionViewLayout+InteractiveReordering.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UICollectionViewLayout+InteractiveReordering.h"; path = "Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.h"; sourceTree = ""; }; + 8D3E03494034124977566245810EF836 /* UIWindow+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIWindow+Extensions.swift"; path = "SwiftMessages/UIWindow+Extensions.swift"; sourceTree = ""; }; + 8D5B3DE73BA8320B88507E03E2B50D19 /* AuthenticationChallengeResponsable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AuthenticationChallengeResponsable.swift; path = Sources/Networking/AuthenticationChallengeResponsable.swift; sourceTree = ""; }; + 8F8C5C6E34EBE4F7FEFFD3E815727A10 /* errorIconLight.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = errorIconLight.png; path = SwiftMessages/Resources/errorIconLight.png; sourceTree = ""; }; + 9060464159E13D7209F7D64BACF83DAD /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; }; + 912F4E96A8B3BB480A20592F7C6B709A /* NSLayoutConstraint+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSLayoutConstraint+Extensions.swift"; path = "SwiftMessages/NSLayoutConstraint+Extensions.swift"; sourceTree = ""; }; + 9166C75ED413481507005F77A9BEE4AF /* successIconLight@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIconLight@2x.png"; path = "SwiftMessages/Resources/successIconLight@2x.png"; sourceTree = ""; }; + 9200674997CD712EE42F9FEF1B4F1D25 /* IGListAdapter+DebugDescription.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "IGListAdapter+DebugDescription.m"; path = "Source/IGListKit/Internal/IGListAdapter+DebugDescription.m"; sourceTree = ""; }; + 92CF5980D57F9F674AE272CB46916FF0 /* IGListMoveIndexPath.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListMoveIndexPath.m; path = Source/IGListDiffKit/IGListMoveIndexPath.m; sourceTree = ""; }; + 92E3930B2051A71F0FD7F48F8E97C348 /* CacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CacheSerializer.swift; path = Sources/Cache/CacheSerializer.swift; sourceTree = ""; }; + 92E5783214B75AAE3D5847FC4443333D /* MemoryStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = MemoryStorage.swift; path = Sources/Cache/MemoryStorage.swift; sourceTree = ""; }; + 93DFC745DDF011893D575891F77993E4 /* IGListSectionMap+DebugDescription.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "IGListSectionMap+DebugDescription.m"; path = "Source/IGListKit/Internal/IGListSectionMap+DebugDescription.m"; sourceTree = ""; }; + 95A39432E536B69E1F62334D0007FAC2 /* warningIcon@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIcon@3x.png"; path = "SwiftMessages/Resources/warningIcon@3x.png"; sourceTree = ""; }; + 9615C14A8AD14AEA71CE750A1DA1EDF8 /* ImageProgressive.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageProgressive.swift; path = Sources/Image/ImageProgressive.swift; sourceTree = ""; }; + 969EC8B144DCFDBB8D6B20563FD767D8 /* IGListAssert.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAssert.h; path = Source/IGListDiffKit/IGListAssert.h; sourceTree = ""; }; + 974BF1702E4695985E60FBB45338A8A7 /* IGListDiff.mm */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.cpp.objcpp; name = IGListDiff.mm; path = Source/IGListDiffKit/IGListDiff.mm; sourceTree = ""; }; + 9833C8624EAC2FC205C021937F16B47D /* infoIcon@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIcon@2x.png"; path = "SwiftMessages/Resources/infoIcon@2x.png"; sourceTree = ""; }; + 98C5BC08D7F9EC2BBA718B8840AE0B46 /* Pods-Bulletin-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "Pods-Bulletin-dummy.m"; sourceTree = ""; }; + 98C6563DD55C21E71E4E2CCB99C78C95 /* NSBundle+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSBundle+Extensions.swift"; path = "SwiftMessages/NSBundle+Extensions.swift"; sourceTree = ""; }; + 99059BAD6A5767A4A437BB9C36600D35 /* IGListDiffKit.modulemap */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.module; path = IGListDiffKit.modulemap; sourceTree = ""; }; + 990E9245247EA175C5B3F06E3FD3ABEF /* UIColor_Hex_Swift.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = UIColor_Hex_Swift.release.xcconfig; sourceTree = ""; }; + 996DBB5DDFA8515120D75647C8498AE8 /* IGListMoveIndexPathInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListMoveIndexPathInternal.h; path = Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h; sourceTree = ""; }; + 9A5FA7FE39BE7656FDDEB10178642B00 /* warningIconLight@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIconLight@3x.png"; path = "SwiftMessages/Resources/warningIconLight@3x.png"; sourceTree = ""; }; + 9A8E96020846E6E688E867EE7770F849 /* IGListDiffKit-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "IGListDiffKit-Info.plist"; sourceTree = ""; }; + 9AA30AE820EDB8A30CFA77ED3357C1E5 /* IGListKit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = IGListKit.release.xcconfig; sourceTree = ""; }; + 9C2D15A0D67EAE136052527797313C80 /* IGListCollectionView.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListCollectionView.h; path = Source/IGListKit/IGListCollectionView.h; sourceTree = ""; }; + 9D034498F4799F076DA40C62E1C1CF6A /* KFAnimatedImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFAnimatedImage.swift; path = Sources/SwiftUI/KFAnimatedImage.swift; sourceTree = ""; }; + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */ = {isa = PBXFileReference; explicitFileType = text.script.ruby; includeInIndex = 1; indentWidth = 2; name = Podfile; path = ../Podfile; sourceTree = SOURCE_ROOT; tabWidth = 2; xcLanguageSpecificationIdentifier = xcode.lang.ruby; }; + 9DD8DD2702C161767569A953B694435D /* UIEdgeInsets+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIEdgeInsets+Extensions.swift"; path = "SwiftMessages/UIEdgeInsets+Extensions.swift"; sourceTree = ""; }; + 9E7573C529987F0DB86026FA9777B8C3 /* Pods-Bulletin.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = "Pods-Bulletin.release.xcconfig"; sourceTree = ""; }; + 9F5679710D252BC32A66875E79EE9BEB /* SessionDataTask.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDataTask.swift; path = Sources/Networking/SessionDataTask.swift; sourceTree = ""; }; + A1B692CB750B2228127BCE50696B349C /* SwiftMessages.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftMessages.release.xcconfig; sourceTree = ""; }; + A3B6D0D716E13A355BB46955460FFB80 /* WKInterfaceImage+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "WKInterfaceImage+Kingfisher.swift"; path = "Sources/Extensions/WKInterfaceImage+Kingfisher.swift"; sourceTree = ""; }; + A3B9660742F1460FA534B5C6DD0333B8 /* IGListAdapterDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterDelegate.h; path = Source/IGListKit/IGListAdapterDelegate.h; sourceTree = ""; }; + A481C64506F5FE39DE63847F8959BE5D /* Filter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Filter.swift; path = Sources/Image/Filter.swift; sourceTree = ""; }; + A4F2B2BFBEFB50E3208CD5BC30F9573C /* UIColor_Hex_Swift-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "UIColor_Hex_Swift-umbrella.h"; sourceTree = ""; }; + A558F0E26DB1E403F2BA4D285835649B /* IGListAdapterUpdater.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterUpdater.h; path = Source/IGListKit/IGListAdapterUpdater.h; sourceTree = ""; }; + A600EBFC4D5E663BCCFC8D3C2BEF24A6 /* UIColorExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UIColorExtension.swift; path = HEXColor/UIColorExtension.swift; sourceTree = ""; }; + A653BDF17479F16550C74F6CE6EA62F6 /* CPListItem+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CPListItem+Kingfisher.swift"; path = "Sources/Extensions/CPListItem+Kingfisher.swift"; sourceTree = ""; }; + A7228A433C789CABC0C81CB754A42B8B /* IGListSectionMap+DebugDescription.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "IGListSectionMap+DebugDescription.h"; path = "Source/IGListKit/Internal/IGListSectionMap+DebugDescription.h"; sourceTree = ""; }; + A88C71355EB0F8098D7C610310E5ADF9 /* ImageFormat.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageFormat.swift; path = Sources/Image/ImageFormat.swift; sourceTree = ""; }; + A99E6797E0FD83202E415971B87BC2AE /* DiskStorage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = DiskStorage.swift; path = Sources/Cache/DiskStorage.swift; sourceTree = ""; }; + AA5472E591CE0BA4D0BA9BD216E9BBB0 /* errorIcon@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIcon@2x.png"; path = "SwiftMessages/Resources/errorIcon@2x.png"; sourceTree = ""; }; + ABA9CF4FA93B0C6F6B68554063B72197 /* PassthroughWindow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PassthroughWindow.swift; path = SwiftMessages/PassthroughWindow.swift; sourceTree = ""; }; + AC0554DE66BE45142D0D9A060832228E /* IGListDebuggingUtilities.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListDebuggingUtilities.m; path = Source/IGListKit/Internal/IGListDebuggingUtilities.m; sourceTree = ""; }; + AD155FF57E053DCC59222AD9E9006734 /* ExtensionHelpers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ExtensionHelpers.swift; path = Sources/Utility/ExtensionHelpers.swift; sourceTree = ""; }; + AD245F26200BAD0F0C9385C5DEA429B7 /* UIButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIButton+Kingfisher.swift"; path = "Sources/Extensions/UIButton+Kingfisher.swift"; sourceTree = ""; }; + AD580318635BBFC7B1A532C8F86EE9B8 /* IGListBindingSectionControllerDataSource.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListBindingSectionControllerDataSource.h; path = Source/IGListKit/IGListBindingSectionControllerDataSource.h; sourceTree = ""; }; + AEA97C6B1B634B2233D0ABCD79202C8E /* Pods-Bulletin-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Pods-Bulletin-umbrella.h"; sourceTree = ""; }; + AED77699F9B880BBE2EA3CCA45FEE504 /* successIcon.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = successIcon.png; path = SwiftMessages/Resources/successIcon.png; sourceTree = ""; }; + AEDF019A7D273CA293038AC2E7E27AF7 /* Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Kingfisher.swift; path = Sources/General/Kingfisher.swift; sourceTree = ""; }; + B0560DAEDE227C49E7CE1C22076E70AC /* IGListReloadDataUpdater.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListReloadDataUpdater.m; path = Source/IGListKit/IGListReloadDataUpdater.m; sourceTree = ""; }; + B190AE5FF026FBD8339F4E3193D1A49E /* IGListSupplementaryViewSource.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListSupplementaryViewSource.h; path = Source/IGListKit/IGListSupplementaryViewSource.h; sourceTree = ""; }; + B1E88B937D987546110BF9126ADB22B0 /* warningIcon@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "warningIcon@2x.png"; path = "SwiftMessages/Resources/warningIcon@2x.png"; sourceTree = ""; }; + B2DFA0063F8757E8167F805F39A2E3E1 /* IGListBatchUpdates.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListBatchUpdates.m; path = Source/IGListKit/Internal/IGListBatchUpdates.m; sourceTree = ""; }; + B3C6C3E0B0FE2750FE588E0AFBA66D62 /* IGListKit */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = IGListKit; path = IGListKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + B3D3E8CEA6EB6B2960A1895BBFF73681 /* IGListDiffKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = IGListDiffKit.debug.xcconfig; sourceTree = ""; }; + B4FD74B7B2B30A494ADC03256687A532 /* KFOptionsSetter.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFOptionsSetter.swift; path = Sources/General/KFOptionsSetter.swift; sourceTree = ""; }; + B6FCEA9D7060088104B1ACB5E98DC48F /* IGListDisplayDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListDisplayDelegate.h; path = Source/IGListKit/IGListDisplayDelegate.h; sourceTree = ""; }; + B714F2FA5DE937147702D595D7104ACC /* IGListAdapter+DebugDescription.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "IGListAdapter+DebugDescription.h"; path = "Source/IGListKit/Internal/IGListAdapter+DebugDescription.h"; sourceTree = ""; }; + BB41663FC2E747F846E5C48FA4003730 /* ResourceBundle-SwiftMessages-SwiftMessages-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "ResourceBundle-SwiftMessages-SwiftMessages-Info.plist"; sourceTree = ""; }; + BB4552B30B4BEA7C253712647396FB87 /* RequestModifier.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RequestModifier.swift; path = Sources/Networking/RequestModifier.swift; sourceTree = ""; }; + BB9D49792750AB8422232444B4D6E947 /* IGListSingleSectionController.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListSingleSectionController.h; path = Source/IGListKit/IGListSingleSectionController.h; sourceTree = ""; }; + BBE21E109176CA5683819D426970A5E5 /* IGListBindable.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListBindable.h; path = Source/IGListKit/IGListBindable.h; sourceTree = ""; }; + BCB4CB332EC3A05BBA96845240C3B5C9 /* CallbackQueue.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = CallbackQueue.swift; path = Sources/Utility/CallbackQueue.swift; sourceTree = ""; }; + BCCD5F85BBD560CCB2EB64FAB0B1F00B /* UIColorInputError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = UIColorInputError.swift; path = HEXColor/UIColorInputError.swift; sourceTree = ""; }; + BD49496D6720223D5BD32660F3566682 /* IGListSectionMap.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListSectionMap.m; path = Source/IGListKit/Internal/IGListSectionMap.m; sourceTree = ""; }; + BF5FCF915C68DC413EF7C3DC3908EA10 /* UICollectionViewLayout+InteractiveReordering.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UICollectionViewLayout+InteractiveReordering.m"; path = "Source/IGListKit/Internal/UICollectionViewLayout+InteractiveReordering.m"; sourceTree = ""; }; + BF7F03396FC5C7FFAB84FF05B036B970 /* IGListBatchUpdates.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListBatchUpdates.h; path = Source/IGListKit/Internal/IGListBatchUpdates.h; sourceTree = ""; }; + BFA65400774FF943C82530BA1C6FBFD5 /* IGListSectionControllerInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListSectionControllerInternal.h; path = Source/IGListKit/Internal/IGListSectionControllerInternal.h; sourceTree = ""; }; + C107FC92873BF8C1318FB8BAB48B491D /* RedirectHandler.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RedirectHandler.swift; path = Sources/Networking/RedirectHandler.swift; sourceTree = ""; }; + C1B6942E8551B1ABFBEBCA492FA29A1F /* StatusLine.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = StatusLine.xib; path = SwiftMessages/Resources/StatusLine.xib; sourceTree = ""; }; + C1B7F17AB35439D95BB2D5B84B8FF273 /* UICollectionView+DebugDescription.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UICollectionView+DebugDescription.h"; path = "Source/IGListKit/Internal/UICollectionView+DebugDescription.h"; sourceTree = ""; }; + C3BAE6351018C0F1408D0BBB486AF803 /* IGListAdapterMoveDelegate.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapterMoveDelegate.h; path = Source/IGListKit/IGListAdapterMoveDelegate.h; sourceTree = ""; }; + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; name = Kingfisher; path = Kingfisher.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + C401DDB5E824718372BEB559155F340E /* RetryStrategy.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = RetryStrategy.swift; path = Sources/Networking/RetryStrategy.swift; sourceTree = ""; }; + C5C70AB815F6269A74BCA19F3C854C39 /* UIColor_Hex_Swift-prefix.pch */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "UIColor_Hex_Swift-prefix.pch"; sourceTree = ""; }; + C8BFDA32ECAAAC6F60280C6BFAEAEE57 /* IGListAdapterUpdater+DebugDescription.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "IGListAdapterUpdater+DebugDescription.m"; path = "Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.m"; sourceTree = ""; }; + C904783C0CEAE96552A679C695669DBC /* IGListBatchUpdateData.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListBatchUpdateData.h; path = Source/IGListDiffKit/IGListBatchUpdateData.h; sourceTree = ""; }; + CA7B91D095A2146E7CB26C435BB87D78 /* IGListCollectionContext.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListCollectionContext.h; path = Source/IGListKit/IGListCollectionContext.h; sourceTree = ""; }; + CB4BCAA40F9050DCE2E7581EECE3B199 /* MessageView.xib */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = file.xib; name = MessageView.xib; path = SwiftMessages/Resources/MessageView.xib; sourceTree = ""; }; + CC2283864C5BFAC9FC50299F4F6071A9 /* StringExtension.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = StringExtension.swift; path = HEXColor/StringExtension.swift; sourceTree = ""; }; + CC497F9013BFBD2093E1EC78B0273F64 /* IGListDiffKit-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "IGListDiffKit-umbrella.h"; sourceTree = ""; }; + CE29AFF1B5B06DB5F46307D8CA2F94A2 /* successIcon@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIcon@2x.png"; path = "SwiftMessages/Resources/successIcon@2x.png"; sourceTree = ""; }; + CF130AF5100D6FFE94E45CAEC5B8222E /* KingfisherError.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherError.swift; path = Sources/General/KingfisherError.swift; sourceTree = ""; }; + CFA306987494C26AB6494A2C70A32AE3 /* successIconSubtle@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIconSubtle@3x.png"; path = "SwiftMessages/Resources/successIconSubtle@3x.png"; sourceTree = ""; }; + CFAF508ECC19E5ED7EE57D68482D8B2D /* IGListKit-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "IGListKit-dummy.m"; sourceTree = ""; }; + CFFD614E723A7CA1CF56C4F27C14BB2C /* IGListWorkingRangeHandler.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListWorkingRangeHandler.h; path = Source/IGListKit/Internal/IGListWorkingRangeHandler.h; sourceTree = ""; }; + D1F58388A6DDB3EF0B970B865F5EDB38 /* AccessibleMessage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = AccessibleMessage.swift; path = SwiftMessages/AccessibleMessage.swift; sourceTree = ""; }; + D242674C42297EEEBD97DEE7B159B1AE /* KingfisherOptionsInfo.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KingfisherOptionsInfo.swift; path = Sources/General/KingfisherOptionsInfo.swift; sourceTree = ""; }; + D2E866BD1073307B39C555AA72687DED /* IGListIndexPathResultInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListIndexPathResultInternal.h; path = Source/IGListDiffKit/Internal/IGListIndexPathResultInternal.h; sourceTree = ""; }; + D2EC6A9540A860E7EDBF13CF404CB4B8 /* Kingfisher.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = Kingfisher.release.xcconfig; sourceTree = ""; }; + D467CB3DCF65BE9C4B4067D5FFDD6859 /* SessionDelegate.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = SessionDelegate.swift; path = Sources/Networking/SessionDelegate.swift; sourceTree = ""; }; + D5B77C4DD4EF76E7FC4D29F3BF7F1AAD /* UIColor_Hex_Swift-dummy.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; path = "UIColor_Hex_Swift-dummy.m"; sourceTree = ""; }; + D6597118FC7A651AD1C1B8186C857884 /* infoIcon.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = infoIcon.png; path = SwiftMessages/Resources/infoIcon.png; sourceTree = ""; }; + D79BB4ADBD345BEC6C9D1C903BDEC902 /* IGListAdapterUpdater+DebugDescription.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "IGListAdapterUpdater+DebugDescription.h"; path = "Source/IGListKit/Internal/IGListAdapterUpdater+DebugDescription.h"; sourceTree = ""; }; + D8DBF71C0BE4BA02FEE918097192B2B7 /* IGListDebugger.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListDebugger.h; path = Source/IGListKit/Internal/IGListDebugger.h; sourceTree = ""; }; + D9B697009E9B2897D4182C96CC40388B /* infoIconSubtle@2x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "infoIconSubtle@2x.png"; path = "SwiftMessages/Resources/infoIconSubtle@2x.png"; sourceTree = ""; }; + D9CEFE6DEF6BE44242643753E89BE983 /* IGListMoveIndexPathInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListMoveIndexPathInternal.h; path = Source/IGListDiffKit/Internal/IGListMoveIndexPathInternal.h; sourceTree = ""; }; + D9E43A922FEECF4031283D9C0CCEAD71 /* UIViewController+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "UIViewController+Extensions.swift"; path = "SwiftMessages/UIViewController+Extensions.swift"; sourceTree = ""; }; + DA9BBA4DCEA951FBE5842187767A324A /* Storage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Storage.swift; path = Sources/Cache/Storage.swift; sourceTree = ""; }; + DB06B0638434865F6E2DA73FF648F3A6 /* MarginAdjustable+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "MarginAdjustable+Extensions.swift"; path = "SwiftMessages/MarginAdjustable+Extensions.swift"; sourceTree = ""; }; + DB1BF169705DB0B275CE4D023A9F5097 /* UIScrollView+IGListKit.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "UIScrollView+IGListKit.m"; path = "Source/IGListKit/Internal/UIScrollView+IGListKit.m"; sourceTree = ""; }; + DB6D5C8DD9281BFC32CE9E0A12F507D9 /* IGListIndexSetResultInternal.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListIndexSetResultInternal.h; path = Source/IGListDiffKit/Internal/IGListIndexSetResultInternal.h; sourceTree = ""; }; + DBE15455BF19E1F22DA0EA9923EDDD39 /* successIconSubtle.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = successIconSubtle.png; path = SwiftMessages/Resources/successIconSubtle.png; sourceTree = ""; }; + DC15D52D5B4A8353B7433818CFC755C3 /* successIcon@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "successIcon@3x.png"; path = "SwiftMessages/Resources/successIcon@3x.png"; sourceTree = ""; }; + DCD553D3612DC6CC76F894AF0823A794 /* ImageDownloader.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDownloader.swift; path = Sources/Networking/ImageDownloader.swift; sourceTree = ""; }; + DEC751C1D0408841BADB8D95359DEDD0 /* ImageDataProcessor.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageDataProcessor.swift; path = Sources/Networking/ImageDataProcessor.swift; sourceTree = ""; }; + DEDF7F99C596705FC4F572460885EBCD /* NSButton+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSButton+Kingfisher.swift"; path = "Sources/Extensions/NSButton+Kingfisher.swift"; sourceTree = ""; }; + DF5C02A0D1A2BFFC1A6EE72656191F77 /* IGListBatchUpdateData+DebugDescription.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "IGListBatchUpdateData+DebugDescription.m"; path = "Source/IGListKit/Internal/IGListBatchUpdateData+DebugDescription.m"; sourceTree = ""; }; + DF5E502A140B8C7136E1F676C002042C /* IGListMoveIndexPath.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListMoveIndexPath.h; path = Source/IGListDiffKit/IGListMoveIndexPath.h; sourceTree = ""; }; + DF93862F9459B5B5101367844C76ACFC /* KFImage.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KFImage.swift; path = Sources/SwiftUI/KFImage.swift; sourceTree = ""; }; + E008BF5B47FEF861254F08D2F41B94E2 /* PassthroughView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = PassthroughView.swift; path = SwiftMessages/PassthroughView.swift; sourceTree = ""; }; + E314F5C9EA1F8E0F93CB7A5863EF4352 /* IGListKit-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "IGListKit-umbrella.h"; sourceTree = ""; }; + E69D9C2CA746D767CAFF47CD3F8F0BB8 /* ImageContext.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageContext.swift; path = Sources/SwiftUI/ImageContext.swift; sourceTree = ""; }; + E8101380E5E87B4F4EC6DDD9D53C9667 /* IGListIndexPathResult.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListIndexPathResult.m; path = Source/IGListDiffKit/IGListIndexPathResult.m; sourceTree = ""; }; + E8D88827F487BE149C1B8D4C8F21A887 /* WindowViewController.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WindowViewController.swift; path = SwiftMessages/WindowViewController.swift; sourceTree = ""; }; + E8FA93108377D1E5B3A047566835F3D3 /* UICollectionView+IGListBatchUpdateData.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "UICollectionView+IGListBatchUpdateData.h"; path = "Source/IGListKit/Internal/UICollectionView+IGListBatchUpdateData.h"; sourceTree = ""; }; + E947DADE1C4D71798D03004B57C09B12 /* WindowScene.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = WindowScene.swift; path = SwiftMessages/WindowScene.swift; sourceTree = ""; }; + E9EF2C50B2F9F3FC8C75CE92032462AA /* IGListDiffKit.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListDiffKit.h; path = Source/IGListDiffKit/IGListDiffKit.h; sourceTree = ""; }; + EA8581BAFB783450DFD66F8C53661FFC /* Pods-Bulletin-frameworks.sh */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.script.sh; path = "Pods-Bulletin-frameworks.sh"; sourceTree = ""; }; + EB56C884B7F74971F1D47C11641BD905 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/Accelerate.framework; sourceTree = DEVELOPER_DIR; }; + EB6036A78A4C36D50745E215A018095B /* IGListBindingSectionController+DebugDescription.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = "IGListBindingSectionController+DebugDescription.m"; path = "Source/IGListKit/Internal/IGListBindingSectionController+DebugDescription.m"; sourceTree = ""; }; + EBDB16CE65088B7BAADD99ED81716273 /* Runtime.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Runtime.swift; path = Sources/Utility/Runtime.swift; sourceTree = ""; }; + EC56036E713E5F3E1D5CF743FB2E30CE /* FormatIndicatedCacheSerializer.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = FormatIndicatedCacheSerializer.swift; path = Sources/Cache/FormatIndicatedCacheSerializer.swift; sourceTree = ""; }; + ED7503AECAE5B4ACEDDA8E6EF1740523 /* NSTextAttachment+Kingfisher.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "NSTextAttachment+Kingfisher.swift"; path = "Sources/Extensions/NSTextAttachment+Kingfisher.swift"; sourceTree = ""; }; + EE7F5B9CFE3D767F3DB305EE26C2283F /* Kingfisher-umbrella.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = "Kingfisher-umbrella.h"; sourceTree = ""; }; + EF2222773C495CA6BA87AAAA93B049B9 /* NSString+IGListDiffable.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = "NSString+IGListDiffable.h"; path = "Source/IGListDiffKit/NSString+IGListDiffable.h"; sourceTree = ""; }; + F007DFBDE1351AA0BB0A55C234B06A7E /* IGListDisplayHandler.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListDisplayHandler.h; path = Source/IGListKit/Internal/IGListDisplayHandler.h; sourceTree = ""; }; + F0150DA8D834C8B8F37D02689D618EB7 /* KF.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = KF.swift; path = Sources/General/KF.swift; sourceTree = ""; }; + F138F35B611E2FD49AEFCF69028D4AB7 /* BackgroundViewable.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = BackgroundViewable.swift; path = SwiftMessages/BackgroundViewable.swift; sourceTree = ""; }; + F1D5521BDDA1A0242A36C1929596B475 /* IGListIndexSetResult.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListIndexSetResult.m; path = Source/IGListDiffKit/IGListIndexSetResult.m; sourceTree = ""; }; + F1EB9AD47A886439B7A26D12430A76C1 /* errorIconSubtle@3x.png */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = image.png; name = "errorIconSubtle@3x.png"; path = "SwiftMessages/Resources/errorIconSubtle@3x.png"; sourceTree = ""; }; + F36F51D00377325A46E8AFB150E6113A /* IGListCollectionViewLayoutCompatible.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListCollectionViewLayoutCompatible.h; path = Source/IGListKit/IGListCollectionViewLayoutCompatible.h; sourceTree = ""; }; + F3A0944D15D667441A61E0CE8B2F5567 /* IGListAdapter.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListAdapter.h; path = Source/IGListKit/IGListAdapter.h; sourceTree = ""; }; + F6ACCD5DC25939E8ED2EC86296E56DF6 /* IGListSectionController.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; name = IGListSectionController.h; path = Source/IGListKit/IGListSectionController.h; sourceTree = ""; }; + F8AD99B18C90F7FD70633BE67798C357 /* Pods-Bulletin-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = "Pods-Bulletin-Info.plist"; sourceTree = ""; }; + FA656E9DD3994DB5C569165361FFF7EE /* CALayer+Extensions.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = "CALayer+Extensions.swift"; path = "SwiftMessages/CALayer+Extensions.swift"; sourceTree = ""; }; + FB85FA489CBC63454660B040F371269C /* SwiftMessages.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; path = SwiftMessages.debug.xcconfig; sourceTree = ""; }; + FBAC4E1C27713FF52E2D11B717422EC0 /* Error.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Error.swift; path = SwiftMessages/Error.swift; sourceTree = ""; }; + FDB7B3024FA7207F2C442C4AB05907D7 /* Indicator.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Indicator.swift; path = Sources/Views/Indicator.swift; sourceTree = ""; }; + FE009ADA0FD378470D467A226946D15F /* ImageBinder.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ImageBinder.swift; path = Sources/SwiftUI/ImageBinder.swift; sourceTree = ""; }; + FF11DBFDB6B6CF5A0D1EA3D416156A68 /* IGListSectionController.m */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.objc; name = IGListSectionController.m; path = Source/IGListKit/IGListSectionController.m; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0DC33FC81EEF5C13DC2B4EF85CA14F78 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7598EFC472D8E80C444EAF66B3D53DDA /* Foundation.framework in Frameworks */, + 017FE38FF3607E56CABEF7C6F2D6D141 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1590561D96F3E074FE469B04D8F764C8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9CFB36995618A21B72917C70D913E41A /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1E5F34F325312F632646732A454D6AD6 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 6B60128BF181756B5A05576C6068566F /* Foundation.framework in Frameworks */, + 760BE3F4C975ECE6B3AFDB55F66F0A78 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 524D0CCC1F888BC91F616D53B4C381E8 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 5C9B3C65E54998F5193CA304A90C9083 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + FB1227598B1A07A4E0FC9AEC4F60E144 /* Foundation.framework in Frameworks */, + 4F7ADAB76F659FA15973E8EE2D390D36 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 81DB1665E1495609510BA493822E5A85 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E719A3B025B9DACE693130120BD9B927 /* Accelerate.framework in Frameworks */, + 420C200A05BB29E1D299D1BADE9139D2 /* CFNetwork.framework in Frameworks */, + 3AD5DBB915C2623991F7DBACD173BBB4 /* Foundation.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F7EC70ECA9F3248B43189E7C38690FA1 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 1A510118417546D747FA5B2DEE7D2009 /* Foundation.framework in Frameworks */, + 1D79FA4B26B801D9043A4BF8224939B9 /* UIKit.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 074A5B71721A1B3F50DB8082DDD11157 /* Pods-Bulletin */ = { + isa = PBXGroup; + children = ( + 6E32E82CDA2470BB9DD50373EE56E2EC /* Pods-Bulletin.modulemap */, + 8A43FF2E5825C8BA65CB38432F0C25D4 /* Pods-Bulletin-acknowledgements.markdown */, + 2B50C6FFEC0097EFE6FB31764A42CAFA /* Pods-Bulletin-acknowledgements.plist */, + 98C5BC08D7F9EC2BBA718B8840AE0B46 /* Pods-Bulletin-dummy.m */, + EA8581BAFB783450DFD66F8C53661FFC /* Pods-Bulletin-frameworks.sh */, + F8AD99B18C90F7FD70633BE67798C357 /* Pods-Bulletin-Info.plist */, + AEA97C6B1B634B2233D0ABCD79202C8E /* Pods-Bulletin-umbrella.h */, + 6DEBD199402AB8F5B24357217344B70B /* Pods-Bulletin.debug.xcconfig */, + 9E7573C529987F0DB86026FA9777B8C3 /* Pods-Bulletin.release.xcconfig */, + ); + name = "Pods-Bulletin"; + path = "Target Support Files/Pods-Bulletin"; + sourceTree = ""; + }; + 07FCA4F85284D25B675A378F88FBBA14 /* Pods */ = { + isa = PBXGroup; + children = ( + B2C6EA9410FBA4324564C77CB2431009 /* IGListDiffKit */, + 2445315595D50385AA52A93DFE93E57C /* IGListKit */, + 7E2B523FF180B21B1BF18B644669F2C6 /* Kingfisher */, + 8CC0F3343BEDDC8D0126C8DC722D0B81 /* SwiftMessages */, + 67587C3C2323DB96F9027E6F594C629F /* UIColor_Hex_Swift */, + ); + name = Pods; + sourceTree = ""; + }; + 0B70A837D1A951EBD07A8957B700F7CC /* Support Files */ = { + isa = PBXGroup; + children = ( + 74F450974763BCC065DF7162314F180D /* IGListKit.modulemap */, + CFAF508ECC19E5ED7EE57D68482D8B2D /* IGListKit-dummy.m */, + 1CCB2C2A7F8FDEAB42ECE286206B4CF3 /* IGListKit-Info.plist */, + 3A977FC7DDB65D48636DAD282F3D1FB8 /* IGListKit-prefix.pch */, + E314F5C9EA1F8E0F93CB7A5863EF4352 /* IGListKit-umbrella.h */, + 7DFA141F9E4B0E2D3D78DA1215079EE0 /* IGListKit.debug.xcconfig */, + 9AA30AE820EDB8A30CFA77ED3357C1E5 /* IGListKit.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/IGListKit"; + sourceTree = ""; + }; + 125B588671DF46336D0F8154F40DE542 /* Support Files */ = { + isa = PBXGroup; + children = ( + BB41663FC2E747F846E5C48FA4003730 /* ResourceBundle-SwiftMessages-SwiftMessages-Info.plist */, + 65A2E0145D001B9CF4A7EAD17835A07F /* SwiftMessages.modulemap */, + 25FFBB29D9878FEFD402D15B13F96F2F /* SwiftMessages-dummy.m */, + 5637AD2FC80473D851AC233FF4B3B506 /* SwiftMessages-Info.plist */, + 82030DF9E124050C32BD06D505EE5FED /* SwiftMessages-prefix.pch */, + 881690ABF91DF5E1AFB107C1A817B51D /* SwiftMessages-umbrella.h */, + FB85FA489CBC63454660B040F371269C /* SwiftMessages.debug.xcconfig */, + A1B692CB750B2228127BCE50696B349C /* SwiftMessages.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/SwiftMessages"; + sourceTree = ""; + }; + 189B53BB925CD0E96CFBEF2E63E4BDC3 /* App */ = { + isa = PBXGroup; + children = ( + D1F58388A6DDB3EF0B970B865F5EDB38 /* AccessibleMessage.swift */, + 80AA6B070EDF3B36ED949C1DE413B445 /* Animator.swift */, + F138F35B611E2FD49AEFCF69028D4AB7 /* BackgroundViewable.swift */, + 21A539BD413B6B92DD2DD6FBD9368394 /* BaseView.swift */, + FA656E9DD3994DB5C569165361FFF7EE /* CALayer+Extensions.swift */, + 321AEF56ADD9B574B8E97C6E37AE6C36 /* CornerRoundingView.swift */, + FBAC4E1C27713FF52E2D11B717422EC0 /* Error.swift */, + 0E56A83EDE9F898293353CD761ED323A /* Identifiable.swift */, + 54AF051AB7BCD460987153D0685F73B2 /* KeyboardTrackingView.swift */, + 09BA0051E9F4F47215585B6787C85C96 /* MarginAdjustable.swift */, + DB06B0638434865F6E2DA73FF648F3A6 /* MarginAdjustable+Extensions.swift */, + 01DFF373D32B02827C3E52EB2CD2A3FE /* MaskingView.swift */, + 8452BF3EA61C1D6EC6A9DCAB1C65173D /* MessageView.swift */, + 98C6563DD55C21E71E4E2CCB99C78C95 /* NSBundle+Extensions.swift */, + 912F4E96A8B3BB480A20592F7C6B709A /* NSLayoutConstraint+Extensions.swift */, + E008BF5B47FEF861254F08D2F41B94E2 /* PassthroughView.swift */, + ABA9CF4FA93B0C6F6B68554063B72197 /* PassthroughWindow.swift */, + 53D11C1A637638ADF43AE1EB8BD508A5 /* PhysicsAnimation.swift */, + 1510A0DCF8427B970410D4A435FA3984 /* PhysicsPanHandler.swift */, + 4CDF76905EE491F6C6CEC1B70BC440D6 /* Presenter.swift */, + 6BEB7CE647AFB6BEE5DF438AE86B2F54 /* SwiftMessages.swift */, + 517254B0F9ADBAC88606A8124B700550 /* SwiftMessages.Config+Extensions.swift */, + 86D541130E17C7E352A8977E69E4D3DC /* SwiftMessagesSegue.swift */, + 461E8E5C8D92714274A6A58953D195C9 /* Theme.swift */, + 49FFAA7BEFB9D929E25C31979D44D129 /* TopBottomAnimation.swift */, + 9DD8DD2702C161767569A953B694435D /* UIEdgeInsets+Extensions.swift */, + D9E43A922FEECF4031283D9C0CCEAD71 /* UIViewController+Extensions.swift */, + 8D3E03494034124977566245810EF836 /* UIWindow+Extensions.swift */, + 6232B2401FA0928C2269BFD66C421986 /* Weak.swift */, + E947DADE1C4D71798D03004B57C09B12 /* WindowScene.swift */, + E8D88827F487BE149C1B8D4C8F21A887 /* WindowViewController.swift */, + 6589DBA334F6FAD920F93C7C11F91996 /* Resources */, + ); + name = App; + sourceTree = ""; + }; + 2445315595D50385AA52A93DFE93E57C /* IGListKit */ = { + isa = PBXGroup; + children = ( + F3A0944D15D667441A61E0CE8B2F5567 /* IGListAdapter.h */, + 1DC6890537D77DABA2EDB7D0C82839D8 /* IGListAdapter.m */, + B714F2FA5DE937147702D595D7104ACC /* IGListAdapter+DebugDescription.h */, + 9200674997CD712EE42F9FEF1B4F1D25 /* IGListAdapter+DebugDescription.m */, + 5CF48A29FE13ED204720994FA4997255 /* IGListAdapter+UICollectionView.h */, + 39CF63638810AE1B4B6F37AF97A30C81 /* IGListAdapter+UICollectionView.m */, + 27B420D1608AC6A991CAF82584A9805B /* IGListAdapterDataSource.h */, + A3B9660742F1460FA534B5C6DD0333B8 /* IGListAdapterDelegate.h */, + 7DAFB222C9BA9663B8F1E40709DBDCA6 /* IGListAdapterInternal.h */, + C3BAE6351018C0F1408D0BBB486AF803 /* IGListAdapterMoveDelegate.h */, + 111AD5BA834CFAE0B8CDB2B897047D06 /* IGListAdapterPerformanceDelegate.h */, + 6A2524ADEFD63F868610552BEE2C047D /* IGListAdapterProxy.h */, + 35E7C9FAC1444B886148B40E4E03A10A /* IGListAdapterProxy.m */, + 67EB0E60915C51E53FA40EB4CB81BAC9 /* IGListAdapterUpdateListener.h */, + A558F0E26DB1E403F2BA4D285835649B /* IGListAdapterUpdater.h */, + 4D59E7CA06D24D19520DA2615F034822 /* IGListAdapterUpdater.m */, + D79BB4ADBD345BEC6C9D1C903BDEC902 /* IGListAdapterUpdater+DebugDescription.h */, + C8BFDA32ECAAAC6F60280C6BFAEAEE57 /* IGListAdapterUpdater+DebugDescription.m */, + 21DAC60AD8416D4C4DC7ECBCA6C41E55 /* IGListAdapterUpdaterDelegate.h */, + 67AD88EDD5E2B4BE3251E36338822D75 /* IGListAdapterUpdaterInternal.h */, + 77CDFE36E22AF91849D4D87D3DD75B16 /* IGListArrayUtilsInternal.h */, + 23A7B6D2D0BE30FBDCFEFC0841849D05 /* IGListBatchContext.h */, + 37B5D75335F8694F04630AECE5892B47 /* IGListBatchUpdateData+DebugDescription.h */, + DF5C02A0D1A2BFFC1A6EE72656191F77 /* IGListBatchUpdateData+DebugDescription.m */, + BF7F03396FC5C7FFAB84FF05B036B970 /* IGListBatchUpdates.h */, + B2DFA0063F8757E8167F805F39A2E3E1 /* IGListBatchUpdates.m */, + 0A2F39EF1CE1C157494607E6C1FE66A7 /* IGListBatchUpdateState.h */, + BBE21E109176CA5683819D426970A5E5 /* IGListBindable.h */, + 118EB5557B6BA57C54ECE70977225895 /* IGListBindingSectionController.h */, + 3CD8D6758B3642D5415A451F5CB22167 /* IGListBindingSectionController.m */, + 6EAD7EC2840F995AE8ABAC9C2B4CF5EA /* IGListBindingSectionController+DebugDescription.h */, + EB6036A78A4C36D50745E215A018095B /* IGListBindingSectionController+DebugDescription.m */, + AD580318635BBFC7B1A532C8F86EE9B8 /* IGListBindingSectionControllerDataSource.h */, + 30513773A49E99A4C75E6115DD8934EC /* IGListBindingSectionControllerSelectionDelegate.h */, + CA7B91D095A2146E7CB26C435BB87D78 /* IGListCollectionContext.h */, + 2A1E479F8F71FB71F3C62C8450C4DC84 /* IGListCollectionScrollingTraits.h */, + 9C2D15A0D67EAE136052527797313C80 /* IGListCollectionView.h */, + 7C7F743B6CB541B1A49D0F89CDE913C6 /* IGListCollectionView.m */, + 4A38804BDF6E084423F6ED17E2119797 /* IGListCollectionViewDelegateLayout.h */, + 4BC79CF900824AEA4ED4B584BE07F257 /* IGListCollectionViewLayout.h */, + 8051E3132AD533F0FFDD835E592EBC46 /* IGListCollectionViewLayout.mm */, + F36F51D00377325A46E8AFB150E6113A /* IGListCollectionViewLayoutCompatible.h */, + 14D8F73E1EEE92B0BB6C4A4862D834A9 /* IGListCollectionViewLayoutInternal.h */, + D8DBF71C0BE4BA02FEE918097192B2B7 /* IGListDebugger.h */, + 709704C7D88AAF115EDAB039595D266D /* IGListDebugger.m */, + 6AD3632ED786F7D034F9E1A6EF5B3DAE /* IGListDebuggingUtilities.h */, + AC0554DE66BE45142D0D9A060832228E /* IGListDebuggingUtilities.m */, + B6FCEA9D7060088104B1ACB5E98DC48F /* IGListDisplayDelegate.h */, + F007DFBDE1351AA0BB0A55C234B06A7E /* IGListDisplayHandler.h */, + 4E3CE7DF25AAEC00FF8F06896801111B /* IGListDisplayHandler.m */, + 8578B293CF58ED79CF3BB15C593A1811 /* IGListGenericSectionController.h */, + 38C2E0D5356E1E9F90038322D59A5320 /* IGListGenericSectionController.m */, + D2E866BD1073307B39C555AA72687DED /* IGListIndexPathResultInternal.h */, + DB6D5C8DD9281BFC32CE9E0A12F507D9 /* IGListIndexSetResultInternal.h */, + 6D1F660EA325793B882E173B84016B9B /* IGListKit.h */, + 5892D28E2B92F79C8909C1A4304EDE19 /* IGListMoveIndexInternal.h */, + D9CEFE6DEF6BE44242643753E89BE983 /* IGListMoveIndexPathInternal.h */, + 8720FC3CB9A344DF6C3C51A81CE61E8F /* IGListReloadDataUpdater.h */, + B0560DAEDE227C49E7CE1C22076E70AC /* IGListReloadDataUpdater.m */, + 72DAB1212901F5B52E7093E822D23577 /* IGListReloadIndexPath.h */, + 6594CBDE5CC688CF8DBC51236163605D /* IGListReloadIndexPath.m */, + 7ED99FCB6BB287CC3833E78421A0E4C1 /* IGListScrollDelegate.h */, + F6ACCD5DC25939E8ED2EC86296E56DF6 /* IGListSectionController.h */, + FF11DBFDB6B6CF5A0D1EA3D416156A68 /* IGListSectionController.m */, + BFA65400774FF943C82530BA1C6FBFD5 /* IGListSectionControllerInternal.h */, + 68248F05BB6436D9514BD865E5AEFD53 /* IGListSectionMap.h */, + BD49496D6720223D5BD32660F3566682 /* IGListSectionMap.m */, + A7228A433C789CABC0C81CB754A42B8B /* IGListSectionMap+DebugDescription.h */, + 93DFC745DDF011893D575891F77993E4 /* IGListSectionMap+DebugDescription.m */, + BB9D49792750AB8422232444B4D6E947 /* IGListSingleSectionController.h */, + 2050EB0365A402B74F479EC3044072B2 /* IGListSingleSectionController.m */, + B190AE5FF026FBD8339F4E3193D1A49E /* IGListSupplementaryViewSource.h */, + 3F4E7F4C4B0ED07AA5930C2E3D5B8FD6 /* IGListTransitionDelegate.h */, + 36AA5F43CA08202B3651464A17D2DC8E /* IGListUpdatingDelegate.h */, + 8212866E5B322E817F4E1594B146D11E /* IGListWorkingRangeDelegate.h */, + CFFD614E723A7CA1CF56C4F27C14BB2C /* IGListWorkingRangeHandler.h */, + 05BF6B6F8ADAC5E7BC99DE8A118BF6B6 /* IGListWorkingRangeHandler.mm */, + C1B7F17AB35439D95BB2D5B84B8FF273 /* UICollectionView+DebugDescription.h */, + 424235E351E1EEF73A585E53FCCE5931 /* UICollectionView+DebugDescription.m */, + E8FA93108377D1E5B3A047566835F3D3 /* UICollectionView+IGListBatchUpdateData.h */, + 4EB7D9E14485B011E821B2552487BFFF /* UICollectionView+IGListBatchUpdateData.m */, + 8C74304E2D1E7F3E9241DE00AF3868A9 /* UICollectionViewLayout+InteractiveReordering.h */, + BF5FCF915C68DC413EF7C3DC3908EA10 /* UICollectionViewLayout+InteractiveReordering.m */, + 1351EB6B5E88098E59C3F20E89B2004C /* UIScrollView+IGListKit.h */, + DB1BF169705DB0B275CE4D023A9F5097 /* UIScrollView+IGListKit.m */, + 0B70A837D1A951EBD07A8957B700F7CC /* Support Files */, + ); + path = IGListKit; + sourceTree = ""; + }; + 43CF91C12C3F2936102F46DA4F2C7C75 /* Support Files */ = { + isa = PBXGroup; + children = ( + 99059BAD6A5767A4A437BB9C36600D35 /* IGListDiffKit.modulemap */, + 2713DB42F5385245C2EC0D72720C5BE6 /* IGListDiffKit-dummy.m */, + 9A8E96020846E6E688E867EE7770F849 /* IGListDiffKit-Info.plist */, + 3B0A6054CE428C4606ABFE6CB328CD2A /* IGListDiffKit-prefix.pch */, + CC497F9013BFBD2093E1EC78B0273F64 /* IGListDiffKit-umbrella.h */, + B3D3E8CEA6EB6B2960A1895BBFF73681 /* IGListDiffKit.debug.xcconfig */, + 1B08200E58B617675FE96006778840FB /* IGListDiffKit.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/IGListDiffKit"; + sourceTree = ""; + }; + 59123D44247562C173720EDD7EDC3E88 /* Support Files */ = { + isa = PBXGroup; + children = ( + 07017C809FA0F80CF1A57230031F6028 /* UIColor_Hex_Swift.modulemap */, + D5B77C4DD4EF76E7FC4D29F3BF7F1AAD /* UIColor_Hex_Swift-dummy.m */, + 4891D530CDAA004230740CADC793F34C /* UIColor_Hex_Swift-Info.plist */, + C5C70AB815F6269A74BCA19F3C854C39 /* UIColor_Hex_Swift-prefix.pch */, + A4F2B2BFBEFB50E3208CD5BC30F9573C /* UIColor_Hex_Swift-umbrella.h */, + 650F2EE29CA64B40F86449904824F62E /* UIColor_Hex_Swift.debug.xcconfig */, + 990E9245247EA175C5B3F06E3FD3ABEF /* UIColor_Hex_Swift.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/UIColor_Hex_Swift"; + sourceTree = ""; + }; + 6589DBA334F6FAD920F93C7C11F91996 /* Resources */ = { + isa = PBXGroup; + children = ( + 73660183B3F95FADC4C3F189620E5797 /* CardView.xib */, + 1091A7C7D19C45920D87A3EAA1AA6C0F /* CenteredView.xib */, + 5088061C5D0ECA51E28E1D5C9C05CDF2 /* errorIcon.png */, + AA5472E591CE0BA4D0BA9BD216E9BBB0 /* errorIcon@2x.png */, + 6A7C4CC9232AC5706CBAB7721F33DE5B /* errorIcon@3x.png */, + 8F8C5C6E34EBE4F7FEFFD3E815727A10 /* errorIconLight.png */, + 8079F476C78D97C2FAE6BD81EEBC744F /* errorIconLight@2x.png */, + 3B2A150E6AA79141831952D08C9CA44A /* errorIconLight@3x.png */, + 0E783AAB1D182A47CE9A4C70AEA15DC7 /* errorIconSubtle.png */, + 4DB2CFEA6FDD21B6081185B3729B51D3 /* errorIconSubtle@2x.png */, + F1EB9AD47A886439B7A26D12430A76C1 /* errorIconSubtle@3x.png */, + D6597118FC7A651AD1C1B8186C857884 /* infoIcon.png */, + 9833C8624EAC2FC205C021937F16B47D /* infoIcon@2x.png */, + 648D06A574C08D26489A675BF3F8CC97 /* infoIcon@3x.png */, + 72EDEFFEFD9FA17167C3024ED8D81D74 /* infoIconLight.png */, + 6AAB51521257D54CBB370D9F3D32C816 /* infoIconLight@2x.png */, + 7E32BABB4A5EC5D4E83FB5D95173A917 /* infoIconLight@3x.png */, + 645A2EAB5E61532B3ABBCEBC6AF722C8 /* infoIconSubtle.png */, + D9B697009E9B2897D4182C96CC40388B /* infoIconSubtle@2x.png */, + 7288C4A89C8BEFB77574C7581276301B /* infoIconSubtle@3x.png */, + CB4BCAA40F9050DCE2E7581EECE3B199 /* MessageView.xib */, + C1B6942E8551B1ABFBEBCA492FA29A1F /* StatusLine.xib */, + AED77699F9B880BBE2EA3CCA45FEE504 /* successIcon.png */, + CE29AFF1B5B06DB5F46307D8CA2F94A2 /* successIcon@2x.png */, + DC15D52D5B4A8353B7433818CFC755C3 /* successIcon@3x.png */, + 17CAC30BEB11B3613D6C1DFA0950F7E9 /* successIconLight.png */, + 9166C75ED413481507005F77A9BEE4AF /* successIconLight@2x.png */, + 08510568CA8F1AEEDE5B0E32306DC71A /* successIconLight@3x.png */, + DBE15455BF19E1F22DA0EA9923EDDD39 /* successIconSubtle.png */, + 2987551134D451FC8A1024048B845790 /* successIconSubtle@2x.png */, + CFA306987494C26AB6494A2C70A32AE3 /* successIconSubtle@3x.png */, + 34EC4406FF95B83EF37D06F08D66A633 /* TabView.xib */, + 1899BB102CCB03C961D392531F94109C /* warningIcon.png */, + B1E88B937D987546110BF9126ADB22B0 /* warningIcon@2x.png */, + 95A39432E536B69E1F62334D0007FAC2 /* warningIcon@3x.png */, + 67E342B558FA9E3757D37EEB2E4D12A3 /* warningIconLight.png */, + 635E1C7D580143C90A7C99F9B6145CC7 /* warningIconLight@2x.png */, + 9A5FA7FE39BE7656FDDEB10178642B00 /* warningIconLight@3x.png */, + 063A43A32CE98A3E1C417772EB31679E /* warningIconSubtle.png */, + 2A7E3CFE0BA27926F6ABFC0513EF1282 /* warningIconSubtle@2x.png */, + 01CD4B0B843A3B4A985651F253653952 /* warningIconSubtle@3x.png */, + ); + name = Resources; + sourceTree = ""; + }; + 67587C3C2323DB96F9027E6F594C629F /* UIColor_Hex_Swift */ = { + isa = PBXGroup; + children = ( + 2F78D97A20A66B010C3E1ABDA41FFA5F /* HEXColor.h */, + CC2283864C5BFAC9FC50299F4F6071A9 /* StringExtension.swift */, + A600EBFC4D5E663BCCFC8D3C2BEF24A6 /* UIColorExtension.swift */, + BCCD5F85BBD560CCB2EB64FAB0B1F00B /* UIColorInputError.swift */, + 59123D44247562C173720EDD7EDC3E88 /* Support Files */, + ); + path = UIColor_Hex_Swift; + sourceTree = ""; + }; + 685AAE32654D08B0A9F021FD45A52D88 /* Products */ = { + isa = PBXGroup; + children = ( + 195AD71F7938FEA988FDCC5B0A10FFDF /* IGListDiffKit */, + B3C6C3E0B0FE2750FE588E0AFBA66D62 /* IGListKit */, + C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */, + 20F98B848D2BF72F16DA1BDACF869D1A /* Pods-Bulletin */, + 4824F23D80FF9070A5F8A452DB11EB9A /* SwiftMessages */, + 753DD909979478A0C1B33E0524504EF1 /* SwiftMessages-SwiftMessages */, + 58FA08FD4595D3CA9B2AA81405DE2EFA /* UIColor_Hex_Swift.framework */, + ); + name = Products; + sourceTree = ""; + }; + 7E2B523FF180B21B1BF18B644669F2C6 /* Kingfisher */ = { + isa = PBXGroup; + children = ( + 52372AE9D7E296FCED90D2D5BA9ECB4D /* AnimatedImageView.swift */, + 8D5B3DE73BA8320B88507E03E2B50D19 /* AuthenticationChallengeResponsable.swift */, + 802CFB873BF930917357D60645A37371 /* AVAssetImageDataProvider.swift */, + 3BE8BFF931E90A8B8F751859A979D9FF /* Box.swift */, + 92E3930B2051A71F0FD7F48F8E97C348 /* CacheSerializer.swift */, + BCB4CB332EC3A05BBA96845240C3B5C9 /* CallbackQueue.swift */, + A653BDF17479F16550C74F6CE6EA62F6 /* CPListItem+Kingfisher.swift */, + 0C09F9726B03BEFB76DB54470EFA079E /* Delegate.swift */, + A99E6797E0FD83202E415971B87BC2AE /* DiskStorage.swift */, + AD155FF57E053DCC59222AD9E9006734 /* ExtensionHelpers.swift */, + A481C64506F5FE39DE63847F8959BE5D /* Filter.swift */, + EC56036E713E5F3E1D5CF743FB2E30CE /* FormatIndicatedCacheSerializer.swift */, + 1D250E60DB977C9D0BFF53ABCA2A997C /* GIFAnimatedImage.swift */, + 31363CA02DABFE037A9597E66D774E50 /* GraphicsContext.swift */, + 3934169222833A8921F287D6737C3D94 /* Image.swift */, + FE009ADA0FD378470D467A226946D15F /* ImageBinder.swift */, + 3579EDF0F2B23FDC5BD85C5B34DC0AD6 /* ImageCache.swift */, + E69D9C2CA746D767CAFF47CD3F8F0BB8 /* ImageContext.swift */, + DEC751C1D0408841BADB8D95359DEDD0 /* ImageDataProcessor.swift */, + 19002AA8D6680BDDAC1C8C5DDE64CD49 /* ImageDataProvider.swift */, + DCD553D3612DC6CC76F894AF0823A794 /* ImageDownloader.swift */, + 2FFEB06874D80B62E4BA3E418B346091 /* ImageDownloaderDelegate.swift */, + 24C3AE2697C2C777471A4F671EFB544B /* ImageDrawing.swift */, + A88C71355EB0F8098D7C610310E5ADF9 /* ImageFormat.swift */, + 0C41EE63D598C9A869E2FAAF99E10B90 /* ImageModifier.swift */, + 18E261DDB018DEF41EF92A4794D0CC81 /* ImagePrefetcher.swift */, + 74DA03441BD1BDF74130264922D71B19 /* ImageProcessor.swift */, + 9615C14A8AD14AEA71CE750A1DA1EDF8 /* ImageProgressive.swift */, + 0F789B805ED414860F2DC4C723AFE42D /* ImageTransition.swift */, + 453ED79EFF2CFCB0BB28AAFB6728A746 /* ImageView+Kingfisher.swift */, + FDB7B3024FA7207F2C442C4AB05907D7 /* Indicator.swift */, + F0150DA8D834C8B8F37D02689D618EB7 /* KF.swift */, + 9D034498F4799F076DA40C62E1C1CF6A /* KFAnimatedImage.swift */, + DF93862F9459B5B5101367844C76ACFC /* KFImage.swift */, + 793410A19FBFCCE0BF378A5C8CF49BFA /* KFImageOptions.swift */, + 0662908419CF488E8FE10FE90D80D493 /* KFImageProtocol.swift */, + 225916A84FF5B787DDE7EB83441E3470 /* KFImageRenderer.swift */, + B4FD74B7B2B30A494ADC03256687A532 /* KFOptionsSetter.swift */, + AEDF019A7D273CA293038AC2E7E27AF7 /* Kingfisher.swift */, + CF130AF5100D6FFE94E45CAEC5B8222E /* KingfisherError.swift */, + 65601298C6B1A422CDB2DC07123D1E51 /* KingfisherManager.swift */, + D242674C42297EEEBD97DEE7B159B1AE /* KingfisherOptionsInfo.swift */, + 92E5783214B75AAE3D5847FC4443333D /* MemoryStorage.swift */, + DEDF7F99C596705FC4F572460885EBCD /* NSButton+Kingfisher.swift */, + ED7503AECAE5B4ACEDDA8E6EF1740523 /* NSTextAttachment+Kingfisher.swift */, + 53BF7416BD4A970D1B5E4B01F294E586 /* Placeholder.swift */, + C107FC92873BF8C1318FB8BAB48B491D /* RedirectHandler.swift */, + BB4552B30B4BEA7C253712647396FB87 /* RequestModifier.swift */, + 4B0D3AE0230F985AC1ED5587C52B99DB /* Resource.swift */, + 6688907CE0F1BF85B4A1ED07A4FD73BB /* Result.swift */, + C401DDB5E824718372BEB559155F340E /* RetryStrategy.swift */, + EBDB16CE65088B7BAADD99ED81716273 /* Runtime.swift */, + 9F5679710D252BC32A66875E79EE9BEB /* SessionDataTask.swift */, + D467CB3DCF65BE9C4B4067D5FFDD6859 /* SessionDelegate.swift */, + 1F3E90B88F5618F4D313769ED312089B /* SizeExtensions.swift */, + 80E936467024EE1D60D40AF511570E73 /* Source.swift */, + DA9BBA4DCEA951FBE5842187767A324A /* Storage.swift */, + 37B13934C4541B84DCD0CE345BE17813 /* String+MD5.swift */, + 71985F3B3B4297E61C48A92C40B847D8 /* TVMonogramView+Kingfisher.swift */, + AD245F26200BAD0F0C9385C5DEA429B7 /* UIButton+Kingfisher.swift */, + A3B6D0D716E13A355BB46955460FFB80 /* WKInterfaceImage+Kingfisher.swift */, + B8A20A7D7F1EBFDFFD7501C858866CEC /* Support Files */, + ); + path = Kingfisher; + sourceTree = ""; + }; + 876F7B3ED4F3B17FD0CD50CAAA681A04 /* iOS */ = { + isa = PBXGroup; + children = ( + EB56C884B7F74971F1D47C11641BD905 /* Accelerate.framework */, + 7388592716E23DB7918BA1CEF956BA19 /* CFNetwork.framework */, + 9060464159E13D7209F7D64BACF83DAD /* Foundation.framework */, + 007F365850B5C8A7EEA9E7F41F325B11 /* UIKit.framework */, + ); + name = iOS; + sourceTree = ""; + }; + 8CC0F3343BEDDC8D0126C8DC722D0B81 /* SwiftMessages */ = { + isa = PBXGroup; + children = ( + 189B53BB925CD0E96CFBEF2E63E4BDC3 /* App */, + 125B588671DF46336D0F8154F40DE542 /* Support Files */, + ); + path = SwiftMessages; + sourceTree = ""; + }; + 8CEC1A3C70C6B1B04C3B1F5803132750 /* Targets Support Files */ = { + isa = PBXGroup; + children = ( + 074A5B71721A1B3F50DB8082DDD11157 /* Pods-Bulletin */, + ); + name = "Targets Support Files"; + sourceTree = ""; + }; + B2C6EA9410FBA4324564C77CB2431009 /* IGListDiffKit */ = { + isa = PBXGroup; + children = ( + 969EC8B144DCFDBB8D6B20563FD767D8 /* IGListAssert.h */, + C904783C0CEAE96552A679C695669DBC /* IGListBatchUpdateData.h */, + 369EB2D2F32E873F3665AE2FB0A59DB9 /* IGListBatchUpdateData.mm */, + 15FF74DFD7DA6973B4D076F529572592 /* IGListCompatibility.h */, + 0495A674038D17605C95A9A5B9213100 /* IGListDiff.h */, + 974BF1702E4695985E60FBB45338A8A7 /* IGListDiff.mm */, + 03C406CF79037D94F19036ADEC84DAC7 /* IGListDiffable.h */, + E9EF2C50B2F9F3FC8C75CE92032462AA /* IGListDiffKit.h */, + 759C0577310E0EFEB52904517125DCF7 /* IGListExperiments.h */, + 4BED2EF6A3B24B9C0BF0E9524B7B6436 /* IGListIndexPathResult.h */, + E8101380E5E87B4F4EC6DDD9D53C9667 /* IGListIndexPathResult.m */, + 526AFE8B2C5F857BB1C8ACF879C0ECCE /* IGListIndexPathResultInternal.h */, + 7AC803E40D56EEDCA7EE11F40F5D2873 /* IGListIndexSetResult.h */, + F1D5521BDDA1A0242A36C1929596B475 /* IGListIndexSetResult.m */, + 07BC0E95B093644150AD1D0260560C1D /* IGListIndexSetResultInternal.h */, + 51A70695551771573DC2AA2EBA4C1856 /* IGListMacros.h */, + 896B6B102742772B634D79305645CA79 /* IGListMoveIndex.h */, + 28E5CF6FD87B92B31A35248BCCC7BDBE /* IGListMoveIndex.m */, + 308B9DAEAE417FCC8AB9D385110BAB88 /* IGListMoveIndexInternal.h */, + DF5E502A140B8C7136E1F676C002042C /* IGListMoveIndexPath.h */, + 92CF5980D57F9F674AE272CB46916FF0 /* IGListMoveIndexPath.m */, + 996DBB5DDFA8515120D75647C8498AE8 /* IGListMoveIndexPathInternal.h */, + 51E9A430A8CB278156CC969E105579DA /* NSNumber+IGListDiffable.h */, + 4E92132F1FBCAEC55FD846EB03A29C5C /* NSNumber+IGListDiffable.m */, + EF2222773C495CA6BA87AAAA93B049B9 /* NSString+IGListDiffable.h */, + 588967168C815A5579F4C6C0A3ADF4CA /* NSString+IGListDiffable.m */, + 43CF91C12C3F2936102F46DA4F2C7C75 /* Support Files */, + ); + path = IGListDiffKit; + sourceTree = ""; + }; + B8A20A7D7F1EBFDFFD7501C858866CEC /* Support Files */ = { + isa = PBXGroup; + children = ( + 841EA054CFC907C24018C45ADC5F3E63 /* Kingfisher.modulemap */, + 10FD14B174196E8692A3FF6CE273842B /* Kingfisher-dummy.m */, + 0F78C89D7312191E330B806A85A69672 /* Kingfisher-Info.plist */, + 2FA03600975416DCA7ED5BA333AC99DB /* Kingfisher-prefix.pch */, + EE7F5B9CFE3D767F3DB305EE26C2283F /* Kingfisher-umbrella.h */, + 7CB626D861306564520283F74F854D64 /* Kingfisher.debug.xcconfig */, + D2EC6A9540A860E7EDBF13CF404CB4B8 /* Kingfisher.release.xcconfig */, + ); + name = "Support Files"; + path = "../Target Support Files/Kingfisher"; + sourceTree = ""; + }; + BA4F31F07263C99FC76E66D632A59F09 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 876F7B3ED4F3B17FD0CD50CAAA681A04 /* iOS */, + ); + name = Frameworks; + sourceTree = ""; + }; + CF1408CF629C7361332E53B88F7BD30C = { + isa = PBXGroup; + children = ( + 9D940727FF8FB9C785EB98E56350EF41 /* Podfile */, + BA4F31F07263C99FC76E66D632A59F09 /* Frameworks */, + 07FCA4F85284D25B675A378F88FBBA14 /* Pods */, + 685AAE32654D08B0A9F021FD45A52D88 /* Products */, + 8CEC1A3C70C6B1B04C3B1F5803132750 /* Targets Support Files */, + ); + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 19125098D221B2D8C5F1214DAE4046C3 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 4EE2EC2F10C2D2BC999CE30C679575FB /* HEXColor.h in Headers */, + 3DB2320AF24E5F4C92900577EE44878C /* UIColor_Hex_Swift-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3229198688E541F69F93DB2A2878FFE6 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 840F7452FC8C95F23BF04DD115213F2D /* IGListAdapter.h in Headers */, + 504FC6E2898BB2EAA2099072FBECB8B8 /* IGListAdapter+DebugDescription.h in Headers */, + 9CE4B00AD0EFD1C0A048270DA64986B2 /* IGListAdapter+UICollectionView.h in Headers */, + 248A5D0758579F45393A48F7F1A816EE /* IGListAdapterDataSource.h in Headers */, + F28F7B608086B6126003F02D8F79D79F /* IGListAdapterDelegate.h in Headers */, + C0BB4FD34FBDE2C9E8571DD42F56FDD3 /* IGListAdapterInternal.h in Headers */, + 857AB5F39FCA5E1F81A5D20A3A16DD19 /* IGListAdapterMoveDelegate.h in Headers */, + 7CFC6E5C81DB4F42F1C469C8194FE64C /* IGListAdapterPerformanceDelegate.h in Headers */, + E1128C6D632504B9D7AF99B3838D3DCC /* IGListAdapterProxy.h in Headers */, + 07ED65FC8B25E7420799D68D2CC59E7D /* IGListAdapterUpdateListener.h in Headers */, + 9ED0DFF2B7B21C647DDFC8559C9479C2 /* IGListAdapterUpdater.h in Headers */, + 9BDCB75960A60DD0DEE0FA6101987B26 /* IGListAdapterUpdater+DebugDescription.h in Headers */, + DF12FA0162AEB2360E9D896FFCE3D8FD /* IGListAdapterUpdaterDelegate.h in Headers */, + 770A7C634F7BBC9E2C030AAC217A383C /* IGListAdapterUpdaterInternal.h in Headers */, + 7597FD2790FC3E3002CC855FDF1EEFAB /* IGListArrayUtilsInternal.h in Headers */, + C3F1FE6BB2F3E83DF225DB091D768ED3 /* IGListBatchContext.h in Headers */, + DA36BB36699075B706863F8E6462F524 /* IGListBatchUpdateData+DebugDescription.h in Headers */, + 29A7C0BC88D85F89318C67E6293CFFB4 /* IGListBatchUpdates.h in Headers */, + 74021AE8DA8068962E566374E4097561 /* IGListBatchUpdateState.h in Headers */, + 3E9D03B9D56D435F1387EEE5B848D0C7 /* IGListBindable.h in Headers */, + 653E4F4EE5A4AE7C8E2C2BD9BE9AD111 /* IGListBindingSectionController.h in Headers */, + C78B03AD94E9ADA8E4A492D852A4036D /* IGListBindingSectionController+DebugDescription.h in Headers */, + AD951EA8C76B4AE7CFB4669804B8275B /* IGListBindingSectionControllerDataSource.h in Headers */, + 4735A168EFDF0E56425C926BCEF42F33 /* IGListBindingSectionControllerSelectionDelegate.h in Headers */, + D8332E09169A7A9D00E0398D4392DB70 /* IGListCollectionContext.h in Headers */, + 5EBA64E274109B68134E970A1C831236 /* IGListCollectionScrollingTraits.h in Headers */, + 8933125F14AA552ACB4B02ACEAC95A46 /* IGListCollectionView.h in Headers */, + 9C5254A71019F6D346E643611192AAF4 /* IGListCollectionViewDelegateLayout.h in Headers */, + 0D293ABB2BD3C45D748001FF3929AD4B /* IGListCollectionViewLayout.h in Headers */, + 456D4E89F94D07D831D2D06212DD3A21 /* IGListCollectionViewLayoutCompatible.h in Headers */, + 73BC2BAEAFC76A79BF2A6E23CA54044F /* IGListCollectionViewLayoutInternal.h in Headers */, + A404C0924ECF70F20F76E1AA5F32876D /* IGListDebugger.h in Headers */, + 1199F40D641095A06DC44FA62F589282 /* IGListDebuggingUtilities.h in Headers */, + 0CFD8E5542E0804B92A255670C94AB24 /* IGListDisplayDelegate.h in Headers */, + 627B24A21C799D9876E8EDA4C203EB5C /* IGListDisplayHandler.h in Headers */, + F6CA0DA52CCF6EB9B47B22196FA14E3E /* IGListGenericSectionController.h in Headers */, + 395296368FAD6FEE70CE19FBCE22C316 /* IGListIndexPathResultInternal.h in Headers */, + 3F69A921AA08DB133E702F3F7C395405 /* IGListIndexSetResultInternal.h in Headers */, + D0D540212AFD7154B4C9207A9E5C911A /* IGListKit.h in Headers */, + A19366C6BC7101DC193D92176EB89C37 /* IGListKit-umbrella.h in Headers */, + F7EF7184D41C22A77D70A2AE233D4E53 /* IGListMoveIndexInternal.h in Headers */, + 5C1B342A3F54657EEA7FC2105EF75523 /* IGListMoveIndexPathInternal.h in Headers */, + 145318846A4197187343A7223A8366BF /* IGListReloadDataUpdater.h in Headers */, + A1FFF4B8FB3410D63594007BBA288632 /* IGListReloadIndexPath.h in Headers */, + 7448C55AC852D9DD05F91D03E7430E4C /* IGListScrollDelegate.h in Headers */, + 4B86DE37B3B553A574A54CCDDBC13680 /* IGListSectionController.h in Headers */, + BF8F8C64DB7DBEC09546AE2C68B1929B /* IGListSectionControllerInternal.h in Headers */, + 0052866F22FC243E2A20CC326BFF3F58 /* IGListSectionMap.h in Headers */, + A306349DCF43F00C535D8A7C2B5DDE55 /* IGListSectionMap+DebugDescription.h in Headers */, + E0B5B9AA33DBB50133BF1886B076688A /* IGListSingleSectionController.h in Headers */, + 256BA1F1360654F69778E29BBE25B04D /* IGListSupplementaryViewSource.h in Headers */, + 73448A3812B760B85DD6B64B85B49165 /* IGListTransitionDelegate.h in Headers */, + EB11BE0DBFDA8DFB6DA4A0C9AA39DEAE /* IGListUpdatingDelegate.h in Headers */, + 8AC9E9ACB2C52077863D538C75C1E430 /* IGListWorkingRangeDelegate.h in Headers */, + B1DAC65FFC831F6D51EAC90BD4B4D92D /* IGListWorkingRangeHandler.h in Headers */, + 61976AFFB89BE9B4FFCCE448B16B895D /* UICollectionView+DebugDescription.h in Headers */, + 0C5AD6C37D168B6D4CEC37F24AEFEF9C /* UICollectionView+IGListBatchUpdateData.h in Headers */, + 6E3E76E4A7F77A3973EE6449CE275137 /* UICollectionViewLayout+InteractiveReordering.h in Headers */, + 29A9E81F33421D1E32648B12D20B4F68 /* UIScrollView+IGListKit.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 366AE1807437E1222BD7F8F084E53552 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + F2DC134E310796CDE38903CADC436726 /* Pods-Bulletin-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3D78E6B12D0C7704AA6257B6D7CE969B /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + E290630326514476CA3D4651D3FB2C12 /* SwiftMessages-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7AE52B176E9872452FD890FC7F460CE1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 122980E44B15C64CF0B14DC94D7EB5C9 /* Kingfisher-umbrella.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + F0A5A4D19AE00FBC278C2B9513587246 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + E7E6D0A1C2F66770BE1A484A01B01A63 /* IGListAssert.h in Headers */, + 2D2A77A53A45AA03F16F2411E6F7FEB7 /* IGListBatchUpdateData.h in Headers */, + EE402590604403A03F734B8AD641AAD9 /* IGListCompatibility.h in Headers */, + 769764555AF9B0D77F7D1BDF6694A0F3 /* IGListDiff.h in Headers */, + AB3D4BBA18DEA9DDCB82E15EE6DA80FE /* IGListDiffable.h in Headers */, + 637DFADEF4F664E6E6932F0E27472FD5 /* IGListDiffKit.h in Headers */, + 4E69250DFE230B7D9134B5360A91EC8D /* IGListDiffKit-umbrella.h in Headers */, + 8EF72BE56C31A8A67872154F61605B81 /* IGListExperiments.h in Headers */, + 5456CE7488C7FE316C00605531107BFD /* IGListIndexPathResult.h in Headers */, + A0778F3A26137BE9262E2F4E042D04DF /* IGListIndexPathResultInternal.h in Headers */, + 7D6EB166CD4530B9E1CB943798269D5B /* IGListIndexSetResult.h in Headers */, + 2D4F11BB21F21B0F1687BDA71200EB74 /* IGListIndexSetResultInternal.h in Headers */, + 6A3B187375746C0888E5CFCDD868FCCB /* IGListMacros.h in Headers */, + 53526734412F401A087EC20F9A3CC7EB /* IGListMoveIndex.h in Headers */, + FC9E1CBF800DFB8FE8F0E29D1CB8EA1A /* IGListMoveIndexInternal.h in Headers */, + 0F9B2F409EF8A5AADE05FC337C4F4B20 /* IGListMoveIndexPath.h in Headers */, + 37E2C53DDA53EE7696F3C78A095A70C2 /* IGListMoveIndexPathInternal.h in Headers */, + 0ABF3498CC7F1EB2B3B3EA0648268D58 /* NSNumber+IGListDiffable.h in Headers */, + 1B2DDB8AD9504EB5AF857E4A45E3A979 /* NSString+IGListDiffable.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 216ED480D97315FF6DBB931E55B48AB1 /* Pods-Bulletin */ = { + isa = PBXNativeTarget; + buildConfigurationList = 61FD5F1D788300894E920658B01F90CA /* Build configuration list for PBXNativeTarget "Pods-Bulletin" */; + buildPhases = ( + 366AE1807437E1222BD7F8F084E53552 /* Headers */, + 10B66A67CB479C53B770CAD85D267537 /* Sources */, + 1590561D96F3E074FE469B04D8F764C8 /* Frameworks */, + 6462E53101F75019610E24424EB457AF /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4AB106F6C169F1F7C5633FFC0EED5546 /* PBXTargetDependency */, + 3C96041AF97B074F4C690C5C62A6368B /* PBXTargetDependency */, + DF5A26E717434EAE7445B6AA418E6781 /* PBXTargetDependency */, + 02CC40193B2425741A35E17204B03688 /* PBXTargetDependency */, + 5C0C1B16990CADEC6C388F5C3EE827D2 /* PBXTargetDependency */, + ); + name = "Pods-Bulletin"; + productName = Pods_Bulletin; + productReference = 20F98B848D2BF72F16DA1BDACF869D1A /* Pods-Bulletin */; + productType = "com.apple.product-type.framework"; + }; + 2D07A05F514BF9C5434DAC907988C708 /* IGListDiffKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 124A39A4C3D1564DB02296737FECE89C /* Build configuration list for PBXNativeTarget "IGListDiffKit" */; + buildPhases = ( + F0A5A4D19AE00FBC278C2B9513587246 /* Headers */, + AA9AF8B13C4EC8776F28671C673BA38B /* Sources */, + 0DC33FC81EEF5C13DC2B4EF85CA14F78 /* Frameworks */, + 8B08ACC7E062463EF1B86EAAE6986B51 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = IGListDiffKit; + productName = IGListDiffKit; + productReference = 195AD71F7938FEA988FDCC5B0A10FFDF /* IGListDiffKit */; + productType = "com.apple.product-type.framework"; + }; + 674FDCAB4D51E702521F4CD31807F659 /* IGListKit */ = { + isa = PBXNativeTarget; + buildConfigurationList = 15F7B5F2389DC795EC831863F9DF95D5 /* Build configuration list for PBXNativeTarget "IGListKit" */; + buildPhases = ( + 3229198688E541F69F93DB2A2878FFE6 /* Headers */, + FC6E57FBE94AA87B13A2A82116BCE1C0 /* Sources */, + F7EC70ECA9F3248B43189E7C38690FA1 /* Frameworks */, + 6274298B3835F66040958FCB20E78D0C /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + D2B8578FFD713994A8FFF50F2D3EBFB8 /* PBXTargetDependency */, + ); + name = IGListKit; + productName = IGListKit; + productReference = B3C6C3E0B0FE2750FE588E0AFBA66D62 /* IGListKit */; + productType = "com.apple.product-type.framework"; + }; + 93789445511E3FEAB3D09A6F552EEE2D /* SwiftMessages-SwiftMessages */ = { + isa = PBXNativeTarget; + buildConfigurationList = 3EBF158ED6C977460E2094CBA2B9AFC1 /* Build configuration list for PBXNativeTarget "SwiftMessages-SwiftMessages" */; + buildPhases = ( + 1347383E8A3EA46FFEE4DA065D68A000 /* Sources */, + 524D0CCC1F888BC91F616D53B4C381E8 /* Frameworks */, + 871EF57FA89342B1161F07B798088308 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = "SwiftMessages-SwiftMessages"; + productName = SwiftMessages; + productReference = 753DD909979478A0C1B33E0524504EF1 /* SwiftMessages-SwiftMessages */; + productType = "com.apple.product-type.bundle"; + }; + A0CA3B0841360687C00D0E48D3E4C1B9 /* UIColor_Hex_Swift */ = { + isa = PBXNativeTarget; + buildConfigurationList = E22B609ECAEFA85B75A98369FF2A6B24 /* Build configuration list for PBXNativeTarget "UIColor_Hex_Swift" */; + buildPhases = ( + 19125098D221B2D8C5F1214DAE4046C3 /* Headers */, + D890B797D9B7D90296E55AB16B09E319 /* Sources */, + 5C9B3C65E54998F5193CA304A90C9083 /* Frameworks */, + 138A97688963F4E7BFE825F22241B122 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = UIColor_Hex_Swift; + productName = UIColor_Hex_Swift; + productReference = 58FA08FD4595D3CA9B2AA81405DE2EFA /* UIColor_Hex_Swift.framework */; + productType = "com.apple.product-type.framework"; + }; + DAB613A18652334F6BFC5F27BADF515D /* SwiftMessages */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4FD7C2B04E1FC5365718930ADC799D5C /* Build configuration list for PBXNativeTarget "SwiftMessages" */; + buildPhases = ( + 3D78E6B12D0C7704AA6257B6D7CE969B /* Headers */, + A65DC52B960385FEF4808166660E82D7 /* Sources */, + 1E5F34F325312F632646732A454D6AD6 /* Frameworks */, + 2748DBA8705A702EA4150DD61A19FD69 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0491256AEC27E3A33CE795014F28F2FD /* PBXTargetDependency */, + ); + name = SwiftMessages; + productName = SwiftMessages; + productReference = 4824F23D80FF9070A5F8A452DB11EB9A /* SwiftMessages */; + productType = "com.apple.product-type.framework"; + }; + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */ = { + isa = PBXNativeTarget; + buildConfigurationList = 69ACD8654734266A348C6FF68E734010 /* Build configuration list for PBXNativeTarget "Kingfisher" */; + buildPhases = ( + 7AE52B176E9872452FD890FC7F460CE1 /* Headers */, + 3C1DA515D615F8CE75565ACE14378882 /* Sources */, + 81DB1665E1495609510BA493822E5A85 /* Frameworks */, + CFEB3E9FD20A01120B40D65B82D8F26F /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Kingfisher; + productName = Kingfisher; + productReference = C3F44C782D64D7EB20B61CE3844EBFAD /* Kingfisher */; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + BFDFE7DC352907FC980B868725387E98 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1300; + LastUpgradeCheck = 1300; + }; + buildConfigurationList = 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + Base, + en, + ); + mainGroup = CF1408CF629C7361332E53B88F7BD30C; + productRefGroup = 685AAE32654D08B0A9F021FD45A52D88 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2D07A05F514BF9C5434DAC907988C708 /* IGListDiffKit */, + 674FDCAB4D51E702521F4CD31807F659 /* IGListKit */, + E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */, + 216ED480D97315FF6DBB931E55B48AB1 /* Pods-Bulletin */, + DAB613A18652334F6BFC5F27BADF515D /* SwiftMessages */, + 93789445511E3FEAB3D09A6F552EEE2D /* SwiftMessages-SwiftMessages */, + A0CA3B0841360687C00D0E48D3E4C1B9 /* UIColor_Hex_Swift */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 138A97688963F4E7BFE825F22241B122 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2748DBA8705A702EA4150DD61A19FD69 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6E5BC0F0DF1D4D4C3A81A3B1ABEB2E52 /* SwiftMessages-SwiftMessages in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6274298B3835F66040958FCB20E78D0C /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 6462E53101F75019610E24424EB457AF /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 871EF57FA89342B1161F07B798088308 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 106E77E6CCA0360F0EBCC653774E25F5 /* CardView.xib in Resources */, + 52800834B090075CA6F35EF7306B5AE5 /* CenteredView.xib in Resources */, + C8ED20CFCDFF904961EA7D6FCFAD1AAF /* errorIcon.png in Resources */, + 48065C2580FF9E5D2DE0E6447AACB110 /* errorIcon@2x.png in Resources */, + 404DCA74511EDA44B896202CD64458A0 /* errorIcon@3x.png in Resources */, + 08CA1A7B0A9F6B96CA4DFCFF977759CC /* errorIconLight.png in Resources */, + EE3740A3D6C42F3766288ED5BCB8B95D /* errorIconLight@2x.png in Resources */, + 1BCE5E9382038062602EC3B882B0B1C1 /* errorIconLight@3x.png in Resources */, + 4A3FACE084B0154043BD4015B3E8FCC2 /* errorIconSubtle.png in Resources */, + 8FB6113E9C4FFEB7A580060F3040B53C /* errorIconSubtle@2x.png in Resources */, + 83557F428BB0AF138061AD66390FAE57 /* errorIconSubtle@3x.png in Resources */, + F4852EA1DC3E1F1F346402EAEFE1109F /* infoIcon.png in Resources */, + 3A16D398DAB6AFE26767E5B5E9A37767 /* infoIcon@2x.png in Resources */, + 21191DB3B6E3981B1D9682644743FF7E /* infoIcon@3x.png in Resources */, + 94E07ACBB6D1F91A84009EE52CA247AD /* infoIconLight.png in Resources */, + BC34CECDF734877FE9651B831504D3F2 /* infoIconLight@2x.png in Resources */, + 96BA7BD41B99BCBE403CFC49DF217990 /* infoIconLight@3x.png in Resources */, + 724DBA853B4874B96363B04EA98D4147 /* infoIconSubtle.png in Resources */, + FF467117653668E74E2DA504B3BC3547 /* infoIconSubtle@2x.png in Resources */, + 13786D2762D17C2F6715CA62D64AE76B /* infoIconSubtle@3x.png in Resources */, + 36AA900737FE6B23825191A9011181DB /* MessageView.xib in Resources */, + 4B7802B665FAD2255F4A3D5FADE64B08 /* StatusLine.xib in Resources */, + 39A7FD94C1F455775CBBBC49131CDE98 /* successIcon.png in Resources */, + 6D093916DC5318912596E0AD1AA26238 /* successIcon@2x.png in Resources */, + 2D7DD1D4C822CE247530FAFBF36B41B4 /* successIcon@3x.png in Resources */, + 2CC46C11380D37A6DE73F58D4DEFF754 /* successIconLight.png in Resources */, + 9F9F378DCF1582EBC63B23D9C8E2F238 /* successIconLight@2x.png in Resources */, + 76E95598BA0A70C68E8782BC178A0B86 /* successIconLight@3x.png in Resources */, + B7384B67D262049B7A3D9A5F44CF3AD8 /* successIconSubtle.png in Resources */, + 82925050E10EBBC55386CBB189824F09 /* successIconSubtle@2x.png in Resources */, + D1354255D2A30A71D2915BD7A0D10960 /* successIconSubtle@3x.png in Resources */, + 6354E790FC26E29FFFAB6F298404FE52 /* TabView.xib in Resources */, + 93149C816CBB4ED433D3A3EE6F5B9372 /* warningIcon.png in Resources */, + C111B17154D65AFC9862B663697D1C5C /* warningIcon@2x.png in Resources */, + A426AB76106748CDFB137114A6F63935 /* warningIcon@3x.png in Resources */, + 4AD71B305F0FFBCD1859562AA2F86A29 /* warningIconLight.png in Resources */, + 80AF29D95B77B1DFCBF7F674954A8513 /* warningIconLight@2x.png in Resources */, + 29CE33F429B08D390973ADE3F05A097F /* warningIconLight@3x.png in Resources */, + 12D9C86DE5B35895C8C4C2F66D98EF81 /* warningIconSubtle.png in Resources */, + 500C34AE9C3CBFFE6854C3627328C849 /* warningIconSubtle@2x.png in Resources */, + DD46E684526BF2A6315775DEBCA54960 /* warningIconSubtle@3x.png in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 8B08ACC7E062463EF1B86EAAE6986B51 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CFEB3E9FD20A01120B40D65B82D8F26F /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 10B66A67CB479C53B770CAD85D267537 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 92A554C6D0F616D62454C16FA59D8183 /* Pods-Bulletin-dummy.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 1347383E8A3EA46FFEE4DA065D68A000 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 3C1DA515D615F8CE75565ACE14378882 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 1952485AFF7A1BCCA4D4B142E82FE627 /* AnimatedImageView.swift in Sources */, + 243D7CFE1D56ED80ACB2B3E71B4CB603 /* AuthenticationChallengeResponsable.swift in Sources */, + D603AA58EF97D461A57B2B1BCB883868 /* AVAssetImageDataProvider.swift in Sources */, + B25E07EA645911443A38DA1E68166156 /* Box.swift in Sources */, + CD7AC3E1C98EA54F7C05C36C52805220 /* CacheSerializer.swift in Sources */, + FF09824309346665E2F1F7F5A45FB10F /* CallbackQueue.swift in Sources */, + DBB8088E14A2ADEDB1CD840BAC835267 /* CPListItem+Kingfisher.swift in Sources */, + 457BE444ED617FA7D6851D6DAA9D7234 /* Delegate.swift in Sources */, + 1FEE89BF952BE7ACA46E642DA2E48CA2 /* DiskStorage.swift in Sources */, + 1185A2B40E14F2FCBC761FC99777CAD8 /* ExtensionHelpers.swift in Sources */, + 3AF7DB9AEFF47F1F7F91AF28440E4AC6 /* Filter.swift in Sources */, + DE532EF7D50A9CF68587DAD4C1A02BD7 /* FormatIndicatedCacheSerializer.swift in Sources */, + F24021BDE9B42D604E3341CAD8E34759 /* GIFAnimatedImage.swift in Sources */, + 582D59E0D2EF62E0575933C99B393704 /* GraphicsContext.swift in Sources */, + 70FEC06F54286257E1BA1ECA0C99198D /* Image.swift in Sources */, + 1AA89F327105C026976BF6E831B193A2 /* ImageBinder.swift in Sources */, + ED0C8BA7560D7324587B353E0960479F /* ImageCache.swift in Sources */, + F5414F8A5B40521D0E4AEEB28378CB49 /* ImageContext.swift in Sources */, + 80738D8956C9987CCCEDF551961E5069 /* ImageDataProcessor.swift in Sources */, + 0285857A24F66E925987A5876F0BE63B /* ImageDataProvider.swift in Sources */, + DD72DC30CF19FFC81AB19CD0B074000D /* ImageDownloader.swift in Sources */, + 881A35B28D93C56E46E305F6138B1A76 /* ImageDownloaderDelegate.swift in Sources */, + DAFC6CE6321395CF4523DD66DADBB9BA /* ImageDrawing.swift in Sources */, + E6D6C7D5E458A05CC736C340F853E9F6 /* ImageFormat.swift in Sources */, + F54DE563418B1783D6EC491A0C3A05DB /* ImageModifier.swift in Sources */, + 00BEA6029C428FEE644AC3D42AD83282 /* ImagePrefetcher.swift in Sources */, + EF9C4588CDA85AED8BBCF77451B2A35B /* ImageProcessor.swift in Sources */, + 5F852F38CBC282496CCBE37C51324B2F /* ImageProgressive.swift in Sources */, + 29FF13E23FD52E46D30530549410AD7C /* ImageTransition.swift in Sources */, + E5B664771063F1A9A372519A8466860B /* ImageView+Kingfisher.swift in Sources */, + DFCDE4638265B4CCD494ECA5D560DBEE /* Indicator.swift in Sources */, + DF4563832C19B8582C810BF502A5CA29 /* KF.swift in Sources */, + 7FFE4021A4F14124342AD41CE1117B3E /* KFAnimatedImage.swift in Sources */, + 5E27DD292D3A55657712DD7AFA7B8FCA /* KFImage.swift in Sources */, + 5ADB30DD9A03859018550A999ACB0652 /* KFImageOptions.swift in Sources */, + F9537B023E24AC4A724E301F7E372491 /* KFImageProtocol.swift in Sources */, + 0510E8EA51914CB2176AD0F173937FAB /* KFImageRenderer.swift in Sources */, + A316388A35648CB2987E761771456087 /* KFOptionsSetter.swift in Sources */, + A39D3555EC8B45B7D6B9505DDAF0F117 /* Kingfisher.swift in Sources */, + 4DCA9775E5CCF599460BDB46E77F6FA4 /* Kingfisher-dummy.m in Sources */, + 4F37E521D341C47CE73DDCF21BA95A52 /* KingfisherError.swift in Sources */, + 1FE693B5ACC6AD7320CEFC20B64546E4 /* KingfisherManager.swift in Sources */, + 05228565AAA7FCED4BAFB2B7EF71D53D /* KingfisherOptionsInfo.swift in Sources */, + 1FD2928BC156D990D68B105F518C60B6 /* MemoryStorage.swift in Sources */, + 059639E700DEFAEF08F56484E5F67BE7 /* NSButton+Kingfisher.swift in Sources */, + 59BC9047F4BEBBC06235608D974E230D /* NSTextAttachment+Kingfisher.swift in Sources */, + 506128E1CC424E40E2691546D9547549 /* Placeholder.swift in Sources */, + 2BE89C24BFD3FB663E37C607C289B3B6 /* RedirectHandler.swift in Sources */, + 509490FB1D30FEC59AE4BC21AEEBB7BB /* RequestModifier.swift in Sources */, + 22BD1346F66BFCB129AAA44EEF322AC9 /* Resource.swift in Sources */, + A88A844D5356E1690E445024CB796E09 /* Result.swift in Sources */, + 22216C300C763044344B9DBF97317E63 /* RetryStrategy.swift in Sources */, + EBB32304E8DD4BA115454E0050D47DED /* Runtime.swift in Sources */, + 7C7418FF01DD7BB909719682B634A8A5 /* SessionDataTask.swift in Sources */, + 0ED8FBFD9A86D21BF69137EC9350E575 /* SessionDelegate.swift in Sources */, + 57FC31B14C753B5C63CEF00560F8A6EF /* SizeExtensions.swift in Sources */, + 9B0A78AC22E7EDA755F51D86527E2D9C /* Source.swift in Sources */, + 25FC036BEA33CAB5D80F5A41644535D3 /* Storage.swift in Sources */, + BD382E78580D295D10100678D4F66A76 /* String+MD5.swift in Sources */, + F17B1F8F2B6580343025237455A29D61 /* TVMonogramView+Kingfisher.swift in Sources */, + 082EDC820D76DF95C71A5018112DE512 /* UIButton+Kingfisher.swift in Sources */, + 9F5FE22DA95B66B8DC21CB13BE25EC9B /* WKInterfaceImage+Kingfisher.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + A65DC52B960385FEF4808166660E82D7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + DD87BCC0696946A029E25B638BFB1971 /* AccessibleMessage.swift in Sources */, + 7947B831C0A0B1D6E01E862603BB6369 /* Animator.swift in Sources */, + 26A9A1D5029BDEB2C0AF0ECFE65ECBB6 /* BackgroundViewable.swift in Sources */, + ABAD2E2E0682AB7FF1C6213388E4EBA2 /* BaseView.swift in Sources */, + A55902F47A9DACFEAC7D0833A569911E /* CALayer+Extensions.swift in Sources */, + AFCAEA6F64C31BBCA51EDE0A22C971FF /* CornerRoundingView.swift in Sources */, + 8AF999CCB198F34068E9AC558864262D /* Error.swift in Sources */, + 6C4C53377507F9D77E67A19F3B0DAE69 /* Identifiable.swift in Sources */, + 69A39E2DCEE7140DF3654C581C401637 /* KeyboardTrackingView.swift in Sources */, + 2CDD1385DB4E94B72AA923A2DB34D030 /* MarginAdjustable.swift in Sources */, + EE2007369A32179A1540374E29C343AE /* MarginAdjustable+Extensions.swift in Sources */, + 2A44FC42DA4E309A6CD0C23845E62FDD /* MaskingView.swift in Sources */, + AEB007C7128575842880AD5559D2D5AC /* MessageView.swift in Sources */, + BFAB4C4DAEFAAE316F3F7006D0E7F45B /* NSBundle+Extensions.swift in Sources */, + A9CCA2DCDE1D1EA45BAD74C5CB3E9253 /* NSLayoutConstraint+Extensions.swift in Sources */, + EF73165F049DB4864538A394FFCBE874 /* PassthroughView.swift in Sources */, + 93427084C7854C572B84FF0C78CAD7C5 /* PassthroughWindow.swift in Sources */, + AD707CFC669380B926AFB1FE7AE9B867 /* PhysicsAnimation.swift in Sources */, + FA006B42B99DAF8263341881FD510FF9 /* PhysicsPanHandler.swift in Sources */, + 33634B64A37F709874AB14578331B98D /* Presenter.swift in Sources */, + AC348A3AA450BFA43A0B4869FB82772A /* SwiftMessages.swift in Sources */, + 08107140946FA9287B2C63728BFABB2E /* SwiftMessages-dummy.m in Sources */, + 7A5320B15F6789F272E61C44F9F4DBB0 /* SwiftMessages.Config+Extensions.swift in Sources */, + 137EF15CD62928D2E8C32B18E35CDA84 /* SwiftMessagesSegue.swift in Sources */, + 645463E624115A42FE45DD945C41DB40 /* Theme.swift in Sources */, + 7E08A94F10299EADB052CAE34F2BFA00 /* TopBottomAnimation.swift in Sources */, + 7E98E18B747F60326E4BA87E15839F29 /* UIEdgeInsets+Extensions.swift in Sources */, + 1F727CB62A71B2ABE2CA4FBDC563A4A1 /* UIViewController+Extensions.swift in Sources */, + 75240B35A5337C0362A81BA4E19C50A8 /* UIWindow+Extensions.swift in Sources */, + 1EEBEB3E8E43888EFF440D810F4FE61F /* Weak.swift in Sources */, + BB2B1C8268535C8BB056EB34B907FE16 /* WindowScene.swift in Sources */, + A122D2A3C925C48C11A200271047323E /* WindowViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + AA9AF8B13C4EC8776F28671C673BA38B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E362C21CF43363A849410617D9B0A524 /* IGListBatchUpdateData.mm in Sources */, + D05EC2EA72C86F0F25AC384102DE7238 /* IGListDiff.mm in Sources */, + 5752BA2732399E70CFA4026649B32D3B /* IGListDiffKit-dummy.m in Sources */, + F209D62202BD5D087E33685E52308500 /* IGListIndexPathResult.m in Sources */, + 7BB3B41A692D42272CEB3CD6023A97B8 /* IGListIndexSetResult.m in Sources */, + DA537286B85140EC0B108E803DFB545E /* IGListMoveIndex.m in Sources */, + 0B5893ABAF3F5EB6896FF6748BA24B9D /* IGListMoveIndexPath.m in Sources */, + 9E2B220A2A3223F08334BBCBDFEE5BC3 /* NSNumber+IGListDiffable.m in Sources */, + FF3EB79404B1F1AF27E7298F4DF62FA2 /* NSString+IGListDiffable.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D890B797D9B7D90296E55AB16B09E319 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 3EEE359E3EA05A87BEABA426D6092571 /* StringExtension.swift in Sources */, + 2EB12379C6E40E835D1AC08FAADFC392 /* UIColor_Hex_Swift-dummy.m in Sources */, + 20A92FC736588CF2C5688AFD384EAD9D /* UIColorExtension.swift in Sources */, + D005E9A215793DD5E318F94052C45350 /* UIColorInputError.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + FC6E57FBE94AA87B13A2A82116BCE1C0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + C0E2F479332ED0297D2152651EEBBC32 /* IGListAdapter.m in Sources */, + C3CE74410F050DCA5E2C8E1FF26A1125 /* IGListAdapter+DebugDescription.m in Sources */, + 31D2A0320106459AC5D7A31F8C41646B /* IGListAdapter+UICollectionView.m in Sources */, + E5FEA90E76B8D19728AE8D30969EB744 /* IGListAdapterProxy.m in Sources */, + 8A465CC2B4BB341735A5AEB15B28C587 /* IGListAdapterUpdater.m in Sources */, + 290CE4B2273A8DD7501C883649D798F4 /* IGListAdapterUpdater+DebugDescription.m in Sources */, + E4B7D3D7779F1394C8104CF3615EC223 /* IGListBatchUpdateData+DebugDescription.m in Sources */, + 3F2E1FB46259BAF163598529C1925BCB /* IGListBatchUpdates.m in Sources */, + F5FC561E72803649ED1023E463AB9D17 /* IGListBindingSectionController.m in Sources */, + F105D53A3A5A848EA428305ACF3FDBBB /* IGListBindingSectionController+DebugDescription.m in Sources */, + 05DF336E6DB67BB0621CCF0C9463ED4A /* IGListCollectionView.m in Sources */, + 0BC00DD25D3C4FF42C37433E81CA6F77 /* IGListCollectionViewLayout.mm in Sources */, + 2F0CA6E635295AA692FAEB9393783F4B /* IGListDebugger.m in Sources */, + DDA74D6EBFE54AC3941181507F8060AB /* IGListDebuggingUtilities.m in Sources */, + 1B17C9041C0794A94A39D5B31C62CEDE /* IGListDisplayHandler.m in Sources */, + C5814CF9B5EB63D89CBE27EE2A19520A /* IGListGenericSectionController.m in Sources */, + B707A3EC8581E09D0BA10908C235D401 /* IGListKit-dummy.m in Sources */, + 8500F9C0DFC843EB5A4ED4FF0664B213 /* IGListReloadDataUpdater.m in Sources */, + 86A6370F297F87A34ABC316470B564C3 /* IGListReloadIndexPath.m in Sources */, + B5E02A83F6C06F6D285E2EB1365A07BE /* IGListSectionController.m in Sources */, + 983C97E8D8F2AA85CAEDDB6D74EBA866 /* IGListSectionMap.m in Sources */, + CC2D622535AED6D39919BAAD8B55B828 /* IGListSectionMap+DebugDescription.m in Sources */, + DC576EBDA8822307BFAA01B1E07EB6B2 /* IGListSingleSectionController.m in Sources */, + C8E736F2681D5A47031328BE6DBAF0D6 /* IGListWorkingRangeHandler.mm in Sources */, + 194E2F707D9AE87266C08DDD38BAC2C5 /* UICollectionView+DebugDescription.m in Sources */, + 0C5C1FCD4460D91B069B29E4E4267EBF /* UICollectionView+IGListBatchUpdateData.m in Sources */, + ECFA80EB6B070B2B500F725449368B48 /* UICollectionViewLayout+InteractiveReordering.m in Sources */, + 6D3253DE7778CCCB009EFB375AA84800 /* UIScrollView+IGListKit.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 02CC40193B2425741A35E17204B03688 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = SwiftMessages; + target = DAB613A18652334F6BFC5F27BADF515D /* SwiftMessages */; + targetProxy = 800A33BC263765E54E7FBE37BFEC3000 /* PBXContainerItemProxy */; + }; + 0491256AEC27E3A33CE795014F28F2FD /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = "SwiftMessages-SwiftMessages"; + target = 93789445511E3FEAB3D09A6F552EEE2D /* SwiftMessages-SwiftMessages */; + targetProxy = 55802A5FB5EBEB01607325E334C83585 /* PBXContainerItemProxy */; + }; + 3C96041AF97B074F4C690C5C62A6368B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = IGListKit; + target = 674FDCAB4D51E702521F4CD31807F659 /* IGListKit */; + targetProxy = 1B3792504F0EED9EE18F016548B44086 /* PBXContainerItemProxy */; + }; + 4AB106F6C169F1F7C5633FFC0EED5546 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = IGListDiffKit; + target = 2D07A05F514BF9C5434DAC907988C708 /* IGListDiffKit */; + targetProxy = 66837020B415409BD9EC27EC5943B9A9 /* PBXContainerItemProxy */; + }; + 5C0C1B16990CADEC6C388F5C3EE827D2 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = UIColor_Hex_Swift; + target = A0CA3B0841360687C00D0E48D3E4C1B9 /* UIColor_Hex_Swift */; + targetProxy = E2E98D973191FE598F2C7D4D813CDBD5 /* PBXContainerItemProxy */; + }; + D2B8578FFD713994A8FFF50F2D3EBFB8 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = IGListDiffKit; + target = 2D07A05F514BF9C5434DAC907988C708 /* IGListDiffKit */; + targetProxy = BECFFC24423DFC78A02F544B59390655 /* PBXContainerItemProxy */; + }; + DF5A26E717434EAE7445B6AA418E6781 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + name = Kingfisher; + target = E8022D22FAA6690B5E1C379C1BCE1491 /* Kingfisher */; + targetProxy = BD3BFF283976737AF4A31E0233395338 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 032EDA8C88F2E73BC059D28AB5720416 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6DEBD199402AB8F5B24357217344B70B /* Pods-Bulletin.debug.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "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-Bulletin/Pods-Bulletin-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-Bulletin/Pods-Bulletin.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; + }; + 1B1FC92834E84FFF3F5F89754187D9C6 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9AA30AE820EDB8A30CFA77ED3357C1E5 /* IGListKit.release.xcconfig */; + buildSettings = { + "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/IGListKit/IGListKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/IGListKit/IGListKit-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/IGListKit/IGListKit.modulemap"; + PRODUCT_MODULE_NAME = IGListKit; + PRODUCT_NAME = IGListKit; + 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; + }; + 2B9E26EAE2CD392AD762421F663075A1 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + }; + 326089C1C11FB65356346DE055F5D586 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7DFA141F9E4B0E2D3D78DA1215079EE0 /* IGListKit.debug.xcconfig */; + buildSettings = { + "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/IGListKit/IGListKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/IGListKit/IGListKit-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/IGListKit/IGListKit.modulemap"; + PRODUCT_MODULE_NAME = IGListKit; + PRODUCT_NAME = IGListKit; + 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; + }; + 4A89754B2096AFF6713D0E1F5B788A0A /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9E7573C529987F0DB86026FA9777B8C3 /* Pods-Bulletin.release.xcconfig */; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO; + CLANG_ENABLE_OBJC_WEAK = NO; + "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-Bulletin/Pods-Bulletin-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-Bulletin/Pods-Bulletin.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; + }; + 4AD2E4574C8421AB16CD302FDE0B41EA /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A1B692CB750B2228127BCE50696B349C /* SwiftMessages.release.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SwiftMessages"; + IBSC_MODULE = SwiftMessages; + INFOPLIST_FILE = "Target Support Files/SwiftMessages/ResourceBundle-SwiftMessages-SwiftMessages-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + PRODUCT_NAME = SwiftMessages; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Release; + }; + 5748CEB3A27DF5D76974F97B6FDAFEB8 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 990E9245247EA175C5B3F06E3FD3ABEF /* UIColor_Hex_Swift.release.xcconfig */; + buildSettings = { + "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/UIColor_Hex_Swift/UIColor_Hex_Swift-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.modulemap"; + PRODUCT_MODULE_NAME = UIColor_Hex_Swift; + PRODUCT_NAME = UIColor_Hex_Swift; + 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; + }; + 63FAF33E1C55B71A5F5A8B3CC8749F99 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES; + 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_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + 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; + }; + 80CFF3701C30078A716619ED51F45BC5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A1B692CB750B2228127BCE50696B349C /* SwiftMessages.release.xcconfig */; + buildSettings = { + "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/SwiftMessages/SwiftMessages-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/SwiftMessages/SwiftMessages-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/SwiftMessages/SwiftMessages.modulemap"; + PRODUCT_MODULE_NAME = SwiftMessages; + PRODUCT_NAME = SwiftMessages; + 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; + }; + 980A58862D8A5086E2825CF9017AC8DD /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7CB626D861306564520283F74F854D64 /* Kingfisher.debug.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "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 = 12.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; + }; + A2CF0BE9EF01BC9B2AC0AF9538E6064D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1B08200E58B617675FE96006778840FB /* IGListDiffKit.release.xcconfig */; + buildSettings = { + "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/IGListDiffKit/IGListDiffKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/IGListDiffKit/IGListDiffKit-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/IGListDiffKit/IGListDiffKit.modulemap"; + PRODUCT_MODULE_NAME = IGListDiffKit; + PRODUCT_NAME = IGListDiffKit; + 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; + }; + AE25B7B6AE679D7B14C8B4DA0C761220 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB85FA489CBC63454660B040F371269C /* SwiftMessages.debug.xcconfig */; + buildSettings = { + CODE_SIGNING_ALLOWED = NO; + CONFIGURATION_BUILD_DIR = "$(BUILD_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME)/SwiftMessages"; + IBSC_MODULE = SwiftMessages; + INFOPLIST_FILE = "Target Support Files/SwiftMessages/ResourceBundle-SwiftMessages-SwiftMessages-Info.plist"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + PRODUCT_NAME = SwiftMessages; + SDKROOT = iphoneos; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + WRAPPER_EXTENSION = bundle; + }; + name = Debug; + }; + B2C89343EB7C202BAEE5FBED397500CA /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FB85FA489CBC63454660B040F371269C /* SwiftMessages.debug.xcconfig */; + buildSettings = { + "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/SwiftMessages/SwiftMessages-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/SwiftMessages/SwiftMessages-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/SwiftMessages/SwiftMessages.modulemap"; + PRODUCT_MODULE_NAME = SwiftMessages; + PRODUCT_NAME = SwiftMessages; + 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; + }; + BFD9E4B58F44191AF73A3434AAF6831F /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = D2EC6A9540A860E7EDBF13CF404CB4B8 /* Kingfisher.release.xcconfig */; + buildSettings = { + CLANG_ENABLE_OBJC_WEAK = NO; + "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 = 12.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; + }; + D85716C223E93E25ED025609282EB63F /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 650F2EE29CA64B40F86449904824F62E /* UIColor_Hex_Swift.debug.xcconfig */; + buildSettings = { + "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/UIColor_Hex_Swift/UIColor_Hex_Swift-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.modulemap"; + PRODUCT_MODULE_NAME = UIColor_Hex_Swift; + PRODUCT_NAME = UIColor_Hex_Swift; + 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; + }; + EA9F71EDF3D48AF084CE2EE37C3D16D2 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B3D3E8CEA6EB6B2960A1895BBFF73681 /* IGListDiffKit.debug.xcconfig */; + buildSettings = { + "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/IGListDiffKit/IGListDiffKit-prefix.pch"; + INFOPLIST_FILE = "Target Support Files/IGListDiffKit/IGListDiffKit-Info.plist"; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MODULEMAP_FILE = "Target Support Files/IGListDiffKit/IGListDiffKit.modulemap"; + PRODUCT_MODULE_NAME = IGListDiffKit; + PRODUCT_NAME = IGListDiffKit; + 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; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 124A39A4C3D1564DB02296737FECE89C /* Build configuration list for PBXNativeTarget "IGListDiffKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + EA9F71EDF3D48AF084CE2EE37C3D16D2 /* Debug */, + A2CF0BE9EF01BC9B2AC0AF9538E6064D /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 15F7B5F2389DC795EC831863F9DF95D5 /* Build configuration list for PBXNativeTarget "IGListKit" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 326089C1C11FB65356346DE055F5D586 /* Debug */, + 1B1FC92834E84FFF3F5F89754187D9C6 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 3EBF158ED6C977460E2094CBA2B9AFC1 /* Build configuration list for PBXNativeTarget "SwiftMessages-SwiftMessages" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AE25B7B6AE679D7B14C8B4DA0C761220 /* Debug */, + 4AD2E4574C8421AB16CD302FDE0B41EA /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4821239608C13582E20E6DA73FD5F1F9 /* Build configuration list for PBXProject "Pods" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2B9E26EAE2CD392AD762421F663075A1 /* Debug */, + 63FAF33E1C55B71A5F5A8B3CC8749F99 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 4FD7C2B04E1FC5365718930ADC799D5C /* Build configuration list for PBXNativeTarget "SwiftMessages" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + B2C89343EB7C202BAEE5FBED397500CA /* Debug */, + 80CFF3701C30078A716619ED51F45BC5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 61FD5F1D788300894E920658B01F90CA /* Build configuration list for PBXNativeTarget "Pods-Bulletin" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 032EDA8C88F2E73BC059D28AB5720416 /* Debug */, + 4A89754B2096AFF6713D0E1F5B788A0A /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 69ACD8654734266A348C6FF68E734010 /* Build configuration list for PBXNativeTarget "Kingfisher" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 980A58862D8A5086E2825CF9017AC8DD /* Debug */, + BFD9E4B58F44191AF73A3434AAF6831F /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E22B609ECAEFA85B75A98369FF2A6B24 /* Build configuration list for PBXNativeTarget "UIColor_Hex_Swift" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D85716C223E93E25ED025609282EB63F /* Debug */, + 5748CEB3A27DF5D76974F97B6FDAFEB8 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = BFDFE7DC352907FC980B868725387E98 /* Project object */; +} diff --git a/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/IGListDiffKit.xcscheme b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/IGListDiffKit.xcscheme new file mode 100644 index 0000000..8434c2e --- /dev/null +++ b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/IGListDiffKit.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/IGListKit.xcscheme b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/IGListKit.xcscheme new file mode 100644 index 0000000..6bae6f9 --- /dev/null +++ b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/IGListKit.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/Kingfisher.xcscheme b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/Kingfisher.xcscheme new file mode 100644 index 0000000..71b3f40 --- /dev/null +++ b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/Kingfisher.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/Pods-Bulletin.xcscheme b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/Pods-Bulletin.xcscheme new file mode 100644 index 0000000..9ca218a --- /dev/null +++ b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/Pods-Bulletin.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/SwiftMessages-SwiftMessages.xcscheme b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/SwiftMessages-SwiftMessages.xcscheme new file mode 100644 index 0000000..08e75a2 --- /dev/null +++ b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/SwiftMessages-SwiftMessages.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/SwiftMessages.xcscheme b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/SwiftMessages.xcscheme new file mode 100644 index 0000000..77630c6 --- /dev/null +++ b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/SwiftMessages.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/UIColor_Hex_Swift.xcscheme b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/UIColor_Hex_Swift.xcscheme new file mode 100644 index 0000000..c3da4ac --- /dev/null +++ b/Demo/Pods/Pods.xcodeproj/xcuserdata/daxeshnagar.xcuserdatad/xcschemes/UIColor_Hex_Swift.xcscheme @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/SwiftMessages/LICENSE.md b/Demo/Pods/SwiftMessages/LICENSE.md new file mode 100644 index 0000000..bced733 --- /dev/null +++ b/Demo/Pods/SwiftMessages/LICENSE.md @@ -0,0 +1,8 @@ +Copyright (c) 2016 SwiftKick Mobile LLC + + +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. \ No newline at end of file diff --git a/Demo/Pods/SwiftMessages/README.md b/Demo/Pods/SwiftMessages/README.md new file mode 100644 index 0000000..97e8374 --- /dev/null +++ b/Demo/Pods/SwiftMessages/README.md @@ -0,0 +1,387 @@ +# SwiftMessages + +[![Twitter: @TimothyMoose](https://img.shields.io/badge/contact-@TimothyMoose-blue.svg?style=flat)](https://twitter.com/TimothyMoose) +[![Version](https://img.shields.io/cocoapods/v/SwiftMessages.svg?style=flat)](http://cocoadocs.org/docsets/SwiftMessages) +[![License](https://img.shields.io/cocoapods/l/SwiftMessages.svg?style=flat)](http://cocoadocs.org/docsets/SwiftMessages) +[![Platform](https://img.shields.io/cocoapods/p/SwiftMessages.svg?style=flat)](http://cocoadocs.org/docsets/SwiftMessages) +[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) + +

+ +

+ +## Overview + +SwiftMessages is a very flexible view and view controller presentation library for iOS. + +Message views and view controllers can be displayed at the top, bottom, or center of the screen, or behind navigation bars and tab bars. There are interactive dismiss gestures including a fun, physics-based one. Multiple background dimming modes. And a lot more! + +In addition to the numerous configuration options, SwiftMessages provides several good-looking layouts and themes. But SwiftMessages is also designer-friendly, which means you can fully and easily customize the view: + +* Copy one of the included nib files into your project and change it. +* Subclass `MessageView` and add elements, etc. +* Or just supply an arbitrary instance of `UIView`. + +Try exploring [the demo app via appetize.io](http://goo.gl/KXw4nD) to get a feel for the extensive configurability of SwiftMessages. + +

+ +

+ +

+ +

+ +## View Controllers + +SwiftMessages can present view controllers using the `SwiftMessagesSegue` custom modal segue! + +

+ +

+ +[`SwiftMessagesSegue`](./SwiftMessages/SwiftMessagesSegue.swift) is a subclass of `UIStoryboardSegue` that integrates directly into Interface Builder as a custom modal segue, enabling view controllers to take advantage of SwiftMessages layouts, animations and more. `SwiftMessagesSegue` works with any UIKIt project — storyboards are not required. Refer to the View Controllers readme below for more information. + +#### [View Controllers Readme](./ViewControllers.md) + +And check out our blog post [Elegant Custom UIViewController Transitioning](http://www.swiftkickmobile.com/elegant-custom-uiviewcontroller-transitioning-uiviewcontrollertransitioningdelegate-uiviewcontrolleranimatedtransitioning/) to learn a great technique you can use to build your own custom segues that utilize `UIViewControllerTransitioningDelegate` and `UIViewControllerAnimatedTransitioning`. + +## Installation + +### Swift Package Manager + +Go to `File | Swift Packages | Add Package Dependency...` in Xcode and search for "SwiftMessages". If multiple results are found, select the one owned by SwiftKick Mobile. + +### CocoaPods + +Add the following line to your Podfile: + +````ruby +pod 'SwiftMessages' +```` + +### Carthage + +Add the following line to your Cartfile: + +````ruby +github "SwiftKickMobile/SwiftMessages" +```` + +If the Carthage build fails, [try using the script](https://github.com/Carthage/Carthage/issues/3019). + +### Manual + +1. Put SwiftMessages repo somewhere in your project directory. +1. In Xcode, add `SwiftMessages.xcodeproj` to your project. +1. On your app's target, add the SwiftMessages framework: + 1. as an embedded binary on the General tab. + 1. as a target dependency on the Build Phases tab. + +## Usage + +### Basics + +````swift +SwiftMessages.show(view: myView) +```` + +Although you can show any instance of `UIView`, SwiftMessages provides a `MessageView` class +and assortment of nib-based layouts that should handle most cases: + +````swift +// Instantiate a message view from the provided card view layout. SwiftMessages searches for nib +// files in the main bundle first, so you can easily copy them into your project and make changes. +let view = MessageView.viewFromNib(layout: .cardView) + +// Theme message elements with the warning style. +view.configureTheme(.warning) + +// Add a drop shadow. +view.configureDropShadow() + +// Set message title, body, and icon. Here, we're overriding the default warning +// image with an emoji character. +let iconText = ["🤔", "😳", "🙄", "😶"].randomElement()! +view.configureContent(title: "Warning", body: "Consider yourself warned.", iconText: iconText) + +// Increase the external margin around the card. In general, the effect of this setting +// depends on how the given layout is constrained to the layout margins. +view.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + +// Reduce the corner radius (applicable to layouts featuring rounded corners). +(view.backgroundView as? CornerRoundingView)?.cornerRadius = 10 + +// Show the message. +SwiftMessages.show(view: view) +```` + +You may wish to use the view provider variant `show(viewProvider:)` to ensure that +your UIKit code is executed on the main queue: + +````swift +SwiftMessages.show { + let view = MessageView.viewFromNib(layout: .cardView) + // ... configure the view + return view +} +```` + +The `SwiftMessages.Config` struct provides numerous configuration options that can be passed to `show()`: + +````swift +var config = SwiftMessages.Config() + +// Slide up from the bottom. +config.presentationStyle = .bottom + +// Display in a window at the specified window level. +config.presentationContext = .window(windowLevel: .statusBar) + +Note that, as of iOS 13, it is no longer possible to cover the status bar +regardless of the window level. A workaround is to hide the status bar instead. +config.prefersStatusBarHidden = true + +// Disable the default auto-hiding behavior. +config.duration = .forever + +// Dim the background like a popover view. Hide when the background is tapped. +config.dimMode = .gray(interactive: true) + +// Disable the interactive pan-to-hide gesture. +config.interactiveHide = false + +// Specify a status bar style to if the message is displayed directly under the status bar. +config.preferredStatusBarStyle = .lightContent + +// Specify one or more event listeners to respond to show and hide events. +config.eventListeners.append() { event in + if case .didHide = event { + print("yep id=\(String(describing: event.id)") + } +} + +SwiftMessages.show(config: config, view: view) +```` + +Specify default configuration options: + +````swift +SwiftMessages.defaultConfig.presentationStyle = .bottom + +// Show message with default config. +SwiftMessages.show(view: view) + +// Customize config using the default as a base. +var config = SwiftMessages.defaultConfig +config.duration = .forever +SwiftMessages.show(config: config, view: view) +```` + +### Accessibility + +SwiftMessages provides excellent VoiceOver support out-of-the-box. + +* The title and body of the message are combined into a single announcement when the message is shown. The `MessageView.accessibilityPrefix` property can be set to prepend additional clarifying text to the announcement. + + Sometimes, a message may contain important visual cues that aren't captured in the title or body. For example, a message may rely on a yellow background to convey a warning rather than having the word "warning" in the title or body. In this case, it might be helpful to set `MessageView.accessibilityPrefix = "warning"`. + +* If the message is shown with a dim view using `config.dimMode`, elements below the dim view are not focusable until the message is hidden. If `config.dimMode.interactive == true`, the dim view itself will be focusable and read out "dismiss" followed by "button". The former text can be customized by setting the `config.dimModeAccessibilityLabel` property. + +See the `AccessibleMessage` protocol for implementing proper accessibility support in custom views. + +### Keyboard Avoidance + +The `KeyboardTrackingView` class can be used to cause the message view to avoid the keyboard by sliding up when the keyboard gets too close. + +````swift +var config = SwiftMessages.defaultConfig +config.keyboardTrackingView = KeyboardTrackingView() +```` + +You can incorporate `KeyboardTrackingView` into your app even when you're not using SwiftMessages. Install into your view hierarchy by pinning `KeyboardTrackingView` to the bottom, leading, and trailing edges of the screen. Then pin the bottom of your content that should avoid the keyboard to the top `KeyboardTrackingView`. Use an equality constraint to strictly track the keyboard or an inequality constraint to only move when the keyboard gets too close. `KeyboardTrackingView` works by observing keyboard notifications and adjusting its height to maintain its top edge above the keyboard, thereby pushing your content up. See the comments in `KeyboardTrackingView` for configuration options. + +### Message Queueing + +You can call `SwiftMessages.show()` as many times as you like. SwiftMessages maintains a queue and shows messages one at a time. If your view implements the `Identifiable` protocol (like `MessageView`), duplicate messages will be removed automatically. The pause between messages can be adjusted: + +````swift +SwiftMessages.pauseBetweenMessages = 1.0 +```` + +There are a few ways to hide messages programatically: + +````swift +// Hide the current message. +SwiftMessages.hide() + +// Or hide the current message and clear the queue. +SwiftMessages.hideAll() + +// Or for a view that implements `Identifiable`: +SwiftMessages.hide(id: someId) + +// Or hide when the number of calls to show() and hideCounted(id:) for a +// given message ID are equal. This can be useful for messages that may be +// shown from multiple code paths to ensure that all paths are ready to hide. +SwiftMessages.hideCounted(id: someId) +```` + +Multiple instances of `SwiftMessages` can be used to show more than one message at a time. Note that the static `SwiftMessages.show()` and other static APIs on `SwiftMessage` are just convenience wrappers around the shared instance `SwiftMessages.sharedInstance`). Instances must be retained, thus it should be a property of something (e.g. your view controller): + +````swift +class SomeViewController: UIViewController { + let otherMessages = SwiftMessages() + + func someMethod() { + SwiftMessages.show(...) + otherMessages.show(...) + } +} +```` + +### Retrieving Messages + +There are several APIs available for retrieving messages that are currently being shown, hidden, or queued to be shown. These APIs are useful for updating messages +when some event happens without needing to keep temporary references around. +See also `eventListeners`. + +````swift +// Get a message view with the given ID if it is currently +// being shown or hidden. +if let view = SwiftMessages.current(id: "some id") { ... } + +// Get a message view with the given ID if is it currently +// queued to be shown. +if let view = SwiftMessages.queued(id: "some id") { ... } + +// Get a message view with the given ID if it is currently being +// shown, hidden or in the queue to be shown. +if let view = SwiftMessages.currentOrQueued(id: "some id") { ... } +```` + +### Customization + +SwiftMessages can display any `UIView`. However, there are varying degrees of customization that can be done to the bundled views. + +#### Nib Files + +All of the message designs bundled with SwiftMessages have associated nib files. You are encouraged to copy any of these nib files into your project and modify them to suit your needs. SwiftMessages will load your copy of the file instead of the original. Nib files may be copied in Xcode using drag-and-drop. + +To facilitate the use of nib-based layouts, `MessageView` provides some type-safe convenience methods for loading the bundled nibs: + +````swift +let view = MessageView.viewFromNib(layout: .cardView) +```` + +In addition, the `SwiftMessages` class provides some generic loading methods: + +````swift +// Instantiate MessageView from a named nib. +let view: MessageView = try! SwiftMessages.viewFromNib(named: "MyCustomNib") + +// Instantiate MyCustomView from a nib named MyCustomView.nib. +let view: MyCustomView = try! SwiftMessages.viewFromNib() +```` + +#### MessageView Class + + +[`MessageView`](./SwiftMessages/MessageView.swift) is a light-weight view that all of the bundled designs use. It primarily consists of the following optional `@IBOutlet` properties: + +Element | Declaration | Description +--------|-----------|----- +Title | `titleLabel: UILabel?` | The message title. +Message body | `bodyLabel: UILabel?` | The body of the message. +Image icon | `iconImageView: UIImageView?` | An image-based icon. +Text icon | `iconLabel: UILabel?` | A text-based (emoji) alternative to the image icon. +Button | `button: UIButton?` | An action button. + +The SwiftMessages nib file use `MessageView` as the top-level view with content connected to these outlets. The layouts are done using stack views, which means that you can remove an element by simply hiding it: + +````swift +view.titleLabel.isHidden = true +```` + +A common mistake is attempting to remove an element by setting the corresponding outlet to `nil`. This does not work because it does not remove the element from the view hierarchy. + +#### Configuration + +`MessageView` provides numerous methods that follow the `configure*` naming convention: + +````swift +view.configureTheme(.warning) +view.configureContent(title: "Warning", body: "Consider yourself warned.", iconText: "🤔") +```` + +All of these methods are shortcuts for quickly configuring the underlying view properties. SwiftMessages strives to avoid doing any internal magic in these methods, so you do not need to call them. You can configure the view properties directly or combine the two approaches. + +#### Interaction + +`MessageView` provides an optional block-based tap handler for the button and another for the view itself: + +````swift +// Hide when button tapped +messageView.buttonTapHandler = { _ in SwiftMessages.hide() } + +// Hide when message view tapped +messageView.tapHandler = { _ in SwiftMessages.hide() } +```` + +#### Extending + +The suggested method for starting with `MessageView` as a base and __adding new elements__, such as additional buttons, is as follows: + + 1. Copy one of the bundled nib files into your project or create a new one from scratch. + 1. Add new elements to the nib file. + 1. Sublcass `MessageView` and create outlets for the new elements. + 1. Assign the top-level view in the nib file to the subclass. + 1. Connect outlets between the nib file and the subclass. + 1. (recommended) override the implementation of `Identifiable` as needed to incorporate new elements into the message's identity. + 1. (recommended) override the implementation of `AccessibleMessage` as needed to incorporate new elements into Voice Over. + 1. Use one of the nib-loading methods above to load the view. + +#### BaseView Class + +[`BaseView`](./SwiftMessages/BaseView.swift) is the superclass of `MessageView` and provides numerous options that aren't specific to the "title + body + icon + button" design of `MessageView`. Custom views that are significantly different from `MessageView`, such as a progress indicator, should subclass `BaseView`. + +#### CornerRoundingView Class + +[`CornerRoundingView`](./SwiftMessages/CornerRoundingView.swift) is a custom view that messages can use for rounding all or a subset of corners with squircles (the smoother method of rounding corners that you see on app icons). The nib files that feature rounded corners have `backgroundView` assigned to a `CornerRoundingView`. It provides a `roundsLeadingCorners` option to dynamically round only the leading corners of the view when presented from top or bottom (a feature used for the tab-style layouts). + +#### Animator Protocol + +[`Animator`](./SwiftMessages/Animator.swift) is the protocol that SwiftMessages uses for presentation and dismissal animations. Custom animations can be done through the `SwiftMessages.PresentationStyle.custom(animator:)`. Some related components: +* [`TopBottomAnimation`](./SwiftMessages/TopBottomAnimation.swift) is a sliding implementation of `Animator` used internally by `.top` and `.bottom` presentation styles. It provides some customization options. +* [`PhysicsAnimation`](./SwiftMessages/PhysicsAnimation.swift) is a scaling + opacity implementation of `Animator` used internally by the `.center` presentation style. It provides a fun physics-based dismissal gesture and provides customization options including `.top` and `.bottom` placement. +* [`PhysicsPanHandler`](./SwiftMessages/PhysicsPanHandler.swift) provides the physics-based dismissal gesture for `PhysicsAnimation` and can be incorporated into other `Animator` implementations. + +High-quality PRs for cool `Animator` implementations are welcome! + +#### MarginAdjustable Protocol + +[`MarginAdjustable`](./SwiftMessages/MarginAdjustable.swift) is a protocol adopted by `BaseView`. If the view being presented adopts `MarginAdjustable`, SwiftMessages takes ownership of the view's layout margins to ensure ideal spacing across the full range of presentation contexts. + +#### BackgroundViewable Protocol + +[`BackgroundViewable`](./SwiftMessages/BackgroundViewable.swift) is a protocol adopted by `BaseView` and requires that a view provide a single `backgroundView` property. `BaseView` initializes `backgroundView = self`, which you can freely re-assign to any subview. + +If the view being presented adopts `BackgroundViewable`, SwiftMessages will ignore touches outside of `backgroundView`. This is important because message views always span the full width of the device. Card and tab-style layouts appear inset from the edges of the device because the message view's background is transparent and `backgroundView` is assigned to a subview constrained to the layout margins. In these layouts, touches in the transparent margins should be ignored. + +#### Identifiable Protocol + +[`Identifiable`](./SwiftMessages/Identifiable.swift) is a protocol adopted by `MessageView` and requires that a view provide a single `id` property, which SwiftMessages uses for message deduplication. + +`MessageView` computes the `id` based on the message content, but `id` can also be set explicitly as needed. + +#### AccessibleMessage Protocol + +[`AccessibleMessage`](./SwiftMessages/AccessibleMessage.swift) is a protocol adopted by `MessageView`. If the view being presented adopts `AccessibleMessage`, SwiftMessages provides improved Voice Over. + + +## About SwiftKick Mobile +We build high quality apps! [Get in touch](http://www.swiftkickmobile.com) if you need help with a project. + +## License + +SwiftMessages is distributed under the MIT license. [See LICENSE](./LICENSE.md) for details. diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/AccessibleMessage.swift b/Demo/Pods/SwiftMessages/SwiftMessages/AccessibleMessage.swift new file mode 100644 index 0000000..d540ece --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/AccessibleMessage.swift @@ -0,0 +1,20 @@ +// +// AccessibleMessage.swift +// SwiftMessages +// +// Created by Timothy Moose on 3/11/17. +// Copyright © 2017 SwiftKick Mobile. All rights reserved. +// + +import Foundation + +/** + Message views that `AccessibleMessage`, as `MessageView` does will + have proper accessibility behavior when displaying messages. + `MessageView` implements this protocol. + */ +public protocol AccessibleMessage { + var accessibilityMessage: String? { get } + var accessibilityElement: NSObject? { get } + var additionalAccessibilityElements: [NSObject]? { get } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Animator.swift b/Demo/Pods/SwiftMessages/SwiftMessages/Animator.swift new file mode 100644 index 0000000..8207406 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Animator.swift @@ -0,0 +1,78 @@ +// +// Animator.swift +// SwiftMessages +// +// Created by Timothy Moose on 6/4/17. +// Copyright © 2017 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +public typealias AnimationCompletion = (_ completed: Bool) -> Void + +public protocol AnimationDelegate: AnyObject { + func hide(animator: Animator) + func panStarted(animator: Animator) + func panEnded(animator: Animator) +} + +/** + An option set representing the known types of safe area conflicts + that could require margin adjustments on the message view in order to + get the layouts to look right. + */ +public struct SafeZoneConflicts: OptionSet { + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// Message view behind status bar + public static let statusBar = SafeZoneConflicts(rawValue: 1 << 0) + + /// Message view behind the sensor notch on iPhone X + public static let sensorNotch = SafeZoneConflicts(rawValue: 1 << 1) + + /// Message view behind home indicator on iPhone X + public static let homeIndicator = SafeZoneConflicts(rawValue: 1 << 2) + + /// Message view is over the status bar on an iPhone 8 or lower. This is a special + /// case because we logically expect the top safe area to be zero, but it is reported as 20 + /// (which seems like an iOS bug). We use the `overStatusBar` to indicate this special case. + public static let overStatusBar = SafeZoneConflicts(rawValue: 1 << 3) +} + +public class AnimationContext { + + public let messageView: UIView + public let containerView: UIView + public let safeZoneConflicts: SafeZoneConflicts + public let interactiveHide: Bool + + init(messageView: UIView, containerView: UIView, safeZoneConflicts: SafeZoneConflicts, interactiveHide: Bool) { + self.messageView = messageView + self.containerView = containerView + self.safeZoneConflicts = safeZoneConflicts + self.interactiveHide = interactiveHide + } +} + +public protocol Animator: AnyObject { + + /// Adopting classes should declare as `weak`. + var delegate: AnimationDelegate? { get set } + + func show(context: AnimationContext, completion: @escaping AnimationCompletion) + + func hide(context: AnimationContext, completion: @escaping AnimationCompletion) + + /// The show animation duration. If the animation duration is unknown, such as if using `UIDynamicAnimator`, + /// then provide an estimate. This value is used by `SwiftMessagesSegue`. + var showDuration: TimeInterval { get } + + /// The hide animation duration. If the animation duration is unknown, such as if using `UIDynamicAnimator`, + /// then provide an estimate. This value is used by `SwiftMessagesSegue`. + var hideDuration: TimeInterval { get } +} + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/BackgroundViewable.swift b/Demo/Pods/SwiftMessages/SwiftMessages/BackgroundViewable.swift new file mode 100644 index 0000000..1c47036 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/BackgroundViewable.swift @@ -0,0 +1,24 @@ +// +// BackgroundViewable.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/15/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +/** + Message views that implement the `BackgroundViewable` protocol will have the + pan-to-hide gesture recognizer installed in the `backgroundView`. Message views + always span the full width of the containing view. Typically, the `backgroundView` + property defines the message view's visible region, allowing for card-style views + where the message view background is transparent and the background view is inset + from by some amount. See CardView.nib, for example. + + This protocol is optional. Message views that don't implement `BackgroundViewable` + will have the pan-to-hide gesture installed in the message view itself. + */ +public protocol BackgroundViewable { + var backgroundView: UIView! { get } +} \ No newline at end of file diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/BaseView.swift b/Demo/Pods/SwiftMessages/SwiftMessages/BaseView.swift new file mode 100644 index 0000000..9480b87 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/BaseView.swift @@ -0,0 +1,375 @@ + +// +// BaseView.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/17/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +/** + The `BaseView` class is a reusable message view base class that implements some + of the optional SwiftMessages protocols and provides some convenience functions + and a configurable tap handler. Message views do not need to inherit from `BaseVew`. + */ +open class BaseView: UIView, BackgroundViewable, MarginAdjustable { + + /* + MARK: - IB outlets + */ + + /** + Fulfills the `BackgroundViewable` protocol and is the target for + the optional `tapHandler` block. Defaults to `self`. + */ + @IBOutlet open weak var backgroundView: UIView! { + didSet { + if let old = oldValue { + old.removeGestureRecognizer(tapRecognizer) + } + installTapRecognizer() + updateBackgroundHeightConstraint() + } + } + + // The `contentView` property was removed because it no longer had any functionality + // in the framework. This is a minor backwards incompatible change. If you've copied + // one of the included nib files from a previous release, you may get a key-value + // coding runtime error related to contentView, in which case you can subclass the + // view and add a `contentView` property or you can remove the outlet connection in + // Interface Builder. + // @IBOutlet public var contentView: UIView! + + /* + MARK: - Initialization + */ + + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + backgroundView = self + layoutMargins = UIEdgeInsets.zero + } + + public override init(frame: CGRect) { + super.init(frame: frame) + backgroundView = self + layoutMargins = UIEdgeInsets.zero + } + + /* + MARK: - Installing background and content + */ + + /** + A convenience function for installing a content view as a subview of `backgroundView` + and pinning the edges to `backgroundView` with the specified `insets`. + + - Parameter contentView: The view to be installed into the background view + and assigned to the `contentView` property. + - Parameter insets: The amount to inset the content view from the background view. + Default is zero inset. + */ + open func installContentView(_ contentView: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) { + contentView.translatesAutoresizingMaskIntoConstraints = false + backgroundView.addSubview(contentView) + contentView.topAnchor.constraint(equalTo: backgroundView.topAnchor, constant: insets.top).isActive = true + contentView.bottomAnchor.constraint(equalTo: backgroundView.bottomAnchor, constant: -insets.bottom).isActive = true + contentView.leftAnchor.constraint(equalTo: backgroundView.leftAnchor, constant: insets.left).isActive = true + contentView.rightAnchor.constraint(equalTo: backgroundView.rightAnchor, constant: -insets.right).isActive = true + contentView.heightAnchor.constraint(equalToConstant: 350).with(priority: UILayoutPriority(rawValue: 200)).isActive = true + } + + /** + A convenience function for installing a background view and pinning to the layout margins. + This is useful for creating programatic layouts where the background view needs to be + inset from the message view's edges (like a card-style layout). + + - Parameter backgroundView: The view to be installed as a subview and + assigned to the `backgroundView` property. + - Parameter insets: The amount to inset the content view from the margins. Default is zero inset. + */ + open func installBackgroundView(_ backgroundView: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) { + backgroundView.translatesAutoresizingMaskIntoConstraints = false + if backgroundView != self { + backgroundView.removeFromSuperview() + } + addSubview(backgroundView) + self.backgroundView = backgroundView + backgroundView.centerXAnchor.constraint(equalTo: centerXAnchor).with(priority: UILayoutPriority(rawValue: 950)).isActive = true + backgroundView.topAnchor.constraint(equalTo: layoutMarginsGuide.topAnchor, constant: insets.top).with(priority: UILayoutPriority(rawValue: 900)).isActive = true + backgroundView.bottomAnchor.constraint(equalTo: layoutMarginsGuide.bottomAnchor, constant: -insets.bottom).with(priority: UILayoutPriority(rawValue: 900)).isActive = true + backgroundView.heightAnchor.constraint(equalToConstant: 350).with(priority: UILayoutPriority(rawValue: 200)).isActive = true + layoutConstraints = [ + backgroundView.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)), + backgroundView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)), + ] + regularWidthLayoutConstraints = [ + backgroundView.leftAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)), + backgroundView.rightAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)), + backgroundView.widthAnchor.constraint(lessThanOrEqualToConstant: 500).with(priority: UILayoutPriority(rawValue: 950)), + backgroundView.widthAnchor.constraint(equalToConstant: 500).with(priority: UILayoutPriority(rawValue: 200)), + ] + installTapRecognizer() + } + + /** + A convenience function for installing a background view and pinning to the horizontal + layout margins and to the vertical edges. This is useful for creating programatic layouts where + the background view needs to be inset from the message view's horizontal edges (like a tab-style layout). + + - Parameter backgroundView: The view to be installed as a subview and + assigned to the `backgroundView` property. + - Parameter insets: The amount to inset the content view from the horizontal margins and vertical edges. + Default is zero inset. + */ + open func installBackgroundVerticalView(_ backgroundView: UIView, insets: UIEdgeInsets = UIEdgeInsets.zero) { + backgroundView.translatesAutoresizingMaskIntoConstraints = false + if backgroundView != self { + backgroundView.removeFromSuperview() + } + addSubview(backgroundView) + self.backgroundView = backgroundView + backgroundView.centerXAnchor.constraint(equalTo: centerXAnchor).with(priority: UILayoutPriority(rawValue: 950)).isActive = true + backgroundView.topAnchor.constraint(equalTo: topAnchor, constant: insets.top).with(priority: UILayoutPriority(rawValue: 1000)).isActive = true + backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -insets.bottom).with(priority: UILayoutPriority(rawValue: 1000)).isActive = true + backgroundView.heightAnchor.constraint(equalToConstant: 350).with(priority: UILayoutPriority(rawValue: 200)).isActive = true + layoutConstraints = [ + backgroundView.leftAnchor.constraint(equalTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)), + backgroundView.rightAnchor.constraint(equalTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)), + ] + regularWidthLayoutConstraints = [ + backgroundView.leftAnchor.constraint(greaterThanOrEqualTo: layoutMarginsGuide.leftAnchor, constant: insets.left).with(priority: UILayoutPriority(rawValue: 900)), + backgroundView.rightAnchor.constraint(lessThanOrEqualTo: layoutMarginsGuide.rightAnchor, constant: -insets.right).with(priority: UILayoutPriority(rawValue: 900)), + backgroundView.widthAnchor.constraint(lessThanOrEqualToConstant: 500).with(priority: UILayoutPriority(rawValue: 950)), + backgroundView.widthAnchor.constraint(equalToConstant: 500).with(priority: UILayoutPriority(rawValue: 200)), + ] + installTapRecognizer() + } + + /* + MARK: - Tap handler + */ + + /** + An optional tap handler that will be called when the `backgroundView` is tapped. + */ + open var tapHandler: ((_ view: BaseView) -> Void)? { + didSet { + installTapRecognizer() + } + } + + fileprivate lazy var tapRecognizer: UITapGestureRecognizer = { + let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(MessageView.tapped)) + return tapRecognizer + }() + + @objc func tapped() { + tapHandler?(self) + } + + fileprivate func installTapRecognizer() { + guard let backgroundView = backgroundView else { return } + removeGestureRecognizer(tapRecognizer) + backgroundView.removeGestureRecognizer(tapRecognizer) + if tapHandler != nil { + // Only install the tap recognizer if there is a tap handler, + // which makes it slightly nicer if one wants to install + // a custom gesture recognizer. + backgroundView.addGestureRecognizer(tapRecognizer) + } + } + + open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + if backgroundView != self { + let backgroundViewPoint = convert(point, to: backgroundView) + return backgroundView.point(inside: backgroundViewPoint, with: event) + } + return super.point(inside: point, with: event) + } + + /* + MARK: - MarginAdjustable + + These properties fulfill the `MarginAdjustable` protocol and are exposed + as `@IBInspectables` so that they can be adjusted directly in nib files + (see MessageView.nib). + */ + + public var layoutMarginAdditions: UIEdgeInsets { + get { + return UIEdgeInsets(top: topLayoutMarginAddition, left: leftLayoutMarginAddition, bottom: bottomLayoutMarginAddition, right: rightLayoutMarginAddition) + } + set { + topLayoutMarginAddition = newValue.top + leftLayoutMarginAddition = newValue.left + bottomLayoutMarginAddition = newValue.bottom + rightLayoutMarginAddition = newValue.right + } + } + + /// Start margins from the safe area. + open var respectSafeArea: Bool = true + + /// IBInspectable access to layoutMarginAdditions.top + @IBInspectable open var topLayoutMarginAddition: CGFloat = 0 + + /// IBInspectable access to layoutMarginAdditions.left + @IBInspectable open var leftLayoutMarginAddition: CGFloat = 0 + + /// IBInspectable access to layoutMarginAdditions.bottom + @IBInspectable open var bottomLayoutMarginAddition: CGFloat = 0 + + /// IBInspectable access to layoutMarginAdditions.right + @IBInspectable open var rightLayoutMarginAddition: CGFloat = 0 + + @IBInspectable open var collapseLayoutMarginAdditions: Bool = true + + @IBInspectable open var bounceAnimationOffset: CGFloat = 5 + + /* + MARK: - Setting the height + */ + + /** + An optional explicit height for the background view, which can be used if + the message view's intrinsic content size does not produce the desired height. + */ + open var backgroundHeight: CGFloat? { + didSet { + updateBackgroundHeightConstraint() + } + } + + private func updateBackgroundHeightConstraint() { + if let existing = backgroundHeightConstraint { + let view = existing.firstItem as! UIView + view.removeConstraint(existing) + backgroundHeightConstraint = nil + } + if let height = backgroundHeight, let backgroundView = backgroundView { + let constraint = NSLayoutConstraint(item: backgroundView, attribute: .height, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: height) + backgroundView.addConstraint(constraint) + backgroundHeightConstraint = constraint + } + } + + private var backgroundHeightConstraint: NSLayoutConstraint? + + /* + Mark: - Layout + */ + + open override func updateConstraints() { + super.updateConstraints() + let on: [NSLayoutConstraint] + let off: [NSLayoutConstraint] + switch traitCollection.horizontalSizeClass { + case .regular: + on = regularWidthLayoutConstraints + off = layoutConstraints + default: + on = layoutConstraints + off = regularWidthLayoutConstraints + } + on.forEach { $0.isActive = true } + off.forEach { $0.isActive = false } + } + + private var layoutConstraints: [NSLayoutConstraint] = [] + private var regularWidthLayoutConstraints: [NSLayoutConstraint] = [] +} + +/* + MARK: - Theming + */ + +extension BaseView { + + /// A convenience function to configure a default drop shadow effect. + /// The shadow is to this view's layer instead of that of the background view + /// because the background view may be masked. So, when modifying the drop shadow, + /// be sure to set the shadow properties of this view's layer. The shadow path is + /// updated for you automatically. + open func configureDropShadow() { + layer.shadowColor = UIColor.black.cgColor + layer.shadowOffset = CGSize(width: 0.0, height: 2.0) + layer.shadowRadius = 6.0 + layer.shadowOpacity = 0.4 + layer.masksToBounds = false + updateShadowPath() + } + + /// A convenience function to turn off drop shadow + open func configureNoDropShadow() { + layer.shadowOpacity = 0 + } + + private func updateShadowPath() { + backgroundView?.layoutIfNeeded() + let shadowLayer = backgroundView?.layer ?? layer + let shadowRect = layer.convert(shadowLayer.bounds, from: shadowLayer) + let shadowPath: CGPath? + if let backgroundMaskLayer = shadowLayer.mask as? CAShapeLayer, + let backgroundMaskPath = backgroundMaskLayer.path { + var transform = CGAffineTransform(translationX: shadowRect.minX, y: shadowRect.minY) + shadowPath = backgroundMaskPath.copy(using: &transform) + } else { + shadowPath = UIBezierPath(roundedRect: shadowRect, cornerRadius: shadowLayer.cornerRadius).cgPath + } + // This is a workaround needed for smooth rotation animations. + if let foundAnimation = layer.findAnimation(forKeyPath: "bounds.size") { + // Update the layer's `shadowPath` with animation, copying the relevant properties + // from the found animation. + let animation = CABasicAnimation(keyPath: "shadowPath") + animation.duration = foundAnimation.duration + animation.timingFunction = foundAnimation.timingFunction + animation.fromValue = layer.shadowPath + animation.toValue = shadowPath + layer.add(animation, forKey: "shadowPath") + layer.shadowPath = shadowPath + } else { + // Update the layer's `shadowPath` without animation + layer.shadowPath = shadowPath } + } + + open override func layoutSubviews() { + super.layoutSubviews() + updateShadowPath() + } +} + +/* + MARK: - Configuring the width + + This extension provides a few convenience functions for configuring the + background view's width. You are encouraged to write your own such functions + if these don't exactly meet your needs. + */ + +extension BaseView { + + /** + A shortcut for configuring the left and right layout margins. For views that + have `backgroundView` as a subview of `MessageView`, the background view should + be pinned to the left and right `layoutMargins` in order for this configuration to work. + */ + public func configureBackgroundView(sideMargin: CGFloat) { + layoutMargins.left = sideMargin + layoutMargins.right = sideMargin + } + + /** + A shortcut for adding a width constraint to the `backgroundView`. When calling this + method, it is important to ensure that the width constraint doesn't conflict with + other constraints. The CardView.nib and TabView.nib layouts are compatible with + this method. + */ + public func configureBackgroundView(width: CGFloat) { + guard let backgroundView = backgroundView else { return } + let constraint = NSLayoutConstraint(item: backgroundView, attribute: .width, relatedBy: .equal, toItem: nil, attribute: .notAnAttribute, multiplier: 1, constant: width) + backgroundView.addConstraint(constraint) + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/CALayer+Extensions.swift b/Demo/Pods/SwiftMessages/SwiftMessages/CALayer+Extensions.swift new file mode 100644 index 0000000..e0b32f5 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/CALayer+Extensions.swift @@ -0,0 +1,18 @@ +// +// CALayer+Extensions.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/3/18. +// Copyright © 2018 SwiftKick Mobile. All rights reserved. +// + +import QuartzCore + +extension CALayer { + func findAnimation(forKeyPath keyPath: String) -> CABasicAnimation? { + return animationKeys()? + .compactMap({ animation(forKey: $0) as? CABasicAnimation }) + .filter({ $0.keyPath == keyPath }) + .first + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/CornerRoundingView.swift b/Demo/Pods/SwiftMessages/SwiftMessages/CornerRoundingView.swift new file mode 100644 index 0000000..398b731 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/CornerRoundingView.swift @@ -0,0 +1,83 @@ +// +// CornerRoundingView.swift +// SwiftMessages +// +// Created by Timothy Moose on 7/28/18. +// Copyright © 2018 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +/// A background view that messages can use for rounding all or a subset of corners with squircles +/// (the smoother method of rounding corners that you see on app icons). +open class CornerRoundingView: UIView { + + /// Specifies the corner radius to use. + @IBInspectable + open var cornerRadius: CGFloat = 0 { + didSet { + updateMaskPath() + } + } + + /// Set to `true` for layouts where only the leading corners should be + /// rounded. For example, the layout in TabView.xib rounds the bottom corners + /// when displayed from the top and the top corners when displayed from the bottom. + /// When this property is `true`, the `roundedCorners` property will be overwritten + /// by relevant animators (e.g. `TopBottomAnimation`). + @IBInspectable + open var roundsLeadingCorners: Bool = false + + /// Specifies which corners should be rounded. When `roundsLeadingCorners = true`, relevant + /// relevant animators (e.g. `TopBottomAnimation`) will overwrite the value of this property. + open var roundedCorners: UIRectCorner = [.allCorners] { + didSet { + updateMaskPath() + } + } + + override public init(frame: CGRect) { + super.init(frame: frame) + sharedInit() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + sharedInit() + } + + private func sharedInit() { + layer.mask = shapeLayer + } + + private let shapeLayer = CAShapeLayer() + + override open func layoutSubviews() { + super.layoutSubviews() + updateMaskPath() + } + + private func updateMaskPath() { + let newPath = UIBezierPath(roundedRect: layer.bounds, byRoundingCorners: roundedCorners, cornerRadii: cornerRadii).cgPath + // Update the `shapeLayer's` path with animation if we detect our `layer's` size is being animated. + // This is a workaround needed for smooth rotation animations. + if let foundAnimation = layer.findAnimation(forKeyPath: "bounds.size") { + // Update the `shapeLayer's` path with animation, copying the relevant properties + // from the found animation. + let animation = CABasicAnimation(keyPath: "path") + animation.duration = foundAnimation.duration + animation.timingFunction = foundAnimation.timingFunction + animation.fromValue = shapeLayer.path + animation.toValue = newPath + shapeLayer.add(animation, forKey: "path") + shapeLayer.path = newPath + } else { + // Update the `shapeLayer's` path without animation + shapeLayer.path = newPath + } + } + + private var cornerRadii: CGSize { + return CGSize(width: cornerRadius, height: cornerRadius) + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Error.swift b/Demo/Pods/SwiftMessages/SwiftMessages/Error.swift new file mode 100644 index 0000000..9a540ca --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Error.swift @@ -0,0 +1,17 @@ +// +// Error.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/7/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import Foundation + +/** + The `SwiftMessagesError` enum contains the errors thrown by SwiftMessages. + */ +enum SwiftMessagesError: Error { + case cannotLoadViewFromNib(nibName: String) + case noRootViewController +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Identifiable.swift b/Demo/Pods/SwiftMessages/SwiftMessages/Identifiable.swift new file mode 100644 index 0000000..4594b2d --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Identifiable.swift @@ -0,0 +1,22 @@ +// +// Identifiable.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/1/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import Foundation + +/** + Message views that adopt the `Identifiable` protocol will have duplicate messages + removed from the `MessageView` queue. Typically, the `id` would be set to a string + representation of the content of the message view. For example, `MessageView`, combines + the title and message body text. + + This protocol is optional. Message views that don't adopt `Identifiable` will not + have duplicates removed. + */ +public protocol Identifiable { + var id: String { get } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/KeyboardTrackingView.swift b/Demo/Pods/SwiftMessages/SwiftMessages/KeyboardTrackingView.swift new file mode 100644 index 0000000..301d969 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/KeyboardTrackingView.swift @@ -0,0 +1,143 @@ +// +// KeyboardTrackingView.swift +// SwiftMessages +// +// Created by Timothy Moose on 5/20/19. +// Copyright © 2019 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +public protocol KeyboardTrackingViewDelegate: AnyObject { + func keyboardTrackingViewWillChange(change: KeyboardTrackingView.Change, userInfo: [AnyHashable : Any]) + func keyboardTrackingViewDidChange(change: KeyboardTrackingView.Change, userInfo: [AnyHashable : Any]) +} + +/// A view that adjusts it's height based on keyboard hide and show notifications. +/// Pin it to the bottom of the screen using Auto Layout and then pin views that +/// should avoid the keyboard to the top of it. Supply an instance of this class +/// on `SwiftMessages.Config.keyboardTrackingView` or `SwiftMessagesSegue.keyboardTrackingView` +/// for automatic keyboard avoidance for the entire SwiftMessages view or view controller. +open class KeyboardTrackingView: UIView { + + public enum Change { + case show + case hide + case frame + } + + public weak var delegate: KeyboardTrackingViewDelegate? + + /// Typically, when a view controller is not being displayed, keyboard + /// tracking should be paused to avoid responding to keyboard events + /// caused by other view controllers or apps. Setting `isPaused = false` in + /// `viewWillAppear` and `isPaused = true` in `viewWillDisappear` usually works. This class + /// automatically pauses and resumes when the app resigns and becomes active, respectively. + open var isPaused = false { + didSet { + if !isPaused { + isAutomaticallyPaused = false + } + } + } + + /// The margin to maintain between the keyboard and the top of the view. + @IBInspectable open var topMargin: CGFloat = 0 + + /// Subclasses can override this to do something before the change. + open func willChange( + change: KeyboardTrackingView.Change, + userInfo: [AnyHashable : Any] + ) {} + + /// Subclasses can override this to do something after the change. + open func didChange( + change: KeyboardTrackingView.Change, + userInfo: [AnyHashable : Any] + ) {} + + override public init(frame: CGRect) { + super.init(frame: frame) + postInit() + } + + required public init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + } + + open override func awakeFromNib() { + super.awakeFromNib() + postInit() + } + + private var isAutomaticallyPaused = false + private var heightConstraint: NSLayoutConstraint! + + private func postInit() { + translatesAutoresizingMaskIntoConstraints = false + heightConstraint = heightAnchor.constraint(equalToConstant: 0) + heightConstraint.isActive = true + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(_:)), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(pause), name: UIApplication.willResignActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(resume), name: UIApplication.didBecomeActiveNotification, object: nil) + backgroundColor = .clear + } + + @objc private func keyboardWillChangeFrame(_ notification: Notification) { + show(change: .frame, notification) + } + + @objc private func keyboardWillShow(_ notification: Notification) { + show(change: .show, notification) + } + + @objc private func keyboardWillHide(_ notification: Notification) { + guard !(isPaused || isAutomaticallyPaused), + let userInfo = (notification as NSNotification).userInfo else { return } + guard heightConstraint.constant != 0 else { return } + delegate?.keyboardTrackingViewWillChange(change: .hide, userInfo: userInfo) + animateKeyboardChange(change: .hide, height: 0, userInfo: userInfo) + } + + @objc private func pause() { + isAutomaticallyPaused = true + } + + @objc private func resume() { + isAutomaticallyPaused = false + } + + private func show(change: Change, _ notification: Notification) { + guard !(isPaused || isAutomaticallyPaused), + let userInfo = (notification as NSNotification).userInfo, + let value = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } + willChange(change: change, userInfo: userInfo) + delegate?.keyboardTrackingViewWillChange(change: change, userInfo: userInfo) + let keyboardRect = value.cgRectValue + let thisRect = convert(bounds, to: nil) + let newHeight = max(0, thisRect.maxY - keyboardRect.minY) + topMargin + guard heightConstraint.constant != newHeight else { return } + animateKeyboardChange(change: change, height: newHeight, userInfo: userInfo) + } + + private func animateKeyboardChange(change: Change, height: CGFloat, userInfo: [AnyHashable: Any]) { + self.heightConstraint.constant = height + if let durationNumber = userInfo[UIResponder.keyboardAnimationDurationUserInfoKey] as? NSNumber, + let curveNumber = userInfo[UIResponder.keyboardAnimationCurveUserInfoKey] as? NSNumber { + CATransaction.begin() + CATransaction.setCompletionBlock { + self.didChange(change: change, userInfo: userInfo) + self.delegate?.keyboardTrackingViewDidChange(change: change, userInfo: userInfo) + } + UIView.beginAnimations(nil, context: nil) + UIView.setAnimationDuration(durationNumber.doubleValue) + UIView.setAnimationCurve(UIView.AnimationCurve(rawValue: curveNumber.intValue)!) + UIView.setAnimationBeginsFromCurrentState(true) + self.superview?.layoutIfNeeded() + UIView.commitAnimations() + CATransaction.commit() + } + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/MarginAdjustable+Extensions.swift b/Demo/Pods/SwiftMessages/SwiftMessages/MarginAdjustable+Extensions.swift new file mode 100644 index 0000000..b627788 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/MarginAdjustable+Extensions.swift @@ -0,0 +1,60 @@ +// +// MarginAdjustable+Extensions.swift +// SwiftMessages +// +// Created by Timothy Moose on 11/5/17. +// Copyright © 2017 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +extension MarginAdjustable where Self: UIView { + public func defaultMarginAdjustment(context: AnimationContext) -> UIEdgeInsets { + var layoutMargins: UIEdgeInsets = layoutMarginAdditions + var safeAreaInsets: UIEdgeInsets = { + guard respectSafeArea else { return .zero } + if #available(iOS 11, *) { + insetsLayoutMarginsFromSafeArea = false + return self.safeAreaInsets + } else { + #if SWIFTMESSAGES_APP_EXTENSIONS + let application: UIApplication? = nil + #else + let application: UIApplication? = UIApplication.shared + #endif + if !context.safeZoneConflicts.isDisjoint(with: [.statusBar]), + let app = application, + app.statusBarOrientation == .portrait || app.statusBarOrientation == .portraitUpsideDown { + let frameInWindow = convert(bounds, to: window) + let top = max(0, 20 - frameInWindow.minY) + return UIEdgeInsets(top: top, left: 0, bottom: 0, right: 0) + } else { + return .zero + } + } + }() + if !context.safeZoneConflicts.isDisjoint(with: .overStatusBar) { + safeAreaInsets.top = 0 + } + layoutMargins = collapseLayoutMarginAdditions + ? layoutMargins.collapse(toInsets: safeAreaInsets) + : layoutMargins + safeAreaInsets + return layoutMargins + } +} + +extension UIEdgeInsets { + func collapse(toInsets insets: UIEdgeInsets) -> UIEdgeInsets { + let top = self.top.collapse(toInset: insets.top) + let left = self.left.collapse(toInset: insets.left) + let bottom = self.bottom.collapse(toInset: insets.bottom) + let right = self.right.collapse(toInset: insets.right) + return UIEdgeInsets(top: top, left: left, bottom: bottom, right: right) + } +} + +extension CGFloat { + func collapse(toInset inset: CGFloat) -> CGFloat { + return Swift.max(self, inset) + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/MarginAdjustable.swift b/Demo/Pods/SwiftMessages/SwiftMessages/MarginAdjustable.swift new file mode 100644 index 0000000..86fe901 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/MarginAdjustable.swift @@ -0,0 +1,42 @@ +// +// MarginAdjustable.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/5/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +/* + Message views that implement the `MarginAdjustable` protocol will have their + `layoutMargins` adjusted by SwiftMessages to account for the height of the + status bar (when displayed under the status bar) and a small amount of + overshoot in the bounce animation. `MessageView` implements this protocol + by way of its parent class `BaseView`. + + For the effect of this protocol to work, subviews should be pinned to the + message view's margins and the `layoutMargins` property should not be modified. + + This protocol is optional. A message view that doesn't implement `MarginAdjustable` + is responsible for setting is own internal margins appropriately. + */ +public protocol MarginAdjustable { + + /// The amount to add to the safe area insets in calculating + /// the layout margins. + var layoutMarginAdditions: UIEdgeInsets { get } + + /// When `true`, SwiftMessages automatically collapses layout margin additions (topLayoutMarginAddition, etc.) + /// when the default layout margins are greater than zero. This is typically used when a margin addition is only + /// needed when the safe area inset is zero for a given edge. When the safe area inset for a given edge is non-zero, + /// the additional margin is not added. + var collapseLayoutMarginAdditions: Bool { get set } + + + /// Start margins from the safe area. + var respectSafeArea: Bool { get set } + + var bounceAnimationOffset: CGFloat { get set } +} + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/MaskingView.swift b/Demo/Pods/SwiftMessages/SwiftMessages/MaskingView.swift new file mode 100644 index 0000000..d631342 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/MaskingView.swift @@ -0,0 +1,79 @@ +// +// MaskingView.swift +// SwiftMessages +// +// Created by Timothy Moose on 3/11/17. +// Copyright © 2017 SwiftKick Mobile. All rights reserved. +// + +import UIKit + + +class MaskingView: PassthroughView { + + func install(keyboardTrackingView: KeyboardTrackingView) { + self.keyboardTrackingView = keyboardTrackingView + keyboardTrackingView.translatesAutoresizingMaskIntoConstraints = false + addSubview(keyboardTrackingView) + keyboardTrackingView.bottomAnchor.constraint(equalTo: bottomAnchor).isActive = true + keyboardTrackingView.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true + keyboardTrackingView.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true + } + + var accessibleElements: [NSObject] = [] + + weak var backgroundView: UIView? { + didSet { + oldValue?.removeFromSuperview() + if let view = backgroundView { + view.isUserInteractionEnabled = false + view.frame = bounds + view.autoresizingMask = [.flexibleWidth, .flexibleHeight] + addSubview(view) + sendSubviewToBack(view) + } + } + } + + override func accessibilityElementCount() -> Int { + return accessibleElements.count + } + + override func accessibilityElement(at index: Int) -> Any? { + return accessibleElements[index] + } + + override func index(ofAccessibilityElement element: Any) -> Int { + guard let object = element as? NSObject else { return 0 } + return accessibleElements.firstIndex(of: object) ?? 0 + } + + init() { + super.init(frame: CGRect.zero) + clipsToBounds = true + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + clipsToBounds = true + } + + private var keyboardTrackingView: KeyboardTrackingView? + + override func addSubview(_ view: UIView) { + super.addSubview(view) + guard let keyboardTrackingView = keyboardTrackingView, + view != keyboardTrackingView, + view != backgroundView else { return } + let offset: CGFloat + if let adjustable = view as? MarginAdjustable { + offset = -adjustable.bounceAnimationOffset + } else { + offset = 0 + } + keyboardTrackingView.topAnchor.constraint( + greaterThanOrEqualTo: view.bottomAnchor, + constant: offset + ).with(priority: UILayoutPriority(250)).isActive = true + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/MessageView.swift b/Demo/Pods/SwiftMessages/SwiftMessages/MessageView.swift new file mode 100644 index 0000000..c71141a --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/MessageView.swift @@ -0,0 +1,440 @@ +// +// MessageView.swift +// SwiftMessages +// +// Created by Timothy Moose on 7/30/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +/* + */ +open class MessageView: BaseView, Identifiable, AccessibleMessage { + + /* + MARK: - Button tap handler + */ + + /// An optional button tap handler. The `button` is automatically + /// configured to call this tap handler on `.TouchUpInside`. + open var buttonTapHandler: ((_ button: UIButton) -> Void)? + + @objc func buttonTapped(_ button: UIButton) { + buttonTapHandler?(button) + } + + /* + MARK: - Touch handling + */ + + open override func point(inside point: CGPoint, with event: UIEvent?) -> Bool { + // Only accept touches within the background view. Anything outside of the + // background view's bounds should be transparent and does not need to receive + // touches. This helps with tap dismissal when using `DimMode.gray` and `DimMode.color`. + return backgroundView == self + ? super.point(inside: point, with: event) + : backgroundView.point(inside: convert(point, to: backgroundView), with: event) + } + + /* + MARK: - IB outlets + */ + + /// An optional title label. + @IBOutlet open var titleLabel: UILabel? + + /// An optional body text label. + @IBOutlet open var bodyLabel: UILabel? + + /// An optional icon image view. + @IBOutlet open var iconImageView: UIImageView? + + /// An optional icon label (e.g. for emoji character, icon font, etc.). + @IBOutlet open var iconLabel: UILabel? + + /// An optional button. This buttons' `.TouchUpInside` event will automatically + /// invoke the optional `buttonTapHandler`, but its fine to add other target + /// action handlers can be added. + @IBOutlet open var button: UIButton? { + didSet { + if let old = oldValue { + old.removeTarget(self, action: #selector(MessageView.buttonTapped(_:)), for: .touchUpInside) + } + if let button = button { + button.addTarget(self, action: #selector(MessageView.buttonTapped(_:)), for: .touchUpInside) + } + } + } + + /* + MARK: - Identifiable + */ + + open var id: String { + get { + return customId ?? "MessageView:title=\(String(describing: titleLabel?.text)), body=\(String(describing: bodyLabel?.text))" + } + set { + customId = newValue + } + } + + private var customId: String? + + /* + MARK: - AccessibleMessage + */ + + /** + An optional prefix for the `accessibilityMessage` that can + be used to further clarify the message for VoiceOver. For example, + the view's background color or icon might convey that a message is + a warning, in which case one may specify the value "warning". + */ + open var accessibilityPrefix: String? + + open var accessibilityMessage: String? { + #if swift(>=4.1) + let components = [accessibilityPrefix, titleLabel?.text, bodyLabel?.text].compactMap { $0 } + #else + let components = [accessibilityPrefix, titleLabel?.text, bodyLabel?.text].flatMap { $0 } + #endif + guard components.count > 0 else { return nil } + return components.joined(separator: ", ") + } + + public var accessibilityElement: NSObject? { + return backgroundView + } + + open var additionalAccessibilityElements: [NSObject]? { + var elements: [NSObject] = [] + func getAccessibleSubviews(view: UIView) { + for subview in view.subviews { + if subview.isAccessibilityElement { + elements.append(subview) + } else { + // Only doing this for non-accessible `subviews`, which avoids + // including button labels, etc. + getAccessibleSubviews(view: subview) + } + } + } + getAccessibleSubviews(view: self.backgroundView) + return elements + } +} + +/* + MARK: - Creating message views + + This extension provides several convenience functions for instantiating + `MessageView` from the included nib files in a type-safe way. These nib + files can be found in the Resources folder and can be drag-and-dropped + into a project and modified. You may still use these APIs if you've + copied the nib files because SwiftMessages looks for them in the main + bundle first. See `SwiftMessages` for additional nib loading options. + */ + +extension MessageView { + + /** + Specifies one of the nib files included in the Resources folders. + */ + public enum Layout: String { + + /** + The standard message view that stretches across the full width of the + container view. + */ + case messageView = "MessageView" + + /** + A floating card-style view with rounded corners. + */ + case cardView = "CardView" + + /** + Like `CardView` with one end attached to the super view. + */ + case tabView = "TabView" + + /** + A 20pt tall view that can be used to overlay the status bar. + Note that this layout will automatically grow taller if displayed + directly under the status bar (see the `ContentInsetting` protocol). + */ + case statusLine = "StatusLine" + + /** + A floating card-style view with elements centered and arranged vertically. + This view is typically used with `.center` presentation style. + */ + case centeredView = "CenteredView" + } + + /** + Loads the nib file associated with the given `Layout` and returns the first + view found in the nib file with the matching type `T: MessageView`. + + - Parameter layout: The `Layout` option to use. + - Parameter filesOwner: An optional files owner. + + - Returns: An instance of generic view type `T: MessageView`. + */ + public static func viewFromNib(layout: Layout, filesOwner: AnyObject = NSNull.init()) -> T { + return try! SwiftMessages.viewFromNib(named: layout.rawValue) + } + + /** + Loads the nib file associated with the given `Layout` from + the given bundle and returns the first view found in the nib + file with the matching type `T: MessageView`. + + - Parameter layout: The `Layout` option to use. + - Parameter bundle: The name of the bundle containing the nib file. + - Parameter filesOwner: An optional files owner. + + - Returns: An instance of generic view type `T: MessageView`. + */ + public static func viewFromNib(layout: Layout, bundle: Bundle, filesOwner: AnyObject = NSNull.init()) -> T { + return try! SwiftMessages.viewFromNib(named: layout.rawValue, bundle: bundle, filesOwner: filesOwner) + } +} + +/* + MARK: - Layout adjustments + + This extension provides a few convenience functions for adjusting the layout. + */ + +extension MessageView { + /** + Constrains the image view to a specified size. By default, the size of the + image view is determined by its `intrinsicContentSize`. + + - Parameter size: The size to be translated into Auto Layout constraints. + - Parameter contentMode: The optional content mode to apply. + */ + public func configureIcon(withSize size: CGSize, contentMode: UIView.ContentMode? = nil) { + var views: [UIView] = [] + if let iconImageView = iconImageView { views.append(iconImageView) } + if let iconLabel = iconLabel { views.append(iconLabel) } + views.forEach { + let constraints = [$0.heightAnchor.constraint(equalToConstant: size.height), + $0.widthAnchor.constraint(equalToConstant: size.width)] + constraints.forEach { $0.priority = UILayoutPriority(999.0) } + $0.addConstraints(constraints) + if let contentMode = contentMode { + $0.contentMode = contentMode + } + } + } +} + +/* + MARK: - Theming + + This extension provides a few convenience functions for setting styles, + colors and icons. You are encouraged to write your own such functions + if these don't exactly meet your needs. + */ + +extension MessageView { + + /** + A convenience function for setting some pre-defined colors and icons. + + - Parameter theme: The theme type to use. + - Parameter iconStyle: The icon style to use. Defaults to `.Default`. + */ + public func configureTheme(_ theme: Theme, iconStyle: IconStyle = .default) { + let iconImage = iconStyle.image(theme: theme) + let backgroundColor: UIColor + let foregroundColor: UIColor + let defaultBackgroundColor: UIColor + let defaultForegroundColor: UIColor + switch theme { + case .info: + defaultBackgroundColor = UIColor(red: 225.0/255.0, green: 225.0/255.0, blue: 225.0/255.0, alpha: 1.0) + defaultForegroundColor = UIColor.darkText + case .success: + defaultBackgroundColor = UIColor(red: 97.0/255.0, green: 161.0/255.0, blue: 23.0/255.0, alpha: 1.0) + defaultForegroundColor = UIColor.white + case .warning: + defaultBackgroundColor = UIColor(red: 246.0/255.0, green: 197.0/255.0, blue: 44.0/255.0, alpha: 1.0) + defaultForegroundColor = UIColor.white + case .error: + defaultBackgroundColor = UIColor(red: 249.0/255.0, green: 66.0/255.0, blue: 47.0/255.0, alpha: 1.0) + defaultForegroundColor = UIColor.white + } + if #available(iOS 13.0, *) { + switch theme { + case .info: + backgroundColor = UIColor { + switch $0.userInterfaceStyle { + case .dark, .unspecified: return UIColor(red: 125/255.0, green: 125/255.0, blue: 125/255.0, alpha: 1.0) + case .light: fallthrough + @unknown default: + return defaultBackgroundColor + } + } + foregroundColor = .label + case .success: + backgroundColor = UIColor { + switch $0.userInterfaceStyle { + case .dark, .unspecified: return UIColor(red: 55/255.0, green: 122/255.0, blue: 0/255.0, alpha: 1.0) + case .light: fallthrough + @unknown default: + return defaultBackgroundColor + } + } + foregroundColor = .white + case .warning: + backgroundColor = UIColor { + switch $0.userInterfaceStyle { + case .dark, .unspecified: return UIColor(red: 239/255.0, green: 184/255.0, blue: 10/255.0, alpha: 1.0) + case .light: fallthrough + @unknown default: + return defaultBackgroundColor + } + } + foregroundColor = .white + case .error: + backgroundColor = UIColor { + switch $0.userInterfaceStyle { + case .dark, .unspecified: return UIColor(red: 195/255.0, green: 12/255.0, blue: 12/255.0, alpha: 1.0) + case .light: fallthrough + @unknown default: + return defaultBackgroundColor + } + } + foregroundColor = .white + } + } else { + backgroundColor = defaultBackgroundColor + foregroundColor = defaultForegroundColor + } + configureTheme(backgroundColor: backgroundColor, foregroundColor: foregroundColor, iconImage: iconImage) + } + + /** + A convenience function for setting a foreground and background color. + Note that images will only display the foreground color if they're + configured with UIImageRenderingMode.AlwaysTemplate. + + - Parameter backgroundColor: The background color to use. + - Parameter foregroundColor: The foreground color to use. + */ + public func configureTheme(backgroundColor: UIColor, foregroundColor: UIColor, iconImage: UIImage? = nil, iconText: String? = nil) { + iconImageView?.image = iconImage + iconLabel?.text = iconText + iconImageView?.tintColor = foregroundColor + let backgroundView = self.backgroundView ?? self + backgroundView.backgroundColor = backgroundColor + iconLabel?.textColor = foregroundColor + titleLabel?.textColor = foregroundColor + bodyLabel?.textColor = foregroundColor + button?.backgroundColor = foregroundColor + button?.tintColor = backgroundColor + button?.contentEdgeInsets = UIEdgeInsets(top: 7.0, left: 7.0, bottom: 7.0, right: 7.0) + button?.layer.cornerRadius = 5.0 + iconImageView?.isHidden = iconImageView?.image == nil + iconLabel?.isHidden = iconLabel?.text == nil + } +} + +/* + MARK: - Configuring the content + + This extension provides a few convenience functions for configuring the + message content. You are encouraged to write your own such functions + if these don't exactly meet your needs. + + SwiftMessages does not try to be clever by adjusting the layout based on + what content you configure. All message elements are optional and it is + up to you to hide or remove elements you don't need. The easiest way to + remove unwanted elements is to drag-and-drop one of the included nib + files into your project as a starting point and make changes. + */ + +extension MessageView { + + /** + Sets the message body text. + + - Parameter body: The message body text to use. + */ + public func configureContent(body: String) { + bodyLabel?.text = body + } + + /** + Sets the message title and body text. + + - Parameter title: The message title to use. + - Parameter body: The message body text to use. + */ + public func configureContent(title: String, body: String) { + configureContent(body: body) + titleLabel?.text = title + } + + /** + Sets the message title, body text and icon image. Also hides the + `iconLabel`. + + - Parameter title: The message title to use. + - Parameter body: The message body text to use. + - Parameter iconImage: The icon image to use. + */ + public func configureContent(title: String, body: String, iconImage: UIImage) { + configureContent(title: title, body: body) + iconImageView?.image = iconImage + iconImageView?.isHidden = false + iconLabel?.text = nil + iconLabel?.isHidden = true + } + + /** + Sets the message title, body text and icon text (e.g. an emoji). + Also hides the `iconImageView`. + + - Parameter title: The message title to use. + - Parameter body: The message body text to use. + - Parameter iconText: The icon text to use (e.g. an emoji). + */ + public func configureContent(title: String, body: String, iconText: String) { + configureContent(title: title, body: body) + iconLabel?.text = iconText + iconLabel?.isHidden = false + iconImageView?.isHidden = true + iconImageView?.image = nil + } + + /** + Sets all configurable elements. + + - Parameter title: The message title to use. + - Parameter body: The message body text to use. + - Parameter iconImage: The icon image to use. + - Parameter iconText: The icon text to use (e.g. an emoji). + - Parameter buttonImage: The button image to use. + - Parameter buttonTitle: The button title to use. + - Parameter buttonTapHandler: The button tap handler block to use. + */ + public func configureContent(title: String?, body: String?, iconImage: UIImage?, iconText: String?, buttonImage: UIImage?, buttonTitle: String?, buttonTapHandler: ((_ button: UIButton) -> Void)?) { + titleLabel?.text = title + bodyLabel?.text = body + iconImageView?.image = iconImage + iconLabel?.text = iconText + button?.setImage(buttonImage, for: .normal) + button?.setTitle(buttonTitle, for: .normal) + self.buttonTapHandler = buttonTapHandler + iconImageView?.isHidden = iconImageView?.image == nil + iconLabel?.isHidden = iconLabel?.text == nil + } +} + + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/NSBundle+Extensions.swift b/Demo/Pods/SwiftMessages/SwiftMessages/NSBundle+Extensions.swift new file mode 100644 index 0000000..fdfb08b --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/NSBundle+Extensions.swift @@ -0,0 +1,48 @@ +// +// NSBundle+Extensions.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/8/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import Foundation + +private class BundleToken {} + +extension Bundle { + // This is copied method from SPM generated Bundle.module for CocoaPods support + static func sm_frameworkBundle() -> Bundle { + + let candidates = [ + // Bundle should be present here when the package is linked into an App. + Bundle.main.resourceURL, + + // Bundle should be present here when the package is linked into a framework. + Bundle(for: BundleToken.self).resourceURL, + + // For command-line tools. + Bundle.main.bundleURL, + ] + + let bundleNames = [ + // For Swift Package Manager + "SwiftMessages_SwiftMessages", + + // For Carthage + "SwiftMessages", + ] + + for bundleName in bundleNames { + for candidate in candidates { + let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle") + if let bundle = bundlePath.flatMap(Bundle.init(url:)) { + return bundle + } + } + } + + // Return whatever bundle this code is in as a last resort. + return Bundle(for: BundleToken.self) + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/NSLayoutConstraint+Extensions.swift b/Demo/Pods/SwiftMessages/SwiftMessages/NSLayoutConstraint+Extensions.swift new file mode 100644 index 0000000..01f678d --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/NSLayoutConstraint+Extensions.swift @@ -0,0 +1,16 @@ +// +// NSLayoutConstraint+Extensions.swift +// SwiftMessages +// +// Created by Timothy Moose on 5/18/19. +// Copyright © 2019 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +extension NSLayoutConstraint { + func with(priority: UILayoutPriority) -> NSLayoutConstraint { + self.priority = priority + return self + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/PassthroughView.swift b/Demo/Pods/SwiftMessages/SwiftMessages/PassthroughView.swift new file mode 100644 index 0000000..d76da24 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/PassthroughView.swift @@ -0,0 +1,37 @@ +// +// PassthroughView.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/5/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +class PassthroughView: UIControl { + + var tappedHander: (() -> Void)? + + override init(frame: CGRect) { + super.init(frame: frame) + initCommon() + } + + required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder) + initCommon() + } + + private func initCommon() { + addTarget(self, action: #selector(tapped), for: .touchUpInside) + } + + @objc func tapped() { + tappedHander?() + } + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + let view = super.hitTest(point, with: event) + return view == self && tappedHander == nil ? nil : view + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/PassthroughWindow.swift b/Demo/Pods/SwiftMessages/SwiftMessages/PassthroughWindow.swift new file mode 100644 index 0000000..704e481 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/PassthroughWindow.swift @@ -0,0 +1,36 @@ +// +// PassthroughWindow.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/5/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +class PassthroughWindow: UIWindow { + + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { + // iOS has started embedding the SwiftMessages view in private views that block + // interaction with views underneath, essentially making the window behave like a modal. + // To work around this, we'll ignore hit test results on these views. + let view = super.hitTest(point, with: event) + if let view = view, + let hitTestView = hitTestView, + hitTestView.isDescendant(of: view) && hitTestView != view { + return nil + } + return view + } + + init(hitTestView: UIView) { + self.hitTestView = hitTestView + super.init(frame: .zero) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private weak var hitTestView: UIView? +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/PhysicsAnimation.swift b/Demo/Pods/SwiftMessages/SwiftMessages/PhysicsAnimation.swift new file mode 100644 index 0000000..0fb976c --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/PhysicsAnimation.swift @@ -0,0 +1,125 @@ +// +// PhysicsAnimation.swift +// SwiftMessages +// +// Created by Timothy Moose on 6/14/17. +// Copyright © 2017 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +public class PhysicsAnimation: NSObject, Animator { + + public enum Placement { + case top + case center + case bottom + } + + open var placement: Placement = .center + + open var showDuration: TimeInterval = 0.5 + + open var hideDuration: TimeInterval = 0.15 + + public var panHandler = PhysicsPanHandler() + + public weak var delegate: AnimationDelegate? + weak var messageView: UIView? + weak var containerView: UIView? + var context: AnimationContext? + + public override init() {} + + init(delegate: AnimationDelegate) { + self.delegate = delegate + } + + public func show(context: AnimationContext, completion: @escaping AnimationCompletion) { + NotificationCenter.default.addObserver(self, selector: #selector(adjustMargins), name: UIDevice.orientationDidChangeNotification, object: nil) + install(context: context) + showAnimation(context: context, completion: completion) + } + + public func hide(context: AnimationContext, completion: @escaping AnimationCompletion) { + NotificationCenter.default.removeObserver(self) + if panHandler.isOffScreen { + context.messageView.alpha = 0 + panHandler.state?.stop() + } + let view = context.messageView + self.context = context + CATransaction.begin() + CATransaction.setCompletionBlock { + view.alpha = 1 + view.transform = CGAffineTransform.identity + completion(true) + } + UIView.animate(withDuration: hideDuration, delay: 0, options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction], animations: { + view.transform = CGAffineTransform(scaleX: 0.8, y: 0.8) + }, completion: nil) + UIView.animate(withDuration: hideDuration, delay: 0, options: [.beginFromCurrentState, .curveEaseIn, .allowUserInteraction], animations: { + view.alpha = 0 + }, completion: nil) + CATransaction.commit() + } + + func install(context: AnimationContext) { + let view = context.messageView + let container = context.containerView + messageView = view + containerView = container + self.context = context + view.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(view) + switch placement { + case .center: + view.centerYAnchor.constraint(equalTo: container.centerYAnchor).with(priority: UILayoutPriority(200)).isActive = true + case .top: + view.topAnchor.constraint(equalTo: container.topAnchor).with(priority: UILayoutPriority(200)).isActive = true + case .bottom: + view.bottomAnchor.constraint(equalTo: container.bottomAnchor).with(priority: UILayoutPriority(200)).isActive = true + } + NSLayoutConstraint(item: view, attribute: .leading, relatedBy: .equal, toItem: container, attribute: .leading, multiplier: 1, constant: 0).isActive = true + NSLayoutConstraint(item: view, attribute: .trailing, relatedBy: .equal, toItem: container, attribute: .trailing, multiplier: 1, constant: 0).isActive = true + // Important to layout now in order to get the right safe area insets + container.layoutIfNeeded() + adjustMargins() + container.layoutIfNeeded() + installInteractive(context: context) + } + + @objc public func adjustMargins() { + guard let adjustable = messageView as? MarginAdjustable & UIView, + let context = context else { return } + adjustable.preservesSuperviewLayoutMargins = false + if #available(iOS 11, *) { + adjustable.insetsLayoutMarginsFromSafeArea = false + } + adjustable.layoutMargins = adjustable.defaultMarginAdjustment(context: context) + } + + func showAnimation(context: AnimationContext, completion: @escaping AnimationCompletion) { + let view = context.messageView + view.alpha = 0.25 + view.transform = CGAffineTransform(scaleX: 0.6, y: 0.6) + CATransaction.begin() + CATransaction.setCompletionBlock { + completion(true) + } + UIView.animate(withDuration: showDuration, delay: 0, usingSpringWithDamping: 0.6, initialSpringVelocity: 0, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: { + view.transform = CGAffineTransform.identity + }, completion: nil) + UIView.animate(withDuration: 0.3 * showDuration, delay: 0, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: { + view.alpha = 1 + }, completion: nil) + CATransaction.commit() + } + + func installInteractive(context: AnimationContext) { + guard context.interactiveHide else { return } + panHandler.configure(context: context, animator: self) + } +} + + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/PhysicsPanHandler.swift b/Demo/Pods/SwiftMessages/SwiftMessages/PhysicsPanHandler.swift new file mode 100644 index 0000000..da82fc1 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/PhysicsPanHandler.swift @@ -0,0 +1,175 @@ +// +// PhysicsPanHandler.swift +// SwiftMessages +// +// Created by Timothy Moose on 6/25/17. +// Copyright © 2017 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +open class PhysicsPanHandler { + + public var hideDelay: TimeInterval = 0.2 + + public struct MotionSnapshot { + var angle: CGFloat + var time: CFAbsoluteTime + } + + public final class State { + + weak var messageView: UIView? + weak var containerView: UIView? + var dynamicAnimator: UIDynamicAnimator + var itemBehavior: UIDynamicItemBehavior + var attachmentBehavior: UIAttachmentBehavior? { + didSet { + if let oldValue = oldValue { + dynamicAnimator.removeBehavior(oldValue) + } + if let attachmentBehavior = attachmentBehavior { + dynamicAnimator.addBehavior(attachmentBehavior) + addSnapshot() + } + } + } + var snapshots: [MotionSnapshot] = [] + + public init(messageView: UIView, containerView: UIView) { + self.messageView = messageView + self.containerView = containerView + let dynamicAnimator = UIDynamicAnimator(referenceView: containerView) + let itemBehavior = UIDynamicItemBehavior(items: [messageView]) + itemBehavior.allowsRotation = true + dynamicAnimator.addBehavior(itemBehavior) + self.itemBehavior = itemBehavior + self.dynamicAnimator = dynamicAnimator + } + + func update(attachmentAnchorPoint anchorPoint: CGPoint) { + addSnapshot() + attachmentBehavior?.anchorPoint = anchorPoint + } + + func addSnapshot() { + let angle = messageView?.angle ?? snapshots.last?.angle ?? 0 + let time = CFAbsoluteTimeGetCurrent() + snapshots.append(MotionSnapshot(angle: angle, time: time)) + } + + public func stop() { + guard let messageView = messageView else { + dynamicAnimator.removeAllBehaviors() + return + } + let center = messageView.center + let transform = messageView.transform + dynamicAnimator.removeAllBehaviors() + messageView.center = center + messageView.transform = transform + } + + public var angularVelocity: CGFloat { + guard let last = snapshots.last else { return 0 } + for previous in snapshots.reversed() { + // Ignore snapshots where the angle or time hasn't changed to avoid degenerate cases. + if previous.angle != last.angle && previous.time != last.time { + return (last.angle - previous.angle) / CGFloat(last.time - previous.time) + } + } + return 0 + } + } + + weak var animator: Animator? + weak var messageView: UIView? + weak var containerView: UIView? + private(set) public var state: State? + private(set) public var isOffScreen = false + private var restingCenter: CGPoint? + + public init() {} + + public private(set) lazy var pan: UIPanGestureRecognizer = { + let pan = UIPanGestureRecognizer() + pan.addTarget(self, action: #selector(pan(_:))) + return pan + }() + + func configure(context: AnimationContext, animator: Animator) { + if let oldView = (messageView as? BackgroundViewable)?.backgroundView ?? messageView { + oldView.removeGestureRecognizer(pan) + } + messageView = context.messageView + let view = (messageView as? BackgroundViewable)?.backgroundView ?? messageView + view?.addGestureRecognizer(pan) + containerView = context.containerView + self.animator = animator + } + + @objc func pan(_ pan: UIPanGestureRecognizer) { + guard let messageView = messageView, let containerView = containerView, let animator = animator else { return } + let anchorPoint = pan.location(in: containerView) + switch pan.state { + case .began: + animator.delegate?.panStarted(animator: animator) + let state = State(messageView: messageView, containerView: containerView) + self.state = state + let center = messageView.center + restingCenter = center + let offset = UIOffset(horizontal: anchorPoint.x - center.x, vertical: anchorPoint.y - center.y) + let attachmentBehavior = UIAttachmentBehavior(item: messageView, offsetFromCenter: offset, attachedToAnchor: anchorPoint) + state.attachmentBehavior = attachmentBehavior + state.itemBehavior.action = { [weak self, weak messageView, weak containerView] in + guard let self = self, !self.isOffScreen, let messageView = messageView, let containerView = containerView, let animator = self.animator else { return } + let view = (messageView as? BackgroundViewable)?.backgroundView ?? messageView + let frame = containerView.convert(view.bounds, from: view) + if !containerView.bounds.intersects(frame) { + self.isOffScreen = true + DispatchQueue.main.asyncAfter(deadline: .now() + self.hideDelay) { + animator.delegate?.hide(animator: animator) + } + } + } + case .changed: + guard let state = state else { return } + state.update(attachmentAnchorPoint: anchorPoint) + case .ended, .cancelled: + guard let state = state else { return } + state.update(attachmentAnchorPoint: anchorPoint) + let velocity = pan.velocity(in: containerView) + let angularVelocity = state.angularVelocity + let speed = sqrt(pow(velocity.x, 2) + pow(velocity.y, 2)) + // The multiplier on angular velocity was determined by hand-tuning + let energy = sqrt(pow(speed, 2) + pow(angularVelocity * 75, 2)) + if energy > 200 && speed > 600 { + // Limit the speed and angular velocity to reasonable values + let speedScale = speed > 0 ? min(1, 1800 / speed) : 1 + let escapeVelocity = CGPoint(x: velocity.x * speedScale, y: velocity.y * speedScale) + let angularSpeedScale = min(1, 10 / abs(angularVelocity)) + let escapeAngularVelocity = angularVelocity * angularSpeedScale + state.itemBehavior.addLinearVelocity(escapeVelocity, for: messageView) + state.itemBehavior.addAngularVelocity(escapeAngularVelocity, for: messageView) + state.attachmentBehavior = nil + } else { + state.stop() + self.state = nil + animator.delegate?.panEnded(animator: animator) + UIView.animate(withDuration: 0.5, delay: 0, usingSpringWithDamping: 0.65, initialSpringVelocity: 0, options: .beginFromCurrentState, animations: { + messageView.center = self.restingCenter ?? CGPoint(x: containerView.bounds.width / 2, y: containerView.bounds.height / 2) + messageView.transform = CGAffineTransform.identity + }, completion: nil) + } + default: + break + } + } +} + +extension UIView { + var angle: CGFloat { + // http://stackoverflow.com/a/2051861/1271826 + return atan2(transform.b, transform.a) + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Presenter.swift b/Demo/Pods/SwiftMessages/SwiftMessages/Presenter.swift new file mode 100644 index 0000000..be093be --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Presenter.swift @@ -0,0 +1,423 @@ +// +// MessagePresenter.swift +// SwiftMessages +// +// Created by Timothy Moose on 7/30/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +protocol PresenterDelegate: AnimationDelegate { + func hide(presenter: Presenter) +} + +class Presenter: NSObject { + + // MARK: - API + + init(config: SwiftMessages.Config, view: UIView, delegate: PresenterDelegate) { + self.config = config + self.view = view + self.delegate = delegate + self.animator = Presenter.animator(forPresentationStyle: config.presentationStyle, delegate: delegate) + if let identifiable = view as? Identifiable { + id = identifiable.id + } else { + var mutableView = view + id = withUnsafePointer(to: &mutableView) { "\($0)" } + } + + super.init() + } + + var id: String + var config: SwiftMessages.Config + let maskingView = MaskingView() + let animator: Animator + var isHiding = false + let view: UIView + + var delayShow: TimeInterval? { + if case .indefinite(let delay, _) = config.duration { return delay } + return nil + } + + var showDate: CFTimeInterval? + + /// Returns the required delay for hiding based on time shown + var delayHide: TimeInterval? { + if interactivelyHidden { return 0 } + if case .indefinite(_, let minimum) = config.duration, let showDate = showDate { + let timeIntervalShown = CACurrentMediaTime() - showDate + return max(0, minimum - timeIntervalShown) + } + return nil + } + + var pauseDuration: TimeInterval? { + let duration: TimeInterval? + switch self.config.duration { + case .automatic: + duration = 2 + case .seconds(let seconds): + duration = seconds + case .forever, .indefinite: + duration = nil + } + return duration + } + + // MARK: - Constants + + enum PresentationContext { + case viewController(_: Weak) + case view(_: Weak) + + func viewControllerValue() -> UIViewController? { + switch self { + case .viewController(let weak): + return weak.value + case .view: + return nil + } + } + + func viewValue() -> UIView? { + switch self { + case .viewController(let weak): + return weak.value?.view + case .view(let weak): + return weak.value + } + } + } + + // MARK: - Variables + + private weak var delegate: PresenterDelegate? + private var presentationContext = PresentationContext.viewController(Weak(value: nil)) + + private var interactivelyHidden = false; + + // MARK: - Showing and hiding + + private static func animator(forPresentationStyle style: SwiftMessages.PresentationStyle, delegate: AnimationDelegate) -> Animator { + switch style { + case .top: + return TopBottomAnimation(style: .top, delegate: delegate) + case .bottom: + return TopBottomAnimation(style: .bottom, delegate: delegate) + case .center: + return PhysicsAnimation(delegate: delegate) + case .custom(let animator): + animator.delegate = delegate + return animator + } + } + + func show(completion: @escaping AnimationCompletion) throws { + try presentationContext = getPresentationContext() + install() + self.config.eventListeners.forEach { $0(.willShow(self.view)) } + showAnimation() { completed in + completion(completed) + if completed { + if self.config.dimMode.modal { + self.showAccessibilityFocus() + } else { + self.showAccessibilityAnnouncement() + } + self.config.eventListeners.forEach { $0(.didShow(self.view)) } + } + } + } + + private func showAnimation(completion: @escaping AnimationCompletion) { + + func dim(_ color: UIColor) { + self.maskingView.backgroundColor = UIColor.clear + UIView.animate(withDuration: 0.2, animations: { + self.maskingView.backgroundColor = color + }) + } + + func blur(style: UIBlurEffect.Style, alpha: CGFloat) { + let blurView = UIVisualEffectView(effect: nil) + blurView.alpha = alpha + maskingView.backgroundView = blurView + UIView.animate(withDuration: 0.3) { + blurView.effect = UIBlurEffect(style: style) + } + } + + let context = animationContext() + animator.show(context: context) { (completed) in + completion(completed) + } + switch config.dimMode { + case .none: + break + case .gray: + dim(UIColor(white: 0, alpha: 0.3)) + case .color(let color, _): + dim(color) + case .blur(let style, let alpha, _): + blur(style: style, alpha: alpha) + } + } + + private func showAccessibilityAnnouncement() { + guard let accessibleMessage = view as? AccessibleMessage, + let message = accessibleMessage.accessibilityMessage else { return } + UIAccessibility.post(notification: UIAccessibility.Notification.announcement, argument: message) + } + + private func showAccessibilityFocus() { + guard let accessibleMessage = view as? AccessibleMessage, + let focus = accessibleMessage.accessibilityElement ?? accessibleMessage.additionalAccessibilityElements?.first else { return } + UIAccessibility.post(notification: UIAccessibility.Notification.layoutChanged, argument: focus) + } + + func hide(animated: Bool, completion: @escaping AnimationCompletion) { + isHiding = true + self.config.eventListeners.forEach { $0(.willHide(self.view)) } + let context = animationContext() + let action = { + if let viewController = self.presentationContext.viewControllerValue() as? WindowViewController { + viewController.uninstall() + } + self.maskingView.removeFromSuperview() + completion(true) + self.config.eventListeners.forEach { $0(.didHide(self.view)) } + } + guard animated else { + action() + return + } + animator.hide(context: context) { (completed) in + action() + } + + func undim() { + UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState, animations: { + self.maskingView.backgroundColor = UIColor.clear + }, completion: nil) + } + + func unblur() { + guard let view = maskingView.backgroundView as? UIVisualEffectView else { return } + UIView.animate(withDuration: 0.2, delay: 0, options: .beginFromCurrentState, animations: { + view.effect = nil + }, completion: nil) + } + + switch config.dimMode { + case .none: + break + case .gray: + undim() + case .color: + undim() + case .blur: + unblur() + } + } + + private func animationContext() -> AnimationContext { + return AnimationContext(messageView: view, containerView: maskingView, safeZoneConflicts: safeZoneConflicts(), interactiveHide: config.interactiveHide) + } + + private func safeZoneConflicts() -> SafeZoneConflicts { + guard let window = maskingView.window else { return [] } + let windowLevel: UIWindow.Level = { + if let vc = presentationContext.viewControllerValue() as? WindowViewController { + return vc.config.windowLevel ?? .normal + } + return .normal + }() + // TODO `underNavigationBar` and `underTabBar` should look up the presentation context's hierarchy + // TODO for cases where both should be true (probably not an issue for typical height messages, though). + let underNavigationBar: Bool = { + if let vc = presentationContext.viewControllerValue() as? UINavigationController { return vc.sm_isVisible(view: vc.navigationBar) } + return false + }() + let underTabBar: Bool = { + if let vc = presentationContext.viewControllerValue() as? UITabBarController { return vc.sm_isVisible(view: vc.tabBar) } + return false + }() + if #available(iOS 11, *) { + if windowLevel > .normal { + // TODO seeing `maskingView.safeAreaInsets.top` value of 20 on + // iPhone 8 with status bar window level. This seems like an iOS bug since + // the message view's window is above the status bar. Applying a special rule + // to allow the animator to revove this amount from the layout margins if needed. + // This may need to be reworked if any future device has a legitimate 20pt top safe area, + // such as with a potentially smaller notch. + if maskingView.safeAreaInsets.top == 20 { + return [.overStatusBar] + } else { + var conflicts: SafeZoneConflicts = [] + if maskingView.safeAreaInsets.top > 0 { + conflicts.formUnion(.sensorNotch) + } + if maskingView.safeAreaInsets.bottom > 0 { + conflicts.formUnion(.homeIndicator) + } + return conflicts + } + } + var conflicts: SafeZoneConflicts = [] + if !underNavigationBar { + conflicts.formUnion(.sensorNotch) + } + if !underTabBar { + conflicts.formUnion(.homeIndicator) + } + return conflicts + } else { + #if SWIFTMESSAGES_APP_EXTENSIONS + return [] + #else + if UIApplication.shared.isStatusBarHidden { return [] } + if (windowLevel > UIWindow.Level.normal) || underNavigationBar { return [] } + let statusBarFrame = UIApplication.shared.statusBarFrame + let statusBarWindowFrame = window.convert(statusBarFrame, from: nil) + let statusBarViewFrame = maskingView.convert(statusBarWindowFrame, from: nil) + return statusBarViewFrame.intersects(maskingView.bounds) ? SafeZoneConflicts.statusBar : [] + #endif + } + } + + private func getPresentationContext() throws -> PresentationContext { + + func newWindowViewController() -> UIViewController { + let viewController = WindowViewController.newInstance(config: config) + return viewController + } + + switch config.presentationContext { + case .automatic: + #if SWIFTMESSAGES_APP_EXTENSIONS + throw SwiftMessagesError.noRootViewController + #else + if let rootViewController = UIWindow.keyWindow?.rootViewController { + let viewController = rootViewController.sm_selectPresentationContextTopDown(config) + return .viewController(Weak(value: viewController)) + } else { + throw SwiftMessagesError.noRootViewController + } + #endif + case .window: + let viewController = newWindowViewController() + return .viewController(Weak(value: viewController)) + case .windowScene: + let viewController = newWindowViewController() + return .viewController(Weak(value: viewController)) + case .viewController(let viewController): + let viewController = viewController.sm_selectPresentationContextBottomUp(config) + return .viewController(Weak(value: viewController)) + case .view(let view): + return .view(Weak(value: view)) + } + } + + /* + MARK: - Installation + */ + + func install() { + + func topLayoutConstraint(view: UIView, containerView: UIView, viewController: UIViewController?) -> NSLayoutConstraint { + if case .top = config.presentationStyle.topBottomStyle, let nav = viewController as? UINavigationController, nav.sm_isVisible(view: nav.navigationBar) { + return NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: nav.navigationBar, attribute: .bottom, multiplier: 1.00, constant: 0.0) + } + return NSLayoutConstraint(item: view, attribute: .top, relatedBy: .equal, toItem: containerView, attribute: .top, multiplier: 1.00, constant: 0.0) + } + + func bottomLayoutConstraint(view: UIView, containerView: UIView, viewController: UIViewController?) -> NSLayoutConstraint { + if case .bottom = config.presentationStyle.topBottomStyle, let tab = viewController as? UITabBarController, tab.sm_isVisible(view: tab.tabBar) { + return NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: tab.tabBar, attribute: .top, multiplier: 1.00, constant: 0.0) + } + return NSLayoutConstraint(item: view, attribute: .bottom, relatedBy: .equal, toItem: containerView, attribute: .bottom, multiplier: 1.00, constant: 0.0) + } + + func installMaskingView(containerView: UIView) { + maskingView.translatesAutoresizingMaskIntoConstraints = false + if let nav = presentationContext.viewControllerValue() as? UINavigationController { + containerView.insertSubview(maskingView, belowSubview: nav.navigationBar) + } else if let tab = presentationContext.viewControllerValue() as? UITabBarController { + containerView.insertSubview(maskingView, belowSubview: tab.tabBar) + } else { + containerView.addSubview(maskingView) + } + maskingView.leadingAnchor.constraint(equalTo: containerView.leadingAnchor).isActive = true + maskingView.trailingAnchor.constraint(equalTo: containerView.trailingAnchor).isActive = true + topLayoutConstraint(view: maskingView, containerView: containerView, viewController: presentationContext.viewControllerValue()).isActive = true + bottomLayoutConstraint(view: maskingView, containerView: containerView, viewController: presentationContext.viewControllerValue()).isActive = true + if let keyboardTrackingView = config.keyboardTrackingView { + maskingView.install(keyboardTrackingView: keyboardTrackingView) + } + // Update the container view's layout in order to know the masking view's frame + containerView.layoutIfNeeded() + } + + func installInteractive() { + guard config.dimMode.modal else { return } + if config.dimMode.interactive { + maskingView.tappedHander = { [weak self] in + guard let strongSelf = self else { return } + strongSelf.interactivelyHidden = true + strongSelf.delegate?.hide(presenter: strongSelf) + } + } else { + // There's no action to take, but the presence of + // a tap handler prevents interaction with underlying views. + maskingView.tappedHander = { } + } + } + + func installAccessibility() { + var elements: [NSObject] = [] + if let accessibleMessage = view as? AccessibleMessage { + if let message = accessibleMessage.accessibilityMessage { + let element = accessibleMessage.accessibilityElement ?? view + element.isAccessibilityElement = true + if element.accessibilityLabel == nil { + element.accessibilityLabel = message + } + elements.append(element) + } + if let additional = accessibleMessage.additionalAccessibilityElements { + elements += additional + } + } else { + elements += [view] + } + if config.dimMode.interactive { + let dismissView = UIView(frame: maskingView.bounds) + dismissView.translatesAutoresizingMaskIntoConstraints = true + dismissView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + maskingView.addSubview(dismissView) + maskingView.sendSubviewToBack(dismissView) + dismissView.isUserInteractionEnabled = false + dismissView.isAccessibilityElement = true + dismissView.accessibilityLabel = config.dimModeAccessibilityLabel + dismissView.accessibilityTraits = UIAccessibilityTraits.button + elements.append(dismissView) + } + if config.dimMode.modal { + maskingView.accessibilityViewIsModal = true + } + maskingView.accessibleElements = elements + } + + guard let containerView = presentationContext.viewValue() else { return } + (presentationContext.viewControllerValue() as? WindowViewController)?.install() + installMaskingView(containerView: containerView) + installInteractive() + installAccessibility() + } +} + + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/CardView.xib b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/CardView.xib new file mode 100644 index 0000000..425a696 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/CardView.xib @@ -0,0 +1,150 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/CenteredView.xib b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/CenteredView.xib new file mode 100644 index 0000000..a9fda2b --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/CenteredView.xib @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/MessageView.xib b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/MessageView.xib new file mode 100644 index 0000000..52ceaa7 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/MessageView.xib @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/StatusLine.xib b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/StatusLine.xib new file mode 100644 index 0000000..3dbef99 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/StatusLine.xib @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/TabView.xib b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/TabView.xib new file mode 100644 index 0000000..141467c --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/TabView.xib @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIcon.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..92f59e794d62d0691feee73f9da2b5f80cde7ae3 GIT binary patch literal 698 zcmV;r0!96aP)Px%bV)=(R9FeUmp_k8Q5eSWiUbh~f)$h~blYet2uq<6UqE~S-$N)g*bksnC?yi1 z5s3sFL39WW3Tr6@zu&~!+&z2d{JCcKNuD|Pp7XxX^UTbdd+*G2?bt%gQMeE8g*)IE z_z`}9kKqqEacoBMBD@Qi;hzj$x6XR|{u>)nxCGzBobk9d`|MX|5DwC{2*(+;%&Vhb z6HZe76}A|aYSmM>3KyvQ1xuns*40%%i#b+H8*hktzejz2lGx4cJJ@fS*0DbNt-%5_ z(=tuz>bDAqIm_*X@2sDPFJMYAp1VTbwL-0a`mW$MN3l>iH^8xfmAe>AK@1R33Zm}_ zTY3EngK0dzVomb^kDtT1&hvf%>*wKbn5uP7U;Vpta9mNC*R&KQ6(INKg0}wMt%?vf zYs)91e|M@PL@q!GIW&m=-6oj(g*>&Whun1_ z4swLP++|x|{YO~fHN2zL=#5C!Tn9oBpBENkN-xZLwO+vij>7jF3gYojMNQ~2NGX7R z`mVxN&VtB#jQXv?c4l6}^$4Jkeo6S>z(@0G22sEGALIl@%To%XuKHD&qiWJm-s-8_ zggsQ>hm-hf)KPCR&eHh|4hdtQ{S$$A$}{i=Ed4`w-8$?0cUVr-s47ad9%61koP|5# gXZQ&kp!u2NFC|*l{R14oz5oCK07*qoM6N<$f;)vtHvj+t literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIcon@2x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..2a0be86e11e8660d4f1b6007c9ad7a4ebd308326 GIT binary patch literal 1338 zcmV-A1;zS_P)1^@s6CPx(^hrcPRCod1oI9u#Nf?F42e_buqSrtT#0M@K=z@p}A{dFGm?#(+?QXOfZn7$f z@uHC?S40FKksyMJpoto2AUE=Ds=MHmU0u=h4O6o-eP_C>y8f!3I|q){-BbPl=bRb) zHKVgy&0?O~FX%}4pz+_PV9it9R@&^TA@#eZ6PEk5pWO5+{zq4-st zahNZ~2gPgRJMmj2sX;AjQhVgFNea)4AH`Vn!}g~(J?J%Z*dawX#9`u7ZKo%_8^>}f zxFDu7>RT5LXlVq$s_T2vS8VEeX!#{)5{uOA>Iel;3>a-_Oo|Pv{#-0JAGLKfqBRLP zDE%dln(1MB&}x1l-J_zPW>5$&95kcd1lv?4<#mA*ooYlo9-{E0_I@L#5}B(m9`I5N z3$@o1F_+lXeei;(U|iH5sm0~4kEb9U*6iFObM?cU7nW+)-(oIdx%=P^kCiy98M(#f z?uW-xY}Tth0ErPcrTu>vzeLsB;JmbBKy;>?x3R&k^fY~yHhEWy5YBhT$ zhUgqL7w=u1)6$?xA=@m&$Q1(q@ZN<*vJ|xlBWDO`#(Nv9^k>LBUo|%-jNBnOFJ#a< zWE=#=>x9u%2yO|(PRjCCWF?ML_yHNCyXtcSN0vO)!Cu!O(ui(7p!HI zVqfzK1092qBLw{MC9}!7xGJnVVdM${?d>y1u4fM+VdM;fAjrV<4${CMP< zSXImN_0I^3xmFJqQ+0Mg4+y;+57kkePX^f@*ZYQmCXh1>9{0Cih{c4g*F!@<7swUH z3!2>%t&R8As|tZ*LK$*|aTfvn$f8%c<#JUBoE(^u$WYC&04x&y7O@~1SZSe5biHR9wLU4U^e=_ji#h+So zLh6ndA-LW-!@zqN{E)(ls54rI;QHqZ1Mhu!sx>Dfbez<_v-_(3zY=o;RTahowc+>M zp?ckXJWsYn?eD^{2*GJ6hG7u` zUhzB-r?k~IuR9V3-$1=GsMV{%g<%l_b@4hBEA>k3r-CM_ZtV#p=9u7u#Uoz3;2)Lr z;+HSApwQMYtP$BD(a(ZNyvE9d9?IU0V7&hGF0d zZ$UVw+2i(Uz*8_de%unrB@Db2kBQ!)EY{4aF$e<>c!>h18S86;oS!nZ_nj(Z(payZ zUy3;mSoA_O+D)-S)gFl!0&~zi(Kx;h)uCCn$Tu( z^{Fy`*sf`#bVfl7ni>K36i39jVv2XzrzXA8km~+MrTsCXsVBr-KQSO6deM{KBZmc2 z^qcrV^m#Yy%%L_t=rtq2`I|>!x5b#}6YNV3YMEaXsAd~Hd&q$w)MTr;QRF|;SS#|8 w=3RCF;OxDYKZyK3np@?^A_u_xB4H%OcUF&el}iNmyZ`_I07*qoM6N<$g6SoE%K!iX literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIcon@3x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..07a9e775a964ecd8b458b361a6dea566bf375c2a GIT binary patch literal 2074 zcmV+#2<7*QP)Px+)Ja4^RCodHoJojWMHt6l)WmIK&?qVfjF`BQgAhbejC$}Oxp>fn9=wPkdeDo@ zO#~N^D1sMn>4?k04Hs}jf-w;hqo4teiJ2r4q9zkHiAG$a|6fnnIMdy4uc}wo{XY0l zb-$(R`~JUvZ@R1B>m+F-7zgGBlSzIeXKRz?(lTkWG$AeQwI`}QA{~|v_1Y=5+of&N zUMctp6~?)8Zk2Skv{C{9=2s0M0cNxGp|nBTCABfcf%#gtTIxtUrGGt$aXJ#?GX7Wx zMgtUd!;>6lV%qk zn>@*vyhkREQ6+asO`nR2Lg?R)yvd(BqK+$+{pV7DfiXU(4(f?67AfO((orc!9^eyxISFPlH%cvt;vV3aliA-VTM(6Ai1=nT z_%>|R=9zpQ;rNDsR^lA>vQP4r{3wotfB0A{n0Hw(jiN!mYr@A`^H{gGAgsO+@iR5D zNHO`fRcd_BB{lo_R{WrpFO&`rOa0hk3wz5RReN2=`gT=$2PU-3@zQ%`cID&zpOWkI zYDZ(d;bEM-I-XY!uXxG>UoVmh2g&6XP#pml|Jfi+Hok%MgD`pc0)o$( zak|$2Ce<}OZU2t?9tJ{-yWtB+8VHkz4LC{)}$F(cX>yBXMr$DxdxI4!sOut z2!5x=ms(TT^kMt&QNJgojykx)6_BDJOh$axIv@?J$;LAL9vUmO)W!g5eejM6lZ%FJ zAZ0X7?9h@l6KyW{4J2d`_=@jyVws-V zG(4{_UqC_!VSK{>nc(ISv%**F8b~Z4@EiYUVx1oOf-rj=S3qI{fgkuk6Fc>2wn;u8 z+XfOF2)xGsZg79KPxFV3WeZ4*AdDYyNUjM01_{*#5;F+uheL9^Wau>ARs|9}2z-Y_ z@_0ZnNUAK5`PwJz8%wR86TUuKmwjQwA$d(IyQAyv%L19>cE*!XLEtAGl20Yq=2q$7 zHjt1(;13*%&UT+HX;@va0vRRCF+8p zJ|YGnlO+0;2kP5g}6H1lw+*j#S8=v-Jh^_eG7mwYglUwgy}m*Ao#x!7{q4D z6yvkC4Z`Fe8<5Ys6ncNSi{BLDt91p!Jb8eihEv6)su&dSpX$pj? z%K-5S0)O$jP&}lqOtLOpfXto-cR5y8-vIIPKf>gIzXLIEV9E3X-qL+5otd(}U0mIP zAGPZR(!u^N_@-}Iu;Kl~R?yWwtBYEmKht(*Sb%n_UNeY0si5ml{inm{35kL z%6ous_-8e4Qg1Dc@*d!ulXy(Mwm^z|fL~5yt$J^PZm&V!joE~#vRq6I0Y4fI=31VT#u*6pSDtfn zLGLbg`p1|-pnCTcvS0N~n{1gAOJZnGhRgAk;^l(FFI-?UqIf3iVJ|l)?M$ zho#v6mykboc>nyMF|Eh>>hV>{*Ol(Jjz;n&Z}K0xxI`7LmAD1imSHaXAwTjY-;s|c zs_+SEzvRNqs(q5r8j1YIF8KCqwe*~HNUCaLo&6Y>@yWy5w{hyLbcFP&k$;ZYOV>)P zq!p5j`~bH_+90i$xDxz#sqt`8tgWI>Q%6@xTv4&Sr)6pvNk!vVZMa?9*3)LS+$nTG z3NAv1k!BV*mn@bhBxXoyx!vrDbXelFLpvqylcI_L0DGJOSwOuDjQ{`u07*qoM6N<$ Ef;`pjS^xk5 literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconLight.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconLight.png new file mode 100644 index 0000000000000000000000000000000000000000..bd00a52f9ff07b972c0f84b3a9b1c61f69373618 GIT binary patch literal 879 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 literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconLight@3x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconLight@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6360adbd6c44bc0f6c73260a447e51d447be1bc0 GIT binary patch literal 2900 zcmV-a3#;^rP)Px=4oO5oRCodHoO`Gp)fvTaVtu8#ij9_P5~G-aErNnbLD1S1EY=4;!3TH|DVF+& z+G15&EtaZit2I<=`;YHXsJ4O%DO5CS5K&`kYl|8miLZ*9)@al=@wI;Oeutg!?m1`X zoINvp?#%~_y=U(|d#`V;bLPzKbM74*+ZNdi@0qw$OwDMQgA=Jm8b-)k4(L#}1nEY~dN9NSre5AM5-L z!fOJTAWDvDFLQp;71dr5j-W3Lx}C%h|6ie1{;j{`*)w{vfZ!Unv#&Irf_%s;X2=U_M~_wlVs8pDV$0P&-U_Sw z7bP$9i{*_TjlTfDszuiq5c;v&XF`x?%n)=zpGIPVR(V{JGz4AuuQF3?Bka;D6O5&W!%ZRs`*!3efAHVbta zf^MN1)X}$8?4wbBzlH7Lol2cuZjc#aB-=8?j4&Q`7=mu-XKPTR?>@r7__aoaCsIFa z;92DdOlTp^riWy-9n=+)x2;-63 z5cESwo1YS$(Yr7%R!?{W^&b|FuGTT3;r)!Tb?Y`{kP*fswIS$eb6bhth4FXw*g(qv z3gd;N%XiGz&<}-h?tb;U3@K}bapS>lxAJNB9f^+UX>(JecQg)AYdDbljnR?AH6Ewa zHVJ->qT|$tR5gO{D84mZs)c8vr_UGNr^FR%@dFQ=GHSGjwhYO^2$VxluQ9q$i5Jxp z4&NOw~EC&3XPzC%~m7Q9yDc0PDYr5=<2mb|6cIL zx*w4G64hI5vQoDpIT}Igu}c;D_u^L7LNmDY*|FDKU!^WXay0@a(beBsqJJ;esur5D zve#38CAA?r8)0gqZ>aZL>)6wEFz9F2*-)9vkS-X3r05%(K^-5)ju3pucj#z1Mm1y@ zBbGLtqb-NNUh^a9{BERUt50_ZH59`d(j_C1GIsZh4_j_wLzmc-`2rm+MO8!K>$m*F ze0-fJ>$ctzCMo*DJLIMahlFHP)N?SxWL@L(!-uiQUE`l=%pg)5 zl9Lh0i_YN-?D5%Z@Vm0_FfxOUEx*yBUxk#gJT$=&TuLhFso2#BIWMLoCk^0qzoVG-S$Wb?#=2VuX2@ zt0A2-BZUz;8xl3+U1L01?%|HxI814T@z3oY;&tBQ!yRH9J_){V>{}`^qRL*=|1^G+ z5a!UYp88iO!^(utAy0f5`>AVWz!&LgA*vc-TO3ulPTDpEeM2LtP0WD|cEym>MA&P72uZL}=&#W?(9uw&GQ#L~!4UKf z%|QR&hV5$C`47vi<}$SrMmKju&^I*WNh3~qo$LHW$Mfi@G3qkHDCcSjx}tAr2Kx8n zc-6vA>L>Pk(^u*?!Widh2)cT$(Z3flx0d&M(^i@?!dT~K2)c%5M6W`t+#og(=U7IG z%BGDl+PN5lp6D8y@raQO%kR5EoRIPsx2h5EYyG7m!4c~@7=oUm5!BIrO0ZYre?D^0 zlu^Sq?4{HE?@K+Y@Hn**#+t44D!)2O`Ik#@xK{}CL~nGD#tOBDgFn{se$kQD8&A?H z-t2HKtJh_OF)sTK;l=Z9J1Ielj_7H5QlfWZ#GVXqEF4|EV~`Ft%n$T}GfAIfiF#9c5Yq?U zYKf~fYiz-*Ygw&3lahZ-7xbz%Zr1Fvg@2>=*|n~m-{j;=-Z4GUsrD$kHbUqwVWvD3 zlII_Vm=TSBmU7n+dmx0^N+H$%_0v>Y2KlWNVn(1#<9C_kTJ$dkgx0D3P+>X-d68et z2=qXgR$>RO`gA@*?jn;~=qmRxlHM`HK4Z{Z#Ik zF8IL{zCH&#CQtIsI{3rE+XN#WE7z$19APIRr+^Q<;1}at}gvS10s z$~V;i6Je3iX0Sdy;1lC%ZRA5-%G)r4by*)C@M$f|zHOTz$cN)m!Z(G(g#&~- z_+j|d!o$LY!V2L7q4lUE*;Gk=NfR9=@QVt&_Agd_SD|cd(7@*f`>a{5`Xj=BgzO?` yGm6CGmUNylSFjIjKD_x54?on`>-a69Gw~Tbna6B;nGLuA0000h($ literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconSubtle@2x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/errorIconSubtle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..0a6d0d9dc2a29e0dfc9afc25f9a8c44e87ae7c75 GIT binary patch literal 402 zcmV;D0d4+?P)Px$Oi4sRR9Fe^m|G5lFc3wFKQ67?=qlZv(Yfj+QYdsDg=mlzuQfe##Z6oMXkbziE4#IL@%uR@4a^4hODk- zQMIB!cM|L_+_Iv0+-Q_r>bq}=R#D_>axL3%b&jRDWzMQZ%b=&IXKstzBqyR+1Y;t# zbQ4)5k~MSf0q?R9LBjKY!w9)Q4==5Px$*-1n}RA>e5nr%+QFbsw#&cUg;04Li$kq|2OsHciGotwOVoj@Xa*+*kPe$;mT zk;`R{X5CzgkshzHeC9F0mrCO>d*asBxGc6cZ1#oBVZ$P zdi$w94hfCSRozhSLIgY_M=d{WT}u-!Yhewb64f+wmEu_sVW_?z@|+d1NCs~ppF$tN z(O*P}7)pQ%MT!_w08+%z0uUnpgn$+iQ~@tWI_Ee8EjLK&*$}0mMZh9Z8bgaf2pC!f zQoxWRA_NR2;;aC@-mmL*Z;d{iyS_)%2+%b;TGsw#=-?I61R_V83M4!-7fnb6bVSZD zA(60=xnV*Wkc=ADLYQ#OT5cg3P#HCgLNZ}8Yu|am0jP+7y}{c=q;0V`fOYPL h;}_QaI1LPG;2V2*pY4uNe(nGO002ovPDHLkV1kT%)>Qxi literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIcon.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..16a0cc7faa0e4130b6c2c66c40b1264d7c603ac5 GIT binary patch literal 495 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 D=VAYv literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconLight.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconLight.png new file mode 100644 index 0000000000000000000000000000000000000000..9195a166d4130cc3ba78539a7be83767bc1474b8 GIT binary patch literal 640 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 Hu0mjfs5#A7 literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconSubtle.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconSubtle.png new file mode 100644 index 0000000000000000000000000000000000000000..aa1a119e326906c7d2ee0197b4700021aa2bdb80 GIT binary patch literal 139 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!3HF^2u;-kQjEnx?oJHr&dIz4a$G%K978Mw zlT#G<{o{`kRLUVOuZ0S}JWDV5mR#{OxUev9rF&7ZvXHd)HLr_bn9#mTeYbbCFo>YS(S!a{TGq^dM8U-`=;sU;1+FAz#*G^{nn| z(;b*5HGJCpH~l5|Uxlg_rSZV3@g*DWX$<}dt-Xfq27(8A5T-G@yGywp(j6#b5 literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconSubtle@3x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/infoIconSubtle@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..769fd02e82191935e96ec0ffddd9f6a3b509ec15 GIT binary patch literal 263 zcmeAS@N?(olHy`uVBq!ia0vp^_CRdL!3HD?0vyGG6k~CayA#8@b22Z1oP(Y&jv*PW zZ)Y6jZ8l&y(r{F8^5J$yrK4RN3hqy=-uS!W-R)cFZ9kO$J|OwzL}S2`33~dx=C->( zUFKTaaLW6z@A-eFKQpTtjV3hmq$n`oH{0;$?$4_Qd@pveol|fRi{EE&JRyhS$@Tv2 zv2s6HH3g@Jr!l*Q%S~9}5)wC)S0c93VUbW^WvW?1$y3HoFPEoNZ#G<<%C4eCyh47i X66Tmmv3-s}&oFqp`njxgN@xNA9#3SO literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIcon.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..1d371c7fef898785975041c5a9fd4a02938609c0 GIT binary patch literal 639 zcmV-_0)YLAP)Px%IY~r8R9FeMm%VBhK^TQ6ApRg)sAxoO1hu#E3KT_)bdp}I?7RW-25fB9T!6J= zD~KXk3L+>r!31rLpb{+veV*us$!2zTXLr5>=i8l`_dVzAe#`#Mg>vknpuHyJxnxDHof&bVvMKKs=fg;R9Bfn5eA^XjNq zgbP%Eg(XIPwd$!m2v@244f{mLtgEYj7H3)QyYR-C$35!nlf+?WKf<_SR{Q$sw+&0o z%*r&StKSx!VKj^DK+T(Q8McGCrtc;mvezz}I@VHmTa%N30!!FEce$0_dlI_|{?Yg!!EofPF9(WNH9I=pWv}_8D;e79N2O+Cgdr z&`1C945mWlg47D2hyLMiCPY_|OEyNx-3Pi3u$k9CT!hU;*S7uFME+q)0R8k2yP#KS zSAb?d6=Z4vef8hK62FaEZW{-g8bBZK)|)uN-q44bL2kiZ*dc5B>AMAY7!7HzRiocF z>}STKtYwnZ(MP`|{5Gils^1Qye(yKPd5YFyySc_#UG)dSSMu6#-s-7a1fLsEVU07O zPW&7y3S#jU_!^E0W1sz1Fo()zcn|ueoeSi;b=KS0i$O&RmD9`}hx70t`~p8i1H6Yt Z_zTb-1=<)B+U@`V002ovPDHLkV1h5)F%19! literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIcon@2x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..719218cfc613e28aa008bf0ef950c55288596f03 GIT binary patch literal 1214 zcmV;v1VQ_WP)1^@s6CPx(cu7P-RCod1oI7X~Q5c0cQHWrKh^FvS2r52OsEtNJY{k+tkit$Yu@pfq1Y6P0 zN<;)7jW&Xfpdf0Yg#?5|j9U5tjS)49o^Rc|>?6B-@7}qO-2-Q5cJ|KypL2GcoxQVW zpp+IvYCl|&@J-8~spJeqXuL!{c8UANF>!N+qgpN%Yw=r8K8Vv5PHFi_d?Ef3dk)K` zctX4@eii?Ek{Z;aCbhH24k^4U&Wpb0TkTJ6deAF7?31E@J+jBIcH3&EN@#1`h$kGTrImb=uzBVJqLre1L=n0hQBn5)P0j@oauN@1g3=%u5fHU#6zB@Ddc zxe1rFCG|UO+kdeTj3JjW@LUHbQJFNg6_RpXyAX`2JC~*1mmaqf$BB`vN^UFqpq#-iRWOH0F3VU)g7HfRA>f&t z%;mGDPXOGx{v!I+F#Dzmf!^g$4xhP)S1Phb+#~viktzg&AcN9^XxjEJ27ZVx0Wh{P1nPip!blwge#pRiy8gZP;h#e*xP*~I2n4}&e`!{9IU@KT&qY`} z*o2Ww2zZ~x%u~@N2*&z`ky8lxc_McGLyvC5$Snkd;D;uqGa^sAp=%g9hJbG}C>`!H z1QVQX7@TCyL@Rw4gX^S<(crrzgVLzzTgWDip~alnY)cga-pQbhDLuvrqtdQvkun6l zS8+jWzCp0R&5==UT3JaM0-Et&#kkgr2*Ndt)FGf3?{#>sXEDRbAp|@(;k33C6-1{a zBZm;si03A((w2EKbQrmWfLA;(ii_G>WDt!eur70p2~qN?}B+`~X4T7zOx)lZ5GVs3$;32kVMhP~>P(V7Y&=SeWCuIEIbpBT7?K`(mJJ39W*Gx!CGc{Y>hBiEME;P*CXqv$ch&rdvpKEvS4H@JG+&jcMNWXv cM8XJ)e+hZsXwv2@djJ3c07*qoM6N<$f`~;q-2eap literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIcon@3x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..74893cabb1d630b086b57575d75f947d98068965 GIT binary patch literal 1930 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 literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconLight@3x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconLight@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5e15299950997a59c182828782cfedae42b3e350 GIT binary patch literal 2743 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 literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconSubtle.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconSubtle.png new file mode 100644 index 0000000000000000000000000000000000000000..6133483801a3b3e34132dcec294f842dfa48f775 GIT binary patch literal 235 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!3HF^2u;-kQjEnx?oJHr&dIz4a+Z3!IEGjV zp1pjK*TF%ACE(E_2KL++jcy^2*06a6^s~jt9-TO|cYaPv=U?rI>dC46`iF&F?dM6{ zl~So+t$a}K<^;i$!ZA*53GG+dCnd)ARbNdqPw{T+-MM~N{=qMf>Fe7>4{&PcCFpNp z*`ic)(fCo*UIz&_MK$$`w!MyvWVRI_%(}2b{6j-+CDYgPiaZ&fIMXn5k$|w>g%{SV iFIdwO-O~Q`?;8ef?|32m_nJTlGkCiCxvXPx#{YgYYR9Fe^)XfdTFboCY-Z*kbVxO+T0L;fU?7##F-fLwkiV8S!eoU1}fd(b` ziyLQ}M%#Y|4%p)CeVaty(4XiRbeBz#FX-4^Vq>IB>?dT1oqL~wUr1tkW;w508^}_r zADJAPLFLAarnd>qQ_JOHk z!Nt#-0yH6Iu?8FCWZVpC!8S+cV7nnxu-%awtQ+JvtUKfu)-5u`UNPVmHriii;0^XH VHP+dQoj(8o002ovPDHLkV1gtpiwgh% literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconSubtle@3x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/successIconSubtle@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..6ce53797084ce9f8a380c80124feabe35bbd875a GIT binary patch literal 418 zcmV;T0bTxyP)Px$TuDShRA>e5mr)ABFbqHk;+H>q3NPY4b^?zkcm}_{k%%Uaz}VcnbxBFm6ta$$ zQr>Huc1;r{3{+=e=bZQme8~86&N;$E@F&Cr;xKA;{DHtfz=%}R?JI7}Zc=TsdtZh5W8Qjevc-s`c)V~-m&5Xmi)TO_y1n;d&q z-@|gd6wVVW_BevyA>I&eApxqu=_jFt0F~e+1gHj=6kq~eLVyZz%x5cC0B8D8+OE=m{(1G)@+0oHoDKjxOuhX4Qo M07*qoM6N<$g4|@P+W-In literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIcon.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..77357618bdc894d6c626d9962b2aead85e00d5bf GIT binary patch literal 493 zcmVPx$r%6OXR9Fekm_1GdF%X3lg6OE|l6o6BLrxH+9D+O00xg^diJTy32n}`8LQ7N5m8u^1+bbR%xW zuLNR`AO2Zb$U8N8EEIp@B;!OLk767!=bCuL9fWgPt;C#!UB1L6elJjZECEg!@q1U~ zNe0gNm?MwIW9(-?FV9PGJ!JY27~KW)A=7Iau__*v(MC+Ydmq5yOTL5hrJhp-U?0Nb zPhJnpDwGgG-WIfC%UzTZL5zk{VK1nOB_i02YGRHEcEg&OA%g9=CZ;s2ck#@qB5Zc? zi#jyq2P?sWq$c)=;6zjtJ4A3KtBEZl))@-ICT9)P=FjPvp$O}|ac?(0{L*kMIa@jD zUd1by_h0Oy6REN6r0ymdQy&NU$U8daL--VL#nhA4y?m@7A9;VF^fA9{bq>y$u?k9d zC3ZQN_@4OW=|%OwJDfU{mOlOy@6Q!*0>W{j3rp36j}3p~WPviGkEDvh9>2B>HFm;v j%ua*o2U>jb|4)1aQn}jcPXB8?00000NkvXXu0mjf7E0F0 literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIcon@2x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIcon@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..448d68651104481fdc9597a0310787f51ecf2cff GIT binary patch literal 916 zcmV;F18e+=P)Px&PDw;TRCodHoV{+;Koo^Hh@XZusY$m{6dr&o4ay79@D#iNk3d05fdmamRJ;Qf zZR#jV$+o0PMUa5=?RqVH8LwvT@!j!WAL+)sWAEI1j>j|C<8k&THh3a8r?L~-vFu28 zAlolYo5^iqH`-pyE@c<8@3NU}khrI)&t&gqSF*nyjXJ1{I;lH*94f*$vLCXppp{?_guDG=}^DJrPXMX^AOqgWWZ@7keIY} zmizCblP*$Ki;S`9%=9fJ+@jKP^7g)iq5dXV_=e+JOtshQ2C69#3nAbL*AlCf9Iv(K zez|vdKfn>LK`_o*&L;$eP@zSOzz1B*+@jhdUTMbznHRtn&NU#2bp}}rpz_74Mc@a{ z^Tx1j5zke?OtUw@8Sdqb@JnwBRQT4oUulPBlf+jOtmA; zWry?;Jc06#Q-t6#oOhfc#Ibi3&;TKLAnP4R2*Fcn?>In+eeWuu0Ylg?*~2Y}k<;*b zM~fIC1rUOjD&BEB3bAMcb0Ju>dEpaDX#dd)j75rQRr-f^+b+wh#kpWlYh z6p`nC0`4;|4e)KTart-7eJR9Oml7;U*SI5y^>n`V++@h#f7AJYB*YivRzXJ{GRrXF z4EGwwM}CDIG?ULYX?FX|C(&!1z2{LmkIX~YQ|huNnN#}-)dxqouEn@eq>zVYH6&Rf zB~z9)osE<}LUq9nj!_sFlT~CImlqadj0;XHt%BTQ7cx#bl*@!F{5JYHE=sS+HNK`$ z5rdX7q|1a@JHMV#;a~g6bvA?*`^4C}PpB~ud|@=nd1xiJR<9!jxJDI`bXjDeq3keJ zMl8`Q@1q!Qa1f~~_a;_1Y9RseO$^a#vySZzQ`P)YX1RnJU--m#cA$fD{gV9Z8DS3e qOg5N*o4}l*1z5|;GRAEGk;Xs0(etE|nl{1!0000 literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIcon@3x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIcon@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..e2c8d46f95919cc1e24eaedce2e3959205bd675b GIT binary patch literal 1577 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 KMNUMnLSTZxY^Qzz literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconLight@3x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconLight@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..11d68596ca4ff5be7eee5bdf1812c3da767719de GIT binary patch literal 2479 zcmV;g2~hTlP)Px;X-PyuRCodHoxN`qM;ONAkXSMbgd$N$fCLSlC1k9Cl*Tla`3I1KMA}dw z3R;Z9pFlx@k`g46V1tAuB0?L8WYIw+3OfM-QWBEqclMgOmv{H}&igUv+gEzLGqbZZ z&-={o%-rmUN8X9swus(@Fd1OC!aIa*!Ut>Ht=hg2o(a#_wkz5$2@eB2QutpXN*jrc z72+QfX4WvJ?U+!My;MPWgqv%)r){*2BW{)3`>JtPxGMZ9{8u5Uk9w&e9z$zf0pAHv zg(@2$tuA=L2VS*U9o^J1d3r`T+=s`1>6qVzKZJ06V7y@52*;6~itiD&3%%NH9dKT_ z(d&VOke^0BF8m^-otZWr!LJA>g=yhap&#S8X@PPmk8*8YX>s_%yN;7q)fR6DRpg?O z=5Uh!T6yM$Q^J0sOrw12p+4$OIHYaij~@eTbPpGWe}ptANq->FZH)_c81+&=JhFJ= z2mbWa?dh=kjM>R!A4$uw6b|r{XrolI>`@-K=W(hUyJfb?61@BB^+DV^0Dh#5*8~*s=`G?>5 zABCL|i&uh2l&RsaC|GWs1b(DV&G8={YBlEWtnGLWF{kuFy&BR)#UK3g{6z=!sKvN+ zJyNWe9oC=RMib!<%KHUm-TDQfxyI-|W-Z ze#Junx*S{P?z1@xKbIp(@M%-{6UFxm+^y`( z5Z$&iO9G>Pl~&wAmXjARuD0GwLLYDG1U>t*gF4FI7-F$3hP5V7e4Eyn(*+)cPIhq- zJ<+u%qx)JEtb^b7R7!c7EiW%#UhVly_Lhv$wcGH}QG+)KF?Q75JtZVP@oid?x6Unk zp_}D~zPZio?_#9sCsm*t7xsdh;W;26$x?6(TgW}W7!^cwB^}e*I3N54WSXy{+3-;N_ckh4C?q>x#CC&d8UQqVA{z7J?*o-=-y$xF=SsC#^}^ zxB@>rMRe%@qcVh-4lk6%Y$4aQaLoMe3p?oFHu@{O)XN(_F&~As3CS14zR$ug=pfM($s(y3Qnxx_s@bsNbYYe_B&{%m2c>21H!3}e6o;OyW z0_bZWOlUX8XQob`jBBiX1+31y>83t3^~OG}HFO6yYIWX0V{9|-@a2KV%2&YZysc@B z@5BK<(>Jv-J}e%Z3|PThJxl*XZXz2`m?qnmqB_8J>)$|;c=0D zErgA+{uFxBP2tdG{shLm%3lTXCb)40&=Xxlv%0tU4bq!Sp)-RN&U|y}y}*;d6}}>z z3exzc$UA*M1!*w^&*mU;E=Qg!v+z!z{pF3`=jv{28^5RhtT_b^hmLocMHQwB}2R2_w=qg)ghU<=|=$y;= zwuNnwnBE7s+`M=$M=QvLZk8YV=CC7=E94DZP7DIi@_`} zFCGI=^s?O06@7cMgUZd9!@|ON#%fnjrIeF7-O6npcV4(542AJ%<0av#kerEiZs7qRcnz&FJ!E#mdkXgB_+>W^^-(YN*FH_H zqnnc7sm2>roSFQh2h8{Beu^@>vpd4gHQdvdHKiylw#Klc&^QyqWPsfY?+}u)fh`S& t(8(CWmQ~dyfjf~29w{7+_~O!G{{s{P*tCk1p7{U(002ovPDHLkV1f{v<+%U= literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconSubtle.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconSubtle.png new file mode 100644 index 0000000000000000000000000000000000000000..e54dcd354bcc5facdc9e7dce7c4c76bfc6108743 GIT binary patch literal 131 zcmeAS@N?(olHy`uVBq!ia0vp^qChOf!3HF^2u;-kQjEnx?oJHr&dIz4a%??a978Mw zlT#G<{o{`kRLUVOuZ0g*EEHi=5PQ2gu}d{TLqy6d@s!gNf9Z}8^CMFPb&f6g eC%Md3o`LBj)52(p`3XRS7(8A5T-G@yGywph=qDHe literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconSubtle@2x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconSubtle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cfd2ebd416fe182c63d3d0c756c66f4c8d732318 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^T0pGA!3HG%pIo60q!^2X+?^QKos)S9|U^NW1_5 literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconSubtle@3x.png b/Demo/Pods/SwiftMessages/SwiftMessages/Resources/warningIconSubtle@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..95715670940c9fca015c85f9a6481421d50f91c5 GIT binary patch literal 259 zcmeAS@N?(olHy`uVBq!ia0vp^_CRdL!3HD?0vyGG6k~CayA#8@b22Z1oV}hdjv*PW zZ)a`fZ7>ix!eD4I+xakq!u|<5bAA=ayb%$JoSuLE=Y!(c%`+!X%vEufy7X}Aqk^|* zl3Ok{obf*T@Yc-Z97`EzPVYLr)wo~Ki`htDv^_grPI(GjN}Lwo^>a3E8oVd=gvhL$ zTiFvJHKB5)MU3@liG?N#pMwf_lsz+a%5-=JGBAGjeZzg_jK_C>T&670$l;>EB*?&G zSN){A{LMqd38uTgUf;i6w(g5w=OH8gNv2VzpBG!WCW;q+WV(N`d;&MnBMhFdelF{r G5}E*XlxE2Q literal 0 HcmV?d00001 diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessages.Config+Extensions.swift b/Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessages.Config+Extensions.swift new file mode 100644 index 0000000..ac0475d --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessages.Config+Extensions.swift @@ -0,0 +1,44 @@ +// +// SwiftMessages.Config+Extensions.swift +// SwiftMessages +// +// Created by Timothy Moose on 12/26/20. +// Copyright © 2020 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +extension SwiftMessages.Config { + var windowLevel: UIWindow.Level? { + switch presentationContext { + case .window(let level): return level + case .windowScene(_, let level): return level + default: return nil + } + } + + @available (iOS 13.0, *) + var windowScene: UIWindowScene? { + switch presentationContext { + case .windowScene(let scene, _): return scene as? UIWindowScene + default: + #if SWIFTMESSAGES_APP_EXTENSIONS + return nil + #else + return UIWindow.keyWindow?.windowScene + #endif + } + } + + var shouldBecomeKeyWindow: Bool { + if let becomeKeyWindow = becomeKeyWindow { return becomeKeyWindow } + switch dimMode { + case .gray, .color, .blur: + // Should become key window in modal presentation style + // for proper VoiceOver handling. + return true + case .none: + return false + } + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessages.swift b/Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessages.swift new file mode 100644 index 0000000..5e9f0c3 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessages.swift @@ -0,0 +1,962 @@ +// +// SwiftMessages.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/1/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +private let globalInstance = SwiftMessages() + +/** + The `SwiftMessages` class provides the interface for showing and hiding messages. + It behaves like a queue, only showing one message at a time. Message views that + adopt the `Identifiable` protocol (as `MessageView` does) will have duplicates removed. + */ +open class SwiftMessages { + + /** + Specifies whether the message view is displayed at the top or bottom + of the selected presentation container. + */ + public enum PresentationStyle { + + /** + Message view slides down from the top. + */ + case top + + /** + Message view slides up from the bottom. + */ + case bottom + + /** + Message view fades into the center. + */ + case center + + /** + User-defined animation + */ + case custom(animator: Animator) + } + + /** + Specifies how the container for presenting the message view + is selected. + */ + public enum PresentationContext { + + /** + Displays the message view under navigation bars and tab bars if an + appropriate one is found. Otherwise, it is displayed in a new window + at level `UIWindow.Level.normal`. Use this option to automatically display + under bars, where applicable. Because this option involves a top-down + search, an appropriate context might not be found when the view controller + hierarchy incorporates custom containers. If this is the case, the + .ViewController option can provide a more targeted context. + */ + case automatic + + /** + Displays the message in a new window at the specified window level. + SwiftMessages automatically increases the top margins of any message + view that adopts the `MarginInsetting` protocol (as `MessageView` does) + to account for the status bar. As of iOS 13, windows can no longer cover the + status bar. The only alternative is to set `Config.prefersStatusBarHidden = true` + to hide it. + */ + case window(windowLevel: UIWindow.Level) + + /** + Displays the message in a new window, at the specified window level, + in the specified window scene. SwiftMessages automatically increases the top margins + of any message view that adopts the `MarginInsetting` protocol (as `MessageView` does) + to account for the status bar. As of iOS 13, windows can no longer cover the + status bar. The only alternative is to set `Config.prefersStatusBarHidden = true` + to hide it. The `WindowScene` protocol works around the change in Xcode 13 that prevents + using `@availability` attribute with `enum` cases containing associated values. + */ + case windowScene(_: WindowScene, windowLevel: UIWindow.Level) + + /** + Displays the message view under navigation bars and tab bars if an + appropriate one is found using the given view controller as a starting + point and searching up the parent view controller chain. Otherwise, it + is displayed in the given view controller's view. This option can be used + for targeted placement in a view controller hierarchy. + */ + case viewController(_: UIViewController) + + /** + Displays the message view in the given container view. + */ + case view(_: UIView) + } + + /** + Specifies the duration of the message view's time on screen before it is + automatically hidden. + */ + public enum Duration { + + /** + Hide the message view after the default duration. + */ + case automatic + + /** + Disables automatic hiding of the message view. + */ + case forever + + /** + Hide the message view after the speficied number of seconds. + + - Parameter seconds: The number of seconds. + */ + case seconds(seconds: TimeInterval) + + /** + The `indefinite` option is similar to `forever` in the sense that + the message view will not be automatically hidden. However, it + provides two options that can be useful in some scenarios: + + - `delay`: wait the specified time interval before displaying + the message. If you hide the message during the delay + interval by calling either `hideAll()` or `hide(id:)`, + the message will not be displayed. This is not the case for + `hide()` because it only acts on a visible message. Messages + shown during another message's delay window are displayed first. + - `minimum`: if the message is displayed, ensure that it is displayed + for a minimum time interval. If you explicitly hide the + during this interval, the message will be hidden at the + end of the interval. + + This option is useful for displaying a message when a process is taking + too long but you don't want to display the message if the process completes + in a reasonable amount of time. The value `indefinite(delay: 0, minimum: 0)` + is equivalent to `forever`. + + For example, if a URL load is expected to complete in 2 seconds, you may use + the value `indefinite(delay: 2, minimum 1)` to ensure that the message will not + be displayed in most cases, but will be displayed for at least 1 second if + the operation takes longer than 2 seconds. By specifying a minimum duration, + you can avoid hiding the message too fast if the operation finishes right + after the delay interval. + */ + case indefinite(delay: TimeInterval, minimum: TimeInterval) + } + + /** + Specifies options for dimming the background behind the message view + similar to a popover view controller. + */ + public enum DimMode { + + /** + Don't dim the background behind the message view. + */ + case none + + /** + Dim the background behind the message view a gray color. + + - `interactive`: Specifies whether or not tapping the + dimmed area dismisses the message view. + */ + case gray(interactive: Bool) + + /** + Dim the background behind the message view using the given color. + SwiftMessages does not apply alpha transparency to the color, so any alpha + must be baked into the `UIColor` instance. + + - `color`: The color of the dim view. + - `interactive`: Specifies whether or not tapping the + dimmed area dismisses the message view. + */ + case color(color: UIColor, interactive: Bool) + + /** + Dim the background behind the message view using a blur effect with + the given style + + - `style`: The blur effect style to use + - `alpha`: The alpha level of the blur + - `interactive`: Specifies whether or not tapping the + dimmed area dismisses the message view. + */ + case blur(style: UIBlurEffect.Style, alpha: CGFloat, interactive: Bool) + + public var interactive: Bool { + switch self { + case .gray(let interactive): + return interactive + case .color(_, let interactive): + return interactive + case .blur (_, _, let interactive): + return interactive + case .none: + return false + } + } + + public var modal: Bool { + switch self { + case .gray, .color, .blur: + return true + case .none: + return false + } + } + } + + /** + Specifies events in the message lifecycle. + */ + public enum Event { + case willShow(UIView) + case didShow(UIView) + case willHide(UIView) + case didHide(UIView) + + public var view: UIView { + switch self { + case .willShow(let view): return view + case .didShow(let view): return view + case .willHide(let view): return view + case .didHide(let view): return view + } + } + + public var id: String? { + return (view as? Identifiable)?.id + } + } + + /** + A closure that takes an `Event` as an argument. + */ + public typealias EventListener = (Event) -> Void + + /** + The `Config` struct specifies options for displaying a single message view. It is + provided as an optional argument to one of the `MessageView.show()` methods. + */ + public struct Config { + + public init() {} + + /** + Specifies whether the message view is displayed at the top or bottom + of the selected presentation container. The default is `.Top`. + */ + public var presentationStyle = PresentationStyle.top + + /** + Specifies how the container for presenting the message view + is selected. The default is `.Automatic`. + */ + public var presentationContext = PresentationContext.automatic { + didSet { + if case .windowScene = presentationContext { + guard #available(iOS 13.0, *) else { + assertionFailure("windowScene is not supported below iOS 13.0.") + return + } + } + } + } + + /** + Specifies the duration of the message view's time on screen before it is + automatically hidden. The default is `.Automatic`. + */ + public var duration = Duration.automatic + + /** + Specifies options for dimming the background behind the message view + similar to a popover view controller. The default is `.None`. + */ + public var dimMode = DimMode.none + + /** + Specifies whether or not the interactive pan-to-hide gesture is enabled + on the message view. For views that implement the `BackgroundViewable` + protocol (as `MessageView` does), the pan gesture recognizer is installed + in the `backgroundView`, which allows for card-style views with transparent + margins that shouldn't be interactive. Otherwise, it is installed in + the message view itself. The default is `true`. + */ + public var interactiveHide = true + + /** + Specifies the preferred status bar style when the view is being + displayed in a window. This can be useful when the view is being + displayed behind the status bar and the message view has a background + color that needs a different status bar style than the current one. + The default is `nil`. + */ + public var preferredStatusBarStyle: UIStatusBarStyle? + + /** + Specifies the preferred status bar visibility when the view is being + displayed in a window. As of iOS 13, windows can no longer cover the + status bar. The only alternative is to hide the status bar by setting + this options to `true`. Default is `nil`. + */ + public var prefersStatusBarHidden: Bool? + + /** + If a view controller is created to host the message view, should the view + controller auto rotate? The default is 'true', meaning it should auto + rotate. + */ + public var shouldAutorotate = true + + /** + Specified whether or not duplicate `Identifiable` messages are ignored. + The default is `true`. + */ + public var ignoreDuplicates = true + + /** + Specifies an optional array of event listeners. + */ + public var eventListeners: [EventListener] = [] + + /** + Specifies that in cases where the message is displayed in its own window, + such as with `.window` presentation context, the window should become + the key window. This option should only be used if the message view + needs to receive non-touch events, such as keyboard input. From Apple's + documentation https://developer.apple.com/reference/uikit/uiwindow: + + > Whereas touch events are delivered to the window where they occurred, + > events that do not have a relevant coordinate value are delivered to + > the key window. Only one window at a time can be the key window, and + > you can use a window’s keyWindow property to determine its status. + > Most of the time, your app’s main window is the key window, but UIKit + > may designate a different window as needed. + */ + public var becomeKeyWindow: Bool? + + /** + The `dimMode` background will use this accessibility + label, e.g. "dismiss" when the `interactive` option is used. + */ + public var dimModeAccessibilityLabel: String = "dismiss" + + /** + The user interface style to use when SwiftMessages displays a message its own window. + Use with apps that don't support dark mode to prevent messages from adopting the + system's interface style. + */ + @available(iOS 13, *) + public var overrideUserInterfaceStyle: UIUserInterfaceStyle { + // Note that this is modelled as a computed property because + // Swift doesn't allow `@available` with stored properties. + get { + guard let rawValue = overrideUserInterfaceStyleRawValue else { return .unspecified } + return UIUserInterfaceStyle(rawValue: rawValue) ?? .unspecified + } + set { + overrideUserInterfaceStyleRawValue = newValue.rawValue + } + } + private var overrideUserInterfaceStyleRawValue: Int? + + /** + If specified, SwiftMessages calls this closure when an instance of + `WindowViewController` is needed. Use this if you need to supply a custom subclass + of `WindowViewController`. + */ + public var windowViewController: ((_ config: SwiftMessages.Config) -> WindowViewController)? + + /** + Supply an instance of `KeyboardTrackingView` to have the message view avoid the keyboard. + */ + public var keyboardTrackingView: KeyboardTrackingView? + } + + /** + Not much to say here. + */ + public init() {} + + /** + Adds the given configuration and view to the message queue to be displayed. + + - Parameter config: The configuration options. + - Parameter view: The view to be displayed. + */ + open func show(config: Config, view: UIView) { + let presenter = Presenter(config: config, view: view, delegate: self) + messageQueue.sync { + enqueue(presenter: presenter) + } + } + + /** + Adds the given view to the message queue to be displayed + with default configuration options. + + - Parameter config: The configuration options. + - Parameter view: The view to be displayed. + */ + public func show(view: UIView) { + show(config: defaultConfig, view: view) + } + + /// A block that returns an arbitrary view. + public typealias ViewProvider = () -> UIView + + /** + Adds the given configuration and view provider to the message queue to be displayed. + + The `viewProvider` block is guaranteed to be called on the main queue where + it is safe to interact with `UIKit` components. This variant of `show()` is + recommended when the message might be added from a background queue. + + - Parameter config: The configuration options. + - Parameter viewProvider: A block that returns the view to be displayed. + */ + open func show(config: Config, viewProvider: @escaping ViewProvider) { + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return } + let view = viewProvider() + strongSelf.show(config: config, view: view) + } + } + + /** + Adds the given view provider to the message queue to be displayed + with default configuration options. + + The `viewProvider` block is guaranteed to be called on the main queue where + it is safe to interact with `UIKit` components. This variant of `show()` is + recommended when the message might be added from a background queue. + + - Parameter viewProvider: A block that returns the view to be displayed. + */ + public func show(viewProvider: @escaping ViewProvider) { + show(config: defaultConfig, viewProvider: viewProvider) + } + + /** + Hide the current message being displayed by animating it away. + */ + open func hide(animated: Bool = true) { + messageQueue.sync { + hideCurrent(animated: animated) + } + } + + /** + Hide the current message, if there is one, by animating it away and + clear the message queue. + */ + open func hideAll() { + messageQueue.sync { + queue.removeAll() + delays.removeAll() + counts.removeAll() + hideCurrent() + } + } + + /** + Hide a message with the given `id`. If the specified message is + currently being displayed, it will be animated away. Works with message + views, such as `MessageView`, that adopt the `Identifiable` protocol. + - Parameter id: The identifier of the message to remove. + */ + open func hide(id: String) { + messageQueue.sync { + if id == _current?.id { + hideCurrent() + } + queue = queue.filter { $0.id != id } + delays.remove(id: id) + counts[id] = nil + } + } + + /** + Hide the message when the number of calls to show() and hideCounted(id:) for a + given message ID are equal. This can be useful for messages that may be + shown from multiple code paths to ensure that all paths are ready to hide. + */ + open func hideCounted(id: String) { + messageQueue.sync { + if let count = counts[id] { + if count < 2 { + counts[id] = nil + } else { + counts[id] = count - 1 + return + } + } + if id == _current?.id { + hideCurrent() + } + queue = queue.filter { $0.id != id } + delays.remove(id: id) + } + } + + /** + Get the count of a message with the given ID (see `hideCounted(id:)`) + */ + public func count(id: String) -> Int { + return counts[id] ?? 0 + } + + /** + Explicitly set the count of a message with the given ID (see `hideCounted(id:)`). + Not sure if there's a use case for this, but why not?! + */ + public func set(count: Int, for id: String) { + guard counts[id] != nil else { return } + return counts[id] = count + } + + /** + Specifies the default configuration to use when calling the variants of + `show()` that don't take a `config` argument or as a base for custom configs. + */ + public var defaultConfig = Config() + + /** + Specifies the amount of time to pause between removing a message + and showing the next. Default is 0.5 seconds. + */ + open var pauseBetweenMessages: TimeInterval = 0.5 + + /// Type for keeping track of delayed presentations + fileprivate class Delays { + + fileprivate func add(presenter: Presenter) { + presenters.insert(presenter) + } + + @discardableResult + fileprivate func remove(presenter: Presenter) -> Bool { + guard presenters.contains(presenter) else { return false } + presenters.remove(presenter) + return true + } + + fileprivate func remove(id: String) { + presenters = presenters.filter { $0.id != id } + } + + fileprivate func removeAll() { + presenters.removeAll() + } + + private var presenters = Set() + } + + func show(presenter: Presenter) { + messageQueue.sync { + enqueue(presenter: presenter) + } + } + + fileprivate let messageQueue = DispatchQueue(label: "it.swiftkick.SwiftMessages", attributes: []) + fileprivate var queue: [Presenter] = [] + fileprivate var delays = Delays() + fileprivate var counts: [String : Int] = [:] + fileprivate var _current: Presenter? = nil { + didSet { + if oldValue != nil { + let delayTime = DispatchTime.now() + pauseBetweenMessages + messageQueue.asyncAfter(deadline: delayTime) { [weak self] in + self?.dequeueNext() + } + } + } + } + + fileprivate func enqueue(presenter: Presenter) { + if presenter.config.ignoreDuplicates { + counts[presenter.id] = (counts[presenter.id] ?? 0) + 1 + if _current?.id == presenter.id && _current?.isHiding == false { return } + if queue.filter({ $0.id == presenter.id }).count > 0 { return } + } + func doEnqueue() { + queue.append(presenter) + dequeueNext() + } + if let delay = presenter.delayShow { + delays.add(presenter: presenter) + messageQueue.asyncAfter(deadline: .now() + delay) { [weak self] in + // Don't enqueue if the view has been hidden during the delay window. + guard let strongSelf = self, strongSelf.delays.remove(presenter: presenter) else { return } + doEnqueue() + } + } else { + doEnqueue() + } + } + + fileprivate func dequeueNext() { + guard self._current == nil, queue.count > 0 else { return } + let current = queue.removeFirst() + self._current = current + // Set `autohideToken` before the animation starts in case + // the dismiss gesture begins before we've queued the autohide + // block on animation completion. + self.autohideToken = current + current.showDate = CACurrentMediaTime() + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return } + do { + try current.show { completed in + guard let strongSelf = self else { return } + guard completed else { + strongSelf.messageQueue.sync { + strongSelf.internalHide(presenter: current) + } + return + } + if current === strongSelf.autohideToken { + strongSelf.queueAutoHide() + } + } + } catch { + strongSelf.messageQueue.sync { + strongSelf._current = nil + } + } + } + } + + fileprivate func internalHide(presenter: Presenter) { + if presenter == _current { + hideCurrent() + } else { + queue = queue.filter { $0 != presenter } + delays.remove(presenter: presenter) + } + } + + fileprivate func hideCurrent(animated: Bool = true) { + guard let current = _current, !current.isHiding else { return } + let action = { [weak self] in + current.hide(animated: animated) { (completed) in + guard completed, let strongSelf = self else { return } + strongSelf.messageQueue.sync { + guard strongSelf._current === current else { return } + strongSelf.counts[current.id] = nil + strongSelf._current = nil + } + } + } + let delay = current.delayHide ?? 0 + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + action() + } + } + + fileprivate weak var autohideToken: AnyObject? + + fileprivate func queueAutoHide() { + guard let current = _current else { return } + autohideToken = current + if let pauseDuration = current.pauseDuration { + let delayTime = DispatchTime.now() + pauseDuration + messageQueue.asyncAfter(deadline: delayTime, execute: { + // Make sure we've still got a green light to auto-hide. + if self.autohideToken !== current { return } + self.internalHide(presenter: current) + }) + } + } + + deinit { + // Prevent orphaned messages + hideCurrent() + } +} + +/* + MARK: - Accessing messages + */ + +extension SwiftMessages { + + /** + Returns the message view of type `T` if it is currently being shown or hidden. + + - Returns: The view of type `T` if it is currently being shown or hidden. + */ + public func current() -> T? { + var view: T? + messageQueue.sync { + view = _current?.view as? T + } + return view + } + + /** + Returns a message view with the given `id` if it is currently being shown or hidden. + + - Parameter id: The id of a message that adopts `Identifiable`. + - Returns: The view with matching id if currently being shown or hidden. + */ + public func current(id: String) -> T? { + var view: T? + messageQueue.sync { + if let current = _current, current.id == id { + view = current.view as? T + } + } + return view + } + + /** + Returns a message view with the given `id` if it is currently in the queue to be shown. + + - Parameter id: The id of a message that adopts `Identifiable`. + - Returns: The view with matching id if currently queued to be shown. + */ + public func queued(id: String) -> T? { + var view: T? + messageQueue.sync { + if let queued = queue.first(where: { $0.id == id }) { + view = queued.view as? T + } + } + return view + } + + /** + Returns a message view with the given `id` if it is currently being + shown, hidden or in the queue to be shown. + + - Parameter id: The id of a message that adopts `Identifiable`. + - Returns: The view with matching id if currently queued to be shown. + */ + public func currentOrQueued(id: String) -> T? { + return current(id: id) ?? queued(id: id) + } +} + +/* + MARK: - PresenterDelegate + */ + +extension SwiftMessages: PresenterDelegate { + + func hide(presenter: Presenter) { + messageQueue.sync { + self.internalHide(presenter: presenter) + } + } + + public func hide(animator: Animator) { + messageQueue.sync { + guard let presenter = self.presenter(forAnimator: animator) else { return } + self.internalHide(presenter: presenter) + } + } + + public func panStarted(animator: Animator) { + autohideToken = nil + } + + public func panEnded(animator: Animator) { + queueAutoHide() + } + + private func presenter(forAnimator animator: Animator) -> Presenter? { + if let current = _current, animator === current.animator { + return current + } + let queued = queue.filter { $0.animator === animator } + return queued.first + } +} + +/** + MARK: - Creating views from nibs + + This extension provides several convenience functions for instantiating views from nib files. + SwiftMessages provides several default nib files in the Resources folder that can be + drag-and-dropped into a project as a starting point and modified. + */ + +extension SwiftMessages { + + /** + Loads a nib file with the same name as the generic view type `T` and returns + the first view found in the nib file with matching type `T`. For example, if + the generic type is `MyView`, a nib file named `MyView.nib` is loaded and the + first top-level view of type `MyView` is returned. The main bundle is searched + first followed by the SwiftMessages bundle. + + - Parameter filesOwner: An optional files owner. + + - Throws: `Error.CannotLoadViewFromNib` if a view matching the + generic type `T` is not found in the nib. + + - Returns: An instance of generic view type `T`. + */ + public class func viewFromNib(_ filesOwner: AnyObject = NSNull.init()) throws -> T { + let name = T.description().components(separatedBy: ".").last + assert(name != nil) + let view: T = try internalViewFromNib(named: name!, bundle: nil, filesOwner: filesOwner) + return view + } + + /** + Loads a nib file with specified name and returns the first view found in the nib file + with matching type `T`. The main bundle is searched first followed by the SwiftMessages bundle. + + - Parameter name: The name of the nib file (excluding the .xib extension). + - Parameter filesOwner: An optional files owner. + + - Throws: `Error.CannotLoadViewFromNib` if a view matching the + generic type `T` is not found in the nib. + + - Returns: An instance of generic view type `T`. + */ + public class func viewFromNib(named name: String, filesOwner: AnyObject = NSNull.init()) throws -> T { + let view: T = try internalViewFromNib(named: name, bundle: nil, filesOwner: filesOwner) + return view + } + + /** + Loads a nib file with specified name in the specified bundle and returns the + first view found in the nib file with matching type `T`. + + - Parameter name: The name of the nib file (excluding the .xib extension). + - Parameter bundle: The name of the bundle containing the nib file. + - Parameter filesOwner: An optional files owner. + + - Throws: `Error.CannotLoadViewFromNib` if a view matching the + generic type `T` is not found in the nib. + + - Returns: An instance of generic view type `T`. + */ + public class func viewFromNib(named name: String, bundle: Bundle, filesOwner: AnyObject = NSNull.init()) throws -> T { + let view: T = try internalViewFromNib(named: name, bundle: bundle, filesOwner: filesOwner) + return view + } + + fileprivate class func internalViewFromNib(named name: String, bundle: Bundle? = nil, filesOwner: AnyObject = NSNull.init()) throws -> T { + let resolvedBundle: Bundle + if let bundle = bundle { + resolvedBundle = bundle + } else { + if Bundle.main.path(forResource: name, ofType: "nib") != nil { + resolvedBundle = Bundle.main + } else { + resolvedBundle = Bundle.sm_frameworkBundle() + } + } + let arrayOfViews = resolvedBundle.loadNibNamed(name, owner: filesOwner, options: nil) ?? [] + #if swift(>=4.1) + guard let view = arrayOfViews.compactMap( { $0 as? T} ).first else { throw SwiftMessagesError.cannotLoadViewFromNib(nibName: name) } + #else + guard let view = arrayOfViews.flatMap( { $0 as? T} ).first else { throw SwiftMessagesError.cannotLoadViewFromNib(nibName: name) } + #endif + return view + } +} + +/* + MARK: - Static APIs + + This extension provides a shared instance of `SwiftMessages` and a static API wrapper around + this instance for simplified syntax. For example, `SwiftMessages.show()` is equivalent + to `SwiftMessages.sharedInstance.show()`. + */ + +extension SwiftMessages { + + /** + A default shared instance of `SwiftMessages`. The `SwiftMessages` class provides + a set of static APIs that wrap calls to this instance. For example, `SwiftMessages.show()` + is equivalent to `SwiftMessages.sharedInstance.show()`. + */ + public static var sharedInstance: SwiftMessages { + return globalInstance + } + + public static func show(viewProvider: @escaping ViewProvider) { + globalInstance.show(viewProvider: viewProvider) + } + + public static func show(config: Config, viewProvider: @escaping ViewProvider) { + globalInstance.show(config: config, viewProvider: viewProvider) + } + + public static func show(view: UIView) { + globalInstance.show(view: view) + } + + public static func show(config: Config, view: UIView) { + globalInstance.show(config: config, view: view) + } + + public static func hide(animated: Bool = true) { + globalInstance.hide(animated: animated) + } + + public static func hideAll() { + globalInstance.hideAll() + } + + public static func hide(id: String) { + globalInstance.hide(id: id) + } + + public static func hideCounted(id: String) { + globalInstance.hideCounted(id: id) + } + + public static var defaultConfig: Config { + get { + return globalInstance.defaultConfig + } + set { + globalInstance.defaultConfig = newValue + } + } + + public static var pauseBetweenMessages: TimeInterval { + get { + return globalInstance.pauseBetweenMessages + } + set { + globalInstance.pauseBetweenMessages = newValue + } + } + + public static func current(id: String) -> T? { + return globalInstance.current(id: id) + } + + public static func queued(id: String) -> T? { + return globalInstance.queued(id: id) + } + + public static func currentOrQueued(id: String) -> T? { + return globalInstance.currentOrQueued(id: id) + } + + public static func count(id: String) -> Int { + return globalInstance.count(id: id) + } + + public static func set(count: Int, for id: String) { + globalInstance.set(count: count, for: id) + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessagesSegue.swift b/Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessagesSegue.swift new file mode 100644 index 0000000..d4cba41 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/SwiftMessagesSegue.swift @@ -0,0 +1,399 @@ +// +// SwiftMessagesSegue.swift +// SwiftMessages +// +// Created by Timothy Moose on 5/30/18. +// Copyright © 2018 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +/** + `SwiftMessagesSegue` is a configurable subclass of `UIStoryboardSegue` that utilizes + SwiftMessages to present and dismiss modal view controllers. It performs these transitions by + becoming your view controller's `transitioningDelegate` and calling SwiftMessage's `show()` + and `hide()` under the hood. + + To use `SwiftMessagesSegue` with Interface Builder, control-drag a segue, then select + "swift messages" from the Segue Type dialog. This configures a default transition. There are + two suggested ways to further configure the transition by setting options on `SwiftMessagesSegue`. + First, and recommended, you may subclass `SwiftMessagesSegue` and override `init(identifier:source:destination:)`. + Subclasses will automatically appear in the segue type dialog using an auto-generated name (for example, the + name for "VeryNiceSegue" would be "very nice"). Second, you may override `prepare(for:sender:)` in the + presenting view controller and downcast the segue to `SwiftMessagesSegue`. + + `SwiftMessagesSegue` can be used without an associated storyboard or segue by doing the following in + the presenting view controller. + + let destinationVC = ... // make a reference to a destination view controller + let segue = SwiftMessagesSegue(identifier: nil, source: self, destination: destinationVC) + ... // do any configuration here + segue.perform() + + To dismiss, call the UIKit API on the presenting view controller: + + dismiss(animated: true, completion: nil) + + It is not necessary to retain `segue` because it retains itself until dismissal. However, you can + retain it if you plan to `perform()` more than once. + + #### Present the controller on top of all controllers + + If you don't know the presenter or you don't want to pass it as a source, like when you + have a completely separated message controller, you can pass a `WindowViewController` + as the `source` argument of the segue's initializer. + + By default, the window will be shown in the current window scene at `.normal` window level. + However, these parameters can be customized by initializing the view controller with a `SwiftMessages.Config` that has the `SwiftMessages.Config.presentationContext` set to either `.window` or `.windowScene`: + + + note: Some additional details: + 1. Your view controller's view will be embedded in a `SwiftMessages.BaseView` in order to + utilize some SwiftMessages features. This view can be accessed and configured via the + `SwiftMessagesSegue.messageView` property. For example, you may configure a default drop + shadow by calling `segue.messageView.configureDropShadow()`. + 2. SwiftMessagesSegue provides static default view controller sizing based on device. + However, it is recommended that you specify sizing appropriate for your content using + one of the following methods. + 1. Define sufficient width and height constraints in your view controller. + 2. Set `preferredContentSize` (a.k.a "Use Preferred Explicit Size" in Interface Builder's + attribute inspector). Zeros are ignored, e.g. `CGSize(width: 0, height: 350)` only + affects the height. + 3. Add explicit width and/or height constraints to `segue.messageView.backgroundView`. + Note that `Layout.topMessage` and `Layout.bottomMessage` are always full screen width. + For other layouts, the there is a maximum 500pt width on iPad (regular horizontal size class) + at 950 priority, which can be overridden by adding higher-priority constraints. + + See the "View Controllers" selection in the Demo app for examples. + */ + +open class SwiftMessagesSegue: UIStoryboardSegue { + + /** + Specifies one of the pre-defined layouts, mirroring a subset of `MessageView.Layout`. + */ + public enum Layout { + + /// The standard message view layout on top. + case topMessage + + /// The standard message view layout on bottom. + case bottomMessage + + /// A floating card-style view with rounded corners on top + case topCard + + /// A floating tab-style view with rounded corners on bottom + case topTab + + /// A floating card-style view with rounded corners on bottom + case bottomCard + + /// A floating tab-style view with rounded corners on top + case bottomTab + + /// A floating card-style view typically used with `.center` presentation style. + case centered + } + + /** + Specifies how the view controller's view is installed into the + containing message view. + */ + public enum Containment { + + /** + The view controller's view is installed for edge-to-edge display, extending into the safe areas + to the device edges. This is done by calling `messageView.installContentView(:insets:)` + See that method's documentation for additional details. + */ + case content + + /** + The view controller's view is installed for card-style layouts, inset from the margins + and avoiding safe areas. This is done by calling `messageView.installBackgroundView(:insets:)`. + See that method's documentation for details. + */ + case background + + /** + The view controller's view is installed for tab-style layouts, inset from the side margins, but extending + to the device edge on the top or bottom. This is done by calling `messageView.installBackgroundVerticalView(:insets:)`. + See that method's documentation for details. + */ + case backgroundVertical + } + + /// The presentation style to use. See the SwiftMessages.PresentationStyle for details. + public var presentationStyle: SwiftMessages.PresentationStyle { + get { return messenger.defaultConfig.presentationStyle } + set { messenger.defaultConfig.presentationStyle = newValue } + } + + /// The dim mode to use. See the SwiftMessages.DimMode for details. + public var dimMode: SwiftMessages.DimMode { + get { return messenger.defaultConfig.dimMode} + set { messenger.defaultConfig.dimMode = newValue } + } + + // duration + public var duration: SwiftMessages.Duration { + get { return messenger.defaultConfig.duration} + set { messenger.defaultConfig.duration = newValue } + } + + /// Specifies whether or not the interactive pan-to-hide gesture is enabled + /// on the message view. The default value is `true`, but may not be appropriate + /// for view controllers that use swipe or pan gestures. + public var interactiveHide: Bool { + get { return messenger.defaultConfig.interactiveHide } + set { messenger.defaultConfig.interactiveHide = newValue } + } + + /// Specifies an optional array of event listeners. + public var eventListeners: [SwiftMessages.EventListener] { + get { return messenger.defaultConfig.eventListeners } + set { messenger.defaultConfig.eventListeners = newValue } + } + + /** + Normally, the destination view controller's `modalPresentationStyle` is changed + to `.custom` in the `perform()` function. Set this property to `false` to prevent it from + being overridden. + */ + public var overrideModalPresentationStyle: Bool = true + + /** + The view that is passed to `SwiftMessages.show(config:view:)` during presentation. + The view controller's view is installed into `containerView`, which is itself installed + into `messageView`. `SwiftMessagesSegue` does this installation automatically based on the + value of the `containment` property. `BaseView` is the parent of `MessageView` and provides a + number of configuration options that you may use. For example, you may configure a default drop + shadow by calling `messageView.configureDropShadow()`. + */ + public var messageView = BaseView() + + /** + The view controller's view is embedded in `containerView` before being installed into + `messageView`. This view provides configurable squircle (round) corners (see the parent + class `CornerRoundingView`). + */ + public var containerView: CornerRoundingView = CornerRoundingView() + + /** + Specifies how the view controller's view is installed into the + containing message view. See `Containment` for details. + */ + public var containment: Containment = .content + + /** + Supply an instance of `KeyboardTrackingView` to have the message view avoid the keyboard. + */ + public var keyboardTrackingView: KeyboardTrackingView? { + get { + return messenger.defaultConfig.keyboardTrackingView + } + set { + messenger.defaultConfig.keyboardTrackingView = newValue + } + } + + private var messenger = SwiftMessages() + private var selfRetainer: SwiftMessagesSegue? = nil + private lazy var hider = { return TransitioningDismisser(segue: self) }() + + private lazy var presenter = { + return Presenter(config: messenger.defaultConfig, view: messageView, delegate: messenger) + }() + + override open func perform() { + (source as? WindowViewController)?.install() + selfRetainer = self + if overrideModalPresentationStyle { + destination.modalPresentationStyle = .custom + } + destination.transitioningDelegate = self + source.present(destination, animated: true, completion: nil) + } + + override public init(identifier: String?, source: UIViewController, destination: UIViewController) { + super.init(identifier: identifier, source: source, destination: destination) + dimMode = .gray(interactive: true) + messenger.defaultConfig.duration = .forever + } + + fileprivate let safeAreaWorkaroundViewController = UIViewController() +} + +extension SwiftMessagesSegue { + /// A convenience method for configuring some pre-defined layouts that mirror a subset of `MessageView.Layout`. + public func configure(layout: Layout) { + messageView.bounceAnimationOffset = 0 + containment = .content + containerView.cornerRadius = 0 + containerView.roundsLeadingCorners = false + messageView.configureDropShadow() + switch layout { + case .topMessage: + messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + messageView.collapseLayoutMarginAdditions = false + let animation = TopBottomAnimation(style: .top) + animation.springDamping = 1 + presentationStyle = .custom(animator: animation) + case .bottomMessage: + messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 20, bottom: 20, right: 20) + messageView.collapseLayoutMarginAdditions = false + let animation = TopBottomAnimation(style: .bottom) + animation.springDamping = 1 + presentationStyle = .custom(animator: animation) + case .topCard: + containment = .background + messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + messageView.collapseLayoutMarginAdditions = true + containerView.cornerRadius = 15 + presentationStyle = .top + case .bottomCard: + containment = .background + messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + messageView.collapseLayoutMarginAdditions = true + containerView.cornerRadius = 15 + presentationStyle = .bottom + case .topTab: + containment = .backgroundVertical + messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10) + messageView.collapseLayoutMarginAdditions = true + containerView.cornerRadius = 15 + containerView.roundsLeadingCorners = true + let animation = TopBottomAnimation(style: .top) + animation.springDamping = 1 + presentationStyle = .custom(animator: animation) + case .bottomTab: + containment = .backgroundVertical + messageView.layoutMarginAdditions = UIEdgeInsets(top: 20, left: 10, bottom: 20, right: 10) + messageView.collapseLayoutMarginAdditions = true + containerView.cornerRadius = 15 + containerView.roundsLeadingCorners = true + let animation = TopBottomAnimation(style: .bottom) + animation.springDamping = 1 + presentationStyle = .custom(animator: animation) + case .centered: + containment = .background + messageView.layoutMarginAdditions = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10) + messageView.collapseLayoutMarginAdditions = true + containerView.cornerRadius = 15 + presentationStyle = .center + } + } +} + +extension SwiftMessagesSegue: UIViewControllerTransitioningDelegate { + public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { + let shower = TransitioningPresenter(segue: self) + messenger.defaultConfig.eventListeners.append { [unowned self] in + switch $0 { + case .didShow: + shower.completeTransition?(true) + case .didHide: + if let completeTransition = self.hider.completeTransition { + completeTransition(true) + } else { + // Case where message is internally hidden by SwiftMessages, such as with a + // dismiss gesture, rather than by view controller dismissal. + source.dismiss(animated: false, completion: nil) + } + (source as? WindowViewController)?.uninstall() + self.selfRetainer = nil + default: break + } + } + return shower + } + + public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { + return hider + } +} + +extension SwiftMessagesSegue { + private class TransitioningPresenter: NSObject, UIViewControllerAnimatedTransitioning { + + fileprivate private(set) var completeTransition: ((Bool) -> Void)? + private weak var segue: SwiftMessagesSegue? + + fileprivate init(segue: SwiftMessagesSegue) { + self.segue = segue + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return segue?.presenter.animator.showDuration ?? 0.5 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let segue = segue, + let toView = transitionContext.view(forKey: .to) else { + transitionContext.completeTransition(false) + return + } + if #available(iOS 12, *) {} + else if #available(iOS 11.0, *) { + // This works around a bug in iOS 11 where the safe area of `messageView` ( + // and all ancestor views) is not set except on iPhone X. By assigning `messageView` + // to a view controller, its safe area is set consistently. This bug has been resolved as + // of Xcode 10 beta 2. + segue.safeAreaWorkaroundViewController.view = segue.presenter.maskingView + } + completeTransition = transitionContext.completeTransition + let transitionContainer = transitionContext.containerView + toView.translatesAutoresizingMaskIntoConstraints = false + segue.containerView.addSubview(toView) + segue.containerView.topAnchor.constraint(equalTo: toView.topAnchor).isActive = true + segue.containerView.bottomAnchor.constraint(equalTo: toView.bottomAnchor).isActive = true + segue.containerView.leadingAnchor.constraint(equalTo: toView.leadingAnchor).isActive = true + segue.containerView.trailingAnchor.constraint(equalTo: toView.trailingAnchor).isActive = true + // Install the `toView` into the message view. + switch segue.containment { + case .content: + segue.messageView.installContentView(segue.containerView) + case .background: + segue.messageView.installBackgroundView(segue.containerView) + case .backgroundVertical: + segue.messageView.installBackgroundVerticalView(segue.containerView) + } + let toVC = transitionContext.viewController(forKey: .to) + if let preferredHeight = toVC?.preferredContentSize.height, + preferredHeight > 0 { + segue.containerView.heightAnchor.constraint(equalToConstant: preferredHeight).with(priority: UILayoutPriority(rawValue: 951)).isActive = true + } + if let preferredWidth = toVC?.preferredContentSize.width, + preferredWidth > 0 { + segue.containerView.widthAnchor.constraint(equalToConstant: preferredWidth).with(priority: UILayoutPriority(rawValue: 951)).isActive = true + } + segue.presenter.config.presentationContext = .view(transitionContainer) + segue.messenger.show(presenter: segue.presenter) + } + } +} + +extension SwiftMessagesSegue { + private class TransitioningDismisser: NSObject, UIViewControllerAnimatedTransitioning { + + fileprivate private(set) var completeTransition: ((Bool) -> Void)? + private weak var segue: SwiftMessagesSegue? + + fileprivate init(segue: SwiftMessagesSegue) { + self.segue = segue + } + + func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { + return segue?.presenter.animator.hideDuration ?? 0.5 + } + + func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { + guard let messenger = segue?.messenger else { + transitionContext.completeTransition(false) + return + } + completeTransition = transitionContext.completeTransition + messenger.hide() + } + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Theme.swift b/Demo/Pods/SwiftMessages/SwiftMessages/Theme.swift new file mode 100644 index 0000000..5bf10a4 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Theme.swift @@ -0,0 +1,67 @@ +// +// Theme.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/7/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +/// The theme enum specifies the built-in theme options +public enum Theme { + case info + case success + case warning + case error +} + +/// The Icon enum provides type-safe access to the included icons. +public enum Icon: String { + + case error = "errorIcon" + case warning = "warningIcon" + case success = "successIcon" + case info = "infoIcon" + case errorLight = "errorIconLight" + case warningLight = "warningIconLight" + case successLight = "successIconLight" + case infoLight = "infoIconLight" + case errorSubtle = "errorIconSubtle" + case warningSubtle = "warningIconSubtle" + case successSubtle = "successIconSubtle" + case infoSubtle = "infoIconSubtle" + + /// Returns the associated image. + public var image: UIImage { + return UIImage(named: rawValue, in: Bundle.sm_frameworkBundle(), compatibleWith: nil)!.withRenderingMode(.alwaysTemplate) + } +} + +/// The IconStyle enum specifies the different variations of the included icons. +public enum IconStyle { + + case `default` + case light + case subtle + case none + + /// Returns the image for the given theme + public func image(theme: Theme) -> UIImage? { + switch (theme, self) { + case (.info, .default): return Icon.info.image + case (.info, .light): return Icon.infoLight.image + case (.info, .subtle): return Icon.infoSubtle.image + case (.success, .default): return Icon.success.image + case (.success, .light): return Icon.successLight.image + case (.success, .subtle): return Icon.successSubtle.image + case (.warning, .default): return Icon.warning.image + case (.warning, .light): return Icon.warningLight.image + case (.warning, .subtle): return Icon.warningSubtle.image + case (.error, .default): return Icon.error.image + case (.error, .light): return Icon.errorLight.image + case (.error, .subtle): return Icon.errorSubtle.image + default: return nil + } + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/TopBottomAnimation.swift b/Demo/Pods/SwiftMessages/SwiftMessages/TopBottomAnimation.swift new file mode 100644 index 0000000..21b0f27 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/TopBottomAnimation.swift @@ -0,0 +1,233 @@ +// +// TopBottomAnimation.swift +// SwiftMessages +// +// Created by Timothy Moose on 6/4/17. +// Copyright © 2017 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +public class TopBottomAnimation: NSObject, Animator { + + public enum Style { + case top + case bottom + } + + public weak var delegate: AnimationDelegate? + + public let style: Style + + public var showDuration: TimeInterval = 0.4 + + public var hideDuration: TimeInterval = 0.2 + + public var springDamping: CGFloat = 0.8 + + public var closeSpeedThreshold: CGFloat = 750.0; + + public var closePercentThreshold: CGFloat = 0.33; + + public var closeAbsoluteThreshold: CGFloat = 75.0; + + public private(set) lazy var panGestureRecognizer: UIPanGestureRecognizer = { + let pan = UIPanGestureRecognizer() + pan.addTarget(self, action: #selector(pan(_:))) + return pan + }() + + weak var messageView: UIView? + weak var containerView: UIView? + var context: AnimationContext? + + public init(style: Style) { + self.style = style + } + + init(style: Style, delegate: AnimationDelegate) { + self.style = style + self.delegate = delegate + } + + public func show(context: AnimationContext, completion: @escaping AnimationCompletion) { + NotificationCenter.default.addObserver(self, selector: #selector(adjustMargins), name: UIDevice.orientationDidChangeNotification, object: nil) + install(context: context) + showAnimation(completion: completion) + } + + public func hide(context: AnimationContext, completion: @escaping AnimationCompletion) { + NotificationCenter.default.removeObserver(self) + let view = context.messageView + self.context = context + UIView.animate(withDuration: hideDuration, delay: 0, options: [.beginFromCurrentState, .curveEaseIn], animations: { + switch self.style { + case .top: + view.transform = CGAffineTransform(translationX: 0, y: -view.frame.height) + case .bottom: + view.transform = CGAffineTransform(translationX: 0, y: view.frame.maxY + view.frame.height) + } + }, completion: { completed in + #if SWIFTMESSAGES_APP_EXTENSIONS + completion(completed) + #else + // Fix #131 by always completing if application isn't active. + completion(completed || UIApplication.shared.applicationState != .active) + #endif + }) + } + + func install(context: AnimationContext) { + let view = context.messageView + let container = context.containerView + messageView = view + containerView = container + self.context = context + if let adjustable = context.messageView as? MarginAdjustable { + bounceOffset = adjustable.bounceAnimationOffset + } + view.translatesAutoresizingMaskIntoConstraints = false + container.addSubview(view) + view.leadingAnchor.constraint(equalTo: container.leadingAnchor).isActive = true + view.trailingAnchor.constraint(equalTo: container.trailingAnchor).isActive = true + switch style { + case .top: + view.topAnchor.constraint(equalTo: container.topAnchor, constant: -bounceOffset).with(priority: UILayoutPriority(200)).isActive = true + case .bottom: + view.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: bounceOffset).with(priority: UILayoutPriority(200)).isActive = true + } + // Important to layout now in order to get the right safe area insets + container.layoutIfNeeded() + adjustMargins() + container.layoutIfNeeded() + let animationDistance = view.frame.height + switch style { + case .top: + view.transform = CGAffineTransform(translationX: 0, y: -animationDistance) + case .bottom: + view.transform = CGAffineTransform(translationX: 0, y: animationDistance) + } + if context.interactiveHide { + if let view = view as? BackgroundViewable { + view.backgroundView.addGestureRecognizer(panGestureRecognizer) + } else { + view.addGestureRecognizer(panGestureRecognizer) + } + } + if let view = view as? BackgroundViewable, + let cornerRoundingView = view.backgroundView as? CornerRoundingView, + cornerRoundingView.roundsLeadingCorners { + switch style { + case .top: + cornerRoundingView.roundedCorners = [.bottomLeft, .bottomRight] + case .bottom: + cornerRoundingView.roundedCorners = [.topLeft, .topRight] + } + } + } + + @objc public func adjustMargins() { + guard let adjustable = messageView as? MarginAdjustable & UIView, + let context = context else { return } + adjustable.preservesSuperviewLayoutMargins = false + if #available(iOS 11, *) { + adjustable.insetsLayoutMarginsFromSafeArea = false + } + var layoutMargins = adjustable.defaultMarginAdjustment(context: context) + switch style { + case .top: + layoutMargins.top += bounceOffset + case .bottom: + layoutMargins.bottom += bounceOffset + } + adjustable.layoutMargins = layoutMargins + } + + func showAnimation(completion: @escaping AnimationCompletion) { + guard let view = messageView else { + completion(false) + return + } + let animationDistance = abs(view.transform.ty) + // Cap the initial velocity at zero because the bounceOffset may not be great + // enough to allow for greater bounce induced by a quick panning motion. + let initialSpringVelocity = animationDistance == 0.0 ? 0.0 : min(0.0, closeSpeed / animationDistance) + UIView.animate(withDuration: showDuration, delay: 0.0, usingSpringWithDamping: springDamping, initialSpringVelocity: initialSpringVelocity, options: [.beginFromCurrentState, .curveLinear, .allowUserInteraction], animations: { + view.transform = .identity + }, completion: { completed in + // Fix #131 by always completing if application isn't active. + #if SWIFTMESSAGES_APP_EXTENSIONS + completion(completed) + #else + completion(completed || UIApplication.shared.applicationState != .active) + #endif + }) + } + + fileprivate var bounceOffset: CGFloat = 5 + + /* + MARK: - Pan to close + */ + + fileprivate var closing = false + fileprivate var rubberBanding = false + fileprivate var closeSpeed: CGFloat = 0.0 + fileprivate var closePercent: CGFloat = 0.0 + fileprivate var panTranslationY: CGFloat = 0.0 + + @objc func pan(_ pan: UIPanGestureRecognizer) { + switch pan.state { + case .changed: + guard let view = messageView else { return } + let height = view.bounds.height - bounceOffset + if height <= 0 { return } + var velocity = pan.velocity(in: view) + var translation = pan.translation(in: view) + if case .top = style { + velocity.y *= -1.0 + translation.y *= -1.0 + } + var translationAmount = translation.y >= 0 ? translation.y : -pow(abs(translation.y), 0.7) + if !closing { + // Turn on rubber banding if background view is inset from message view. + if let background = (messageView as? BackgroundViewable)?.backgroundView, background != view { + switch style { + case .top: + rubberBanding = background.frame.minY > 0 + case .bottom: + rubberBanding = background.frame.maxY < view.bounds.height + } + } + if !rubberBanding && translationAmount < 0 { return } + closing = true + delegate?.panStarted(animator: self) + } + if !rubberBanding && translationAmount < 0 { translationAmount = 0 } + switch style { + case .top: + view.transform = CGAffineTransform(translationX: 0, y: -translationAmount) + case .bottom: + view.transform = CGAffineTransform(translationX: 0, y: translationAmount) + } + closeSpeed = velocity.y + closePercent = translation.y / height + panTranslationY = translation.y + case .ended, .cancelled: + if closeSpeed > closeSpeedThreshold || closePercent > closePercentThreshold || panTranslationY > closeAbsoluteThreshold { + delegate?.hide(animator: self) + } else { + closing = false + rubberBanding = false + closeSpeed = 0.0 + closePercent = 0.0 + panTranslationY = 0.0 + showAnimation(completion: { (completed) in + self.delegate?.panEnded(animator: self) + }) + } + default: + break + } + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/UIEdgeInsets+Extensions.swift b/Demo/Pods/SwiftMessages/SwiftMessages/UIEdgeInsets+Extensions.swift new file mode 100644 index 0000000..19bbe4e --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/UIEdgeInsets+Extensions.swift @@ -0,0 +1,27 @@ +// +// UIEdgeInsets+Extensions.swift +// SwiftMessages +// +// Created by Timothy Moose on 5/23/18. +// Copyright © 2018 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +extension UIEdgeInsets { + public static func +(left: UIEdgeInsets, right: UIEdgeInsets) -> UIEdgeInsets { + let topSum = left.top + right.top + let leftSum = left.left + right.left + let bottomSum = left.bottom + right.bottom + let rightSum = left.right + right.right + return UIEdgeInsets(top: topSum, left: leftSum, bottom: bottomSum, right: rightSum) + } + + public static func -(left: UIEdgeInsets, right: UIEdgeInsets) -> UIEdgeInsets { + let topSum = left.top - right.top + let leftSum = left.left - right.left + let bottomSum = left.bottom - right.bottom + let rightSum = left.right - right.right + return UIEdgeInsets(top: topSum, left: leftSum, bottom: bottomSum, right: rightSum) + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/UIViewController+Extensions.swift b/Demo/Pods/SwiftMessages/SwiftMessages/UIViewController+Extensions.swift new file mode 100644 index 0000000..7fbd4dd --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/UIViewController+Extensions.swift @@ -0,0 +1,101 @@ +// +// UIViewController+Extensions.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/5/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +extension UIViewController { + + func sm_selectPresentationContextTopDown(_ config: SwiftMessages.Config) -> UIViewController { + let topBottomStyle = config.presentationStyle.topBottomStyle + if let presented = presentedViewController { + return presented.sm_selectPresentationContextTopDown(config) + } else if case .top? = topBottomStyle, let navigationController = sm_selectNavigationControllerTopDown() { + return navigationController + } else if case .bottom? = topBottomStyle, let tabBarController = sm_selectTabBarControllerTopDown() { + return tabBarController + } + return WindowViewController.newInstance(config: config) + } + + fileprivate func sm_selectNavigationControllerTopDown() -> UINavigationController? { + if let presented = presentedViewController { + return presented.sm_selectNavigationControllerTopDown() + } else if let navigationController = self as? UINavigationController { + if navigationController.sm_isVisible(view: navigationController.navigationBar) { + return navigationController + } + return navigationController.topViewController?.sm_selectNavigationControllerTopDown() + } else if let tabBarController = self as? UITabBarController { + return tabBarController.selectedViewController?.sm_selectNavigationControllerTopDown() + } + return nil + } + + fileprivate func sm_selectTabBarControllerTopDown() -> UITabBarController? { + if let presented = presentedViewController { + return presented.sm_selectTabBarControllerTopDown() + } else if let navigationController = self as? UINavigationController { + return navigationController.topViewController?.sm_selectTabBarControllerTopDown() + } else if let tabBarController = self as? UITabBarController { + if tabBarController.sm_isVisible(view: tabBarController.tabBar) { + return tabBarController + } + return tabBarController.selectedViewController?.sm_selectTabBarControllerTopDown() + } + return nil + } + + func sm_selectPresentationContextBottomUp(_ config: SwiftMessages.Config) -> UIViewController { + let topBottomStyle = config.presentationStyle.topBottomStyle + if let parent = parent { + if let navigationController = parent as? UINavigationController { + if case .top? = topBottomStyle, navigationController.sm_isVisible(view: navigationController.navigationBar) { + return navigationController + } + return navigationController.sm_selectPresentationContextBottomUp(config) + } else if let tabBarController = parent as? UITabBarController { + if case .bottom? = topBottomStyle, tabBarController.sm_isVisible(view: tabBarController.tabBar) { + return tabBarController + } + return tabBarController.sm_selectPresentationContextBottomUp(config) + } + } + if self.view is UITableView { + // Never select scroll view as presentation context + // because, you know, it scrolls. + if let parent = self.parent { + return parent.sm_selectPresentationContextBottomUp(config) + } else { + return WindowViewController.newInstance(config: config) + } + } + return self + } + + func sm_isVisible(view: UIView) -> Bool { + if view.isHidden { return false } + if view.alpha == 0.0 { return false } + let frame = self.view.convert(view.bounds, from: view) + if !self.view.bounds.intersects(frame) { return false } + return true + } +} + +extension SwiftMessages.PresentationStyle { + /// A temporary workaround to allow custom presentation contexts using `TopBottomAnimation` + /// to display properly behind bars. THe long term solution is to refactor all of the + /// presentation context logic to work with safe area insets. + var topBottomStyle: TopBottomAnimation.Style? { + switch self { + case .top: return .top + case .bottom: return .bottom + case .custom(let animator): return (animator as? TopBottomAnimation)?.style + case .center: return nil + } + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/UIWindow+Extensions.swift b/Demo/Pods/SwiftMessages/SwiftMessages/UIWindow+Extensions.swift new file mode 100644 index 0000000..316fcac --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/UIWindow+Extensions.swift @@ -0,0 +1,38 @@ +// +// UIWindow+Extensions.swift +// SwiftMessages +// +// Created by Timothy Moose on 3/11/21. +// Copyright © 2021 SwiftKick Mobile. All rights reserved. +// + +import UIKit + +extension UIWindow { + #if !SWIFTMESSAGES_APP_EXTENSIONS + static var keyWindow: UIWindow? { + if #available(iOS 13.0, *) { + return UIApplication.shared.connectedScenes + .sorted { $0.activationState.sortPriority < $1.activationState.sortPriority } + .compactMap { $0 as? UIWindowScene } + .compactMap { $0.windows.first { $0.isKeyWindow } } + .first + } else { + return UIApplication.shared.keyWindow + } + } + #endif +} + +@available(iOS 13.0, *) +private extension UIScene.ActivationState { + var sortPriority: Int { + switch self { + case .foregroundActive: return 1 + case .foregroundInactive: return 2 + case .background: return 3 + case .unattached: return 4 + @unknown default: return 5 + } + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/Weak.swift b/Demo/Pods/SwiftMessages/SwiftMessages/Weak.swift new file mode 100644 index 0000000..a9bc470 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/Weak.swift @@ -0,0 +1,16 @@ +// +// Weak.swift +// SwiftMessages +// +// Created by Timothy Moose on 6/4/17. +// Copyright © 2017 SwiftKick Mobile. All rights reserved. +// + +import Foundation + +public class Weak { + public weak var value : T? + public init(value: T?) { + self.value = value + } +} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/WindowScene.swift b/Demo/Pods/SwiftMessages/SwiftMessages/WindowScene.swift new file mode 100644 index 0000000..a3bbb4c --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/WindowScene.swift @@ -0,0 +1,9 @@ +import Foundation +import UIKit + +/// A workaround for the change in Xcode 13 that prevents using `@availability` attribute +/// with `enum` cases containing associated values. +public protocol WindowScene {} + +@available(iOS 13.0, *) +extension UIWindowScene: WindowScene {} diff --git a/Demo/Pods/SwiftMessages/SwiftMessages/WindowViewController.swift b/Demo/Pods/SwiftMessages/SwiftMessages/WindowViewController.swift new file mode 100644 index 0000000..9c2b721 --- /dev/null +++ b/Demo/Pods/SwiftMessages/SwiftMessages/WindowViewController.swift @@ -0,0 +1,95 @@ +// +// WindowViewController.swift +// SwiftMessages +// +// Created by Timothy Moose on 8/1/16. +// Copyright © 2016 SwiftKick Mobile LLC. All rights reserved. +// + +import UIKit + +open class WindowViewController: UIViewController +{ + override open var shouldAutorotate: Bool { + return config.shouldAutorotate + } + + convenience public init() { + self.init(config: SwiftMessages.Config()) + } + + public init(config: SwiftMessages.Config) { + self.config = config + let view = PassthroughView() + let window = PassthroughWindow(hitTestView: view) + self.window = window + super.init(nibName: nil, bundle: nil) + self.view = view + window.rootViewController = self + window.windowLevel = config.windowLevel ?? UIWindow.Level.normal + if #available(iOS 13, *) { + window.overrideUserInterfaceStyle = config.overrideUserInterfaceStyle + } + } + + func install() { + if #available(iOS 13, *) { + window?.windowScene = config.windowScene + #if !SWIFTMESSAGES_APP_EXTENSIONS + previousKeyWindow = UIWindow.keyWindow + #endif + show( + becomeKey: config.shouldBecomeKeyWindow, + frame: config.windowScene?.coordinateSpace.bounds + ) + } else { + show(becomeKey: config.shouldBecomeKeyWindow) + } + } + + private func show(becomeKey: Bool, frame: CGRect? = nil) { + guard let window = window else { return } + window.frame = frame ?? UIScreen.main.bounds + if becomeKey { + window.makeKeyAndVisible() + } else { + window.isHidden = false + } + } + + func uninstall() { + if window?.isKeyWindow == true { + previousKeyWindow?.makeKey() + } + if #available(iOS 13, *) { + window?.windowScene = nil + } + window?.isHidden = true + window = nil + } + + required public init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override open var preferredStatusBarStyle: UIStatusBarStyle { + return config.preferredStatusBarStyle ?? super.preferredStatusBarStyle + } + + open override var prefersStatusBarHidden: Bool { + return config.prefersStatusBarHidden ?? super.prefersStatusBarHidden + } + + // MARK: - Variables + + private var window: UIWindow? + private weak var previousKeyWindow: UIWindow? + + let config: SwiftMessages.Config +} + +extension WindowViewController { + static func newInstance(config: SwiftMessages.Config) -> WindowViewController { + return config.windowViewController?(config) ?? WindowViewController(config: config) + } +} diff --git a/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-Info.plist b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-Info.plist new file mode 100644 index 0000000..fee5e01 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 4.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-dummy.m b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-dummy.m new file mode 100644 index 0000000..1780c1c --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_IGListDiffKit : NSObject +@end +@implementation PodsDummy_IGListDiffKit +@end diff --git a/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-prefix.pch b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-umbrella.h b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-umbrella.h new file mode 100644 index 0000000..beba78c --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit-umbrella.h @@ -0,0 +1,30 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "IGListAssert.h" +#import "IGListBatchUpdateData.h" +#import "IGListCompatibility.h" +#import "IGListDiff.h" +#import "IGListDiffable.h" +#import "IGListDiffKit.h" +#import "IGListExperiments.h" +#import "IGListIndexPathResult.h" +#import "IGListIndexSetResult.h" +#import "IGListMacros.h" +#import "IGListMoveIndex.h" +#import "IGListMoveIndexPath.h" +#import "NSNumber+IGListDiffable.h" +#import "NSString+IGListDiffable.h" + +FOUNDATION_EXPORT double IGListDiffKitVersionNumber; +FOUNDATION_EXPORT const unsigned char IGListDiffKitVersionString[]; + diff --git a/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.debug.xcconfig b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.debug.xcconfig new file mode 100644 index 0000000..c8f56e1 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.debug.xcconfig @@ -0,0 +1,15 @@ +CLANG_CXX_LANGUAGE_STANDARD = c++11 +CLANG_CXX_LIBRARY = libc++ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/IGListDiffKit +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_LDFLAGS = $(inherited) -l"c++" -framework "UIKit" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/IGListDiffKit +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.modulemap b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.modulemap new file mode 100644 index 0000000..24eddd1 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.modulemap @@ -0,0 +1,6 @@ +framework module IGListDiffKit { + umbrella header "IGListDiffKit-umbrella.h" + + export * + module * { export * } +} diff --git a/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.release.xcconfig b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.release.xcconfig new file mode 100644 index 0000000..c8f56e1 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListDiffKit/IGListDiffKit.release.xcconfig @@ -0,0 +1,15 @@ +CLANG_CXX_LANGUAGE_STANDARD = c++11 +CLANG_CXX_LIBRARY = libc++ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/IGListDiffKit +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_LDFLAGS = $(inherited) -l"c++" -framework "UIKit" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/IGListDiffKit +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/IGListKit/IGListKit-Info.plist b/Demo/Pods/Target Support Files/IGListKit/IGListKit-Info.plist new file mode 100644 index 0000000..fee5e01 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListKit/IGListKit-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 4.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Demo/Pods/Target Support Files/IGListKit/IGListKit-dummy.m b/Demo/Pods/Target Support Files/IGListKit/IGListKit-dummy.m new file mode 100644 index 0000000..50cf882 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListKit/IGListKit-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_IGListKit : NSObject +@end +@implementation PodsDummy_IGListKit +@end diff --git a/Demo/Pods/Target Support Files/IGListKit/IGListKit-prefix.pch b/Demo/Pods/Target Support Files/IGListKit/IGListKit-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListKit/IGListKit-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Demo/Pods/Target Support Files/IGListKit/IGListKit-umbrella.h b/Demo/Pods/Target Support Files/IGListKit/IGListKit-umbrella.h new file mode 100644 index 0000000..bef36ee --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListKit/IGListKit-umbrella.h @@ -0,0 +1,46 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "IGListAdapter.h" +#import "IGListAdapterDataSource.h" +#import "IGListAdapterDelegate.h" +#import "IGListAdapterMoveDelegate.h" +#import "IGListAdapterPerformanceDelegate.h" +#import "IGListAdapterUpdateListener.h" +#import "IGListAdapterUpdater.h" +#import "IGListAdapterUpdaterDelegate.h" +#import "IGListBatchContext.h" +#import "IGListBindable.h" +#import "IGListBindingSectionController.h" +#import "IGListBindingSectionControllerDataSource.h" +#import "IGListBindingSectionControllerSelectionDelegate.h" +#import "IGListCollectionContext.h" +#import "IGListCollectionScrollingTraits.h" +#import "IGListCollectionView.h" +#import "IGListCollectionViewDelegateLayout.h" +#import "IGListCollectionViewLayout.h" +#import "IGListCollectionViewLayoutCompatible.h" +#import "IGListDisplayDelegate.h" +#import "IGListGenericSectionController.h" +#import "IGListKit.h" +#import "IGListReloadDataUpdater.h" +#import "IGListScrollDelegate.h" +#import "IGListSectionController.h" +#import "IGListSingleSectionController.h" +#import "IGListSupplementaryViewSource.h" +#import "IGListTransitionDelegate.h" +#import "IGListUpdatingDelegate.h" +#import "IGListWorkingRangeDelegate.h" + +FOUNDATION_EXPORT double IGListKitVersionNumber; +FOUNDATION_EXPORT const unsigned char IGListKitVersionString[]; + diff --git a/Demo/Pods/Target Support Files/IGListKit/IGListKit.debug.xcconfig b/Demo/Pods/Target Support Files/IGListKit/IGListKit.debug.xcconfig new file mode 100644 index 0000000..af17724 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListKit/IGListKit.debug.xcconfig @@ -0,0 +1,16 @@ +CLANG_CXX_LANGUAGE_STANDARD = c++11 +CLANG_CXX_LIBRARY = libc++ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/IGListKit +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IGListDiffKit" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_LDFLAGS = $(inherited) -l"c++" -framework "IGListDiffKit" -framework "UIKit" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/IGListKit +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/IGListKit/IGListKit.modulemap b/Demo/Pods/Target Support Files/IGListKit/IGListKit.modulemap new file mode 100644 index 0000000..76fc8f0 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListKit/IGListKit.modulemap @@ -0,0 +1,6 @@ +framework module IGListKit { + umbrella header "IGListKit-umbrella.h" + + export * + module * { export * } +} diff --git a/Demo/Pods/Target Support Files/IGListKit/IGListKit.release.xcconfig b/Demo/Pods/Target Support Files/IGListKit/IGListKit.release.xcconfig new file mode 100644 index 0000000..af17724 --- /dev/null +++ b/Demo/Pods/Target Support Files/IGListKit/IGListKit.release.xcconfig @@ -0,0 +1,16 @@ +CLANG_CXX_LANGUAGE_STANDARD = c++11 +CLANG_CXX_LIBRARY = libc++ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/IGListKit +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IGListDiffKit" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +OTHER_LDFLAGS = $(inherited) -l"c++" -framework "IGListDiffKit" -framework "UIKit" +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/IGListKit +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist new file mode 100644 index 0000000..119cb52 --- /dev/null +++ b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 7.6.2 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m new file mode 100644 index 0000000..1b89d0e --- /dev/null +++ b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Kingfisher : NSObject +@end +@implementation PodsDummy_Kingfisher +@end diff --git a/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h new file mode 100644 index 0000000..75a7996 --- /dev/null +++ b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double KingfisherVersionNumber; +FOUNDATION_EXPORT const unsigned char KingfisherVersionString[]; + diff --git a/Demo/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig new file mode 100644 index 0000000..849f1e9 --- /dev/null +++ b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig @@ -0,0 +1,15 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher.modulemap new file mode 100644 index 0000000..2a20d91 --- /dev/null +++ b/Demo/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/Demo/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig new file mode 100644 index 0000000..849f1e9 --- /dev/null +++ b/Demo/Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig @@ -0,0 +1,15 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "Accelerate" -framework "CFNetwork" -weak_framework "Combine" -weak_framework "SwiftUI" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/Kingfisher +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-Info.plist b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-Info.plist new file mode 100644 index 0000000..19cf209 --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0.0 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-acknowledgements.markdown b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-acknowledgements.markdown new file mode 100644 index 0000000..02ff2fc --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-acknowledgements.markdown @@ -0,0 +1,112 @@ +# Acknowledgements +This application makes use of the following third party libraries: + +## IGListDiffKit + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +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. + +## IGListKit + +MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +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) 2019 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. + + + +## SwiftMessages + +Copyright (c) 2016 SwiftKick Mobile LLC + + +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. + +## UIColor_Hex_Swift + +The MIT License (MIT) + +Copyright (c) 2014 R0CKSTAR + +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/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-acknowledgements.plist b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-acknowledgements.plist new file mode 100644 index 0000000..ca35106 --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-acknowledgements.plist @@ -0,0 +1,168 @@ + + + + + PreferenceSpecifiers + + + FooterText + This application makes use of the following third party libraries: + Title + Acknowledgements + Type + PSGroupSpecifier + + + FooterText + MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +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. + License + MIT + Title + IGListDiffKit + Type + PSGroupSpecifier + + + FooterText + MIT License + +Copyright (c) Facebook, Inc. and its affiliates. + +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. + License + MIT + Title + IGListKit + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2019 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. + + + License + MIT + Title + Kingfisher + Type + PSGroupSpecifier + + + FooterText + Copyright (c) 2016 SwiftKick Mobile LLC + + +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. + License + MIT + Title + SwiftMessages + Type + PSGroupSpecifier + + + FooterText + The MIT License (MIT) + +Copyright (c) 2014 R0CKSTAR + +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. + License + MIT + Title + UIColor_Hex_Swift + Type + PSGroupSpecifier + + + FooterText + Generated by CocoaPods - https://cocoapods.org + Title + + Type + PSGroupSpecifier + + + StringsTable + Acknowledgements + Title + Acknowledgements + + diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-dummy.m b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-dummy.m new file mode 100644 index 0000000..d0fcb93 --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_Pods_Bulletin : NSObject +@end +@implementation PodsDummy_Pods_Bulletin +@end diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Debug-input-files.xcfilelist b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 0000000..f7ae177 --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,6 @@ +${PODS_ROOT}/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks.sh +${BUILT_PRODUCTS_DIR}/IGListDiffKit/IGListDiffKit.framework +${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework +${BUILT_PRODUCTS_DIR}/SwiftMessages/SwiftMessages.framework +${BUILT_PRODUCTS_DIR}/UIColor_Hex_Swift/UIColor_Hex_Swift.framework \ No newline at end of file diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Debug-output-files.xcfilelist b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 0000000..644b818 --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1,5 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListDiffKit.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListKit.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftMessages.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/UIColor_Hex_Swift.framework \ No newline at end of file diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Release-input-files.xcfilelist b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Release-input-files.xcfilelist new file mode 100644 index 0000000..f7ae177 --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,6 @@ +${PODS_ROOT}/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks.sh +${BUILT_PRODUCTS_DIR}/IGListDiffKit/IGListDiffKit.framework +${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework +${BUILT_PRODUCTS_DIR}/SwiftMessages/SwiftMessages.framework +${BUILT_PRODUCTS_DIR}/UIColor_Hex_Swift/UIColor_Hex_Swift.framework \ No newline at end of file diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Release-output-files.xcfilelist b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Release-output-files.xcfilelist new file mode 100644 index 0000000..644b818 --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks-Release-output-files.xcfilelist @@ -0,0 +1,5 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListDiffKit.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListKit.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/SwiftMessages.framework +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/UIColor_Hex_Swift.framework \ No newline at end of file diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks.sh b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks.sh new file mode 100755 index 0000000..c399c7b --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-frameworks.sh @@ -0,0 +1,194 @@ +#!/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}" +BCSYMBOLMAP_DIR="BCSymbolMaps" + + +# 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 -f "${source}")" + fi + + if [ -d "${source}/${BCSYMBOLMAP_DIR}" ]; then + # Locate and install any .bcsymbolmaps if present, and remove them from the .framework before the framework is copied + find "${source}/${BCSYMBOLMAP_DIR}" -name "*.bcsymbolmap"|while read f; do + echo "Installing $f" + install_bcsymbolmap "$f" "$destination" + rm "$f" + done + rmdir "${source}/${BCSYMBOLMAP_DIR}" + fi + + # Use filter instead of exclude so missing patterns don't throw errors. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --filter \"- CVS/\" --filter \"- .svn/\" --filter \"- .git/\" --filter \"- .hg/\" --filter \"- Headers\" --filter \"- PrivateHeaders\" --filter \"- Modules\" \"${source}\" \"${destination}\"" + rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --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" + warn_missing_arch=${2:-true} + if [ -r "$source" ]; then + # Copy the dSYM into 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 .dSYM "$source")" + binary_name="$(ls "$source/Contents/Resources/DWARF")" + binary="${DERIVED_FILES_DIR}/${basename}.dSYM/Contents/Resources/DWARF/${binary_name}" + + # Strip invalid architectures from the dSYM. + if [[ "$(file "$binary")" == *"Mach-O "*"dSYM companion"* ]]; then + strip_invalid_archs "$binary" "$warn_missing_arch" + fi + if [[ $STRIP_BINARY_RETVAL == 0 ]]; then + # Move the stripped file into its final destination. + echo "rsync --delete -av "${RSYNC_PROTECT_TMP_FILES[@]}" --links --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[@]}" --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "${DERIVED_FILES_DIR}/${basename}.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. + mkdir -p "${DWARF_DSYM_FOLDER_PATH}" + touch "${DWARF_DSYM_FOLDER_PATH}/${basename}.dSYM" + fi + fi +} + +# Used as a return value for each invocation of `strip_invalid_archs` function. +STRIP_BINARY_RETVAL=0 + +# Strip invalid architectures +strip_invalid_archs() { + binary="$1" + warn_missing_arch=${2:-true} + # 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 + if [[ "$warn_missing_arch" == "true" ]]; then + echo "warning: [CP] Vendored binary '$binary' contains architectures ($binary_archs) none of which match the current build architectures ($ARCHS)." + fi + STRIP_BINARY_RETVAL=1 + 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=0 +} + +# 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 +} + +if [[ "$CONFIGURATION" == "Debug" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/IGListDiffKit/IGListDiffKit.framework" + install_framework "${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/SwiftMessages/SwiftMessages.framework" + install_framework "${BUILT_PRODUCTS_DIR}/UIColor_Hex_Swift/UIColor_Hex_Swift.framework" +fi +if [[ "$CONFIGURATION" == "Release" ]]; then + install_framework "${BUILT_PRODUCTS_DIR}/IGListDiffKit/IGListDiffKit.framework" + install_framework "${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework" + install_framework "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework" + install_framework "${BUILT_PRODUCTS_DIR}/SwiftMessages/SwiftMessages.framework" + install_framework "${BUILT_PRODUCTS_DIR}/UIColor_Hex_Swift/UIColor_Hex_Swift.framework" +fi +if [ "${COCOAPODS_PARALLEL_CODE_SIGN}" == "true" ]; then + wait +fi diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-umbrella.h b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-umbrella.h new file mode 100644 index 0000000..3494f0c --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double Pods_BulletinVersionNumber; +FOUNDATION_EXPORT const unsigned char Pods_BulletinVersionString[]; + diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.debug.xcconfig b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.debug.xcconfig new file mode 100644 index 0000000..f59b86d --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.debug.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IGListDiffKit" "${PODS_CONFIGURATION_BUILD_DIR}/IGListKit" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftMessages" "${PODS_CONFIGURATION_BUILD_DIR}/UIColor_Hex_Swift" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IGListDiffKit/IGListDiffKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IGListKit/IGListKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftMessages/SwiftMessages.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/UIColor_Hex_Swift/UIColor_Hex_Swift.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -l"c++" -framework "Accelerate" -framework "CFNetwork" -framework "IGListDiffKit" -framework "IGListKit" -framework "Kingfisher" -framework "SwiftMessages" -framework "UIColor_Hex_Swift" -framework "UIKit" -weak_framework "Combine" -weak_framework "SwiftUI" +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 +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.modulemap b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.modulemap new file mode 100644 index 0000000..2a95ea7 --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.modulemap @@ -0,0 +1,6 @@ +framework module Pods_Bulletin { + umbrella header "Pods-Bulletin-umbrella.h" + + export * + module * { export * } +} diff --git a/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.release.xcconfig b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.release.xcconfig new file mode 100644 index 0000000..f59b86d --- /dev/null +++ b/Demo/Pods/Target Support Files/Pods-Bulletin/Pods-Bulletin.release.xcconfig @@ -0,0 +1,15 @@ +ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +FRAMEWORK_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IGListDiffKit" "${PODS_CONFIGURATION_BUILD_DIR}/IGListKit" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftMessages" "${PODS_CONFIGURATION_BUILD_DIR}/UIColor_Hex_Swift" +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +HEADER_SEARCH_PATHS = $(inherited) "${PODS_CONFIGURATION_BUILD_DIR}/IGListDiffKit/IGListDiffKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/IGListKit/IGListKit.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/Kingfisher/Kingfisher.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/SwiftMessages/SwiftMessages.framework/Headers" "${PODS_CONFIGURATION_BUILD_DIR}/UIColor_Hex_Swift/UIColor_Hex_Swift.framework/Headers" +LD_RUNPATH_SEARCH_PATHS = $(inherited) /usr/lib/swift '@executable_path/Frameworks' '@loader_path/Frameworks' +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -l"c++" -framework "Accelerate" -framework "CFNetwork" -framework "IGListDiffKit" -framework "IGListKit" -framework "Kingfisher" -framework "SwiftMessages" -framework "UIColor_Hex_Swift" -framework "UIKit" -weak_framework "Combine" -weak_framework "SwiftUI" +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 +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/SwiftMessages/ResourceBundle-SwiftMessages-SwiftMessages-Info.plist b/Demo/Pods/Target Support Files/SwiftMessages/ResourceBundle-SwiftMessages-SwiftMessages-Info.plist new file mode 100644 index 0000000..e0f20fc --- /dev/null +++ b/Demo/Pods/Target Support Files/SwiftMessages/ResourceBundle-SwiftMessages-SwiftMessages-Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + BNDL + CFBundleShortVersionString + 9.0.6 + CFBundleSignature + ???? + CFBundleVersion + 1 + NSPrincipalClass + + + diff --git a/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-Info.plist b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-Info.plist new file mode 100644 index 0000000..ec23eec --- /dev/null +++ b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 9.0.6 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-dummy.m b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-dummy.m new file mode 100644 index 0000000..dc460b8 --- /dev/null +++ b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_SwiftMessages : NSObject +@end +@implementation PodsDummy_SwiftMessages +@end diff --git a/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-prefix.pch b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-umbrella.h b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-umbrella.h new file mode 100644 index 0000000..9aa634f --- /dev/null +++ b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages-umbrella.h @@ -0,0 +1,16 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + + +FOUNDATION_EXPORT double SwiftMessagesVersionNumber; +FOUNDATION_EXPORT const unsigned char SwiftMessagesVersionString[]; + diff --git a/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.debug.xcconfig b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.debug.xcconfig new file mode 100644 index 0000000..7359800 --- /dev/null +++ b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.debug.xcconfig @@ -0,0 +1,15 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftMessages +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "UIKit" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftMessages +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.modulemap b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.modulemap new file mode 100644 index 0000000..96306d6 --- /dev/null +++ b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.modulemap @@ -0,0 +1,6 @@ +framework module SwiftMessages { + umbrella header "SwiftMessages-umbrella.h" + + export * + module * { export * } +} diff --git a/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.release.xcconfig b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.release.xcconfig new file mode 100644 index 0000000..7359800 --- /dev/null +++ b/Demo/Pods/Target Support Files/SwiftMessages/SwiftMessages.release.xcconfig @@ -0,0 +1,15 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/SwiftMessages +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "UIKit" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/SwiftMessages +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-Info.plist b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-Info.plist new file mode 100644 index 0000000..b5c956c --- /dev/null +++ b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-Info.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + ${PODS_DEVELOPMENT_LANGUAGE} + CFBundleExecutable + ${EXECUTABLE_NAME} + CFBundleIdentifier + ${PRODUCT_BUNDLE_IDENTIFIER} + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + ${PRODUCT_NAME} + CFBundlePackageType + FMWK + CFBundleShortVersionString + 5.1.7 + CFBundleSignature + ???? + CFBundleVersion + ${CURRENT_PROJECT_VERSION} + NSPrincipalClass + + + diff --git a/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-dummy.m b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-dummy.m new file mode 100644 index 0000000..fb34f15 --- /dev/null +++ b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-dummy.m @@ -0,0 +1,5 @@ +#import +@interface PodsDummy_UIColor_Hex_Swift : NSObject +@end +@implementation PodsDummy_UIColor_Hex_Swift +@end diff --git a/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-prefix.pch b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-prefix.pch new file mode 100644 index 0000000..beb2a24 --- /dev/null +++ b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-prefix.pch @@ -0,0 +1,12 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + diff --git a/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-umbrella.h b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-umbrella.h new file mode 100644 index 0000000..d463f41 --- /dev/null +++ b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift-umbrella.h @@ -0,0 +1,17 @@ +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +#import "HEXColor.h" + +FOUNDATION_EXPORT double UIColor_Hex_SwiftVersionNumber; +FOUNDATION_EXPORT const unsigned char UIColor_Hex_SwiftVersionString[]; + diff --git a/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.debug.xcconfig b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.debug.xcconfig new file mode 100644 index 0000000..b0259dd --- /dev/null +++ b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.debug.xcconfig @@ -0,0 +1,15 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/UIColor_Hex_Swift +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "UIKit" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/UIColor_Hex_Swift +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.modulemap b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.modulemap new file mode 100644 index 0000000..bc61251 --- /dev/null +++ b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.modulemap @@ -0,0 +1,6 @@ +framework module UIColor_Hex_Swift { + umbrella header "UIColor_Hex_Swift-umbrella.h" + + export * + module * { export * } +} diff --git a/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.release.xcconfig b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.release.xcconfig new file mode 100644 index 0000000..b0259dd --- /dev/null +++ b/Demo/Pods/Target Support Files/UIColor_Hex_Swift/UIColor_Hex_Swift.release.xcconfig @@ -0,0 +1,15 @@ +CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = NO +CONFIGURATION_BUILD_DIR = ${PODS_CONFIGURATION_BUILD_DIR}/UIColor_Hex_Swift +GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 +LIBRARY_SEARCH_PATHS = $(inherited) "${DT_TOOLCHAIN_DIR}/usr/lib/swift/${PLATFORM_NAME}" /usr/lib/swift +OTHER_LDFLAGS = $(inherited) -framework "UIKit" +OTHER_SWIFT_FLAGS = $(inherited) -D COCOAPODS +PODS_BUILD_DIR = ${BUILD_DIR} +PODS_CONFIGURATION_BUILD_DIR = ${PODS_BUILD_DIR}/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME) +PODS_DEVELOPMENT_LANGUAGE = ${DEVELOPMENT_LANGUAGE} +PODS_ROOT = ${SRCROOT} +PODS_TARGET_SRCROOT = ${PODS_ROOT}/UIColor_Hex_Swift +PODS_XCFRAMEWORKS_BUILD_DIR = $(PODS_CONFIGURATION_BUILD_DIR)/XCFrameworkIntermediates +PRODUCT_BUNDLE_IDENTIFIER = org.cocoapods.${PRODUCT_NAME:rfc1034identifier} +SKIP_INSTALL = YES +USE_RECURSIVE_SCRIPT_INPUTS_IN_SCRIPT_PHASES = YES diff --git a/Demo/Pods/UIColor_Hex_Swift/HEXColor/HEXColor.h b/Demo/Pods/UIColor_Hex_Swift/HEXColor/HEXColor.h new file mode 100644 index 0000000..7ad5a18 --- /dev/null +++ b/Demo/Pods/UIColor_Hex_Swift/HEXColor/HEXColor.h @@ -0,0 +1,17 @@ +// +// HEXColor.h +// HEXColor +// +// Created by Yuki Nagai on 10/5/15. +// Copyright © 2015 P.D.Q. All rights reserved. +// + +#import + +//! Project version number for HEXColor. +FOUNDATION_EXPORT double HEXColorVersionNumber; + +//! Project version string for HEXColor. +FOUNDATION_EXPORT const unsigned char HEXColorVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import diff --git a/Demo/Pods/UIColor_Hex_Swift/HEXColor/StringExtension.swift b/Demo/Pods/UIColor_Hex_Swift/HEXColor/StringExtension.swift new file mode 100644 index 0000000..93eafe8 --- /dev/null +++ b/Demo/Pods/UIColor_Hex_Swift/HEXColor/StringExtension.swift @@ -0,0 +1,31 @@ +// +// StringExtension.swift +// HEXColor-iOS +// +// Created by Sergey Pugach on 2/2/18. +// Copyright © 2018 P.D.Q. All rights reserved. +// + +import Foundation + + +extension String { + /** + Convert argb string to rgba string. + */ + public var argb2rgba: String? { + guard self.hasPrefix("#") else { + return nil + } + + let hexString: String = String(self[self.index(self.startIndex, offsetBy: 1)...]) + switch hexString.count { + case 4: + return "#\(String(hexString[self.index(self.startIndex, offsetBy: 1)...]))\(String(hexString[..> 8) / divisor + let green = CGFloat((hex3 & 0x0F0) >> 4) / divisor + let blue = CGFloat( hex3 & 0x00F ) / divisor + self.init(red: red, green: green, blue: blue, alpha: alpha) + } + + /** + The shorthand four-digit hexadecimal representation of color with alpha. + #RGBA defines to the color #RRGGBBAA. + + - parameter hex4: Four-digit hexadecimal value. + */ + public convenience init(hex4: UInt16) { + let divisor = CGFloat(15) + let red = CGFloat((hex4 & 0xF000) >> 12) / divisor + let green = CGFloat((hex4 & 0x0F00) >> 8) / divisor + let blue = CGFloat((hex4 & 0x00F0) >> 4) / divisor + let alpha = CGFloat( hex4 & 0x000F ) / divisor + self.init(red: red, green: green, blue: blue, alpha: alpha) + } + + /** + The six-digit hexadecimal representation of color of the form #RRGGBB. + + - parameter hex6: Six-digit hexadecimal value. + */ + public convenience init(hex6: UInt32, alpha: CGFloat = 1) { + let divisor = CGFloat(255) + let red = CGFloat((hex6 & 0xFF0000) >> 16) / divisor + let green = CGFloat((hex6 & 0x00FF00) >> 8) / divisor + let blue = CGFloat( hex6 & 0x0000FF ) / divisor + self.init(red: red, green: green, blue: blue, alpha: alpha) + } + + /** + The six-digit hexadecimal representation of color with alpha of the form #RRGGBBAA. + + - parameter hex8: Eight-digit hexadecimal value. + */ + public convenience init(hex8: UInt32) { + let divisor = CGFloat(255) + let red = CGFloat((hex8 & 0xFF000000) >> 24) / divisor + let green = CGFloat((hex8 & 0x00FF0000) >> 16) / divisor + let blue = CGFloat((hex8 & 0x0000FF00) >> 8) / divisor + let alpha = CGFloat( hex8 & 0x000000FF ) / divisor + self.init(red: red, green: green, blue: blue, alpha: alpha) + } + + /** + The rgba string representation of color with alpha of the form #RRGGBBAA/#RRGGBB, throws error. + + - parameter rgba: String value. + */ + public convenience init(rgba_throws rgba: String) throws { + guard rgba.hasPrefix("#") else { + let error = UIColorInputError.missingHashMarkAsPrefix(rgba) + print(error.localizedDescription) + throw error + } + + let hexString: String = String(rgba[String.Index(utf16Offset: 1, in: rgba)...]) + var hexValue: UInt32 = 0 + + guard Scanner(string: hexString).scanHexInt32(&hexValue) else { + let error = UIColorInputError.unableToScanHexValue(rgba) + print(error.localizedDescription) + throw error + } + + switch (hexString.count) { + case 3: + self.init(hex3: UInt16(hexValue)) + case 4: + self.init(hex4: UInt16(hexValue)) + case 6: + self.init(hex6: hexValue) + case 8: + self.init(hex8: hexValue) + default: + let error = UIColorInputError.mismatchedHexStringLength(rgba) + print(error.localizedDescription) + throw error + } + } + + /** + The rgba string representation of color with alpha of the form #RRGGBBAA/#RRGGBB, fails to default color. + + - parameter rgba: String value. + */ +#if os(macOS) + public convenience init?(_ rgba: String, defaultColor: NSColor = NSColor.clear) { + guard let color = try? Color(rgba_throws: rgba) else { + self.init(cgColor: defaultColor.cgColor) + return + } + self.init(cgColor: color.cgColor) + } +#else + public convenience init(_ rgba: String, defaultColor: UIColor = UIColor.clear) { + guard let color = try? UIColor(rgba_throws: rgba) else { + self.init(cgColor: defaultColor.cgColor) + return + } + self.init(cgColor: color.cgColor) + } +#endif + + /** + Hex string of a UIColor instance, throws error. + + - parameter includeAlpha: Whether the alpha should be included. + */ + public func hexStringThrows(_ includeAlpha: Bool = true) throws -> String { + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + self.getRed(&r, green: &g, blue: &b, alpha: &a) + + guard r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1 else { + let error = UIColorInputError.unableToOutputHexStringForWideDisplayColor + print(error.localizedDescription) + throw error + } + + if (includeAlpha) { + return String(format: "#%02X%02X%02X%02X", + Int(round(r * 255)), Int(round(g * 255)), + Int(round(b * 255)), Int(round(a * 255))) + } else { + return String(format: "#%02X%02X%02X", Int(round(r * 255)), + Int(round(g * 255)), Int(round(b * 255))) + } + } + + /** + Hex string of a UIColor instance, fails to empty string. + + - parameter includeAlpha: Whether the alpha should be included. + */ + public func hexString(_ includeAlpha: Bool = true) -> String { + guard let hexString = try? hexStringThrows(includeAlpha) else { + return "" + } + return hexString + } +} diff --git a/Demo/Pods/UIColor_Hex_Swift/HEXColor/UIColorInputError.swift b/Demo/Pods/UIColor_Hex_Swift/HEXColor/UIColorInputError.swift new file mode 100644 index 0000000..bdcaae4 --- /dev/null +++ b/Demo/Pods/UIColor_Hex_Swift/HEXColor/UIColorInputError.swift @@ -0,0 +1,36 @@ +// +// UIColorInputError.swift +// HEXColor-iOS +// +// Created by Sergey Pugach on 2/2/18. +// Copyright © 2018 P.D.Q. All rights reserved. +// + +import Foundation + +public enum UIColorInputError: Error { + + case missingHashMarkAsPrefix(String) + case unableToScanHexValue(String) + case mismatchedHexStringLength(String) + case unableToOutputHexStringForWideDisplayColor +} + +extension UIColorInputError: LocalizedError { + + public var errorDescription: String? { + switch self { + case .missingHashMarkAsPrefix(let hex): + return "Invalid RGB string, missing '#' as prefix in \(hex)" + + case .unableToScanHexValue(let hex): + return "Scan \(hex) error" + + case .mismatchedHexStringLength(let hex): + return "Invalid RGB string from \(hex), number of characters after '#' should be either 3, 4, 6 or 8" + + case .unableToOutputHexStringForWideDisplayColor: + return "Unable to output hex string for wide display color" + } + } +} diff --git a/Demo/Pods/UIColor_Hex_Swift/LICENSE b/Demo/Pods/UIColor_Hex_Swift/LICENSE new file mode 100644 index 0000000..b73ee02 --- /dev/null +++ b/Demo/Pods/UIColor_Hex_Swift/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 R0CKSTAR + +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. \ No newline at end of file diff --git a/Demo/Pods/UIColor_Hex_Swift/README.md b/Demo/Pods/UIColor_Hex_Swift/README.md new file mode 100644 index 0000000..9420955 --- /dev/null +++ b/Demo/Pods/UIColor_Hex_Swift/README.md @@ -0,0 +1,81 @@ +

+ +

+ +UIColor+Hex, now Swift. +[![Build Status](https://travis-ci.org/yeahdongcn/UIColor-Hex-Swift.svg?branch=master)](https://travis-ci.org/yeahdongcn/UIColor-Hex-Swift) [![codecov.io](https://codecov.io/gh/yeahdongcn/UIColor-Hex-Swift/branch/master/graphs/badge.svg)](https://codecov.io/gh/yeahdongcn/UIColor-Hex-Swift/branch/master) ![](https://img.shields.io/badge/Swift-5.0-blue.svg?style=flat) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) ![](https://img.shields.io/badge/license-MIT-blue.svg?style=flat) +================= +Convenience method for creating autoreleased color using RGBA hex string. + +```swift + // Solid color + let strokeColor = UIColor("#FFCC00").cgColor + + // Color with alpha + let fillColor = UIColor("#FFCC00DD").cgColor + + // Supports shorthand 3 character representation + let backgroundColor = UIColor("#FFF") + + // Supports shorthand 4 character representation (with alpha) + let menuTextColor = UIColor("#013E") + + // "#FF0000FF" + let hexString = UIColor.red.hexString() + + // Convert shorthand 4 character representation (with alpha) from argb to rgba + if let rgba = "#AFFF".argb2rgba { + let androidBackgroundColor = UIColor(rgba) + } + + // Convert 8 character representation (with alpha) from argb to rgba + if let rgba = "#AAFFFFFF".argb2rgba { + let androidFrontColor = UIColor(rgba) + } +``` +## Release Notes + +* Upgrade to Swift 5. +* macOS gets supported. + +## Installation + +### [Swift Package Manager](https://github.com/apple/swift-package-manager) + +To add a package dependency to your Xcode project, select File > Swift Packages > Add Package Dependency and enter https://github.com/yeahdongcn/UIColor-Hex-Swift to the text field. + +### [CocoaPods](http://cocoapods.org) + +Simply add the following lines to your `Podfile`: +```ruby +# required by CocoaPods 0.36.0.rc.1 for Swift Pods +use_frameworks! + +pod 'UIColor_Hex_Swift', '~> 5.1.7' +``` + +Then import it where you use it: +```swift +import UIColor_Hex_Swift +``` + +*(CocoaPods v0.36 or later required. See [this blog post](http://blog.cocoapods.org/Pod-Authors-Guide-to-CocoaPods-Frameworks/) for details.)* + +### [Carthage](http://github.com/Carthage/Carthage) + +Simply add the following line to your `Cartfile`: + +```ruby +github "yeahdongcn/UIColor-Hex-Swift" >= 5.1.7 +``` + +Then add the HexColor.framework to your frameworks list in the Xcode project. + +Then import it where you use it: +```swift +import HEXColor +``` + +--- + +See more in [RSBarcodes_Swift](https://github.com/yeahdongcn/RSBarcodes_Swift) and [objc version](https://github.com/yeahdongcn/RSBarcodes) From 7a2d6c2c79734125cb3c9579104f18ee853e483c Mon Sep 17 00:00:00 2001 From: Daxesh Date: Thu, 27 Jul 2023 10:25:30 +0530 Subject: [PATCH 07/12] Cell refactoring --- BulletinSDK/BulletinSDK.swift | 25 +++- .../Classes/Categories/String+Unicode.swift | 21 +++ .../Categories/UIView+SubviewAspectSize.swift | 55 +++++++ .../Classes/Constants/Appearance.swift | 31 ++-- BulletinSDK/Classes/Constants/Text.swift | 3 + BulletinSDK/Modals/BulletinInfo.swift | 13 +- .../ActionButton/ActionButtonViewCell.swift | 46 +++--- .../ActionButton/ActionButtonViewCell.xib | 17 ++- .../BulletPoint/BulletPointViewCell.swift | 135 ++++++++++-------- .../View/BulletPoint/BulletPointViewCell.xib | 10 +- BulletinSDK/View/Media/MediaViewCell.swift | 90 ++++++++---- BulletinSDK/View/Media/MediaViewCell.xib | 18 ++- .../View/Message/MessageViewCell.swift | 70 +++++---- BulletinSDK/View/Message/MessageViewCell.xib | 4 +- BulletinSDK/View/Title/TitleViewCell.swift | 57 +++++--- BulletinSDK/View/Title/TitleViewCell.xib | 18 +-- .../BulletinListSectionController.swift | 16 ++- .../Controllers/BulletinListView.swift | 51 ++++++- .../Bulletin/Controllers/BulletinListView.xib | 56 +++++++- .../Controllers/BulletinSection.swift | 35 ++--- Demo/Bulletin/ViewController.swift | 43 +++--- 21 files changed, 553 insertions(+), 261 deletions(-) create mode 100644 BulletinSDK/Classes/Categories/String+Unicode.swift create mode 100644 BulletinSDK/Classes/Categories/UIView+SubviewAspectSize.swift diff --git a/BulletinSDK/BulletinSDK.swift b/BulletinSDK/BulletinSDK.swift index 962df84..e1a0f1e 100644 --- a/BulletinSDK/BulletinSDK.swift +++ b/BulletinSDK/BulletinSDK.swift @@ -15,10 +15,11 @@ class BulletinSDK { // MARK: Initialisation Method - public init(dataStore: BulletinDataStore) { + public init(dataStore: BulletinDataStore, appearance: Appearance) { // Set Data Store self.dataStore = dataStore + applyAppearance(theme: appearance) } @@ -59,11 +60,25 @@ class BulletinSDK { guard let items = items, items.isEmpty == false else { return nil } - let bulletinSection = BulletinSection() - bulletinSection.bulletinInfo = items - - let bulletinView = BulletinListView.instance(bulletinSection: bulletinSection) + let bulletinView = BulletinListView.instance(items: items) return bulletinView } + + private func applyAppearance(theme: Appearance) { + + // Apply Theme + switch theme { + case .darkKnight: + + // Apply Dark Knight Theme + Appearance.darkKnight.apply(shouldBroadcastUpdate: true) + + case .whiteKnight: + + // Apply Dark Knight Theme + Appearance.whiteKnight.apply(shouldBroadcastUpdate: true) + } + + } } diff --git a/BulletinSDK/Classes/Categories/String+Unicode.swift b/BulletinSDK/Classes/Categories/String+Unicode.swift new file mode 100644 index 0000000..ad41174 --- /dev/null +++ b/BulletinSDK/Classes/Categories/String+Unicode.swift @@ -0,0 +1,21 @@ +// +// String+Unicode.swift +// Bulletin +// +// Created by Daxesh Nagar on 26/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import Foundation + +extension String { + + var unicodeString: String? { + if let charCode = UInt32(self, radix: 16), + let unicode = UnicodeScalar(charCode) { + let str = String(unicode) + return str + } + return nil + } +} diff --git a/BulletinSDK/Classes/Categories/UIView+SubviewAspectSize.swift b/BulletinSDK/Classes/Categories/UIView+SubviewAspectSize.swift new file mode 100644 index 0000000..6964744 --- /dev/null +++ b/BulletinSDK/Classes/Categories/UIView+SubviewAspectSize.swift @@ -0,0 +1,55 @@ +// +// UIView+SubviewAspectSize.swift +// Bulletin +// +// Created by Daxesh Nagar on 26/07/23. +// Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. +// + +import UIKit + +extension UIView { + + // Get Subview Aspect Size + func subviewAspectSizeWithPadding(padding: CGFloat, subviewWidth: CGFloat, subviewHeight: CGFloat) -> CGSize { + let viewWidth = frame.size.width - padding + let viewHeight = ((frame.size.width - padding) / frame.size.width) * frame.size.height + + var newSubviewWidth: CGFloat = 0 + var newSubviewHeight: CGFloat = 0 + + if viewWidth < viewHeight { + // View Height Is Large + newSubviewWidth = viewWidth + newSubviewHeight = getNewSubviewHeight(using: newSubviewWidth, subviewWidth: subviewWidth, subviewHeight: subviewHeight) + + if newSubviewHeight > viewHeight { + newSubviewHeight = viewHeight + newSubviewWidth = getNewSubviewWidth(using: newSubviewHeight, subviewWidth: subviewWidth, subviewHeight: subviewHeight) + } + } + else { + // View Width Is Large + newSubviewHeight = viewHeight + newSubviewWidth = getNewSubviewWidth(using: newSubviewHeight, subviewWidth: subviewWidth, subviewHeight: subviewHeight) + + if newSubviewWidth > viewWidth { + newSubviewWidth = viewWidth + newSubviewHeight = getNewSubviewHeight(using: newSubviewWidth, subviewWidth: subviewWidth, subviewHeight: subviewHeight) + } + } + + return CGSize(width: newSubviewWidth, height: newSubviewHeight) + } + + // Get New Subview Proportional Width + func getNewSubviewWidth(using newSubviewHeight: CGFloat, subviewWidth: CGFloat, subviewHeight: CGFloat) -> CGFloat { + return (newSubviewHeight * subviewWidth) / subviewHeight + } + + // Get New Subview Proportional Height + func getNewSubviewHeight(using newSubviewWidth: CGFloat, subviewWidth: CGFloat, subviewHeight: CGFloat) -> CGFloat { + return (newSubviewWidth * subviewHeight) / subviewWidth + } +} + diff --git a/BulletinSDK/Classes/Constants/Appearance.swift b/BulletinSDK/Classes/Constants/Appearance.swift index 336965a..6cff720 100644 --- a/BulletinSDK/Classes/Constants/Appearance.swift +++ b/BulletinSDK/Classes/Constants/Appearance.swift @@ -18,10 +18,13 @@ public enum Appearance: String { // MARK: Constants public static func DefaultAnimationDuration() -> TimeInterval { return 0.3 } + // public static func Current() -> Appearance { // return UserDefaults.lastSelectedAppearance() ?? .darkKnight // } + + // MARK: Conveniance Method To Apply Any Appearance public func apply(shouldBroadcastUpdate: Bool) { @@ -35,20 +38,20 @@ public enum Appearance: String { } - // Update UISwitch Appearance - UISwitch.appearance().onTintColor = AppStyle.Color.SwitchOn - - // Update UITextField Appearance - UITextField.appearance().keyboardAppearance = AppStyle.KeyboardAppearance - UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedString.Key.foregroundColor: AppStyle.Color.PrimaryText] - UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = AppStyle.Color.SelectedElement - - // Update UIRefreshControl Appearance - UIRefreshControl.appearance().tintColor = AppStyle.Color.Loader - - // Update UIToolBar Appearance - UIToolbar.appearance().tintColor = AppStyle.Color.SelectedElement - UIToolbar.appearance().barTintColor = AppStyle.Color.Background +// // Update UISwitch Appearance +// UISwitch.appearance().onTintColor = AppStyle.Color.SwitchOn +// +// // Update UITextField Appearance +// UITextField.appearance().keyboardAppearance = AppStyle.KeyboardAppearance +// UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedString.Key.foregroundColor: AppStyle.Color.PrimaryText] +// UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = AppStyle.Color.SelectedElement +// +// // Update UIRefreshControl Appearance +// UIRefreshControl.appearance().tintColor = AppStyle.Color.Loader +// +// // Update UIToolBar Appearance +// UIToolbar.appearance().tintColor = AppStyle.Color.SelectedElement +// UIToolbar.appearance().barTintColor = AppStyle.Color.Background // Broadcast Appearance Update Notification diff --git a/BulletinSDK/Classes/Constants/Text.swift b/BulletinSDK/Classes/Constants/Text.swift index d3d209b..4a16b6d 100644 --- a/BulletinSDK/Classes/Constants/Text.swift +++ b/BulletinSDK/Classes/Constants/Text.swift @@ -11,6 +11,9 @@ import UIKit public struct Text { // MARK: - Common Texts + public static func GotIt() -> String { return "OKay, Go it".localized() } + public static func WhatsNewInThisUpdate() -> String { return "What’s new in this update?".localized() } + public static func WazirX() -> String { return "WazirX".localized() } public static func Binance() -> String { return "Binance".localized() } public static func Close() -> String { return "Close".localized() } diff --git a/BulletinSDK/Modals/BulletinInfo.swift b/BulletinSDK/Modals/BulletinInfo.swift index a44a665..d5e41ea 100644 --- a/BulletinSDK/Modals/BulletinInfo.swift +++ b/BulletinSDK/Modals/BulletinInfo.swift @@ -7,8 +7,9 @@ // import UIKit +import IGListKit -class BulletinInfo { +class BulletinInfo : NSObject { // MARK: - Variables @@ -27,3 +28,13 @@ class BulletinInfo { } } +extension BulletinInfo : ListDiffable { + public func diffIdentifier() -> NSObjectProtocol { + return self + } + + public func isEqual(toDiffableObject object: ListDiffable?) -> Bool { + return isEqual(object) + } +} + diff --git a/BulletinSDK/View/ActionButton/ActionButtonViewCell.swift b/BulletinSDK/View/ActionButton/ActionButtonViewCell.swift index 89cc93a..9102ac0 100644 --- a/BulletinSDK/View/ActionButton/ActionButtonViewCell.swift +++ b/BulletinSDK/View/ActionButton/ActionButtonViewCell.swift @@ -22,24 +22,14 @@ class ActionButtonViewCell: BaseCollectionViewCell { public var item: ActionButton? { didSet { - - // Validation - guard let actionButtonItem = item else { - - // Reset To Nil - actionButton = nil - - return - } - - - // Open Gift Button - if let actionButtonTitle = actionButtonItem.title { - actionButton.setTitle(actionButtonTitle.uppercased(), for: .normal) - } - + updateUI() } } + + override var intrinsicContentSize: CGSize { + let size = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return CGSize(width: UIView.noIntrinsicMetric, height: size.height) + } override func awakeFromNib() { super.awakeFromNib() @@ -52,7 +42,7 @@ class ActionButtonViewCell: BaseCollectionViewCell { super.updateAppearance() // Set BackgroundColor - backgroundColor = AppStyle.Color.Background + backgroundColor = AppStyle.Color.MainBgSurface_Alt // Set Action Button Container View actionButton.setTitleColor(AppStyle.Color.BrandTextPrimary, for: .normal) @@ -62,6 +52,28 @@ class ActionButtonViewCell: BaseCollectionViewCell { } + private func updateUI() { + + // Validation + guard let actionButtonItem = item else { + + // Reset To Nil + actionButton = nil + + return + } + + + // Open Gift Button + if let actionButtonTitle = actionButtonItem.title { + actionButton.setTitle(actionButtonTitle.uppercased(), for: .normal) + } + + // Layout Cell + setNeedsLayout() + layoutIfNeeded() + } + @IBAction func updateProfileButtonTapped(_ sender: Any) { // Validation diff --git a/BulletinSDK/View/ActionButton/ActionButtonViewCell.xib b/BulletinSDK/View/ActionButton/ActionButtonViewCell.xib index 0abc524..86c0517 100644 --- a/BulletinSDK/View/ActionButton/ActionButtonViewCell.xib +++ b/BulletinSDK/View/ActionButton/ActionButtonViewCell.xib @@ -11,20 +11,20 @@ - + - + - + - + - + @@ -41,16 +41,15 @@ - - + - + diff --git a/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift index 938aa53..73fc7f8 100644 --- a/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift +++ b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift @@ -24,60 +24,14 @@ class BulletPointViewCell: BaseCollectionViewCell { public var item: BulletPoint? { didSet { - - // Validation - guard let bulletPointItem = item else { - - // Reset To Nil - bulletTitle = nil - bulletDesc = nil - bulletImageView = nil - bulletName = nil - - return - } - - switch (bulletPointItem.bullet?.bulletType) { - - case .unicode: - - let htmlAttributedString = bulletPointItem.bullet?.unicode?.html2AttributedString(usingFont: AppStyle.Font.SemiBold(size: 14.0), color: AppStyle.Color.SecondaryText)?.trailingNewlineChopped - - if let attributedMessage = htmlAttributedString { - - // Set Description Message - bulletTitle.attributedText = attributedMessage - } - - case .image: - - // Set downloaded Image Icon - if let iconUrl = bulletPointItem.bullet?.imageUrl { - bulletImageView.kf.setImage(with: iconUrl) { [weak self] (result) in - switch result { - case .success(let value): - - // Image downloaded - if let sourceUrl = value.source.url, - sourceUrl == iconUrl { - self?.bulletImageView.image = value.image - } - case .failure(let error): - print("\(error.localizedDescription)") - } - } - } - - case .none: - bulletTitle.isHidden = true - bulletDesc.isHidden = true - } - - bulletTitle.text = bulletPointItem.titleText - bulletDesc.text = bulletPointItem.subTitleText - + updateUI() } } + + override var intrinsicContentSize: CGSize { + let size = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return CGSize(width: UIView.noIntrinsicMetric, height: size.height) + } override func awakeFromNib() { super.awakeFromNib() @@ -88,18 +42,83 @@ class BulletPointViewCell: BaseCollectionViewCell { super.updateAppearance() // Set Background Color - backgroundColor = AppStyle.Color.Background + backgroundColor = AppStyle.Color.MainBgSurface_Alt // Set preTitle Label - bulletTitle.small_medium() - bulletTitle.textColor = AppStyle.Color.SuccessTextPrimary + bulletTitle.large_semibold() + bulletTitle.textColor = AppStyle.Color.MainTextPrimary // Set Title Label - bulletDesc.heading4_semibold() - bulletDesc.textColor = AppStyle.Color.MainTextPrimary + bulletDesc.base_regular() + bulletDesc.textColor = AppStyle.Color.MainTextSecondary + + // bulletName.heading4_semibold() + } + + private func updateUI() { + + // Validation + guard let bulletPointItem = item else { + + // Reset To Nil + bulletTitle = nil + bulletDesc = nil + bulletImageView = nil + bulletName = nil + + return + } + + switch (bulletPointItem.bullet?.bulletType) { + + case .unicode: + + if let test = bulletPointItem.bullet?.unicode?.unicodeString { + bulletName.text = test + } + + case .image: + + // Set downloaded Image Icon + if let iconUrl = bulletPointItem.bullet?.imageUrl { + bulletImageView.kf.setImage(with: iconUrl) { [weak self] (result) in + switch result { + case .success(let value): + + // Image downloaded + if let sourceUrl = value.source.url, + sourceUrl == iconUrl { + self?.bulletImageView.image = value.image + } + case .failure(let error): + print("\(error.localizedDescription)") + } + } + } + + case .none: + bulletTitle.isHidden = false + // bulletDesc.isHidden = true + } + + if let title = bulletPointItem.titleText { + bulletTitle.isHidden = false + bulletTitle.text = title + } else { + bulletTitle.isHidden = true + } + + if let subTitle = bulletPointItem.subTitleText { + bulletDesc.isHidden = false + bulletDesc.text = subTitle + } else { + bulletDesc.isHidden = true + } - bulletName.heading4_semibold() + // Layout Cell + setNeedsLayout() + layoutIfNeeded() } } diff --git a/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib b/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib index 6bd7b1d..23b4368 100644 --- a/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib +++ b/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib @@ -24,7 +24,7 @@ - + - + - - + diff --git a/BulletinSDK/View/Media/MediaViewCell.swift b/BulletinSDK/View/Media/MediaViewCell.swift index 2d1484e..b41c231 100644 --- a/BulletinSDK/View/Media/MediaViewCell.swift +++ b/BulletinSDK/View/Media/MediaViewCell.swift @@ -23,39 +23,77 @@ class MediaViewCell: BaseCollectionViewCell { public var item: Media? { didSet { - - // Validation - guard let mediaItem = item else { - - // Reset To Nil - artworkImageView.image = nil - return - } - - // Set downloaded Image Icon - if let iconUrl = mediaItem.url { - artworkImageView.kf.setImage(with: iconUrl) { [weak self] (result) in - switch result { - case .success(let value): - - // Image downloaded - if let sourceUrl = value.source.url, - sourceUrl == iconUrl { - self?.artworkImageView.image = value.image - } - case .failure(let error): - print("\(error.localizedDescription)") - } - } - } - + updateUI() } } + + override func layoutSubviews() { + super.layoutSubviews() + + // Setting Frame Of Inside Container SubView Of Cell +// let size = subviewAspectSizeWithPadding(padding: 0, subviewWidth: item?.size?.width ?? 0.0, subviewHeight: 135.0) +// cardViewContainer.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) +// artworkImageView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) +// +// // Set Container View In Center Of Cell +// cardViewContainer.center = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) + } override func awakeFromNib() { super.awakeFromNib() // Initialization code + + // Resize All InnerViews According To Content And Sent Width Height Of Cell + cardViewContainer.autoresizesSubviews = true + for _viewInner in cardViewContainer.subviews { + + _viewInner.autoresizesSubviews = true; + _viewInner.autoresizingMask = [.flexibleWidth, .flexibleHeight, .flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin] + } } + private func updateUI() { + + // Validation + guard let mediaItem = item else { + + // Reset To Nil + artworkImageView.image = nil + return + } + + // Set downloaded Image Icon + if let iconUrl = mediaItem.url { + artworkImageView.kf.setImage(with: iconUrl) { [weak self] (result) in + switch result { + case .success(let value): + + // Image downloaded + if let sourceUrl = value.source.url, + sourceUrl == iconUrl { + self?.artworkImageView.image = value.image + } + case .failure(let error): + print("\(error.localizedDescription)") + } + } + } + + // Setting Frame Of Inside Container SubView Of Cell + let size = subviewAspectSizeWithPadding(padding: 0, subviewWidth: item?.size?.width ?? 0.0, subviewHeight: item?.size?.height ?? 0.0) + cardViewContainer.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) + artworkImageView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) + + // Set Container View In Center Of Cell + cardViewContainer.center = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) + } + + override func updateAppearance() { + super.updateAppearance() + + // Set Background Color + backgroundColor = AppStyle.Color.SuccessTextPrimary + cardViewContainer.backgroundColor = AppStyle.Color.DangerTextPrimary + } } diff --git a/BulletinSDK/View/Media/MediaViewCell.xib b/BulletinSDK/View/Media/MediaViewCell.xib index c28774e..eb1cd65 100644 --- a/BulletinSDK/View/Media/MediaViewCell.xib +++ b/BulletinSDK/View/Media/MediaViewCell.xib @@ -11,19 +11,19 @@ - + - + - - + + - - - + + + @@ -35,11 +35,9 @@ + - - - diff --git a/BulletinSDK/View/Message/MessageViewCell.swift b/BulletinSDK/View/Message/MessageViewCell.swift index 9b73ca6..5a573c5 100644 --- a/BulletinSDK/View/Message/MessageViewCell.swift +++ b/BulletinSDK/View/Message/MessageViewCell.swift @@ -21,36 +21,14 @@ class MessageViewCell: BaseCollectionViewCell { public var item: Message? { didSet { - - // Validation - guard let messageItem = item else { - - // Reset To Nil - descTitle.text = nil - - return - } - - switch (messageItem.messageType) { - - case .html: - - let htmlAttributedString = messageItem.text?.html2AttributedString(usingFont: AppStyle.Font.SemiBold(size: 14.0), color: AppStyle.Color.SecondaryText)?.trailingNewlineChopped - - if let attributedMessage = htmlAttributedString { - - // Set Description Message - descTitle.attributedText = attributedMessage - } - - case .text: - - // Set Description Message - descTitle.text = messageItem.text - } - + updateUI() } } + + override var intrinsicContentSize: CGSize { + let size = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return CGSize(width: UIView.noIntrinsicMetric, height: size.height) + } override func awakeFromNib() { super.awakeFromNib() @@ -61,7 +39,7 @@ class MessageViewCell: BaseCollectionViewCell { super.updateAppearance() // Set Background Color - backgroundColor = AppStyle.Color.Background + backgroundColor = AppStyle.Color.MainBgSurface_Alt // Set Description Label descTitle.base_regular() @@ -69,4 +47,38 @@ class MessageViewCell: BaseCollectionViewCell { } + private func updateUI() { + + // Validation + guard let messageItem = item else { + + // Reset To Nil + descTitle.text = nil + + return + } + + switch (messageItem.messageType) { + + case .html: + + let htmlAttributedString = messageItem.text?.html2AttributedString(usingFont: AppStyle.Font.SemiBold(size: 14.0), color: AppStyle.Color.SecondaryText)?.trailingNewlineChopped + + if let attributedMessage = htmlAttributedString { + + // Set Description Message + descTitle.attributedText = attributedMessage + } + + case .text: + + // Set Description Message + descTitle.text = messageItem.text + } + + // Layout Cell + setNeedsLayout() + layoutIfNeeded() + } + } diff --git a/BulletinSDK/View/Message/MessageViewCell.xib b/BulletinSDK/View/Message/MessageViewCell.xib index 3c878cc..80d47ec 100644 --- a/BulletinSDK/View/Message/MessageViewCell.xib +++ b/BulletinSDK/View/Message/MessageViewCell.xib @@ -21,13 +21,13 @@ - + diff --git a/BulletinSDK/View/Title/TitleViewCell.swift b/BulletinSDK/View/Title/TitleViewCell.swift index 4af4f1e..d2f2762 100644 --- a/BulletinSDK/View/Title/TitleViewCell.swift +++ b/BulletinSDK/View/Title/TitleViewCell.swift @@ -25,29 +25,14 @@ class TitleViewCell: BaseCollectionViewCell { public var item: Title? { didSet { - - // Validation - guard let titleItem = item else { - - // Reset To Nil - preTitle.text = nil - Title.text = nil - subTitle.text = nil - - return - } - - // Set Version Number - preTitle.text = titleItem.preTitleText - - // Set Version Title - Title.text = titleItem.titleText - - // Set Version SubTitle - subTitle.text = titleItem.subTitleText - + updateUI() } } + + override var intrinsicContentSize: CGSize { + let size = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return CGSize(width: UIView.noIntrinsicMetric, height: size.height) + } override func awakeFromNib() { super.awakeFromNib() @@ -63,8 +48,7 @@ class TitleViewCell: BaseCollectionViewCell { super.updateAppearance() // Set Background Color - backgroundColor = AppStyle.Color.Background - + backgroundColor = AppStyle.Color.MainBgSurface_Alt // Set preTitle Label preTitle.small_medium() @@ -80,5 +64,32 @@ class TitleViewCell: BaseCollectionViewCell { } + private func updateUI() { + + // Validation + guard let titleItem = item else { + + // Reset To Nil + preTitle.text = nil + Title.text = nil + subTitle.text = nil + + return + } + + // Set Version Number + preTitle.text = titleItem.preTitleText + + // Set Version Title + Title.text = titleItem.titleText + + // Set Version SubTitle + subTitle.text = titleItem.subTitleText + + // Layout Cell + setNeedsLayout() + layoutIfNeeded() + } + } diff --git a/BulletinSDK/View/Title/TitleViewCell.xib b/BulletinSDK/View/Title/TitleViewCell.xib index 3f13804..3fcc97f 100644 --- a/BulletinSDK/View/Title/TitleViewCell.xib +++ b/BulletinSDK/View/Title/TitleViewCell.xib @@ -11,26 +11,26 @@ - + - + - + - + - diff --git a/Demo/Bulletin/Controllers/BulletinListSectionController.swift b/Demo/Bulletin/Controllers/BulletinListSectionController.swift index 8b2d855..fe5de9e 100644 --- a/Demo/Bulletin/Controllers/BulletinListSectionController.swift +++ b/Demo/Bulletin/Controllers/BulletinListSectionController.swift @@ -31,6 +31,10 @@ class BulletinListSectionController: ListSectionController { override init() { super.init() + // Set Minimum Spacing + minimumLineSpacing = 16 + + // Init Sizing Cells bulletPointViewCell = Bundle.main.loadNibNamed("BulletPointViewCell", owner: self, options: nil)?.first as? BulletPointViewCell actionButtonViewCell = Bundle.main.loadNibNamed("ActionButtonViewCell", owner: self, options: nil)?.first as? ActionButtonViewCell @@ -72,7 +76,7 @@ class BulletinListSectionController: ListSectionController { messageViewCell?.item = item as? Message // Calculate Height - let height = max(messageViewCell?.intrinsicContentSize.height ?? 0 , 54) + let height = max(messageViewCell?.intrinsicContentSize.height ?? 0 , 30) // Return Size return CGSize(width: context.containerSize.width, height: height) @@ -81,13 +85,11 @@ class BulletinListSectionController: ListSectionController { // Set Cell Item mediaViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) - mediaViewCell?.item = item as? Media - // Calculate Height - let height = max(mediaViewCell?.intrinsicContentSize.height ?? 0 , 54) + mediaViewCell?.item = item as? Media // Return Size - return CGSize(width: context.containerSize.width, height: height) + return CGSize(width: context.containerSize.width , height: context.containerSize.height) case .bulletPoint: @@ -96,7 +98,7 @@ class BulletinListSectionController: ListSectionController { bulletPointViewCell?.item = item as? BulletPoint // Calculate Height - let height = max(bulletPointViewCell?.intrinsicContentSize.height ?? 0 , 54) + let height = max(bulletPointViewCell?.intrinsicContentSize.height ?? 0 , 38) // Return Size return CGSize(width: context.containerSize.width, height: height) @@ -108,7 +110,7 @@ class BulletinListSectionController: ListSectionController { actionButtonViewCell?.item = item as? ActionButton // Calculate Height - let height = max(actionButtonViewCell?.intrinsicContentSize.height ?? 0 , 54) + let height = max(actionButtonViewCell?.intrinsicContentSize.height ?? 0 , 50) // Return Size return CGSize(width: context.containerSize.width, height: height) diff --git a/Demo/Bulletin/Controllers/BulletinListView.swift b/Demo/Bulletin/Controllers/BulletinListView.swift index 80e7f20..b49e483 100644 --- a/Demo/Bulletin/Controllers/BulletinListView.swift +++ b/Demo/Bulletin/Controllers/BulletinListView.swift @@ -13,8 +13,12 @@ class BulletinListView: UIView, BulletinSectionControllerDelegate { // MARK: - Variables @IBOutlet private var collectionView : UICollectionView! + @IBOutlet private var headerTitleLabel : UILabel! + @IBOutlet private var headerView : UIStackView! + @IBOutlet private var footerView : UIStackView! + @IBOutlet private var gotItButton : UIButton! - private var bulletinSection = BulletinSection() { + private var bulletinSection = [BulletinInfo]() { didSet { // Update Collection View DispatchQueue.main.async { [weak self] in @@ -24,13 +28,17 @@ class BulletinListView: UIView, BulletinSectionControllerDelegate { } } + internal var bulletinItems : [ListDiffable] { + return bulletinSection + } + internal lazy var adapter: ListAdapter = { return ListAdapter(updater: ListAdapterUpdater(), viewController: nil, workingRangeSize: 0) }() // MARK: - Initialisation Methods - public class func instance(bulletinSection: BulletinSection) -> BulletinListView { + public class func instance(items: [BulletinInfo]) -> BulletinListView { // Create Instance From XIB let className = String(describing: BulletinListView.self) @@ -40,14 +48,14 @@ class BulletinListView: UIView, BulletinSectionControllerDelegate { // Set View Flexibility view.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.translatesAutoresizingMaskIntoConstraints = true - view.bulletinSection = bulletinSection + view.bulletinSection = items return view } // Manually Create Instance let bulletinListView = BulletinListView() - bulletinListView.bulletinSection = bulletinSection + bulletinListView.bulletinSection = items return bulletinListView } @@ -58,6 +66,37 @@ class BulletinListView: UIView, BulletinSectionControllerDelegate { adapter.collectionView = collectionView adapter.dataSource = self adapter.collectionViewDelegate = self + + gotItButton.layer.cornerRadius = AppStyle.ButtonCornerRadius + + // Set Insets + collectionView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 64, right: 0) + + updateAppearance() + + updateContent() + } + + func updateAppearance() { + + // Set Default Properties + headerView.backgroundColor = AppStyle.Color.MainNavigationBg + footerView.backgroundColor = AppStyle.Color.MainBgSurface_Alt + gotItButton.backgroundColor = AppStyle.Color.BrandBgPrimary + + headerTitleLabel.textColor = AppStyle.Color.BrandTextOnPrimary + headerTitleLabel.large_semibold() + + gotItButton.setTitleColor(AppStyle.Color.BrandTextOnPrimary, for: .normal) + gotItButton.titleLabel?.base_semibold() + + } + + func updateContent() { + + // Set Default Proeprties + headerTitleLabel.text = Text.WhatsNewInThisUpdate() + gotItButton.setTitle(Text.GotIt().uppercased(), for: .normal) } // MARK: - Initialisation Methods @@ -89,9 +128,7 @@ class BulletinListView: UIView, BulletinSectionControllerDelegate { extension BulletinListView: ListAdapterDataSource { func objects(for listAdapter: ListAdapter) -> [ListDiffable] { - var items = [ListDiffable]() - items += [bulletinSection] as [ListDiffable] - return items + return bulletinItems } func listAdapter(_ listAdapter: ListAdapter, sectionControllerFor object: Any) -> ListSectionController { diff --git a/Demo/Bulletin/Controllers/BulletinListView.xib b/Demo/Bulletin/Controllers/BulletinListView.xib index c765b4b..24818d7 100644 --- a/Demo/Bulletin/Controllers/BulletinListView.xib +++ b/Demo/Bulletin/Controllers/BulletinListView.xib @@ -15,28 +15,72 @@ + + + + + + + - + - + + + + + + + + + + + + + - - - - + + + + + + + + + + + + + + + + diff --git a/Demo/Bulletin/Controllers/BulletinSection.swift b/Demo/Bulletin/Controllers/BulletinSection.swift index 2fef8b7..bd5c0a6 100644 --- a/Demo/Bulletin/Controllers/BulletinSection.swift +++ b/Demo/Bulletin/Controllers/BulletinSection.swift @@ -10,20 +10,21 @@ import UIKit import IGListKit -class BulletinSection : NSObject { - - - // MARK: - Variables - public var bulletinInfo = [BulletinInfo]() - -} - -extension BulletinSection : ListDiffable { - public func diffIdentifier() -> NSObjectProtocol { - return self - } - - public func isEqual(toDiffableObject object: ListDiffable?) -> Bool { - return isEqual(object) - } -} +//class BulletinSection : NSObject { +// +// +// // MARK: - Variables +// public var bulletinInfo = [BulletinInfo]() +// // public var items = [BulletinItem]() +// +//} +// +//extension BulletinSection : ListDiffable { +// public func diffIdentifier() -> NSObjectProtocol { +// return self +// } +// +// public func isEqual(toDiffableObject object: ListDiffable?) -> Bool { +// return isEqual(object) +// } +//} diff --git a/Demo/Bulletin/ViewController.swift b/Demo/Bulletin/ViewController.swift index 4fc58fd..8de7f8b 100644 --- a/Demo/Bulletin/ViewController.swift +++ b/Demo/Bulletin/ViewController.swift @@ -17,22 +17,21 @@ class ViewController: UIViewController { let dataSource = BulletinDataStore() // Register Multiple Versions - registerBulletingDetails(forVersion: Version("1.11")!, inDataStore: dataSource) - registerBulletingDetails(forVersion: Version("1.12")!, inDataStore: dataSource) - registerBulletingDetails(forVersion: Version("1.12.1")!, inDataStore: dataSource) - registerBulletingDetails(forVersion: Version("1.12.2")!, inDataStore: dataSource) - registerBulletingDetails(forVersion: Version("1.13")!, inDataStore: dataSource) - registerBulletingDetails(forVersion: Version("1.13.1")!, inDataStore: dataSource) - registerBulletingDetails(forVersion: Version("1.14")!, inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.11"), inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.12"), inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.12.1"), inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.12.2"), inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.13"), inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.13.1"), inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.14"), inDataStore: dataSource) - let sdk = BulletinSDK(dataStore: dataSource) + let sdk = BulletinSDK(dataStore: dataSource,appearance: Appearance.whiteKnight) let bulletinView = sdk.getFullBulletin() - let _ = sdk.getLastBulletins(limit: 5) - let _ = sdk.getUnseenBulletins(limit: 4) +// let _ = sdk.getLastBulletins(limit: 5) +// let _ = sdk.getUnseenBulletins(limit: 4) if let bulletinView = bulletinView { - bulletinView.backgroundColor = UIColor.red self.view.addSubview(bulletinView) } @@ -52,20 +51,29 @@ class ViewController: UIViewController { // } } - private func registerBulletingDetails(forVersion version: Version, inDataStore dataStore: BulletinDataStore) { + private func registerBulletingDetails(forVersion version: Version?, inDataStore dataStore: BulletinDataStore) { + + // Validation + guard let version = version else { + return + } let title2 = Title() title2.preTitleText = "Version " + version.version title2.titleText = "In this update" - title2.subTitleText = "loreum ipsum loreum ipsum loreum ipsum" + title2.subTitleText = "Vestibulum id ligula porta felis euismod semper. Vesti bu lum id ligula porta felis euismod semper." let message2 = Message() message2.messageType = .text - message2.text = "Vestibulum id ligula porta felis euismod semper." + message2.text = "Vestibulum id ligula porta felis euismod semper. Morbi leo risus, porta ac consectetur ac, vestibulum at eros." + + let media = Media() + media.size = CGSize(width: 300.0, height: 300.0) + media.url = URL(string:"https://media.wazirx.com/test_resources/crypto_gifts.png") let bullet2 = Bullet() bullet2.bulletType = .unicode - bullet2.unicode = "U+0031" + bullet2.unicode = "1f44d" let bulletPoint21 = BulletPoint() bulletPoint21.bullet = bullet2 @@ -76,9 +84,12 @@ class ViewController: UIViewController { bulletPoint22.bullet = bullet2 bulletPoint22.titleText = "Justo Condimentum" bulletPoint22.subTitleText = "Sed posuere consectetur est at lobortis. Vivamus sagittis lacus vel augue laoreet rutrum faucibus dolor auctor." + + let actionButton = ActionButton() + actionButton.title = "Take me to Crypto Gifts" // Create Items List - let bulletItems2 = [title2, message2, bulletPoint21, bulletPoint22] + let bulletItems2 = [title2, message2, actionButton,media, bulletPoint21]// [title2, message2, bulletPoint21, bulletPoint22] // Register Item dataStore.registerVersionInfo(version: version, items: bulletItems2) From 0f3e6e6798f1fa37710354f40bc42774eeed896c Mon Sep 17 00:00:00 2001 From: Daxesh Date: Mon, 31 Jul 2023 15:32:06 +0530 Subject: [PATCH 08/12] minor correction --- .../Classes/Categories/String+HTML.swift | 1 + .../Categories/UIView+SubviewAspectSize.swift | 11 +++ .../ActionButton/ActionButtonViewCell.swift | 5 +- .../ActionButton/ActionButtonViewCell.xib | 4 + .../BulletPoint/BulletPointViewCell.swift | 21 +++-- BulletinSDK/View/Media/MediaViewCell.swift | 81 +++++++++++-------- BulletinSDK/View/Media/MediaViewCell.xib | 30 +++---- .../View/Message/MessageViewCell.swift | 41 ++++++---- BulletinSDK/View/Message/MessageViewCell.xib | 2 +- BulletinSDK/View/Title/TitleViewCell.swift | 52 +++++++----- BulletinSDK/View/Title/TitleViewCell.xib | 6 +- Demo/Bulletin.xcodeproj/project.pbxproj | 12 ++- .../BulletinListSectionController.swift | 41 ++++++---- .../Controllers/BulletinListView.swift | 32 +++++--- .../Bulletin/Controllers/BulletinListView.xib | 24 +++--- .../Controllers/BulletinSection.swift | 30 ------- Demo/Bulletin/ViewController.swift | 47 ++++++++--- 17 files changed, 263 insertions(+), 177 deletions(-) delete mode 100644 Demo/Bulletin/Controllers/BulletinSection.swift diff --git a/BulletinSDK/Classes/Categories/String+HTML.swift b/BulletinSDK/Classes/Categories/String+HTML.swift index 88780ac..cb0e2e3 100644 --- a/BulletinSDK/Classes/Categories/String+HTML.swift +++ b/BulletinSDK/Classes/Categories/String+HTML.swift @@ -32,6 +32,7 @@ extension String { print("error:", error) return nil } + } } diff --git a/BulletinSDK/Classes/Categories/UIView+SubviewAspectSize.swift b/BulletinSDK/Classes/Categories/UIView+SubviewAspectSize.swift index 6964744..e2cdabe 100644 --- a/BulletinSDK/Classes/Categories/UIView+SubviewAspectSize.swift +++ b/BulletinSDK/Classes/Categories/UIView+SubviewAspectSize.swift @@ -42,6 +42,17 @@ extension UIView { return CGSize(width: newSubviewWidth, height: newSubviewHeight) } + // Get Subview Aspect Height + func getAspectHeightForSubView(subviewWidth: CGFloat, subviewHeight: CGFloat) -> CGFloat { + let viewWidth = frame.size.width + let viewHeight = frame.size.height + + var newSubviewWidth: CGFloat = viewWidth + var newSubviewHeight: CGFloat = getNewSubviewHeight(using: newSubviewWidth, subviewWidth: subviewWidth, subviewHeight: subviewHeight) + + return newSubviewHeight + } + // Get New Subview Proportional Width func getNewSubviewWidth(using newSubviewHeight: CGFloat, subviewWidth: CGFloat, subviewHeight: CGFloat) -> CGFloat { return (newSubviewHeight * subviewWidth) / subviewHeight diff --git a/BulletinSDK/View/ActionButton/ActionButtonViewCell.swift b/BulletinSDK/View/ActionButton/ActionButtonViewCell.swift index 9102ac0..880028b 100644 --- a/BulletinSDK/View/ActionButton/ActionButtonViewCell.swift +++ b/BulletinSDK/View/ActionButton/ActionButtonViewCell.swift @@ -17,7 +17,6 @@ class ActionButtonViewCell: BaseCollectionViewCell { // MARK: - Variables @IBOutlet private var actionButton: PushButton! - public weak var delegate: ActionButtonViewCellDelegate? public var item: ActionButton? { @@ -31,6 +30,7 @@ class ActionButtonViewCell: BaseCollectionViewCell { return CGSize(width: UIView.noIntrinsicMetric, height: size.height) } + // MARK: - Initialisation Methods override func awakeFromNib() { super.awakeFromNib() @@ -52,6 +52,7 @@ class ActionButtonViewCell: BaseCollectionViewCell { } + //MARK: - Helper Methods private func updateUI() { // Validation @@ -74,7 +75,7 @@ class ActionButtonViewCell: BaseCollectionViewCell { layoutIfNeeded() } - @IBAction func updateProfileButtonTapped(_ sender: Any) { + @IBAction func actionButtonTapped(_ sender: Any) { // Validation guard let item = item else { return } diff --git a/BulletinSDK/View/ActionButton/ActionButtonViewCell.xib b/BulletinSDK/View/ActionButton/ActionButtonViewCell.xib index 86c0517..a45a461 100644 --- a/BulletinSDK/View/ActionButton/ActionButtonViewCell.xib +++ b/BulletinSDK/View/ActionButton/ActionButtonViewCell.xib @@ -31,6 +31,9 @@ + + + @@ -41,6 +44,7 @@ + diff --git a/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift index 73fc7f8..f926b08 100644 --- a/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift +++ b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift @@ -33,6 +33,7 @@ class BulletPointViewCell: BaseCollectionViewCell { return CGSize(width: UIView.noIntrinsicMetric, height: size.height) } + // MARK: - Initialisation Methods override func awakeFromNib() { super.awakeFromNib() // Initialization code @@ -56,6 +57,7 @@ class BulletPointViewCell: BaseCollectionViewCell { // bulletName.heading4_semibold() } + //MARK: - Helper Methods private func updateUI() { // Validation @@ -74,13 +76,14 @@ class BulletPointViewCell: BaseCollectionViewCell { case .unicode: - if let test = bulletPointItem.bullet?.unicode?.unicodeString { - bulletName.text = test + // Set Unicode Icon + if let bulletUnicode = bulletPointItem.bullet?.unicode?.unicodeString { + bulletName.text = bulletUnicode } case .image: - // Set downloaded Image Icon + // Set Image Icon if let iconUrl = bulletPointItem.bullet?.imageUrl { bulletImageView.kf.setImage(with: iconUrl) { [weak self] (result) in switch result { @@ -89,26 +92,32 @@ class BulletPointViewCell: BaseCollectionViewCell { // Image downloaded if let sourceUrl = value.source.url, sourceUrl == iconUrl { + self?.bulletImageView.isHidden = true self?.bulletImageView.image = value.image } case .failure(let error): print("\(error.localizedDescription)") } } + } else { + bulletImageView.image = nil + bulletImageView.isHidden = true } case .none: - bulletTitle.isHidden = false - // bulletDesc.isHidden = true + bulletImageView.isHidden = true + bulletName.isHidden = true } - if let title = bulletPointItem.titleText { + // Set Bullet Title + if let title = bulletPointItem.titleText,title.isEmpty == false { bulletTitle.isHidden = false bulletTitle.text = title } else { bulletTitle.isHidden = true } + // Set Bullet Desc if let subTitle = bulletPointItem.subTitleText { bulletDesc.isHidden = false bulletDesc.text = subTitle diff --git a/BulletinSDK/View/Media/MediaViewCell.swift b/BulletinSDK/View/Media/MediaViewCell.swift index b41c231..1782c52 100644 --- a/BulletinSDK/View/Media/MediaViewCell.swift +++ b/BulletinSDK/View/Media/MediaViewCell.swift @@ -16,76 +16,87 @@ protocol MediaViewCellDelegate: AnyObject { class MediaViewCell: BaseCollectionViewCell { // MARK: - Variables - @IBOutlet public var cardViewContainer: UIView! - @IBOutlet private var artworkImageView: UIImageView! + // @IBOutlet private weak var cardViewContainer: UIView! + @IBOutlet private weak var mediaImageView: UIImageView! + @IBOutlet private weak var imageViewHeightConstraint: NSLayoutConstraint! public weak var delegate: MediaViewCellDelegate? public var item: Media? { didSet { - updateUI() + self.updateUI() } } - override func layoutSubviews() { - super.layoutSubviews() - - // Setting Frame Of Inside Container SubView Of Cell -// let size = subviewAspectSizeWithPadding(padding: 0, subviewWidth: item?.size?.width ?? 0.0, subviewHeight: 135.0) -// cardViewContainer.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) -// artworkImageView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) -// -// // Set Container View In Center Of Cell -// cardViewContainer.center = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) + override var intrinsicContentSize: CGSize { + let size = contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return CGSize(width: UIView.noIntrinsicMetric, height: size.height) } - + + + // MARK: - Initialisation Methods override func awakeFromNib() { super.awakeFromNib() // Initialization code + } + + override func layoutSubviews() { + super.layoutSubviews() - // Resize All InnerViews According To Content And Sent Width Height Of Cell - cardViewContainer.autoresizesSubviews = true - for _viewInner in cardViewContainer.subviews { - - _viewInner.autoresizesSubviews = true; - _viewInner.autoresizingMask = [.flexibleWidth, .flexibleHeight, .flexibleLeftMargin, .flexibleRightMargin, .flexibleTopMargin, .flexibleBottomMargin] - } + // Setting Frame Of Inside Container SubView Of Cell +// if let item = item, +// let itemSize = item.size { +// let height : CGFloat = (contentView.frame.width * itemSize.height) / itemSize.width +// imageViewHeightConstraint.constant = height +// } else { +// imageViewHeightConstraint.constant = 135 +// } } + //MARK: - Helper Methods private func updateUI() { + // Reset To Nil + mediaImageView.kf.cancelDownloadTask() + mediaImageView.image = nil + // Validation guard let mediaItem = item else { - - // Reset To Nil - artworkImageView.image = nil return } - // Set downloaded Image Icon + // Set Image if let iconUrl = mediaItem.url { - artworkImageView.kf.setImage(with: iconUrl) { [weak self] (result) in + mediaImageView.kf.setImage(with: iconUrl) { [weak self] (result) in switch result { case .success(let value): // Image downloaded - if let sourceUrl = value.source.url, + if let weakSelf = self, + let sourceUrl = value.source.url, sourceUrl == iconUrl { - self?.artworkImageView.image = value.image + weakSelf.mediaImageView.image = value.image + weakSelf.mediaImageView.isHidden = false } case .failure(let error): print("\(error.localizedDescription)") } } + } else { + mediaImageView.isHidden = true } // Setting Frame Of Inside Container SubView Of Cell - let size = subviewAspectSizeWithPadding(padding: 0, subviewWidth: item?.size?.width ?? 0.0, subviewHeight: item?.size?.height ?? 0.0) - cardViewContainer.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) - artworkImageView.frame = CGRect(x: 0, y: 0, width: size.width, height: size.height) - - // Set Container View In Center Of Cell - cardViewContainer.center = CGPoint(x: bounds.size.width / 2.0, y: bounds.size.height / 2.0) + if let itemSize = mediaItem.size { + let height : CGFloat = (frame.width * itemSize.height) / itemSize.width + imageViewHeightConstraint.constant = height + } else { + imageViewHeightConstraint.constant = 135 + } + + // Layout Cell + layoutIfNeeded() + } override func updateAppearance() { @@ -93,7 +104,7 @@ class MediaViewCell: BaseCollectionViewCell { // Set Background Color backgroundColor = AppStyle.Color.SuccessTextPrimary - cardViewContainer.backgroundColor = AppStyle.Color.DangerTextPrimary + // cardViewContainer.backgroundColor = AppStyle.Color.DangerTextPrimary } } diff --git a/BulletinSDK/View/Media/MediaViewCell.xib b/BulletinSDK/View/Media/MediaViewCell.xib index eb1cd65..4e158af 100644 --- a/BulletinSDK/View/Media/MediaViewCell.xib +++ b/BulletinSDK/View/Media/MediaViewCell.xib @@ -10,32 +10,32 @@ - + - - + - - - - - - - - - - + + + + + + + + + + + - - + + diff --git a/BulletinSDK/View/Message/MessageViewCell.swift b/BulletinSDK/View/Message/MessageViewCell.swift index 5a573c5..68208cf 100644 --- a/BulletinSDK/View/Message/MessageViewCell.swift +++ b/BulletinSDK/View/Message/MessageViewCell.swift @@ -15,7 +15,7 @@ protocol MessageViewCellDelegate: AnyObject { class MessageViewCell: BaseCollectionViewCell { // MARK: - Variables - @IBOutlet private var descTitle: UILabel! + @IBOutlet private var descMsgLabel: UILabel! public weak var delegate: MessageViewCellDelegate? @@ -30,6 +30,7 @@ class MessageViewCell: BaseCollectionViewCell { return CGSize(width: UIView.noIntrinsicMetric, height: size.height) } + // MARK: - Initialisation Methods override func awakeFromNib() { super.awakeFromNib() // Initialization code @@ -37,23 +38,24 @@ class MessageViewCell: BaseCollectionViewCell { override func updateAppearance() { super.updateAppearance() - + // Set Background Color backgroundColor = AppStyle.Color.MainBgSurface_Alt - + // Set Description Label - descTitle.base_regular() - descTitle.textColor = AppStyle.Color.MainTextPrimary - + descMsgLabel.base_regular() + descMsgLabel.textColor = AppStyle.Color.MainTextPrimary + } + //MARK: - Helper Methods private func updateUI() { // Validation guard let messageItem = item else { // Reset To Nil - descTitle.text = nil + descMsgLabel.text = nil return } @@ -62,18 +64,27 @@ class MessageViewCell: BaseCollectionViewCell { case .html: - let htmlAttributedString = messageItem.text?.html2AttributedString(usingFont: AppStyle.Font.SemiBold(size: 14.0), color: AppStyle.Color.SecondaryText)?.trailingNewlineChopped - - if let attributedMessage = htmlAttributedString { - - // Set Description Message - descTitle.attributedText = attributedMessage + // Set Html Description Message + if let descMessage = messageItem.text, + descMessage.isEmpty == false, + let htmlAttributedString = descMessage.html2AttributedString(usingFont: nil, color: AppStyle.Color.SecondaryText)?.trailingNewlineChopped + { + descMsgLabel.attributedText = htmlAttributedString + descMsgLabel.isHidden = false + } else { + descMsgLabel.isHidden = true } case .text: - + // Set Description Message - descTitle.text = messageItem.text + if let descMessage = messageItem.text,descMessage.isEmpty == false { + descMsgLabel.text = descMessage + descMsgLabel.isHidden = false + } else { + descMsgLabel.isHidden = true + } + } // Layout Cell diff --git a/BulletinSDK/View/Message/MessageViewCell.xib b/BulletinSDK/View/Message/MessageViewCell.xib index 80d47ec..90cebc0 100644 --- a/BulletinSDK/View/Message/MessageViewCell.xib +++ b/BulletinSDK/View/Message/MessageViewCell.xib @@ -40,7 +40,7 @@ - + diff --git a/BulletinSDK/View/Title/TitleViewCell.swift b/BulletinSDK/View/Title/TitleViewCell.swift index d2f2762..4d6f308 100644 --- a/BulletinSDK/View/Title/TitleViewCell.swift +++ b/BulletinSDK/View/Title/TitleViewCell.swift @@ -15,14 +15,12 @@ protocol TitleViewCellDelegate: AnyObject { class TitleViewCell: BaseCollectionViewCell { // MARK: - Variables - @IBOutlet private var actionContainerStackView: UIStackView! - @IBOutlet private var preTitle: UILabel! - @IBOutlet private var Title: UILabel! - @IBOutlet private var subTitle: UILabel! + @IBOutlet private var preTitleLabel: UILabel! + @IBOutlet private var titleLabel: UILabel! + @IBOutlet private var subTitleLabel: UILabel! public weak var delegate: TitleViewCellDelegate? - public var item: Title? { didSet { updateUI() @@ -34,6 +32,7 @@ class TitleViewCell: BaseCollectionViewCell { return CGSize(width: UIView.noIntrinsicMetric, height: size.height) } + // MARK: - Initialisation Methods override func awakeFromNib() { super.awakeFromNib() // Initialization code @@ -51,41 +50,56 @@ class TitleViewCell: BaseCollectionViewCell { backgroundColor = AppStyle.Color.MainBgSurface_Alt // Set preTitle Label - preTitle.small_medium() - preTitle.textColor = AppStyle.Color.SuccessTextPrimary + preTitleLabel.small_medium() + preTitleLabel.textColor = AppStyle.Color.SuccessTextPrimary // Set Title Label - Title.heading4_semibold() - Title.textColor = AppStyle.Color.MainTextPrimary + titleLabel.heading4_semibold() + titleLabel.textColor = AppStyle.Color.MainTextPrimary // Set subTitle Label - subTitle.base_regular() - subTitle.textColor = AppStyle.Color.MainTextPrimary + subTitleLabel.base_regular() + subTitleLabel.textColor = AppStyle.Color.MainTextPrimary } + //MARK: - Helper Methods private func updateUI() { // Validation guard let titleItem = item else { // Reset To Nil - preTitle.text = nil - Title.text = nil - subTitle.text = nil - + preTitleLabel.text = nil + titleLabel.text = nil + subTitleLabel.text = nil return } // Set Version Number - preTitle.text = titleItem.preTitleText + if let preTitle = titleItem.preTitleText,preTitle.isEmpty == false { + preTitleLabel.text = preTitle + preTitleLabel.isHidden = false + } else { + preTitleLabel.isHidden = true + } // Set Version Title - Title.text = titleItem.titleText + if let title = titleItem.titleText,title.isEmpty == false { + titleLabel.text = title + titleLabel.isHidden = false + } else { + titleLabel.isHidden = true + } // Set Version SubTitle - subTitle.text = titleItem.subTitleText - + if let subTitle = titleItem.subTitleText,subTitle.isEmpty == false { + subTitleLabel.text = subTitle + subTitleLabel.isHidden = false + } else { + subTitleLabel.isHidden = true + } + // Layout Cell setNeedsLayout() layoutIfNeeded() diff --git a/BulletinSDK/View/Title/TitleViewCell.xib b/BulletinSDK/View/Title/TitleViewCell.xib index 3fcc97f..a0e3aa4 100644 --- a/BulletinSDK/View/Title/TitleViewCell.xib +++ b/BulletinSDK/View/Title/TitleViewCell.xib @@ -57,9 +57,9 @@ - - - + + + diff --git a/Demo/Bulletin.xcodeproj/project.pbxproj b/Demo/Bulletin.xcodeproj/project.pbxproj index 632eeaf..2fc618c 100644 --- a/Demo/Bulletin.xcodeproj/project.pbxproj +++ b/Demo/Bulletin.xcodeproj/project.pbxproj @@ -26,13 +26,14 @@ 73B17D612A6649240085EB1A /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B17D5E2A6649240085EB1A /* Text.swift */; }; 73B17D622A6649240085EB1A /* Appearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B17D5F2A6649240085EB1A /* Appearance.swift */; }; 73CC10192A650D1600BB08C5 /* BaseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73CC10182A650D1600BB08C5 /* BaseCollectionViewCell.swift */; }; + 73D8C8412A715A6400652504 /* String+Unicode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D8C8402A715A6400652504 /* String+Unicode.swift */; }; + 73D8C8432A7164C700652504 /* UIView+SubviewAspectSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D8C8422A7164C700652504 /* UIView+SubviewAspectSize.swift */; }; 73E824F92A6A4F72007AA8D7 /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E824F82A6A4F72007AA8D7 /* String+HTML.swift */; }; 73E824FB2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E824FA2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift */; }; 73E824FF2A6A67F5007AA8D7 /* PushButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E824FE2A6A67F5007AA8D7 /* PushButton.swift */; }; 73F62F782A6E2CF0007CBC36 /* BottomSheetSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F62F772A6E2CF0007CBC36 /* BottomSheetSegue.swift */; }; 73F62F7A2A6E2D0E007CBC36 /* AlertSegue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F62F792A6E2D0E007CBC36 /* AlertSegue.swift */; }; 73F62F7C2A6E4D79007CBC36 /* BulletinHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F62F7B2A6E4D79007CBC36 /* BulletinHelper.swift */; }; - 73F62F7E2A6E83AF007CBC36 /* BulletinSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73F62F7D2A6E83AF007CBC36 /* BulletinSection.swift */; }; 73F62F882A6E90B1007CBC36 /* IBMPlexSans-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73F62F822A6E90B1007CBC36 /* IBMPlexSans-Regular.ttf */; }; 73F62F892A6E90B1007CBC36 /* IBMPlexSans-SemiBold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73F62F832A6E90B1007CBC36 /* IBMPlexSans-SemiBold.ttf */; }; 73F62F8A2A6E90B1007CBC36 /* IBMPlexSans-SemiBoldItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 73F62F842A6E90B1007CBC36 /* IBMPlexSans-SemiBoldItalic.ttf */; }; @@ -82,13 +83,14 @@ 73B17D5E2A6649240085EB1A /* Text.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; 73B17D5F2A6649240085EB1A /* Appearance.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Appearance.swift; sourceTree = ""; }; 73CC10182A650D1600BB08C5 /* BaseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCollectionViewCell.swift; sourceTree = ""; }; + 73D8C8402A715A6400652504 /* String+Unicode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Unicode.swift"; sourceTree = ""; }; + 73D8C8422A7164C700652504 /* UIView+SubviewAspectSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+SubviewAspectSize.swift"; sourceTree = ""; }; 73E824F82A6A4F72007AA8D7 /* String+HTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = ""; }; 73E824FA2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Utility.swift"; sourceTree = ""; }; 73E824FE2A6A67F5007AA8D7 /* PushButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushButton.swift; sourceTree = ""; }; 73F62F772A6E2CF0007CBC36 /* BottomSheetSegue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomSheetSegue.swift; sourceTree = ""; }; 73F62F792A6E2D0E007CBC36 /* AlertSegue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertSegue.swift; sourceTree = ""; }; 73F62F7B2A6E4D79007CBC36 /* BulletinHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinHelper.swift; sourceTree = ""; }; - 73F62F7D2A6E83AF007CBC36 /* BulletinSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinSection.swift; sourceTree = ""; }; 73F62F822A6E90B1007CBC36 /* IBMPlexSans-Regular.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSans-Regular.ttf"; sourceTree = ""; }; 73F62F832A6E90B1007CBC36 /* IBMPlexSans-SemiBold.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSans-SemiBold.ttf"; sourceTree = ""; }; 73F62F842A6E90B1007CBC36 /* IBMPlexSans-SemiBoldItalic.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "IBMPlexSans-SemiBoldItalic.ttf"; sourceTree = ""; }; @@ -200,6 +202,8 @@ 732D1C302A66C6F500FB5BFE /* UILabel+FontStyle.swift */, 73E824FA2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift */, 73E824F82A6A4F72007AA8D7 /* String+HTML.swift */, + 73D8C8402A715A6400652504 /* String+Unicode.swift */, + 73D8C8422A7164C700652504 /* UIView+SubviewAspectSize.swift */, ); path = Categories; sourceTree = ""; @@ -210,7 +214,6 @@ 738B01EB2A67BCEE00CFDB09 /* BulletinListView.swift */, 738B01ED2A67BCFB00CFDB09 /* BulletinListView.xib */, 738B01EF2A6819C900CFDB09 /* BulletinListSectionController.swift */, - 73F62F7D2A6E83AF007CBC36 /* BulletinSection.swift */, ); path = Controllers; sourceTree = ""; @@ -546,6 +549,7 @@ 73B17D602A6649240085EB1A /* Constants.swift in Sources */, 73F62F782A6E2CF0007CBC36 /* BottomSheetSegue.swift in Sources */, 7315D8492A696EFC00B7CE4F /* BulletPointViewCell.swift in Sources */, + 73D8C8412A715A6400652504 /* String+Unicode.swift in Sources */, 815E3DB828DC884C0042CEA5 /* Title.swift in Sources */, 732D1C312A66C6F500FB5BFE /* UILabel+FontStyle.swift in Sources */, 73E824FF2A6A67F5007AA8D7 /* PushButton.swift in Sources */, @@ -560,12 +564,12 @@ 81E010E228DDA2D9003FFE79 /* UserDefaults+Storage.swift in Sources */, 815E3DB328DC884C0042CEA5 /* BulletinDataStore.swift in Sources */, 815E3DBB28DC884C0042CEA5 /* BulletinSDK.swift in Sources */, - 73F62F7E2A6E83AF007CBC36 /* BulletinSection.swift in Sources */, 819772DE28E40B8100962913 /* Version.swift in Sources */, 738B01F02A6819C900CFDB09 /* BulletinListSectionController.swift in Sources */, 7315D8592A69703700B7CE4F /* TitleViewCell.swift in Sources */, 73E824F92A6A4F72007AA8D7 /* String+HTML.swift in Sources */, 815E3DB228DC884C0042CEA5 /* String+Class.swift in Sources */, + 73D8C8432A7164C700652504 /* UIView+SubviewAspectSize.swift in Sources */, 819772E028E40C4800962913 /* String+Version.swift in Sources */, 815E3DB728DC884C0042CEA5 /* BulletPoint.swift in Sources */, 819772DC28E2CBEF00962913 /* BulletinInfo.swift in Sources */, diff --git a/Demo/Bulletin/Controllers/BulletinListSectionController.swift b/Demo/Bulletin/Controllers/BulletinListSectionController.swift index fe5de9e..c3c2646 100644 --- a/Demo/Bulletin/Controllers/BulletinListSectionController.swift +++ b/Demo/Bulletin/Controllers/BulletinListSectionController.swift @@ -11,7 +11,7 @@ import IGListKit import UIKit internal protocol BulletinSectionControllerDelegate: AnyObject { - // func bulletinSectionController(_ sectionController: BulletinListSectionController, didClickItem item: WZSettingsSectionBaseItem, withEventType eventType: WZSettingsSectionItemEventType) + func bulletinSectionController(_ sectionController: BulletinListSectionController, didClickItem item: BulletinItem) } class BulletinListSectionController: ListSectionController { @@ -33,6 +33,7 @@ class BulletinListSectionController: ListSectionController { // Set Minimum Spacing minimumLineSpacing = 16 + inset = UIEdgeInsets(top: 16, left: 0, bottom:0, right: 0) // Init Sizing Cells @@ -64,7 +65,7 @@ class BulletinListSectionController: ListSectionController { titleViewCell?.item = item as? Title // Calculate Height - let height = max(titleViewCell?.intrinsicContentSize.height ?? 0 , 54) + let height = max(titleViewCell?.intrinsicContentSize.height ?? 0 , 0) // Return Size return CGSize(width: context.containerSize.width, height: height) @@ -72,24 +73,36 @@ class BulletinListSectionController: ListSectionController { case .message: // Set Cell Item - messageViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) + messageViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width - inset.left - inset.right, height: CGFloat.greatestFiniteMagnitude) messageViewCell?.item = item as? Message // Calculate Height - let height = max(messageViewCell?.intrinsicContentSize.height ?? 0 , 30) + let height = max(messageViewCell?.intrinsicContentSize.height ?? 0 , 0) // Return Size - return CGSize(width: context.containerSize.width, height: height) + return CGSize(width: context.containerSize.width - inset.left - inset.right, height: height) case .media: - // Set Cell Item - mediaViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) - + // Set Announcement Info + let cellWidth = context.containerSize.width - inset.left - inset.right + mediaViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: cellWidth, height: CGFloat.greatestFiniteMagnitude) mediaViewCell?.item = item as? Media + // Calculate Size + let size = CGSize(width: cellWidth, height: mediaViewCell?.intrinsicContentSize.height ?? 0.0) + + return size + +// // Set Cell Item +// mediaViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) +// mediaViewCell?.item = item as? Media +// +// // Calculate Height +// let height = max(mediaViewCell?.intrinsicContentSize.height ?? 0 , 0) + // Return Size - return CGSize(width: context.containerSize.width , height: context.containerSize.height) +// return CGSize(width: context.containerSize.width , height: height) case .bulletPoint: @@ -98,7 +111,7 @@ class BulletinListSectionController: ListSectionController { bulletPointViewCell?.item = item as? BulletPoint // Calculate Height - let height = max(bulletPointViewCell?.intrinsicContentSize.height ?? 0 , 38) + let height = max(bulletPointViewCell?.intrinsicContentSize.height ?? 0 , 0) // Return Size return CGSize(width: context.containerSize.width, height: height) @@ -110,7 +123,7 @@ class BulletinListSectionController: ListSectionController { actionButtonViewCell?.item = item as? ActionButton // Calculate Height - let height = max(actionButtonViewCell?.intrinsicContentSize.height ?? 0 , 50) + let height = max(actionButtonViewCell?.intrinsicContentSize.height ?? 0 , 0) // Return Size return CGSize(width: context.containerSize.width, height: height) @@ -166,7 +179,7 @@ class BulletinListSectionController: ListSectionController { } // Set Delegate - cell.delegate = self + // cell.delegate = self return cell @@ -271,7 +284,7 @@ extension BulletinListSectionController: MessageViewCellDelegate { extension BulletinListSectionController: ActionButtonViewCellDelegate { func actionButtonViewCell(_ cell: ActionButtonViewCell, ofItem item: BulletinItem) { - + delegate?.bulletinSectionController(self, didClickItem: item) } } @@ -280,7 +293,7 @@ extension BulletinListSectionController: ActionButtonViewCellDelegate { extension BulletinListSectionController: BulletPointViewCellDelegate { func bulletPointViewCell(_ cell: BulletPointViewCell, ofItem item: BulletinItem) { - + delegate?.bulletinSectionController(self, didClickItem: item) } } diff --git a/Demo/Bulletin/Controllers/BulletinListView.swift b/Demo/Bulletin/Controllers/BulletinListView.swift index b49e483..3131374 100644 --- a/Demo/Bulletin/Controllers/BulletinListView.swift +++ b/Demo/Bulletin/Controllers/BulletinListView.swift @@ -9,7 +9,11 @@ import UIKit import IGListKit -class BulletinListView: UIView, BulletinSectionControllerDelegate { +internal protocol BulletinListDelegate: AnyObject { + func BulletinList(didClickItem item: BulletinItem) +} + +class BulletinListView: UIViewController { // MARK: - Variables @IBOutlet private var collectionView : UICollectionView! @@ -17,16 +21,10 @@ class BulletinListView: UIView, BulletinSectionControllerDelegate { @IBOutlet private var headerView : UIStackView! @IBOutlet private var footerView : UIStackView! @IBOutlet private var gotItButton : UIButton! + public weak var delegate: BulletinListDelegate? - private var bulletinSection = [BulletinInfo]() { - didSet { - // Update Collection View - DispatchQueue.main.async { [weak self] in - // Perform Updates - self?.adapter.performUpdates(animated: true, completion: nil) - } - } - } + private var bulletinSection = [BulletinInfo]() + internal var bulletinItems : [ListDiffable] { return bulletinSection @@ -70,7 +68,7 @@ class BulletinListView: UIView, BulletinSectionControllerDelegate { gotItButton.layer.cornerRadius = AppStyle.ButtonCornerRadius // Set Insets - collectionView.contentInset = UIEdgeInsets(top: 20, left: 0, bottom: 64, right: 0) + collectionView.contentInset = UIEdgeInsets(top: 20, left: 16, bottom: 64, right: 16) updateAppearance() @@ -114,7 +112,7 @@ class BulletinListView: UIView, BulletinSectionControllerDelegate { } // MARK: - Helper Method - internal func performUpdates(animated: Bool, completion: ListUpdaterCompletion? = nil) { + public func reload(animated: Bool, completion: ListUpdaterCompletion? = nil) { DispatchQueue.main.async { [weak self] in // Perform Updates @@ -144,3 +142,13 @@ extension BulletinListView: ListAdapterDataSource { extension BulletinListView: UICollectionViewDelegate { public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {} } + +// MARK: - BulletinSectionControllerDelegate +extension BulletinListView: BulletinSectionControllerDelegate { + func bulletinSectionController(_ sectionController: BulletinListSectionController, didClickItem item: BulletinItem) { + + // Clicked Item + delegate?.BulletinList(didClickItem: item) + } + +} diff --git a/Demo/Bulletin/Controllers/BulletinListView.xib b/Demo/Bulletin/Controllers/BulletinListView.xib index 24818d7..5e4373a 100644 --- a/Demo/Bulletin/Controllers/BulletinListView.xib +++ b/Demo/Bulletin/Controllers/BulletinListView.xib @@ -16,10 +16,10 @@ - + - + @@ -38,13 +38,13 @@ - + - + - + + @@ -49,11 +50,11 @@ - + - + diff --git a/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift index f926b08..c69042f 100644 --- a/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift +++ b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift @@ -44,8 +44,7 @@ class BulletPointViewCell: BaseCollectionViewCell { // Set Background Color backgroundColor = AppStyle.Color.MainBgSurface_Alt - - + // Set preTitle Label bulletTitle.large_semibold() bulletTitle.textColor = AppStyle.Color.MainTextPrimary diff --git a/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib b/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib index 23b4368..709beb8 100644 --- a/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib +++ b/BulletinSDK/View/BulletPoint/BulletPointViewCell.xib @@ -21,10 +21,10 @@ - + - + - + - + + diff --git a/BulletinSDK/View/Media/MediaViewCell.swift b/BulletinSDK/View/Media/MediaViewCell.swift index 1782c52..24235a3 100644 --- a/BulletinSDK/View/Media/MediaViewCell.swift +++ b/BulletinSDK/View/Media/MediaViewCell.swift @@ -16,7 +16,7 @@ protocol MediaViewCellDelegate: AnyObject { class MediaViewCell: BaseCollectionViewCell { // MARK: - Variables - // @IBOutlet private weak var cardViewContainer: UIView! + @IBOutlet private weak var containerStackView: UIStackView! @IBOutlet private weak var mediaImageView: UIImageView! @IBOutlet private weak var imageViewHeightConstraint: NSLayoutConstraint! @@ -44,13 +44,13 @@ class MediaViewCell: BaseCollectionViewCell { super.layoutSubviews() // Setting Frame Of Inside Container SubView Of Cell -// if let item = item, -// let itemSize = item.size { -// let height : CGFloat = (contentView.frame.width * itemSize.height) / itemSize.width -// imageViewHeightConstraint.constant = height -// } else { -// imageViewHeightConstraint.constant = 135 -// } + if let item = item, + let itemSize = item.size { + let height : CGFloat = ((contentView.frame.width - containerStackView.layoutMargins.left - containerStackView.layoutMargins.right) * itemSize.height) / itemSize.width + imageViewHeightConstraint.constant = height + } else { + imageViewHeightConstraint.constant = 135 + } } //MARK: - Helper Methods @@ -86,13 +86,13 @@ class MediaViewCell: BaseCollectionViewCell { mediaImageView.isHidden = true } - // Setting Frame Of Inside Container SubView Of Cell - if let itemSize = mediaItem.size { - let height : CGFloat = (frame.width * itemSize.height) / itemSize.width - imageViewHeightConstraint.constant = height - } else { - imageViewHeightConstraint.constant = 135 - } +// // Setting Frame Of Inside Container SubView Of Cell +// if let itemSize = mediaItem.size { +// let height : CGFloat = (frame.width * itemSize.height) / itemSize.width +// imageViewHeightConstraint.constant = height +// } else { +// imageViewHeightConstraint.constant = 135 +// } // Layout Cell layoutIfNeeded() @@ -103,8 +103,7 @@ class MediaViewCell: BaseCollectionViewCell { super.updateAppearance() // Set Background Color - backgroundColor = AppStyle.Color.SuccessTextPrimary - // cardViewContainer.backgroundColor = AppStyle.Color.DangerTextPrimary + backgroundColor = AppStyle.Color.MainBgSurface_Alt } } diff --git a/BulletinSDK/View/Media/MediaViewCell.xib b/BulletinSDK/View/Media/MediaViewCell.xib index 4e158af..b1c38f2 100644 --- a/BulletinSDK/View/Media/MediaViewCell.xib +++ b/BulletinSDK/View/Media/MediaViewCell.xib @@ -11,33 +11,40 @@ - + - + - - - - - - - + + + + + + + + + + + + + - - - - + + + + - + - + + - + diff --git a/Demo/Bulletin.xcodeproj/project.pbxproj b/Demo/Bulletin.xcodeproj/project.pbxproj index 2fc618c..36aa0fe 100644 --- a/Demo/Bulletin.xcodeproj/project.pbxproj +++ b/Demo/Bulletin.xcodeproj/project.pbxproj @@ -19,8 +19,6 @@ 7315D85A2A69703700B7CE4F /* TitleViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 7315D8582A69703700B7CE4F /* TitleViewCell.xib */; }; 732D1C2E2A66BAEA00FB5BFE /* LocalizationHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 732D1C2D2A66BAEA00FB5BFE /* LocalizationHelper.swift */; }; 732D1C312A66C6F500FB5BFE /* UILabel+FontStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 732D1C302A66C6F500FB5BFE /* UILabel+FontStyle.swift */; }; - 738B01EC2A67BCEE00CFDB09 /* BulletinListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B01EB2A67BCEE00CFDB09 /* BulletinListView.swift */; }; - 738B01EE2A67BCFB00CFDB09 /* BulletinListView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 738B01ED2A67BCFB00CFDB09 /* BulletinListView.xib */; }; 738B01F02A6819C900CFDB09 /* BulletinListSectionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 738B01EF2A6819C900CFDB09 /* BulletinListSectionController.swift */; }; 73B17D602A6649240085EB1A /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B17D5D2A6649240085EB1A /* Constants.swift */; }; 73B17D612A6649240085EB1A /* Text.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73B17D5E2A6649240085EB1A /* Text.swift */; }; @@ -28,6 +26,8 @@ 73CC10192A650D1600BB08C5 /* BaseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73CC10182A650D1600BB08C5 /* BaseCollectionViewCell.swift */; }; 73D8C8412A715A6400652504 /* String+Unicode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D8C8402A715A6400652504 /* String+Unicode.swift */; }; 73D8C8432A7164C700652504 /* UIView+SubviewAspectSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D8C8422A7164C700652504 /* UIView+SubviewAspectSize.swift */; }; + 73E28D922A77E0DE002F73B3 /* BulletinListVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E28D902A77E0DE002F73B3 /* BulletinListVC.swift */; }; + 73E28D932A77E0DE002F73B3 /* BulletinListVC.xib in Resources */ = {isa = PBXBuildFile; fileRef = 73E28D912A77E0DE002F73B3 /* BulletinListVC.xib */; }; 73E824F92A6A4F72007AA8D7 /* String+HTML.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E824F82A6A4F72007AA8D7 /* String+HTML.swift */; }; 73E824FB2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E824FA2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift */; }; 73E824FF2A6A67F5007AA8D7 /* PushButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73E824FE2A6A67F5007AA8D7 /* PushButton.swift */; }; @@ -76,8 +76,6 @@ 7315D8582A69703700B7CE4F /* TitleViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = TitleViewCell.xib; sourceTree = ""; }; 732D1C2D2A66BAEA00FB5BFE /* LocalizationHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalizationHelper.swift; sourceTree = ""; }; 732D1C302A66C6F500FB5BFE /* UILabel+FontStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UILabel+FontStyle.swift"; sourceTree = ""; }; - 738B01EB2A67BCEE00CFDB09 /* BulletinListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinListView.swift; sourceTree = ""; }; - 738B01ED2A67BCFB00CFDB09 /* BulletinListView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BulletinListView.xib; sourceTree = ""; }; 738B01EF2A6819C900CFDB09 /* BulletinListSectionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinListSectionController.swift; sourceTree = ""; }; 73B17D5D2A6649240085EB1A /* Constants.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; 73B17D5E2A6649240085EB1A /* Text.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Text.swift; sourceTree = ""; }; @@ -85,6 +83,8 @@ 73CC10182A650D1600BB08C5 /* BaseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseCollectionViewCell.swift; sourceTree = ""; }; 73D8C8402A715A6400652504 /* String+Unicode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Unicode.swift"; sourceTree = ""; }; 73D8C8422A7164C700652504 /* UIView+SubviewAspectSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+SubviewAspectSize.swift"; sourceTree = ""; }; + 73E28D902A77E0DE002F73B3 /* BulletinListVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BulletinListVC.swift; sourceTree = ""; }; + 73E28D912A77E0DE002F73B3 /* BulletinListVC.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = BulletinListVC.xib; sourceTree = ""; }; 73E824F82A6A4F72007AA8D7 /* String+HTML.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+HTML.swift"; sourceTree = ""; }; 73E824FA2A6A51AD007AA8D7 /* NSAttributedString+Utility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Utility.swift"; sourceTree = ""; }; 73E824FE2A6A67F5007AA8D7 /* PushButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushButton.swift; sourceTree = ""; }; @@ -211,8 +211,8 @@ 732FF0E32A67A2F800904913 /* Controllers */ = { isa = PBXGroup; children = ( - 738B01EB2A67BCEE00CFDB09 /* BulletinListView.swift */, - 738B01ED2A67BCFB00CFDB09 /* BulletinListView.xib */, + 73E28D902A77E0DE002F73B3 /* BulletinListVC.swift */, + 73E28D912A77E0DE002F73B3 /* BulletinListVC.xib */, 738B01EF2A6819C900CFDB09 /* BulletinListSectionController.swift */, ); path = Controllers; @@ -483,8 +483,8 @@ 7315D85A2A69703700B7CE4F /* TitleViewCell.xib in Resources */, 73F62F882A6E90B1007CBC36 /* IBMPlexSans-Regular.ttf in Resources */, 7315D8562A696FE400B7CE4F /* MediaViewCell.xib in Resources */, + 73E28D932A77E0DE002F73B3 /* BulletinListVC.xib in Resources */, 73F62F8B2A6E90B1007CBC36 /* IBMPlexSans-Medium.ttf in Resources */, - 738B01EE2A67BCFB00CFDB09 /* BulletinListView.xib in Resources */, 815E3D8528DC85510042CEA5 /* LaunchScreen.storyboard in Resources */, 815E3D8228DC85510042CEA5 /* Assets.xcassets in Resources */, 815E3D8028DC85510042CEA5 /* Main.storyboard in Resources */, @@ -554,7 +554,6 @@ 732D1C312A66C6F500FB5BFE /* UILabel+FontStyle.swift in Sources */, 73E824FF2A6A67F5007AA8D7 /* PushButton.swift in Sources */, 73B17D622A6649240085EB1A /* Appearance.swift in Sources */, - 738B01EC2A67BCEE00CFDB09 /* BulletinListView.swift in Sources */, 815E3D7D28DC85510042CEA5 /* ViewController.swift in Sources */, 7315D8512A696FA100B7CE4F /* MessageViewCell.swift in Sources */, 73CC10192A650D1600BB08C5 /* BaseCollectionViewCell.swift in Sources */, @@ -562,6 +561,7 @@ 7315D84D2A696F4300B7CE4F /* ActionButtonViewCell.swift in Sources */, 815E3DB428DC884C0042CEA5 /* BulletinItem.swift in Sources */, 81E010E228DDA2D9003FFE79 /* UserDefaults+Storage.swift in Sources */, + 73E28D922A77E0DE002F73B3 /* BulletinListVC.swift in Sources */, 815E3DB328DC884C0042CEA5 /* BulletinDataStore.swift in Sources */, 815E3DBB28DC884C0042CEA5 /* BulletinSDK.swift in Sources */, 819772DE28E40B8100962913 /* Version.swift in Sources */, diff --git a/Demo/Bulletin/Base.lproj/Main.storyboard b/Demo/Bulletin/Base.lproj/Main.storyboard index 07d81af..3b9197c 100644 --- a/Demo/Bulletin/Base.lproj/Main.storyboard +++ b/Demo/Bulletin/Base.lproj/Main.storyboard @@ -24,6 +24,15 @@ + @@ -31,7 +40,7 @@ - + diff --git a/Demo/Bulletin/Controllers/BulletinListSectionController.swift b/Demo/Bulletin/Controllers/BulletinListSectionController.swift index c3c2646..22c47c1 100644 --- a/Demo/Bulletin/Controllers/BulletinListSectionController.swift +++ b/Demo/Bulletin/Controllers/BulletinListSectionController.swift @@ -33,7 +33,7 @@ class BulletinListSectionController: ListSectionController { // Set Minimum Spacing minimumLineSpacing = 16 - inset = UIEdgeInsets(top: 16, left: 0, bottom:0, right: 0) + inset = UIEdgeInsets(top: 20, left: 0, bottom:0, right: 0) // Init Sizing Cells @@ -60,15 +60,18 @@ class BulletinListSectionController: ListSectionController { case .title: + // Calculate Width + let width = context.containerSize.width - inset.left - inset.right + // Set Cell Item - titleViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) + titleViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: width, height: CGFloat.greatestFiniteMagnitude) titleViewCell?.item = item as? Title // Calculate Height let height = max(titleViewCell?.intrinsicContentSize.height ?? 0 , 0) // Return Size - return CGSize(width: context.containerSize.width, height: height) + return CGSize(width: width, height: height) case .message: diff --git a/Demo/Bulletin/Controllers/BulletinListView.swift b/Demo/Bulletin/Controllers/BulletinListVC.swift similarity index 55% rename from Demo/Bulletin/Controllers/BulletinListView.swift rename to Demo/Bulletin/Controllers/BulletinListVC.swift index 3131374..86c6fdd 100644 --- a/Demo/Bulletin/Controllers/BulletinListView.swift +++ b/Demo/Bulletin/Controllers/BulletinListVC.swift @@ -1,8 +1,8 @@ // -// BulletinListView.swift +// BulletinListVC.swift // Bulletin // -// Created by Daxesh Nagar on 19/07/23. +// Created by Daxesh Nagar on 31/07/23. // Copyright © 2023 Copyright © 2022 Zanmai Labs Private Limited. All rights reserved. // @@ -13,7 +13,7 @@ internal protocol BulletinListDelegate: AnyObject { func BulletinList(didClickItem item: BulletinItem) } -class BulletinListView: UIViewController { +class BulletinListVC: UIViewController { // MARK: - Variables @IBOutlet private var collectionView : UICollectionView! @@ -21,6 +21,8 @@ class BulletinListView: UIViewController { @IBOutlet private var headerView : UIStackView! @IBOutlet private var footerView : UIStackView! @IBOutlet private var gotItButton : UIButton! + @IBOutlet private weak var collectionViewHeightConstraint: NSLayoutConstraint? + public weak var delegate: BulletinListDelegate? private var bulletinSection = [BulletinInfo]() @@ -36,25 +38,20 @@ class BulletinListView: UIViewController { // MARK: - Initialisation Methods - public class func instance(items: [BulletinInfo]) -> BulletinListView { + public class func instance(items: [BulletinInfo]) -> BulletinListVC { - // Create Instance From XIB - let className = String(describing: BulletinListView.self) - let arrayOfViews = Bundle.main.loadNibNamed(className, owner: self, options: nil) - if let view = arrayOfViews?.first as? BulletinListView { - - // Set View Flexibility - view.autoresizingMask = [.flexibleWidth, .flexibleHeight] - view.translatesAutoresizingMaskIntoConstraints = true - view.bulletinSection = items + // Create Settings VC + let bulletinVC = BulletinListVC() + bulletinVC.bulletinSection = items + return bulletinVC - return view - } + // Create Navigation Controller +// let navController = WZBaseNavigationController(rootViewController: settingsVC) - // Manually Create Instance - let bulletinListView = BulletinListView() - bulletinListView.bulletinSection = items - return bulletinListView +// // Manually Create Instance +// let bulletinListView = BulletinListVC() +// bulletinListView.bulletinSection = items +// return bulletinListView } // MARK: - View Lifecycle Methods @@ -68,7 +65,7 @@ class BulletinListView: UIViewController { gotItButton.layer.cornerRadius = AppStyle.ButtonCornerRadius // Set Insets - collectionView.contentInset = UIEdgeInsets(top: 20, left: 16, bottom: 64, right: 16) + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) updateAppearance() @@ -98,19 +95,42 @@ class BulletinListView: UIViewController { } // MARK: - Initialisation Methods - override func awakeFromNib() { - super.awakeFromNib() - - // Add Corner Curve - if #available(iOS 13.0, *) { - layer.cornerCurve = .continuous - } + override func viewDidLoad() { + super.viewDidLoad() + + // Do any additional setup after loading the view. // Load Variables loadVariables() + } + + override func viewDidLayoutSubviews() { + super.viewDidLayoutSubviews() + + // ReLayout to Forces View And Its Subviews to update immediately + // view.layoutIfNeeded() + + // Calculate Scroll View Height + let minTopMargin: CGFloat = 200 + var collectionViewHeight = collectionView.contentSize.height + let extraViewHeight: CGFloat = headerView.frame.height + footerView.frame.height + var superViewHeight: CGFloat = 480 + if let window = UIApplication.shared.keyWindow { + superViewHeight = window.frame.size.height - window.safeAreaInsets.top - window.safeAreaInsets.bottom + } + if collectionViewHeight > (superViewHeight - minTopMargin - extraViewHeight) { + collectionViewHeight = superViewHeight - minTopMargin - extraViewHeight + collectionView.isScrollEnabled = true + } else { + collectionView.isScrollEnabled = false + } + // Update Height + collectionViewHeightConstraint?.constant = collectionViewHeight + preferredContentSize = CGSize(width: preferredContentSize.width, height: collectionViewHeight) } + // MARK: - Helper Method public func reload(animated: Bool, completion: ListUpdaterCompletion? = nil) { @@ -119,11 +139,29 @@ class BulletinListView: UIViewController { self?.adapter.performUpdates(animated: animated, completion: completion) } } + +// private func calculateTotalContentHeight() -> CGFloat { +// var totalHeight: CGFloat = 0 +// let collectionViewContentHeight = collectionView.contentSize.height +// +// +// let minTopMargin: CGFloat = 50 +// var viewHeight = collectionView.contentSize.height +// let extraViewHeight: CGFloat = 86 + 45 +// var superViewHeight: CGFloat = 480 +// // Optionally, add space for any additional header or footer views if present. +// totalHeight += additionalHeaderHeight + additionalFooterHeight +// +// // Optionally, add some extra space for padding, if needed. +// // totalHeight += paddingHeight +// +// return totalHeight +// } } // MARK: - ListAdapterDataSource -extension BulletinListView: ListAdapterDataSource { +extension BulletinListVC: ListAdapterDataSource { func objects(for listAdapter: ListAdapter) -> [ListDiffable] { return bulletinItems @@ -139,12 +177,12 @@ extension BulletinListView: ListAdapterDataSource { } // MARK: - UICollectionViewDelegate -extension BulletinListView: UICollectionViewDelegate { +extension BulletinListVC: UICollectionViewDelegate { public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {} } // MARK: - BulletinSectionControllerDelegate -extension BulletinListView: BulletinSectionControllerDelegate { +extension BulletinListVC: BulletinSectionControllerDelegate { func bulletinSectionController(_ sectionController: BulletinListSectionController, didClickItem item: BulletinItem) { // Clicked Item diff --git a/Demo/Bulletin/Controllers/BulletinListView.xib b/Demo/Bulletin/Controllers/BulletinListVC.xib similarity index 53% rename from Demo/Bulletin/Controllers/BulletinListView.xib rename to Demo/Bulletin/Controllers/BulletinListVC.xib index 5e4373a..52be523 100644 --- a/Demo/Bulletin/Controllers/BulletinListView.xib +++ b/Demo/Bulletin/Controllers/BulletinListVC.xib @@ -9,85 +9,92 @@ - + + + + + + + + + + + - - + + - - + + - - + - - + + - + + + + - + - - + + - - + + - - + - - + + - - - - - - - - - - + + + + + + + + + + - - - - - - - - - - + + + + diff --git a/Demo/Bulletin/ViewController.swift b/Demo/Bulletin/ViewController.swift index 1c8a4d2..2473b61 100644 --- a/Demo/Bulletin/ViewController.swift +++ b/Demo/Bulletin/ViewController.swift @@ -17,7 +17,7 @@ class ViewController: UIViewController { let dataSource = BulletinDataStore() // Register Multiple Versions - registerBulletingDetails(forVersion: Version("1.11"), inDataStore: dataSource) +// registerBulletingDetails(forVersion: Version("1.11"), inDataStore: dataSource) // registerBulletingDetails(forVersion: Version("1.12"), inDataStore: dataSource) // registerBulletingDetails(forVersion: Version("1.12.1"), inDataStore: dataSource) // registerBulletingDetails(forVersion: Version("1.12.2"), inDataStore: dataSource) @@ -27,19 +27,10 @@ class ViewController: UIViewController { let sdk = BulletinSDK(dataStore: dataSource,appearance: Appearance.whiteKnight) - let bulletinView = sdk.getFullBulletin() + let bulletinVC = sdk.getFullBulletin() // let _ = sdk.getLastBulletins(limit: 5) // let _ = sdk.getUnseenBulletins(limit: 4) - if let bulletinView = bulletinView { - bulletinView.frame = view.bounds - bulletinView.autoresizingMask = [.flexibleWidth,.flexibleHeight] - bulletinView.translatesAutoresizingMaskIntoConstraints = true - bulletinView.delegate = self - self.view.addSubview(bulletinView) - bulletinView.reload(animated: false) - } - // Get Top Most View Controller // BulletinHelper.topMostViewController { (topMostVC) in // @@ -48,11 +39,19 @@ class ViewController: UIViewController { // return // } // +// guard let bulletinVC = bulletinVC else { +// return +// } +// +// bulletinVC.delegate = self +// // // Display Self Declaration PopUp VC -// let segue = AlertSegue(identifier: nil, source: topMostVC, destination: bulletinView) +// let segue = AlertSegue(identifier: nil, source: topMostVC, destination: bulletinVC) // topMostVC.prepare(for: segue, sender: nil) // segue.perform() // +// bulletinVC.reload(animated: false) +// // } } @@ -101,6 +100,43 @@ class ViewController: UIViewController { // Register Item dataStore.registerVersionInfo(version: version, items: bulletItems2) } + + @IBAction func bulletinButtonTapped(_ sender: Any) { + + // Creage DataStore Object + let dataSource = BulletinDataStore() + + // Register Multiple Versions + registerBulletingDetails(forVersion: Version("1.11"), inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.12"), inDataStore: dataSource) + + let sdk = BulletinSDK(dataStore: dataSource,appearance: Appearance.whiteKnight) + let bulletinVC = sdk.getFullBulletin() + + // Get Top Most View Controller + BulletinHelper.topMostViewController { (topMostVC) in + + // Validation + guard let topMostVC = topMostVC else { + return + } + + guard let bulletinVC = bulletinVC else { + return + } + + bulletinVC.delegate = self + + // Display Self Declaration PopUp VC + let segue = AlertSegue(identifier: nil, source: topMostVC, destination: bulletinVC) + topMostVC.prepare(for: segue, sender: nil) + segue.perform() + + bulletinVC.reload(animated: false) + + } + + } } @@ -109,13 +145,6 @@ extension ViewController: BulletinListDelegate { func BulletinList(didClickItem item: BulletinItem) { - - // Set Message Type -// if let messasgeTypeString = attributes["messageType"] as? String, -// let messageType = MessageType(rawValue: messasgeTypeString) { -// self.messageType = messageType -// } -// // Validation if let actionButtonItem = item as? ActionButton, let actionButtonPayload = actionButtonItem.clickPayload { From bed514a2bef1b20ed0666fe757a6789ae3788e9d Mon Sep 17 00:00:00 2001 From: Daxesh Date: Wed, 2 Aug 2023 09:51:56 +0530 Subject: [PATCH 10/12] Bulletin minor fixes --- BulletinSDK/View/Message/MessageViewCell.xib | 14 ++-- Demo/Bulletin/Base.lproj/Main.storyboard | 18 +++-- .../Bulletin/Controllers/BulletinListVC.swift | 81 +++++++------------ Demo/Bulletin/Controllers/BulletinListVC.xib | 81 +++++++++---------- Demo/Bulletin/ViewController.swift | 23 +++--- 5 files changed, 99 insertions(+), 118 deletions(-) diff --git a/BulletinSDK/View/Message/MessageViewCell.xib b/BulletinSDK/View/Message/MessageViewCell.xib index 90cebc0..bb0c0b3 100644 --- a/BulletinSDK/View/Message/MessageViewCell.xib +++ b/BulletinSDK/View/Message/MessageViewCell.xib @@ -18,10 +18,10 @@ - + - + - - - - + + + + diff --git a/Demo/Bulletin/Base.lproj/Main.storyboard b/Demo/Bulletin/Base.lproj/Main.storyboard index 3b9197c..840da48 100644 --- a/Demo/Bulletin/Base.lproj/Main.storyboard +++ b/Demo/Bulletin/Base.lproj/Main.storyboard @@ -17,22 +17,24 @@ - + diff --git a/Demo/Bulletin/Controllers/BulletinListVC.swift b/Demo/Bulletin/Controllers/BulletinListVC.swift index 86c6fdd..e094622 100644 --- a/Demo/Bulletin/Controllers/BulletinListVC.swift +++ b/Demo/Bulletin/Controllers/BulletinListVC.swift @@ -18,7 +18,7 @@ class BulletinListVC: UIViewController { // MARK: - Variables @IBOutlet private var collectionView : UICollectionView! @IBOutlet private var headerTitleLabel : UILabel! - @IBOutlet private var headerView : UIStackView! + @IBOutlet private var headerView : UIView! @IBOutlet private var footerView : UIStackView! @IBOutlet private var gotItButton : UIButton! @IBOutlet private weak var collectionViewHeightConstraint: NSLayoutConstraint? @@ -40,18 +40,10 @@ class BulletinListVC: UIViewController { // MARK: - Initialisation Methods public class func instance(items: [BulletinInfo]) -> BulletinListVC { - // Create Settings VC + // Create Bulletin VC let bulletinVC = BulletinListVC() bulletinVC.bulletinSection = items return bulletinVC - - // Create Navigation Controller -// let navController = WZBaseNavigationController(rootViewController: settingsVC) - -// // Manually Create Instance -// let bulletinListView = BulletinListVC() -// bulletinListView.bulletinSection = items -// return bulletinListView } // MARK: - View Lifecycle Methods @@ -65,7 +57,7 @@ class BulletinListVC: UIViewController { gotItButton.layer.cornerRadius = AppStyle.ButtonCornerRadius // Set Insets - collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) + collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 20, right: 0) updateAppearance() @@ -106,28 +98,8 @@ class BulletinListVC: UIViewController { override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() - - // ReLayout to Forces View And Its Subviews to update immediately - // view.layoutIfNeeded() - - // Calculate Scroll View Height - let minTopMargin: CGFloat = 200 - var collectionViewHeight = collectionView.contentSize.height - let extraViewHeight: CGFloat = headerView.frame.height + footerView.frame.height - var superViewHeight: CGFloat = 480 - if let window = UIApplication.shared.keyWindow { - superViewHeight = window.frame.size.height - window.safeAreaInsets.top - window.safeAreaInsets.bottom - } - if collectionViewHeight > (superViewHeight - minTopMargin - extraViewHeight) { - collectionViewHeight = superViewHeight - minTopMargin - extraViewHeight - collectionView.isScrollEnabled = true - } else { - collectionView.isScrollEnabled = false - } - // Update Height - collectionViewHeightConstraint?.constant = collectionViewHeight - preferredContentSize = CGSize(width: preferredContentSize.width, height: collectionViewHeight) + collectionView.collectionViewLayout.invalidateLayout() } @@ -135,29 +107,36 @@ class BulletinListVC: UIViewController { public func reload(animated: Bool, completion: ListUpdaterCompletion? = nil) { DispatchQueue.main.async { [weak self] in + guard let weakSelf = self else { return } + // Perform Updates - self?.adapter.performUpdates(animated: animated, completion: completion) + weakSelf.adapter.performUpdates(animated: animated, completion: {(finished) in + + // ReLayout to Forces View And Its Subviews to update immediately + weakSelf.view.layoutIfNeeded() + var superViewHeight: CGFloat = 480 + let minTopMargin: CGFloat = 50 + var collectionViewHeight = weakSelf.collectionView.contentSize.height + let extraViewHeight: CGFloat = weakSelf.headerView.frame.height + weakSelf.footerView.frame.height + if let window = UIApplication.shared.keyWindow { + superViewHeight = window.frame.size.height - window.safeAreaInsets.top - window.safeAreaInsets.bottom + } + if collectionViewHeight > (superViewHeight - minTopMargin - extraViewHeight) { + collectionViewHeight = superViewHeight - minTopMargin - extraViewHeight + weakSelf.collectionView.isScrollEnabled = true + } else { + weakSelf.collectionView.isScrollEnabled = false + } + + let contentHeight = min(collectionViewHeight, superViewHeight) + weakSelf.collectionViewHeightConstraint?.constant = contentHeight + weakSelf.view.setNeedsLayout() + weakSelf.preferredContentSize = CGSize(width: weakSelf.preferredContentSize.width, height: contentHeight) + completion?(finished) + }) } } -// private func calculateTotalContentHeight() -> CGFloat { -// var totalHeight: CGFloat = 0 -// let collectionViewContentHeight = collectionView.contentSize.height -// -// -// let minTopMargin: CGFloat = 50 -// var viewHeight = collectionView.contentSize.height -// let extraViewHeight: CGFloat = 86 + 45 -// var superViewHeight: CGFloat = 480 -// // Optionally, add space for any additional header or footer views if present. -// totalHeight += additionalHeaderHeight + additionalFooterHeight -// -// // Optionally, add some extra space for padding, if needed. -// // totalHeight += paddingHeight -// -// return totalHeight -// } - } // MARK: - ListAdapterDataSource diff --git a/Demo/Bulletin/Controllers/BulletinListVC.xib b/Demo/Bulletin/Controllers/BulletinListVC.xib index 52be523..85e25ad 100644 --- a/Demo/Bulletin/Controllers/BulletinListVC.xib +++ b/Demo/Bulletin/Controllers/BulletinListVC.xib @@ -4,7 +4,6 @@ - @@ -16,7 +15,7 @@ - + @@ -25,36 +24,43 @@ - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - + - - - - - - - - - - - + + + + diff --git a/Demo/Bulletin/ViewController.swift b/Demo/Bulletin/ViewController.swift index 2473b61..d91af5c 100644 --- a/Demo/Bulletin/ViewController.swift +++ b/Demo/Bulletin/ViewController.swift @@ -6,6 +6,7 @@ // import UIKit +import SwiftMessages class ViewController: UIViewController { @@ -42,7 +43,7 @@ class ViewController: UIViewController { // guard let bulletinVC = bulletinVC else { // return // } -// +// // bulletinVC.delegate = self // // // Display Self Declaration PopUp VC @@ -108,7 +109,7 @@ class ViewController: UIViewController { // Register Multiple Versions registerBulletingDetails(forVersion: Version("1.11"), inDataStore: dataSource) - registerBulletingDetails(forVersion: Version("1.12"), inDataStore: dataSource) + // registerBulletingDetails(forVersion: Version("1.12"), inDataStore: dataSource) let sdk = BulletinSDK(dataStore: dataSource,appearance: Appearance.whiteKnight) let bulletinVC = sdk.getFullBulletin() @@ -126,18 +127,18 @@ class ViewController: UIViewController { } bulletinVC.delegate = self - - // Display Self Declaration PopUp VC - let segue = AlertSegue(identifier: nil, source: topMostVC, destination: bulletinVC) - topMostVC.prepare(for: segue, sender: nil) - segue.perform() - - bulletinVC.reload(animated: false) - + + bulletinVC.loadViewIfNeeded() + bulletinVC.reload(animated: false,completion: {_ in + // Display Bulletin PopUp VC + let segue = AlertSegue(identifier: nil, source: topMostVC, destination: bulletinVC) + topMostVC.prepare(for: segue, sender: nil) + segue.perform() + }) } } - + } //// MARK: - WZSettingsSectionControllerDelegate From 50d39e449d66d1eb214f3bae54ffe86840b4c4e3 Mon Sep 17 00:00:00 2001 From: Daxesh Date: Wed, 2 Aug 2023 10:18:16 +0530 Subject: [PATCH 11/12] Removed unwanted code --- .../Classes/Constants/Appearance.swift | 23 ------ BulletinSDK/Classes/Constants/Constants.swift | 77 ------------------- BulletinSDK/Classes/Constants/Text.swift | 14 ---- .../BulletPoint/BulletPointViewCell.swift | 13 ++-- BulletinSDK/View/Media/MediaViewCell.swift | 8 -- .../BulletinListSectionController.swift | 35 +++------ 6 files changed, 19 insertions(+), 151 deletions(-) diff --git a/BulletinSDK/Classes/Constants/Appearance.swift b/BulletinSDK/Classes/Constants/Appearance.swift index 6cff720..95c70a0 100644 --- a/BulletinSDK/Classes/Constants/Appearance.swift +++ b/BulletinSDK/Classes/Constants/Appearance.swift @@ -19,13 +19,7 @@ public enum Appearance: String { // MARK: Constants public static func DefaultAnimationDuration() -> TimeInterval { return 0.3 } -// public static func Current() -> Appearance { -// return UserDefaults.lastSelectedAppearance() ?? .darkKnight -// } - - - // MARK: Conveniance Method To Apply Any Appearance public func apply(shouldBroadcastUpdate: Bool) { @@ -36,24 +30,7 @@ public enum Appearance: String { case .whiteKnight: applyDefaultAppearance() } - - -// // Update UISwitch Appearance -// UISwitch.appearance().onTintColor = AppStyle.Color.SwitchOn -// -// // Update UITextField Appearance -// UITextField.appearance().keyboardAppearance = AppStyle.KeyboardAppearance -// UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).defaultTextAttributes = [NSAttributedString.Key.foregroundColor: AppStyle.Color.PrimaryText] -// UITextField.appearance(whenContainedInInstancesOf: [UISearchBar.self]).tintColor = AppStyle.Color.SelectedElement -// -// // Update UIRefreshControl Appearance -// UIRefreshControl.appearance().tintColor = AppStyle.Color.Loader -// -// // Update UIToolBar Appearance -// UIToolbar.appearance().tintColor = AppStyle.Color.SelectedElement -// UIToolbar.appearance().barTintColor = AppStyle.Color.Background - // Broadcast Appearance Update Notification if shouldBroadcastUpdate == true { NotificationCenter.default.post(name: .AppearanceDidUpdate, object: nil, userInfo: nil) diff --git a/BulletinSDK/Classes/Constants/Constants.swift b/BulletinSDK/Classes/Constants/Constants.swift index 841c5df..74b2272 100644 --- a/BulletinSDK/Classes/Constants/Constants.swift +++ b/BulletinSDK/Classes/Constants/Constants.swift @@ -8,84 +8,7 @@ import UIKit - - -// MARK: - -public struct App { - - // App Lock State - public enum LockState: Int { - case locked = 0 - case unlocked - } - - // MARK: Variables - public static let Platform = "iOS" - public static let Name = "WazirX" - public static let DevLanguage = "Swift" - public static let TeamID = "CAQ757V6Q3" - public static let BundleID = Bundle.main.bundleIdentifier! - public static let Version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as! String - public static let BuildNumber = Bundle.main.infoDictionary?[kCFBundleVersionKey as String] as! String - public static func URLScheme() -> String { return "wazirx://"} - - - - // Redirects User To App Permissions In Device Settings - public static func OpenAppPermissionsInDeviceSettings(completion: ((Bool) -> Swift.Void)?) { - if let url = URL(string: UIApplication.openSettingsURLString), - UIApplication.shared.canOpenURL(url) - { - UIApplication.shared.open(url, options: Dictionary(), completionHandler: completion) - } - } - -} - - // MARK: - extension NSNotification.Name { public static let AppearanceDidUpdate = NSNotification.Name("AppearanceDidUpdate") - public static let UserDidLogin = NSNotification.Name("UserDidLogin") -} - - -// MARK: - -public struct HappinessPoints { - public static func Threshold() -> Int { return 20 } - public static func BuyOrder() -> Int { return 7 } - public static func SellOrder() -> Int { return 7 } - public static func ShareWithFriends() -> Int { return 5 } - public static func Error() -> Int { return 2 } -} - - -// MARK: - -public struct AppRating { - public static func DaysIntervalBetweenPointsReset() -> Int { return 14 } - public static func DaysIntervalBetweenTwoAppRatePopup() -> Int { return 40 } -} - - - -// MARK: - -public struct S3ImageName { - - // Create S3 Images Constants - public static func qbs_texture() -> String { return "qbs_texture" } - public static func no_token() -> String { return "no_token" } - public static func qbs_failure() -> String { return "qbs_failure" } - public static func qbs_success() -> String { return "qbs_success" } - public static func qbs_warning() -> String { return "qbs_warning" } - public static func sand_clock() -> String { return "sand_clock" } - -} - - -// MARK: - Regex -public struct Regex { - - public static func email() -> String { - return "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@" + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+" - } } diff --git a/BulletinSDK/Classes/Constants/Text.swift b/BulletinSDK/Classes/Constants/Text.swift index 4a16b6d..16d4441 100644 --- a/BulletinSDK/Classes/Constants/Text.swift +++ b/BulletinSDK/Classes/Constants/Text.swift @@ -14,18 +14,4 @@ public struct Text { public static func GotIt() -> String { return "OKay, Go it".localized() } public static func WhatsNewInThisUpdate() -> String { return "What’s new in this update?".localized() } - public static func WazirX() -> String { return "WazirX".localized() } - public static func Binance() -> String { return "Binance".localized() } - public static func Close() -> String { return "Close".localized() } - public static func Cancel() -> String { return "Cancel".localized() } - public static func Cancelled() -> String { return "Cancelled".localized() } - public static func CancelAll() -> String { return "Cancel all".localized() } - public static func Back() -> String { return "Back".localized() } - public static func Ok() -> String { return "Ok".localized() } - public static func Or() -> String { return "Or".localized() } - public static func PopularCoins() -> String { return "Popular coins".localized() } - public static func Buy() -> String { return "Buy".localized() } - public static func AverageBuyPrice() -> String { return "Average buy price".localized() } - public static func ProfitAndLoss() -> String { return "Profit & loss".localized() } - public static func ProfitAndLossProcessing() -> String { return "Updating P&L.. (processing recent transactions)".localized() } } diff --git a/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift index c69042f..728b1b7 100644 --- a/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift +++ b/BulletinSDK/View/BulletPoint/BulletPointViewCell.swift @@ -52,13 +52,15 @@ class BulletPointViewCell: BaseCollectionViewCell { // Set Title Label bulletDesc.base_regular() bulletDesc.textColor = AppStyle.Color.MainTextSecondary - - // bulletName.heading4_semibold() } //MARK: - Helper Methods private func updateUI() { + // Reset To Nil + bulletImageView.kf.cancelDownloadTask() + bulletImageView.image = nil + // Validation guard let bulletPointItem = item else { @@ -89,10 +91,11 @@ class BulletPointViewCell: BaseCollectionViewCell { case .success(let value): // Image downloaded - if let sourceUrl = value.source.url, + if let weakSelf = self, + let sourceUrl = value.source.url, sourceUrl == iconUrl { - self?.bulletImageView.isHidden = true - self?.bulletImageView.image = value.image + weakSelf.bulletImageView.image = value.image + weakSelf.bulletImageView.isHidden = false } case .failure(let error): print("\(error.localizedDescription)") diff --git a/BulletinSDK/View/Media/MediaViewCell.swift b/BulletinSDK/View/Media/MediaViewCell.swift index 24235a3..e191758 100644 --- a/BulletinSDK/View/Media/MediaViewCell.swift +++ b/BulletinSDK/View/Media/MediaViewCell.swift @@ -86,14 +86,6 @@ class MediaViewCell: BaseCollectionViewCell { mediaImageView.isHidden = true } -// // Setting Frame Of Inside Container SubView Of Cell -// if let itemSize = mediaItem.size { -// let height : CGFloat = (frame.width * itemSize.height) / itemSize.width -// imageViewHeightConstraint.constant = height -// } else { -// imageViewHeightConstraint.constant = 135 -// } - // Layout Cell layoutIfNeeded() diff --git a/Demo/Bulletin/Controllers/BulletinListSectionController.swift b/Demo/Bulletin/Controllers/BulletinListSectionController.swift index 22c47c1..92d43c4 100644 --- a/Demo/Bulletin/Controllers/BulletinListSectionController.swift +++ b/Demo/Bulletin/Controllers/BulletinListSectionController.swift @@ -15,7 +15,7 @@ internal protocol BulletinSectionControllerDelegate: AnyObject { } class BulletinListSectionController: ListSectionController { - + // MARK: - Variable private var bulletinListSection: BulletinInfo? public weak var delegate: BulletinSectionControllerDelegate? @@ -97,16 +97,6 @@ class BulletinListSectionController: ListSectionController { return size -// // Set Cell Item -// mediaViewCell?.frame = CGRect(x: 0.0, y: 0.0, width: context.containerSize.width, height: CGFloat.greatestFiniteMagnitude) -// mediaViewCell?.item = item as? Media -// -// // Calculate Height -// let height = max(mediaViewCell?.intrinsicContentSize.height ?? 0 , 0) - - // Return Size -// return CGSize(width: context.containerSize.width , height: height) - case .bulletPoint: // Set Cell Item @@ -156,7 +146,7 @@ class BulletinListSectionController: ListSectionController { // Item Validation if let item = item as? Title { - + // Set Item cell.item = item } @@ -176,14 +166,11 @@ class BulletinListSectionController: ListSectionController { // Item Validation if let item = item as? Message { - + // Set Item cell.item = item } - // Set Delegate - // cell.delegate = self - return cell case .media: @@ -195,7 +182,7 @@ class BulletinListSectionController: ListSectionController { // Item Validation if let item = item as? Media { - + // Set Item cell.item = item } @@ -214,7 +201,7 @@ class BulletinListSectionController: ListSectionController { // Item Validation if let item = item as? BulletPoint { - + // Set Item cell.item = item } @@ -233,7 +220,7 @@ class BulletinListSectionController: ListSectionController { // Item Validation if let item = item as? ActionButton { - + // Set Item cell.item = item } @@ -257,7 +244,7 @@ class BulletinListSectionController: ListSectionController { // MARK: - BulletListTitleCellDelegate extension BulletinListSectionController: TitleViewCellDelegate { - + func titleViewCell(_ cell: TitleViewCell, ofItem item: BulletinItem) { } @@ -266,7 +253,7 @@ extension BulletinListSectionController: TitleViewCellDelegate { // MARK: - BulletListTitleCellDelegate extension BulletinListSectionController: MediaViewCellDelegate { - + func mediaViewCell(_ cell: MediaViewCell, ofItem item: BulletinItem) { } @@ -276,7 +263,7 @@ extension BulletinListSectionController: MediaViewCellDelegate { // MARK: - BulletListTitleCellDelegate extension BulletinListSectionController: MessageViewCellDelegate { - + func messageViewCell(_ cell: MessageViewCell, ofItem item: BulletinItem) { } @@ -285,7 +272,7 @@ extension BulletinListSectionController: MessageViewCellDelegate { // MARK: - BulletListTitleCellDelegate extension BulletinListSectionController: ActionButtonViewCellDelegate { - + func actionButtonViewCell(_ cell: ActionButtonViewCell, ofItem item: BulletinItem) { delegate?.bulletinSectionController(self, didClickItem: item) } @@ -294,7 +281,7 @@ extension BulletinListSectionController: ActionButtonViewCellDelegate { // MARK: - BulletListTitleCellDelegate extension BulletinListSectionController: BulletPointViewCellDelegate { - + func bulletPointViewCell(_ cell: BulletPointViewCell, ofItem item: BulletinItem) { delegate?.bulletinSectionController(self, didClickItem: item) } From 986fbe579c2390f5f580fcb15b8b7fa7036e746b Mon Sep 17 00:00:00 2001 From: Daxesh Date: Wed, 2 Aug 2023 17:05:21 +0530 Subject: [PATCH 12/12] Bulletin Item --- BulletinSDK/BulletinSDK.swift | 2 +- BulletinSDK/View/Message/MessageViewCell.swift | 2 +- Demo/Bulletin/Controllers/BulletinListVC.swift | 2 +- Demo/Bulletin/ViewController.swift | 12 ++++++------ 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/BulletinSDK/BulletinSDK.swift b/BulletinSDK/BulletinSDK.swift index 71de24e..c4042bd 100644 --- a/BulletinSDK/BulletinSDK.swift +++ b/BulletinSDK/BulletinSDK.swift @@ -26,7 +26,7 @@ class BulletinSDK { // MARK: - Helper Methods public func getFullBulletin() -> BulletinListVC? { - // Get Bulletin Items + // Get Bulletin Items let items = dataStore.getData(fromNewVersion: nil, toOldVersion: nil) // Show Bulletin diff --git a/BulletinSDK/View/Message/MessageViewCell.swift b/BulletinSDK/View/Message/MessageViewCell.swift index 68208cf..a78c9eb 100644 --- a/BulletinSDK/View/Message/MessageViewCell.swift +++ b/BulletinSDK/View/Message/MessageViewCell.swift @@ -67,7 +67,7 @@ class MessageViewCell: BaseCollectionViewCell { // Set Html Description Message if let descMessage = messageItem.text, descMessage.isEmpty == false, - let htmlAttributedString = descMessage.html2AttributedString(usingFont: nil, color: AppStyle.Color.SecondaryText)?.trailingNewlineChopped + let htmlAttributedString = descMessage.html2AttributedString(usingFont: AppStyle.Font.Medium(size: 12), color: AppStyle.Color.MainTextPrimary)?.trailingNewlineChopped { descMsgLabel.attributedText = htmlAttributedString descMsgLabel.isHidden = false diff --git a/Demo/Bulletin/Controllers/BulletinListVC.swift b/Demo/Bulletin/Controllers/BulletinListVC.swift index e094622..e28e0b2 100644 --- a/Demo/Bulletin/Controllers/BulletinListVC.swift +++ b/Demo/Bulletin/Controllers/BulletinListVC.swift @@ -131,7 +131,7 @@ class BulletinListVC: UIViewController { let contentHeight = min(collectionViewHeight, superViewHeight) weakSelf.collectionViewHeightConstraint?.constant = contentHeight weakSelf.view.setNeedsLayout() - weakSelf.preferredContentSize = CGSize(width: weakSelf.preferredContentSize.width, height: contentHeight) + weakSelf.preferredContentSize = CGSize(width: weakSelf.preferredContentSize.width, height: superViewHeight - 40) completion?(finished) }) } diff --git a/Demo/Bulletin/ViewController.swift b/Demo/Bulletin/ViewController.swift index d91af5c..98bb3de 100644 --- a/Demo/Bulletin/ViewController.swift +++ b/Demo/Bulletin/ViewController.swift @@ -27,7 +27,7 @@ class ViewController: UIViewController { // registerBulletingDetails(forVersion: Version("1.14"), inDataStore: dataSource) - let sdk = BulletinSDK(dataStore: dataSource,appearance: Appearance.whiteKnight) + let sdk = BulletinSDK(dataStore: dataSource,appearance: Appearance.darkKnight) let bulletinVC = sdk.getFullBulletin() // let _ = sdk.getLastBulletins(limit: 5) // let _ = sdk.getUnseenBulletins(limit: 4) @@ -69,10 +69,10 @@ class ViewController: UIViewController { title2.subTitleText = "Vestibulum id ligula porta felis euismod semper. Vesti bu lum id ligula porta felis euismod semper." let message2 = Message() - message2.messageType = .html - message2.text = "

Harry Potter's House

Privet Drive,4
Little Whinging
Surrey
England
Great Britain

" + message2.messageType = .text + // message2.text = "

Harry Potter's House

Privet Drive,4
Little Whinging
Surrey
England
Great Britain

" //"
\n" + "

Harry Potter's House

\n" + "

\n" + "Privet Drive, 4
Little Whinging
Surrey
England
Great Britain\n" + "

\n" + "
" - // message2.text = "Vestibulum id ligula porta felis euismod semper. Morbi leo risus, porta ac consectetur ac, vestibulum at eros." + message2.text = "Vestibulum id ligula porta felis euismod semper. Morbi leo risus, porta ac consectetur ac, vestibulum at eros." let media = Media() media.size = CGSize(width: 1180, height: 596) @@ -109,9 +109,9 @@ class ViewController: UIViewController { // Register Multiple Versions registerBulletingDetails(forVersion: Version("1.11"), inDataStore: dataSource) - // registerBulletingDetails(forVersion: Version("1.12"), inDataStore: dataSource) + registerBulletingDetails(forVersion: Version("1.12"), inDataStore: dataSource) - let sdk = BulletinSDK(dataStore: dataSource,appearance: Appearance.whiteKnight) + let sdk = BulletinSDK(dataStore: dataSource,appearance: Appearance.darkKnight) let bulletinVC = sdk.getFullBulletin() // Get Top Most View Controller

daTBtl~@mC(Nx+V?^G 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 Q*dYhwGq(Ls8MpKQ0qZt9BLDyZ literal 0 HcmV?d00001 diff --git a/BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-SemiBold.ttf b/BulletinSDK/Resources/Fonts/IBMPlexSans/IBMPlexSans-SemiBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0dd734719d5b2b9fba84fab2fad5d2b1e7029517 GIT binary patch literal 194760 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